pg-boss 12.35.1 → 12.37.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/README.md +2 -1
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +26 -0
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +35 -4
- package/dist/cli.js +8 -2
- package/dist/contractor.d.ts +1 -0
- package/dist/contractor.d.ts.map +1 -1
- package/dist/contractor.js +4 -0
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +14 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -9
- package/dist/latency.d.ts +15 -0
- package/dist/latency.d.ts.map +1 -0
- package/dist/latency.js +45 -0
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +296 -75
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +194 -0
- package/dist/nurse.d.ts +20 -0
- package/dist/nurse.d.ts.map +1 -0
- package/dist/nurse.js +287 -0
- package/dist/plans.d.ts +27 -2
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +556 -55
- package/dist/registrar.d.ts +16 -0
- package/dist/registrar.d.ts.map +1 -0
- package/dist/registrar.js +226 -0
- package/dist/schema.json +514 -2
- package/dist/telemetry.d.ts +65 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +349 -0
- package/dist/timekeeper.d.ts +4 -1
- package/dist/timekeeper.d.ts.map +1 -1
- package/dist/timekeeper.js +22 -5
- package/dist/types.d.ts +213 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +2 -0
- package/package.json +14 -5
package/dist/plans.js
CHANGED
|
@@ -3,7 +3,11 @@ import { indexKeysRaw, indexIncludeRaw, indexPredicateRaw, displayIndexDefinitio
|
|
|
3
3
|
import schemaManifest from './schema.json' with { type: 'json' };
|
|
4
4
|
import { normalizeSchemaName, resolveSchemaName } from "./tools.js";
|
|
5
5
|
export const PG_ERROR = {
|
|
6
|
-
divisionByZero: '22012'
|
|
6
|
+
divisionByZero: '22012',
|
|
7
|
+
lockNotAvailable: '55P03',
|
|
8
|
+
foreignKeyViolation: '23503',
|
|
9
|
+
undefinedTable: '42P01',
|
|
10
|
+
checkViolation: '23514'
|
|
7
11
|
};
|
|
8
12
|
export const DEFAULT_SCHEMA = 'pgboss';
|
|
9
13
|
export const MIGRATE_RACE_MESSAGE = 'division by zero';
|
|
@@ -159,6 +163,7 @@ export function create(schema, version, options) {
|
|
|
159
163
|
noPartitioning ? '' : ensureQueueStatsPartitions(schema),
|
|
160
164
|
createTableJobDependency(schema),
|
|
161
165
|
createIndexJobDependencyParent(schema),
|
|
166
|
+
createTableInstance(schema),
|
|
162
167
|
createQueueFunction(schema, noPartitioning),
|
|
163
168
|
deleteQueueFunction(schema, noPartitioning),
|
|
164
169
|
insertVersion(schema, version)
|
|
@@ -184,6 +189,7 @@ function createInline(schema, version, options) {
|
|
|
184
189
|
inlineIntoCreateTable(createTableWarning(schema), [createIndexWarning(schema)]),
|
|
185
190
|
inlineIntoCreateTable(createTableQueueStats(schema, true), [createIndexQueueStats(schema, options.noCovering)]),
|
|
186
191
|
inlineIntoCreateTable(createTableJobDependency(schema), [createIndexJobDependencyParent(schema)]),
|
|
192
|
+
createTableInstance(schema),
|
|
187
193
|
createQueueFunction(schema, true),
|
|
188
194
|
deleteQueueFunction(schema, true),
|
|
189
195
|
insertVersion(schema, version)
|
|
@@ -304,7 +310,11 @@ function createTableVersion(schema) {
|
|
|
304
310
|
// try-lock - without capturedOn claiming a freshness the counts do not have.
|
|
305
311
|
// created_delta / completed_delta / failed_delta are not gauges like the counts beside them: they
|
|
306
312
|
// are how many jobs went through between the previous monitor pass and the latest one. delta_on and
|
|
307
|
-
// delta_seconds are the window those three cover, null until a pass counts.
|
|
313
|
+
// delta_seconds are the window those three cover, null until a pass counts. wait_bins and run_bins
|
|
314
|
+
// are the wait and run times of the jobs that finished in that window, as histograms of LATENCY_SLOTS
|
|
315
|
+
// counts (see LATENCY_BINS), and ready_oldest_seconds is how long the oldest job ready to run had
|
|
316
|
+
// waited at the pass. Like the deltas, all of them are null until a pass counts, and a pass that
|
|
317
|
+
// counts writes a value: every slot null when nothing finished, 0 when nothing was waiting.
|
|
308
318
|
/* eslint-disable no-restricted-syntax -- column defaults stay on the real clock: every pg-boss write names its timestamps through job_now() */
|
|
309
319
|
function createTableQueue(schema) {
|
|
310
320
|
return `
|
|
@@ -322,6 +332,7 @@ function createTableQueue(schema) {
|
|
|
322
332
|
partition bool NOT NULL,
|
|
323
333
|
table_name text NOT NULL,
|
|
324
334
|
deferred_count int NOT NULL default 0,
|
|
335
|
+
blocked_count int NOT NULL default 0,
|
|
325
336
|
queued_count int NOT NULL default 0,
|
|
326
337
|
ready_count int NOT NULL default 0,
|
|
327
338
|
warning_queued int NOT NULL default 0,
|
|
@@ -333,6 +344,9 @@ function createTableQueue(schema) {
|
|
|
333
344
|
failed_delta int NOT NULL default 0,
|
|
334
345
|
delta_on timestamp with time zone,
|
|
335
346
|
delta_seconds int,
|
|
347
|
+
wait_bins int[],
|
|
348
|
+
run_bins int[],
|
|
349
|
+
ready_oldest_seconds int,
|
|
336
350
|
ready_history int[] NOT NULL default '{}',
|
|
337
351
|
heartbeat_seconds int,
|
|
338
352
|
notify bool NOT NULL DEFAULT false,
|
|
@@ -435,9 +449,269 @@ export function createTableJobDependency(schema) {
|
|
|
435
449
|
)
|
|
436
450
|
`;
|
|
437
451
|
}
|
|
452
|
+
// An instance is live until it misses this many heartbeats, and its row is deleted once its heartbeat
|
|
453
|
+
// has not moved for this many days.
|
|
454
|
+
export const INSTANCE_QUIET_BEATS = 3;
|
|
455
|
+
export const INSTANCE_RETENTION_DAYS = 7;
|
|
456
|
+
// A crash never sets stopped_on, so a crash loop leaves a quiet row per restart, and a deploy whose
|
|
457
|
+
// processes never call stop() leaves one per replica. Registering keeps the newest of these dead rows
|
|
458
|
+
// for its own name (for its host, when unnamed), and maintenance keeps the newest overall, so neither
|
|
459
|
+
// can grow the table for the whole retention. Keyed on the name rather than the host because a
|
|
460
|
+
// Kubernetes pod that is recreated comes back under a new hostname.
|
|
461
|
+
export const INSTANCE_DEAD_KEPT_PER_NAME = 20;
|
|
462
|
+
export const INSTANCE_DEAD_KEPT = 1000;
|
|
463
|
+
// Stopped, or quiet: the complement of getInstances' live.
|
|
464
|
+
function instanceDead(schema) {
|
|
465
|
+
return `(stopped_on IS NOT NULL OR heartbeat_on < ${schema}.job_now() - heartbeat_seconds * ${INSTANCE_QUIET_BEATS} * interval '1 second')`;
|
|
466
|
+
}
|
|
467
|
+
// Deletes the dead rows ranked past `keep`, newest start first, among those `where` selects. Ranked
|
|
468
|
+
// before locking and locked with SKIP LOCKED, so a registration and maintenance pruning at once
|
|
469
|
+
// cannot deadlock, and neither deletes a row the other's ranking kept. Used on every backend, unlike
|
|
470
|
+
// the fetch's SKIP LOCKED: where it can pass over an unlocked row (CockroachDB), that row is only
|
|
471
|
+
// left for the next prune.
|
|
472
|
+
function pruneDeadInstances(schema, where, keep) {
|
|
473
|
+
return `
|
|
474
|
+
DELETE FROM ${schema}.instance
|
|
475
|
+
WHERE id IN (
|
|
476
|
+
SELECT id FROM ${schema}.instance
|
|
477
|
+
WHERE id IN (
|
|
478
|
+
SELECT id FROM (
|
|
479
|
+
SELECT id, row_number() OVER (ORDER BY started_on DESC, id DESC) as n
|
|
480
|
+
FROM ${schema}.instance
|
|
481
|
+
WHERE ${where} AND ${instanceDead(schema)}
|
|
482
|
+
) ranked
|
|
483
|
+
WHERE n > ${keep}
|
|
484
|
+
)
|
|
485
|
+
FOR UPDATE SKIP LOCKED
|
|
486
|
+
)
|
|
487
|
+
`;
|
|
488
|
+
}
|
|
489
|
+
// Run by an instance after it registers: $1 its id, $2 its name, $3 its host.
|
|
490
|
+
export function pruneInstanceLives(schema, keep) {
|
|
491
|
+
return pruneDeadInstances(schema, 'id <> $1 AND (CASE WHEN $2::text IS NULL THEN name IS NULL AND host = $3 ELSE name = $2 END)', keep);
|
|
492
|
+
}
|
|
493
|
+
export function trimDeadInstances(schema, keep) {
|
|
494
|
+
return pruneDeadInstances(schema, 'true', keep);
|
|
495
|
+
}
|
|
496
|
+
// Crash restarts: how many lives in a row on this name and host ended without stop() before this one
|
|
497
|
+
// started. Worked out from the rows rather than carried from one life to the next, because at
|
|
498
|
+
// registration a row that still reads live is either a sibling process (pm2, a second PgBoss) or a
|
|
499
|
+
// predecessor that crashed seconds ago, and only its next missed heartbeats tell which. So a crashed
|
|
500
|
+
// life counts toward an instance only once it is dead, and only if its last heartbeat came before the
|
|
501
|
+
// instance started, which a live sibling's keeps moving past.
|
|
502
|
+
//
|
|
503
|
+
// $1 this instance's id, $2 its name, $3 its host, $4 its pid, $5 when its process started: a row
|
|
504
|
+
// with this pid whose heartbeat predates that is an earlier process that reused the pid, dead at once
|
|
505
|
+
// (a container restart, pid 1 each time), and a row with this pid started since is another PgBoss
|
|
506
|
+
// object in this same process, never a predecessor.
|
|
507
|
+
function crashSlot(schema) {
|
|
508
|
+
return `
|
|
509
|
+
me AS (SELECT id, started_on FROM ${schema}.instance WHERE id = $1),
|
|
510
|
+
slot AS (
|
|
511
|
+
SELECT i.*
|
|
512
|
+
FROM ${schema}.instance i, me
|
|
513
|
+
WHERE i.id <> me.id
|
|
514
|
+
AND i.host = $3
|
|
515
|
+
AND (i.name = $2 OR (i.name IS NULL AND $2::text IS NULL))
|
|
516
|
+
AND i.started_on < me.started_on
|
|
517
|
+
AND i.heartbeat_on <= me.started_on
|
|
518
|
+
AND NOT (i.pid = $4 AND i.started_on >= $5::timestamptz)
|
|
519
|
+
),
|
|
520
|
+
boundary AS (SELECT max(started_on) as at FROM slot WHERE stopped_on IS NOT NULL),
|
|
521
|
+
streak AS (
|
|
522
|
+
SELECT s.*
|
|
523
|
+
FROM slot s, boundary b
|
|
524
|
+
WHERE s.stopped_on IS NULL
|
|
525
|
+
AND (b.at IS NULL OR s.started_on > b.at)
|
|
526
|
+
)`;
|
|
527
|
+
}
|
|
528
|
+
function crashDead(schema) {
|
|
529
|
+
return `(heartbeat_on < ${schema}.job_now() - heartbeat_seconds * ${INSTANCE_QUIET_BEATS} * interval '1 second'
|
|
530
|
+
OR (pid = $4 AND heartbeat_on < $5::timestamptz))`;
|
|
531
|
+
}
|
|
532
|
+
// Numbers the dead lives of the current streak from the earliest one still kept, whose count stands
|
|
533
|
+
// for any the pruning has taken, and writes each later life's count and this instance's. Rewriting
|
|
534
|
+
// the streak is what keeps the counts exact: a life that crashed before its own recheck is corrected
|
|
535
|
+
// by its successor, so the earliest kept row is always right when the pruning moves past it.
|
|
536
|
+
export function countCrashRestarts(schema) {
|
|
537
|
+
return `
|
|
538
|
+
WITH ${crashSlot(schema)},
|
|
539
|
+
crashed AS (
|
|
540
|
+
SELECT id, crash_restarts, crash_restarts_since, heartbeat_on,
|
|
541
|
+
row_number() OVER (ORDER BY started_on, id) as n,
|
|
542
|
+
count(*) OVER () as k
|
|
543
|
+
FROM streak
|
|
544
|
+
WHERE ${crashDead(schema)}
|
|
545
|
+
),
|
|
546
|
+
head AS (
|
|
547
|
+
SELECT crash_restarts as base, COALESCE(crash_restarts_since, heartbeat_on) as since, k
|
|
548
|
+
FROM crashed WHERE n = 1
|
|
549
|
+
),
|
|
550
|
+
counts AS (
|
|
551
|
+
SELECT c.id, h.base + c.n - 1 as restarts, h.since
|
|
552
|
+
FROM crashed c, head h
|
|
553
|
+
WHERE c.n > 1
|
|
554
|
+
UNION ALL
|
|
555
|
+
SELECT me.id, COALESCE(h.base + h.k, 0), h.since
|
|
556
|
+
FROM me LEFT JOIN head h ON true
|
|
557
|
+
)
|
|
558
|
+
UPDATE ${schema}.instance i
|
|
559
|
+
SET crash_restarts = counts.restarts::int,
|
|
560
|
+
crash_restarts_since = counts.since
|
|
561
|
+
FROM counts
|
|
562
|
+
WHERE i.id = counts.id
|
|
563
|
+
`;
|
|
564
|
+
}
|
|
565
|
+
// When the rows this count could not yet judge would go quiet, or null when there are none: the
|
|
566
|
+
// registrar counts again then.
|
|
567
|
+
export function crashRecountAt(schema) {
|
|
568
|
+
return `
|
|
569
|
+
WITH ${crashSlot(schema)}
|
|
570
|
+
SELECT max(heartbeat_on + heartbeat_seconds * ${INSTANCE_QUIET_BEATS} * interval '1 second') as "recountAt"
|
|
571
|
+
FROM streak
|
|
572
|
+
WHERE NOT ${crashDead(schema)}
|
|
573
|
+
`;
|
|
574
|
+
}
|
|
575
|
+
// Instance registry statements. $1 is the instance id throughout. Registering replaces the row, so a
|
|
576
|
+
// PgBoss object restarted after stop() reads as live again from a new started_on. A heartbeat is an
|
|
577
|
+
// upsert too: a row pruned while its process was paused (a laptop asleep past the retention) comes
|
|
578
|
+
// back on the next beat, keeping the started_on the instance registered with ($20).
|
|
579
|
+
const INSTANCE_COLUMNS = `id, name, host, pid, version, node_version, heartbeat_seconds,
|
|
580
|
+
supervise, schedule, migrate, persist_queue_stats, persist_warnings,
|
|
581
|
+
pool_max, pool_total, pool_idle, pool_waiting, workers, metrics, config, application_name, started_on, heartbeat_on`;
|
|
582
|
+
const INSTANCE_VALUES = `$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17::text::jsonb, $18::text::jsonb, $19::text::jsonb,
|
|
583
|
+
current_setting('application_name')`;
|
|
584
|
+
const INSTANCE_BEAT = `pool_max = EXCLUDED.pool_max,
|
|
585
|
+
pool_total = EXCLUDED.pool_total,
|
|
586
|
+
pool_idle = EXCLUDED.pool_idle,
|
|
587
|
+
pool_waiting = EXCLUDED.pool_waiting,
|
|
588
|
+
workers = EXCLUDED.workers,
|
|
589
|
+
metrics = EXCLUDED.metrics,
|
|
590
|
+
heartbeat_on = EXCLUDED.heartbeat_on`;
|
|
591
|
+
export function registerInstance(schema) {
|
|
592
|
+
return `
|
|
593
|
+
INSERT INTO ${schema}.instance (${INSTANCE_COLUMNS})
|
|
594
|
+
VALUES (${INSTANCE_VALUES}, ${schema}.job_now(), ${schema}.job_now())
|
|
595
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
596
|
+
name = EXCLUDED.name,
|
|
597
|
+
host = EXCLUDED.host,
|
|
598
|
+
pid = EXCLUDED.pid,
|
|
599
|
+
version = EXCLUDED.version,
|
|
600
|
+
node_version = EXCLUDED.node_version,
|
|
601
|
+
heartbeat_seconds = EXCLUDED.heartbeat_seconds,
|
|
602
|
+
supervise = EXCLUDED.supervise,
|
|
603
|
+
schedule = EXCLUDED.schedule,
|
|
604
|
+
migrate = EXCLUDED.migrate,
|
|
605
|
+
persist_queue_stats = EXCLUDED.persist_queue_stats,
|
|
606
|
+
persist_warnings = EXCLUDED.persist_warnings,
|
|
607
|
+
application_name = EXCLUDED.application_name,
|
|
608
|
+
config = EXCLUDED.config,
|
|
609
|
+
started_on = EXCLUDED.started_on,
|
|
610
|
+
stopped_on = NULL,
|
|
611
|
+
crash_restarts = 0,
|
|
612
|
+
crash_restarts_since = NULL,
|
|
613
|
+
${INSTANCE_BEAT}
|
|
614
|
+
RETURNING started_on as "startedOn"
|
|
615
|
+
`;
|
|
616
|
+
}
|
|
617
|
+
export function heartbeatInstance(schema) {
|
|
618
|
+
return `
|
|
619
|
+
INSERT INTO ${schema}.instance (${INSTANCE_COLUMNS})
|
|
620
|
+
VALUES (${INSTANCE_VALUES}, $20::timestamptz, ${schema}.job_now())
|
|
621
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
622
|
+
${INSTANCE_BEAT}
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
export function stopInstance(schema) {
|
|
626
|
+
return `UPDATE ${schema}.instance SET stopped_on = ${schema}.job_now() WHERE id = $1`;
|
|
627
|
+
}
|
|
628
|
+
// Rows whose heartbeat has not moved for the retention, stopped or quiet alike. A live instance's
|
|
629
|
+
// row never qualifies, since its heartbeat keeps moving.
|
|
630
|
+
export function deleteOldInstances(schema, days) {
|
|
631
|
+
return `
|
|
632
|
+
DELETE FROM ${schema}.instance
|
|
633
|
+
WHERE heartbeat_on < ${schema}.job_now() - interval '${days} days'
|
|
634
|
+
`;
|
|
635
|
+
}
|
|
636
|
+
export function getInstances(schema) {
|
|
637
|
+
return `
|
|
638
|
+
SELECT
|
|
639
|
+
id,
|
|
640
|
+
name,
|
|
641
|
+
host,
|
|
642
|
+
pid,
|
|
643
|
+
version,
|
|
644
|
+
node_version as "nodeVersion",
|
|
645
|
+
application_name as "applicationName",
|
|
646
|
+
heartbeat_seconds as "heartbeatSeconds",
|
|
647
|
+
supervise,
|
|
648
|
+
schedule,
|
|
649
|
+
migrate,
|
|
650
|
+
persist_queue_stats as "persistQueueStats",
|
|
651
|
+
persist_warnings as "persistWarnings",
|
|
652
|
+
pool_max as "poolMax",
|
|
653
|
+
pool_total as "poolTotal",
|
|
654
|
+
pool_idle as "poolIdle",
|
|
655
|
+
pool_waiting as "poolWaiting",
|
|
656
|
+
workers,
|
|
657
|
+
metrics,
|
|
658
|
+
config,
|
|
659
|
+
crash_restarts as "crashRestarts",
|
|
660
|
+
crash_restarts_since as "crashRestartsSince",
|
|
661
|
+
started_on as "startedOn",
|
|
662
|
+
heartbeat_on as "heartbeatOn",
|
|
663
|
+
stopped_on as "stoppedOn",
|
|
664
|
+
stopped_on IS NULL AND heartbeat_on >= ${schema}.job_now() - heartbeat_seconds * ${INSTANCE_QUIET_BEATS} * interval '1 second' as live
|
|
665
|
+
FROM ${schema}.instance
|
|
666
|
+
ORDER BY started_on, id
|
|
667
|
+
`;
|
|
668
|
+
}
|
|
438
669
|
export function createIndexJobDependencyParent(schema) {
|
|
439
670
|
return `CREATE INDEX IF NOT EXISTS job_dep_parent_idx ON ${schema}.job_dependency (parent_name, parent_id)`;
|
|
440
671
|
}
|
|
672
|
+
/* eslint-disable no-restricted-syntax -- column defaults stay on the real clock: every pg-boss write names its timestamps through job_now() */
|
|
673
|
+
// One row per PgBoss object, written by the object itself at start() and on each heartbeat, so a
|
|
674
|
+
// database can say which instances share it and what each is doing. A crashed process never sets
|
|
675
|
+
// stopped_on; it goes quiet instead, when heartbeat_on stops moving, and heartbeat_seconds says how
|
|
676
|
+
// long that takes for this row, since the interval is per instance. workers is one entry per work()
|
|
677
|
+
// call. The pool columns are node-postgres's counts at the last heartbeat, null for a pool pg-boss
|
|
678
|
+
// was handed and cannot read. metrics is the process's CPU, memory and event loop at the last
|
|
679
|
+
// heartbeat against its container's limits (see nurse.ts), null until the first sample lands.
|
|
680
|
+
// config is the options it runs with, as the registrar's allowlist picks them, for comparing instances.
|
|
681
|
+
// application_name is the one the registering session carried, so pg_stat_activity joins to the row
|
|
682
|
+
// wherever it is unique.
|
|
683
|
+
export function createTableInstance(schema) {
|
|
684
|
+
return `
|
|
685
|
+
CREATE TABLE ${schema}.instance (
|
|
686
|
+
id uuid PRIMARY KEY,
|
|
687
|
+
name text,
|
|
688
|
+
host text NOT NULL,
|
|
689
|
+
pid int NOT NULL,
|
|
690
|
+
version text NOT NULL,
|
|
691
|
+
node_version text NOT NULL,
|
|
692
|
+
application_name text,
|
|
693
|
+
heartbeat_seconds int NOT NULL,
|
|
694
|
+
supervise bool NOT NULL,
|
|
695
|
+
schedule bool NOT NULL,
|
|
696
|
+
migrate bool NOT NULL,
|
|
697
|
+
persist_queue_stats bool NOT NULL,
|
|
698
|
+
persist_warnings bool NOT NULL,
|
|
699
|
+
pool_max int,
|
|
700
|
+
pool_total int,
|
|
701
|
+
pool_idle int,
|
|
702
|
+
pool_waiting int,
|
|
703
|
+
workers jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
704
|
+
metrics jsonb,
|
|
705
|
+
config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
706
|
+
crash_restarts int NOT NULL DEFAULT 0,
|
|
707
|
+
crash_restarts_since timestamptz,
|
|
708
|
+
started_on timestamptz NOT NULL DEFAULT now(),
|
|
709
|
+
heartbeat_on timestamptz NOT NULL DEFAULT now(),
|
|
710
|
+
stopped_on timestamptz
|
|
711
|
+
)
|
|
712
|
+
`;
|
|
713
|
+
}
|
|
714
|
+
/* eslint-enable no-restricted-syntax */
|
|
441
715
|
// Anchored so a schema name that itself contains these substrings (e.g. `job_intake`) isn't
|
|
442
716
|
// mangled: `\.job\y` matches only the base table reference (`schema.job`, not `schema.job_i5` whose
|
|
443
717
|
// `job` is followed by `_`, nor `.job_dependency`), and `\yjob_i(\d+)` matches only the bare
|
|
@@ -583,7 +857,9 @@ function createTableJob(schema, noPartitioning = false) {
|
|
|
583
857
|
source_created_on timestamp with time zone,
|
|
584
858
|
source_retry_count int,
|
|
585
859
|
source_output jsonb,
|
|
586
|
-
source_root_id uuid
|
|
860
|
+
source_root_id uuid,
|
|
861
|
+
trace_context jsonb,
|
|
862
|
+
upsert_by_key bool
|
|
587
863
|
) ${partitionClause}
|
|
588
864
|
`;
|
|
589
865
|
}
|
|
@@ -638,6 +914,7 @@ function createTableJobCommon(schema) {
|
|
|
638
914
|
SELECT ${schema}.job_table_run($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, '${COMMON_JOB_TABLE}');
|
|
639
915
|
SELECT ${schema}.job_table_run($cmd$${createIndexJobBlocking(schema)}$cmd$, '${COMMON_JOB_TABLE}');
|
|
640
916
|
SELECT ${schema}.job_table_run($cmd$${createIndexJobSourceRoot(schema)}$cmd$, '${COMMON_JOB_TABLE}');
|
|
917
|
+
SELECT ${schema}.job_table_run($cmd$${createIndexJobUpsert(schema)}$cmd$, '${COMMON_JOB_TABLE}');
|
|
641
918
|
|
|
642
919
|
ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.${COMMON_JOB_TABLE} DEFAULT;
|
|
643
920
|
`;
|
|
@@ -659,6 +936,7 @@ function createTableJobIndexes(schema, noDeferrableConstraints = false, noCoveri
|
|
|
659
936
|
${createIndexJobGroupConcurrency(schema)};
|
|
660
937
|
${createIndexJobBlocking(schema)};
|
|
661
938
|
${createIndexJobSourceRoot(schema)};
|
|
939
|
+
${createIndexJobUpsert(schema)};
|
|
662
940
|
`;
|
|
663
941
|
}
|
|
664
942
|
function createQueueFunction(schema, noPartitioning = false) {
|
|
@@ -782,6 +1060,7 @@ function createQueueFunction(schema, noPartitioning = false) {
|
|
|
782
1060
|
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, tablename);
|
|
783
1061
|
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobBlocking(schema)}$cmd$, tablename);
|
|
784
1062
|
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobSourceRoot(schema)}$cmd$, tablename);
|
|
1063
|
+
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobUpsert(schema)}$cmd$, tablename);
|
|
785
1064
|
|
|
786
1065
|
IF options->>'policy' = 'short' THEN
|
|
787
1066
|
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobPolicyShort(schema)}$cmd$, tablename);
|
|
@@ -864,8 +1143,32 @@ export function notifyChannelSql(schema) {
|
|
|
864
1143
|
export function notifyQueue(schema, name) {
|
|
865
1144
|
return `SELECT pg_notify(${notifyChannelSql(schema)}, '${name}')`;
|
|
866
1145
|
}
|
|
867
|
-
|
|
868
|
-
|
|
1146
|
+
// Dropping a queue's own table (partition: true) needs ACCESS EXCLUSIVE on it, on job and on job_common,
|
|
1147
|
+
// and an insert into job_common or a query through job takes those in other orders. So the locks are
|
|
1148
|
+
// taken first, all with NOWAIT: the transaction never waits holding one, so it can never be part of a
|
|
1149
|
+
// deadlock, and the caller tries again when one is busy. The table is read in the same transaction, so
|
|
1150
|
+
// a stale cache cannot name the wrong one, and a queue already gone is left alone.
|
|
1151
|
+
export function deleteQueue(schema, name, noAdvisoryLocks, partitioned = false) {
|
|
1152
|
+
const sql = partitioned
|
|
1153
|
+
? `
|
|
1154
|
+
DO $$
|
|
1155
|
+
DECLARE
|
|
1156
|
+
v_table text;
|
|
1157
|
+
v_partition bool;
|
|
1158
|
+
BEGIN
|
|
1159
|
+
SELECT table_name, partition FROM ${schema}.queue WHERE name = '${name}' INTO v_table, v_partition;
|
|
1160
|
+
IF NOT FOUND THEN
|
|
1161
|
+
RETURN;
|
|
1162
|
+
END IF;
|
|
1163
|
+
|
|
1164
|
+
IF v_partition AND to_regclass(format('${schema}.%I', v_table)) IS NOT NULL THEN
|
|
1165
|
+
EXECUTE format('LOCK TABLE ${schema}.${COMMON_JOB_TABLE}, ${schema}.%I, ${schema}.${BASE_JOB_TABLE} IN ACCESS EXCLUSIVE MODE NOWAIT', v_table);
|
|
1166
|
+
END IF;
|
|
1167
|
+
|
|
1168
|
+
PERFORM ${schema}.delete_queue('${name}');
|
|
1169
|
+
END $$
|
|
1170
|
+
`
|
|
1171
|
+
: `SELECT ${schema}.delete_queue('${name}')`;
|
|
869
1172
|
return locked(schema, sql, 'delete-queue', noAdvisoryLocks);
|
|
870
1173
|
}
|
|
871
1174
|
function createPrimaryKeyJob(schema) {
|
|
@@ -959,6 +1262,14 @@ function createIndexJobBlocking(schema) {
|
|
|
959
1262
|
function createIndexJobSourceRoot(schema) {
|
|
960
1263
|
return `CREATE INDEX job_i12 ON ${schema}.job (source_root_id) WHERE source_root_id IS NOT NULL`;
|
|
961
1264
|
}
|
|
1265
|
+
// At most one waiting job per key among the jobs upsert() inserted by singletonKey, which is what makes
|
|
1266
|
+
// concurrent upserts of one key agree: the second insert waits on the first's index entry, conflicts,
|
|
1267
|
+
// and edits that job instead. upsert_by_key is set only on those inserts, so send() keeps its own policy's
|
|
1268
|
+
// rules and a queue that never upserts carries the index empty. Only created, not retry, so a failing
|
|
1269
|
+
// job never collides with a newer upsert on its way back to retry.
|
|
1270
|
+
function createIndexJobUpsert(schema) {
|
|
1271
|
+
return `CREATE UNIQUE INDEX job_i13 ON ${schema}.job (name, singleton_key) WHERE state = '${JOB_STATES.created}' AND upsert_by_key`;
|
|
1272
|
+
}
|
|
962
1273
|
// The interval claim for a monitor pass. It stamps monitor_claim_on, never monitor_on, which only
|
|
963
1274
|
// the stats aggregate writes, so a pass that skips the aggregate leaves capturedOn aging.
|
|
964
1275
|
//
|
|
@@ -1178,6 +1489,9 @@ export function updateQueue(schema) {
|
|
|
1178
1489
|
WHERE name = $1
|
|
1179
1490
|
`;
|
|
1180
1491
|
}
|
|
1492
|
+
export function currentDatabase() {
|
|
1493
|
+
return 'SELECT current_database() AS name';
|
|
1494
|
+
}
|
|
1181
1495
|
export function getQueues(schema, names) {
|
|
1182
1496
|
const hasNames = names && names.length > 0;
|
|
1183
1497
|
return {
|
|
@@ -1197,6 +1511,7 @@ export function getQueues(schema, names) {
|
|
|
1197
1511
|
q.notify,
|
|
1198
1512
|
q.dead_letter as "deadLetter",
|
|
1199
1513
|
q.deferred_count as "deferredCount",
|
|
1514
|
+
q.blocked_count as "blockedCount",
|
|
1200
1515
|
q.warning_queued as "warningQueueSize",
|
|
1201
1516
|
q.queued_count as "queuedCount",
|
|
1202
1517
|
q.ready_count as "readyCount",
|
|
@@ -1208,6 +1523,9 @@ export function getQueues(schema, names) {
|
|
|
1208
1523
|
q.failed_delta as "failedDelta",
|
|
1209
1524
|
q.delta_seconds as "deltaSeconds",
|
|
1210
1525
|
q.delta_on as "deltaOn",
|
|
1526
|
+
q.wait_bins as "waitBins",
|
|
1527
|
+
q.run_bins as "runBins",
|
|
1528
|
+
q.ready_oldest_seconds as "readyOldestSeconds",
|
|
1211
1529
|
q.singletons_active as "singletonsActive",
|
|
1212
1530
|
q.table_name as "table",
|
|
1213
1531
|
q.created_on as "createdOn",
|
|
@@ -1247,6 +1565,7 @@ export function zeroQueueStats(schema, one) {
|
|
|
1247
1565
|
return `
|
|
1248
1566
|
UPDATE ${schema}.queue SET
|
|
1249
1567
|
deferred_count = 0,
|
|
1568
|
+
blocked_count = 0,
|
|
1250
1569
|
queued_count = 0,
|
|
1251
1570
|
ready_count = 0,
|
|
1252
1571
|
active_count = 0,
|
|
@@ -1433,6 +1752,9 @@ export function createTableQueueStats(schema, noPartitioning = false) {
|
|
|
1433
1752
|
failed_delta int,
|
|
1434
1753
|
delta_seconds int,
|
|
1435
1754
|
delta_on timestamptz,
|
|
1755
|
+
wait_bins int[],
|
|
1756
|
+
run_bins int[],
|
|
1757
|
+
ready_oldest_seconds int,
|
|
1436
1758
|
captured_on timestamptz NOT NULL DEFAULT now(),
|
|
1437
1759
|
${noPartitioning ? 'PRIMARY KEY (id)' : 'PRIMARY KEY (id, captured_on)'}
|
|
1438
1760
|
) ${noPartitioning ? '' : 'PARTITION BY RANGE (captured_on)'}
|
|
@@ -1525,9 +1847,11 @@ export function insertQueueStats(schema, queues, noAdvisoryLocks) {
|
|
|
1525
1847
|
const sql = `
|
|
1526
1848
|
INSERT INTO ${schema}.queue_stats
|
|
1527
1849
|
(name, deferred_count, queued_count, ready_count, active_count, failed_count, total_count,
|
|
1528
|
-
created_delta, completed_delta, failed_delta, delta_seconds, delta_on,
|
|
1850
|
+
created_delta, completed_delta, failed_delta, delta_seconds, delta_on,
|
|
1851
|
+
wait_bins, run_bins, ready_oldest_seconds, captured_on)
|
|
1529
1852
|
SELECT name, deferred_count, queued_count, ready_count, active_count, failed_count, total_count,
|
|
1530
|
-
created_delta, completed_delta, failed_delta, delta_seconds, delta_on,
|
|
1853
|
+
created_delta, completed_delta, failed_delta, delta_seconds, delta_on,
|
|
1854
|
+
wait_bins, run_bins, ready_oldest_seconds, ${schema}.job_now()
|
|
1531
1855
|
FROM ${schema}.queue
|
|
1532
1856
|
WHERE name = ANY(${serializeArrayParam(queues)})
|
|
1533
1857
|
`;
|
|
@@ -1560,6 +1884,9 @@ export function getQueueStatsCache(schema) {
|
|
|
1560
1884
|
failed_delta as "failedDelta",
|
|
1561
1885
|
delta_seconds as "deltaSeconds",
|
|
1562
1886
|
delta_on as "deltaOn",
|
|
1887
|
+
wait_bins as "waitBins",
|
|
1888
|
+
run_bins as "runBins",
|
|
1889
|
+
ready_oldest_seconds as "readyOldestSeconds",
|
|
1563
1890
|
table_name as "table",
|
|
1564
1891
|
monitor_on as "capturedOn",
|
|
1565
1892
|
(extract(epoch from (${schema}.job_now() - monitor_on)) * 1000)::float8 as "cacheAgeMs",
|
|
@@ -1583,6 +1910,9 @@ export function getQueueStatsHistory(schema) {
|
|
|
1583
1910
|
failed_delta as "failedDelta",
|
|
1584
1911
|
delta_seconds as "deltaSeconds",
|
|
1585
1912
|
delta_on as "deltaOn",
|
|
1913
|
+
wait_bins as "waitBins",
|
|
1914
|
+
run_bins as "runBins",
|
|
1915
|
+
ready_oldest_seconds as "readyOldestSeconds",
|
|
1586
1916
|
captured_on as "capturedOn"
|
|
1587
1917
|
FROM ${schema}.queue_stats
|
|
1588
1918
|
WHERE name = $1
|
|
@@ -1636,6 +1966,11 @@ const STATS_AGG = {
|
|
|
1636
1966
|
// YugabyteDB, none of which can rely on it. to_timestamp / extract(epoch) / floor exist on all of
|
|
1637
1967
|
// them (extract returns double on PG13, numeric on PG14+; floor/division handle both identically),
|
|
1638
1968
|
// and buckets align to the Unix epoch so their boundaries are stable across calls.
|
|
1969
|
+
//
|
|
1970
|
+
// Wait and run histograms are added up per returned bucket in SQL (passes, slots, histograms), so a
|
|
1971
|
+
// bucket comes back as one histogram however many passes it covers. Each pass lands in the bucket
|
|
1972
|
+
// its counters were placed in, then its counts are summed per bucket and slot, a slot with no job in
|
|
1973
|
+
// any pass as 0. A bucket no pass measured has no histogram row and comes back null.
|
|
1639
1974
|
export function getQueueStatsHistoryBucketed(schema, aggregate, mode) {
|
|
1640
1975
|
const agg = STATS_AGG[aggregate];
|
|
1641
1976
|
const widthCte = mode === 'auto'
|
|
@@ -1651,7 +1986,7 @@ export function getQueueStatsHistoryBucketed(schema, aggregate, mode) {
|
|
|
1651
1986
|
FROM extent
|
|
1652
1987
|
),
|
|
1653
1988
|
w AS (
|
|
1654
|
-
SELECT greatest(1, ceil(extract(epoch from (hi - lo))::float8 / greatest($5, 1)::float8)::bigint)
|
|
1989
|
+
SELECT greatest(1, ceil(extract(epoch from (hi - lo))::float8 / greatest($5, 1)::float8)::bigint) AS secs
|
|
1655
1990
|
FROM bounds
|
|
1656
1991
|
)`
|
|
1657
1992
|
: 'WITH w AS (SELECT greatest($5, 1)::bigint AS secs)';
|
|
@@ -1686,7 +2021,8 @@ export function getQueueStatsHistoryBucketed(schema, aggregate, mode) {
|
|
|
1686
2021
|
sum(completed_delta)::int as "completedDelta",
|
|
1687
2022
|
sum(failed_delta)::int as "failedDelta",
|
|
1688
2023
|
sum(delta_seconds)::int as "deltaSeconds",
|
|
1689
|
-
max(delta_on) as "deltaOn"
|
|
2024
|
+
max(delta_on) as "deltaOn",
|
|
2025
|
+
max(ready_oldest_seconds) as "readyOldestSeconds"
|
|
1690
2026
|
FROM ${schema}.queue_stats, w
|
|
1691
2027
|
WHERE name = $1
|
|
1692
2028
|
AND delta_on IS NOT NULL
|
|
@@ -1710,10 +2046,37 @@ export function getQueueStatsHistoryBucketed(schema, aggregate, mode) {
|
|
|
1710
2046
|
c."completedDelta",
|
|
1711
2047
|
c."failedDelta",
|
|
1712
2048
|
c."deltaSeconds",
|
|
1713
|
-
c."deltaOn"
|
|
2049
|
+
c."deltaOn",
|
|
2050
|
+
c."readyOldestSeconds",
|
|
2051
|
+
c.bucket as "counterBucket"
|
|
1714
2052
|
FROM gauges g
|
|
1715
2053
|
FULL JOIN counters c ON c.bucket = g.bucket
|
|
1716
|
-
)
|
|
2054
|
+
),
|
|
2055
|
+
passes AS (
|
|
2056
|
+
SELECT ${bucket('delta_on')} as "counterBucket", wait_bins, run_bins
|
|
2057
|
+
FROM ${schema}.queue_stats, w
|
|
2058
|
+
WHERE name = $1
|
|
2059
|
+
AND delta_on IS NOT NULL
|
|
2060
|
+
AND wait_bins IS NOT NULL
|
|
2061
|
+
AND ($2::timestamptz IS NULL OR delta_on >= $2)
|
|
2062
|
+
AND ($3::timestamptz IS NULL OR delta_on <= $3)
|
|
2063
|
+
),
|
|
2064
|
+
slots AS (
|
|
2065
|
+
SELECT p.bucket, u.slot, coalesce(sum(u.w), 0)::int AS w, coalesce(sum(u.r), 0)::int AS r
|
|
2066
|
+
FROM (SELECT DISTINCT bucket, "counterBucket" FROM placed) p
|
|
2067
|
+
JOIN passes ps ON ps."counterBucket" = p."counterBucket",
|
|
2068
|
+
unnest(ps.wait_bins, ps.run_bins) WITH ORDINALITY AS u(w, r, slot)
|
|
2069
|
+
GROUP BY 1, 2
|
|
2070
|
+
),
|
|
2071
|
+
histograms AS (
|
|
2072
|
+
SELECT
|
|
2073
|
+
bucket,
|
|
2074
|
+
array_agg(w ORDER BY slot) as "waitBins",
|
|
2075
|
+
array_agg(r ORDER BY slot) as "runBins"
|
|
2076
|
+
FROM slots
|
|
2077
|
+
GROUP BY 1
|
|
2078
|
+
),
|
|
2079
|
+
folded AS (
|
|
1717
2080
|
SELECT
|
|
1718
2081
|
bucket as "capturedOn",
|
|
1719
2082
|
max("deferredCount")::int as "deferredCount",
|
|
@@ -1726,11 +2089,16 @@ export function getQueueStatsHistoryBucketed(schema, aggregate, mode) {
|
|
|
1726
2089
|
sum("completedDelta")::int as "completedDelta",
|
|
1727
2090
|
sum("failedDelta")::int as "failedDelta",
|
|
1728
2091
|
sum("deltaSeconds")::int as "deltaSeconds",
|
|
1729
|
-
max("deltaOn") as "deltaOn"
|
|
2092
|
+
max("deltaOn") as "deltaOn",
|
|
2093
|
+
max("readyOldestSeconds")::int as "readyOldestSeconds"
|
|
1730
2094
|
FROM placed
|
|
1731
2095
|
WHERE bucket IS NOT NULL
|
|
1732
2096
|
GROUP BY 1
|
|
1733
|
-
|
|
2097
|
+
)
|
|
2098
|
+
SELECT f.*, h."waitBins", h."runBins"
|
|
2099
|
+
FROM folded f
|
|
2100
|
+
LEFT JOIN histograms h ON h.bucket = f."capturedOn"
|
|
2101
|
+
ORDER BY f."capturedOn" DESC
|
|
1734
2102
|
LIMIT ${limit}
|
|
1735
2103
|
`;
|
|
1736
2104
|
}
|
|
@@ -1837,7 +2205,7 @@ function buildFetchParams(options) {
|
|
|
1837
2205
|
* exceeds fetch time.
|
|
1838
2206
|
*/
|
|
1839
2207
|
export function fetchNextJob(options, noSkipLocked = false) {
|
|
1840
|
-
const { schema, table, name, policy, limit, includeMetadata, ignoreStartAfter = false, groupConcurrency, minPriority, maxPriority } = options;
|
|
2208
|
+
const { schema, table, name, policy, limit, includeMetadata, includeTraceContext = false, ignoreStartAfter = false, groupConcurrency, minPriority, maxPriority } = options;
|
|
1841
2209
|
const keyStrictFifo = policy === QUEUE_POLICIES.key_strict_fifo;
|
|
1842
2210
|
const singletonFetch = limit > 1 && (policy === QUEUE_POLICIES.singleton || policy === QUEUE_POLICIES.stately);
|
|
1843
2211
|
const hasIgnoreSingletons = options.ignoreSingletons != null && options.ignoreSingletons.length > 0;
|
|
@@ -2025,7 +2393,7 @@ export function fetchNextJob(options, noSkipLocked = false) {
|
|
|
2025
2393
|
WHERE name = '${name}' AND ${updateMatch}
|
|
2026
2394
|
${singletonFetch && !hasGroupConcurrency ? 'AND singleton_rn = 1' : ''}
|
|
2027
2395
|
${distributedStateCheck}
|
|
2028
|
-
RETURNING j.${includeMetadata ? JOB_COLUMNS_ALL : JOB_COLUMNS_MIN}
|
|
2396
|
+
RETURNING j.${includeMetadata ? JOB_COLUMNS_ALL : JOB_COLUMNS_MIN}${includeTraceContext ? ', j.trace_context as "__traceContext"' : ''}
|
|
2029
2397
|
`,
|
|
2030
2398
|
values: params.values
|
|
2031
2399
|
};
|
|
@@ -2107,10 +2475,16 @@ function lockedChildrenCte(schema) {
|
|
|
2107
2475
|
FOR UPDATE OF j
|
|
2108
2476
|
)`;
|
|
2109
2477
|
}
|
|
2478
|
+
// A child released by its last parent has its start_after moved up to the release, so its wait (in
|
|
2479
|
+
// the monitor's histograms and ready_oldest_seconds) counts from when it could first run rather than
|
|
2480
|
+
// from when the flow was sent. A start_after still in the future is kept.
|
|
2110
2481
|
function unblockChildrenUpdate(schema) {
|
|
2111
2482
|
return `UPDATE ${schema}.job j
|
|
2112
2483
|
SET pending_dependencies = GREATEST(j.pending_dependencies - lc.n, 0),
|
|
2113
|
-
blocked = GREATEST(j.pending_dependencies - lc.n, 0) > 0
|
|
2484
|
+
blocked = GREATEST(j.pending_dependencies - lc.n, 0) > 0,
|
|
2485
|
+
start_after = CASE WHEN GREATEST(j.pending_dependencies - lc.n, 0) = 0
|
|
2486
|
+
THEN GREATEST(j.start_after, ${schema}.job_now())
|
|
2487
|
+
ELSE j.start_after END
|
|
2114
2488
|
FROM locked_children lc
|
|
2115
2489
|
WHERE j.name = lc.name
|
|
2116
2490
|
AND j.id = lc.id`;
|
|
@@ -2184,12 +2558,19 @@ export function cancelJobs(schema, table, fenced) {
|
|
|
2184
2558
|
${settledCountAndIds()}
|
|
2185
2559
|
`;
|
|
2186
2560
|
}
|
|
2561
|
+
// A resumed job's start_after moves up to now, as a released flow child's does, so its wait (in the
|
|
2562
|
+
// monitor's histograms and ready_oldest_seconds) counts from when it could run again rather than
|
|
2563
|
+
// from when it was first sent. A start_after still in the future is kept. It also loses upsert_by_key,
|
|
2564
|
+
// here and in restoreJobs, so it never collides in job_i13 with a newer job upserted by the same key,
|
|
2565
|
+
// and queues beside it.
|
|
2187
2566
|
export function resumeJobs(schema, table) {
|
|
2188
2567
|
return `
|
|
2189
2568
|
WITH results as (
|
|
2190
2569
|
UPDATE ${schema}.${table}
|
|
2191
2570
|
SET completed_on = NULL,
|
|
2192
|
-
state = '${JOB_STATES.created}'
|
|
2571
|
+
state = '${JOB_STATES.created}',
|
|
2572
|
+
upsert_by_key = NULL,
|
|
2573
|
+
start_after = GREATEST(start_after, ${schema}.job_now())
|
|
2193
2574
|
WHERE name = $1
|
|
2194
2575
|
AND id = ANY($2::uuid[])
|
|
2195
2576
|
AND state = '${JOB_STATES.cancelled}'
|
|
@@ -2203,7 +2584,8 @@ export function restoreJobs(schema, table) {
|
|
|
2203
2584
|
UPDATE ${schema}.${table}
|
|
2204
2585
|
SET state = '${JOB_STATES.created}',
|
|
2205
2586
|
started_on = NULL,
|
|
2206
|
-
heartbeat_on = NULL
|
|
2587
|
+
heartbeat_on = NULL,
|
|
2588
|
+
upsert_by_key = NULL
|
|
2207
2589
|
WHERE name = $1
|
|
2208
2590
|
AND id = ANY($2::uuid[])
|
|
2209
2591
|
`;
|
|
@@ -2228,7 +2610,11 @@ export function restoreJobs(schema, table) {
|
|
|
2228
2610
|
function isDateTimeString(expression) {
|
|
2229
2611
|
return `(right(${expression}, 1) = 'Z' OR ${expression} ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}')`;
|
|
2230
2612
|
}
|
|
2231
|
-
|
|
2613
|
+
// The queue is LEFT JOINed and every NOT NULL column it supplies falls back to the schema default, so
|
|
2614
|
+
// a job for a queue deleted after the caller cached it still reaches q_fkey and fails there (23503),
|
|
2615
|
+
// rather than producing no row, which would read the same as a singleton or throttle refusal. The
|
|
2616
|
+
// fallbacks never apply to a queue that exists, since its own columns are NOT NULL.
|
|
2617
|
+
export function insertJobs(schema, { table, name, returnId = true, notify = false, slots = false, upsertByKey = false }) {
|
|
2232
2618
|
// When notify is enabled we always RETURN start_after so the wrapper below can gate
|
|
2233
2619
|
// the NOTIFY on immediate availability, regardless of whether the caller wants ids.
|
|
2234
2620
|
const returning = notify ? 'RETURNING id, start_after' : returnId ? 'RETURNING id' : '';
|
|
@@ -2264,7 +2650,8 @@ export function insertJobs(schema, { table, name, returnId = true, notify = fals
|
|
|
2264
2650
|
heartbeat_seconds,
|
|
2265
2651
|
blocked,
|
|
2266
2652
|
blocking,
|
|
2267
|
-
pending_dependencies
|
|
2653
|
+
pending_dependencies,
|
|
2654
|
+
trace_context${upsertByKey ? ', upsert_by_key' : ''}
|
|
2268
2655
|
)
|
|
2269
2656
|
SELECT
|
|
2270
2657
|
COALESCE(id, gen_random_uuid()) as id,
|
|
@@ -2281,19 +2668,20 @@ export function insertJobs(schema, { table, name, returnId = true, notify = fals
|
|
|
2281
2668
|
END as singleton_on,
|
|
2282
2669
|
"groupId" as group_id,
|
|
2283
2670
|
"groupTier" as group_tier,
|
|
2284
|
-
COALESCE("expireInSeconds", q.expire_seconds) as expire_seconds,
|
|
2285
|
-
COALESCE("deleteAfterSeconds", q.deletion_seconds) as deletion_seconds,
|
|
2286
|
-
j.start_after + (COALESCE("retentionSeconds", q.retention_seconds) * interval '1s') as keep_until,
|
|
2287
|
-
COALESCE("retryLimit", q.retry_limit) as retry_limit,
|
|
2288
|
-
COALESCE("retryDelay", q.retry_delay) as retry_delay,
|
|
2289
|
-
COALESCE("retryBackoff", q.retry_backoff,
|
|
2671
|
+
COALESCE("expireInSeconds", q.expire_seconds, ${QUEUE_DEFAULTS.expire_seconds}) as expire_seconds,
|
|
2672
|
+
COALESCE("deleteAfterSeconds", q.deletion_seconds, ${QUEUE_DEFAULTS.deletion_seconds}) as deletion_seconds,
|
|
2673
|
+
j.start_after + (COALESCE("retentionSeconds", q.retention_seconds, ${QUEUE_DEFAULTS.retention_seconds}) * interval '1s') as keep_until,
|
|
2674
|
+
COALESCE("retryLimit", q.retry_limit, ${QUEUE_DEFAULTS.retry_limit}) as retry_limit,
|
|
2675
|
+
COALESCE("retryDelay", q.retry_delay, ${QUEUE_DEFAULTS.retry_delay}) as retry_delay,
|
|
2676
|
+
COALESCE("retryBackoff", q.retry_backoff, ${QUEUE_DEFAULTS.retry_backoff}) as retry_backoff,
|
|
2290
2677
|
COALESCE("retryDelayMax", q.retry_delay_max) as retry_delay_max,
|
|
2291
2678
|
q.policy,
|
|
2292
2679
|
COALESCE("deadLetter", q.dead_letter) as dead_letter,
|
|
2293
2680
|
COALESCE("heartbeatSeconds", q.heartbeat_seconds) as heartbeat_seconds,
|
|
2294
2681
|
COALESCE(blocked, false) as blocked,
|
|
2295
2682
|
COALESCE(blocking, false) as blocking,
|
|
2296
|
-
COALESCE("pendingDependencies", 0) as pending_dependencies
|
|
2683
|
+
COALESCE("pendingDependencies", 0) as pending_dependencies,
|
|
2684
|
+
"__traceContext" as trace_context${upsertByKey ? ', true as upsert_by_key' : ''}
|
|
2297
2685
|
FROM (
|
|
2298
2686
|
SELECT *,
|
|
2299
2687
|
CASE
|
|
@@ -2322,10 +2710,11 @@ export function insertJobs(schema, { table, name, returnId = true, notify = fals
|
|
|
2322
2710
|
"heartbeatSeconds" integer,
|
|
2323
2711
|
blocked boolean,
|
|
2324
2712
|
blocking boolean,
|
|
2325
|
-
"pendingDependencies" integer
|
|
2713
|
+
"pendingDependencies" integer,
|
|
2714
|
+
"__traceContext" jsonb
|
|
2326
2715
|
)
|
|
2327
2716
|
) j
|
|
2328
|
-
JOIN ${schema}.queue q ON q.name = '${name}'
|
|
2717
|
+
LEFT JOIN ${schema}.queue q ON q.name = '${name}'
|
|
2329
2718
|
ON CONFLICT DO NOTHING
|
|
2330
2719
|
${returning}
|
|
2331
2720
|
`;
|
|
@@ -2478,7 +2867,8 @@ function failJobsBody(schema, table, where, output, forceTerminal = false) {
|
|
|
2478
2867
|
source_created_on,
|
|
2479
2868
|
source_retry_count,
|
|
2480
2869
|
source_output,
|
|
2481
|
-
source_root_id
|
|
2870
|
+
source_root_id,
|
|
2871
|
+
trace_context
|
|
2482
2872
|
)
|
|
2483
2873
|
SELECT
|
|
2484
2874
|
id,
|
|
@@ -2524,7 +2914,8 @@ function failJobsBody(schema, table, where, output, forceTerminal = false) {
|
|
|
2524
2914
|
source_created_on,
|
|
2525
2915
|
source_retry_count,
|
|
2526
2916
|
source_output,
|
|
2527
|
-
source_root_id
|
|
2917
|
+
source_root_id,
|
|
2918
|
+
trace_context
|
|
2528
2919
|
FROM deleted_jobs
|
|
2529
2920
|
ON CONFLICT DO NOTHING
|
|
2530
2921
|
RETURNING *
|
|
@@ -2565,7 +2956,8 @@ function failJobsBody(schema, table, where, output, forceTerminal = false) {
|
|
|
2565
2956
|
source_created_on,
|
|
2566
2957
|
source_retry_count,
|
|
2567
2958
|
source_output,
|
|
2568
|
-
source_root_id
|
|
2959
|
+
source_root_id,
|
|
2960
|
+
trace_context
|
|
2569
2961
|
)
|
|
2570
2962
|
SELECT
|
|
2571
2963
|
id,
|
|
@@ -2602,7 +2994,8 @@ function failJobsBody(schema, table, where, output, forceTerminal = false) {
|
|
|
2602
2994
|
source_created_on,
|
|
2603
2995
|
source_retry_count,
|
|
2604
2996
|
source_output,
|
|
2605
|
-
source_root_id
|
|
2997
|
+
source_root_id,
|
|
2998
|
+
trace_context
|
|
2606
2999
|
FROM deleted_jobs
|
|
2607
3000
|
WHERE id NOT IN (SELECT id from retried_jobs)
|
|
2608
3001
|
RETURNING *
|
|
@@ -2615,7 +3008,7 @@ function failJobsBody(schema, table, where, output, forceTerminal = false) {
|
|
|
2615
3008
|
dlq_jobs as (
|
|
2616
3009
|
INSERT INTO ${schema}.job (name, priority, data, retry_limit, retry_backoff, retry_delay, start_after, created_on, keep_until, deletion_seconds,
|
|
2617
3010
|
expire_seconds, singleton_key, group_id, group_tier, heartbeat_seconds,
|
|
2618
|
-
source_name, source_id, source_created_on, source_retry_count, source_output, source_root_id)
|
|
3011
|
+
source_name, source_id, source_created_on, source_retry_count, source_output, source_root_id, trace_context)
|
|
2619
3012
|
SELECT
|
|
2620
3013
|
r.dead_letter,
|
|
2621
3014
|
r.priority,
|
|
@@ -2637,7 +3030,8 @@ function failJobsBody(schema, table, where, output, forceTerminal = false) {
|
|
|
2637
3030
|
r.created_on,
|
|
2638
3031
|
r.retry_count,
|
|
2639
3032
|
r.output,
|
|
2640
|
-
COALESCE(r.source_root_id, r.id)
|
|
3033
|
+
COALESCE(r.source_root_id, r.id),
|
|
3034
|
+
r.trace_context
|
|
2641
3035
|
FROM results r
|
|
2642
3036
|
JOIN ${schema}.queue q ON q.name = r.dead_letter
|
|
2643
3037
|
WHERE state = '${JOB_STATES.failed}'
|
|
@@ -2838,10 +3232,10 @@ export function insertRetryJob(schema, table) {
|
|
|
2838
3232
|
group_id, group_tier, expire_seconds, deletion_seconds, created_on, completed_on,
|
|
2839
3233
|
keep_until, policy, output, dead_letter,
|
|
2840
3234
|
heartbeat_on, heartbeat_seconds, blocked, blocking, pending_dependencies,
|
|
2841
|
-
source_name, source_id, source_created_on, source_retry_count, source_output, source_root_id
|
|
3235
|
+
source_name, source_id, source_created_on, source_retry_count, source_output, source_root_id, trace_context
|
|
2842
3236
|
) VALUES (
|
|
2843
3237
|
$1, $2, $3, $4::text::jsonb, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22,
|
|
2844
|
-
$23::text::jsonb, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34::text::jsonb, $35
|
|
3238
|
+
$23::text::jsonb, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34::text::jsonb, $35, $36::text::jsonb
|
|
2845
3239
|
) ON CONFLICT DO NOTHING
|
|
2846
3240
|
RETURNING id
|
|
2847
3241
|
`;
|
|
@@ -2850,10 +3244,10 @@ export function insertDeadLetterJob(schema) {
|
|
|
2850
3244
|
return `
|
|
2851
3245
|
INSERT INTO ${schema}.job (name, data, priority, retry_limit, retry_backoff, retry_delay, start_after, created_on, keep_until, deletion_seconds,
|
|
2852
3246
|
expire_seconds, singleton_key, group_id, group_tier, heartbeat_seconds,
|
|
2853
|
-
source_name, source_id, source_created_on, source_retry_count, source_output, source_root_id)
|
|
3247
|
+
source_name, source_id, source_created_on, source_retry_count, source_output, source_root_id, trace_context)
|
|
2854
3248
|
SELECT $1, $2::text::jsonb, $9, q.retry_limit, q.retry_backoff, q.retry_delay, ${schema}.job_now(), ${schema}.job_now(), ${schema}.job_now() + q.retention_seconds * interval '1s', q.deletion_seconds,
|
|
2855
3249
|
q.expire_seconds, $8, $10, $11, q.heartbeat_seconds,
|
|
2856
|
-
$4, $5, $6, $7, $3::text::jsonb, COALESCE($12::uuid, $5::uuid)
|
|
3250
|
+
$4, $5, $6, $7, $3::text::jsonb, COALESCE($12::uuid, $5::uuid), $13::text::jsonb
|
|
2857
3251
|
FROM ${schema}.queue q WHERE q.name = $1
|
|
2858
3252
|
`;
|
|
2859
3253
|
}
|
|
@@ -2892,13 +3286,13 @@ function redriveWhere(schema, table) {
|
|
|
2892
3286
|
// Job-identity columns (priority, singleton_key, group_id, group_tier) are carried over instead.
|
|
2893
3287
|
const REDRIVE_INSERT_COLUMNS = `(id, name, data, priority, retry_limit, retry_backoff, retry_delay, retry_delay_max,
|
|
2894
3288
|
expire_seconds, start_after, created_on, keep_until, deletion_seconds, policy, singleton_key, group_id, group_tier,
|
|
2895
|
-
heartbeat_seconds, dead_letter, source_root_id)`;
|
|
3289
|
+
heartbeat_seconds, dead_letter, source_root_id, trace_context)`;
|
|
2896
3290
|
function redriveInsertValues(schema, newId, destination) {
|
|
2897
3291
|
return `${newId}, COALESCE(${destination}, m.source_name), m.data, m.priority, q.retry_limit, q.retry_backoff,
|
|
2898
3292
|
q.retry_delay, q.retry_delay_max, q.expire_seconds, ${schema}.job_now(), ${schema}.job_now(),
|
|
2899
3293
|
${schema}.job_now() + q.retention_seconds * interval '1s', q.deletion_seconds, q.policy,
|
|
2900
3294
|
m.singleton_key, m.group_id, m.group_tier, q.heartbeat_seconds, q.dead_letter,
|
|
2901
|
-
COALESCE(m.source_root_id, m.source_id)`;
|
|
3295
|
+
COALESCE(m.source_root_id, m.source_id), m.trace_context`;
|
|
2902
3296
|
}
|
|
2903
3297
|
// What a job that could not be re-created becomes: failed, in place, in the dead letter queue, with
|
|
2904
3298
|
// the reason as its output. Never deleted: before 12.35 it was, and the job was simply gone. As a
|
|
@@ -3065,13 +3459,15 @@ export function deletion(schema, table, queues, noAdvisoryLocks) {
|
|
|
3065
3459
|
`;
|
|
3066
3460
|
return locked(schema, sql, table + 'deletion', noAdvisoryLocks);
|
|
3067
3461
|
}
|
|
3462
|
+
// start_after moves up to now, as in resumeJobs.
|
|
3068
3463
|
export function retryJobs(schema, table) {
|
|
3069
3464
|
return `
|
|
3070
3465
|
WITH results as (
|
|
3071
3466
|
UPDATE ${schema}.job
|
|
3072
3467
|
SET state = '${JOB_STATES.retry}',
|
|
3073
3468
|
retry_limit = retry_limit + 1,
|
|
3074
|
-
completed_on = NULL
|
|
3469
|
+
completed_on = NULL,
|
|
3470
|
+
start_after = GREATEST(start_after, ${schema}.job_now())
|
|
3075
3471
|
WHERE name = $1
|
|
3076
3472
|
AND id = ANY($2::uuid[])
|
|
3077
3473
|
AND state = '${JOB_STATES.failed}'
|
|
@@ -3222,7 +3618,10 @@ function throughputAssignments(end, resetMax) {
|
|
|
3222
3618
|
completed_delta = COALESCE(stats."completedDelta", 0),
|
|
3223
3619
|
failed_delta = COALESCE(stats."failedDelta", 0),
|
|
3224
3620
|
delta_seconds = CASE WHEN ${seconds} < 0 THEN 0 ELSE ${seconds} END,
|
|
3225
|
-
delta_on = GREATEST(queue.delta_on, ${end})
|
|
3621
|
+
delta_on = GREATEST(queue.delta_on, ${end}),
|
|
3622
|
+
wait_bins = COALESCE(stats."waitBins", ${EMPTY_BINS}),
|
|
3623
|
+
run_bins = COALESCE(stats."runBins", ${EMPTY_BINS}),
|
|
3624
|
+
ready_oldest_seconds = COALESCE(stats."readyOldestSeconds", 0),`;
|
|
3226
3625
|
}
|
|
3227
3626
|
// The windows a true-up may still revise, per queue: every recorded snapshot whose window ends
|
|
3228
3627
|
// after the anchor `h`, up to the newest one, `top`, with the counters they hold between them.
|
|
@@ -3386,6 +3785,61 @@ export function trueUpQueueStats(schema, table, queues, noAdvisoryLocks, window
|
|
|
3386
3785
|
`;
|
|
3387
3786
|
return transaction(sql);
|
|
3388
3787
|
}
|
|
3788
|
+
// Wait and run times, as the monitor records them: a histogram per counted pass, of the jobs that
|
|
3789
|
+
// finished in its window. Slot 0 holds times under 10 ms, slots 1 to LATENCY_BINS bins that each
|
|
3790
|
+
// grow by √2 (slot k runs from 10 ms · √2^(k-1) to 10 ms · √2^k), and the last slot everything past
|
|
3791
|
+
// about 23 hours. Log-spaced because the times span six orders of magnitude, and a percentile read
|
|
3792
|
+
// from them lies in the same bin as the exact one, a factor of √2 at most. Histograms rather than
|
|
3793
|
+
// percentiles, because histograms add: a reader sums them across passes, buckets or queues and
|
|
3794
|
+
// reads any percentile from the sum.
|
|
3795
|
+
// Stored as 48 slots in slot order, null where no job landed: Postgres keeps a null as one bit
|
|
3796
|
+
// rather than four bytes, and most slots are empty. getQueueStats() hands them out with nulls as 0.
|
|
3797
|
+
// Readers that add slots in SQL coalesce, since a null plus a count is null.
|
|
3798
|
+
export const LATENCY_BINS = 46;
|
|
3799
|
+
export const LATENCY_SLOTS = LATENCY_BINS + 2;
|
|
3800
|
+
export const LATENCY_MIN_SECONDS = 0.01;
|
|
3801
|
+
// A measured histogram in which no job finished: every slot null, not a null array.
|
|
3802
|
+
const EMPTY_BINS = `'{${new Array(LATENCY_SLOTS).fill('NULL').join(',')}}'::int[]`;
|
|
3803
|
+
// Both bins travel packed in one integer (wait * LATENCY_PACK + run), so the aggregate needs one
|
|
3804
|
+
// array and one filter. Two arrays, each with its own filter evaluated on every row of the table,
|
|
3805
|
+
// cost twice as much: measured on 2.5M rows, +55 ms on a ~560 ms pass packed, +80 to 120 ms apart.
|
|
3806
|
+
const LATENCY_PACK = 64;
|
|
3807
|
+
// date_part rather than extract: extract returns numeric on PostgreSQL 14 and later, and the numeric
|
|
3808
|
+
// arithmetic on every finished job was a large share of the histograms' cost. date_part is float8
|
|
3809
|
+
// on every supported backend, as the bin math needs anyway.
|
|
3810
|
+
const waitSeconds = "date_part('epoch', (j.started_on - GREATEST(j.created_on, j.start_after)))";
|
|
3811
|
+
const runSeconds = "date_part('epoch', (j.completed_on - j.started_on))";
|
|
3812
|
+
// The slot is width_bucket's, written out: CockroachDB's width_bucket takes decimals, not float8.
|
|
3813
|
+
// ln(t / 10 ms) over ln(√2), plus one, clamped to slot 0 below 10 ms and the last slot past the end.
|
|
3814
|
+
// A duration of zero or less (stamps from two clocks) lands in slot 0.
|
|
3815
|
+
function latencyBin(seconds) {
|
|
3816
|
+
const lo = Math.log(LATENCY_MIN_SECONDS);
|
|
3817
|
+
const width = Math.log(2) / 2;
|
|
3818
|
+
const raw = `floor((ln(GREATEST((${seconds})::float8, 0.001::float8)) - ${lo}::float8) / ${width}::float8)::int + 1`;
|
|
3819
|
+
return `LEAST(GREATEST(${raw}, 0), ${LATENCY_SLOTS - 1})`;
|
|
3820
|
+
}
|
|
3821
|
+
// The aggregate's array is unnested once per queue, in a lateral join after the aggregate, and
|
|
3822
|
+
// counted by packed value: at most LATENCY_SLOTS² rows (ps, ns) however many jobs finished. Both
|
|
3823
|
+
// histograms are read from those, so the per-job array is walked once rather than once each.
|
|
3824
|
+
function latencyCounts(packed) {
|
|
3825
|
+
return `LEFT JOIN LATERAL (
|
|
3826
|
+
SELECT array_agg(g.p) AS ps, array_agg(g.n) AS ns
|
|
3827
|
+
FROM (SELECT p, count(*)::int AS n FROM unnest(${packed}) AS u(p) GROUP BY p) g
|
|
3828
|
+
) latency ON true`;
|
|
3829
|
+
}
|
|
3830
|
+
// One histogram from latencyCounts: every slot in slot order, null where no job landed. A pass in
|
|
3831
|
+
// which nothing finished still writes the 48 slots (unnest of a null array is no rows, and the left
|
|
3832
|
+
// join keeps every slot), so a pass that counted says so, as its deltas do. floor() of a float
|
|
3833
|
+
// division rather than integer division, which CockroachDB answers in decimal.
|
|
3834
|
+
function latencySlotOf(which) {
|
|
3835
|
+
return which === 'wait' ? `floor(p / ${LATENCY_PACK}.0)::int` : `(p % ${LATENCY_PACK})::int`;
|
|
3836
|
+
}
|
|
3837
|
+
function latencyHistogram(which) {
|
|
3838
|
+
return `(SELECT array_agg(c.n ORDER BY s.slot)
|
|
3839
|
+
FROM generate_series(0, ${LATENCY_SLOTS - 1}) AS s(slot)
|
|
3840
|
+
LEFT JOIN (SELECT ${latencySlotOf(which)} AS slot, sum(n)::int AS n
|
|
3841
|
+
FROM unnest(latency.ps, latency.ns) AS u(p, n) GROUP BY 1) c ON c.slot = s.slot)`;
|
|
3842
|
+
}
|
|
3389
3843
|
// Every count the monitor keeps, from one pass over the queue's table.
|
|
3390
3844
|
//
|
|
3391
3845
|
// Six of them are gauges — what the queue looks like right now. Three are not:
|
|
@@ -3409,6 +3863,13 @@ export function trueUpQueueStats(schema, table, queues, noAdvisoryLocks, window
|
|
|
3409
3863
|
// filtered on completed_on, would be a whole extra scan of the largest table in
|
|
3410
3864
|
// the schema, and there is no index on that column to make it cheaper.
|
|
3411
3865
|
export function getQueueStats(schema, table, queues, throughput = false, window = {}) {
|
|
3866
|
+
// A queued job is exactly one of blocked (waiting on a flow parent), deferred (start_after still
|
|
3867
|
+
// ahead) or ready, so the three add up to queuedCount. Blocked wins over deferred: a job whose
|
|
3868
|
+
// parent has not finished cannot run when its start_after comes round.
|
|
3869
|
+
const queued = `j.state < '${JOB_STATES.active}'`;
|
|
3870
|
+
const blocked = `${queued} AND j.blocked`;
|
|
3871
|
+
const deferred = `${queued} AND NOT j.blocked AND j.start_after > ${schema}.job_now()`;
|
|
3872
|
+
const ready = `${queued} AND NOT j.blocked AND j.start_after <= ${schema}.job_now()`;
|
|
3412
3873
|
// Counted only with persistQueueStats. Otherwise the aggregate does what it did before throughput
|
|
3413
3874
|
// existed: no join, no extra counts, no cost. The measured price is in the `persistQueueStats` docs.
|
|
3414
3875
|
const end = deltaWindowEnd(schema, window.lag);
|
|
@@ -3423,11 +3884,18 @@ export function getQueueStats(schema, table, queues, throughput = false, window
|
|
|
3423
3884
|
"createdDelta",
|
|
3424
3885
|
"completedDelta",
|
|
3425
3886
|
"failedDelta",
|
|
3887
|
+
${latencyHistogram('wait')} as "waitBins",
|
|
3888
|
+
${latencyHistogram('run')} as "runBins",
|
|
3889
|
+
"readyOldestSeconds",
|
|
3426
3890
|
COALESCE("recount" > "settled", false) as "trueUp",`,
|
|
3427
3891
|
counts: `
|
|
3428
3892
|
(count(*) FILTER (WHERE ${inWindow('created_on')}))::int as "createdDelta",
|
|
3429
3893
|
(count(*) FILTER (WHERE j.state = '${JOB_STATES.completed}' AND ${inWindow('completed_on')}))::int as "completedDelta",
|
|
3430
3894
|
(count(*) FILTER (WHERE j.state = '${JOB_STATES.failed}' AND ${inWindow('completed_on')}))::int as "failedDelta",
|
|
3895
|
+
array_agg(${latencyBin(waitSeconds)} * ${LATENCY_PACK} + ${latencyBin(runSeconds)})
|
|
3896
|
+
FILTER (WHERE j.state IN ('${JOB_STATES.completed}', '${JOB_STATES.failed}') AND j.started_on IS NOT NULL AND ${inWindow('completed_on')}) as "latencyBins",
|
|
3897
|
+
round(extract(epoch from (${schema}.job_now() - min(GREATEST(j.created_on, j.start_after))
|
|
3898
|
+
FILTER (WHERE ${ready}))))::int as "readyOldestSeconds",
|
|
3431
3899
|
sum(
|
|
3432
3900
|
CASE WHEN ${settled('created_on')} THEN 1 ELSE 0 END +
|
|
3433
3901
|
CASE WHEN ${settled('completed_on')} AND j.state IN ('${JOB_STATES.completed}', '${JOB_STATES.failed}') THEN 1 ELSE 0 END
|
|
@@ -3437,16 +3905,18 @@ export function getQueueStats(schema, table, queues, throughput = false, window
|
|
|
3437
3905
|
SELECT q.name, q.delta_on, a.h, t.top, t.settled
|
|
3438
3906
|
FROM ${schema}.queue q${trueUpSettled(schema, 'q', window.trueUpMax)}
|
|
3439
3907
|
WHERE q.name = ANY($1::text[])
|
|
3440
|
-
) q ON q.name = j.name
|
|
3908
|
+
) q ON q.name = j.name`,
|
|
3909
|
+
lateral: latencyCounts('stats."latencyBins"')
|
|
3441
3910
|
}
|
|
3442
|
-
: { select: '', counts: '', join: '' };
|
|
3911
|
+
: { select: '', counts: '', join: '', lateral: '' };
|
|
3443
3912
|
return {
|
|
3444
3913
|
text: `
|
|
3445
3914
|
SELECT
|
|
3446
3915
|
name,
|
|
3447
3916
|
"deferredCount",
|
|
3448
3917
|
"queuedCount",
|
|
3449
|
-
|
|
3918
|
+
"readyCount",
|
|
3919
|
+
"blockedCount",
|
|
3450
3920
|
"activeCount",
|
|
3451
3921
|
"failedCount",
|
|
3452
3922
|
"totalCount",${counters.select}
|
|
@@ -3454,8 +3924,10 @@ export function getQueueStats(schema, table, queues, throughput = false, window
|
|
|
3454
3924
|
FROM (
|
|
3455
3925
|
SELECT
|
|
3456
3926
|
j.name,
|
|
3457
|
-
(count(*) FILTER (WHERE
|
|
3458
|
-
(count(*) FILTER (WHERE
|
|
3927
|
+
(count(*) FILTER (WHERE ${deferred}))::int as "deferredCount",
|
|
3928
|
+
(count(*) FILTER (WHERE ${blocked}))::int as "blockedCount",
|
|
3929
|
+
(count(*) FILTER (WHERE ${ready}))::int as "readyCount",
|
|
3930
|
+
(count(*) FILTER (WHERE ${queued}))::int as "queuedCount",
|
|
3459
3931
|
(count(*) FILTER (WHERE j.state = '${JOB_STATES.active}'))::int as "activeCount",
|
|
3460
3932
|
(count(*) FILTER (WHERE j.state = '${JOB_STATES.failed}'))::int as "failedCount",
|
|
3461
3933
|
count(*)::int as "totalCount",${counters.counts}
|
|
@@ -3465,6 +3937,7 @@ export function getQueueStats(schema, table, queues, throughput = false, window
|
|
|
3465
3937
|
WHERE j.name = ANY($1::text[])
|
|
3466
3938
|
GROUP BY 1
|
|
3467
3939
|
) stats
|
|
3940
|
+
${counters.lateral}
|
|
3468
3941
|
`,
|
|
3469
3942
|
values: [queues]
|
|
3470
3943
|
};
|
|
@@ -3514,6 +3987,7 @@ export function cacheQueueStats(schema, table, queues, noAdvisoryLocks, throughp
|
|
|
3514
3987
|
WITH ${lock.cte}stats AS (SELECT * FROM (${statsText}) agg WHERE true${lock.guard})
|
|
3515
3988
|
UPDATE ${schema}.queue SET
|
|
3516
3989
|
deferred_count = COALESCE(stats."deferredCount", 0),
|
|
3990
|
+
blocked_count = COALESCE(stats."blockedCount", 0),
|
|
3517
3991
|
queued_count = COALESCE(stats."queuedCount", 0),
|
|
3518
3992
|
ready_count = COALESCE(stats."readyCount", 0),
|
|
3519
3993
|
active_count = COALESCE(stats."activeCount", 0),
|
|
@@ -3589,6 +4063,7 @@ export function refreshQueueStats(schema, table, name, options = {}) {
|
|
|
3589
4063
|
WITH ${lock.cte}stats AS (SELECT * FROM (${statsText}) agg WHERE true${lock.guard})
|
|
3590
4064
|
UPDATE ${schema}.queue SET
|
|
3591
4065
|
deferred_count = COALESCE(stats."deferredCount", 0),
|
|
4066
|
+
blocked_count = COALESCE(stats."blockedCount", 0),
|
|
3592
4067
|
queued_count = COALESCE(stats."queuedCount", 0),
|
|
3593
4068
|
ready_count = COALESCE(stats."readyCount", 0),
|
|
3594
4069
|
active_count = COALESCE(stats."activeCount", 0),
|
|
@@ -4025,7 +4500,7 @@ const POLICY_JOB_INDEXES = {
|
|
|
4025
4500
|
// missing: the fetch index was replaced in v40 and the retired number is not reused.
|
|
4026
4501
|
const BASE_JOB_INDEXES = [4, 7, 9, 11, 12];
|
|
4027
4502
|
// The fixed (non-job) managed tables; job/job_common/partitions are handled separately.
|
|
4028
|
-
const FIXED_MANAGED_TABLES = ['version', 'queue', 'schedule', 'subscription', 'bam', 'warning', 'queue_stats', 'job_dependency'];
|
|
4503
|
+
const FIXED_MANAGED_TABLES = ['version', 'queue', 'schedule', 'subscription', 'bam', 'warning', 'queue_stats', 'job_dependency', 'instance'];
|
|
4029
4504
|
// Selects the manifest section for the live architecture, and substitutes the real schema name back in
|
|
4030
4505
|
// for the placeholder the manifest stores.
|
|
4031
4506
|
function manifestSection(partitioned) {
|
|
@@ -4034,6 +4509,32 @@ function manifestSection(partitioned) {
|
|
|
4034
4509
|
function applyManifestSchema(text, schema) {
|
|
4035
4510
|
return text.split(schemaManifest.schemaToken).join(schema);
|
|
4036
4511
|
}
|
|
4512
|
+
// The argument types in a manifest function's definition, without names or defaults, as DROP FUNCTION
|
|
4513
|
+
// takes them: "create_queue(queue_name text, options jsonb)" gives "text, jsonb".
|
|
4514
|
+
function functionArgTypes(def) {
|
|
4515
|
+
const args = def.slice(def.indexOf('(') + 1, def.indexOf(')'));
|
|
4516
|
+
return args.split(',')
|
|
4517
|
+
.map(arg => arg.trim().replace(/\s+DEFAULT\s+.*$/i, ''))
|
|
4518
|
+
.filter(Boolean)
|
|
4519
|
+
.map(arg => arg.split(/\s+/).slice(1).join(' '))
|
|
4520
|
+
.join(', ');
|
|
4521
|
+
}
|
|
4522
|
+
// Removes everything create() installs, for a schema pg-boss shares with other objects; a schema of
|
|
4523
|
+
// its own is simply dropped. Read from the manifest, so it covers whatever this version installs.
|
|
4524
|
+
// Dropping job takes every queue's own table with it: a partitioned queue's table is a partition of
|
|
4525
|
+
// job, and without partitioning every queue's jobs are in job itself. queue_stats' daily partitions
|
|
4526
|
+
// go with queue_stats the same way.
|
|
4527
|
+
export function uninstall(schema, partitioned = true) {
|
|
4528
|
+
const section = manifestSection(partitioned);
|
|
4529
|
+
const tables = section.tables.map(table => `${schema}.${table}`).join(', ');
|
|
4530
|
+
const functions = section.functions.map(fn => `${schema}.${fn.name}(${functionArgTypes(fn.def)})`).join(', ');
|
|
4531
|
+
// Functions first: CockroachDB records a function as depending on the tables its body names.
|
|
4532
|
+
return [
|
|
4533
|
+
`DROP FUNCTION IF EXISTS ${functions};`,
|
|
4534
|
+
`DROP TABLE IF EXISTS ${tables};`,
|
|
4535
|
+
`DROP TYPE IF EXISTS ${schema}.job_state;`
|
|
4536
|
+
].join('\n');
|
|
4537
|
+
}
|
|
4037
4538
|
// The job_state enum values in declaration order, from the manifest (both sections carry the same enum).
|
|
4038
4539
|
// Order is significant. The numeric base type makes created < retry < … < failed load-bearing.
|
|
4039
4540
|
export const EXPECTED_JOB_STATES = schemaManifest.partitioned.enum;
|
|
@@ -4413,12 +4914,12 @@ export function getXminHorizon(lastVacuum, sources = XMIN_HORIZON_QUERY_SOURCES)
|
|
|
4413
4914
|
// query it is follows from pid, application_name and role, looked up live where the catalog's own
|
|
4414
4915
|
// privilege rules still apply.
|
|
4415
4916
|
//
|
|
4416
|
-
// selfApplicationName is what makes "ours or theirs" answerable.
|
|
4417
|
-
// 'pgboss'
|
|
4418
|
-
// it to itself - the monitor's own aggregate, most likely - which has a
|
|
4419
|
-
// from an external reporting tool holding a transaction open. It is
|
|
4420
|
-
// because an adapter-supplied pool sets whatever the host app
|
|
4421
|
-
// definitely pg-boss would be a guess.
|
|
4917
|
+
// selfApplicationName is what makes "ours or theirs" answerable. pg-boss names the pool it owns
|
|
4918
|
+
// 'pgboss', or 'pgboss:<id>' for a registered instance, so a holder matching this connection's own
|
|
4919
|
+
// value is pg-boss doing it to itself - the monitor's own aggregate, most likely - which has a
|
|
4920
|
+
// completely different fix from an external reporting tool holding a transaction open. It is
|
|
4921
|
+
// compared rather than hardcoded because an adapter-supplied pool sets whatever the host app
|
|
4922
|
+
// chose, and claiming that is definitely pg-boss would be a guess.
|
|
4422
4923
|
const backendIdentity = sources.includes('backends')
|
|
4423
4924
|
? `,
|
|
4424
4925
|
(SELECT to_jsonb(h) FROM (
|