@dbos-inc/dbos-enterprise 5.4.1-preview → 5.5.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 +0 -2
- package/dist/src/auto_upgrade.d.ts +7 -20
- package/dist/src/auto_upgrade.d.ts.map +1 -1
- package/dist/src/auto_upgrade.js +8 -51
- package/dist/src/auto_upgrade.js.map +1 -1
- package/dist/src/cockroach.d.ts +7 -1
- package/dist/src/cockroach.d.ts.map +1 -1
- package/dist/src/cockroach.js +7 -1
- package/dist/src/cockroach.js.map +1 -1
- package/dist/src/conductor/conductor.d.ts +8 -5
- package/dist/src/conductor/conductor.d.ts.map +1 -1
- package/dist/src/conductor/conductor.js +768 -735
- package/dist/src/conductor/conductor.js.map +1 -1
- package/dist/src/conductor/protocol.d.ts +31 -7
- package/dist/src/conductor/protocol.d.ts.map +1 -1
- package/dist/src/conductor/protocol.js +84 -17
- package/dist/src/conductor/protocol.js.map +1 -1
- package/dist/src/export.d.ts +7 -0
- package/dist/src/export.d.ts.map +1 -1
- package/dist/src/export.js +19 -24
- package/dist/src/export.js.map +1 -1
- package/dist/src/external_storage.d.ts +23 -0
- package/dist/src/external_storage.d.ts.map +1 -0
- package/dist/src/external_storage.js +278 -0
- package/dist/src/external_storage.js.map +1 -0
- package/dist/src/external_stores.d.ts +36 -0
- package/dist/src/external_stores.d.ts.map +1 -0
- package/dist/src/external_stores.js +177 -0
- package/dist/src/external_stores.js.map +1 -0
- package/dist/src/fork.d.ts +7 -0
- package/dist/src/fork.d.ts.map +1 -1
- package/dist/src/fork.js +11 -3
- package/dist/src/fork.js.map +1 -1
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +17 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/locks.d.ts +20 -0
- package/dist/src/locks.d.ts.map +1 -0
- package/dist/src/locks.js +47 -0
- package/dist/src/locks.js.map +1 -0
- package/dist/src/observability.d.ts +8 -0
- package/dist/src/observability.d.ts.map +1 -1
- package/dist/src/observability.js +69 -44
- package/dist/src/observability.js.map +1 -1
- package/dist/src/retention.d.ts +18 -34
- package/dist/src/retention.d.ts.map +1 -1
- package/dist/src/retention.js +159 -177
- package/dist/src/retention.js.map +1 -1
- package/dist/src/sharding/index.d.ts +9 -0
- package/dist/src/sharding/index.d.ts.map +1 -0
- package/dist/src/sharding/index.js +13 -0
- package/dist/src/sharding/index.js.map +1 -0
- package/dist/src/sharding/migrations.d.ts +20 -0
- package/dist/src/sharding/migrations.d.ts.map +1 -0
- package/dist/src/sharding/migrations.js +544 -0
- package/dist/src/sharding/migrations.js.map +1 -0
- package/dist/src/sharding/schema.d.ts +9 -0
- package/dist/src/sharding/schema.d.ts.map +1 -0
- package/dist/src/sharding/schema.js +12 -0
- package/dist/src/sharding/schema.js.map +1 -0
- package/dist/src/sharding/sys_db.d.ts +104 -0
- package/dist/src/sharding/sys_db.d.ts.map +1 -0
- package/dist/src/sharding/sys_db.js +776 -0
- package/dist/src/sharding/sys_db.js.map +1 -0
- package/dist/src/suspend.d.ts +24 -0
- package/dist/src/suspend.d.ts.map +1 -0
- package/dist/src/suspend.js +110 -0
- package/dist/src/suspend.js.map +1 -0
- package/package.json +17 -4
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
// Copyright (c) 2026 DBOS, Inc. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// This file is part of Transact Enterprise, proprietary software licensed under the
|
|
4
|
+
// DBOS Conductor Software License: https://www.dbos.dev/conductor-license
|
|
5
|
+
// You may not use, copy, modify, or distribute this file except as that license permits.
|
|
6
|
+
// Disabling or circumventing the entitlement checks in @dbos-inc/dbos-sdk's src/enterprise.ts
|
|
7
|
+
// that gate it is a material breach of that license.
|
|
8
|
+
"use strict";
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
26
|
+
if (mod && mod.__esModule) return mod;
|
|
27
|
+
var result = {};
|
|
28
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
29
|
+
__setModuleDefault(result, mod);
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.ShardedSystemDatabase = void 0;
|
|
34
|
+
const context_1 = require("@dbos-inc/dbos-sdk/internal/context");
|
|
35
|
+
const error_1 = require("@dbos-inc/dbos-sdk/internal/error");
|
|
36
|
+
const debugpoint_1 = require("@dbos-inc/dbos-sdk/internal/debugpoint");
|
|
37
|
+
const serialization_1 = require("@dbos-inc/dbos-sdk/internal/serialization");
|
|
38
|
+
const shard_keys_1 = require("@dbos-inc/dbos-sdk/internal/shard_keys");
|
|
39
|
+
const system_database_1 = require("@dbos-inc/dbos-sdk/internal/system_database");
|
|
40
|
+
const utils_1 = require("@dbos-inc/dbos-sdk/internal/utils");
|
|
41
|
+
const workflow_1 = require("@dbos-inc/dbos-sdk/internal/workflow");
|
|
42
|
+
const migrations = __importStar(require("./migrations"));
|
|
43
|
+
const schema_1 = require("./schema");
|
|
44
|
+
const CITUS_COMMIT_CONFLICT = 'could not commit transaction for shard';
|
|
45
|
+
function quoteIdentifier(name) {
|
|
46
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
47
|
+
}
|
|
48
|
+
class ShardedSystemDatabase extends system_database_1.SystemDatabase {
|
|
49
|
+
discoveryIntervalMs = 60_000;
|
|
50
|
+
discoveryEmptyIntervalMs = 5_000;
|
|
51
|
+
claimSlotsPerKey = 8;
|
|
52
|
+
dbPollingIntervalEventMs = 1_000;
|
|
53
|
+
#onCitus = undefined;
|
|
54
|
+
#discovered = new Map();
|
|
55
|
+
#rediscovering = new Set();
|
|
56
|
+
#foundWork = new Map();
|
|
57
|
+
constructor(options) {
|
|
58
|
+
super(options.systemDatabaseUrl, options.logger, options.serializer, options.sysDbPoolSize ?? system_database_1.DEFAULT_POOL_SIZE, options.systemDatabasePool, options.schemaName, false, options.pollingConcurrency, undefined, options.appName, options.observabilityQueryTimeoutMs, options.idleTransactionTimeoutMs);
|
|
59
|
+
}
|
|
60
|
+
get sharded() {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
resolvedKey(workflowID, explicit) {
|
|
64
|
+
if (explicit !== undefined && explicit !== null) {
|
|
65
|
+
(0, shard_keys_1.validateShardKey)(explicit);
|
|
66
|
+
return explicit;
|
|
67
|
+
}
|
|
68
|
+
const bound = (0, shard_keys_1.boundShardKey)(workflowID);
|
|
69
|
+
if (bound !== undefined) {
|
|
70
|
+
return bound;
|
|
71
|
+
}
|
|
72
|
+
const ctx = (0, context_1.getCurrentContextStore)();
|
|
73
|
+
if (ctx !== undefined && ctx.workflowId === workflowID && ctx.shardKey) {
|
|
74
|
+
return ctx.shardKey;
|
|
75
|
+
}
|
|
76
|
+
return (0, shard_keys_1.scopedShardKey)();
|
|
77
|
+
}
|
|
78
|
+
shardKeyOf(workflowID, explicit) {
|
|
79
|
+
const key = this.resolvedKey(workflowID, explicit);
|
|
80
|
+
if (key !== undefined) {
|
|
81
|
+
return key;
|
|
82
|
+
}
|
|
83
|
+
throw new error_1.DBOSError(`No shard key for workflow ${workflowID}. On a sharded system database, pass the workflow's shardKey, ` +
|
|
84
|
+
'or use a handle that carries it.');
|
|
85
|
+
}
|
|
86
|
+
scopeKey() {
|
|
87
|
+
const scoped = (0, shard_keys_1.scopedShardKey)();
|
|
88
|
+
if (scoped === undefined) {
|
|
89
|
+
throw new error_1.DBOSError('No shard key for this operation. On a sharded system database, set one with DBOS.withShardKey(...).');
|
|
90
|
+
}
|
|
91
|
+
return scoped;
|
|
92
|
+
}
|
|
93
|
+
singleKey(keys, operation) {
|
|
94
|
+
const distinct = new Set(keys);
|
|
95
|
+
if (distinct.size > 1) {
|
|
96
|
+
throw new error_1.DBOSError(`On a sharded system database, one ${operation} acts only on workflows under a single shard key; ` +
|
|
97
|
+
`${operation} each key's workflows separately`);
|
|
98
|
+
}
|
|
99
|
+
return [...distinct][0];
|
|
100
|
+
}
|
|
101
|
+
groupByKey(workflowIDs) {
|
|
102
|
+
const groups = new Map();
|
|
103
|
+
for (const workflowID of workflowIDs) {
|
|
104
|
+
const key = this.shardKeyOf(workflowID);
|
|
105
|
+
const ids = groups.get(key) ?? [];
|
|
106
|
+
ids.push(workflowID);
|
|
107
|
+
groups.set(key, ids);
|
|
108
|
+
}
|
|
109
|
+
return groups;
|
|
110
|
+
}
|
|
111
|
+
wf(alias, workflowID, params, column = 'workflow_uuid', shardKey) {
|
|
112
|
+
const key = this.shardKeyOf(workflowID, shardKey);
|
|
113
|
+
params.push(key, workflowID);
|
|
114
|
+
return `${(0, system_database_1.qualify)(alias, 'shard_key')} = $${params.length - 1} AND ${(0, system_database_1.qualify)(alias, column)} = $${params.length}`;
|
|
115
|
+
}
|
|
116
|
+
wfIn(alias, workflowIDs, params) {
|
|
117
|
+
const groups = this.groupByKey(workflowIDs);
|
|
118
|
+
if (groups.size === 0) {
|
|
119
|
+
params.push(workflowIDs);
|
|
120
|
+
return `${(0, system_database_1.qualify)(alias, 'workflow_uuid')} = ANY($${params.length}::text[])`;
|
|
121
|
+
}
|
|
122
|
+
const predicates = Array.from(groups, ([key, ids]) => {
|
|
123
|
+
params.push(key, ids);
|
|
124
|
+
return `${(0, system_database_1.qualify)(alias, 'shard_key')} = $${params.length - 1} AND ${(0, system_database_1.qualify)(alias, 'workflow_uuid')} = ANY($${params.length}::text[])`;
|
|
125
|
+
});
|
|
126
|
+
if (predicates.length === 1) {
|
|
127
|
+
return predicates[0];
|
|
128
|
+
}
|
|
129
|
+
return `(${predicates.map((p) => `(${p})`).join(' OR ')})`;
|
|
130
|
+
}
|
|
131
|
+
wfJoin(left, right) {
|
|
132
|
+
return `${left}.shard_key = ${right}.shard_key AND ${left}.workflow_uuid = ${right}.workflow_uuid`;
|
|
133
|
+
}
|
|
134
|
+
wfValues(workflowID, shardKey) {
|
|
135
|
+
return { shard_key: this.shardKeyOf(workflowID, shardKey) };
|
|
136
|
+
}
|
|
137
|
+
parentValues(parentWorkflowID) {
|
|
138
|
+
if (parentWorkflowID === undefined) {
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
return { parent_shard_key: this.shardKeyOf(parentWorkflowID) };
|
|
142
|
+
}
|
|
143
|
+
childValues(childWorkflowID) {
|
|
144
|
+
if (childWorkflowID === undefined || childWorkflowID === null) {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
return { child_shard_key: this.resolvedKey(childWorkflowID) ?? null };
|
|
148
|
+
}
|
|
149
|
+
wfColumns(workflowID, params) {
|
|
150
|
+
params.push(this.shardKeyOf(workflowID));
|
|
151
|
+
return [['shard_key', `$${params.length}::text`]];
|
|
152
|
+
}
|
|
153
|
+
keyConflict(...columns) {
|
|
154
|
+
return ['shard_key', ...columns].join(', ');
|
|
155
|
+
}
|
|
156
|
+
async isCitus() {
|
|
157
|
+
if (this.#onCitus === undefined) {
|
|
158
|
+
const client = await this.connect();
|
|
159
|
+
try {
|
|
160
|
+
this.#onCitus = await migrations.citusInstalled(client);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
client.release();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return this.#onCitus;
|
|
167
|
+
}
|
|
168
|
+
async backgroundJobLock(jobName, callback) {
|
|
169
|
+
const name = `dbos-background-job:${this.schemaName}:${this.appName ?? ''}:${jobName}`;
|
|
170
|
+
const claim = `/* pgdog_sharding_key: "${schema_1.GLOBAL_SHARD_KEY}" */ ` +
|
|
171
|
+
`SELECT ${quoteIdentifier(this.schemaName)}.claim_gate(${(0, utils_1.quoteLiteral)(name)}, 1)`;
|
|
172
|
+
return await this.lockedBackgroundJob(jobName, `${claim} FROM set_config('idle_in_transaction_session_timeout', '${Math.trunc(this.backgroundJobLockIdleTimeoutMs)}', true)`, async (client) => {
|
|
173
|
+
const { rows } = await client.query({ text: claim, rowMode: 'array' });
|
|
174
|
+
if (rows[0]?.[0] !== true) {
|
|
175
|
+
throw new error_1.DBOSError(`Lost the ${jobName} lock`);
|
|
176
|
+
}
|
|
177
|
+
}, callback);
|
|
178
|
+
}
|
|
179
|
+
async outcomeInOneStatement() {
|
|
180
|
+
return !(await this.isCitus());
|
|
181
|
+
}
|
|
182
|
+
async insertRows(client, table, columns, rows, suffix = '') {
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
out.push(...(await super.insertRows(client, table, columns, [row], suffix)));
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
claimGate(queueName, partitionKey, params) {
|
|
190
|
+
const name = `dbos-claim:${queueName}:${this.scopeKey()}:${partitionKey ?? ''}`;
|
|
191
|
+
params.push(name, this.claimSlotsPerKey);
|
|
192
|
+
return `(SELECT ${quoteIdentifier(this.schemaName)}.claim_gate($${params.length - 1}, $${params.length}::int4))`;
|
|
193
|
+
}
|
|
194
|
+
claimLockQuery(queueName, partitionKey, params) {
|
|
195
|
+
const name = JSON.stringify(['dbos-claim-lock', this.schemaName, queueName, this.scopeKey(), partitionKey ?? null]);
|
|
196
|
+
params.push(name, queueName, workflow_1.StatusString.ENQUEUED);
|
|
197
|
+
const n = params.length;
|
|
198
|
+
const s = quoteIdentifier(this.schemaName);
|
|
199
|
+
return `SELECT (SELECT ${s}.claim_gate($${n - 2}, 1)) AS acquired
|
|
200
|
+
FROM ${s}.workflow_status
|
|
201
|
+
WHERE ${this.scoped(undefined, params)} AND queue_name = $${n - 1} AND status = $${n}
|
|
202
|
+
LIMIT 1`;
|
|
203
|
+
}
|
|
204
|
+
partitionLockQuery(queueName, partitionKeys, params) {
|
|
205
|
+
const name = JSON.stringify(['dbos-partition-claim', this.schemaName, queueName, this.scopeKey()]);
|
|
206
|
+
params.push(partitionKeys, name, queueName, workflow_1.StatusString.ENQUEUED);
|
|
207
|
+
const n = params.length;
|
|
208
|
+
const s = quoteIdentifier(this.schemaName);
|
|
209
|
+
return `SELECT ARRAY(
|
|
210
|
+
SELECT pk FROM unnest($${n - 3}::text[]) AS pk
|
|
211
|
+
WHERE ${s}.claim_gate($${n - 2}::text || ':' || pk, 1)
|
|
212
|
+
) AS held
|
|
213
|
+
FROM ${s}.workflow_status
|
|
214
|
+
WHERE ${this.scoped(undefined, params)} AND queue_name = $${n - 1} AND status = $${n}
|
|
215
|
+
LIMIT 1`;
|
|
216
|
+
}
|
|
217
|
+
scoped(alias, params) {
|
|
218
|
+
params.push(this.scopeKey());
|
|
219
|
+
return `${(0, system_database_1.qualify)(alias, 'shard_key')} = $${params.length}`;
|
|
220
|
+
}
|
|
221
|
+
scopedColumns(params) {
|
|
222
|
+
params.push(this.scopeKey());
|
|
223
|
+
return [['shard_key', `$${params.length}::text`]];
|
|
224
|
+
}
|
|
225
|
+
global(alias, params) {
|
|
226
|
+
params.push(schema_1.GLOBAL_SHARD_KEY);
|
|
227
|
+
return `${(0, system_database_1.qualify)(alias, 'shard_key')} = $${params.length}`;
|
|
228
|
+
}
|
|
229
|
+
globalValues() {
|
|
230
|
+
return { shard_key: schema_1.GLOBAL_SHARD_KEY };
|
|
231
|
+
}
|
|
232
|
+
keyIn(alias, shardKeys, params, column = 'shard_key') {
|
|
233
|
+
for (const shardKey of shardKeys) {
|
|
234
|
+
(0, shard_keys_1.validateShardKey)(shardKey);
|
|
235
|
+
}
|
|
236
|
+
if (shardKeys.length === 0) {
|
|
237
|
+
return 'FALSE';
|
|
238
|
+
}
|
|
239
|
+
const binds = shardKeys.map((shardKey) => `$${params.push(shardKey)}`);
|
|
240
|
+
return `${(0, system_database_1.qualify)(alias, column)} IN (${binds.join(', ')})`;
|
|
241
|
+
}
|
|
242
|
+
keyColumn(alias, column = 'shard_key') {
|
|
243
|
+
return (0, system_database_1.qualify)(alias, column);
|
|
244
|
+
}
|
|
245
|
+
keyHint(workflowID) {
|
|
246
|
+
return this.shardKeyOf(workflowID);
|
|
247
|
+
}
|
|
248
|
+
scheduleValues(schedule) {
|
|
249
|
+
const shardKey = schedule.shardKey;
|
|
250
|
+
if (shardKey === undefined || shardKey === null) {
|
|
251
|
+
throw new error_1.DBOSError(`Schedule '${schedule.scheduleName}' has no shard key. On a sharded system database, every schedule ` +
|
|
252
|
+
'names the shard key its workflows run under.');
|
|
253
|
+
}
|
|
254
|
+
(0, shard_keys_1.validateShardKey)(shardKey);
|
|
255
|
+
return { workflow_shard_key: shardKey };
|
|
256
|
+
}
|
|
257
|
+
scheduleKeyColumn() {
|
|
258
|
+
return 'workflow_shard_key';
|
|
259
|
+
}
|
|
260
|
+
async runMigrations() {
|
|
261
|
+
await migrations.runMigrations(this);
|
|
262
|
+
}
|
|
263
|
+
async verifyMigrations() {
|
|
264
|
+
await migrations.verifyMigrations(this);
|
|
265
|
+
}
|
|
266
|
+
isSerializationConflict(e) {
|
|
267
|
+
return super.isSerializationConflict(e) || (e instanceof Error && e.message.includes(CITUS_COMMIT_CONFLICT));
|
|
268
|
+
}
|
|
269
|
+
static requireKey(status) {
|
|
270
|
+
if (status.shardKey === undefined || status.shardKey === null) {
|
|
271
|
+
throw new error_1.DBOSError(`Workflow ${status.workflowUUID} has no shard key. On a sharded system database, start or enqueue every ` +
|
|
272
|
+
'workflow under an explicit shard key: with DBOS.withShardKey(...), or with the shardKey enqueue option ' +
|
|
273
|
+
'for DBOSClient.');
|
|
274
|
+
}
|
|
275
|
+
(0, shard_keys_1.validateShardKey)(status.shardKey);
|
|
276
|
+
}
|
|
277
|
+
async insertWorkflowStatus(client, initStatus, creatorXid) {
|
|
278
|
+
ShardedSystemDatabase.requireKey(initStatus);
|
|
279
|
+
if (initStatus.deduplicationID !== undefined && initStatus.deduplicationID !== null) {
|
|
280
|
+
throw new error_1.DBOSError('Deduplication is not supported on a sharded system database: a deduplication ID could only be unique ' +
|
|
281
|
+
'within one shard key.');
|
|
282
|
+
}
|
|
283
|
+
return await super.insertWorkflowStatus(client, initStatus, creatorXid);
|
|
284
|
+
}
|
|
285
|
+
debounceDelayedWorkflow() {
|
|
286
|
+
throw new error_1.DBOSError('Debouncing is not supported on a sharded system database: it relies on deduplication, which could only be ' +
|
|
287
|
+
'unique within one shard key.');
|
|
288
|
+
}
|
|
289
|
+
enqueueWorkflows() {
|
|
290
|
+
throw new error_1.DBOSError('Batch-inserting workflows is not supported on a sharded system database.');
|
|
291
|
+
}
|
|
292
|
+
async enqueueScheduledWorkflows(statuses, firedAt) {
|
|
293
|
+
for (const status of statuses) {
|
|
294
|
+
ShardedSystemDatabase.requireKey(status);
|
|
295
|
+
}
|
|
296
|
+
const byKey = new Map();
|
|
297
|
+
for (const status of statuses) {
|
|
298
|
+
const keyed = byKey.get(status.shardKey) ?? [];
|
|
299
|
+
keyed.push(status);
|
|
300
|
+
byKey.set(status.shardKey, keyed);
|
|
301
|
+
}
|
|
302
|
+
const inserted = new Set();
|
|
303
|
+
try {
|
|
304
|
+
for (const keyed of byKey.values()) {
|
|
305
|
+
const ids = await (0, system_database_1.withDbRetry)(async () => {
|
|
306
|
+
const createdAts = await this.assignBatchCreatedAt(keyed);
|
|
307
|
+
return await this.#transaction((client) => this.insertWorkflowBatch(client, keyed, createdAts));
|
|
308
|
+
});
|
|
309
|
+
for (const id of ids) {
|
|
310
|
+
inserted.add(id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
if (inserted.size > 0) {
|
|
316
|
+
await (0, system_database_1.withDbRetry)(() => this.#transaction((client) => this.stampLastFiredAt(client, statuses, firedAt, inserted)));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return inserted;
|
|
320
|
+
}
|
|
321
|
+
renameApplication() {
|
|
322
|
+
throw new error_1.DBOSError('Renaming an application is not supported on a sharded system database.');
|
|
323
|
+
}
|
|
324
|
+
async precheckOwner(workflowID) {
|
|
325
|
+
if (workflowID === undefined) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const ownerXid = (0, context_1.currentOwnerXid)(workflowID);
|
|
329
|
+
if (ownerXid !== undefined) {
|
|
330
|
+
await (0, system_database_1.withDbRetry)(async () => {
|
|
331
|
+
const client = await this.connect();
|
|
332
|
+
try {
|
|
333
|
+
await this.inTransaction(client, () => this.checkOwner(client, workflowID, ownerXid));
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
client.release();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async initChildWorkflowStatus(initStatus, creatorXid, parentWorkflowID, parentFunctionID, startTimeEpochMs, endTimeEpochMs, reusePolicy = 'return-existing') {
|
|
342
|
+
if (reusePolicy === 'reject') {
|
|
343
|
+
throw new error_1.DBOSError("On a sharded system database, workflowIDReusePolicy 'reject' is not supported for a workflow started " +
|
|
344
|
+
'from inside another workflow; use it only outside workflows.');
|
|
345
|
+
}
|
|
346
|
+
ShardedSystemDatabase.requireKey(initStatus);
|
|
347
|
+
const sameKey = initStatus.shardKey === this.shardKeyOf(parentWorkflowID);
|
|
348
|
+
if (!sameKey && initStatus.status === workflow_1.StatusString.PENDING) {
|
|
349
|
+
throw new error_1.DBOSError(`Workflow ${parentWorkflowID} can't start or call child workflow ${initStatus.workflowUUID} under another ` +
|
|
350
|
+
"shard key. On a sharded system database, a child runs under its parent's key unless it's enqueued.");
|
|
351
|
+
}
|
|
352
|
+
const parentOwnerXid = (0, context_1.currentOwnerXid)(parentWorkflowID);
|
|
353
|
+
const result = await (0, system_database_1.withDbRetry)(async () => {
|
|
354
|
+
if (sameKey) {
|
|
355
|
+
return await this.#transaction(async (client) => {
|
|
356
|
+
if (parentOwnerXid !== undefined) {
|
|
357
|
+
await this.checkOwner(client, parentWorkflowID, parentOwnerXid);
|
|
358
|
+
}
|
|
359
|
+
const inserted = await this.initWorkflowStatusInternal(client, initStatus, creatorXid, reusePolicy);
|
|
360
|
+
await this.recordChild(client, initStatus, parentWorkflowID, parentFunctionID, startTimeEpochMs, endTimeEpochMs);
|
|
361
|
+
return inserted;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (parentOwnerXid !== undefined) {
|
|
365
|
+
await this.#transaction((client) => this.checkOwner(client, parentWorkflowID, parentOwnerXid));
|
|
366
|
+
}
|
|
367
|
+
const inserted = await this.#transaction((client) => this.initWorkflowStatusInternal(client, initStatus, creatorXid, reusePolicy));
|
|
368
|
+
await this.#transaction(async (client) => {
|
|
369
|
+
if (parentOwnerXid !== undefined) {
|
|
370
|
+
await this.checkOwner(client, parentWorkflowID, parentOwnerXid);
|
|
371
|
+
}
|
|
372
|
+
await this.recordChild(client, initStatus, parentWorkflowID, parentFunctionID, startTimeEpochMs, endTimeEpochMs);
|
|
373
|
+
});
|
|
374
|
+
return inserted;
|
|
375
|
+
});
|
|
376
|
+
await (0, debugpoint_1.debugTriggerPoint)(debugpoint_1.DEBUG_TRIGGER_INITWF_COMMIT);
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
async #transaction(fn) {
|
|
380
|
+
const client = await this.connect();
|
|
381
|
+
try {
|
|
382
|
+
return await this.inTransaction(client, () => fn(client));
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
client.release();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async recordChild(client, status, parentWorkflowID, parentFunctionID, startTimeEpochMs, endTimeEpochMs) {
|
|
389
|
+
const childWorkflowID = status.workflowUUID;
|
|
390
|
+
const values = {
|
|
391
|
+
...this.wfValues(parentWorkflowID),
|
|
392
|
+
workflow_uuid: parentWorkflowID,
|
|
393
|
+
function_id: parentFunctionID,
|
|
394
|
+
function_name: status.workflowName,
|
|
395
|
+
child_workflow_id: childWorkflowID,
|
|
396
|
+
child_shard_key: status.shardKey,
|
|
397
|
+
started_at_epoch_ms: startTimeEpochMs,
|
|
398
|
+
completed_at_epoch_ms: endTimeEpochMs,
|
|
399
|
+
application_name: this.appName ?? null,
|
|
400
|
+
};
|
|
401
|
+
const columns = Object.keys(values);
|
|
402
|
+
const { rows: inserted } = await client.query(`INSERT INTO "${this.schemaName}".operation_outputs (${columns.join(', ')}, retention_timestamp)
|
|
403
|
+
VALUES (${columns.map((_, i) => `$${i + 1}`).join(', ')}, (EXTRACT(EPOCH FROM now()) * 1000)::bigint)
|
|
404
|
+
ON CONFLICT DO NOTHING
|
|
405
|
+
RETURNING function_id`, Object.values(values));
|
|
406
|
+
if (inserted.length === 0) {
|
|
407
|
+
const params = [parentFunctionID];
|
|
408
|
+
const { rows: existing } = await client.query(`SELECT child_workflow_id FROM "${this.schemaName}".operation_outputs
|
|
409
|
+
WHERE ${this.wf(undefined, parentWorkflowID, params)} AND function_id = $1`, params);
|
|
410
|
+
if (existing.length === 0 || existing[0].child_workflow_id !== childWorkflowID) {
|
|
411
|
+
throw new error_1.DBOSStepNondeterminismError(parentWorkflowID, parentFunctionID);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
keyedMessage(destinationID, shardKey, idempotencyKey, workflowID, functionID) {
|
|
416
|
+
if (shardKey === undefined || shardKey === null) {
|
|
417
|
+
throw new error_1.DBOSError(`Message to workflow ${destinationID} has no shard key. On a sharded system database, pass the ` +
|
|
418
|
+
"destination's shardKey.");
|
|
419
|
+
}
|
|
420
|
+
(0, shard_keys_1.validateShardKey)(shardKey);
|
|
421
|
+
if (workflowID !== undefined && !idempotencyKey) {
|
|
422
|
+
const senderKey = this.shardKeyOf(workflowID);
|
|
423
|
+
idempotencyKey = `${[...senderKey].length}:${senderKey}:${workflowID}::${functionID}::0`;
|
|
424
|
+
}
|
|
425
|
+
return { key: shardKey, idempotencyKey };
|
|
426
|
+
}
|
|
427
|
+
async #deliver(key, db, destinationID, message, topic, serialization, idempotencyKey) {
|
|
428
|
+
await (0, shard_keys_1.shardKeyScope)(key, () => this.sendDirectInternal(db, destinationID, message, topic, serialization, idempotencyKey, key));
|
|
429
|
+
}
|
|
430
|
+
async send(workflowID, functionID, destinationID, message, topic, serialization, idempotencyKey, shardKey) {
|
|
431
|
+
const startTime = Date.now();
|
|
432
|
+
const recorded = await this.getOperationResultAndThrowIfCancelled(workflowID, functionID);
|
|
433
|
+
if (recorded !== undefined) {
|
|
434
|
+
if (recorded.functionName !== system_database_1.DBOS_FUNCNAME_SEND) {
|
|
435
|
+
throw new error_1.DBOSUnexpectedStepError(workflowID, functionID, system_database_1.DBOS_FUNCNAME_SEND, recorded.functionName);
|
|
436
|
+
}
|
|
437
|
+
if (recorded.error) {
|
|
438
|
+
throw await (0, serialization_1.deserializeResError)(recorded.error, recorded.serialization ?? null, this.serializer);
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const keyed = this.keyedMessage(destinationID, shardKey, idempotencyKey, workflowID, functionID);
|
|
443
|
+
await this.precheckOwner(workflowID);
|
|
444
|
+
try {
|
|
445
|
+
await (0, system_database_1.withDbRetry)(() => this.#deliver(keyed.key, this.pool, destinationID, message, topic, serialization, keyed.idempotencyKey));
|
|
446
|
+
}
|
|
447
|
+
catch (e) {
|
|
448
|
+
if (e instanceof error_1.DBOSNonExistentWorkflowError) {
|
|
449
|
+
await this.recordOperationError(workflowID, functionID, system_database_1.DBOS_FUNCNAME_SEND, startTime, e);
|
|
450
|
+
}
|
|
451
|
+
throw e;
|
|
452
|
+
}
|
|
453
|
+
await this.recordOperationResult(workflowID, functionID, system_database_1.DBOS_FUNCNAME_SEND, true, startTime, Date.now(), {
|
|
454
|
+
output: null,
|
|
455
|
+
serialization: serialization_1.DBOSPortableJSON.name(),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async sendDirect(destinationID, message, topic, serialization, idempotencyKey, client, shardKey) {
|
|
459
|
+
const keyed = this.keyedMessage(destinationID, shardKey, idempotencyKey, undefined, undefined);
|
|
460
|
+
if (client !== undefined) {
|
|
461
|
+
await this.#deliver(keyed.key, client, destinationID, message, topic, serialization, keyed.idempotencyKey);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
await (0, system_database_1.withDbRetry)(() => this.#deliver(keyed.key, this.pool, destinationID, message, topic, serialization, keyed.idempotencyKey));
|
|
465
|
+
}
|
|
466
|
+
async sendFromStep(workflowID, destinationID, message, topic, serialization, idempotencyKey, shardKey) {
|
|
467
|
+
const keyed = this.keyedMessage(destinationID, shardKey, idempotencyKey, undefined, undefined);
|
|
468
|
+
await this.precheckOwner(workflowID);
|
|
469
|
+
await (0, system_database_1.withDbRetry)(() => this.#deliver(keyed.key, this.pool, destinationID, message, topic, serialization, keyed.idempotencyKey));
|
|
470
|
+
await this.precheckOwner(workflowID);
|
|
471
|
+
}
|
|
472
|
+
async runTransactionalStep(workflowID, functionID, functionName, callback) {
|
|
473
|
+
const startTime = Date.now();
|
|
474
|
+
const recorded = await this.getOperationResultAndThrowIfCancelled(workflowID, functionID);
|
|
475
|
+
if (recorded !== undefined) {
|
|
476
|
+
return recorded;
|
|
477
|
+
}
|
|
478
|
+
let output;
|
|
479
|
+
try {
|
|
480
|
+
output = await (0, system_database_1.withDbRetry)(() => this.#transaction(callback));
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
await this.recordOperationError(workflowID, functionID, functionName, startTime, e);
|
|
484
|
+
throw e;
|
|
485
|
+
}
|
|
486
|
+
await this.recordOperationResult(workflowID, functionID, functionName, true, startTime, Date.now(), { output });
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
async getWorkflowStatus(workflowID, callerID, callerFN) {
|
|
490
|
+
if (!callerID || callerFN === undefined) {
|
|
491
|
+
return await super.getWorkflowStatus(workflowID);
|
|
492
|
+
}
|
|
493
|
+
const startTime = Date.now();
|
|
494
|
+
const recorded = await this.getOperationResultAndThrowIfCancelled(callerID, callerFN);
|
|
495
|
+
if (recorded !== undefined) {
|
|
496
|
+
if (recorded.functionName !== system_database_1.DBOS_FUNCNAME_GETSTATUS) {
|
|
497
|
+
throw new error_1.DBOSUnexpectedStepError(callerID, callerFN, system_database_1.DBOS_FUNCNAME_GETSTATUS, recorded.functionName);
|
|
498
|
+
}
|
|
499
|
+
if (recorded.error) {
|
|
500
|
+
throw await (0, serialization_1.deserializeResError)(recorded.error, recorded.serialization ?? null, this.serializer);
|
|
501
|
+
}
|
|
502
|
+
return recorded.output ? JSON.parse(recorded.output) : null;
|
|
503
|
+
}
|
|
504
|
+
const status = await super.getWorkflowStatus(workflowID);
|
|
505
|
+
await this.recordOperationResult(callerID, callerFN, system_database_1.DBOS_FUNCNAME_GETSTATUS, true, startTime, Date.now(), {
|
|
506
|
+
output: status ? JSON.stringify(status) : null,
|
|
507
|
+
serialization: serialization_1.DBOSPortableJSON.name(),
|
|
508
|
+
});
|
|
509
|
+
return status;
|
|
510
|
+
}
|
|
511
|
+
keyOfBatch(workflowIDs, operation) {
|
|
512
|
+
return this.singleKey(workflowIDs.map((id) => this.shardKeyOf(id)), operation);
|
|
513
|
+
}
|
|
514
|
+
async deadLetterWorkflows(workflowIDs, minRecoveryAttempts) {
|
|
515
|
+
const key = this.keyOfBatch(workflowIDs, 'deadLetterWorkflows');
|
|
516
|
+
if (key === undefined) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
await (0, shard_keys_1.shardKeyScope)(key, () => super.deadLetterWorkflows(workflowIDs, minRecoveryAttempts));
|
|
520
|
+
}
|
|
521
|
+
async resumeWorkflows(workflowIDs, queueName) {
|
|
522
|
+
const key = this.keyOfBatch(workflowIDs, 'resumeWorkflows');
|
|
523
|
+
if (key === undefined) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
await (0, shard_keys_1.shardKeyScope)(key, () => super.resumeWorkflows(workflowIDs, queueName));
|
|
527
|
+
}
|
|
528
|
+
async listWorkflows(input, options = {}) {
|
|
529
|
+
const keys = input.shardKey;
|
|
530
|
+
if (typeof keys === 'string' || (Array.isArray(keys) && keys.length === 1)) {
|
|
531
|
+
return await super.listWorkflows(input, options);
|
|
532
|
+
}
|
|
533
|
+
const limit = input.limit;
|
|
534
|
+
const offset = input.offset ?? 0;
|
|
535
|
+
const rows = await super.listWorkflows({ ...input, limit: limit ? offset + limit : undefined, offset: undefined }, options);
|
|
536
|
+
const desc = input.sortDesc ?? false;
|
|
537
|
+
rows.sort((a, b) => {
|
|
538
|
+
const byCreated = (a.createdAt ?? 0) - (b.createdAt ?? 0);
|
|
539
|
+
const order = byCreated !== 0 ? byCreated : a.workflowUUID < b.workflowUUID ? -1 : a.workflowUUID > b.workflowUUID ? 1 : 0;
|
|
540
|
+
return desc ? -order : order;
|
|
541
|
+
});
|
|
542
|
+
return rows.slice(offset, limit ? offset + limit : undefined);
|
|
543
|
+
}
|
|
544
|
+
async getWorkflowStatuses(workflowIDs) {
|
|
545
|
+
const found = new Map();
|
|
546
|
+
for (const [key, ids] of this.groupByKey(workflowIDs)) {
|
|
547
|
+
const statuses = await (0, shard_keys_1.shardKeyScope)(key, () => super.getWorkflowStatuses(ids));
|
|
548
|
+
for (const [id, status] of statuses) {
|
|
549
|
+
found.set(id, status);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return new Map(workflowIDs.filter((id) => found.has(id)).map((id) => [id, found.get(id)]));
|
|
553
|
+
}
|
|
554
|
+
async checkFirstWorkflowId(workflowIds) {
|
|
555
|
+
if (workflowIds.length === 0) {
|
|
556
|
+
throw new error_1.DBOSError('workflowIds must not be empty');
|
|
557
|
+
}
|
|
558
|
+
for (const [key, ids] of this.groupByKey(workflowIds)) {
|
|
559
|
+
const first = await (0, shard_keys_1.shardKeyScope)(key, () => super.checkFirstWorkflowId(ids));
|
|
560
|
+
if (first !== undefined) {
|
|
561
|
+
return first;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
async checkCompletedWorkflowIds(workflowIds) {
|
|
567
|
+
const completed = [];
|
|
568
|
+
for (const [key, ids] of this.groupByKey(workflowIds)) {
|
|
569
|
+
completed.push(...(await (0, shard_keys_1.shardKeyScope)(key, () => super.checkCompletedWorkflowIds(ids))));
|
|
570
|
+
}
|
|
571
|
+
return completed;
|
|
572
|
+
}
|
|
573
|
+
async childrenOf(parents) {
|
|
574
|
+
if (parents.length === 0) {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
const byKey = ShardedSystemDatabase.#pairsByKey(parents);
|
|
578
|
+
const params = [];
|
|
579
|
+
const predicates = Array.from(byKey, ([key, ids]) => {
|
|
580
|
+
params.push(key, ids);
|
|
581
|
+
return `(parent_shard_key = $${params.length - 1} AND parent_workflow_id = ANY($${params.length}::text[]))`;
|
|
582
|
+
});
|
|
583
|
+
const { rows } = await (0, system_database_1.withDbRetry)(() => this.pool.query(`SELECT workflow_uuid, shard_key FROM "${this.schemaName}".workflow_status WHERE ${predicates.join(' OR ')}`, params));
|
|
584
|
+
return rows.map((row) => [row.workflow_uuid, row.shard_key]);
|
|
585
|
+
}
|
|
586
|
+
getDirectChildren() {
|
|
587
|
+
throw new error_1.DBOSError('On a sharded system database, children are found by (workflow ID, shard key) with childrenOf');
|
|
588
|
+
}
|
|
589
|
+
static #pairsByKey(pairs) {
|
|
590
|
+
const groups = new Map();
|
|
591
|
+
for (const [workflowID, key] of pairs) {
|
|
592
|
+
const ids = groups.get(key) ?? [];
|
|
593
|
+
ids.push(workflowID);
|
|
594
|
+
groups.set(key, ids);
|
|
595
|
+
}
|
|
596
|
+
return groups;
|
|
597
|
+
}
|
|
598
|
+
async #applyByLevel(workflowIDs, descend, apply, operation) {
|
|
599
|
+
const rootKey = this.keyOfBatch(workflowIDs, operation);
|
|
600
|
+
const pairKey = (id, key) => `${key.length}:${key}:${id}`;
|
|
601
|
+
let level = workflowIDs.map((id) => [id, rootKey]);
|
|
602
|
+
const visited = new Set(level.map(([id, key]) => pairKey(id, key)));
|
|
603
|
+
while (level.length > 0) {
|
|
604
|
+
for (const [key, ids] of ShardedSystemDatabase.#pairsByKey(level)) {
|
|
605
|
+
await (0, shard_keys_1.bindShardKeys)(ids.map((id) => [id, key]), () => (0, shard_keys_1.shardKeyScope)(key, () => apply(ids)));
|
|
606
|
+
}
|
|
607
|
+
if (!descend) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
level = (await this.childrenOf(level)).filter(([id, key]) => !visited.has(pairKey(id, key)));
|
|
611
|
+
for (const [id, key] of level) {
|
|
612
|
+
visited.add(pairKey(id, key));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async cancelWorkflows(workflowIDs, cancelChildren = false) {
|
|
617
|
+
await this.#applyByLevel(workflowIDs, cancelChildren, (ids) => super.cancelWorkflows(ids, false), 'cancelWorkflows');
|
|
618
|
+
}
|
|
619
|
+
async deleteWorkflows(workflowIDs, deleteChildren = false) {
|
|
620
|
+
await this.#applyByLevel(workflowIDs, deleteChildren, (ids) => this.deleteWorkflowsInternal(ids), 'deleteWorkflows');
|
|
621
|
+
}
|
|
622
|
+
async bulkForkWorkflows(originalWorkflowIDs, forkedWorkflowIDs, startSteps, options = {}) {
|
|
623
|
+
if (originalWorkflowIDs.length !== forkedWorkflowIDs.length || originalWorkflowIDs.length !== startSteps.length) {
|
|
624
|
+
throw new error_1.DBOSError('originalWorkflowIDs, forkedWorkflowIDs, and startSteps must have the same length');
|
|
625
|
+
}
|
|
626
|
+
const key = this.keyOfBatch(originalWorkflowIDs, 'forkWorkflow');
|
|
627
|
+
if (key === undefined) {
|
|
628
|
+
return forkedWorkflowIDs;
|
|
629
|
+
}
|
|
630
|
+
await (0, shard_keys_1.shardKeyScope)(key, () => (0, shard_keys_1.bindShardKeys)(forkedWorkflowIDs.map((f) => [f, key]), () => super.bulkForkWorkflows(originalWorkflowIDs, forkedWorkflowIDs, startSteps, options)));
|
|
631
|
+
return forkedWorkflowIDs;
|
|
632
|
+
}
|
|
633
|
+
async dequeueScopes(queueName) {
|
|
634
|
+
const cached = this.#discovered.get(queueName);
|
|
635
|
+
const found = this.#foundWork.get(queueName);
|
|
636
|
+
this.#foundWork.delete(queueName);
|
|
637
|
+
const now = performance.now();
|
|
638
|
+
if (cached !== undefined &&
|
|
639
|
+
cached.keys.length === 0 &&
|
|
640
|
+
now - cached.at < Math.min(this.discoveryEmptyIntervalMs, this.discoveryIntervalMs) * cached.share) {
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
if (cached !== undefined && cached.keys.length > 0 && this.discoveryIntervalMs > 0) {
|
|
644
|
+
if (found === false || now - cached.at >= this.discoveryIntervalMs * cached.share) {
|
|
645
|
+
this.#rediscover(queueName, cached.keys);
|
|
646
|
+
}
|
|
647
|
+
return [...cached.keys];
|
|
648
|
+
}
|
|
649
|
+
const scopes = await this.discoverScopes(queueName);
|
|
650
|
+
this.#remember(queueName, scopes);
|
|
651
|
+
return [...scopes];
|
|
652
|
+
}
|
|
653
|
+
#noteClaim(queueName, found) {
|
|
654
|
+
this.#foundWork.set(queueName, (this.#foundWork.get(queueName) ?? false) || found);
|
|
655
|
+
}
|
|
656
|
+
async claimStartableWorkflows(queue, executorID, appVersion, queuePartitionKey, localRunningCount = 0, partitionLocalRunningCount = 0, ownerXid = null, isLatestVersion) {
|
|
657
|
+
const claim = await super.claimStartableWorkflows(queue, executorID, appVersion, queuePartitionKey, localRunningCount, partitionLocalRunningCount, ownerXid, isLatestVersion);
|
|
658
|
+
this.#noteClaim(queue.name, claim.claimedIDs.length > 0 || claim.stopReason !== undefined);
|
|
659
|
+
return claim;
|
|
660
|
+
}
|
|
661
|
+
async findAndMarkStartablePartitionedWorkflows(queue, executorID, appVersion, maxTasks = Infinity, ownerXid = null, isLatestVersion) {
|
|
662
|
+
const claimed = await super.findAndMarkStartablePartitionedWorkflows(queue, executorID, appVersion, maxTasks, ownerXid, isLatestVersion);
|
|
663
|
+
this.#noteClaim(queue.name, claimed.length > 0);
|
|
664
|
+
return claimed;
|
|
665
|
+
}
|
|
666
|
+
async getQueuePartitions(queueName) {
|
|
667
|
+
const partitions = await super.getQueuePartitions(queueName);
|
|
668
|
+
if (partitions.length === 0) {
|
|
669
|
+
this.#noteClaim(queueName, false);
|
|
670
|
+
}
|
|
671
|
+
return partitions;
|
|
672
|
+
}
|
|
673
|
+
#remember(queueName, keys) {
|
|
674
|
+
this.#discovered.set(queueName, { at: performance.now(), share: 0.8 + Math.random() * 0.4, keys });
|
|
675
|
+
}
|
|
676
|
+
#rediscover(queueName, keys) {
|
|
677
|
+
if (this.#rediscovering.has(queueName)) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this.#rediscovering.add(queueName);
|
|
681
|
+
void (async () => {
|
|
682
|
+
try {
|
|
683
|
+
this.#remember(queueName, await this.discoverScopes(queueName));
|
|
684
|
+
}
|
|
685
|
+
catch (e) {
|
|
686
|
+
this.logger.warn(`Exception finding work for queue ${queueName}: ${e.message}`);
|
|
687
|
+
this.#remember(queueName, keys);
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
this.#rediscovering.delete(queueName);
|
|
691
|
+
}
|
|
692
|
+
})();
|
|
693
|
+
}
|
|
694
|
+
async discoverScopes(queueName) {
|
|
695
|
+
const params = [queueName];
|
|
696
|
+
const enqueued = (alias) => `${alias}.queue_name = $1 AND ${alias}.status = '${workflow_1.StatusString.ENQUEUED}' AND ${this.appNameFilter(`${alias}.application_name`, this.appName, params)}`;
|
|
697
|
+
if (await this.isCitus()) {
|
|
698
|
+
const filter = enqueued('ws');
|
|
699
|
+
const { rows } = await this.pool.query(`SELECT DISTINCT ws.shard_key FROM "${this.schemaName}".workflow_status ws WHERE ${filter}`, params);
|
|
700
|
+
return rows.map((row) => row.shard_key);
|
|
701
|
+
}
|
|
702
|
+
return await this.#allKeys(enqueued('ws'), params);
|
|
703
|
+
}
|
|
704
|
+
async #allKeys(enqueued, params) {
|
|
705
|
+
const { rows } = await this.pool.query(`WITH RECURSIVE keys(k) AS (
|
|
706
|
+
SELECT ''::text
|
|
707
|
+
UNION ALL
|
|
708
|
+
SELECT (SELECT min(ws.shard_key) FROM "${this.schemaName}".workflow_status ws
|
|
709
|
+
WHERE ${enqueued} AND ws.shard_key > keys.k)
|
|
710
|
+
FROM keys WHERE keys.k IS NOT NULL
|
|
711
|
+
)
|
|
712
|
+
SELECT k FROM keys WHERE k IS NOT NULL AND k != ''`, params);
|
|
713
|
+
return rows.map((row) => row.k);
|
|
714
|
+
}
|
|
715
|
+
async cancelTimedOutWorkflows(limit) {
|
|
716
|
+
const nowMs = Date.now();
|
|
717
|
+
const active = [workflow_1.StatusString.ENQUEUED, workflow_1.StatusString.PENDING, workflow_1.StatusString.DELAYED, workflow_1.StatusString.SUSPENDED];
|
|
718
|
+
const params = [active, nowMs, limit];
|
|
719
|
+
const scope = this.appNameFilter('application_name', this.appName, params);
|
|
720
|
+
const { rows: candidates } = await this.pool.query(`SELECT workflow_uuid, shard_key FROM "${this.schemaName}".workflow_status
|
|
721
|
+
WHERE status = ANY($1::text[])
|
|
722
|
+
AND workflow_deadline_epoch_ms IS NOT NULL
|
|
723
|
+
AND workflow_deadline_epoch_ms <= $2
|
|
724
|
+
AND ${scope}
|
|
725
|
+
ORDER BY workflow_deadline_epoch_ms
|
|
726
|
+
LIMIT $3`, params);
|
|
727
|
+
const cancelled = [];
|
|
728
|
+
for (const [key, ids] of ShardedSystemDatabase.#pairsByKey(candidates.map((r) => [r.workflow_uuid, r.shard_key]))) {
|
|
729
|
+
const { rows } = await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
730
|
+
SET status = $1, owner_xid = NULL, queue_name = NULL, started_at_epoch_ms = NULL, suspend_key = NULL,
|
|
731
|
+
updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint,
|
|
732
|
+
completed_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint
|
|
733
|
+
WHERE shard_key = $2 AND workflow_uuid = ANY($3::text[])
|
|
734
|
+
AND status = ANY($4::text[]) AND workflow_deadline_epoch_ms <= $5
|
|
735
|
+
RETURNING workflow_uuid`, [workflow_1.StatusString.CANCELLED, key, ids, active, nowMs]);
|
|
736
|
+
cancelled.push(...rows.map((row) => row.workflow_uuid));
|
|
737
|
+
}
|
|
738
|
+
return cancelled;
|
|
739
|
+
}
|
|
740
|
+
async reenqueueWorkflowsForRecovery(executorID, appVersion, recoveryQueueName) {
|
|
741
|
+
const params = [workflow_1.StatusString.PENDING, executorID, appVersion];
|
|
742
|
+
const scope = this.appNameFilter('application_name', this.appName, params);
|
|
743
|
+
const { rows: pending } = await (0, system_database_1.withDbRetry)(() => this.pool.query(`SELECT workflow_uuid, shard_key FROM "${this.schemaName}".workflow_status
|
|
744
|
+
WHERE status = $1 AND executor_id = $2 AND application_version = $3 AND ${scope}`, params));
|
|
745
|
+
const recovered = [];
|
|
746
|
+
for (const [key, ids] of ShardedSystemDatabase.#pairsByKey(pending.map((r) => [r.workflow_uuid, r.shard_key]))) {
|
|
747
|
+
const updateParams = [
|
|
748
|
+
workflow_1.StatusString.ENQUEUED,
|
|
749
|
+
recoveryQueueName,
|
|
750
|
+
workflow_1.StatusString.PENDING,
|
|
751
|
+
executorID,
|
|
752
|
+
key,
|
|
753
|
+
ids,
|
|
754
|
+
];
|
|
755
|
+
let rows;
|
|
756
|
+
try {
|
|
757
|
+
({ rows } = await (0, system_database_1.withDbRetry)(() => this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
758
|
+
SET started_at_epoch_ms = NULL,
|
|
759
|
+
status = $1,
|
|
760
|
+
owner_xid = NULL,
|
|
761
|
+
updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint,
|
|
762
|
+
queue_name = COALESCE(queue_name, $2)
|
|
763
|
+
WHERE shard_key = $5 AND workflow_uuid = ANY($6::text[]) AND status = $3 AND executor_id = $4
|
|
764
|
+
RETURNING workflow_uuid`, updateParams)));
|
|
765
|
+
}
|
|
766
|
+
catch (e) {
|
|
767
|
+
this.logger.error(`Exception encountered when recovering the workflows of shard key ${key}: ${String(e)}`);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
recovered.push(...rows.map((row) => ({ workflowID: row.workflow_uuid, shardKey: key })));
|
|
771
|
+
}
|
|
772
|
+
return recovered;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
exports.ShardedSystemDatabase = ShardedSystemDatabase;
|
|
776
|
+
//# sourceMappingURL=sys_db.js.map
|