@ultimat3/jobs 1.2.0 → 2.0.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/CLAUDE.md +567 -0
- package/README.md +355 -16
- package/package.json +7 -5
- package/src/backfill-gate.ts +97 -0
- package/src/backfill-inspect.ts +73 -0
- package/src/backfill-ledger.ts +183 -0
- package/src/backfill-pass.ts +276 -0
- package/src/backfill-pending.ts +131 -0
- package/src/backfill-rate.ts +109 -0
- package/src/backfill-registry.ts +108 -0
- package/src/backfill-scope.ts +70 -0
- package/src/backfill.ts +213 -0
- package/src/driver-memory.ts +61 -9
- package/src/driver-nats.ts +2 -1
- package/src/driver-pg-ddl.ts +180 -0
- package/src/driver-pg-rows.ts +123 -0
- package/src/driver-pg-sql.ts +251 -55
- package/src/driver-pg.ts +138 -92
- package/src/driver-redis.ts +2 -1
- package/src/driver.ts +91 -7
- package/src/errors.ts +290 -4
- package/src/events-pg.ts +121 -0
- package/src/events.ts +7 -1
- package/src/execute.ts +289 -0
- package/src/heartbeat.ts +146 -0
- package/src/index.ts +121 -27
- package/src/inspect.ts +43 -2
- package/src/job.ts +72 -2
- package/src/leases.ts +90 -0
- package/src/limits.ts +0 -0
- package/src/metrics.ts +35 -0
- package/src/outbox-pg.ts +137 -0
- package/src/outbox.ts +115 -53
- package/src/register.ts +1 -1
- package/src/retry.ts +6 -1
- package/src/run-signal.ts +50 -0
- package/src/scheduler-pg.ts +103 -0
- package/src/scheduler.ts +159 -245
- package/src/steps.ts +155 -31
- package/src/task.ts +229 -0
- package/src/tenant.ts +61 -0
- package/src/worker-fleet-slots.ts +124 -0
- package/src/worker-run.ts +132 -0
- package/src/worker.ts +207 -190
package/src/driver-pg-sql.ts
CHANGED
|
@@ -2,69 +2,32 @@
|
|
|
2
2
|
// agent debugging a stuck queue should be able to read, run and correct the exact statement
|
|
3
3
|
// it saw in a log, without reassembling it from a builder. Kept beside the driver rather than
|
|
4
4
|
// inside it so the driver file stays the control flow and this one stays the wire format.
|
|
5
|
+
//
|
|
6
|
+
// The schema those statements run against is `driver-pg-ddl.ts` — a second responsibility, since
|
|
7
|
+
// it is applied once at boot and never by a driver method. Re-exported from here so the install
|
|
8
|
+
// point keeps the ONE import path every caller already uses.
|
|
5
9
|
|
|
6
|
-
export
|
|
7
|
-
create table if not exists x_jobs (
|
|
8
|
-
id uuid primary key,
|
|
9
|
-
name text not null,
|
|
10
|
-
queue text not null default 'default',
|
|
11
|
-
input jsonb not null,
|
|
12
|
-
idempotency_key text not null,
|
|
13
|
-
run_id uuid not null,
|
|
14
|
-
attempt int not null default 0,
|
|
15
|
-
max_attempts int not null default 3,
|
|
16
|
-
state text not null default 'ready',
|
|
17
|
-
run_at timestamptz not null default now(),
|
|
18
|
-
visible_at timestamptz,
|
|
19
|
-
claimed_by text,
|
|
20
|
-
last_error text,
|
|
21
|
-
tenant_id text,
|
|
22
|
-
created_at timestamptz not null default now(),
|
|
23
|
-
updated_at timestamptz not null default now()
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
-- Partial unique index: one LIVE job per idempotency key. Completed rows stay for history,
|
|
27
|
-
-- so re-running the same work tomorrow is allowed and re-delivering it today is not.
|
|
28
|
-
create unique index if not exists x_jobs_idempotency_live_idx
|
|
29
|
-
on x_jobs (idempotency_key)
|
|
30
|
-
where state in ('ready', 'delayed', 'running', 'suspended');
|
|
31
|
-
|
|
32
|
-
create index if not exists x_jobs_claim_idx
|
|
33
|
-
on x_jobs (queue, run_at)
|
|
34
|
-
where state in ('ready', 'delayed', 'suspended');
|
|
35
|
-
|
|
36
|
-
create table if not exists x_job_steps (
|
|
37
|
-
run_id uuid not null,
|
|
38
|
-
name text not null,
|
|
39
|
-
status text not null,
|
|
40
|
-
output jsonb,
|
|
41
|
-
started_at timestamptz not null default now(),
|
|
42
|
-
completed_at timestamptz,
|
|
43
|
-
wake_at timestamptz,
|
|
44
|
-
event text,
|
|
45
|
-
correlation_key text,
|
|
46
|
-
attempts int not null default 1,
|
|
47
|
-
error text,
|
|
48
|
-
primary key (run_id, name)
|
|
49
|
-
);
|
|
50
|
-
`.trim();
|
|
10
|
+
export { SQL_JOBS_TABLE, SQL_OUTBOX_TABLE } from './driver-pg-ddl';
|
|
51
11
|
|
|
52
12
|
export const SQL_ENQUEUE = `
|
|
53
13
|
insert into x_jobs
|
|
54
|
-
(id, name, queue, input, idempotency_key, run_id, max_attempts, state, run_at, tenant_id
|
|
14
|
+
(id, name, queue, input, idempotency_key, run_id, max_attempts, state, run_at, tenant_id,
|
|
15
|
+
traceparent, enqueued_by)
|
|
55
16
|
values
|
|
56
17
|
($1, $2, $3, $4::jsonb, $5, $6, $7,
|
|
57
18
|
case when to_timestamp($8 / 1000.0) > now() then 'delayed' else 'ready' end,
|
|
58
|
-
to_timestamp($8 / 1000.0), $9)
|
|
59
|
-
on conflict (idempotency_key)
|
|
19
|
+
to_timestamp($8 / 1000.0), $9, $10, $11)
|
|
20
|
+
on conflict (name, idempotency_key)
|
|
60
21
|
where state in ('ready', 'delayed', 'running', 'suspended')
|
|
61
22
|
do nothing
|
|
62
23
|
returning id, run_id
|
|
63
24
|
`.trim();
|
|
64
25
|
|
|
26
|
+
/** Scoped by NAME as well as key — the index is, so a lookup that was not would find a stranger. */
|
|
65
27
|
export const SQL_FIND_LIVE_BY_KEY = `
|
|
66
28
|
select id, run_id from x_jobs
|
|
67
|
-
where
|
|
29
|
+
where name = $1
|
|
30
|
+
and idempotency_key = $2
|
|
68
31
|
and state in ('ready', 'delayed', 'running', 'suspended')
|
|
69
32
|
limit 1
|
|
70
33
|
`.trim();
|
|
@@ -72,7 +35,8 @@ select id, run_id from x_jobs
|
|
|
72
35
|
/**
|
|
73
36
|
* The claim. SKIP LOCKED is the whole design: without it, worker 2 blocks on worker 1's row
|
|
74
37
|
* lock and throughput collapses to one worker. `visible_at` in the predicate reclaims leases
|
|
75
|
-
* abandoned by a crashed worker.
|
|
38
|
+
* abandoned by a crashed worker. `cancelled` is absent from every branch by construction — a
|
|
39
|
+
* cancelled row is terminal, so it is never handed to a worker again.
|
|
76
40
|
*/
|
|
77
41
|
export const SQL_CLAIM = `
|
|
78
42
|
with claimed as (
|
|
@@ -98,16 +62,23 @@ update x_jobs j
|
|
|
98
62
|
where j.id = c.id
|
|
99
63
|
returning j.id, j.name, j.queue, j.input, j.idempotency_key, j.run_id, j.attempt,
|
|
100
64
|
j.max_attempts, j.state, j.tenant_id, j.last_error, j.claimed_by,
|
|
65
|
+
j.traceparent, j.enqueued_by,
|
|
101
66
|
(extract(epoch from j.run_at) * 1000)::bigint as run_at,
|
|
102
67
|
(extract(epoch from j.visible_at) * 1000)::bigint as visible_at,
|
|
103
68
|
(extract(epoch from j.created_at) * 1000)::bigint as created_at,
|
|
104
69
|
(extract(epoch from j.updated_at) * 1000)::bigint as updated_at
|
|
105
70
|
`.trim();
|
|
106
71
|
|
|
72
|
+
/**
|
|
73
|
+
* `and state = 'running'` is a FENCE, not a filter. Without it an ack from the worker that was
|
|
74
|
+
* cancelled — or from one whose lease lapsed and whose job another worker already re-claimed —
|
|
75
|
+
* overwrites the row it no longer owns: `x jobs cancel` would be undone by the next settle, and
|
|
76
|
+
* a re-delivered job would be marked done by the attempt that lost it.
|
|
77
|
+
*/
|
|
107
78
|
export const SQL_ACK = `
|
|
108
79
|
update x_jobs
|
|
109
80
|
set state = 'done', visible_at = null, claimed_by = null, updated_at = now()
|
|
110
|
-
where id = $1
|
|
81
|
+
where id = $1 and state = 'running'
|
|
111
82
|
`.trim();
|
|
112
83
|
|
|
113
84
|
export const SQL_NACK = `
|
|
@@ -119,19 +90,47 @@ update x_jobs
|
|
|
119
90
|
claimed_by = null,
|
|
120
91
|
last_error = coalesce($5, last_error),
|
|
121
92
|
updated_at = now()
|
|
122
|
-
where id = $1
|
|
93
|
+
where id = $1 and state = 'running'
|
|
94
|
+
`.trim();
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Stop a job from outside. `state <> 'done'` and not `state = 'ready'`: the runaway backfill this
|
|
98
|
+
* exists for is `running`, and a job that already finished has nothing to stop. The worker holding
|
|
99
|
+
* it learns on its next heartbeat, which no longer matches `state = 'running'`.
|
|
100
|
+
*/
|
|
101
|
+
export const SQL_CANCEL = `
|
|
102
|
+
update x_jobs
|
|
103
|
+
set state = 'cancelled', visible_at = null, claimed_by = null,
|
|
104
|
+
last_error = coalesce($2, last_error), updated_at = now()
|
|
105
|
+
where id = $1 and state <> 'done'
|
|
106
|
+
returning *
|
|
123
107
|
`.trim();
|
|
124
108
|
|
|
109
|
+
/**
|
|
110
|
+
* `returning id` so the caller can tell a renewal that LANDED from one that matched no row. A
|
|
111
|
+
* heartbeat that updates nothing means this worker no longer owns the job — cancelled from
|
|
112
|
+
* outside, or re-claimed after the lease lapsed — and continuing to run it is the double-execution
|
|
113
|
+
* the lease exists to prevent.
|
|
114
|
+
*/
|
|
125
115
|
export const SQL_HEARTBEAT = `
|
|
126
116
|
update x_jobs
|
|
127
117
|
set visible_at = now() + ($2::bigint * interval '1 millisecond'), updated_at = now()
|
|
128
|
-
where id = $1 and state = 'running'
|
|
118
|
+
where id = $1 and state = 'running' and ($3::text is null or claimed_by = $3)
|
|
119
|
+
returning id
|
|
129
120
|
`.trim();
|
|
130
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Five buckets a queue depth is SUMMED from, so a row may land in exactly one. `run_at > now()`
|
|
124
|
+
* unqualified was every state's future row: a `step.sleep` job counted as `suspended` AND
|
|
125
|
+
* `delayed`, a delayed dead-letter as `dead` AND `delayed`. The memory driver's `if/else if` chain
|
|
126
|
+
* has always been exclusive, so `x jobs` reported one depth under `x dev` and a larger one in
|
|
127
|
+
* production — off the same rows.
|
|
128
|
+
*/
|
|
131
129
|
export const SQL_STATS = `
|
|
132
130
|
select queue,
|
|
133
131
|
count(*) filter (where state = 'ready' and run_at <= now()) as ready,
|
|
134
|
-
count(*) filter (where state = 'delayed'
|
|
132
|
+
count(*) filter (where state = 'delayed'
|
|
133
|
+
or (state = 'ready' and run_at > now())) as delayed,
|
|
135
134
|
count(*) filter (where state = 'running') as running,
|
|
136
135
|
count(*) filter (where state = 'suspended') as suspended,
|
|
137
136
|
count(*) filter (where state = 'dead') as dead,
|
|
@@ -142,10 +141,156 @@ select queue,
|
|
|
142
141
|
order by queue
|
|
143
142
|
`.trim();
|
|
144
143
|
|
|
145
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Session-scoped, so a crashed node's lock releases itself — and released just as surely when a
|
|
146
|
+
* POOLED connection goes back to the pool, which is why `createPgLeader` is not what a scheduler
|
|
147
|
+
* running on a shared executor should use. See `SQL_LEADER_ACQUIRE` below.
|
|
148
|
+
*/
|
|
146
149
|
export const SQL_TRY_ADVISORY_LOCK = 'select pg_try_advisory_lock($1) as locked';
|
|
147
150
|
export const SQL_ADVISORY_UNLOCK = 'select pg_advisory_unlock($1) as unlocked';
|
|
148
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Lease-based leader election, safe on a pooled executor. One statement, and the primary key is
|
|
154
|
+
* what makes it atomic: an insert wins an unheld key, and the `do update` only fires for the
|
|
155
|
+
* holder itself (a renewal) or for a lease that has already expired. A second node reaching for a
|
|
156
|
+
* live lease matches neither branch and gets no row back.
|
|
157
|
+
*/
|
|
158
|
+
export const SQL_LEADER_ACQUIRE = `
|
|
159
|
+
insert into x_scheduler_leader (lock_key, holder, expires_at)
|
|
160
|
+
values ($1, $2, now() + ($3::bigint * interval '1 millisecond'))
|
|
161
|
+
on conflict (lock_key) do update
|
|
162
|
+
set holder = excluded.holder, expires_at = excluded.expires_at
|
|
163
|
+
where x_scheduler_leader.holder = excluded.holder
|
|
164
|
+
or x_scheduler_leader.expires_at <= now()
|
|
165
|
+
returning holder
|
|
166
|
+
`.trim();
|
|
167
|
+
|
|
168
|
+
/** Only the holder may hand it back. A release by a node that already lost it is a no-op. */
|
|
169
|
+
export const SQL_LEADER_RELEASE = `
|
|
170
|
+
delete from x_scheduler_leader where lock_key = $1 and holder = $2
|
|
171
|
+
`.trim();
|
|
172
|
+
|
|
173
|
+
export const SQL_SCHEDULER_STATE_GET = `
|
|
174
|
+
select (extract(epoch from last_fired_at) * 1000)::bigint as last_fired_at
|
|
175
|
+
from x_scheduler_state where task_name = $1
|
|
176
|
+
`.trim();
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `greatest` and not a plain assignment: the watermark only ever moves FORWARD. Two rounds that
|
|
180
|
+
* overlapped across a rolling restart would otherwise let the older one rewind it, and every
|
|
181
|
+
* occurrence between the two values fires a second time.
|
|
182
|
+
*/
|
|
183
|
+
export const SQL_SCHEDULER_STATE_MARK = `
|
|
184
|
+
insert into x_scheduler_state (task_name, last_fired_at, updated_at)
|
|
185
|
+
values ($1, to_timestamp($2 / 1000.0), now())
|
|
186
|
+
on conflict (task_name) do update
|
|
187
|
+
set last_fired_at = greatest(x_scheduler_state.last_fired_at, excluded.last_fired_at),
|
|
188
|
+
updated_at = now()
|
|
189
|
+
`.trim();
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Take the lowest free slot under `limit`, or nothing. Race-safe without an advisory lock: the
|
|
193
|
+
* `(lease_key, slot)` primary key serialises two workers that picked the same slot, and the
|
|
194
|
+
* loser's `do update` is guarded on the slot having expired — a live slot returns no row, so the
|
|
195
|
+
* grant is never doubled. Under contention this can refuse a slot that is genuinely free; a
|
|
196
|
+
* refusal costs one poll interval and an over-grant costs the guarantee.
|
|
197
|
+
*/
|
|
198
|
+
export const SQL_LEASE_ACQUIRE = `
|
|
199
|
+
insert into x_job_leases (lease_key, slot, holder, expires_at)
|
|
200
|
+
select $1, s.slot, $2, now() + ($4::bigint * interval '1 millisecond')
|
|
201
|
+
from generate_series(0, $3::int - 1) as s(slot)
|
|
202
|
+
where not exists (
|
|
203
|
+
select 1 from x_job_leases l
|
|
204
|
+
where l.lease_key = $1 and l.slot = s.slot and l.expires_at > now()
|
|
205
|
+
)
|
|
206
|
+
order by s.slot
|
|
207
|
+
limit 1
|
|
208
|
+
on conflict (lease_key, slot) do update
|
|
209
|
+
set holder = excluded.holder, expires_at = excluded.expires_at
|
|
210
|
+
where x_job_leases.expires_at <= now()
|
|
211
|
+
returning slot
|
|
212
|
+
`.trim();
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Two fences, and each answers a case the other cannot. `holder` answers "another worker has this
|
|
216
|
+
* slot"; `expires_at > now()` answers "nobody has it YET" — a lapsed slot is one any worker may
|
|
217
|
+
* take on its next acquire, so reviving it makes the TTL an expiry only other processes observe,
|
|
218
|
+
* and `held()` (which counts live rows) disagreed with the holder's own belief in between. The
|
|
219
|
+
* memory store has always answered `false` here, and `worker-fleet-slots.ts` turns that into
|
|
220
|
+
* `X_JOB_SLOT_LOST`; without this line the same lapsed slot cancelled the run under `x dev` and
|
|
221
|
+
* silently continued in production.
|
|
222
|
+
*/
|
|
223
|
+
export const SQL_LEASE_RENEW = `
|
|
224
|
+
update x_job_leases
|
|
225
|
+
set expires_at = now() + ($4::bigint * interval '1 millisecond')
|
|
226
|
+
where lease_key = $1 and slot = $2::int and holder = $3 and expires_at > now()
|
|
227
|
+
returning slot
|
|
228
|
+
`.trim();
|
|
229
|
+
|
|
230
|
+
export const SQL_LEASE_RELEASE = `
|
|
231
|
+
delete from x_job_leases where lease_key = $1 and slot = $2::int and holder = $3
|
|
232
|
+
`.trim();
|
|
233
|
+
|
|
234
|
+
export const SQL_EVENT_PUBLISH = `
|
|
235
|
+
insert into x_job_events (id, name, payload, correlation_key, published_at, expires_at)
|
|
236
|
+
values ($1, $2, $3::jsonb, $4, to_timestamp($5 / 1000.0), to_timestamp($6 / 1000.0))
|
|
237
|
+
`.trim();
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Earliest matching event at or after `afterMs`, so a resumed step consumes events in publication
|
|
241
|
+
* order rather than jumping to the newest one — the memory bus's rule, in SQL.
|
|
242
|
+
*/
|
|
243
|
+
export const SQL_EVENT_FIND = `
|
|
244
|
+
select payload, (extract(epoch from published_at) * 1000)::bigint as published_at
|
|
245
|
+
from x_job_events
|
|
246
|
+
where name = $1
|
|
247
|
+
and expires_at > now()
|
|
248
|
+
and published_at >= to_timestamp($3 / 1000.0)
|
|
249
|
+
and ($2::text is null or correlation_key = $2)
|
|
250
|
+
order by published_at
|
|
251
|
+
limit 1
|
|
252
|
+
`.trim();
|
|
253
|
+
|
|
254
|
+
export const SQL_EVENT_LIST = `
|
|
255
|
+
select id, name, payload, correlation_key,
|
|
256
|
+
(extract(epoch from published_at) * 1000)::bigint as published_at,
|
|
257
|
+
(extract(epoch from expires_at) * 1000)::bigint as expires_at
|
|
258
|
+
from x_job_events
|
|
259
|
+
where ($1::text is null or name = $1)
|
|
260
|
+
order by published_at
|
|
261
|
+
limit $2
|
|
262
|
+
`.trim();
|
|
263
|
+
|
|
264
|
+
export const SQL_EVENT_PURGE = `delete from x_job_events where expires_at <= now()`;
|
|
265
|
+
|
|
266
|
+
export const SQL_OUTBOX_STAGE = `
|
|
267
|
+
insert into x_outbox
|
|
268
|
+
(id, job, queue, input, idempotency_key, max_attempts, run_at, staged_at, tenant_id,
|
|
269
|
+
traceparent, enqueued_by)
|
|
270
|
+
values ($1, $2, $3, $4::jsonb, $5, $6, to_timestamp($7 / 1000.0), to_timestamp($8 / 1000.0),
|
|
271
|
+
$9, $10, $11)
|
|
272
|
+
`.trim();
|
|
273
|
+
|
|
274
|
+
export const SQL_OUTBOX_CLAIM = `
|
|
275
|
+
select id, job, queue, input, idempotency_key, max_attempts, tenant_id,
|
|
276
|
+
traceparent, enqueued_by,
|
|
277
|
+
(extract(epoch from run_at) * 1000)::bigint as run_at,
|
|
278
|
+
(extract(epoch from staged_at) * 1000)::bigint as staged_at
|
|
279
|
+
from x_outbox
|
|
280
|
+
where published_at is null
|
|
281
|
+
order by staged_at
|
|
282
|
+
limit $1
|
|
283
|
+
for update skip locked
|
|
284
|
+
`.trim();
|
|
285
|
+
|
|
286
|
+
export const SQL_OUTBOX_MARK_PUBLISHED = `
|
|
287
|
+
update x_outbox set published_at = to_timestamp($2 / 1000.0) where id = $1
|
|
288
|
+
`.trim();
|
|
289
|
+
|
|
290
|
+
export const SQL_OUTBOX_PENDING_COUNT = `
|
|
291
|
+
select count(*)::bigint as pending from x_outbox where published_at is null
|
|
292
|
+
`.trim();
|
|
293
|
+
|
|
149
294
|
export const SQL_STEP_GET = `
|
|
150
295
|
select run_id, name, status, output, attempts, error,
|
|
151
296
|
(extract(epoch from started_at) * 1000)::bigint as started_at,
|
|
@@ -168,3 +313,54 @@ on conflict (run_id, name) do update
|
|
|
168
313
|
completed_at = excluded.completed_at, wake_at = excluded.wake_at,
|
|
169
314
|
attempts = excluded.attempts, error = excluded.error
|
|
170
315
|
`.trim();
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Open the row, or adopt the one an earlier attempt of the SAME run opened. `do update` rather
|
|
319
|
+
* than `do nothing` because a failed attempt left `failed` behind and this one is running:
|
|
320
|
+
* `started_at`, `app_version` and `checksum` stay as the pass began, and the progress columns
|
|
321
|
+
* stay where the last attempt got to.
|
|
322
|
+
*
|
|
323
|
+
* `completed_at` is cleared, and is the one column here that is not preserved: `finish` stamps it
|
|
324
|
+
* for `failed` as well as for `completed`, so a retried run that kept it would report a running
|
|
325
|
+
* pass with a completion time in the past — on `x db backfill --list`, on `x jobs show` and in
|
|
326
|
+
* `/_x`, all of which project that column straight through.
|
|
327
|
+
*/
|
|
328
|
+
export const SQL_BACKFILL_START = `
|
|
329
|
+
insert into x_backfills (run_id, name, checksum, app_version)
|
|
330
|
+
values ($1, $2, $3, $4)
|
|
331
|
+
on conflict (run_id) do update set status = 'running', completed_at = null
|
|
332
|
+
`.trim();
|
|
333
|
+
|
|
334
|
+
export const SQL_BACKFILL_PROGRESS = `
|
|
335
|
+
update x_backfills
|
|
336
|
+
set rows_processed = $2, last_cursor = $3
|
|
337
|
+
where run_id = $1
|
|
338
|
+
`.trim();
|
|
339
|
+
|
|
340
|
+
/** A failure KEEPS its cursor: where a pass stopped is the first thing anyone asks about one. */
|
|
341
|
+
export const SQL_BACKFILL_FINISH = `
|
|
342
|
+
update x_backfills
|
|
343
|
+
set status = $2,
|
|
344
|
+
rows_processed = $3,
|
|
345
|
+
completed_at = now(),
|
|
346
|
+
last_cursor = case when $2 = 'completed' then null else last_cursor end
|
|
347
|
+
where run_id = $1
|
|
348
|
+
`.trim();
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* `$3::uuid`, where its two neighbours are `::text`, because `run_id` IS a uuid: the cast on the
|
|
352
|
+
* null test is what tells Postgres the parameter's type, and `::text` there pins it to text for
|
|
353
|
+
* the comparison below it — `uuid = text` has no operator, so every call failed, filtered or not.
|
|
354
|
+
* Nothing hand-types this value; it arrives from a job record the same database wrote.
|
|
355
|
+
*/
|
|
356
|
+
export const SQL_BACKFILL_LIST = `
|
|
357
|
+
select run_id, name, checksum, status, app_version, rows_processed, last_cursor,
|
|
358
|
+
(extract(epoch from started_at) * 1000)::bigint as started_at,
|
|
359
|
+
(extract(epoch from completed_at) * 1000)::bigint as completed_at
|
|
360
|
+
from x_backfills
|
|
361
|
+
where ($1::text is null or name = $1)
|
|
362
|
+
and ($2::text is null or status = $2)
|
|
363
|
+
and ($3::uuid is null or run_id = $3)
|
|
364
|
+
order by started_at desc
|
|
365
|
+
limit $4
|
|
366
|
+
`.trim();
|