@ultimat3/jobs 30.8.0 → 30.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/backfill-pass.ts +2 -1
- package/src/backfill-rate.ts +33 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/jobs",
|
|
3
|
-
"version": "30.
|
|
3
|
+
"version": "30.9.0",
|
|
4
4
|
"description": "Durable background work: steps, transactional outbox, cron tasks, one driver interface",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ultimate",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"test": "bun test"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@ultimat3/core": "30.
|
|
48
|
-
"@ultimat3/db": "30.
|
|
49
|
-
"@ultimat3/entity": "30.
|
|
50
|
-
"@ultimat3/http": "30.
|
|
51
|
-
"@ultimat3/schema": "30.
|
|
52
|
-
"@ultimat3/time": "30.
|
|
47
|
+
"@ultimat3/core": "30.9.0",
|
|
48
|
+
"@ultimat3/db": "30.9.0",
|
|
49
|
+
"@ultimat3/entity": "30.9.0",
|
|
50
|
+
"@ultimat3/http": "30.9.0",
|
|
51
|
+
"@ultimat3/schema": "30.9.0",
|
|
52
|
+
"@ultimat3/time": "30.9.0"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/backfill-pass.ts
CHANGED
|
@@ -248,7 +248,8 @@ export async function backfillPass<Row>(
|
|
|
248
248
|
//
|
|
249
249
|
// The signal is the run's cancellation composed with this step's ceiling, so a cancelled
|
|
250
250
|
// pass unwinds out of the wait instead of sitting in a timer nobody is waiting for.
|
|
251
|
-
|
|
251
|
+
// The clock is the run's, so a pass under a frozen one waits on it, never in real time.
|
|
252
|
+
await pace.wait({ signal, step: stepName, clock: args.ctx.clock });
|
|
252
253
|
// A backfill visits every row once, in key order: the scan is the point, and the query-plan
|
|
253
254
|
// sweep is told so rather than asked to index a column no read filters on.
|
|
254
255
|
const page = await expectedSeqScan(BACKFILL_SCAN, () => reader.read(cursor, signal));
|
package/src/backfill-rate.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// serving, so a backfill that runs flat out is an outage of its own making — the pacer is what
|
|
4
4
|
// makes "one pass over the table" background work rather than a load test.
|
|
5
5
|
|
|
6
|
-
import type { Clock } from '@ultimat3/core';
|
|
6
|
+
import type { Clock, FrozenClock } from '@ultimat3/core';
|
|
7
7
|
import { assert, systemClock } from '@ultimat3/core';
|
|
8
8
|
import { nowMs } from './clock';
|
|
9
9
|
import { JobAbortedError } from './errors';
|
|
@@ -33,10 +33,12 @@ export interface PacerOptions {
|
|
|
33
33
|
readonly rate: number;
|
|
34
34
|
/** The name a cancellation is reported under — one pacer belongs to exactly one backfill. */
|
|
35
35
|
readonly job: string;
|
|
36
|
+
/** Overrides the run's clock for every wait. Unset: each wait reads the clock it is handed. */
|
|
36
37
|
readonly clock?: Clock | undefined;
|
|
37
38
|
/**
|
|
38
39
|
* The wait itself, injectable for the same reason the clock is: a test asserts what was ASKED
|
|
39
40
|
* for instead of spending it. Must settle when `signal` aborts rather than waiting the rest out.
|
|
41
|
+
* Unset: a frozen clock is advanced by the wait, any other is waited out on a real timer.
|
|
40
42
|
*/
|
|
41
43
|
readonly sleep?: ((ms: number, signal: AbortSignal) => Promise<void>) | undefined;
|
|
42
44
|
}
|
|
@@ -48,7 +50,15 @@ export interface Pacer {
|
|
|
48
50
|
* Hold the caller until this batch's slot comes round. Rejects with `JobAbortedError` when the
|
|
49
51
|
* attempt was cancelled — before the wait or during it, never after sitting the timer out.
|
|
50
52
|
*/
|
|
51
|
-
wait(args: {
|
|
53
|
+
wait(args: {
|
|
54
|
+
readonly signal: AbortSignal;
|
|
55
|
+
readonly step?: string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The clock the RUN measures by — `ctx.clock`. The pacer is built once per declaration, so
|
|
58
|
+
* the run's clock can only reach it here; a pass under a frozen clock is paced on that clock.
|
|
59
|
+
*/
|
|
60
|
+
readonly clock?: Clock | undefined;
|
|
61
|
+
}): Promise<void>;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
/** Real timers, cleared on abort so a cancelled pass leaves nothing pending behind it. */
|
|
@@ -64,6 +74,24 @@ function timerSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
|
64
74
|
});
|
|
65
75
|
}
|
|
66
76
|
|
|
77
|
+
/** Structural, as `Clock` itself is: a clock that only moves when told to has `advance`. */
|
|
78
|
+
const isFrozenClock = (clock: Clock): clock is FrozenClock =>
|
|
79
|
+
typeof (clock as { readonly advance?: unknown }).advance === 'function';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The default wait, on the clock the batches are measured by. A frozen clock never moves on its
|
|
83
|
+
* own, so a real timer under one spent wall time the pacer could not even see — every batch read
|
|
84
|
+
* as instant and paid the whole interval for real. Moving the clock IS the wait there: the sweep
|
|
85
|
+
* finishes at once and the clock shows the time the throttle cost.
|
|
86
|
+
*/
|
|
87
|
+
function clockSleep(clock: Clock): (ms: number, signal: AbortSignal) => Promise<void> {
|
|
88
|
+
if (!isFrozenClock(clock)) return timerSleep;
|
|
89
|
+
return (ms) => {
|
|
90
|
+
clock.advance(ms);
|
|
91
|
+
return Promise.resolve();
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
/**
|
|
68
96
|
* Built once, at declaration, and shared by every attempt of that backfill in this process: the
|
|
69
97
|
* rate is a property of the table being swept and the pool it is swept through, not of whichever
|
|
@@ -79,14 +107,14 @@ export function backfillPacer(options: PacerOptions): Pacer {
|
|
|
79
107
|
`backfillPacer({ rate: ${String(options.rate)} }) for "${options.job}" — a rate is batches per second, greater than zero`,
|
|
80
108
|
`pass rate: ${DEFAULT_BACKFILL_RATE} — to sweep faster raise the number, there is no unthrottled mode`,
|
|
81
109
|
);
|
|
82
|
-
const clock = options.clock ?? systemClock;
|
|
83
|
-
const sleep = options.sleep ?? timerSleep;
|
|
84
110
|
const intervalMs = 1000 / options.rate;
|
|
85
111
|
let lastAt: number | undefined;
|
|
86
112
|
|
|
87
113
|
return {
|
|
88
114
|
rate: options.rate,
|
|
89
|
-
async wait({ signal, step }) {
|
|
115
|
+
async wait({ signal, step, clock: runClock }) {
|
|
116
|
+
const clock = options.clock ?? runClock ?? systemClock;
|
|
117
|
+
const sleep = options.sleep ?? clockSleep(clock);
|
|
90
118
|
const aborted = (): JobAbortedError =>
|
|
91
119
|
new JobAbortedError({ job: options.job, ...(step === undefined ? {} : { step }) });
|
|
92
120
|
if (signal.aborted) throw aborted();
|