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.
@@ -1 +1 @@
1
- {"version":3,"file":"migrationStore.d.ts","sourceRoot":"","sources":["../src/migrationStore.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAKnC,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,OAAO,CAAA;IAKrB,eAAe,CAAC,EAAE,KAAK,CAAC,kBAAkB,EAAE,CAAA;CAC7C;AAoGD,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AASD,iBAAS,QAAQ,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,UAsB5G;AAED,iBAAS,IAAI,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,UAQxG;AAKD,iBAAS,eAAe,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,iBAAiB,CAiDrK;AAKD,iBAAS,OAAO,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,UAIzI;AAgoCD,iBAAS,aAAa,CAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9C;AAID,iBAAS,eAAe,CAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,0BAA0B,EAAE,QAAQ,GAAG,qBAAqB,GAAG,mBAAmB,GAAG,qBAAqB,CAAC,GAAG,KAAK,CAAC,SAAS,EAAE,CAE3K;AAOD,iBAAS,MAAM,CAAE,MAAM,EAAE,MAAM,EAAE,cAAc,UAAQ,EAAE,UAAU,UAAQ,EAAE,mBAAmB,UAAQ,GAAG,KAAK,CAAC,SAAS,EAAE,CA4oB3H;AAED,OAAO,EACL,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,eAAe,EACf,MAAM,EACN,eAAe,EACf,aAAa,GACd,CAAA"}
1
+ {"version":3,"file":"migrationStore.d.ts","sourceRoot":"","sources":["../src/migrationStore.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAKnC,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,OAAO,CAAA;IAKrB,eAAe,CAAC,EAAE,KAAK,CAAC,kBAAkB,EAAE,CAAA;CAC7C;AAoGD,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AASD,iBAAS,QAAQ,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,UAsB5G;AAED,iBAAS,IAAI,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,UAQxG;AAKD,iBAAS,eAAe,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,iBAAiB,CAiDrK;AAKD,iBAAS,OAAO,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,UAIzI;AA6tCD,iBAAS,aAAa,CAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9C;AAID,iBAAS,eAAe,CAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,0BAA0B,EAAE,QAAQ,GAAG,qBAAqB,GAAG,mBAAmB,GAAG,qBAAqB,CAAC,GAAG,KAAK,CAAC,SAAS,EAAE,CAE3K;AAOD,iBAAS,MAAM,CAAE,MAAM,EAAE,MAAM,EAAE,cAAc,UAAQ,EAAE,UAAU,UAAQ,EAAE,mBAAmB,UAAQ,GAAG,KAAK,CAAC,SAAS,EAAE,CAivB3H;AAED,OAAO,EACL,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,eAAe,EACf,MAAM,EACN,eAAe,EACf,aAAa,GACd,CAAA"}
@@ -1087,6 +1087,99 @@ const createQueueFn = {
1087
1087
  EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
1088
1088
  END IF;
1089
1089
 
1090
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
1091
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
1092
+ END;
1093
+ $$
1094
+ LANGUAGE plpgsql;
1095
+ `,
1096
+ 45: (schema) => `
1097
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
1098
+ RETURNS VOID AS
1099
+ $$
1100
+ DECLARE
1101
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
1102
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
1103
+ ELSE 'job_common'
1104
+ END;
1105
+ queue_created_on timestamptz;
1106
+ BEGIN
1107
+
1108
+ WITH q as (
1109
+ INSERT INTO ${schema}.queue (
1110
+ name,
1111
+ policy,
1112
+ retry_limit,
1113
+ retry_delay,
1114
+ retry_backoff,
1115
+ retry_delay_max,
1116
+ expire_seconds,
1117
+ retention_seconds,
1118
+ deletion_seconds,
1119
+ warning_queued,
1120
+ dead_letter,
1121
+ partition,
1122
+ table_name,
1123
+ heartbeat_seconds,
1124
+ notify,
1125
+ created_on,
1126
+ updated_on
1127
+ )
1128
+ VALUES (
1129
+ queue_name,
1130
+ options->>'policy',
1131
+ COALESCE((options->>'retryLimit')::int, 2),
1132
+ COALESCE((options->>'retryDelay')::int, 0),
1133
+ COALESCE((options->>'retryBackoff')::bool, false),
1134
+ (options->>'retryDelayMax')::int,
1135
+ COALESCE((options->>'expireInSeconds')::int, 900),
1136
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
1137
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
1138
+ COALESCE((options->>'warningQueueSize')::int, 0),
1139
+ options->>'deadLetter',
1140
+ COALESCE((options->>'partition')::bool, false),
1141
+ tablename,
1142
+ (options->>'heartbeatSeconds')::int,
1143
+ COALESCE((options->>'notify')::bool, false),
1144
+ ${schema}.job_now(),
1145
+ ${schema}.job_now()
1146
+ )
1147
+ ON CONFLICT DO NOTHING
1148
+ RETURNING created_on
1149
+ )
1150
+ SELECT created_on into queue_created_on from q;
1151
+
1152
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
1153
+ RETURN;
1154
+ END IF;
1155
+
1156
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
1157
+
1158
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
1159
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
1160
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
1161
+
1162
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i11 ON ${schema}.job (name, priority DESC, created_on, start_after) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
1163
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
1164
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
1165
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i9 ON ${schema}.job (name, id) WHERE blocking AND state = 'completed'$cmd$, tablename);
1166
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i12 ON ${schema}.job (source_root_id) WHERE source_root_id IS NOT NULL$cmd$, tablename);
1167
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i13 ON ${schema}.job (name, singleton_key) WHERE state = 'created' AND upsert_by_key$cmd$, tablename);
1168
+
1169
+ IF options->>'policy' = 'short' THEN
1170
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
1171
+ ELSIF options->>'policy' = 'singleton' THEN
1172
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
1173
+ ELSIF options->>'policy' = 'stately' THEN
1174
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
1175
+ ELSIF options->>'policy' = 'exclusive' THEN
1176
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
1177
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
1178
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
1179
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i10 ON ${schema}.job (name, singleton_key, state DESC, created_on, id) INCLUDE (start_after) WHERE state < 'active' AND NOT blocked AND policy = 'key_strict_fifo'$cmd$, tablename);
1180
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
1181
+ END IF;
1182
+
1090
1183
  EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
1091
1184
  EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
1092
1185
  END;
@@ -1963,6 +2056,107 @@ AS $function$
1963
2056
  DROP COLUMN source_output,
1964
2057
  DROP COLUMN source_root_id`
1965
2058
  ]
2059
+ },
2060
+ {
2061
+ release: '12.36.0',
2062
+ version: 44,
2063
+ previous: 43,
2064
+ // The instance table starts empty: each instance registers on its next start().
2065
+ // blocked_count's constant default adds it without a table rewrite, and existing queues read 0
2066
+ // until the next monitor pass counts them. It goes on the queue row only, not queue_stats, so
2067
+ // the covering index on queue_stats needs no rebuild.
2068
+ // The histogram and ready_oldest_seconds columns are nullable with no default, like the v43
2069
+ // deltas, so no statement rewrites a table, and a snapshot captured before them reads as not
2070
+ // counted rather than as a queue with no waits.
2071
+ // trace_context is nullable with no default too, so adding it rewrites no rows.
2072
+ install: [
2073
+ /* eslint-disable no-restricted-syntax -- column defaults stay on the real clock: every pg-boss write names its timestamps through job_now() */
2074
+ `CREATE TABLE ${schema}.instance (
2075
+ id uuid PRIMARY KEY,
2076
+ name text,
2077
+ host text NOT NULL,
2078
+ pid int NOT NULL,
2079
+ version text NOT NULL,
2080
+ node_version text NOT NULL,
2081
+ application_name text,
2082
+ heartbeat_seconds int NOT NULL,
2083
+ supervise bool NOT NULL,
2084
+ schedule bool NOT NULL,
2085
+ migrate bool NOT NULL,
2086
+ persist_queue_stats bool NOT NULL,
2087
+ persist_warnings bool NOT NULL,
2088
+ pool_max int,
2089
+ pool_total int,
2090
+ pool_idle int,
2091
+ pool_waiting int,
2092
+ workers jsonb NOT NULL DEFAULT '[]'::jsonb,
2093
+ metrics jsonb,
2094
+ config jsonb NOT NULL DEFAULT '{}'::jsonb,
2095
+ crash_restarts int NOT NULL DEFAULT 0,
2096
+ crash_restarts_since timestamptz,
2097
+ started_on timestamptz NOT NULL DEFAULT now(),
2098
+ heartbeat_on timestamptz NOT NULL DEFAULT now(),
2099
+ stopped_on timestamptz
2100
+ )`,
2101
+ /* eslint-enable no-restricted-syntax */
2102
+ `ALTER TABLE ${schema}.queue
2103
+ ADD COLUMN blocked_count int NOT NULL DEFAULT 0,
2104
+ ADD COLUMN wait_bins int[],
2105
+ ADD COLUMN run_bins int[],
2106
+ ADD COLUMN ready_oldest_seconds int`,
2107
+ `ALTER TABLE ${schema}.queue_stats
2108
+ ADD COLUMN wait_bins int[],
2109
+ ADD COLUMN run_bins int[],
2110
+ ADD COLUMN ready_oldest_seconds int`,
2111
+ `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS trace_context jsonb`
2112
+ ],
2113
+ uninstall: [
2114
+ `DROP TABLE ${schema}.instance`,
2115
+ `ALTER TABLE ${schema}.queue
2116
+ DROP COLUMN blocked_count,
2117
+ DROP COLUMN wait_bins,
2118
+ DROP COLUMN run_bins,
2119
+ DROP COLUMN ready_oldest_seconds`,
2120
+ `ALTER TABLE ${schema}.queue_stats
2121
+ DROP COLUMN wait_bins,
2122
+ DROP COLUMN run_bins,
2123
+ DROP COLUMN ready_oldest_seconds`,
2124
+ `ALTER TABLE ${schema}.job DROP COLUMN trace_context`
2125
+ ]
2126
+ },
2127
+ {
2128
+ release: '12.37.0',
2129
+ version: 45,
2130
+ previous: 44,
2131
+ // upsert_by_key is nullable with no default, so adding it rewrites no rows, and no existing job
2132
+ // carries it. job_i13 therefore starts empty and cannot fail on existing duplicates.
2133
+ // It is built the way v43 built job_i12: inline without partitioning, otherwise through
2134
+ // create_queue for new partitions and BAM, concurrently, for the tables that already exist.
2135
+ install: [
2136
+ `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS upsert_by_key bool`,
2137
+ noPartitioning
2138
+ ? `CREATE UNIQUE INDEX job_i13 ON ${schema}.job (name, singleton_key) WHERE state = 'created' AND upsert_by_key`
2139
+ : createQueueFn[45](schema)
2140
+ ],
2141
+ async: noPartitioning
2142
+ ? []
2143
+ : [
2144
+ {
2145
+ name: 'upsert_index_build',
2146
+ command: `CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS job_i13 ON ${schema}.job (name, singleton_key) WHERE state = 'created' AND upsert_by_key`
2147
+ }
2148
+ ],
2149
+ // The index goes before its column, and create_queue is restored first, as in v43. CASCADE because
2150
+ // CockroachDB will not drop a unique index without it.
2151
+ uninstall: [
2152
+ ...(noPartitioning
2153
+ ? [`DROP INDEX IF EXISTS ${schema}.job_i13 CASCADE`]
2154
+ : [
2155
+ createQueueFn[43](schema),
2156
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i13$cmd$)`
2157
+ ]),
2158
+ `ALTER TABLE ${schema}.job DROP COLUMN upsert_by_key`
2159
+ ]
1966
2160
  }
1967
2161
  ];
1968
2162
  }
@@ -0,0 +1,20 @@
1
+ import type * as types from './types.ts';
2
+ interface Hierarchy {
3
+ dirs: string[];
4
+ }
5
+ export interface Cgroup {
6
+ version: 1 | 2;
7
+ cpu: Hierarchy | null;
8
+ memory: Hierarchy | null;
9
+ }
10
+ export declare function findCgroup(prefix?: string): Promise<Cgroup | null>;
11
+ export declare function countCpuList(list: string): number | null;
12
+ declare class Nurse {
13
+ #private;
14
+ constructor(prefix?: string);
15
+ start(): void;
16
+ stop(): void;
17
+ check(): Promise<types.InstanceMetrics>;
18
+ }
19
+ export default Nurse;
20
+ //# sourceMappingURL=nurse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nurse.d.ts","sourceRoot":"","sources":["../src/nurse.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAmBxC,UAAU,SAAS;IAGjB,IAAI,EAAE,MAAM,EAAE,CAAA;CACf;AAED,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE,CAAC,GAAG,CAAC,CAAA;IACd,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,SAAS,GAAG,IAAI,CAAA;CACzB;AAmDD,wBAAsB,UAAU,CAAE,MAAM,SAAK,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkDrE;AA0ED,wBAAgB,YAAY,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUzD;AAYD,cAAM,KAAK;;gBASI,MAAM,SAAK;IAIxB,KAAK;IAgBL,IAAI;IAOE,KAAK,IAAK,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC;CAmF/C;AAED,eAAe,KAAK,CAAA"}
package/dist/nurse.js ADDED
@@ -0,0 +1,287 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
4
+ import v8 from 'node:v8';
5
+ // Takes this process's vitals on each registry heartbeat: what it is using and what it is allowed. Inside a
6
+ // container the host-wide figures Node offers are the wrong ones: os.totalmem(), os.freemem(),
7
+ // os.loadavg() and os.cpus() describe the machine, not the container, and os.availableParallelism()
8
+ // ignores a CPU quota under one core (12 at --cpus=0.5) and rounds others down (1 at --cpus=1.5). So
9
+ // limits and container usage are read from the cgroup files this process is charged to, found through
10
+ // /proc/self/cgroup and /proc/self/mountinfo, which covers cgroup v1 and v2 and a private or host
11
+ // cgroup namespace alike. Off Linux, or where those files cannot be read, the limits fall back to the
12
+ // host and the container-only figures are null. Nothing here throws: a figure that cannot be read is
13
+ // null, and the heartbeat goes out regardless.
14
+ // Event loop delay is sampled on a timer of this resolution, which every sample includes and which is
15
+ // subtracted, so an idle loop reads close to zero.
16
+ const LOOP_RESOLUTION_MS = 20;
17
+ // cgroup v1 writes "no limit" as the largest page-aligned int64, not as a word.
18
+ const V1_UNLIMITED = 2 ** 60;
19
+ // mountinfo escapes spaces and other octal characters in paths as \NNN.
20
+ const unescape = (s) => s.replace(/\\([0-7]{3})/g, (_, n) => String.fromCharCode(parseInt(n, 8)));
21
+ function parseMountInfo(text) {
22
+ const mounts = [];
23
+ for (const line of text.split('\n')) {
24
+ const sep = line.indexOf(' - ');
25
+ if (sep < 0)
26
+ continue;
27
+ const pre = line.slice(0, sep).split(' ');
28
+ const post = line.slice(sep + 3).split(' ');
29
+ if (pre.length < 5 || post.length < 3)
30
+ continue;
31
+ mounts.push({ root: unescape(pre[3]), mountPoint: unescape(pre[4]), fsType: post[0], superOptions: post[2].split(',') });
32
+ }
33
+ return mounts;
34
+ }
35
+ // Maps a cgroup path, as /proc/self/cgroup gives it, to directories under the mount that shows it. A
36
+ // mount's root is the part of the hierarchy it exposes: "/" for a whole hierarchy or a private cgroup
37
+ // namespace, the container's own cgroup for Docker on cgroup v1.
38
+ function hierarchy(prefix, mount, path) {
39
+ let rel = path;
40
+ if (mount.root !== '/') {
41
+ rel = path === mount.root || path.startsWith(mount.root + '/') ? path.slice(mount.root.length) : '';
42
+ }
43
+ const base = prefix + mount.mountPoint;
44
+ const parts = rel.split('/').filter(Boolean);
45
+ const dirs = [];
46
+ for (let i = parts.length; i >= 0; i--) {
47
+ dirs.push([base, ...parts.slice(0, i)].join('/'));
48
+ }
49
+ return { dirs };
50
+ }
51
+ // Finds the cgroup this process is charged to. `prefix` is prepended to every path read, for tests.
52
+ export async function findCgroup(prefix = '') {
53
+ let self;
54
+ try {
55
+ self = await readFile(prefix + '/proc/self/cgroup', 'utf8');
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ const entries = self.split('\n').filter(Boolean).map(line => {
61
+ const [id, controllers, ...path] = line.split(':');
62
+ return { id, controllers: controllers ? controllers.split(',') : [], path: path.join(':') };
63
+ });
64
+ // A sandbox can refuse mountinfo while allowing the rest (AppArmor under snap does). Then assume
65
+ // the usual layout, each hierarchy mounted whole under /sys/fs/cgroup: walking up from the path
66
+ // still reaches the mount's root, which is the container's own cgroup under a private namespace
67
+ // and under Docker's v1 mounts alike.
68
+ let mounts;
69
+ try {
70
+ mounts = parseMountInfo(await readFile(prefix + '/proc/self/mountinfo', 'utf8'));
71
+ }
72
+ catch {
73
+ mounts = entries.map(e => e.controllers.length
74
+ ? { root: '/', mountPoint: '/sys/fs/cgroup/' + e.controllers.join(','), fsType: 'cgroup', superOptions: e.controllers }
75
+ : { root: '/', mountPoint: '/sys/fs/cgroup', fsType: 'cgroup2', superOptions: [] });
76
+ }
77
+ // A v1 hierarchy for a controller wins over a v2 one: on a hybrid host the unified hierarchy is
78
+ // mounted but the cpu and memory controllers stay on v1.
79
+ const v1 = (controller) => {
80
+ const entry = entries.find(e => e.controllers.includes(controller));
81
+ const mount = mounts.find(m => m.fsType === 'cgroup' && m.superOptions.includes(controller));
82
+ return entry && mount ? hierarchy(prefix, mount, entry.path) : null;
83
+ };
84
+ const cpu = v1('cpu');
85
+ const memory = v1('memory');
86
+ if (cpu || memory) {
87
+ return { version: 1, cpu, memory };
88
+ }
89
+ const unified = entries.find(e => e.id === '0' && e.controllers.length === 0);
90
+ const mount = mounts.find(m => m.fsType === 'cgroup2');
91
+ if (unified && mount) {
92
+ const h = hierarchy(prefix, mount, unified.path);
93
+ return { version: 2, cpu: h, memory: h };
94
+ }
95
+ return null;
96
+ }
97
+ async function read(path) {
98
+ try {
99
+ return (await readFile(path, 'utf8')).trim();
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ function statField(text, key) {
106
+ if (!text)
107
+ return null;
108
+ const m = new RegExp(`^${key} (\\d+)$`, 'm').exec(text);
109
+ return m ? Number(m[1]) : null;
110
+ }
111
+ // The tightest CPU quota in cores on this cgroup or a parent, and where it is set.
112
+ async function cpuQuota(cg) {
113
+ if (!cg.cpu)
114
+ return null;
115
+ let best = null;
116
+ for (const dir of cg.cpu.dirs) {
117
+ let cores = null;
118
+ if (cg.version === 2) {
119
+ const [quota, period] = (await read(dir + '/cpu.max'))?.split(' ') ?? [];
120
+ if (quota && quota !== 'max' && Number(period) > 0)
121
+ cores = Number(quota) / Number(period);
122
+ }
123
+ else {
124
+ const quota = Number(await read(dir + '/cpu.cfs_quota_us'));
125
+ const period = Number(await read(dir + '/cpu.cfs_period_us'));
126
+ if (quota > 0 && period > 0)
127
+ cores = quota / period;
128
+ }
129
+ if (cores !== null && cores > 0 && (!best || cores < best.value))
130
+ best = { value: cores, dir };
131
+ }
132
+ return best;
133
+ }
134
+ // The tightest memory limit in bytes on this cgroup or a parent, and where it is set.
135
+ async function memoryLimit(cg) {
136
+ if (!cg.memory)
137
+ return null;
138
+ let best = null;
139
+ for (const dir of cg.memory.dirs) {
140
+ const text = await read(dir + (cg.version === 2 ? '/memory.max' : '/memory.limit_in_bytes'));
141
+ if (!text || text === 'max')
142
+ continue;
143
+ const bytes = Number(text);
144
+ if (!(bytes > 0) || bytes >= V1_UNLIMITED)
145
+ continue;
146
+ if (!best || bytes < best.value)
147
+ best = { value: bytes, dir };
148
+ }
149
+ return best;
150
+ }
151
+ // Memory in use by the cgroup less its inactive file cache: the working set that container runtimes
152
+ // and the kubelet compare against the limit, since inactive cache is reclaimed before anything is killed.
153
+ async function workingSet(cg, dir) {
154
+ const usage = Number(await read(dir + (cg.version === 2 ? '/memory.current' : '/memory.usage_in_bytes')) ?? NaN);
155
+ if (!(usage >= 0))
156
+ return null;
157
+ const inactive = statField(await read(dir + '/memory.stat'), cg.version === 2 ? 'inactive_file' : 'total_inactive_file') ?? 0;
158
+ return Math.max(0, usage - inactive);
159
+ }
160
+ // Counts the CPUs this process may be scheduled on, from a list such as "0-3,8,10-11".
161
+ export function countCpuList(list) {
162
+ let n = 0;
163
+ for (const part of list.split(',')) {
164
+ const m = /^(\d+)(?:-(\d+))?$/.exec(part.trim());
165
+ if (!m)
166
+ return null;
167
+ n += m[2] === undefined ? 1 : Number(m[2]) - Number(m[1]) + 1;
168
+ }
169
+ return n > 0 ? n : null;
170
+ }
171
+ async function allowedCpus(prefix) {
172
+ const status = await read(prefix + '/proc/self/status');
173
+ const list = status && /^Cpus_allowed_list:\s*(\S+)$/m.exec(status)?.[1];
174
+ const n = list ? countCpuList(list) : null;
175
+ return n ?? os.availableParallelism();
176
+ }
177
+ const round = (n, places) => Math.round(n * 10 ** places) / 10 ** places;
178
+ class Nurse {
179
+ #prefix;
180
+ #cgroup = null;
181
+ #loop = null;
182
+ #elu = null;
183
+ #cpu = null;
184
+ #at = null;
185
+ #throttle = null;
186
+ constructor(prefix = '') {
187
+ this.#prefix = prefix;
188
+ }
189
+ start() {
190
+ this.#cgroup = findCgroup(this.#prefix).catch(() => null);
191
+ try {
192
+ this.#loop = monitorEventLoopDelay({ resolution: LOOP_RESOLUTION_MS });
193
+ this.#loop.enable();
194
+ }
195
+ catch {
196
+ this.#loop = null;
197
+ }
198
+ this.#cpu = null;
199
+ this.#at = null;
200
+ this.#elu = null;
201
+ this.#throttle = null;
202
+ }
203
+ stop() {
204
+ this.#loop?.disable();
205
+ this.#loop = null;
206
+ }
207
+ // Rates cover the time since the previous check. The first check after start() only sets the
208
+ // baseline, since at registration it would cover a few microseconds, so its rates are null.
209
+ async check() {
210
+ const first = this.#at === null;
211
+ const now = process.hrtime.bigint();
212
+ const cpuNow = process.cpuUsage();
213
+ const elapsedMicros = first ? 0 : Number(now - this.#at) / 1000;
214
+ const cpu = !first && this.#cpu && elapsedMicros > 0
215
+ ? ((cpuNow.user - this.#cpu.user) + (cpuNow.system - this.#cpu.system)) / elapsedMicros
216
+ : null;
217
+ this.#cpu = cpuNow;
218
+ this.#at = now;
219
+ let loopDelay = null;
220
+ let loopDelayMax = null;
221
+ if (this.#loop) {
222
+ if (!first && this.#loop.count > 0) {
223
+ loopDelay = Math.max(0, this.#loop.percentile(99) / 1e6 - LOOP_RESOLUTION_MS);
224
+ loopDelayMax = Math.max(0, this.#loop.max / 1e6 - LOOP_RESOLUTION_MS);
225
+ }
226
+ this.#loop.reset();
227
+ }
228
+ let loopUtilization = null;
229
+ try {
230
+ const next = performance.eventLoopUtilization();
231
+ // Bun's is a stub that always reads zero, which would chart as an idle loop.
232
+ if (next.idle === 0 && next.active === 0)
233
+ throw new Error('unsupported');
234
+ if (!first && this.#elu)
235
+ loopUtilization = performance.eventLoopUtilization(next, this.#elu).utilization;
236
+ this.#elu = next;
237
+ }
238
+ catch { }
239
+ let heapUsed = null;
240
+ let heapLimit = null;
241
+ try {
242
+ const heap = v8.getHeapStatistics();
243
+ heapUsed = heap.used_heap_size;
244
+ heapLimit = heap.heap_size_limit;
245
+ }
246
+ catch { }
247
+ let rss = null;
248
+ try {
249
+ rss = process.memoryUsage.rss();
250
+ }
251
+ catch { }
252
+ const cg = await this.#cgroup;
253
+ const quota = cg ? await cpuQuota(cg) : null;
254
+ const memLimit = cg ? await memoryLimit(cg) : null;
255
+ const cpus = await allowedCpus(this.#prefix);
256
+ return {
257
+ cgroup: cg?.version ?? null,
258
+ cpu: cpu === null ? null : round(cpu, 3),
259
+ cpuLimit: round(quota ? Math.min(quota.value, cpus) : cpus, 3),
260
+ cpuThrottled: cg && quota ? await this.#throttled(cg, quota.dir) : null,
261
+ rss,
262
+ heapUsed,
263
+ heapLimit,
264
+ memoryUsed: cg && memLimit ? await workingSet(cg, memLimit.dir) : null,
265
+ memoryLimit: memLimit ? memLimit.value : os.totalmem(),
266
+ loopDelay: loopDelay === null ? null : round(loopDelay, 1),
267
+ loopDelayMax: loopDelayMax === null ? null : round(loopDelayMax, 1),
268
+ loopUtilization: loopUtilization === null ? null : round(loopUtilization, 3)
269
+ };
270
+ }
271
+ // The share of CPU periods since the previous check in which the cgroup that holds the quota ran
272
+ // out of it and was paused. Null on the first check, or when the quota moved to another cgroup.
273
+ async #throttled(cg, dir) {
274
+ const text = await read(dir + '/cpu.stat');
275
+ const periods = statField(text, 'nr_periods');
276
+ const throttled = statField(text, 'nr_throttled');
277
+ if (periods === null || throttled === null)
278
+ return null;
279
+ const prev = this.#throttle;
280
+ this.#throttle = { dir, periods, throttled };
281
+ if (!prev || prev.dir !== dir)
282
+ return null;
283
+ const dp = periods - prev.periods;
284
+ return dp > 0 ? round((throttled - prev.throttled) / dp, 3) : 0;
285
+ }
286
+ }
287
+ export default Nurse;
package/dist/plans.d.ts CHANGED
@@ -6,6 +6,10 @@ export interface SqlQuery {
6
6
  }
7
7
  export declare const PG_ERROR: {
8
8
  divisionByZero: string;
9
+ lockNotAvailable: string;
10
+ foreignKeyViolation: string;
11
+ undefinedTable: string;
12
+ checkViolation: string;
9
13
  };
10
14
  export declare const DEFAULT_SCHEMA = "pgboss";
11
15
  export declare const MIGRATE_RACE_MESSAGE = "division by zero";
@@ -90,12 +94,26 @@ export declare function createClockFunction(schema: string, options?: {
90
94
  export declare function createTableWarning(schema: string): string;
91
95
  export declare function createIndexWarning(schema: string): string;
92
96
  export declare function createTableJobDependency(schema: string): string;
97
+ export declare const INSTANCE_QUIET_BEATS = 3;
98
+ export declare const INSTANCE_RETENTION_DAYS = 7;
99
+ export declare const INSTANCE_DEAD_KEPT_PER_NAME = 20;
100
+ export declare const INSTANCE_DEAD_KEPT = 1000;
101
+ export declare function pruneInstanceLives(schema: string, keep: number): string;
102
+ export declare function trimDeadInstances(schema: string, keep: number): string;
103
+ export declare function countCrashRestarts(schema: string): string;
104
+ export declare function crashRecountAt(schema: string): string;
105
+ export declare function registerInstance(schema: string): string;
106
+ export declare function heartbeatInstance(schema: string): string;
107
+ export declare function stopInstance(schema: string): string;
108
+ export declare function deleteOldInstances(schema: string, days: number): string;
109
+ export declare function getInstances(schema: string): string;
93
110
  export declare function createIndexJobDependencyParent(schema: string): string;
111
+ export declare function createTableInstance(schema: string): string;
94
112
  export declare function jobTableFormatFunction(schema: string): string;
95
113
  export declare function createQueue(schema: string, name: string, options: unknown, noAdvisoryLocks?: boolean): string;
96
114
  export declare function notifyChannelSql(schema: string): string;
97
115
  export declare function notifyQueue(schema: string, name: string): string;
98
- export declare function deleteQueue(schema: string, name: string, noAdvisoryLocks?: boolean): string;
116
+ export declare function deleteQueue(schema: string, name: string, noAdvisoryLocks?: boolean, partitioned?: boolean): string;
99
117
  export declare function trySetQueueMonitorTime(schema: string, queues: string[], seconds: number, skipLocked?: boolean): SqlQuery;
100
118
  export declare function setMonitorBackoff(schema: string, elapsedSeconds: number): SqlQuery;
101
119
  export declare function trySetQueueDeletionTime(schema: string, queues: string[], seconds: number, skipLocked?: boolean): SqlQuery;
@@ -105,6 +123,7 @@ export declare function trySetFlowTime(schema: string, seconds: number): string;
105
123
  export declare function trySetReindexTime(schema: string, seconds: number): string;
106
124
  export declare function queueClaimSkipLocked(backend: string | undefined, noSkipLocked?: boolean): boolean;
107
125
  export declare function updateQueue(schema: string): string;
126
+ export declare function currentDatabase(): string;
108
127
  export declare function getQueues(schema: string, names?: string[]): SqlQuery;
109
128
  export declare function deleteJobsById(schema: string, table: string, fenced?: boolean): string;
110
129
  export declare function deleteQueuedJobs(schema: string, table: string): string;
@@ -165,6 +184,7 @@ interface FetchJobOptions {
165
184
  policy: string | undefined;
166
185
  limit: number;
167
186
  includeMetadata?: boolean;
187
+ includeTraceContext?: boolean;
168
188
  priority?: boolean;
169
189
  orderByCreatedOn?: boolean;
170
190
  ignoreStartAfter?: boolean;
@@ -204,8 +224,9 @@ interface InsertJobsOptions {
204
224
  returnId?: boolean;
205
225
  notify?: boolean;
206
226
  slots?: boolean;
227
+ upsertByKey?: boolean;
207
228
  }
208
- export declare function insertJobs(schema: string, { table, name, returnId, notify, slots }: InsertJobsOptions): string;
229
+ export declare function insertJobs(schema: string, { table, name, returnId, notify, slots, upsertByKey }: InsertJobsOptions): string;
209
230
  export declare function insertFlowJobs(schema: string, { table, name }: {
210
231
  table: string;
211
232
  name: string;
@@ -244,6 +265,9 @@ export interface DeltaWindowOptions {
244
265
  }
245
266
  export declare function deltaResetMax(intervalSeconds: number): string;
246
267
  export declare function trueUpQueueStats(schema: string, table: string, queues: string[], noAdvisoryLocks?: boolean, window?: DeltaWindowOptions): string;
268
+ export declare const LATENCY_BINS = 46;
269
+ export declare const LATENCY_SLOTS: number;
270
+ export declare const LATENCY_MIN_SECONDS = 0.01;
247
271
  export declare function getQueueStats(schema: string, table: string, queues: string[], throughput?: boolean, window?: DeltaWindowOptions): SqlQuery;
248
272
  export declare const READY_HISTORY_SIZE = 60;
249
273
  export declare function cacheQueueStats(schema: string, table: string, queues: string[], noAdvisoryLocks?: boolean, throughput?: boolean, window?: DeltaWindowOptions): string;
@@ -287,6 +311,7 @@ interface QueuePartition {
287
311
  table: string;
288
312
  policy?: string | null;
289
313
  }
314
+ export declare function uninstall(schema: string, partitioned?: boolean): string;
290
315
  export declare const EXPECTED_JOB_STATES: readonly string[];
291
316
  export declare function expectedManagedTables(schema: string, partitioned: boolean, partitions?: QueuePartition[]): string[];
292
317
  export declare function expectedManagedColumns(schema: string, partitioned: boolean, partitions?: QueuePartition[]): ExpectedColumns[];