Files
TREK/server/src/db/repositories/SchedulerLeases.repository.ts
T
Maurice 68504b337f refactor(scheduling): take a cron lease in one conditional upsert
acquire() made sure a row existed and then raced on a conditional UPDATE,
two statements the registrar wrapped in a transaction of its own. lint:tx
cannot follow a repository reached through a local variable, so it
reported the method. One INSERT ... ON CONFLICT DO UPDATE ... WHERE now
takes a free lease, renews the holder's and leaves anyone else's alone,
atomically on SQLite and on Postgres, and needs no transaction.
2026-10-08 19:05:03 +02:00

44 lines
2.0 KiB
TypeScript

import type { SchedulerLeases } from '../entities/SchedulerLeases.entity';
import { TrekRepository } from './_shared/trek-repository';
import type { DB } from '../kysely/db';
/**
* `scheduler_leases`: which process may run a cron job's tick, and until when
* (`CronRegistrarService`). A lease is free when no row exists yet, when its
* `expires_at` has passed, or when the asking process already holds it.
*/
export class SchedulerLeasesRepository extends TrekRepository<SchedulerLeases> {
/**
* Take (or renew) the lease on `name` for `owner` until `until`, if it is
* free at `now`. One statement: the insert takes a lease nobody has held
* yet, and on a conflict the update only applies while the lease has run out
* or already belongs to `owner`. Of two processes asking at the same moment
* exactly one gets a row written. True when `owner` holds the lease afterwards.
*/
async acquire(name: string, owner: string, now: number, until: number): Promise<boolean> {
const result = await this.kysely<Pick<DB, 'scheduler_leases'>>()
.insertInto('scheduler_leases')
.values({ name, owner, expires_at: until })
.onConflict((oc) =>
oc
.column('name')
.doUpdateSet({ owner, expires_at: until })
.where((eb) => eb.or([eb('scheduler_leases.expires_at', '<=', now), eb('scheduler_leases.owner', '=', owner)])),
)
.executeTakeFirst();
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
}
/** Move the expiry of a lease `owner` still holds; false when somebody else has it now. */
async extend(name: string, owner: string, until: number): Promise<boolean> {
const changed = await this.nativeUpdate({ name, owner }, { expires_at: until });
return changed > 0;
}
/** The current holder and expiry, for diagnostics and tests. */
async holder(name: string): Promise<{ owner: string; expires_at: number } | null> {
const row = await this.findOne({ name }, { fields: ['owner', 'expires_at'] });
return row ? { owner: row.owner, expires_at: row.expires_at } : null;
}
}