From acf7e22e63583f8d8c3b7eb983864257e489bbc1 Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 12:47:54 +0200 Subject: [PATCH 01/10] feat: add property delay to MomoJob to allow scheduling immediate jobs with delay Signed-off-by: Ute Weiss --- README.md | 13 +++++++------ src/job/Job.ts | 6 ++++-- src/job/MomoJob.ts | 1 + src/repository/JobRepository.ts | 10 +--------- src/scheduler/calculateDelay.ts | 2 +- test/executor/JobExecutor.spec.ts | 1 + test/repository/JobRepository.integration.spec.ts | 10 ++++++---- test/scheduler/JobScheduler.spec.ts | 1 + test/scheduler/calculateDelay.spec.ts | 11 ++++++----- 9 files changed, 28 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index bad9461f..dac32953 100644 --- a/README.md +++ b/README.md @@ -54,13 +54,14 @@ await mongoSchedule.disconnect(); ### MomoJob -| property | type | optional | default | description | -|-------------|----------|----------|---------|-------------| +| property | type | optional | default | description | +|-------------|------------|----------|---------|-------------| | name | `string` | false | | The name of the job. Used as a unique identifier. | | interval | `string` | false | | Specifies the time interval at which the job is started. Time intervals in human-readable formats (like '1 minute', 'ten days' or 'twenty-one days and 2 hours') are accepted. Check documentation of [human-interval](https://www.npmjs.com/package/human-interval) library for details. | -| immediate | `boolean` | true | `false` | If set to true AND the job was never run before, the job will be started immediately after start. | -| concurrency | `number` | true | `1` | How many instances of a job are started at a time. | -| maxRunning | `number` | true | `0` | Maximum number of job executions that is allowed at a time. Set to 0 for no max. The schedule will trigger no more job executions if maxRunning is reached. However, there is no guarantee that the schedule always respects the limit; in rare cases with multiple Momo instances maxRunning may be exceeded. | +| immediate | `boolean` | true | `false` | If set to true AND the job was never run before, the job will be started after `delay` milliseconds. | +| delay | `number` | true | `0` | If immediate is set to true AND the job was never run before, the job will be started after `delay` milliseconds. | +| concurrency | `number` | true | `1` | How many instances of a job are started at a time. | +| maxRunning | `number` | true | `0` | Maximum number of job executions that is allowed at a time. Set to 0 for no max. The schedule will trigger no more job executions if maxRunning is reached. However, there is no guarantee that the schedule always respects the limit; in rare cases with multiple Momo instances maxRunning may be exceeded. | | handler | `function` | false | | The function to execute. | ### MongoSchedule @@ -91,7 +92,7 @@ If the parameter is omitted, all jobs are started/stopped/cancelled/removed. ### MomoJobDescription -The job description returned by the `list` and `get`functions contains the following properties: +The job description returned by the `list` and `get` functions contains the following properties: | property | type | optional | description | |-------------|----------|----------|-------------| diff --git a/src/job/Job.ts b/src/job/Job.ts index 517bdd03..64695202 100644 --- a/src/job/Job.ts +++ b/src/job/Job.ts @@ -8,6 +8,7 @@ export type MomoJobStatus = WithoutId; export interface JobDefinition { name: string; interval: string; + delay: number; concurrency: number; maxRunning: number; } @@ -18,13 +19,14 @@ export interface Job extends JobDefinition { } export function toJob(job: MomoJob): Job { - return { immediate: false, concurrency: 1, maxRunning: 0, ...job }; + return { delay: 0, immediate: false, concurrency: 1, maxRunning: 0, ...job }; } -export function toJobDefinition(job: Job): JobDefinition { +export function toJobDefinition(job: T): JobDefinition { return { name: job.name, interval: job.interval, + delay: job.delay, maxRunning: job.maxRunning, concurrency: job.concurrency, }; diff --git a/src/job/MomoJob.ts b/src/job/MomoJob.ts index 69f2f6ee..ccc21280 100644 --- a/src/job/MomoJob.ts +++ b/src/job/MomoJob.ts @@ -5,6 +5,7 @@ export interface MomoJob { handler: Handler; name: string; interval: string; + delay?: number; concurrency?: number; maxRunning?: number; } diff --git a/src/repository/JobRepository.ts b/src/repository/JobRepository.ts index 80c41ef4..27d07ed8 100644 --- a/src/repository/JobRepository.ts +++ b/src/repository/JobRepository.ts @@ -66,15 +66,7 @@ export class JobRepository extends Repository { async list(): Promise { const jobs = await this.find(); - return jobs.map((job) => { - return { - name: job.name, - interval: job.interval, - concurrency: job.concurrency, - maxRunning: job.maxRunning, - executionInfo: job.executionInfo, - }; - }); + return jobs.map((job) => ({ ...toJobDefinition(job), executionInfo: job.executionInfo })); } async updateJob(name: string, update: Partial): Promise { diff --git a/src/scheduler/calculateDelay.ts b/src/scheduler/calculateDelay.ts index 6f1f1c84..3d68715b 100644 --- a/src/scheduler/calculateDelay.ts +++ b/src/scheduler/calculateDelay.ts @@ -6,7 +6,7 @@ import { JobEntity } from '../repository/JobEntity'; export function calculateDelay(millisecondsInterval: number, immediate: boolean, job: JobEntity): number { const nextStart = calculateNextStart(millisecondsInterval, job); if (nextStart === undefined) { - return immediate ? 0 : millisecondsInterval; + return immediate ? job.delay : millisecondsInterval; } return max([nextStart - DateTime.now().toMillis(), 0]) ?? 0; diff --git a/test/executor/JobExecutor.spec.ts b/test/executor/JobExecutor.spec.ts index 825a3f22..78ea5396 100644 --- a/test/executor/JobExecutor.spec.ts +++ b/test/executor/JobExecutor.spec.ts @@ -14,6 +14,7 @@ describe('JobExecutor', () => { const job: Job = { name: 'test', interval: '1 minute', + delay: 0, immediate: false, concurrency: 1, maxRunning: 0, diff --git a/test/repository/JobRepository.integration.spec.ts b/test/repository/JobRepository.integration.spec.ts index ddf5eea6..c2ca1a75 100644 --- a/test/repository/JobRepository.integration.spec.ts +++ b/test/repository/JobRepository.integration.spec.ts @@ -92,19 +92,19 @@ describe('JobRepository', () => { describe('list', () => { it('returns jobs', async () => { - const job1 = { + const job1: JobEntity = { name: 'job1', interval: '1 minute', + delay: 0, executionInfo: {} as ExecutionInfo, - running: 2, concurrency: 1, maxRunning: 3, }; - const job2 = { + const job2: JobEntity = { name: 'job2', interval: '2 minutes', + delay: 0, executionInfo: {} as ExecutionInfo, - running: 0, concurrency: 1, maxRunning: 0, }; @@ -117,6 +117,7 @@ describe('JobRepository', () => { { name: job1.name, interval: job1.interval, + delay: job1.delay, concurrency: job1.concurrency, maxRunning: job1.maxRunning, executionInfo: {}, @@ -124,6 +125,7 @@ describe('JobRepository', () => { { name: job2.name, interval: job2.interval, + delay: job1.delay, concurrency: job2.concurrency, maxRunning: job2.maxRunning, executionInfo: {}, diff --git a/test/scheduler/JobScheduler.spec.ts b/test/scheduler/JobScheduler.spec.ts index bd2e8f37..6e43fa61 100644 --- a/test/scheduler/JobScheduler.spec.ts +++ b/test/scheduler/JobScheduler.spec.ts @@ -13,6 +13,7 @@ describe('JobScheduler', () => { const defaultJob = { name: 'test', interval: '1 second', + delay: 0, immediate: false, concurrency: 1, maxRunning: 0, diff --git a/test/scheduler/calculateDelay.spec.ts b/test/scheduler/calculateDelay.spec.ts index 759fb5d7..061dd26d 100644 --- a/test/scheduler/calculateDelay.spec.ts +++ b/test/scheduler/calculateDelay.spec.ts @@ -13,6 +13,7 @@ describe('calculateDelay', () => { job = { name: 'test', interval: 'one second', + delay: 100, concurrency: 0, maxRunning: 1, }; @@ -22,13 +23,13 @@ describe('calculateDelay', () => { afterEach(() => clock.uninstall()); describe('immediate=false', () => { - it('calculates delay when job was never started before', () => { + it('calculates delay if job was never started before (ignores configured delay)', () => { const delay = calculateDelay(1000, false, job); expect(delay).toBe(1000); }); - it('calculates delay when job was started before', () => { + it('calculates delay based on lastStarted', () => { job.executionInfo = { lastStarted: DateTime.now().toISO() } as ExecutionInfo; clock.tick(500); @@ -39,13 +40,13 @@ describe('calculateDelay', () => { }); describe('immediate=true', () => { - it('calculates delay when job was never started before', () => { + it('uses configured delay if job was never started before', () => { const delay = calculateDelay(1000, true, job); - expect(delay).toBe(0); + expect(delay).toBe(job.delay); }); - it('calculates delay when job was started before', () => { + it('calculates delay based on lastStarted', () => { job.executionInfo = { lastStarted: DateTime.now().toISO() } as ExecutionInfo; clock.tick(500); From e91ad5d7164212d6a95076e1bb80dab348812298 Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 13:12:45 +0200 Subject: [PATCH 02/10] feat: add job/scheduling examples to README.md Signed-off-by: Ute Weiss --- README.md | 158 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index dac32953..030b57b3 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ In essence, it provides an easy way to tell your application to "run that job ev ## Features -* allows concurrent jobs -* allows immediate jobs -* supports long-running jobs -* allows error handling -* allows updating jobs during runtime -* supports cluster mode (e.g. several services using the same job database) +- allows concurrent jobs +- allows immediate jobs +- supports long-running jobs +- allows error handling +- allows updating jobs during runtime +- supports cluster mode (e.g. several services using the same job database) ## Installation @@ -42,8 +42,12 @@ const job: MomoJob = { name: 'momo test', interval: '1 minute', handler: () => c await mongoSchedule.define(job); // optional: listen to error and debug events -mongoSchedule.on('error', (error: MomoErrorEvent) => { /* handle error */ }); -mongoSchedule.on('debug', (debug: MomoEvent) => { /* ... */ }); +mongoSchedule.on('error', (error: MomoErrorEvent) => { + /* handle error */ +}); +mongoSchedule.on('debug', (debug: MomoEvent) => { + /* ... */ +}); await mongoSchedule.start(); @@ -54,15 +58,15 @@ await mongoSchedule.disconnect(); ### MomoJob -| property | type | optional | default | description | -|-------------|------------|----------|---------|-------------| -| name | `string` | false | | The name of the job. Used as a unique identifier. | -| interval | `string` | false | | Specifies the time interval at which the job is started. Time intervals in human-readable formats (like '1 minute', 'ten days' or 'twenty-one days and 2 hours') are accepted. Check documentation of [human-interval](https://www.npmjs.com/package/human-interval) library for details. | -| immediate | `boolean` | true | `false` | If set to true AND the job was never run before, the job will be started after `delay` milliseconds. | -| delay | `number` | true | `0` | If immediate is set to true AND the job was never run before, the job will be started after `delay` milliseconds. | -| concurrency | `number` | true | `1` | How many instances of a job are started at a time. | +| property | type | optional | default | description | +| ----------- | ---------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | false | | The name of the job. Used as a unique identifier. | +| interval | `string` | false | | Specifies the time interval at which the job is started. Time intervals in human-readable formats (like '1 minute', 'ten days' or 'twenty-one days and 2 hours') are accepted. Check documentation of [human-interval](https://www.npmjs.com/package/human-interval) library for details. | +| immediate | `boolean` | true | `false` | If set to true AND the job was never run before, the job will be started after `delay` milliseconds. | +| delay | `number` | true | `0` | If immediate is set to true AND the job was never run before, the job will be started after `delay` milliseconds. | +| concurrency | `number` | true | `1` | How many instances of a job are started at a time. | | maxRunning | `number` | true | `0` | Maximum number of job executions that is allowed at a time. Set to 0 for no max. The schedule will trigger no more job executions if maxRunning is reached. However, there is no guarantee that the schedule always respects the limit; in rare cases with multiple Momo instances maxRunning may be exceeded. | -| handler | `function` | false | | The function to execute. | +| handler | `function` | false | | The function to execute. | ### MongoSchedule @@ -71,36 +75,36 @@ Only the job with the provided name is started/stopped/cancelled/removed. If there is no job with this name on the schedule, nothing is done. If the parameter is omitted, all jobs are started/stopped/cancelled/removed. -| function | parameters | description | -|------------|-----------------------|-------------| -| define | `MomoJob` | Creates a new MomoJob on the schedule. | -| start | | Starts jobs that are on the schedule. | -| stop | | Stops jobs, but does not remove them from either the schedule or the database. | -| cancel | | Stops and removes jobs from the schedule, does not remove them from the database. | -| remove | | Stops and removes jobs from both the schedule and the database. | -| startJob | `string` | Starts the job with the provided name (if on the schedule). | -| stopJob | `string` | Stops the job with the provided name (if on the schedule), but does not remove it from either the schedule or the database. | -| cancelJob | `string` | Stops and removes the job with the provided name (if on the schedule) from the schedule, does not remove it from the database. | -| removeJob | `string` | Stops and removes the job with the provided name (if on the schedule) from both the schedule and the database. | -| count | `boolean` (optional) | Returns the number of jobs on the schedule. Only started jobs are counted if parameter is set to true. | -| list | | Returns descriptions of all jobs on the schedule. | -| check | `string` | Returns execution information of the job with the provided name from the database. This also works if the job is not on the schedule. | -| clear | | Removes all jobs from the database. This also removes jobs that are not on this schedule, but were defined by other schedules. However, does NOT stop job executions - this will cause currently running jobs to fail. Consider using stop/cancel/remove methods instead! | -| get | `string` | Returns a description of the job. Returns undefined if no job with the provided name is defined. | -| run | `string` | Runs the job with the provided name once, immediately. Note that `maxRunning` is respected, ie. the execution is skipped if the job is already running `maxRunning` times. | -| on | `'debug'` or `'error'`, `function` | Define a callback for debug or error events. | +| function | parameters | description | +| --------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| define | `MomoJob` | Creates a new MomoJob on the schedule. | +| start | | Starts jobs that are on the schedule. | +| stop | | Stops jobs, but does not remove them from either the schedule or the database. | +| cancel | | Stops and removes jobs from the schedule, does not remove them from the database. | +| remove | | Stops and removes jobs from both the schedule and the database. | +| startJob | `string` | Starts the job with the provided name (if on the schedule). | +| stopJob | `string` | Stops the job with the provided name (if on the schedule), but does not remove it from either the schedule or the database. | +| cancelJob | `string` | Stops and removes the job with the provided name (if on the schedule) from the schedule, does not remove it from the database. | +| removeJob | `string` | Stops and removes the job with the provided name (if on the schedule) from both the schedule and the database. | +| count | `boolean` (optional) | Returns the number of jobs on the schedule. Only started jobs are counted if parameter is set to true. | +| list | | Returns descriptions of all jobs on the schedule. | +| check | `string` | Returns execution information of the job with the provided name from the database. This also works if the job is not on the schedule. | +| clear | | Removes all jobs from the database. This also removes jobs that are not on this schedule, but were defined by other schedules. However, does NOT stop job executions - this will cause currently running jobs to fail. Consider using stop/cancel/remove methods instead! | +| get | `string` | Returns a description of the job. Returns undefined if no job with the provided name is defined. | +| run | `string` | Runs the job with the provided name once, immediately. Note that `maxRunning` is respected, ie. the execution is skipped if the job is already running `maxRunning` times. | +| on | `'debug'` or `'error'`, `function` | Define a callback for debug or error events. | ### MomoJobDescription -The job description returned by the `list` and `get` functions contains the following properties: +The job description returned by the `list` and `get` functions of the `MongoSchedule` contains the following properties: -| property | type | optional | description | -|-------------|----------|----------|-------------| -| name | `string` | false | The name of the job. Used as a unique identifier. | -| interval | `string` | false | Specifies the time interval at which the job is started. | -| concurrency | `number` | false | How many instances of a job are started at a time. | -| maxRunning | `number` | false | Maximum number of job executions that is allowed at a time. Set to 0 for no max. The job will not be started anymore if maxRunning is reached. | -| schedulerStatus | `{ interval: string, running: number }` | true | Only present if the job was started, reports the number of currently running executions of the job and the time interval at which job execution is triggered. This might differ from the top-level `interval` as the interval of an already started job is not changed automatically when the job is updated. | +| property | type | optional | description | +| --------------- | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | false | The name of the job. Used as a unique identifier. | +| interval | `string` | false | Specifies the time interval at which the job is started. | +| concurrency | `number` | false | How many instances of a job are started at a time. | +| maxRunning | `number` | false | Maximum number of job executions that is allowed at a time. Set to 0 for no max. The job will not be started anymore if maxRunning is reached. | +| schedulerStatus | `{ interval: string, running: number }` | true | Only present if the job was started, reports the number of currently running executions of the job and the time interval at which job execution is triggered. This might differ from the top-level `interval` as the interval of an already started job is not changed automatically when the job is updated. | The MongoSchedule is an EventEmitter, emitting `'debug'` and `'error'` events. You can define callbacks to handle them: @@ -117,12 +121,70 @@ mongoSchedule.on('debug', ({ data, message }: MomoEvent) => { ### MomoEvent and MomoErrorEvent -| event | property | type | description | -|-------|-------------------------|--------------------------|-------------| -| both | message | `string` | Some information about the event that occurred. | -| both | data (optional) | `{ name?: string; ... }` | Contains additional information like the name of the affected job. | -| error | type | `MomoErrorType` | `'defining job failed'` or `'scheduling job failed'` or `'executing job failed'` or `'stopping job failed'` | -| error | error (optional) | `Error` | The root cause of the error. | +| event | property | type | description | +| ----- | ---------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------- | +| both | message | `string` | Some information about the event that occurred. | +| both | data (optional) | `{ name?: string; ... }` | Contains additional information like the name of the affected job. | +| error | type | `MomoErrorType` | `'defining job failed'` or `'scheduling job failed'` or `'executing job failed'` or `'stopping job failed'` | +| error | error (optional) | `Error` | The root cause of the error. | + +### Job Examples + +``` +const example1: MomoJob = { + name: 'example 1', + interval: '5 minutes', + immediate: true, + handler: () => console.log('This is momo'), +}; + +const example2: MomoJob = { + name: 'example 2', + interval: '5 minutes', + immediate: false, + handler: () => console.log('This is momo'), +}; + +const example3: MomoJob = { + name: 'example 3', + interval: '5 minutes', + delay: 60 * 1000, // 1 minute + immediate: true, + handler: () => console.log('This is momo'), +}; + +const example4: MomoJob = { + name: 'example 4', + interval: '5 minutes', + delay: 60 * 1000, // 1 minute + immediate: false, + handler: () => console.log('This is momo'), +}; + +const example5: MomoJob = { + name: 'example 5', + interval: '5 minutes', + delay: 10 * 60 * 1000, // 10 minutes + immediate: true, + handler: () => console.log('This is momo'), +}; +``` + +Assume it is 12:00 AM when the MongoSchedule with these four example jobs is started. + +- `example 1` will be run immediately, at 12:00, and then every five minutes. +- `example 2` will be run after 5 minutes (the configured interval), at 12:05, and then every five minutes. +- `example 3` will be run after 1 minute (the configured delay), at 12:01, and then every five minutes. +- `example 4` will be run after 5 minutes (the configured interval), at 12:05, and then every five minutes. The configured delay is ignored because `immediate` is set to `false`. +- `example 5` will be run after 10 minutes (the configured delay), at 12:10, and then every five minutes. + +Now assume the MongoSchedule is stopped at 12:02 and then immediately started again. + +- `example 1` will be run after 5 minutes (the configured interval) after the last execution, at 12:05. The job is not run immediately because it already ran before. +- `example 2` will be run after 5 minutes (the configured interval) after the start, at 12:07. +- `example 3` will be run after 5 minutes (the configured interval) after the last execution, at 12:06. The job is not run immediately because it already ran before. +- `example 4` will be run after 5 minutes (the configured interval) after the start at 12:05. +- `example 5` will be run after 10 minutes (the configured delay), at 12:12, and then every five minutes. ## License From 86d916b81c64e7d0834720a23141c56b3bee84f5 Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 14:09:03 +0200 Subject: [PATCH 03/10] refactor: remove property immediate from MomoJob BREAKING CHANGE: use "delay: 0" instead of "immediate: true" or simply omit the delay instead of "immediate: false" Signed-off-by: Ute Weiss --- README.md | 22 +++------------- src/job/Job.ts | 5 ++-- src/job/MomoJob.ts | 1 - src/job/MomoJobBuilder.ts | 4 +-- src/job/validate.ts | 7 +++++- src/logging/error/MomoError.ts | 1 + src/repository/JobRepository.ts | 19 +++++++++++++- src/schedule/Schedule.ts | 1 + src/scheduler/JobScheduler.ts | 5 ++-- src/scheduler/calculateDelay.ts | 4 +-- test/executor/JobExecutor.spec.ts | 2 -- test/job/Job.spec.ts | 1 - test/job/MomoJobBuilder.spec.ts | 6 ++--- test/job/validate.spec.ts | 13 ++++++++++ test/schedule/momo.integration.spec.ts | 2 +- test/scheduler/JobScheduler.spec.ts | 9 +++---- test/scheduler/calculateDelay.spec.ts | 35 ++++++++++++++------------ 17 files changed, 77 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 030b57b3..7bfc229d 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,7 @@ await mongoSchedule.disconnect(); | ----------- | ---------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | `string` | false | | The name of the job. Used as a unique identifier. | | interval | `string` | false | | Specifies the time interval at which the job is started. Time intervals in human-readable formats (like '1 minute', 'ten days' or 'twenty-one days and 2 hours') are accepted. Check documentation of [human-interval](https://www.npmjs.com/package/human-interval) library for details. | -| immediate | `boolean` | true | `false` | If set to true AND the job was never run before, the job will be started after `delay` milliseconds. | -| delay | `number` | true | `0` | If immediate is set to true AND the job was never run before, the job will be started after `delay` milliseconds. | +| delay | `number` | true | | If set AND the job was never run before, first job execution will be started after `delay` milliseconds. Set to `0` to run the job immediately after the start. | | concurrency | `number` | true | `1` | How many instances of a job are started at a time. | | maxRunning | `number` | true | `0` | Maximum number of job executions that is allowed at a time. Set to 0 for no max. The schedule will trigger no more job executions if maxRunning is reached. However, there is no guarantee that the schedule always respects the limit; in rare cases with multiple Momo instances maxRunning may be exceeded. | | handler | `function` | false | | The function to execute. | @@ -134,14 +133,13 @@ mongoSchedule.on('debug', ({ data, message }: MomoEvent) => { const example1: MomoJob = { name: 'example 1', interval: '5 minutes', - immediate: true, + delay: 0, handler: () => console.log('This is momo'), }; const example2: MomoJob = { name: 'example 2', interval: '5 minutes', - immediate: false, handler: () => console.log('This is momo'), }; @@ -149,23 +147,13 @@ const example3: MomoJob = { name: 'example 3', interval: '5 minutes', delay: 60 * 1000, // 1 minute - immediate: true, handler: () => console.log('This is momo'), }; const example4: MomoJob = { name: 'example 4', interval: '5 minutes', - delay: 60 * 1000, // 1 minute - immediate: false, - handler: () => console.log('This is momo'), -}; - -const example5: MomoJob = { - name: 'example 5', - interval: '5 minutes', delay: 10 * 60 * 1000, // 10 minutes - immediate: true, handler: () => console.log('This is momo'), }; ``` @@ -175,16 +163,14 @@ Assume it is 12:00 AM when the MongoSchedule with these four example jobs is sta - `example 1` will be run immediately, at 12:00, and then every five minutes. - `example 2` will be run after 5 minutes (the configured interval), at 12:05, and then every five minutes. - `example 3` will be run after 1 minute (the configured delay), at 12:01, and then every five minutes. -- `example 4` will be run after 5 minutes (the configured interval), at 12:05, and then every five minutes. The configured delay is ignored because `immediate` is set to `false`. -- `example 5` will be run after 10 minutes (the configured delay), at 12:10, and then every five minutes. +- `example 4` will be run after 10 minutes (the configured delay), at 12:10, and then every five minutes. Now assume the MongoSchedule is stopped at 12:02 and then immediately started again. - `example 1` will be run after 5 minutes (the configured interval) after the last execution, at 12:05. The job is not run immediately because it already ran before. - `example 2` will be run after 5 minutes (the configured interval) after the start, at 12:07. - `example 3` will be run after 5 minutes (the configured interval) after the last execution, at 12:06. The job is not run immediately because it already ran before. -- `example 4` will be run after 5 minutes (the configured interval) after the start at 12:05. -- `example 5` will be run after 10 minutes (the configured delay), at 12:12, and then every five minutes. +- `example 4` will be run after 10 minutes (the configured delay), at 12:12, and then every five minutes. ## License diff --git a/src/job/Job.ts b/src/job/Job.ts index 64695202..84bf7e0b 100644 --- a/src/job/Job.ts +++ b/src/job/Job.ts @@ -8,18 +8,17 @@ export type MomoJobStatus = WithoutId; export interface JobDefinition { name: string; interval: string; - delay: number; + delay?: number; concurrency: number; maxRunning: number; } export interface Job extends JobDefinition { - immediate: boolean; handler: Handler; } export function toJob(job: MomoJob): Job { - return { delay: 0, immediate: false, concurrency: 1, maxRunning: 0, ...job }; + return { concurrency: 1, maxRunning: 0, ...job }; } export function toJobDefinition(job: T): JobDefinition { diff --git a/src/job/MomoJob.ts b/src/job/MomoJob.ts index ccc21280..c7813107 100644 --- a/src/job/MomoJob.ts +++ b/src/job/MomoJob.ts @@ -1,7 +1,6 @@ export type Handler = () => Promise | string | undefined | void; export interface MomoJob { - immediate?: boolean; handler: Handler; name: string; interval: string; diff --git a/src/job/MomoJobBuilder.ts b/src/job/MomoJobBuilder.ts index 6066ce25..80f9838e 100644 --- a/src/job/MomoJobBuilder.ts +++ b/src/job/MomoJobBuilder.ts @@ -13,8 +13,8 @@ export class MomoJobBuilder { return this; } - withImmediate(immediate: boolean): this { - this.momoJob.immediate = immediate; + withDelay(delay: number): this { + this.momoJob.delay = delay; return this; } diff --git a/src/job/validate.ts b/src/job/validate.ts index 1f3c3851..f78b24b3 100644 --- a/src/job/validate.ts +++ b/src/job/validate.ts @@ -5,7 +5,12 @@ import { Logger } from '../logging/Logger'; import { MomoErrorType } from '../logging/error/MomoErrorType'; import { momoError } from '../logging/error/MomoError'; -export function validate({ name, interval, concurrency, maxRunning }: Job, logger?: Logger): boolean { +export function validate({ name, interval, delay, concurrency, maxRunning }: Job, logger?: Logger): boolean { + if (delay && delay < 0) { + logger?.error('job cannot be defined', MomoErrorType.defineJob, { name, delay }, momoError.invalidDelay); + return false; + } + if (maxRunning < 0) { logger?.error('job cannot be defined', MomoErrorType.defineJob, { name, maxRunning }, momoError.invalidMaxRunning); return false; diff --git a/src/logging/error/MomoError.ts b/src/logging/error/MomoError.ts index 0a06ef61..936f6285 100644 --- a/src/logging/error/MomoError.ts +++ b/src/logging/error/MomoError.ts @@ -3,4 +3,5 @@ export const momoError = { invalidConcurrency: new Error('concurrency must be at least 1'), invalidMaxRunning: new Error('maxRunning must be at least 0'), jobNotFound: new Error('job not found in database'), + invalidDelay: new Error('delay must be at least 0'), }; diff --git a/src/repository/JobRepository.ts b/src/repository/JobRepository.ts index 27d07ed8..2a6e639e 100644 --- a/src/repository/JobRepository.ts +++ b/src/repository/JobRepository.ts @@ -1,4 +1,4 @@ -import { MongoClient } from 'mongodb'; +import { Filter, MongoClient } from 'mongodb'; import { ExecutionInfo } from '../job/ExecutionInfo'; import { Job, MomoJobStatus, toJobDefinition } from '../job/Job'; @@ -9,6 +9,13 @@ import { findLatest } from '../job/findLatest'; export const JOBS_COLLECTION_NAME = 'jobs'; +function mapNullToUndefined(entity: JobEntity) { + return { + ...entity, + delay: entity.delay === null ? undefined : entity.delay, + }; +} + export class JobRepository extends Repository { private logger: Logger | undefined; @@ -63,6 +70,16 @@ export class JobRepository extends Repository { return latest; } + async findOne(filter: Filter = {}): Promise { + const entity = await super.findOne(filter); + return entity ? mapNullToUndefined(entity) : undefined; + } + + async find(filter: Filter = {}): Promise { + const entities = await super.find(filter); + return entities.map(mapNullToUndefined); + } + async list(): Promise { const jobs = await this.find(); diff --git a/src/schedule/Schedule.ts b/src/schedule/Schedule.ts index beeefa27..c9eaca9a 100644 --- a/src/schedule/Schedule.ts +++ b/src/schedule/Schedule.ts @@ -1,4 +1,5 @@ import { sum } from 'lodash'; + import { ExecutionInfo, ExecutionStatus, JobResult } from '../job/ExecutionInfo'; import { ExecutionsRepository } from '../repository/ExecutionsRepository'; import { JobRepository } from '../repository/JobRepository'; diff --git a/src/scheduler/JobScheduler.ts b/src/scheduler/JobScheduler.ts index e01596e8..fa365281 100644 --- a/src/scheduler/JobScheduler.ts +++ b/src/scheduler/JobScheduler.ts @@ -21,7 +21,6 @@ export class JobScheduler { constructor( private readonly jobName: string, - private readonly immediate: boolean, private readonly jobExecutor: JobExecutor, private readonly scheduleId: string, private readonly executionsRepository: ExecutionsRepository, @@ -37,7 +36,7 @@ export class JobScheduler { jobRepository: JobRepository ): JobScheduler { const executor = new JobExecutor(job.handler, scheduleId, executionsRepository, jobRepository, logger); - return new JobScheduler(job.name, job.immediate, executor, scheduleId, executionsRepository, jobRepository, logger); + return new JobScheduler(job.name, executor, scheduleId, executionsRepository, jobRepository, logger); } getUnexpectedErrorCount(): number { @@ -87,7 +86,7 @@ export class JobScheduler { this.interval = jobEntity.interval; - const delay = calculateDelay(interval, this.immediate, jobEntity); + const delay = calculateDelay(interval, jobEntity); this.jobHandle = setIntervalWithDelay(this.executeConcurrently.bind(this), interval, delay); diff --git a/src/scheduler/calculateDelay.ts b/src/scheduler/calculateDelay.ts index 3d68715b..637e3367 100644 --- a/src/scheduler/calculateDelay.ts +++ b/src/scheduler/calculateDelay.ts @@ -3,10 +3,10 @@ import { max } from 'lodash'; import { JobEntity } from '../repository/JobEntity'; -export function calculateDelay(millisecondsInterval: number, immediate: boolean, job: JobEntity): number { +export function calculateDelay(millisecondsInterval: number, job: JobEntity): number { const nextStart = calculateNextStart(millisecondsInterval, job); if (nextStart === undefined) { - return immediate ? job.delay : millisecondsInterval; + return job.delay ?? millisecondsInterval; } return max([nextStart - DateTime.now().toMillis(), 0]) ?? 0; diff --git a/test/executor/JobExecutor.spec.ts b/test/executor/JobExecutor.spec.ts index 78ea5396..dfa115c4 100644 --- a/test/executor/JobExecutor.spec.ts +++ b/test/executor/JobExecutor.spec.ts @@ -14,8 +14,6 @@ describe('JobExecutor', () => { const job: Job = { name: 'test', interval: '1 minute', - delay: 0, - immediate: false, concurrency: 1, maxRunning: 0, handler, diff --git a/test/job/Job.spec.ts b/test/job/Job.spec.ts index d6898177..c0e9edb2 100644 --- a/test/job/Job.spec.ts +++ b/test/job/Job.spec.ts @@ -5,7 +5,6 @@ describe('fromMomoJob', () => { const job = { name: 'test', interval: '1 second', handler: () => undefined }; expect(toJob(job)).toMatchObject({ ...job, - immediate: false, concurrency: 1, maxRunning: 0, }); diff --git a/test/job/MomoJobBuilder.spec.ts b/test/job/MomoJobBuilder.spec.ts index 9dfe03e4..8a968a5e 100644 --- a/test/job/MomoJobBuilder.spec.ts +++ b/test/job/MomoJobBuilder.spec.ts @@ -5,7 +5,7 @@ describe('MomoJobBuilder', () => { const momoJob = new MomoJobBuilder() .withName('name') .withInterval('one minute') - .withImmediate(true) + .withDelay(0) .withConcurrency(1) .withMaxRunning(1) .withHandler(jest.fn()) @@ -13,7 +13,7 @@ describe('MomoJobBuilder', () => { expect(momoJob.name).toEqual('name'); expect(momoJob.interval).toEqual('one minute'); - expect(momoJob.immediate).toEqual(true); + expect(momoJob.delay).toEqual(0); expect(momoJob.concurrency).toEqual(1); expect(momoJob.maxRunning).toEqual(1); expect(momoJob.handler.toString()).toEqual(jest.fn().toString()); @@ -24,7 +24,7 @@ describe('MomoJobBuilder', () => { expect(momoJob.name).toEqual('name'); expect(momoJob.interval).toEqual('one minute'); - expect(momoJob.immediate).toBeUndefined(); + expect(momoJob.delay).toBeUndefined(); expect(momoJob.concurrency).toBeUndefined(); expect(momoJob.maxRunning).toBeUndefined(); expect(momoJob.handler.toString()).toEqual(jest.fn().toString()); diff --git a/test/job/validate.spec.ts b/test/job/validate.spec.ts index 1647781a..7e0a7a2b 100644 --- a/test/job/validate.spec.ts +++ b/test/job/validate.spec.ts @@ -42,6 +42,19 @@ describe('validate', () => { ); }); + it('reports error when delay is invalid', async () => { + const job: Job = toJob({ name: 'test', interval: '1 minute', handler: () => 'finished', delay: -1 }); + expect(validate(job, logger)).toBe(false); + + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + 'job cannot be defined', + MomoErrorType.defineJob, + { name: job.name, delay: -1 }, + momoError.invalidDelay + ); + }); + it('reports error when maxRunning is invalid', async () => { const job: Job = toJob({ name: 'test', interval: '1 minute', handler: () => 'finished', maxRunning: -1 }); expect(validate(job, logger)).toBe(false); diff --git a/test/schedule/momo.integration.spec.ts b/test/schedule/momo.integration.spec.ts index 781df2f6..e74a638b 100644 --- a/test/schedule/momo.integration.spec.ts +++ b/test/schedule/momo.integration.spec.ts @@ -104,7 +104,7 @@ describe('Momo', () => { }); it('executes an immediate job periodically', async () => { - await mongoSchedule.define({ ...momoJob, immediate: true }); + await mongoSchedule.define({ ...momoJob, delay: 0 }); await mongoSchedule.start(); diff --git a/test/scheduler/JobScheduler.spec.ts b/test/scheduler/JobScheduler.spec.ts index 6e43fa61..3466e52b 100644 --- a/test/scheduler/JobScheduler.spec.ts +++ b/test/scheduler/JobScheduler.spec.ts @@ -10,11 +10,9 @@ import { loggerForTests } from '../utils/logging'; import { sleep } from '../utils/sleep'; describe('JobScheduler', () => { - const defaultJob = { + const defaultJob: Job = { name: 'test', interval: '1 second', - delay: 0, - immediate: false, concurrency: 1, maxRunning: 0, handler: jest.fn(), @@ -42,7 +40,6 @@ describe('JobScheduler', () => { const job = { ...defaultJob, ...partialJob }; jobScheduler = new JobScheduler( job.name, - job.immediate, instance(jobExecutor), scheduleId, instance(executionsRepository), @@ -63,8 +60,8 @@ describe('JobScheduler', () => { verify(await jobExecutor.execute(anything())).once(); }); - it('executes an immediate job', async () => { - createJob({ immediate: true }); + it('executes a job with delay 0 immediately', async () => { + createJob({ delay: 0 }); await jobScheduler.start(); diff --git a/test/scheduler/calculateDelay.spec.ts b/test/scheduler/calculateDelay.spec.ts index 061dd26d..b10123ca 100644 --- a/test/scheduler/calculateDelay.spec.ts +++ b/test/scheduler/calculateDelay.spec.ts @@ -6,25 +6,20 @@ import { JobEntity } from '../../src/repository/JobEntity'; import { calculateDelay } from '../../src/scheduler/calculateDelay'; describe('calculateDelay', () => { - let job: JobEntity; - let clock: Clock; + const clock: Clock = install(); - beforeEach(() => { - job = { + afterAll(() => clock.uninstall()); + + describe('with undefined delay', () => { + const job: JobEntity = { name: 'test', interval: 'one second', - delay: 100, concurrency: 0, maxRunning: 1, }; - clock = install(); - }); - - afterEach(() => clock.uninstall()); - describe('immediate=false', () => { - it('calculates delay if job was never started before (ignores configured delay)', () => { - const delay = calculateDelay(1000, false, job); + it('calculates delay if job was never started before', () => { + const delay = calculateDelay(1000, job); expect(delay).toBe(1000); }); @@ -34,14 +29,22 @@ describe('calculateDelay', () => { clock.tick(500); - const delay = calculateDelay(1000, false, job); + const delay = calculateDelay(1000, job); expect(delay).toBe(500); }); }); - describe('immediate=true', () => { + describe('with delay', () => { + const job: JobEntity = { + name: 'test', + interval: 'one second', + delay: 500, + concurrency: 0, + maxRunning: 1, + }; + it('uses configured delay if job was never started before', () => { - const delay = calculateDelay(1000, true, job); + const delay = calculateDelay(1000, job); expect(delay).toBe(job.delay); }); @@ -51,7 +54,7 @@ describe('calculateDelay', () => { clock.tick(500); - const delay = calculateDelay(1000, true, job); + const delay = calculateDelay(1000, job); expect(delay).toBe(500); }); }); From fc14cf9f2c1599fb2ba75b1d1dbac23e7ccdab80 Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 14:16:55 +0200 Subject: [PATCH 04/10] fix: improve readme Signed-off-by: Ute Weiss --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bfc229d..8ee316ed 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ await mongoSchedule.disconnect(); | ----------- | ---------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | `string` | false | | The name of the job. Used as a unique identifier. | | interval | `string` | false | | Specifies the time interval at which the job is started. Time intervals in human-readable formats (like '1 minute', 'ten days' or 'twenty-one days and 2 hours') are accepted. Check documentation of [human-interval](https://www.npmjs.com/package/human-interval) library for details. | -| delay | `number` | true | | If set AND the job was never run before, first job execution will be started after `delay` milliseconds. Set to `0` to run the job immediately after the start. | +| delay | `number` | true | | If set AND the job was never run before, first job execution will be started after `delay` milliseconds. If set to `0`, the job will be run immediately after the start. If omitted, the job will be run after the `interval` has elapsed. | | concurrency | `number` | true | `1` | How many instances of a job are started at a time. | | maxRunning | `number` | true | `0` | Maximum number of job executions that is allowed at a time. Set to 0 for no max. The schedule will trigger no more job executions if maxRunning is reached. However, there is no guarantee that the schedule always respects the limit; in rare cases with multiple Momo instances maxRunning may be exceeded. | | handler | `function` | false | | The function to execute. | From 2e95630fddb0f4a4b74261e0915aeec1ebeb2e97 Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 14:31:14 +0200 Subject: [PATCH 05/10] fix: format and fix types null vs. undefined Signed-off-by: Ute Weiss --- src/job/validate.ts | 2 +- src/repository/JobRepository.ts | 4 +++- src/scheduler/calculateDelay.ts | 2 +- test/job/validate.spec.ts | 8 ++++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/job/validate.ts b/src/job/validate.ts index f78b24b3..8b346469 100644 --- a/src/job/validate.ts +++ b/src/job/validate.ts @@ -6,7 +6,7 @@ import { MomoErrorType } from '../logging/error/MomoErrorType'; import { momoError } from '../logging/error/MomoError'; export function validate({ name, interval, delay, concurrency, maxRunning }: Job, logger?: Logger): boolean { - if (delay && delay < 0) { + if (delay !== undefined && delay < 0) { logger?.error('job cannot be defined', MomoErrorType.defineJob, { name, delay }, momoError.invalidDelay); return false; } diff --git a/src/repository/JobRepository.ts b/src/repository/JobRepository.ts index 2a6e639e..420486ac 100644 --- a/src/repository/JobRepository.ts +++ b/src/repository/JobRepository.ts @@ -9,7 +9,9 @@ import { findLatest } from '../job/findLatest'; export const JOBS_COLLECTION_NAME = 'jobs'; -function mapNullToUndefined(entity: JobEntity) { +// mongodb returns null instead of undefined for optional fields +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function mapNullToUndefined(entity: any): JobEntity { return { ...entity, delay: entity.delay === null ? undefined : entity.delay, diff --git a/src/scheduler/calculateDelay.ts b/src/scheduler/calculateDelay.ts index 637e3367..e6d41657 100644 --- a/src/scheduler/calculateDelay.ts +++ b/src/scheduler/calculateDelay.ts @@ -6,7 +6,7 @@ import { JobEntity } from '../repository/JobEntity'; export function calculateDelay(millisecondsInterval: number, job: JobEntity): number { const nextStart = calculateNextStart(millisecondsInterval, job); if (nextStart === undefined) { - return job.delay ?? millisecondsInterval; + return job.delay ?? millisecondsInterval; } return max([nextStart - DateTime.now().toMillis(), 0]) ?? 0; diff --git a/test/job/validate.spec.ts b/test/job/validate.spec.ts index 7e0a7a2b..574f7809 100644 --- a/test/job/validate.spec.ts +++ b/test/job/validate.spec.ts @@ -48,10 +48,10 @@ describe('validate', () => { expect(logger.error).toHaveBeenCalledTimes(1); expect(logger.error).toHaveBeenCalledWith( - 'job cannot be defined', - MomoErrorType.defineJob, - { name: job.name, delay: -1 }, - momoError.invalidDelay + 'job cannot be defined', + MomoErrorType.defineJob, + { name: job.name, delay: -1 }, + momoError.invalidDelay ); }); From d70e5ad703afac610d6921b04209d688aced2556 Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 14:52:48 +0200 Subject: [PATCH 06/10] refactor: move mapping null to undefined workaround to generic repository Signed-off-by: Ute Weiss --- src/repository/JobRepository.ts | 21 +-------------------- src/repository/Repository.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/repository/JobRepository.ts b/src/repository/JobRepository.ts index 420486ac..27d07ed8 100644 --- a/src/repository/JobRepository.ts +++ b/src/repository/JobRepository.ts @@ -1,4 +1,4 @@ -import { Filter, MongoClient } from 'mongodb'; +import { MongoClient } from 'mongodb'; import { ExecutionInfo } from '../job/ExecutionInfo'; import { Job, MomoJobStatus, toJobDefinition } from '../job/Job'; @@ -9,15 +9,6 @@ import { findLatest } from '../job/findLatest'; export const JOBS_COLLECTION_NAME = 'jobs'; -// mongodb returns null instead of undefined for optional fields -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function mapNullToUndefined(entity: any): JobEntity { - return { - ...entity, - delay: entity.delay === null ? undefined : entity.delay, - }; -} - export class JobRepository extends Repository { private logger: Logger | undefined; @@ -72,16 +63,6 @@ export class JobRepository extends Repository { return latest; } - async findOne(filter: Filter = {}): Promise { - const entity = await super.findOne(filter); - return entity ? mapNullToUndefined(entity) : undefined; - } - - async find(filter: Filter = {}): Promise { - const entities = await super.find(filter); - return entities.map(mapNullToUndefined); - } - async list(): Promise { const jobs = await this.find(); diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 2529675c..3d03fd73 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -18,11 +18,13 @@ export class Repository { } async find(filter: Filter = {}): Promise { - return this.collection.find(filter).toArray(); + const entities = await this.collection.find(filter).toArray(); + return entities.map(this.mapNullToUndefined); } async findOne(filter: Filter = {}): Promise { - return this.collection.findOne(filter); + const entity = await this.collection.findOne(filter); + return entity === undefined ? undefined : this.mapNullToUndefined(entity); } async delete(filter: Filter = {}): Promise { @@ -33,4 +35,12 @@ export class Repository { async deleteOne(filter: Filter): Promise { await this.collection.deleteOne(filter); } + + // mongodb returns null instead of undefined for optional fields + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private mapNullToUndefined(entity: any): ENTITY { + const keys = Object.keys(entity); + const entries = keys.map((key) => [key, entity[key] ?? undefined]); + return Object.fromEntries(entries) as ENTITY; + } } From 2b801ae1bcab63d937c87dec97d10cc7ceac1261 Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 15:38:24 +0200 Subject: [PATCH 07/10] ci!: drop node 10 support Signed-off-by: Ute Weiss --- .github/workflows/ci.yml | 4 ++-- README.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39b1155a..ebfe3918 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node: [10, 12, 14, 16] + node: [12, 14, 16] steps: - name: Checkout the repository @@ -42,7 +42,7 @@ jobs: strategy: matrix: - node: [10, 12, 14, 16] + node: [12, 14, 16] steps: - name: Checkout the repository diff --git a/README.md b/README.md index 8ee316ed..a0223b2c 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,10 @@ Now assume the MongoSchedule is stopped at 12:02 and then immediately started ag - `example 3` will be run after 5 minutes (the configured interval) after the last execution, at 12:06. The job is not run immediately because it already ran before. - `example 4` will be run after 10 minutes (the configured delay), at 12:12, and then every five minutes. +## Supported Node Versions + +momo-scheduler supports node 12, 14 and 16. + ## License This project is open source and licensed under [Apache 2.0](LICENSE). From d8d1ec5e412d4fcf673db0f5e75c156bcf43b5df Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 15:39:43 +0200 Subject: [PATCH 08/10] chore: update to mongodb version 4.1.1 Signed-off-by: Ute Weiss --- package-lock.json | 38 ++++++++++++++++++++++++------------ package.json | 2 +- src/repository/Repository.ts | 2 +- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3bb3013..f69ddbcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1548,9 +1548,9 @@ } }, "bson": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-4.5.1.tgz", - "integrity": "sha512-XqFP74pbTVLyLy5KFxVfTUyRrC1mgOlmu/iXHfXqfCKT59jyP9lwbotGfbN59cHBRbJSamZNkrSopjv+N0SqAA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.5.2.tgz", + "integrity": "sha512-8CEMJpwc7qlQtrn2rney38jQSEeMar847lz0LyitwRmVknAW8iHXrzW4fTjHfyWm0E3sukyD/zppdH+QU1QefA==", "requires": { "buffer": "^5.6.0" } @@ -4063,23 +4063,34 @@ "dev": true }, "mongodb": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.0.tgz", - "integrity": "sha512-Gx9U9MsFWgJ3E0v4oHAdWvYTGBznNYPCkhmD/3i/kPTY/URnPfHD5/6VoKUFrdgQTK3icFiM9976hVbqCRBO9Q==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.1.tgz", + "integrity": "sha512-fbACrWEyvr6yl0sSiCGV0sqEiBwTtDJ8iSojmkDjAfw9JnOZSAkUyv9seFSPYhPPKwxp1PDtyjvBNfMDz0WBLQ==", "requires": { - "bson": "^4.4.0", + "bson": "^4.5.1", "denque": "^1.5.0", - "mongodb-connection-string-url": "^1.0.1", + "mongodb-connection-string-url": "^2.0.0", "saslprep": "^1.0.0" } }, "mongodb-connection-string-url": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-1.1.2.tgz", - "integrity": "sha512-mp5lv4guWuykOpkwNNqQ0tKKytuJUjL/aC/bu/DqoJVWL5NSh4j/u+gJ+EiOdweLujHyq6JZZqcTVipHhL5xRg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.1.0.tgz", + "integrity": "sha512-Qf9Zw7KGiRljWvMrrUFDdVqo46KIEiDuCzvEN97rh/PcKzk2bd6n9KuzEwBwW9xo5glwx69y1mI6s+jFUD/aIQ==", "requires": { - "@types/whatwg-url": "^8.0.0", - "whatwg-url": "^8.4.0" + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^9.1.0" + }, + "dependencies": { + "whatwg-url": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz", + "integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==", + "requires": { + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + } } }, "mongodb-memory-server": { @@ -5450,6 +5461,7 @@ "version": "8.7.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, "requires": { "lodash": "^4.7.0", "tr46": "^2.1.0", diff --git a/package.json b/package.json index 60c08d41..b10b9c0a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "human-interval": "2.0.1", "lodash": "4.17.21", "luxon": "2.0.2", - "mongodb": "4.1.0", + "mongodb": "4.1.1", "typed-emitter": "1.3.1", "uuid": "8.3.2" }, diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 3d03fd73..ef98ad7f 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -24,7 +24,7 @@ export class Repository { async findOne(filter: Filter = {}): Promise { const entity = await this.collection.findOne(filter); - return entity === undefined ? undefined : this.mapNullToUndefined(entity); + return entity === null ? undefined : this.mapNullToUndefined(entity); } async delete(filter: Filter = {}): Promise { From b6d3145b2865d1e2dfbaa2acfad160f87e3e7f7f Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 16:06:37 +0200 Subject: [PATCH 09/10] refactor: rename delay to firstRunAfter Signed-off-by: Ute Weiss --- README.md | 16 ++++++++-------- src/job/Job.ts | 4 ++-- src/job/MomoJob.ts | 2 +- src/job/MomoJobBuilder.ts | 4 ++-- src/job/validate.ts | 11 ++++++++--- src/logging/error/MomoError.ts | 2 +- src/scheduler/calculateDelay.ts | 2 +- test/job/MomoJobBuilder.spec.ts | 6 +++--- test/job/validate.spec.ts | 8 ++++---- .../repository/JobRepository.integration.spec.ts | 8 ++++---- test/schedule/momo.integration.spec.ts | 2 +- test/scheduler/JobScheduler.spec.ts | 4 ++-- test/scheduler/calculateDelay.spec.ts | 4 ++-- 13 files changed, 39 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index a0223b2c..e8ce916d 100644 --- a/README.md +++ b/README.md @@ -58,14 +58,14 @@ await mongoSchedule.disconnect(); ### MomoJob -| property | type | optional | default | description | -| ----------- | ---------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | `string` | false | | The name of the job. Used as a unique identifier. | -| interval | `string` | false | | Specifies the time interval at which the job is started. Time intervals in human-readable formats (like '1 minute', 'ten days' or 'twenty-one days and 2 hours') are accepted. Check documentation of [human-interval](https://www.npmjs.com/package/human-interval) library for details. | -| delay | `number` | true | | If set AND the job was never run before, first job execution will be started after `delay` milliseconds. If set to `0`, the job will be run immediately after the start. If omitted, the job will be run after the `interval` has elapsed. | -| concurrency | `number` | true | `1` | How many instances of a job are started at a time. | -| maxRunning | `number` | true | `0` | Maximum number of job executions that is allowed at a time. Set to 0 for no max. The schedule will trigger no more job executions if maxRunning is reached. However, there is no guarantee that the schedule always respects the limit; in rare cases with multiple Momo instances maxRunning may be exceeded. | -| handler | `function` | false | | The function to execute. | +| property | type | optional | default | description | +| ------------- | ---------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | false | | The name of the job. Used as a unique identifier. | +| interval | `string` | false | | Specifies the time interval at which the job is started. Time intervals in human-readable formats (like '1 minute', 'ten days' or 'twenty-one days and 2 hours') are accepted. Check documentation of [human-interval](https://www.npmjs.com/package/human-interval) library for details. | +| firstRunAfter | `number` | true | | If set AND the job was never run before, the job will be run after `firstRunAfter` milliseconds for the first time. If set to `0`, the job will be run immediately once the MongoSchedule is started. If omitted, the job will be run after the `interval` has elapsed. | +| concurrency | `number` | true | `1` | How many instances of a job are started at a time. | +| maxRunning | `number` | true | `0` | Maximum number of job executions that is allowed at a time. Set to 0 for no max. The schedule will trigger no more job executions if maxRunning is reached. However, there is no guarantee that the schedule always respects the limit; in rare cases with multiple Momo instances maxRunning may be exceeded. | +| handler | `function` | false | | The function to execute. | ### MongoSchedule diff --git a/src/job/Job.ts b/src/job/Job.ts index 84bf7e0b..e2ed45f3 100644 --- a/src/job/Job.ts +++ b/src/job/Job.ts @@ -8,7 +8,7 @@ export type MomoJobStatus = WithoutId; export interface JobDefinition { name: string; interval: string; - delay?: number; + firstRunAfter?: number; concurrency: number; maxRunning: number; } @@ -25,7 +25,7 @@ export function toJobDefinition(job: T): JobDefinition return { name: job.name, interval: job.interval, - delay: job.delay, + firstRunAfter: job.firstRunAfter, maxRunning: job.maxRunning, concurrency: job.concurrency, }; diff --git a/src/job/MomoJob.ts b/src/job/MomoJob.ts index c7813107..01441cf2 100644 --- a/src/job/MomoJob.ts +++ b/src/job/MomoJob.ts @@ -4,7 +4,7 @@ export interface MomoJob { handler: Handler; name: string; interval: string; - delay?: number; + firstRunAfter?: number; concurrency?: number; maxRunning?: number; } diff --git a/src/job/MomoJobBuilder.ts b/src/job/MomoJobBuilder.ts index 80f9838e..20d6bc07 100644 --- a/src/job/MomoJobBuilder.ts +++ b/src/job/MomoJobBuilder.ts @@ -13,8 +13,8 @@ export class MomoJobBuilder { return this; } - withDelay(delay: number): this { - this.momoJob.delay = delay; + withFirstRunAfter(firstRunAfter: number): this { + this.momoJob.firstRunAfter = firstRunAfter; return this; } diff --git a/src/job/validate.ts b/src/job/validate.ts index 8b346469..de6f2bee 100644 --- a/src/job/validate.ts +++ b/src/job/validate.ts @@ -5,9 +5,14 @@ import { Logger } from '../logging/Logger'; import { MomoErrorType } from '../logging/error/MomoErrorType'; import { momoError } from '../logging/error/MomoError'; -export function validate({ name, interval, delay, concurrency, maxRunning }: Job, logger?: Logger): boolean { - if (delay !== undefined && delay < 0) { - logger?.error('job cannot be defined', MomoErrorType.defineJob, { name, delay }, momoError.invalidDelay); +export function validate({ name, interval, firstRunAfter, concurrency, maxRunning }: Job, logger?: Logger): boolean { + if (firstRunAfter !== undefined && firstRunAfter < 0) { + logger?.error( + 'job cannot be defined', + MomoErrorType.defineJob, + { name, firstRunAfter }, + momoError.invalidFirstRunAfter + ); return false; } diff --git a/src/logging/error/MomoError.ts b/src/logging/error/MomoError.ts index 936f6285..ecca88c5 100644 --- a/src/logging/error/MomoError.ts +++ b/src/logging/error/MomoError.ts @@ -3,5 +3,5 @@ export const momoError = { invalidConcurrency: new Error('concurrency must be at least 1'), invalidMaxRunning: new Error('maxRunning must be at least 0'), jobNotFound: new Error('job not found in database'), - invalidDelay: new Error('delay must be at least 0'), + invalidFirstRunAfter: new Error('firstRunAfter must be at least 0'), }; diff --git a/src/scheduler/calculateDelay.ts b/src/scheduler/calculateDelay.ts index e6d41657..c25bf7b0 100644 --- a/src/scheduler/calculateDelay.ts +++ b/src/scheduler/calculateDelay.ts @@ -6,7 +6,7 @@ import { JobEntity } from '../repository/JobEntity'; export function calculateDelay(millisecondsInterval: number, job: JobEntity): number { const nextStart = calculateNextStart(millisecondsInterval, job); if (nextStart === undefined) { - return job.delay ?? millisecondsInterval; + return job.firstRunAfter ?? millisecondsInterval; } return max([nextStart - DateTime.now().toMillis(), 0]) ?? 0; diff --git a/test/job/MomoJobBuilder.spec.ts b/test/job/MomoJobBuilder.spec.ts index 8a968a5e..00e0b4a9 100644 --- a/test/job/MomoJobBuilder.spec.ts +++ b/test/job/MomoJobBuilder.spec.ts @@ -5,7 +5,7 @@ describe('MomoJobBuilder', () => { const momoJob = new MomoJobBuilder() .withName('name') .withInterval('one minute') - .withDelay(0) + .withFirstRunAfter(0) .withConcurrency(1) .withMaxRunning(1) .withHandler(jest.fn()) @@ -13,7 +13,7 @@ describe('MomoJobBuilder', () => { expect(momoJob.name).toEqual('name'); expect(momoJob.interval).toEqual('one minute'); - expect(momoJob.delay).toEqual(0); + expect(momoJob.firstRunAfter).toEqual(0); expect(momoJob.concurrency).toEqual(1); expect(momoJob.maxRunning).toEqual(1); expect(momoJob.handler.toString()).toEqual(jest.fn().toString()); @@ -24,7 +24,7 @@ describe('MomoJobBuilder', () => { expect(momoJob.name).toEqual('name'); expect(momoJob.interval).toEqual('one minute'); - expect(momoJob.delay).toBeUndefined(); + expect(momoJob.firstRunAfter).toBeUndefined(); expect(momoJob.concurrency).toBeUndefined(); expect(momoJob.maxRunning).toBeUndefined(); expect(momoJob.handler.toString()).toEqual(jest.fn().toString()); diff --git a/test/job/validate.spec.ts b/test/job/validate.spec.ts index 574f7809..f4676e27 100644 --- a/test/job/validate.spec.ts +++ b/test/job/validate.spec.ts @@ -42,16 +42,16 @@ describe('validate', () => { ); }); - it('reports error when delay is invalid', async () => { - const job: Job = toJob({ name: 'test', interval: '1 minute', handler: () => 'finished', delay: -1 }); + it('reports error when firstRunAfter is invalid', async () => { + const job: Job = toJob({ name: 'test', interval: '1 minute', handler: () => 'finished', firstRunAfter: -1 }); expect(validate(job, logger)).toBe(false); expect(logger.error).toHaveBeenCalledTimes(1); expect(logger.error).toHaveBeenCalledWith( 'job cannot be defined', MomoErrorType.defineJob, - { name: job.name, delay: -1 }, - momoError.invalidDelay + { name: job.name, firstRunAfter: -1 }, + momoError.invalidFirstRunAfter ); }); diff --git a/test/repository/JobRepository.integration.spec.ts b/test/repository/JobRepository.integration.spec.ts index c2ca1a75..b3f80474 100644 --- a/test/repository/JobRepository.integration.spec.ts +++ b/test/repository/JobRepository.integration.spec.ts @@ -95,7 +95,7 @@ describe('JobRepository', () => { const job1: JobEntity = { name: 'job1', interval: '1 minute', - delay: 0, + firstRunAfter: 0, executionInfo: {} as ExecutionInfo, concurrency: 1, maxRunning: 3, @@ -103,7 +103,7 @@ describe('JobRepository', () => { const job2: JobEntity = { name: 'job2', interval: '2 minutes', - delay: 0, + firstRunAfter: 0, executionInfo: {} as ExecutionInfo, concurrency: 1, maxRunning: 0, @@ -117,7 +117,7 @@ describe('JobRepository', () => { { name: job1.name, interval: job1.interval, - delay: job1.delay, + firstRunAfter: job1.firstRunAfter, concurrency: job1.concurrency, maxRunning: job1.maxRunning, executionInfo: {}, @@ -125,7 +125,7 @@ describe('JobRepository', () => { { name: job2.name, interval: job2.interval, - delay: job1.delay, + firstRunAfter: job1.firstRunAfter, concurrency: job2.concurrency, maxRunning: job2.maxRunning, executionInfo: {}, diff --git a/test/schedule/momo.integration.spec.ts b/test/schedule/momo.integration.spec.ts index e74a638b..97f1e365 100644 --- a/test/schedule/momo.integration.spec.ts +++ b/test/schedule/momo.integration.spec.ts @@ -104,7 +104,7 @@ describe('Momo', () => { }); it('executes an immediate job periodically', async () => { - await mongoSchedule.define({ ...momoJob, delay: 0 }); + await mongoSchedule.define({ ...momoJob, firstRunAfter: 0 }); await mongoSchedule.start(); diff --git a/test/scheduler/JobScheduler.spec.ts b/test/scheduler/JobScheduler.spec.ts index 3466e52b..7bc389f5 100644 --- a/test/scheduler/JobScheduler.spec.ts +++ b/test/scheduler/JobScheduler.spec.ts @@ -60,8 +60,8 @@ describe('JobScheduler', () => { verify(await jobExecutor.execute(anything())).once(); }); - it('executes a job with delay 0 immediately', async () => { - createJob({ delay: 0 }); + it('executes a job with firstRunAfter=0 immediately', async () => { + createJob({ firstRunAfter: 0 }); await jobScheduler.start(); diff --git a/test/scheduler/calculateDelay.spec.ts b/test/scheduler/calculateDelay.spec.ts index b10123ca..4d3c5d8c 100644 --- a/test/scheduler/calculateDelay.spec.ts +++ b/test/scheduler/calculateDelay.spec.ts @@ -38,7 +38,7 @@ describe('calculateDelay', () => { const job: JobEntity = { name: 'test', interval: 'one second', - delay: 500, + firstRunAfter: 500, concurrency: 0, maxRunning: 1, }; @@ -46,7 +46,7 @@ describe('calculateDelay', () => { it('uses configured delay if job was never started before', () => { const delay = calculateDelay(1000, job); - expect(delay).toBe(job.delay); + expect(delay).toBe(job.firstRunAfter); }); it('calculates delay based on lastStarted', () => { From 5766babe5a86b3bf578b72efaba4326c7f03b6ae Mon Sep 17 00:00:00 2001 From: Ute Weiss Date: Wed, 29 Sep 2021 16:19:06 +0200 Subject: [PATCH 10/10] fix: fix wording in readme Signed-off-by: Ute Weiss --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e8ce916d..b0c7b407 100644 --- a/README.md +++ b/README.md @@ -167,9 +167,9 @@ Assume it is 12:00 AM when the MongoSchedule with these four example jobs is sta Now assume the MongoSchedule is stopped at 12:02 and then immediately started again. -- `example 1` will be run after 5 minutes (the configured interval) after the last execution, at 12:05. The job is not run immediately because it already ran before. -- `example 2` will be run after 5 minutes (the configured interval) after the start, at 12:07. -- `example 3` will be run after 5 minutes (the configured interval) after the last execution, at 12:06. The job is not run immediately because it already ran before. +- `example 1` will be run 5 minutes (the configured interval) after the last execution, at 12:05. The job is not run immediately because it already ran before. +- `example 2` will be run 5 minutes (the configured interval) after the start, at 12:07. +- `example 3` will be run 5 minutes (the configured interval) after the last execution, at 12:06. The job is not run immediately because it already ran before. - `example 4` will be run after 10 minutes (the configured delay), at 12:12, and then every five minutes. ## Supported Node Versions