diff --git a/README.md b/README.md index 9522915..98a6d69 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Backend for movie-web - [X] catpcha support - [X] global namespacing (accounts are stored on a namespace) - [ ] cleanup jobs - - [ ] cleanup expired sessions + - [X] cleanup expired sessions - [ ] cleanup old provider metrics ## Second todo list diff --git a/package.json b/package.json index 942664f..c32e0f8 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@fastify/cors": "^8.3.0", "@mikro-orm/core": "^5.9.0", "@mikro-orm/postgresql": "^5.9.0", + "cron": "^3.1.5", "fastify": "^4.21.0", "fastify-metrics": "^10.3.2", "fastify-type-provider-zod": "^1.1.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa5589a..4362c36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@mikro-orm/postgresql': specifier: ^5.9.0 version: 5.9.0(@mikro-orm/core@5.9.0) + cron: + specifier: ^3.1.5 + version: 3.1.5 fastify: specifier: ^4.21.0 version: 4.24.3 @@ -432,6 +435,10 @@ packages: '@types/node': 20.8.9 dev: true + /@types/luxon@3.3.3: + resolution: {integrity: sha512-/BJF3NT0pRMuxrenr42emRUF67sXwcZCd+S1ksG/Fcf9O7C3kKCY4uJSbKBE4KDUIYr3WMsvfmWD8hRjXExBJQ==} + dev: false + /@types/node@20.8.9: resolution: {integrity: sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==} dependencies: @@ -892,6 +899,13 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true + /cron@3.1.5: + resolution: {integrity: sha512-e/ivHUhSZVvF5PUqgj7dzQ96KqAhK1/peMDr5Mmfm/vEho01/O+ySJnhTBJ2JPvFEWXpjLESIJBke0ZpZ7r7FA==} + dependencies: + '@types/luxon': 3.3.3 + luxon: 3.4.3 + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1830,6 +1844,11 @@ packages: dependencies: yallist: 4.0.0 + /luxon@3.4.3: + resolution: {integrity: sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==} + engines: {node: '>=12'} + dev: false + /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true diff --git a/src/main.ts b/src/main.ts index bb51828..697e168 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ import { setupFastify, startFastify } from '@/modules/fastify'; +import { setupJobs } from '@/modules/jobs'; import { setupMetrics } from '@/modules/metrics'; import { setupMikroORM } from '@/modules/mikro'; import { scopedLogger } from '@/services/logger'; @@ -13,6 +14,7 @@ async function bootstrap(): Promise { const app = await setupFastify(); await setupMikroORM(); await setupMetrics(app); + await setupJobs(); await startFastify(app); diff --git a/src/modules/jobs/index.ts b/src/modules/jobs/index.ts new file mode 100644 index 0000000..9b1ecdf --- /dev/null +++ b/src/modules/jobs/index.ts @@ -0,0 +1,5 @@ +import { sessionExpiryJob } from '@/modules/jobs/list/sessionExpiry'; + +export async function setupJobs() { + sessionExpiryJob.start(); +} diff --git a/src/modules/jobs/job.ts b/src/modules/jobs/job.ts new file mode 100644 index 0000000..30c7b5b --- /dev/null +++ b/src/modules/jobs/job.ts @@ -0,0 +1,43 @@ +import { getORM } from '@/modules/mikro'; +import { scopedLogger } from '@/services/logger'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { CronJob } from 'cron'; + +const minOffset = 0; +const maxOffset = 60 * 4; +const secondsOffset = + Math.floor(Math.random() * (maxOffset - minOffset)) + minOffset; + +const log = scopedLogger('jobs'); + +const wait = (sec: number) => + new Promise((resolve) => { + setTimeout(() => resolve(), sec * 1000); + }); + +/** + * @param cron crontime in this order: (min of hour) (hour of day) (day of month) (day of week) (sec of month) + */ +export function job( + cron: string, + cb: (ctx: { em: EntityManager }) => Promise, +): CronJob { + return CronJob.from({ + cronTime: cron, + onTick: async () => { + // offset by random amount of seconds, just to prevent jobs running at + // the same time when running multiple instances + await wait(secondsOffset); + + // actually run the job + try { + const em = getORM().em.fork(); + await cb({ em }); + } catch (err) { + log.error('Failed to run job!'); + log.error(err); + } + }, + start: false, + }); +} diff --git a/src/modules/jobs/list/sessionExpiry.ts b/src/modules/jobs/list/sessionExpiry.ts new file mode 100644 index 0000000..5ab5de1 --- /dev/null +++ b/src/modules/jobs/list/sessionExpiry.ts @@ -0,0 +1,15 @@ +import { Session } from '@/db/models/Session'; +import { job } from '@/modules/jobs/job'; + +// every day at 12:00:00 +export const sessionExpiryJob = job('0 12 * * *', async ({ em }) => { + await em + .createQueryBuilder(Session) + .delete() + .where({ + expiresAt: { + $lt: new Date(), + }, + }) + .execute(); +});