-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(timed-events): add cron based timed events (#21)
* feat: add database-stored cronjobs * feat: add events to change handlers at fixed times * chore: move old fixed cronjobs to seeder * feat: add skipping next occurrence of a timed event * fix: register timed events on system start * chore: clean code
- Loading branch information
Showing
14 changed files
with
468 additions
and
33 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import cron, { ScheduledTask } from 'node-cron'; | ||
import HandlerService from '../root/handler-service'; | ||
import logger from '../../logger'; | ||
import { TimedEvent } from './entities'; | ||
import { AuditService } from '../audit'; | ||
import HandlerManager from '../root/handler-manager'; | ||
import RootAudioService from '../root/root-audio-service'; | ||
import RootLightsService from '../root/root-lights-service'; | ||
import RootScreenService from '../root/root-screen-service'; | ||
|
||
export class CronExpressionError extends Error {} | ||
|
||
export default class CronManager { | ||
private cronTasks: Map<number, ScheduledTask>; | ||
|
||
constructor(private eventIsSkipped: (event: TimedEvent) => Promise<void>) { | ||
this.cronTasks = new Map(); | ||
} | ||
|
||
/** | ||
* Schedule an event | ||
* @param timedEvent | ||
*/ | ||
registerEvent(timedEvent: TimedEvent) { | ||
if (!cron.validate(timedEvent.cronExpression)) { | ||
throw new CronExpressionError('Invalid cron expression'); | ||
} | ||
|
||
// Remove the old event first if it already exists | ||
this.removeEvent(timedEvent); | ||
|
||
const task = cron.schedule(timedEvent.cronExpression, this.handleEvent.bind(this, timedEvent)); | ||
this.cronTasks.set(timedEvent.id, task); | ||
} | ||
|
||
/** | ||
* Unregister an event, such that it will not be scheduled anymore | ||
* @param timedEvent | ||
*/ | ||
removeEvent(timedEvent: TimedEvent) { | ||
if (this.cronTasks.has(timedEvent.id)) { | ||
const task = this.cronTasks.get(timedEvent.id)!; | ||
task.stop(); | ||
this.cronTasks.delete(timedEvent.id); | ||
} | ||
} | ||
|
||
/** | ||
* Callbacks when an event should be executed | ||
* @param event | ||
*/ | ||
async handleEvent(event: TimedEvent): Promise<void> { | ||
const spec = event.eventSpec; | ||
try { | ||
if (event.skipNext) { | ||
logger.info(`Timed event "${event.id}": skip.`); | ||
event.skipNext = false; | ||
await this.eventIsSkipped(event); | ||
|
||
return; | ||
} | ||
|
||
switch (spec.type) { | ||
case 'system-reset': | ||
logger.audit( | ||
{ id: 'cron', name: 'Scheduled Task', roles: [] }, | ||
'Reset system state to default.', | ||
); | ||
await new HandlerService().resetToDefaults(); | ||
break; | ||
|
||
case 'clean-audit-logs': | ||
logger.audit( | ||
{ id: 'cron', name: 'Scheduled Task', roles: [] }, | ||
`Remove audit logs older than ${process.env.AUDIT_LOGS_MAX_AGE_DAYS} days.`, | ||
); | ||
await new AuditService().removeExpiredAuditLogs(); | ||
break; | ||
|
||
case 'switch-handler-audio': | ||
const audio = await new RootAudioService().getSingleAudio(spec.params.id); | ||
if (audio == null) { | ||
logger.error(`Timed event "${event.id}": audio with ID "${spec.params.id}" not found.`); | ||
return; | ||
} | ||
|
||
const foundAudio = HandlerManager.getInstance().registerHandler( | ||
audio, | ||
spec.params.handler, | ||
); | ||
if (!foundAudio) { | ||
logger.error( | ||
`Timed event "${event.id}": audio handler with name "${spec.params.handler}" not found.`, | ||
); | ||
} | ||
|
||
logger.audit( | ||
{ id: 'cron', name: 'Scheduled Task', roles: [] }, | ||
`Change "${audio.name}" (id: ${audio.id}) audio handler to "${spec.params.handler}".`, | ||
); | ||
|
||
return; | ||
|
||
case 'switch-handler-lights': | ||
const lights = await new RootLightsService().getSingleLightsGroup(spec.params.id); | ||
if (lights == null) { | ||
logger.error( | ||
`Timed event "${event.id}": lightsGroup with ID "${spec.params.id}" not found.`, | ||
); | ||
return; | ||
} | ||
|
||
const foundLights = HandlerManager.getInstance().registerHandler( | ||
lights, | ||
spec.params.handler, | ||
); | ||
if (!foundLights) { | ||
logger.error( | ||
`Timed event "${event.id}": lightsGroup handler with name "${spec.params.handler}" not found.`, | ||
); | ||
} | ||
|
||
logger.audit( | ||
{ id: 'cron', name: 'Scheduled Task', roles: [] }, | ||
`Change "${lights.name}" (id: ${lights.id}) lights handler to "${spec.params.handler}".`, | ||
); | ||
|
||
return; | ||
|
||
case 'switch-handler-screen': | ||
const screen = await new RootScreenService().getSingleScreen(spec.params.id); | ||
if (screen == null) { | ||
logger.error( | ||
`Timed event "${event.id}": screen with ID "${spec.params.id}" not found.`, | ||
); | ||
return; | ||
} | ||
|
||
const foundScreen = HandlerManager.getInstance().registerHandler( | ||
screen, | ||
spec.params.handler, | ||
); | ||
if (!foundScreen) { | ||
logger.error( | ||
`Timed event "${event.id}": screen handler with name "${spec.params.handler}" not found.`, | ||
); | ||
} | ||
|
||
logger.audit( | ||
{ id: 'cron', name: 'Scheduled Task', roles: [] }, | ||
`Change "${screen.name}" (id: ${screen.id}) screen handler to "${spec.params.handler}".`, | ||
); | ||
|
||
return; | ||
|
||
default: | ||
logger.error(`Timed event "${event.id}": unknown type "${(spec as any).type}".`); | ||
} | ||
} catch (error: unknown) { | ||
logger.error(`Timed event "${event.id}": ${error}`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import TimedEvent from './timed-event'; | ||
|
||
export { default as TimedEvent } from './timed-event'; | ||
|
||
export const Entities = [TimedEvent]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import BaseEntity from '../../root/entities/base-entity'; | ||
import { Column, Entity } from 'typeorm'; | ||
import EventSpec from '../event-spec'; | ||
|
||
@Entity() | ||
export default class TimedEvent extends BaseEntity { | ||
@Column() | ||
public cronExpression: string; | ||
|
||
@Column({ | ||
type: 'varchar', | ||
transformer: { | ||
from(value: string): EventSpec { | ||
return JSON.parse(value); | ||
}, | ||
to(value: EventSpec): string { | ||
return JSON.stringify(value); | ||
}, | ||
}, | ||
}) | ||
public eventSpec: EventSpec; | ||
|
||
/** | ||
* Whether the next time this should fire, it should be skipped instead | ||
*/ | ||
@Column({ default: false }) | ||
public skipNext: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import TimedEventReset from './timed-event-reset'; | ||
import TimedEventCleanAuditLogs from './timed-event-clean-audit-logs'; | ||
import { | ||
TimedEventSwitchHandlerAudio, | ||
TimedEventSwitchHandlerLights, | ||
TimedEventSwitchHandlerScreen, | ||
} from './timed-event-switch-handler'; | ||
|
||
type EventSpec = | ||
| TimedEventReset | ||
| TimedEventCleanAuditLogs | ||
| TimedEventSwitchHandlerAudio | ||
| TimedEventSwitchHandlerLights | ||
| TimedEventSwitchHandlerScreen; | ||
|
||
export default EventSpec; |
5 changes: 5 additions & 0 deletions
5
src/modules/timed-events/event-spec/timed-event-clean-audit-logs.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
type TimedEventCleanAuditLogs = { | ||
type: 'clean-audit-logs'; | ||
}; | ||
|
||
export default TimedEventCleanAuditLogs; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
type TimedEventReset = { | ||
type: 'system-reset'; | ||
}; | ||
|
||
export default TimedEventReset; |
19 changes: 19 additions & 0 deletions
19
src/modules/timed-events/event-spec/timed-event-switch-handler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
interface SwitchHandlerParams { | ||
id: number; | ||
handler: string; | ||
} | ||
|
||
export type TimedEventSwitchHandlerAudio = { | ||
type: 'switch-handler-audio'; | ||
params: SwitchHandlerParams; | ||
}; | ||
|
||
export type TimedEventSwitchHandlerLights = { | ||
type: 'switch-handler-lights'; | ||
params: SwitchHandlerParams; | ||
}; | ||
|
||
export type TimedEventSwitchHandlerScreen = { | ||
type: 'switch-handler-screen'; | ||
params: SwitchHandlerParams; | ||
}; |
Oops, something went wrong.