From 509a398639616f2ae6b666f1281b2645b1a16ecc Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Thu, 26 Dec 2019 23:30:46 +0100 Subject: [PATCH] implementing after trigger --- src/backend/middlewares/admin/SettingsMWs.ts | 2 +- src/backend/model/jobs/JobManager.ts | 71 +++++----------- src/backend/model/jobs/jobs/FileJob.ts | 4 +- src/backend/model/jobs/jobs/IJob.ts | 2 +- src/backend/model/jobs/jobs/Job.ts | 12 +-- src/backend/routes/admin/SettingsRouter.ts | 4 +- .../private/PrivateConfigDefaultsClass.ts | 6 +- src/common/entities/job/JobScheduleDTO.ts | 60 +++++++++++++- .../app/ui/admin/admin.component.html | 82 +++++++++--------- .../_abstract/abstract.settings.component.ts | 4 +- .../jobs/jobs.settings.component.html | 83 +++++++++++++++---- .../settings/jobs/jobs.settings.component.ts | 62 ++++++++++++-- 12 files changed, 258 insertions(+), 134 deletions(-) diff --git a/src/backend/middlewares/admin/SettingsMWs.ts b/src/backend/middlewares/admin/SettingsMWs.ts index fa1ce2ba..b375c490 100644 --- a/src/backend/middlewares/admin/SettingsMWs.ts +++ b/src/backend/middlewares/admin/SettingsMWs.ts @@ -436,7 +436,7 @@ export class SettingsMWs { } } - public static async updateTasksSettings(req: Request, res: Response, next: NextFunction) { + public static async updateJobSettings(req: Request, res: Response, next: NextFunction) { if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) { return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed')); } diff --git a/src/backend/model/jobs/JobManager.ts b/src/backend/model/jobs/JobManager.ts index 172d379f..3e45898c 100644 --- a/src/backend/model/jobs/JobManager.ts +++ b/src/backend/model/jobs/JobManager.ts @@ -3,8 +3,9 @@ import {JobProgressDTO} from '../../../common/entities/settings/JobProgressDTO'; import {IJob} from './jobs/IJob'; import {JobRepository} from './JobRepository'; import {Config} from '../../../common/config/private/Config'; -import {JobScheduleDTO, JobTriggerType} from '../../../common/entities/job/JobScheduleDTO'; +import {AfterJobTrigger, JobScheduleDTO, JobTriggerType} from '../../../common/entities/job/JobScheduleDTO'; import {Logger} from '../../Logger'; +import {NotificationManager} from '../NotifocationManager'; declare var global: NodeJS.Global; @@ -32,7 +33,9 @@ export class JobManager implements IJobManager { async run(jobName: string, config: T): Promise { const t = this.findJob(jobName); if (t) { - await t.start(config); + await t.start(config, () => { + this.onJobFinished(t); + }); } else { Logger.warn(LOG_TAG, 'cannot find job to start:' + jobName); } @@ -50,6 +53,22 @@ export class JobManager implements IJobManager { } } + async onJobFinished(job: IJob): Promise { + const sch = Config.Server.Jobs.scheduled.find(s => s.jobName === job.Name); + if (sch) { + console.log('found parent'); + const children = Config.Server.Jobs.scheduled.filter(s => s.trigger.type === JobTriggerType.after && + (s.trigger).afterScheduleName === sch.name); + for (let i = 0; i < children.length; ++i) { + try { + await this.run(children[i].jobName, children[i].config); + } catch (e) { + NotificationManager.warning('Job running error:' + children[i].name, e.toString()); + } + } + } + } + getAvailableJobs(): IJob[] { return JobRepository.Instance.getAvailableJobs(); } @@ -65,58 +84,12 @@ export class JobManager implements IJobManager { Config.Server.Jobs.scheduled.forEach(s => this.runSchedule(s)); } - protected getNextDayOfTheWeek(refDate: Date, dayOfWeek: number) { - const date = new Date(refDate); - date.setDate(refDate.getDate() + (dayOfWeek + 1 + 7 - refDate.getDay()) % 7); - if (date.getDay() === refDate.getDay()) { - return new Date(refDate); - } - date.setHours(0, 0, 0, 0); - return date; - } - - protected nextValidDate(date: Date, h: number, m: number, dayDiff: number): Date { - - date.setSeconds(0); - if (date.getHours() < h || (date.getHours() === h && date.getMinutes() < m)) { - date.setHours(h); - date.setMinutes(m); - } else { - date.setTime(date.getTime() + dayDiff); - date.setHours(h); - date.setMinutes(m); - } - return date; - } - - protected getDateFromSchedule(refDate: Date, schedule: JobScheduleDTO): Date { - switch (schedule.trigger.type) { - case JobTriggerType.scheduled: - return new Date(schedule.trigger.time); - - case JobTriggerType.periodic: - - - const hour = Math.floor(schedule.trigger.atTime / 1000 / (60 * 60)); - const minute = (schedule.trigger.atTime / 1000 / 60) % 60; - - if (schedule.trigger.periodicity <= 6) { // Between Monday and Sunday - const nextRunDate = this.getNextDayOfTheWeek(refDate, schedule.trigger.periodicity); - return this.nextValidDate(nextRunDate, hour, minute, 7 * 24 * 60 * 60 * 1000); - } - - // every day - return this.nextValidDate(new Date(refDate), hour, minute, 24 * 60 * 60 * 1000); - } - return null; - } - protected findJob(jobName: string): IJob { return this.getAvailableJobs().find(t => t.Name === jobName); } private runSchedule(schedule: JobScheduleDTO) { - const nextDate = this.getDateFromSchedule(new Date(), schedule); + const nextDate = JobScheduleDTO.getNextRunningDate(new Date(), schedule); if (nextDate && nextDate.getTime() > Date.now()) { Logger.debug(LOG_TAG, 'running schedule: ' + schedule.jobName + ' at ' + nextDate.toLocaleString(undefined, {hour12: false})); diff --git a/src/backend/model/jobs/jobs/FileJob.ts b/src/backend/model/jobs/jobs/FileJob.ts index 4cdcf54c..9c7d5969 100644 --- a/src/backend/model/jobs/jobs/FileJob.ts +++ b/src/backend/model/jobs/jobs/FileJob.ts @@ -10,7 +10,7 @@ import {Logger} from '../../../Logger'; declare var global: NodeJS.Global; -const LOG_TAG = '[FileTask]'; +const LOG_TAG = '[FileJob]'; export abstract class FileJob extends Job { @@ -56,7 +56,7 @@ export abstract class FileJob extends Job { await this.processFile(file); } catch (e) { console.error(e); - Logger.error(LOG_TAG, 'Error during processing file: ' + e.toString()); + Logger.error(LOG_TAG, 'Error during processing file.' + ', ' + e.toString()); } } return this.progress; diff --git a/src/backend/model/jobs/jobs/IJob.ts b/src/backend/model/jobs/jobs/IJob.ts index b369dc62..cf094be1 100644 --- a/src/backend/model/jobs/jobs/IJob.ts +++ b/src/backend/model/jobs/jobs/IJob.ts @@ -6,7 +6,7 @@ export interface IJob extends JobDTO { Supported: boolean; Progress: JobProgressDTO; - start(config: T): Promise; + start(config: T, onFinishCB?: () => void): Promise; stop(): void; diff --git a/src/backend/model/jobs/jobs/Job.ts b/src/backend/model/jobs/jobs/Job.ts index 2ec7262c..8127f34c 100644 --- a/src/backend/model/jobs/jobs/Job.ts +++ b/src/backend/model/jobs/jobs/Job.ts @@ -2,7 +2,6 @@ import {JobProgressDTO, JobState} from '../../../../common/entities/settings/Job import {Logger} from '../../../Logger'; import {IJob} from './IJob'; import {ConfigTemplateEntry, JobDTO} from '../../../../common/entities/job/JobDTO'; -import * as rimraf from 'rimraf'; declare const process: any; @@ -16,20 +15,19 @@ export abstract class Job implements IJob { protected prResolve: () => void; protected IsInstant = false; - public abstract get Supported(): boolean; public abstract get Name(): string; - public abstract get ConfigTemplate(): ConfigTemplateEntry[]; - public get Progress(): JobProgressDTO { return this.progress; } - public start(config: T): Promise { + public start(config: T, onFinishCB = () => { + }): Promise { + this.OnFinishCB = onFinishCB; if (this.state === JobState.idle && this.Supported) { Logger.info(LOG_TAG, 'Running job: ' + this.Name); this.config = config; @@ -72,6 +70,9 @@ export abstract class Job implements IJob { }; } + protected OnFinishCB = () => { + }; + protected abstract async step(): Promise; protected abstract async init(): Promise; @@ -85,6 +86,7 @@ export abstract class Job implements IJob { if (this.IsInstant) { this.prResolve(); } + this.OnFinishCB(); } private run() { diff --git a/src/backend/routes/admin/SettingsRouter.ts b/src/backend/routes/admin/SettingsRouter.ts index 959c4962..56372848 100644 --- a/src/backend/routes/admin/SettingsRouter.ts +++ b/src/backend/routes/admin/SettingsRouter.ts @@ -104,10 +104,10 @@ export class SettingsRouter { SettingsMWs.updateIndexingSettings, RenderingMWs.renderOK ); - app.put('/api/settings/tasks', + app.put('/api/settings/jobs', AuthenticationMWs.authenticate, AuthenticationMWs.authorise(UserRoles.Admin), - SettingsMWs.updateTasksSettings, + SettingsMWs.updateJobSettings, RenderingMWs.renderOK ); } diff --git a/src/common/config/private/PrivateConfigDefaultsClass.ts b/src/common/config/private/PrivateConfigDefaultsClass.ts index d51450e8..08f26b5b 100644 --- a/src/common/config/private/PrivateConfigDefaultsClass.ts +++ b/src/common/config/private/PrivateConfigDefaultsClass.ts @@ -76,17 +76,15 @@ export class PrivateConfigDefaultsClass extends PublicConfigClass implements IPr }, Jobs: { scheduled: [{ + name: DefaultsJobs[DefaultsJobs['Database Reset']], jobName: DefaultsJobs[DefaultsJobs['Database Reset']], config: {}, trigger: {type: JobTriggerType.never} }, { + name: DefaultsJobs[DefaultsJobs.Indexing], jobName: DefaultsJobs[DefaultsJobs.Indexing], config: {}, trigger: {type: JobTriggerType.never} - }, { - jobName: DefaultsJobs[DefaultsJobs['Video Converting']], - config: {}, - trigger: {type: JobTriggerType.never} }] } }; diff --git a/src/common/entities/job/JobScheduleDTO.ts b/src/common/entities/job/JobScheduleDTO.ts index 7176c96a..a7958522 100644 --- a/src/common/entities/job/JobScheduleDTO.ts +++ b/src/common/entities/job/JobScheduleDTO.ts @@ -1,5 +1,5 @@ export enum JobTriggerType { - never = 1, scheduled = 2, periodic = 3 + never = 1, scheduled = 2, periodic = 3, after = 4 } export interface JobTrigger { @@ -21,8 +21,64 @@ export interface PeriodicJobTrigger extends JobTrigger { atTime: number; // day time } +export interface AfterJobTrigger extends JobTrigger { + type: JobTriggerType.after; + afterScheduleName: string; // runs after schedule +} + export interface JobScheduleDTO { + name: string; jobName: string; config: any; - trigger: NeverJobTrigger | ScheduledJobTrigger | PeriodicJobTrigger; + trigger: NeverJobTrigger | ScheduledJobTrigger | PeriodicJobTrigger | AfterJobTrigger; +} + + +export module JobScheduleDTO { + + const getNextDayOfTheWeek = (refDate: Date, dayOfWeek: number) => { + const date = new Date(refDate); + date.setDate(refDate.getDate() + (dayOfWeek + 1 + 7 - refDate.getDay()) % 7); + if (date.getDay() === refDate.getDay()) { + return new Date(refDate); + } + date.setHours(0, 0, 0, 0); + return date; + }; + + const nextValidDate = (date: Date, h: number, m: number, dayDiff: number): Date => { + + date.setSeconds(0); + if (date.getHours() < h || (date.getHours() === h && date.getMinutes() < m)) { + date.setHours(h); + date.setMinutes(m); + } else { + date.setTime(date.getTime() + dayDiff); + date.setHours(h); + date.setMinutes(m); + } + return date; + }; + + export const getNextRunningDate = (refDate: Date, schedule: JobScheduleDTO): Date => { + switch (schedule.trigger.type) { + case JobTriggerType.scheduled: + return new Date(schedule.trigger.time); + + case JobTriggerType.periodic: + + + const hour = Math.floor(schedule.trigger.atTime / 1000 / (60 * 60)); + const minute = (schedule.trigger.atTime / 1000 / 60) % 60; + + if (schedule.trigger.periodicity <= 6) { // Between Monday and Sunday + const nextRunDate = getNextDayOfTheWeek(refDate, schedule.trigger.periodicity); + return nextValidDate(nextRunDate, hour, minute, 7 * 24 * 60 * 60 * 1000); + } + + // every day + return nextValidDate(new Date(refDate), hour, minute, 24 * 60 * 60 * 1000); + } + return null; + }; } diff --git a/src/frontend/app/ui/admin/admin.component.html b/src/frontend/app/ui/admin/admin.component.html index 726bb462..078c7a53 100644 --- a/src/frontend/app/ui/admin/admin.component.html +++ b/src/frontend/app/ui/admin/admin.component.html @@ -64,47 +64,47 @@
- - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts b/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts index 15f431cf..00568469 100644 --- a/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts +++ b/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts @@ -27,8 +27,8 @@ export abstract class SettingsComponent{}; - public original: T = {}; + public settings: T = {}; + public original: T = {}; text = { Enabled: 'Enabled', Disabled: 'Disabled', diff --git a/src/frontend/app/ui/settings/jobs/jobs.settings.component.html b/src/frontend/app/ui/settings/jobs/jobs.settings.component.html index 0649d887..0f32cf83 100644 --- a/src/frontend/app/ui/settings/jobs/jobs.settings.component.html +++ b/src/frontend/app/ui/settings/jobs/jobs.settings.component.html @@ -1,16 +1,17 @@
- {{Name}}* + {{Name}} + *
-
-
-
+
+
+
- {{schedule.jobName}} @ @@ -20,28 +21,24 @@ {{schedule.trigger.time | date:"medium"}} never + + after + {{schedule.trigger.afterScheduleName}} +
-
+
- - Select a job to schedule. - + {{schedule.jobName}}
@@ -60,6 +57,23 @@
+
+ +
+ + The job will run after that job finishes. + +
+
+
@@ -191,10 +205,47 @@ (click)="reset()" i18n>Reset
+ + + + diff --git a/src/frontend/app/ui/settings/jobs/jobs.settings.component.ts b/src/frontend/app/ui/settings/jobs/jobs.settings.component.ts index ccf48124..dd8f5abe 100644 --- a/src/frontend/app/ui/settings/jobs/jobs.settings.component.ts +++ b/src/frontend/app/ui/settings/jobs/jobs.settings.component.ts @@ -1,4 +1,4 @@ -import {Component, OnChanges, OnDestroy, OnInit} from '@angular/core'; +import {Component, OnChanges, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {JobsSettingsService} from './jobs.settings.service'; import {AuthenticationService} from '../../../model/network/authentication.service'; import {NavigationService} from '../../../model/navigation.service'; @@ -8,6 +8,7 @@ import {I18n} from '@ngx-translate/i18n-polyfill'; import {ErrorDTO} from '../../../../../common/entities/Error'; import {ScheduledJobsService} from '../scheduled-jobs.service'; import { + AfterJobTrigger, JobScheduleDTO, JobTriggerType, NeverJobTrigger, @@ -18,6 +19,8 @@ import {Utils} from '../../../../../common/Utils'; import {ServerConfig} from '../../../../../common/config/private/IPrivateConfig'; import {ConfigTemplateEntry} from '../../../../../common/entities/job/JobDTO'; import {JobState} from '../../../../../common/entities/settings/JobProgressDTO'; +import {Job} from '../../../../../backend/model/jobs/jobs/Job'; +import {ModalDirective} from 'ngx-bootstrap/modal'; @Component({ selector: 'app-settings-jobs', @@ -29,12 +32,21 @@ import {JobState} from '../../../../../common/entities/settings/JobProgressDTO'; export class JobsSettingsComponent extends SettingsComponent implements OnInit, OnDestroy, OnChanges { + @ViewChild('jobModal', {static: false}) public jobModal: ModalDirective; disableButtons = false; JobTriggerTypeMap: { key: number, value: string }[]; JobTriggerType = JobTriggerType; periods: string[] = []; showDetails: boolean[] = []; JobState = JobState; + newSchedule: JobScheduleDTO = { + name: '', + config: null, + jobName: '', + trigger: { + type: JobTriggerType.never + } + }; constructor(_authService: AuthenticationService, _navigation: NavigationService, @@ -63,7 +75,6 @@ export class JobsSettingsComponent extends SettingsComponent t.Name === JobName); if (job && job.ConfigTemplate && job.ConfigTemplate.length > 0) { @@ -83,7 +94,6 @@ export class JobsSettingsComponent extends SettingsComponent t.Name === schedule.jobName); schedule.config = schedule.config || {}; @@ -135,9 +144,10 @@ export class JobsSettingsComponent extends SettingsComponent{}, trigger: { @@ -146,11 +156,12 @@ export class JobsSettingsComponent extends SettingsComponent t.Name === jobName); - newSchedule.config = newSchedule.config || {}; + this.newSchedule.config = this.newSchedule.config || {}; if (job.ConfigTemplate) { - job.ConfigTemplate.forEach(ct => newSchedule.config[ct.id] = ct.defaultValue); + job.ConfigTemplate.forEach(ct => this.newSchedule.config[ct.id] = ct.defaultValue); } - this.settings.scheduled.push(newSchedule); + + this.jobModal.show(); } jobTriggerTypeChanged(triggerType: JobTriggerType, schedule: JobScheduleDTO) { @@ -183,6 +194,39 @@ export class JobsSettingsComponent extends SettingsComponent { + return this.getNextRunningDate(a, this.settings.scheduled) - this.getNextRunningDate(b, this.settings.scheduled); + }); + } + + addNewJob() { + const jobName = this.newSchedule.jobName; + const count = this.settings.scheduled.filter(s => s.jobName === jobName).length; + this.newSchedule.name = count === 0 ? jobName : jobName + ' ' + (count + 1); + this.settings.scheduled.push(this.newSchedule); + } + + private getNextRunningDate(sch: JobScheduleDTO, list: JobScheduleDTO[], depth: number = 0): number { + if (depth > list.length) { + return 0; + } + if (sch.trigger.type === JobTriggerType.never) { + return list.map(s => s.name).sort().indexOf(sch.name) * -1; + } + if (sch.trigger.type === JobTriggerType.after) { + const parent = list.find(s => s.name === (sch.trigger).afterScheduleName); + if (parent) { + return this.getNextRunningDate(parent, list, depth + 1) + 0.001; + } + } + const d = JobScheduleDTO.getNextRunningDate(new Date(), sch); + return d !== null ? d.getTime() : 0; + } }