diff --git a/src/backend/middlewares/admin/AdminMWs.ts b/src/backend/middlewares/admin/AdminMWs.ts index d229534f..c9c40b04 100644 --- a/src/backend/middlewares/admin/AdminMWs.ts +++ b/src/backend/middlewares/admin/AdminMWs.ts @@ -51,21 +51,21 @@ export class AdminMWs { } - public static startJob(soloRun: boolean) { - return async (req: Request, res: Response, next: NextFunction) => { - try { - const id = req.params.id; - const JobConfig: any = req.body.config; - await ObjectManagers.getInstance().JobManager.run(id, JobConfig, soloRun); - req.resultPipe = 'ok'; - return next(); - } catch (err) { - if (err instanceof Error) { - return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + err.toString(), err)); - } - return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + JSON.stringify(err, null, ' '), err)); + public static async startJob(req: Request, res: Response, next: NextFunction) { + try { + const id = req.params.id; + const JobConfig: any = req.body.config; + const soloRun: boolean = req.body.soloRun; + const allowParallelRun: boolean = req.body.allowParallelRun; + await ObjectManagers.getInstance().JobManager.run(id, JobConfig, soloRun, allowParallelRun); + req.resultPipe = 'ok'; + return next(); + } catch (err) { + if (err instanceof Error) { + return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + err.toString(), err)); } - }; + return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + JSON.stringify(err, null, ' '), err)); + } } public static stopJob(req: Request, res: Response, next: NextFunction) { diff --git a/src/backend/model/database/interfaces/IJobManager.ts b/src/backend/model/database/interfaces/IJobManager.ts index ac882dc9..867d6f32 100644 --- a/src/backend/model/database/interfaces/IJobManager.ts +++ b/src/backend/model/database/interfaces/IJobManager.ts @@ -4,7 +4,7 @@ import {JobDTO} from '../../../../common/entities/job/JobDTO'; export interface IJobManager { - run(jobId: string, config: any, soloRun: boolean): Promise; + run(jobId: string, config: any, soloRun: boolean, allowParallelRun: boolean): Promise; stop(jobId: string): void; diff --git a/src/backend/model/jobs/JobManager.ts b/src/backend/model/jobs/JobManager.ts index 8c885cfc..94655ea8 100644 --- a/src/backend/model/jobs/JobManager.ts +++ b/src/backend/model/jobs/JobManager.ts @@ -22,15 +22,28 @@ export class JobManager implements IJobManager, IJobListener { this.runSchedules(); } + protected get JobRunning(): boolean { + return JobRepository.Instance.getAvailableJobs().findIndex(j => j.InProgress === true) !== -1; + } + + protected get JobNoParallelRunning(): boolean { + return JobRepository.Instance.getAvailableJobs() + .findIndex(j => j.InProgress === true && j.allowParallelRun) !== -1; + } + getProgresses(): { [id: string]: JobProgressDTO } { return this.progressManager.Progresses; } - async run(jobName: string, config: T, soloRun: boolean): Promise { + async run(jobName: string, config: T, soloRun: boolean, allowParallelRun: boolean): Promise { + if ((allowParallelRun === false && this.JobRunning === true) || this.JobNoParallelRunning === true) { + throw new Error('Can\'t start this job while an other is running'); + } + const t = this.findJob(jobName); if (t) { t.JobListener = this; - await t.start(config, soloRun); + await t.start(config, soloRun, allowParallelRun); } else { Logger.warn(LOG_TAG, 'cannot find job to start:' + jobName); } @@ -49,7 +62,6 @@ export class JobManager implements IJobManager, IJobListener { this.progressManager.onJobProgressUpdate(progress.toDTO()); }; - onJobFinished = async (job: IJob, state: JobProgressStates, soloRun: boolean): Promise => { // if it was not finished peacefully or was a soloRun, do not start the next one if (state !== JobProgressStates.finished || soloRun === true) { @@ -61,7 +73,7 @@ export class JobManager implements IJobManager, IJobListener { (s.trigger).afterScheduleName === sch.name); for (let i = 0; i < children.length; ++i) { try { - await this.run(children[i].jobName, children[i].config, false); + await this.run(children[i].jobName, children[i].config, false, children[i].allowParallelRun); } catch (e) { NotificationManager.warning('Job running error:' + children[i].name, e.toString()); } @@ -96,7 +108,7 @@ export class JobManager implements IJobManager, IJobListener { const timer: NodeJS.Timeout = setTimeout(async () => { this.timers = this.timers.filter(t => t.timer !== timer); - await this.run(schedule.jobName, schedule.config, false); + await this.run(schedule.jobName, schedule.config, false, schedule.allowParallelRun); this.runSchedule(schedule); }, nextDate.getTime() - Date.now()); this.timers.push({schedule: schedule, timer: timer}); diff --git a/src/backend/model/jobs/JobProgressManager.ts b/src/backend/model/jobs/JobProgressManager.ts index 7c205551..5f153baa 100644 --- a/src/backend/model/jobs/JobProgressManager.ts +++ b/src/backend/model/jobs/JobProgressManager.ts @@ -33,6 +33,7 @@ export class JobProgressManager { } + onJobProgressUpdate(progress: JobProgressDTO) { this.db.progresses[progress.HashName] = {progress: progress, timestamp: Date.now()}; this.delayedSave(); diff --git a/src/backend/model/jobs/jobs/IJob.ts b/src/backend/model/jobs/jobs/IJob.ts index 6ca30d7c..acd84d5a 100644 --- a/src/backend/model/jobs/jobs/IJob.ts +++ b/src/backend/model/jobs/jobs/IJob.ts @@ -7,8 +7,10 @@ export interface IJob extends JobDTO { Supported: boolean; Progress: JobProgress; JobListener: IJobListener; + InProgress: boolean; + allowParallelRun: boolean; - start(config: T, soloRun?: boolean): Promise; + start(config: T, soloRun: boolean, allowParallelRun: boolean): Promise; cancel(): void; diff --git a/src/backend/model/jobs/jobs/Job.ts b/src/backend/model/jobs/jobs/Job.ts index 30a7b3ca..0ad52a36 100644 --- a/src/backend/model/jobs/jobs/Job.ts +++ b/src/backend/model/jobs/jobs/Job.ts @@ -11,6 +11,7 @@ declare const global: any; const LOG_TAG = '[JOB]'; export abstract class Job implements IJob { + public allowParallelRun: boolean = null; protected progress: JobProgress = null; protected config: T; protected prResolve: () => void; @@ -33,15 +34,16 @@ export abstract class Job implements IJob { return this.progress; } - protected get InProgress(): boolean { + public get InProgress(): boolean { return this.Progress !== null && (this.Progress.State === JobProgressStates.running || this.Progress.State === JobProgressStates.cancelling); } - public start(config: T, soloRun = false): Promise { + public start(config: T, soloRun = false, allowParallelRun = false): Promise { if (this.InProgress === false && this.Supported === true) { Logger.info(LOG_TAG, 'Running job ' + (soloRun === true ? 'solo' : '') + ': ' + this.Name); this.soloRun = soloRun; + this.allowParallelRun = allowParallelRun; this.config = config; this.progress = new JobProgress(this.Name, JobDTO.getHashName(this.Name, this.config)); this.progress.OnChange = this.jobListener.onProgressUpdate; diff --git a/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts b/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts index c347cd6d..9c71d461 100644 --- a/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts +++ b/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts @@ -29,14 +29,14 @@ export class ThumbnailGenerationJob extends FileJob<{ sizes: number[], indexedOn return true; } - start(config: { sizes: number[], indexedOnly: boolean }, soloRun = false): Promise { + start(config: { sizes: number[], indexedOnly: boolean }, soloRun = false, allowParallelRun = false): Promise { for (let i = 0; i < config.sizes.length; ++i) { if (Config.Client.Media.Thumbnail.thumbnailSizes.indexOf(config.sizes[i]) === -1) { throw new Error('unknown thumbnails size: ' + config.sizes[i] + '. Add it to the possible thumbnail sizes.'); } } - return super.start(config, soloRun); + return super.start(config, soloRun, allowParallelRun); } protected async filterMediaFiles(files: FileDTO[]): Promise { diff --git a/src/backend/routes/admin/AdminRouter.ts b/src/backend/routes/admin/AdminRouter.ts index 9bb77fe5..5bd4d1c9 100644 --- a/src/backend/routes/admin/AdminRouter.ts +++ b/src/backend/routes/admin/AdminRouter.ts @@ -46,13 +46,7 @@ export class AdminRouter { app.post('/api/admin/jobs/scheduled/:id/start', AuthenticationMWs.authenticate, AuthenticationMWs.authorise(UserRoles.Admin), - AdminMWs.startJob(false), - RenderingMWs.renderResult - ); - app.post('/api/admin/jobs/scheduled/:id/soloStart', - AuthenticationMWs.authenticate, - AuthenticationMWs.authorise(UserRoles.Admin), - AdminMWs.startJob(true), + AdminMWs.startJob, RenderingMWs.renderResult ); app.post('/api/admin/jobs/scheduled/:id/stop', diff --git a/src/common/config/private/PrivateConfigDefaultsClass.ts b/src/common/config/private/PrivateConfigDefaultsClass.ts index 80ab9dc3..94032041 100644 --- a/src/common/config/private/PrivateConfigDefaultsClass.ts +++ b/src/common/config/private/PrivateConfigDefaultsClass.ts @@ -75,6 +75,7 @@ export class PrivateConfigDefaultsClass extends PublicConfigClass implements IPr { name: DefaultsJobs[DefaultsJobs.Indexing], jobName: DefaultsJobs[DefaultsJobs.Indexing], + allowParallelRun: false, config: {}, trigger: {type: JobTriggerType.never} }, @@ -82,24 +83,27 @@ export class PrivateConfigDefaultsClass extends PublicConfigClass implements IPr name: DefaultsJobs[DefaultsJobs['Thumbnail Generation']], jobName: DefaultsJobs[DefaultsJobs['Thumbnail Generation']], config: {sizes: [240]}, + allowParallelRun: false, trigger: { type: JobTriggerType.after, afterScheduleName: DefaultsJobs[DefaultsJobs.Indexing] } }, - /* { - name: DefaultsJobs[DefaultsJobs['Photo Converting']], - jobName: DefaultsJobs[DefaultsJobs['Photo Converting']], - config: {}, - trigger: { - type: JobTriggerType.after, - afterScheduleName: DefaultsJobs[DefaultsJobs['Thumbnail Generation']] - } - },*/ + /* { + name: DefaultsJobs[DefaultsJobs['Photo Converting']], + jobName: DefaultsJobs[DefaultsJobs['Photo Converting']], + config: {}, + parallelRunEnabled:false, + trigger: { + type: JobTriggerType.after, + afterScheduleName: DefaultsJobs[DefaultsJobs['Thumbnail Generation']] + } + },*/ { name: DefaultsJobs[DefaultsJobs['Video Converting']], jobName: DefaultsJobs[DefaultsJobs['Video Converting']], config: {}, + allowParallelRun: false, trigger: { type: JobTriggerType.after, afterScheduleName: DefaultsJobs[DefaultsJobs['Thumbnail Generation']] @@ -109,6 +113,7 @@ export class PrivateConfigDefaultsClass extends PublicConfigClass implements IPr name: DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']], jobName: DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']], config: {}, + allowParallelRun: false, trigger: { type: JobTriggerType.after, afterScheduleName: DefaultsJobs[DefaultsJobs['Video Converting']] diff --git a/src/common/entities/job/JobScheduleDTO.ts b/src/common/entities/job/JobScheduleDTO.ts index a7958522..8a366694 100644 --- a/src/common/entities/job/JobScheduleDTO.ts +++ b/src/common/entities/job/JobScheduleDTO.ts @@ -30,6 +30,7 @@ export interface JobScheduleDTO { name: string; jobName: string; config: any; + allowParallelRun: boolean; trigger: NeverJobTrigger | ScheduledJobTrigger | PeriodicJobTrigger | AfterJobTrigger; } diff --git a/src/frontend/app/ui/settings/indexing/indexing.settings.component.html b/src/frontend/app/ui/settings/indexing/indexing.settings.component.html index d14402e8..5ad471af 100644 --- a/src/frontend/app/ui/settings/indexing/indexing.settings.component.html +++ b/src/frontend/app/ui/settings/indexing/indexing.settings.component.html @@ -114,6 +114,7 @@ @@ -121,6 +122,7 @@ danger="true" [soloRun]="true" (error)="error=$event" + [allowParallelRun]="false" [disabled]="indexingButton.Running" [jobName]="resetJobName"> diff --git a/src/frontend/app/ui/settings/jobs/button/job-button.settings.component.ts b/src/frontend/app/ui/settings/jobs/button/job-button.settings.component.ts index 23bdf312..0cd141a8 100644 --- a/src/frontend/app/ui/settings/jobs/button/job-button.settings.component.ts +++ b/src/frontend/app/ui/settings/jobs/button/job-button.settings.component.ts @@ -18,6 +18,7 @@ export class JobButtonComponent { @Input() shortName = false; @Input() disabled = false; @Input() soloRun = false; + @Input() allowParallelRun = false; @Input() danger = false; JobProgressStates = JobProgressStates; @Output() error = new EventEmitter(); @@ -40,7 +41,7 @@ export class JobButtonComponent { public async start() { this.error.emit(''); try { - await this.jobsService.start(this.jobName, this.config, this.soloRun); + await this.jobsService.start(this.jobName, this.config, this.soloRun, this.allowParallelRun); this.notification.success(this.i18n('Job started') + ': ' + this.backendTextService.getJobName(this.jobName)); return true; } catch (err) { @@ -57,7 +58,7 @@ export class JobButtonComponent { this.error.emit(''); try { await this.jobsService.stop(this.jobName); - this.notification.info(this.i18n('Job stopped') + ': ' + this.backendTextService.getJobName(this.jobName)); + this.notification.info(this.i18n('Stopping job') + ': ' + this.backendTextService.getJobName(this.jobName)); return true; } catch (err) { console.log(err); 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 f33b245c..ca76f1e5 100644 --- a/src/frontend/app/ui/settings/jobs/jobs.settings.component.html +++ b/src/frontend/app/ui/settings/jobs/jobs.settings.component.html @@ -24,16 +24,19 @@ *ngSwitchCase="JobTriggerType.scheduled">{{schedule.trigger.time | date:"medium"}} never - after: + after + : {{schedule.trigger.afterScheduleName}}
-
@@ -51,10 +54,11 @@ {{backendTextService.getJobName(schedule.jobName)}}
- +
@@ -121,6 +125,26 @@ [(timestamp)]="schedule.trigger.atTime"> +
+ +
+ + + Enables the job to start even if an other job is already running. + +
+
@@ -137,7 +161,6 @@ {}, trigger: { type: JobTriggerType.never - } + }, + allowParallelRun: false }; const job = this._settingsService.availableJobs.value.find(t => t.Name === jobName); diff --git a/src/frontend/app/ui/settings/photo/photo.settings.component.html b/src/frontend/app/ui/settings/photo/photo.settings.component.html index 74e6fe3c..9c0e966e 100644 --- a/src/frontend/app/ui/settings/photo/photo.settings.component.html +++ b/src/frontend/app/ui/settings/photo/photo.settings.component.html @@ -103,6 +103,7 @@ diff --git a/src/frontend/app/ui/settings/scheduled-jobs.service.ts b/src/frontend/app/ui/settings/scheduled-jobs.service.ts index 5ada4906..3d537e5c 100644 --- a/src/frontend/app/ui/settings/scheduled-jobs.service.ts +++ b/src/frontend/app/ui/settings/scheduled-jobs.service.ts @@ -41,14 +41,23 @@ export class ScheduledJobsService { return await this.loadProgress(); } - public async start(jobName: string, config?: any, soloStart: boolean = false): Promise { - this.jobStartingStopping[jobName] = true; - await this._networkService.postJson('/admin/jobs/scheduled/' + jobName + '/' + (soloStart === true ? 'soloStart' : 'start'), - {config: config}); - // placeholder to force showing running job - this.addDummyProgress(jobName, config); - delete this.jobStartingStopping[jobName]; - this.forceUpdate(); + public async start(jobName: string, config?: any, soloStart: boolean = false, allowParallelRun = false): Promise { + try { + this.jobStartingStopping[jobName] = true; + await this._networkService.postJson('/admin/jobs/scheduled/' + jobName + '/start', + { + config: config, + allowParallelRun: allowParallelRun, + soloStart: soloStart + }); + // placeholder to force showing running job + this.addDummyProgress(jobName, config); + } catch (e) { + throw e; + } finally { + delete this.jobStartingStopping[jobName]; + this.forceUpdate(); + } } public async stop(jobName: string): Promise { @@ -70,7 +79,7 @@ export class ScheduledJobsService { this.progress.value[prg].state === JobProgressStates.cancelling) )) { this.onJobFinish.emit(prg); - this.notification.info(this.i18n('Job finished') + ': ' + this.backendTextService.getJobName(prevPrg[prg].jobName)); + this.notification.success(this.i18n('Job finished') + ': ' + this.backendTextService.getJobName(prevPrg[prg].jobName)); } } } diff --git a/src/frontend/app/ui/settings/thumbnail/thumbnail.settings.component.html b/src/frontend/app/ui/settings/thumbnail/thumbnail.settings.component.html index 6e5ea85e..bd1dea7f 100644 --- a/src/frontend/app/ui/settings/thumbnail/thumbnail.settings.component.html +++ b/src/frontend/app/ui/settings/thumbnail/thumbnail.settings.component.html @@ -78,6 +78,7 @@ [soloRun]="true" (error)="error=$event" [jobName]="jobName" + [allowParallelRun]="false" [config]="Config"> diff --git a/src/frontend/app/ui/settings/video/video.settings.component.html b/src/frontend/app/ui/settings/video/video.settings.component.html index 15eabdd4..d6da35a9 100644 --- a/src/frontend/app/ui/settings/video/video.settings.component.html +++ b/src/frontend/app/ui/settings/video/video.settings.component.html @@ -130,6 +130,7 @@