* Record the result of a run and atomically advance next_run_at. * * Returns true if this caller won the claim, false if a concurrent tick had * already advanced next_run_at. The whole thing is one transaction: we * advance next_run_at FIRST (the atomic claim), and only if we win do we
(runId: number, scheduleId: string, status: 'success' | 'error' | 'skipped', exitCode: number, message: string, durationMs: number)
| 181 | * "this run did not happen / do not fire". |
| 182 | */ |
| 183 | recordRunFinish(runId: number, scheduleId: string, status: 'success' | 'error' | 'skipped', exitCode: number, message: string, durationMs: number): boolean { |
| 184 | const CLAIM_LOST = Symbol('claim-lost'); |
| 185 | const tx = this.db.transaction(() => { |
| 186 | // Atomic claim first: if we lose the race, abort the transaction so none |
| 187 | // of the bookkeeping below is committed. |
| 188 | if (!this.recomputeNext(scheduleId)) { |
| 189 | throw CLAIM_LOST; |
| 190 | } |
| 191 | this.db.prepare(` |
| 192 | UPDATE schedule_runs SET finished_at = CURRENT_TIMESTAMP, status = ?, exit_code = ?, message = ?, duration_ms = ? |
| 193 | WHERE id = ? |
| 194 | `).run(status, exitCode, message, durationMs, runId); |
| 195 | this.db.prepare(` |
| 196 | UPDATE schedules SET |
| 197 | last_run_at = CURRENT_TIMESTAMP, |
| 198 | last_status = ?, |
| 199 | last_message = ?, |
| 200 | last_duration_ms = ?, |
| 201 | run_count = run_count + 1 |
| 202 | WHERE id = ? |
| 203 | `).run(status, message, durationMs, scheduleId); |
| 204 | }); |
| 205 | try { |
| 206 | tx(); |
| 207 | return true; |
| 208 | } catch (err) { |
| 209 | if (err === CLAIM_LOST) return false; |
| 210 | throw err; |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | /** |
| 215 | * Recompute next_run_at from now. Called after every run and on enable. |
no test coverage detected