1351b6b82a
* feat(server task scheduler): sketch out core task scheduler implementation * feat(server weekly activity digests): add function lock duration to the weekly digest execution * feat(server scheduled tasks): add scheduled tasks type definition, db schema and migration * feat(server scheduled tasks): add scheduled tasks repository * feat(server task scheduler): add task scheduler service implementation * chore(server deps): add mocha type definitions * refactor(server scheduled tasks): refactor scheduled tasks migration * refactor(server scheduled tasks): refactor scheduled task db schema and type definitions * feat(server scheduled tasks): implement db side lock acquire * refactor(server scheduled tasks): refactor task scheduler with lock on query mechanism * test(server scheduled tasks): add tests for scheduled tasks implementation * refactor(server weekly activity digests): refactor to new task scheduler implementation * feat(server weekly activity digest): switch to a 1000 seconds trigger period for testing purposes * fix(server task scheduler): fix not catching lock acquire function errors Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
123 lines
4.4 KiB
TypeScript
123 lines
4.4 KiB
TypeScript
import { describe } from 'mocha'
|
|
import { ScheduledTasks } from '@/modules/core/dbSchema'
|
|
import { truncateTables } from '@/test/hooks'
|
|
import { acquireTaskLock } from '@/modules/core/repositories/scheduledTasks'
|
|
import { ensureError } from '@/modules/shared/helpers/errorHelper'
|
|
import {
|
|
scheduledCallbackWrapper,
|
|
scheduleExecution
|
|
} from '@/modules/core/services/taskScheduler'
|
|
import { expect } from 'chai'
|
|
import { sleep } from '@/test/helpers'
|
|
import cryptoRandomString from 'crypto-random-string'
|
|
|
|
describe('Scheduled tasks @core', () => {
|
|
describe('Task lock repository', () => {
|
|
before(async () => {
|
|
await truncateTables([ScheduledTasks.name])
|
|
})
|
|
it('can acquire task lock for a new function name', async () => {
|
|
const taskName = cryptoRandomString({ length: 10 })
|
|
const scheduledTask = { taskName, lockExpiresAt: new Date() }
|
|
const lock = await acquireTaskLock(scheduledTask)
|
|
expect(lock).to.be.deep.equal(scheduledTask)
|
|
})
|
|
it('can acquire task lock if previous lock has expired', async () => {
|
|
const taskName = cryptoRandomString({ length: 10 })
|
|
const oldTask = { taskName, lockExpiresAt: new Date() }
|
|
await acquireTaskLock(oldTask)
|
|
|
|
await sleep(100)
|
|
const newTask = { taskName, lockExpiresAt: new Date() }
|
|
const lock = await acquireTaskLock(newTask)
|
|
expect(lock).to.be.deep.equal(newTask)
|
|
})
|
|
it('returns an invalid lock (null), if there is another lock in place', async () => {
|
|
const taskName = cryptoRandomString({ length: 10 })
|
|
const oldTask = {
|
|
taskName,
|
|
lockExpiresAt: new Date('2366-12-28 00:30:57.000+00')
|
|
}
|
|
await acquireTaskLock(oldTask)
|
|
const newTask = { taskName, lockExpiresAt: new Date() }
|
|
const lock = await acquireTaskLock(newTask)
|
|
expect(lock).to.be.null
|
|
})
|
|
})
|
|
describe('Task scheduler', () => {
|
|
describe('scheduled callback wrapper function', () => {
|
|
let callbackExecuted = false
|
|
async function fakeCallback() {
|
|
callbackExecuted = true
|
|
}
|
|
beforeEach(() => {
|
|
callbackExecuted = false
|
|
})
|
|
it("doesn't invoke the callback if it aquires an invalid lock", async () => {
|
|
expect(callbackExecuted).to.be.false
|
|
const taskName = cryptoRandomString({ length: 10 })
|
|
await scheduledCallbackWrapper(
|
|
new Date(),
|
|
taskName,
|
|
100,
|
|
fakeCallback,
|
|
// fake lock aquire, always returning an invalid lock
|
|
async () => null
|
|
)
|
|
expect(callbackExecuted).to.be.false
|
|
})
|
|
it('invokes the callback if a task lock is acquired', async () => {
|
|
expect(callbackExecuted).to.be.false
|
|
const taskName = cryptoRandomString({ length: 10 })
|
|
await scheduledCallbackWrapper(
|
|
new Date(),
|
|
taskName,
|
|
100,
|
|
fakeCallback,
|
|
// fake lock aquire, always returning an invalid lock
|
|
async () => ({ taskName, lockExpiresAt: new Date() })
|
|
)
|
|
expect(callbackExecuted).to.be.true
|
|
})
|
|
it('handles all callback errors gracefully', async () => {
|
|
expect(callbackExecuted).to.be.false
|
|
const taskName = cryptoRandomString({ length: 10 })
|
|
await scheduledCallbackWrapper(
|
|
new Date(),
|
|
taskName,
|
|
100,
|
|
async () => {
|
|
callbackExecuted = true
|
|
throw 'catch this'
|
|
},
|
|
// fake lock aquire, always returning an invalid lock
|
|
async () => ({ taskName, lockExpiresAt: new Date() })
|
|
)
|
|
expect(callbackExecuted).to.be.true
|
|
})
|
|
})
|
|
describe('schedule execution', () => {
|
|
it('throws an InvalidArgimentError if the cron expression is not valid', async () => {
|
|
const cronExpression = 'this is a borked cron expression'
|
|
try {
|
|
scheduleExecution(cronExpression, 'tick tick boom', async () => {
|
|
return
|
|
})
|
|
throw new Error('this should have ')
|
|
} catch (err) {
|
|
expect(ensureError(err).message).to.equal(
|
|
`The given cron expression ${cronExpression} is not valid`
|
|
)
|
|
}
|
|
})
|
|
it('returns a cron scheduled task instance if the config is valid', async () => {
|
|
const cronExpression = '*/1000 * * * *'
|
|
const task = scheduleExecution(cronExpression, 'tick tick boom', async () => {
|
|
return
|
|
})
|
|
expect(task).to.not.be.null
|
|
})
|
|
})
|
|
})
|
|
})
|