@dbos-inc/dbos-sdk 4.26.3-preview → 4.26.4-preview
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/dist/schemas/system_db_schema.d.ts +5 -0
- package/dist/schemas/system_db_schema.d.ts.map +1 -1
- package/dist/schemas/system_db_schema.js.map +1 -1
- package/dist/src/adminserver.js +1 -1
- package/dist/src/adminserver.js.map +1 -1
- package/dist/src/cli/cli.d.ts.map +1 -1
- package/dist/src/cli/cli.js +64 -0
- package/dist/src/cli/cli.js.map +1 -1
- package/dist/src/cli/schema.d.ts.map +1 -1
- package/dist/src/cli/schema.js +26 -16
- package/dist/src/cli/schema.js.map +1 -1
- package/dist/src/client.d.ts +62 -4
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +58 -8
- package/dist/src/client.js.map +1 -1
- package/dist/src/conductor/conductor.d.ts.map +1 -1
- package/dist/src/conductor/conductor.js +13 -2
- package/dist/src/conductor/conductor.js.map +1 -1
- package/dist/src/conductor/protocol.d.ts +16 -2
- package/dist/src/conductor/protocol.d.ts.map +1 -1
- package/dist/src/conductor/protocol.js +10 -2
- package/dist/src/conductor/protocol.js.map +1 -1
- package/dist/src/context.d.ts +1 -0
- package/dist/src/context.d.ts.map +1 -1
- package/dist/src/context.js +2 -1
- package/dist/src/context.js.map +1 -1
- package/dist/src/dbos-executor.d.ts +1 -0
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +11 -2
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos.d.ts +41 -7
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +61 -4
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/debouncer.d.ts +4 -0
- package/dist/src/debouncer.d.ts.map +1 -1
- package/dist/src/debouncer.js +32 -8
- package/dist/src/debouncer.js.map +1 -1
- package/dist/src/enqueue_options.d.ts +59 -0
- package/dist/src/enqueue_options.d.ts.map +1 -0
- package/dist/src/enqueue_options.js +61 -0
- package/dist/src/enqueue_options.js.map +1 -0
- package/dist/src/enqueue_workflow.d.ts +12 -0
- package/dist/src/enqueue_workflow.d.ts.map +1 -0
- package/dist/src/enqueue_workflow.js +91 -0
- package/dist/src/enqueue_workflow.js.map +1 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/scheduler/scheduler.d.ts +1 -0
- package/dist/src/scheduler/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler/scheduler.js +13 -5
- package/dist/src/scheduler/scheduler.js.map +1 -1
- package/dist/src/sysdb_migrations/internal/migrations.d.ts +2 -1
- package/dist/src/sysdb_migrations/internal/migrations.d.ts.map +1 -1
- package/dist/src/sysdb_migrations/internal/migrations.js +186 -2
- package/dist/src/sysdb_migrations/internal/migrations.js.map +1 -1
- package/dist/src/sysdb_migrations/migration_runner.d.ts +5 -0
- package/dist/src/sysdb_migrations/migration_runner.d.ts.map +1 -1
- package/dist/src/sysdb_migrations/migration_runner.js +48 -10
- package/dist/src/sysdb_migrations/migration_runner.js.map +1 -1
- package/dist/src/system_database.d.ts +65 -10
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +544 -152
- package/dist/src/system_database.js.map +1 -1
- package/dist/src/utils.d.ts +1 -0
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +1 -0
- package/dist/src/utils.js.map +1 -1
- package/dist/src/wfqueue.d.ts +2 -0
- package/dist/src/wfqueue.d.ts.map +1 -1
- package/dist/src/wfqueue.js +5 -1
- package/dist/src/wfqueue.js.map +1 -1
- package/dist/src/workflow.d.ts +2 -0
- package/dist/src/workflow.d.ts.map +1 -1
- package/dist/src/workflow.js.map +1 -1
- package/dist/src/workflow_management.d.ts.map +1 -1
- package/dist/src/workflow_management.js +4 -9
- package/dist/src/workflow_management.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.SystemDatabase = exports.ensureSystemDatabase = exports.grantDbosSchemaPermissions = exports.getDbosSchemaPermissionsSql = exports.DEFAULT_NOTIFICATION_COALESCE_MS = exports.DBOS_STREAMS_CHANNEL = exports.DBOS_WORKFLOW_EVENTS_CHANNEL = exports.DBOS_NOTIFICATIONS_CHANNEL = exports.DBOS_STREAM_CLOSED_SENTINEL = exports.DEFAULT_POOL_SIZE = exports.DBOS_FUNCNAME_CLOSESTREAM = exports.DBOS_FUNCNAME_WRITESTREAM = exports.DBOS_FUNCNAME_GETSTATUS = exports.DBOS_FUNCNAME_SLEEP = exports.DBOS_FUNCNAME_GETEVENT = exports.DBOS_FUNCNAME_SETEVENT = exports.DBOS_FUNCNAME_RECV = exports.DBOS_FUNCNAME_SEND = void 0;
|
|
12
|
+
exports.SystemDatabase = exports.ensureSystemDatabase = exports.grantDbosSchemaPermissions = exports.getDbosSchemaPermissionsSql = exports.DEFAULT_RENAME_BATCH_SIZE = exports.DEFAULT_NOTIFICATION_COALESCE_MS = exports.DBOS_STREAMS_CHANNEL = exports.DBOS_WORKFLOW_EVENTS_CHANNEL = exports.DBOS_NOTIFICATIONS_CHANNEL = exports.DBOS_STREAM_CLOSED_SENTINEL = exports.DEFAULT_POOL_SIZE = exports.DBOS_FUNCNAME_CLOSESTREAM = exports.DBOS_FUNCNAME_WRITESTREAM = exports.DBOS_FUNCNAME_GETSTATUS = exports.DBOS_FUNCNAME_SLEEP = exports.DBOS_FUNCNAME_GETEVENT = exports.DBOS_FUNCNAME_SETEVENT = exports.DBOS_FUNCNAME_RECV = exports.DBOS_FUNCNAME_SEND = void 0;
|
|
13
13
|
const dbos_executor_1 = require("./dbos-executor");
|
|
14
14
|
const pg_1 = require("pg");
|
|
15
15
|
const error_1 = require("./error");
|
|
@@ -38,6 +38,8 @@ exports.DBOS_WORKFLOW_EVENTS_CHANNEL = 'dbos_workflow_events_channel';
|
|
|
38
38
|
exports.DBOS_STREAMS_CHANNEL = 'dbos_streams_channel';
|
|
39
39
|
// Interval for coalescing LISTEN/NOTIFY notifications off the write path; caps the rate of notifying commits regardless of write throughput.
|
|
40
40
|
exports.DEFAULT_NOTIFICATION_COALESCE_MS = 10;
|
|
41
|
+
// Workflows re-owned per transaction by a rename. Matches the GC default.
|
|
42
|
+
exports.DEFAULT_RENAME_BATCH_SIZE = 10_000;
|
|
41
43
|
const QUEUE_COLUMN_BY_FIELD = {
|
|
42
44
|
concurrency: 'concurrency',
|
|
43
45
|
workerConcurrency: 'worker_concurrency',
|
|
@@ -47,6 +49,7 @@ const QUEUE_COLUMN_BY_FIELD = {
|
|
|
47
49
|
partitionQueue: 'partition_queue',
|
|
48
50
|
pollingIntervalSec: 'polling_interval_sec',
|
|
49
51
|
};
|
|
52
|
+
const QUEUE_COLUMNS = 'name, concurrency, worker_concurrency, rate_limit_max, rate_limit_period_sec, priority_enabled, partition_queue, polling_interval_sec, application_name';
|
|
50
53
|
function queueRecordFromRow(row) {
|
|
51
54
|
return {
|
|
52
55
|
name: row.name,
|
|
@@ -57,6 +60,7 @@ function queueRecordFromRow(row) {
|
|
|
57
60
|
priorityEnabled: row.priority_enabled,
|
|
58
61
|
partitionQueue: row.partition_queue,
|
|
59
62
|
pollingIntervalSec: row.polling_interval_sec,
|
|
63
|
+
applicationName: row.application_name ?? undefined,
|
|
60
64
|
};
|
|
61
65
|
}
|
|
62
66
|
/** The statements granting permissions on all entities in the system schema to a role. */
|
|
@@ -172,6 +176,32 @@ class NotificationMap {
|
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
}
|
|
179
|
+
function mapVersionInfo(row) {
|
|
180
|
+
return {
|
|
181
|
+
versionId: row.version_id,
|
|
182
|
+
versionName: row.version_name,
|
|
183
|
+
versionTimestamp: Number(row.version_timestamp),
|
|
184
|
+
createdAt: Number(row.created_at),
|
|
185
|
+
applicationName: row.application_name ?? undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const SCHEDULE_COLUMNS = 'schedule_id, schedule_name, workflow_name, workflow_class_name, schedule, status, context, last_fired_at, automatic_backfill, cron_timezone, queue_name, application_name';
|
|
189
|
+
function mapWorkflowSchedule(row) {
|
|
190
|
+
return {
|
|
191
|
+
scheduleId: row.schedule_id,
|
|
192
|
+
scheduleName: row.schedule_name,
|
|
193
|
+
workflowName: row.workflow_name,
|
|
194
|
+
workflowClassName: row.workflow_class_name,
|
|
195
|
+
schedule: row.schedule,
|
|
196
|
+
status: row.status,
|
|
197
|
+
context: row.context,
|
|
198
|
+
lastFiredAt: row.last_fired_at ?? null,
|
|
199
|
+
automaticBackfill: !!row.automatic_backfill,
|
|
200
|
+
cronTimezone: row.cron_timezone ?? null,
|
|
201
|
+
queueName: row.queue_name ?? null,
|
|
202
|
+
applicationName: row.application_name ?? undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
175
205
|
function mapWorkflowStatus(row) {
|
|
176
206
|
return {
|
|
177
207
|
workflowUUID: row.workflow_uuid,
|
|
@@ -209,6 +239,7 @@ function mapWorkflowStatus(row) {
|
|
|
209
239
|
scheduleName: row.schedule_name ?? undefined,
|
|
210
240
|
debounceDeadlineEpochMS: row.debounce_deadline_epoch_ms ? Number(row.debounce_deadline_epoch_ms) : undefined,
|
|
211
241
|
isDebounced: row.is_debounced ?? false,
|
|
242
|
+
applicationName: row.application_name ?? undefined,
|
|
212
243
|
};
|
|
213
244
|
}
|
|
214
245
|
// SQLSTATE classes/codes that are generally safe to retry
|
|
@@ -364,6 +395,7 @@ class SystemDatabase {
|
|
|
364
395
|
systemDatabaseUrl;
|
|
365
396
|
logger;
|
|
366
397
|
serializer;
|
|
398
|
+
appName;
|
|
367
399
|
// ==================== Lifecycle ====================
|
|
368
400
|
pool;
|
|
369
401
|
schemaName;
|
|
@@ -411,10 +443,13 @@ class SystemDatabase {
|
|
|
411
443
|
runningWorkflowMap = new Map(); // Map from workflowID to workflow promise, queue name and partition key
|
|
412
444
|
// Per-partition-key created_at cursors: keep per-key queue order monotonic across batches
|
|
413
445
|
#batchCreatedAtCursors = new Map();
|
|
414
|
-
constructor(systemDatabaseUrl, logger, serializer, sysDbPoolSize = exports.DEFAULT_POOL_SIZE, systemDatabasePool, schemaName = 'dbos', useListenNotify = true, pollingConcurrency, notificationCoalesceMs = exports.DEFAULT_NOTIFICATION_COALESCE_MS
|
|
446
|
+
constructor(systemDatabaseUrl, logger, serializer, sysDbPoolSize = exports.DEFAULT_POOL_SIZE, systemDatabasePool, schemaName = 'dbos', useListenNotify = true, pollingConcurrency, notificationCoalesceMs = exports.DEFAULT_NOTIFICATION_COALESCE_MS,
|
|
447
|
+
// The application this handle acts for; undefined writes unclaimed rows.
|
|
448
|
+
appName) {
|
|
415
449
|
this.systemDatabaseUrl = systemDatabaseUrl;
|
|
416
450
|
this.logger = logger;
|
|
417
451
|
this.serializer = serializer;
|
|
452
|
+
this.appName = appName;
|
|
418
453
|
this.schemaName = schemaName;
|
|
419
454
|
this.shouldUseDBNotifications = useListenNotify;
|
|
420
455
|
this.notificationCoalesceMs = notificationCoalesceMs;
|
|
@@ -448,6 +483,60 @@ class SystemDatabase {
|
|
|
448
483
|
getSerializer() {
|
|
449
484
|
return this.serializer;
|
|
450
485
|
}
|
|
486
|
+
// ==================== Application Ownership ====================
|
|
487
|
+
/**
|
|
488
|
+
* A predicate matching rows owned by these applications plus unclaimed ones, which belong to
|
|
489
|
+
* every application; unset or empty matches everything. Appends its bind parameter to `params`.
|
|
490
|
+
*/
|
|
491
|
+
#appNameFilter(column, value, params) {
|
|
492
|
+
// An empty name is no name: it is not a value any application could be configured with.
|
|
493
|
+
const names = !value ? [] : Array.isArray(value) ? value : [value];
|
|
494
|
+
if (names.length === 0)
|
|
495
|
+
return 'TRUE';
|
|
496
|
+
params.push(names);
|
|
497
|
+
return `(${column} = ANY($${params.length}) OR ${column} IS NULL)`;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* The filter above defaulted to this handle's own application: an unset filter scopes to what
|
|
501
|
+
* this application owns, not to every application's rows. A handle with no application of its
|
|
502
|
+
* own still matches every one.
|
|
503
|
+
*/
|
|
504
|
+
#observabilityFilter(column, value, params) {
|
|
505
|
+
return this.#appNameFilter(column, value ?? this.appName, params);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* The version name a dequeue treats as latest: this application's own plus unclaimed
|
|
509
|
+
* ones, so a named peer's deploy does not demote this one.
|
|
510
|
+
*/
|
|
511
|
+
async #latestApplicationVersionName(client) {
|
|
512
|
+
const params = [];
|
|
513
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
514
|
+
const { rows } = await client.query(`SELECT version_name
|
|
515
|
+
FROM "${this.schemaName}".application_versions
|
|
516
|
+
WHERE ${scope}
|
|
517
|
+
ORDER BY version_timestamp DESC LIMIT 1`, params);
|
|
518
|
+
return rows[0]?.version_name;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The owner to persist when writing a row that may already exist. A nameless writer
|
|
522
|
+
* leaves the owner intact; a named one collides only with a different name.
|
|
523
|
+
*/
|
|
524
|
+
async #resolveRowOwner(client, table, keyColumn, name, owner, kind) {
|
|
525
|
+
const { rows } = await client.query(`SELECT application_name FROM "${this.schemaName}".${table} WHERE ${keyColumn} = $1`, [name]);
|
|
526
|
+
const current = rows[0]?.application_name ?? null;
|
|
527
|
+
if (current === null)
|
|
528
|
+
return owner;
|
|
529
|
+
if (owner === undefined || current === owner)
|
|
530
|
+
return current;
|
|
531
|
+
// A version name is computed or pinned, so "pick another" is config advice, not a rename.
|
|
532
|
+
const takeANewName = kind === 'Application version'
|
|
533
|
+
? `set a distinct applicationVersion for '${owner}'`
|
|
534
|
+
: `give '${owner}' a different ${kind.toLowerCase()} name`;
|
|
535
|
+
throw new error_1.DBOSError(`${kind} '${name}' is already registered by application '${current}' in this system database. ` +
|
|
536
|
+
`${kind} names must be unique across applications sharing a system database. ` +
|
|
537
|
+
`Either ${takeANewName}, or, if '${current}' was renamed to '${owner}', ` +
|
|
538
|
+
`re-own its rows first with dbos rename-application`);
|
|
539
|
+
}
|
|
451
540
|
async init() {
|
|
452
541
|
await ensureSystemDatabase(this.systemDatabaseUrl, this.logger, this.customPool ? this.pool : undefined, this.schemaName, this.shouldUseDBNotifications);
|
|
453
542
|
if (this.shouldUseDBNotifications) {
|
|
@@ -658,6 +747,7 @@ class SystemDatabase {
|
|
|
658
747
|
'delay_until_epoch_ms',
|
|
659
748
|
'attributes',
|
|
660
749
|
'schedule_name',
|
|
750
|
+
'application_name',
|
|
661
751
|
];
|
|
662
752
|
const client = await this.pool.connect();
|
|
663
753
|
try {
|
|
@@ -675,7 +765,7 @@ class SystemDatabase {
|
|
|
675
765
|
tuples.push(`(${columns.map(() => `$${paramIdx++}`).join(', ')})`);
|
|
676
766
|
params.push(status.workflowUUID, status.status, status.workflowName,
|
|
677
767
|
// For cross-language compatibility, these MUST be NULL in the database when not set
|
|
678
|
-
status.workflowClassName === '' ? null : status.workflowClassName, status.workflowConfigName === '' ? null : status.workflowConfigName, status.queueName ?? null, status.authenticatedUser, status.assumedRole, JSON.stringify(status.authenticatedRoles), JSON.stringify(status.request), status.executorId, status.applicationVersion ?? null, status.applicationID, createdAt, 0, createdAt, status.timeoutMS ?? null, status.deadlineEpochMS ?? null, status.input, null, status.priority, status.queuePartitionKey ?? null, status.parentWorkflowID ?? null, status.serialization, null, status.delayUntilEpochMS ?? null, status.attributes ? JSON.stringify(status.attributes) : null, status.scheduleName ?? null);
|
|
768
|
+
status.workflowClassName === '' ? null : status.workflowClassName, status.workflowConfigName === '' ? null : status.workflowConfigName, status.queueName ?? null, status.authenticatedUser, status.assumedRole, JSON.stringify(status.authenticatedRoles), JSON.stringify(status.request), status.executorId, status.applicationVersion ?? null, status.applicationID, createdAt, 0, createdAt, status.timeoutMS ?? null, status.deadlineEpochMS ?? null, status.input, null, status.priority, status.queuePartitionKey ?? null, status.parentWorkflowID ?? null, status.serialization, null, status.delayUntilEpochMS ?? null, status.attributes ? JSON.stringify(status.attributes) : null, status.scheduleName ?? null, status.applicationName ?? null);
|
|
679
769
|
}
|
|
680
770
|
const { rows } = await client.query(`INSERT INTO "${this.schemaName}".workflow_status (${columns.join(', ')})
|
|
681
771
|
VALUES ${tuples.join(', ')}
|
|
@@ -734,15 +824,28 @@ class SystemDatabase {
|
|
|
734
824
|
return rowCount > 0;
|
|
735
825
|
}
|
|
736
826
|
async getPendingWorkflows(executorID, appVersion) {
|
|
827
|
+
const params = [workflow_1.StatusString.PENDING, executorID, appVersion];
|
|
828
|
+
// executor_id defaults to "local", so it collides across applications.
|
|
829
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
737
830
|
const getWorkflows = await this.pool.query(`SELECT workflow_uuid
|
|
738
831
|
FROM "${this.schemaName}".workflow_status
|
|
739
|
-
WHERE status=$1 AND executor_id=$2 AND application_version=$3
|
|
832
|
+
WHERE status=$1 AND executor_id=$2 AND application_version=$3 AND ${scope}`, params);
|
|
740
833
|
return getWorkflows.rows.map((i) => ({
|
|
741
834
|
workflowUUID: i.workflow_uuid,
|
|
742
835
|
}));
|
|
743
836
|
}
|
|
744
837
|
// Recovery re-enqueues rather than executing directly so the queue's atomic dequeue admits exactly one runner, and the executor ID predicate rejects sweeps for rows a live executor has already claimed.
|
|
745
838
|
async reenqueueWorkflowsForRecovery(executorID, appVersion, recoveryQueueName) {
|
|
839
|
+
const params = [
|
|
840
|
+
workflow_1.StatusString.ENQUEUED,
|
|
841
|
+
Date.now(),
|
|
842
|
+
recoveryQueueName,
|
|
843
|
+
workflow_1.StatusString.PENDING,
|
|
844
|
+
executorID,
|
|
845
|
+
appVersion,
|
|
846
|
+
];
|
|
847
|
+
// executor_id defaults to "local", so it collides across applications.
|
|
848
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
746
849
|
const result = await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
747
850
|
SET started_at_epoch_ms = NULL,
|
|
748
851
|
status = $1,
|
|
@@ -751,7 +854,8 @@ class SystemDatabase {
|
|
|
751
854
|
WHERE status = $4
|
|
752
855
|
AND executor_id = $5
|
|
753
856
|
AND application_version = $6
|
|
754
|
-
|
|
857
|
+
AND ${scope}
|
|
858
|
+
RETURNING workflow_uuid`, params);
|
|
755
859
|
return result.rows.map((row) => row.workflow_uuid);
|
|
756
860
|
}
|
|
757
861
|
async getWorkflowStatus(workflowID, callerID, callerFN) {
|
|
@@ -877,9 +981,9 @@ class SystemDatabase {
|
|
|
877
981
|
// Insert a patchmarker
|
|
878
982
|
const dn = Date.now();
|
|
879
983
|
await this.pool.query(`INSERT INTO ${this.schemaName}.operation_outputs
|
|
880
|
-
(workflow_uuid, function_id, output, error, function_name, child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms)
|
|
881
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
882
|
-
ON CONFLICT DO NOTHING;`, [workflowID, functionID, null, null, patchName, null, dn, dn]);
|
|
984
|
+
(workflow_uuid, function_id, output, error, function_name, child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms, application_name)
|
|
985
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
986
|
+
ON CONFLICT DO NOTHING;`, [workflowID, functionID, null, null, patchName, null, dn, dn, this.appName ?? null]);
|
|
883
987
|
return { isPatched: true, hasEntry: true };
|
|
884
988
|
}
|
|
885
989
|
// ==================== Workflow Management ====================
|
|
@@ -937,8 +1041,9 @@ class SystemDatabase {
|
|
|
937
1041
|
* Extend an existing debounced DELAYED workflow's delay and update its inputs, atomically.
|
|
938
1042
|
* The new delay is capped at the workflow's debounce_deadline_epoch_ms, if one is set.
|
|
939
1043
|
* Matching on workflow name and class ensures a debounce-key collision between different
|
|
940
|
-
* workflows never overwrites another workflow's inputs.
|
|
941
|
-
*
|
|
1044
|
+
* workflows never overwrites another workflow's inputs. The bounce acts for `params.applicationName`:
|
|
1045
|
+
* it extends only that application's holders plus unclaimed ones, claiming those. If nothing matched,
|
|
1046
|
+
* returns the current holder (or that the key is unheld) so the caller can start fresh or surface a conflict.
|
|
942
1047
|
* Runs on `client` if given, joining its transaction (e.g. a transactional step's);
|
|
943
1048
|
* otherwise in its own retried transaction.
|
|
944
1049
|
*/
|
|
@@ -966,17 +1071,7 @@ class SystemDatabase {
|
|
|
966
1071
|
}
|
|
967
1072
|
async #debounceDelayedWorkflowInternal(client, params) {
|
|
968
1073
|
const classNameOrNull = params.workflowClassName === '' ? null : params.workflowClassName;
|
|
969
|
-
const
|
|
970
|
-
SET delay_until_epoch_ms = CASE
|
|
971
|
-
WHEN debounce_deadline_epoch_ms IS NOT NULL AND debounce_deadline_epoch_ms < $1
|
|
972
|
-
THEN debounce_deadline_epoch_ms
|
|
973
|
-
ELSE $1
|
|
974
|
-
END,
|
|
975
|
-
inputs = $2, serialization = $3, updated_at = $4
|
|
976
|
-
WHERE name = $5 AND class_name IS NOT DISTINCT FROM $6
|
|
977
|
-
AND queue_name = $7 AND deduplication_id = $8
|
|
978
|
-
AND status = $9 AND is_debounced = TRUE
|
|
979
|
-
RETURNING workflow_uuid`, [
|
|
1074
|
+
const updateParams = [
|
|
980
1075
|
params.delayUntilEpochMS,
|
|
981
1076
|
params.input,
|
|
982
1077
|
params.serialization,
|
|
@@ -986,7 +1081,24 @@ class SystemDatabase {
|
|
|
986
1081
|
params.queueName,
|
|
987
1082
|
params.deduplicationID,
|
|
988
1083
|
workflow_1.StatusString.DELAYED,
|
|
989
|
-
|
|
1084
|
+
params.applicationName ?? null,
|
|
1085
|
+
];
|
|
1086
|
+
// Never extend a workflow the target application doesn't own; falls through to the holder below.
|
|
1087
|
+
const ownScope = this.#appNameFilter('application_name', params.applicationName, updateParams);
|
|
1088
|
+
const updated = await client.query(`UPDATE "${this.schemaName}".workflow_status
|
|
1089
|
+
SET delay_until_epoch_ms = CASE
|
|
1090
|
+
WHEN debounce_deadline_epoch_ms IS NOT NULL AND debounce_deadline_epoch_ms < $1
|
|
1091
|
+
THEN debounce_deadline_epoch_ms
|
|
1092
|
+
ELSE $1
|
|
1093
|
+
END,
|
|
1094
|
+
inputs = $2, serialization = $3, updated_at = $4,
|
|
1095
|
+
-- Claim it for the target, as its dequeue would: left unclaimed, every peer coalesces onto the one workflow and the last inputs win.
|
|
1096
|
+
application_name = COALESCE(application_name, $10)
|
|
1097
|
+
WHERE name = $5 AND class_name IS NOT DISTINCT FROM $6
|
|
1098
|
+
AND queue_name = $7 AND deduplication_id = $8
|
|
1099
|
+
AND status = $9 AND is_debounced = TRUE
|
|
1100
|
+
AND ${ownScope}
|
|
1101
|
+
RETURNING workflow_uuid`, updateParams);
|
|
990
1102
|
if (updated.rows.length > 0) {
|
|
991
1103
|
return {
|
|
992
1104
|
bouncedWorkflowID: updated.rows[0].workflow_uuid,
|
|
@@ -994,10 +1106,11 @@ class SystemDatabase {
|
|
|
994
1106
|
holderIsDebounced: false,
|
|
995
1107
|
holderWorkflowName: null,
|
|
996
1108
|
holderWorkflowClassName: null,
|
|
1109
|
+
holderApplicationName: null,
|
|
997
1110
|
};
|
|
998
1111
|
}
|
|
999
|
-
//
|
|
1000
|
-
const holder = await client.query(`SELECT workflow_uuid, is_debounced, name, class_name
|
|
1112
|
+
// Unscoped, so a holder that blocked the update above is reportable.
|
|
1113
|
+
const holder = await client.query(`SELECT workflow_uuid, is_debounced, name, class_name, application_name
|
|
1001
1114
|
FROM "${this.schemaName}".workflow_status
|
|
1002
1115
|
WHERE queue_name = $1 AND deduplication_id = $2`, [params.queueName, params.deduplicationID]);
|
|
1003
1116
|
if (holder.rows.length === 0) {
|
|
@@ -1007,6 +1120,7 @@ class SystemDatabase {
|
|
|
1007
1120
|
holderIsDebounced: false,
|
|
1008
1121
|
holderWorkflowName: null,
|
|
1009
1122
|
holderWorkflowClassName: null,
|
|
1123
|
+
holderApplicationName: null,
|
|
1010
1124
|
};
|
|
1011
1125
|
}
|
|
1012
1126
|
return {
|
|
@@ -1015,6 +1129,7 @@ class SystemDatabase {
|
|
|
1015
1129
|
holderIsDebounced: holder.rows[0].is_debounced ?? false,
|
|
1016
1130
|
holderWorkflowName: holder.rows[0].name,
|
|
1017
1131
|
holderWorkflowClassName: holder.rows[0].class_name ?? null,
|
|
1132
|
+
holderApplicationName: holder.rows[0].application_name ?? null,
|
|
1018
1133
|
};
|
|
1019
1134
|
}
|
|
1020
1135
|
// Get the immediate (one-level) child workflow IDs for a set of workflows.
|
|
@@ -1126,7 +1241,7 @@ class SystemDatabase {
|
|
|
1126
1241
|
// Fetch the status of all original workflows inside the transaction.
|
|
1127
1242
|
const { rows: statusRows } = await client.query(`SELECT workflow_uuid, name, class_name, config_name, application_id,
|
|
1128
1243
|
authenticated_user, authenticated_roles, assumed_role, inputs, serialization,
|
|
1129
|
-
request, application_version, attributes
|
|
1244
|
+
request, application_version, attributes, application_name
|
|
1130
1245
|
FROM "${this.schemaName}".workflow_status
|
|
1131
1246
|
WHERE workflow_uuid = ANY($1)`, [originalWorkflowIDs]);
|
|
1132
1247
|
const statusByID = new Map(statusRows.map((r) => [r.workflow_uuid, r]));
|
|
@@ -1155,10 +1270,16 @@ class SystemDatabase {
|
|
|
1155
1270
|
'forked_from',
|
|
1156
1271
|
'serialization',
|
|
1157
1272
|
'attributes',
|
|
1273
|
+
'application_name',
|
|
1158
1274
|
];
|
|
1159
1275
|
if (options.timeoutMS !== undefined) {
|
|
1160
1276
|
insertCols.push('workflow_timeout_ms');
|
|
1161
1277
|
}
|
|
1278
|
+
// One owner per fork, shared by its status row and its copied steps: the source's, or this application claiming an unclaimed one.
|
|
1279
|
+
const forkOwners = new Map(originalWorkflowIDs.map((origID, i) => [
|
|
1280
|
+
forkedWorkflowIDs[i],
|
|
1281
|
+
statusByID.get(origID).application_name ?? this.appName ?? null,
|
|
1282
|
+
]));
|
|
1162
1283
|
const valuesPlaceholders = [];
|
|
1163
1284
|
const params = [];
|
|
1164
1285
|
let paramIdx = 1;
|
|
@@ -1168,7 +1289,7 @@ class SystemDatabase {
|
|
|
1168
1289
|
const ws = statusByID.get(origID);
|
|
1169
1290
|
const placeholders = insertCols.map(() => `$${paramIdx++}`).join(', ');
|
|
1170
1291
|
valuesPlaceholders.push(`(${placeholders})`);
|
|
1171
|
-
params.push(forkID, workflow_1.StatusString.ENQUEUED, ws.name, ws.class_name ?? null, ws.config_name ?? null, queueName, ws.authenticated_user, ws.assumed_role, ws.authenticated_roles, ws.request, options.applicationVersion ?? ws.application_version ?? null, ws.application_id, ws.inputs, options.queuePartitionKey ?? null, origID, ws.serialization, ws.attributes ? JSON.stringify(ws.attributes) : null);
|
|
1292
|
+
params.push(forkID, workflow_1.StatusString.ENQUEUED, ws.name, ws.class_name ?? null, ws.config_name ?? null, queueName, ws.authenticated_user, ws.assumed_role, ws.authenticated_roles, ws.request, options.applicationVersion ?? ws.application_version ?? null, ws.application_id, ws.inputs, options.queuePartitionKey ?? null, origID, ws.serialization, ws.attributes ? JSON.stringify(ws.attributes) : null, forkOwners.get(forkID) ?? null);
|
|
1172
1293
|
if (options.timeoutMS !== undefined) {
|
|
1173
1294
|
params.push(options.timeoutMS);
|
|
1174
1295
|
}
|
|
@@ -1187,11 +1308,12 @@ class SystemDatabase {
|
|
|
1187
1308
|
const mappingParams = [];
|
|
1188
1309
|
let mIdx = 1;
|
|
1189
1310
|
for (const m of forkMappings) {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1311
|
+
// Cast: an unclaimed fork makes owner a bare NULL the VALUES list cannot type.
|
|
1312
|
+
mappingValues.push(`($${mIdx}::text, $${mIdx + 1}::text, $${mIdx + 2}::int, $${mIdx + 3}::text)`);
|
|
1313
|
+
mappingParams.push(m.origID, m.forkID, m.startStep, forkOwners.get(m.forkID) ?? null);
|
|
1314
|
+
mIdx += 4;
|
|
1193
1315
|
}
|
|
1194
|
-
const mappingCTE = `WITH mapping(orig_id, fork_id, start_step) AS (VALUES ${mappingValues.join(', ')})`;
|
|
1316
|
+
const mappingCTE = `WITH mapping(orig_id, fork_id, start_step, owner) AS (VALUES ${mappingValues.join(', ')})`;
|
|
1195
1317
|
// Build the child_workflow_id expression, applying replacements if provided.
|
|
1196
1318
|
let childWfExpr = 'oo.child_workflow_id';
|
|
1197
1319
|
const ooParams = [...mappingParams];
|
|
@@ -1207,8 +1329,8 @@ class SystemDatabase {
|
|
|
1207
1329
|
// Copy operation outputs
|
|
1208
1330
|
await client.query(`${mappingCTE}
|
|
1209
1331
|
INSERT INTO "${this.schemaName}".operation_outputs
|
|
1210
|
-
(workflow_uuid, function_id, output, error, serialization, function_name, child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms)
|
|
1211
|
-
SELECT m.fork_id, oo.function_id, oo.output, oo.error, oo.serialization, oo.function_name, ${childWfExpr}, oo.started_at_epoch_ms, oo.completed_at_epoch_ms
|
|
1332
|
+
(workflow_uuid, function_id, output, error, serialization, function_name, child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms, application_name)
|
|
1333
|
+
SELECT m.fork_id, oo.function_id, oo.output, oo.error, oo.serialization, oo.function_name, ${childWfExpr}, oo.started_at_epoch_ms, oo.completed_at_epoch_ms, m.owner
|
|
1212
1334
|
FROM mapping m
|
|
1213
1335
|
JOIN "${this.schemaName}".operation_outputs oo
|
|
1214
1336
|
ON oo.workflow_uuid = m.orig_id AND oo.function_id < m.start_step`, ooParams);
|
|
@@ -1276,7 +1398,7 @@ class SystemDatabase {
|
|
|
1276
1398
|
deduplication_id, inputs, priority, queue_partition_key, forked_from,
|
|
1277
1399
|
parent_workflow_id, serialization, delay_until_epoch_ms,
|
|
1278
1400
|
was_forked_from, rate_limited, completed_at, attributes, schedule_name,
|
|
1279
|
-
debounce_deadline_epoch_ms, is_debounced
|
|
1401
|
+
debounce_deadline_epoch_ms, is_debounced, application_name
|
|
1280
1402
|
FROM "${this.schemaName}".workflow_status
|
|
1281
1403
|
WHERE workflow_uuid = $1`, [wfID]);
|
|
1282
1404
|
if (statusResult.rows.length === 0) {
|
|
@@ -1287,7 +1409,7 @@ class SystemDatabase {
|
|
|
1287
1409
|
const outputsResult = await client.query(`SELECT
|
|
1288
1410
|
workflow_uuid, function_id, function_name, output, error,
|
|
1289
1411
|
child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms,
|
|
1290
|
-
serialization
|
|
1412
|
+
serialization, application_name
|
|
1291
1413
|
FROM "${this.schemaName}".operation_outputs
|
|
1292
1414
|
WHERE workflow_uuid = $1`, [wfID]);
|
|
1293
1415
|
// Export workflow_events
|
|
@@ -1332,8 +1454,8 @@ class SystemDatabase {
|
|
|
1332
1454
|
deduplication_id, inputs, priority, queue_partition_key, forked_from,
|
|
1333
1455
|
parent_workflow_id, serialization, delay_until_epoch_ms,
|
|
1334
1456
|
was_forked_from, rate_limited, completed_at, attributes, schedule_name,
|
|
1335
|
-
debounce_deadline_epoch_ms, is_debounced
|
|
1336
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36)`, [
|
|
1457
|
+
debounce_deadline_epoch_ms, is_debounced, application_name
|
|
1458
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37)`, [
|
|
1337
1459
|
status.workflow_uuid,
|
|
1338
1460
|
status.status,
|
|
1339
1461
|
status.name,
|
|
@@ -1372,14 +1494,15 @@ class SystemDatabase {
|
|
|
1372
1494
|
status.schedule_name ?? null,
|
|
1373
1495
|
status.debounce_deadline_epoch_ms ?? null,
|
|
1374
1496
|
status.is_debounced ?? false,
|
|
1497
|
+
status.application_name ?? null,
|
|
1375
1498
|
]);
|
|
1376
1499
|
// Import operation_outputs
|
|
1377
1500
|
for (const output of workflow.operation_outputs) {
|
|
1378
1501
|
await client.query(`INSERT INTO "${this.schemaName}".operation_outputs (
|
|
1379
1502
|
workflow_uuid, function_id, function_name, output, error,
|
|
1380
1503
|
child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms,
|
|
1381
|
-
serialization
|
|
1382
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [
|
|
1504
|
+
serialization, application_name
|
|
1505
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, [
|
|
1383
1506
|
output.workflow_uuid,
|
|
1384
1507
|
output.function_id,
|
|
1385
1508
|
output.function_name,
|
|
@@ -1389,6 +1512,7 @@ class SystemDatabase {
|
|
|
1389
1512
|
output.started_at_epoch_ms,
|
|
1390
1513
|
output.completed_at_epoch_ms,
|
|
1391
1514
|
output.serialization,
|
|
1515
|
+
output.application_name ?? null,
|
|
1392
1516
|
]);
|
|
1393
1517
|
}
|
|
1394
1518
|
// Import workflow_events
|
|
@@ -2092,10 +2216,13 @@ class SystemDatabase {
|
|
|
2092
2216
|
// Transition workflows from DELAYED to ENQUEUED when their delay has expired.
|
|
2093
2217
|
// For debounced workflows, clear the deduplication ID in the same atomic update: it is a
|
|
2094
2218
|
// debounce key held only while DELAYED, so a later same-key debounce starts a fresh workflow.
|
|
2219
|
+
const params = [workflow_1.StatusString.ENQUEUED, Date.now(), workflow_1.StatusString.DELAYED];
|
|
2220
|
+
// Only what this application would dequeue: a peer's debounce key is not ours to clear.
|
|
2221
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
2095
2222
|
await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
2096
2223
|
SET status = $1, updated_at = $2,
|
|
2097
2224
|
deduplication_id = CASE WHEN is_debounced THEN NULL ELSE deduplication_id END
|
|
2098
|
-
WHERE status = $3 AND delay_until_epoch_ms <= $2
|
|
2225
|
+
WHERE status = $3 AND delay_until_epoch_ms <= $2 AND ${scope}`, params);
|
|
2099
2226
|
}
|
|
2100
2227
|
async getDeduplicatedWorkflow(queueName, deduplicationID) {
|
|
2101
2228
|
const { rows } = await this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status
|
|
@@ -2107,18 +2234,21 @@ class SystemDatabase {
|
|
|
2107
2234
|
}
|
|
2108
2235
|
async getQueuePartitions(queueName) {
|
|
2109
2236
|
// Recursive-CTE loose index scan: SELECT DISTINCT would scan every ENQUEUED row, whereas each iteration here is one seek on idx_workflow_status_partition_dequeue_v2, so cost scales with the number of partitions rather than the backlog depth.
|
|
2237
|
+
const params = [queueName, workflow_1.StatusString.ENQUEUED];
|
|
2238
|
+
// Only partitions this application can actually dequeue from.
|
|
2239
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
2110
2240
|
const { rows } = await this.pool.query(`WITH RECURSIVE partitions AS (
|
|
2111
2241
|
(SELECT MIN(queue_partition_key) AS pk
|
|
2112
2242
|
FROM "${this.schemaName}".workflow_status
|
|
2113
|
-
WHERE queue_name = $1 AND status = $2 AND queue_partition_key IS NOT NULL)
|
|
2243
|
+
WHERE queue_name = $1 AND status = $2 AND queue_partition_key IS NOT NULL AND ${scope})
|
|
2114
2244
|
UNION ALL
|
|
2115
2245
|
(SELECT (SELECT MIN(queue_partition_key)
|
|
2116
2246
|
FROM "${this.schemaName}".workflow_status
|
|
2117
|
-
WHERE queue_name = $1 AND status = $2 AND queue_partition_key > partitions.pk)
|
|
2247
|
+
WHERE queue_name = $1 AND status = $2 AND queue_partition_key > partitions.pk AND ${scope})
|
|
2118
2248
|
FROM partitions
|
|
2119
2249
|
WHERE partitions.pk IS NOT NULL)
|
|
2120
2250
|
)
|
|
2121
|
-
SELECT pk FROM partitions WHERE pk IS NOT NULL`,
|
|
2251
|
+
SELECT pk FROM partitions WHERE pk IS NOT NULL`, params);
|
|
2122
2252
|
return rows.map((row) => row.pk);
|
|
2123
2253
|
}
|
|
2124
2254
|
async findAndMarkStartableWorkflows(queue, executorID, appVersion, queuePartitionKey) {
|
|
@@ -2152,11 +2282,14 @@ class SystemDatabase {
|
|
|
2152
2282
|
startTimeMs - limiterPeriodMS,
|
|
2153
2283
|
...partitionParams,
|
|
2154
2284
|
];
|
|
2285
|
+
// Count only what this application would dequeue, matching the select below.
|
|
2286
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
2155
2287
|
const countResult = await client.query(`SELECT COUNT(*) FROM "${this.schemaName}".workflow_status
|
|
2156
2288
|
WHERE queue_name = $1
|
|
2157
2289
|
AND rate_limited = TRUE
|
|
2158
2290
|
AND status NOT IN ($2, $3)
|
|
2159
2291
|
AND started_at_epoch_ms > $4
|
|
2292
|
+
AND ${scope}
|
|
2160
2293
|
${partitionFilter.replace('$PARTITION', '$5')}`, params);
|
|
2161
2294
|
numRecentQueries = Number(countResult.rows[0].count);
|
|
2162
2295
|
if (numRecentQueries >= queue.rateLimit.limitPerPeriod) {
|
|
@@ -2175,9 +2308,10 @@ class SystemDatabase {
|
|
|
2175
2308
|
if (queue.concurrency !== undefined) {
|
|
2176
2309
|
// Global concurrency still requires a DB query since other workers may be running workflows too.
|
|
2177
2310
|
const params = [queue.name, workflow_1.StatusString.PENDING, ...partitionParams];
|
|
2311
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
2178
2312
|
const runningTasksResult = await client.query(`SELECT COUNT(*) as task_count
|
|
2179
2313
|
FROM "${this.schemaName}".workflow_status
|
|
2180
|
-
WHERE queue_name = $1 AND status = $2
|
|
2314
|
+
WHERE queue_name = $1 AND status = $2 AND ${scope}
|
|
2181
2315
|
${partitionFilter.replace('$PARTITION', '$3')}`, params);
|
|
2182
2316
|
const totalRunningTasks = Number(runningTasksResult.rows[0]?.task_count ?? 0);
|
|
2183
2317
|
if (totalRunningTasks > queue.concurrency) {
|
|
@@ -2192,10 +2326,7 @@ class SystemDatabase {
|
|
|
2192
2326
|
return claimedIDs;
|
|
2193
2327
|
}
|
|
2194
2328
|
// Retrieve the first max_tasks workflows in the queue.
|
|
2195
|
-
const
|
|
2196
|
-
FROM "${this.schemaName}".application_versions
|
|
2197
|
-
ORDER BY version_timestamp DESC LIMIT 1`);
|
|
2198
|
-
const latestVersion = latestVersionResult.rows[0]?.version_name;
|
|
2329
|
+
const latestVersion = await this.#latestApplicationVersionName(client);
|
|
2199
2330
|
const isLatestVersion = latestVersion === undefined || latestVersion === appVersion;
|
|
2200
2331
|
const versionClause = isLatestVersion
|
|
2201
2332
|
? '(application_version = $3 OR application_version IS NULL)'
|
|
@@ -2203,12 +2334,14 @@ class SystemDatabase {
|
|
|
2203
2334
|
const lockMode = queue.concurrency ? 'FOR UPDATE NOWAIT' : 'FOR UPDATE SKIP LOCKED';
|
|
2204
2335
|
const limitClause = maxTasks !== Infinity ? `LIMIT ${maxTasks}` : '';
|
|
2205
2336
|
const selectParams = [workflow_1.StatusString.ENQUEUED, queue.name, appVersion, ...partitionParams];
|
|
2337
|
+
const selectScope = this.#appNameFilter('application_name', this.appName, selectParams);
|
|
2206
2338
|
const selectQuery = `
|
|
2207
2339
|
SELECT workflow_uuid
|
|
2208
2340
|
FROM "${this.schemaName}".workflow_status
|
|
2209
2341
|
WHERE status = $1
|
|
2210
2342
|
AND queue_name = $2
|
|
2211
2343
|
AND ${versionClause}
|
|
2344
|
+
AND ${selectScope}
|
|
2212
2345
|
${partitionFilter.replace('$PARTITION', '$4')}
|
|
2213
2346
|
ORDER BY priority ASC, created_at ASC
|
|
2214
2347
|
${limitClause}
|
|
@@ -2229,26 +2362,32 @@ class SystemDatabase {
|
|
|
2229
2362
|
// Start the functions by marking them as pending and updating their executor IDs.
|
|
2230
2363
|
// Only claim the workflow if the UPDATE actually transitioned an ENQUEUED row —
|
|
2231
2364
|
// otherwise another worker won the race and we must not re-dispatch it.
|
|
2365
|
+
const updateParams = [
|
|
2366
|
+
workflow_1.StatusString.PENDING,
|
|
2367
|
+
executorID,
|
|
2368
|
+
appVersion,
|
|
2369
|
+
startTimeMs,
|
|
2370
|
+
queue.rateLimit !== undefined,
|
|
2371
|
+
id,
|
|
2372
|
+
workflow_1.StatusString.ENQUEUED,
|
|
2373
|
+
// Claim an unclaimed row for this application; a nameless dequeuer leaves ownership untouched.
|
|
2374
|
+
this.appName ?? null,
|
|
2375
|
+
];
|
|
2376
|
+
// Re-check ownership alongside status, as the partitioned claim guard does.
|
|
2377
|
+
const claimScope = this.#appNameFilter('application_name', this.appName, updateParams);
|
|
2232
2378
|
const updateRes = await client.query(`UPDATE "${this.schemaName}".workflow_status
|
|
2233
2379
|
SET status = $1,
|
|
2234
2380
|
executor_id = $2,
|
|
2235
2381
|
application_version = $3,
|
|
2236
2382
|
started_at_epoch_ms = $4,
|
|
2237
2383
|
rate_limited = $5,
|
|
2384
|
+
application_name = COALESCE(application_name, $8),
|
|
2238
2385
|
workflow_deadline_epoch_ms = CASE
|
|
2239
2386
|
WHEN workflow_timeout_ms IS NOT NULL AND workflow_deadline_epoch_ms IS NULL
|
|
2240
2387
|
THEN (EXTRACT(epoch FROM now()) * 1000)::bigint + workflow_timeout_ms
|
|
2241
2388
|
ELSE workflow_deadline_epoch_ms
|
|
2242
2389
|
END
|
|
2243
|
-
WHERE workflow_uuid = $6 AND status = $7`,
|
|
2244
|
-
workflow_1.StatusString.PENDING,
|
|
2245
|
-
executorID,
|
|
2246
|
-
appVersion,
|
|
2247
|
-
startTimeMs,
|
|
2248
|
-
queue.rateLimit !== undefined,
|
|
2249
|
-
id,
|
|
2250
|
-
workflow_1.StatusString.ENQUEUED,
|
|
2251
|
-
]);
|
|
2390
|
+
WHERE workflow_uuid = $6 AND status = $7 AND ${claimScope}`, updateParams);
|
|
2252
2391
|
if ((updateRes.rowCount ?? 0) > 0) {
|
|
2253
2392
|
claimedIDs.push(id);
|
|
2254
2393
|
}
|
|
@@ -2277,23 +2416,28 @@ class SystemDatabase {
|
|
|
2277
2416
|
const client = await this.pool.connect();
|
|
2278
2417
|
try {
|
|
2279
2418
|
await client.query('BEGIN');
|
|
2280
|
-
const
|
|
2281
|
-
FROM "${this.schemaName}".application_versions
|
|
2282
|
-
ORDER BY version_timestamp DESC LIMIT 1`);
|
|
2283
|
-
const latestVersion = latestVersionResult.rows[0]?.version_name;
|
|
2419
|
+
const latestVersion = await this.#latestApplicationVersionName(client);
|
|
2284
2420
|
const isLatestVersion = latestVersion === undefined || latestVersion === appVersion;
|
|
2285
2421
|
const versionClause = (n) => isLatestVersion
|
|
2286
2422
|
? `(application_version = $${n} OR application_version IS NULL)`
|
|
2287
2423
|
: `application_version = $${n}`;
|
|
2424
|
+
const candidateParams = [
|
|
2425
|
+
queue.name,
|
|
2426
|
+
workflow_1.StatusString.ENQUEUED,
|
|
2427
|
+
appVersion,
|
|
2428
|
+
workflow_1.StatusString.PENDING,
|
|
2429
|
+
this.partitionedDequeueSweepCap,
|
|
2430
|
+
];
|
|
2431
|
+
const candidateScope = this.#appNameFilter('application_name', this.appName, candidateParams);
|
|
2288
2432
|
// Walk distinct partition keys with a recursive-CTE loose index scan (one seek per key, mirroring getQueuePartitions) so sweep cost scales with partition count, not backlog depth.
|
|
2289
2433
|
const candidateResult = await client.query(`WITH RECURSIVE partitions AS (
|
|
2290
2434
|
(SELECT MIN(queue_partition_key) AS pk
|
|
2291
2435
|
FROM "${this.schemaName}".workflow_status
|
|
2292
|
-
WHERE queue_name = $1 AND status = $2 AND queue_partition_key IS NOT NULL)
|
|
2436
|
+
WHERE queue_name = $1 AND status = $2 AND queue_partition_key IS NOT NULL AND ${candidateScope})
|
|
2293
2437
|
UNION ALL
|
|
2294
2438
|
(SELECT (SELECT MIN(queue_partition_key)
|
|
2295
2439
|
FROM "${this.schemaName}".workflow_status
|
|
2296
|
-
WHERE queue_name = $1 AND status = $2 AND queue_partition_key > partitions.pk)
|
|
2440
|
+
WHERE queue_name = $1 AND status = $2 AND queue_partition_key > partitions.pk AND ${candidateScope})
|
|
2297
2441
|
FROM partitions
|
|
2298
2442
|
WHERE partitions.pk IS NOT NULL)
|
|
2299
2443
|
)
|
|
@@ -2306,12 +2450,13 @@ class SystemDatabase {
|
|
|
2306
2450
|
WHERE queue_name = $1 AND status = $2
|
|
2307
2451
|
AND queue_partition_key = partitions.pk
|
|
2308
2452
|
AND ${versionClause(3)}
|
|
2453
|
+
AND ${candidateScope}
|
|
2309
2454
|
-- workflow_uuid totalizes the head order (same head for every worker under created_at ties) and the index's trailing workflow_uuid keeps this a pure top-1 probe.
|
|
2310
2455
|
ORDER BY priority ASC, created_at ASC, workflow_uuid ASC
|
|
2311
2456
|
LIMIT 1
|
|
2312
2457
|
) head ON TRUE
|
|
2313
2458
|
WHERE partitions.pk IS NOT NULL
|
|
2314
|
-
--
|
|
2459
|
+
-- Unscoped by design: a mutual-exclusion probe must block on any owner's row.
|
|
2315
2460
|
AND NOT EXISTS (
|
|
2316
2461
|
SELECT 1
|
|
2317
2462
|
FROM "${this.schemaName}".workflow_status
|
|
@@ -2319,7 +2464,7 @@ class SystemDatabase {
|
|
|
2319
2464
|
AND queue_partition_key IS NOT NULL AND queue_partition_key = partitions.pk
|
|
2320
2465
|
)
|
|
2321
2466
|
ORDER BY partitions.pk ASC
|
|
2322
|
-
LIMIT $5`,
|
|
2467
|
+
LIMIT $5`, candidateParams);
|
|
2323
2468
|
const candidateIDs = candidateResult.rows.map((row) => row.workflow_uuid);
|
|
2324
2469
|
if (candidateIDs.length === 0) {
|
|
2325
2470
|
await client.query('COMMIT');
|
|
@@ -2327,16 +2472,19 @@ class SystemDatabase {
|
|
|
2327
2472
|
}
|
|
2328
2473
|
await (0, debugpoint_1.debugTriggerPoint)(debugpoint_1.DEBUG_TRIGGER_PARTITIONED_DEQUEUE_AFTER_CANDIDATES);
|
|
2329
2474
|
// Re-check queue/partition/version alongside status so a row resumeWorkflows moved to another queue mid-sweep is dropped, not hijacked.
|
|
2330
|
-
const claimGuard = (ids, status, name, version) => `workflow_uuid = ANY($${ids}::text[])
|
|
2475
|
+
const claimGuard = (ids, status, name, version, scope) => `workflow_uuid = ANY($${ids}::text[])
|
|
2331
2476
|
AND status = $${status}
|
|
2332
2477
|
AND queue_name = $${name}
|
|
2333
2478
|
AND queue_partition_key IS NOT NULL
|
|
2334
|
-
AND ${versionClause(version)}
|
|
2479
|
+
AND ${versionClause(version)}
|
|
2480
|
+
AND ${scope}`;
|
|
2481
|
+
const lockedParams = [candidateIDs, workflow_1.StatusString.ENQUEUED, queue.name, appVersion];
|
|
2482
|
+
const lockedScope = this.#appNameFilter('application_name', this.appName, lockedParams);
|
|
2335
2483
|
// Lock the fixed candidate set — never a LIMIT query, whose SKIP LOCKED could slide past a locked head and admit out of order.
|
|
2336
2484
|
const lockedResult = await client.query(`SELECT workflow_uuid
|
|
2337
2485
|
FROM "${this.schemaName}".workflow_status
|
|
2338
|
-
WHERE ${claimGuard(1, 2, 3, 4)}
|
|
2339
|
-
FOR UPDATE SKIP LOCKED`,
|
|
2486
|
+
WHERE ${claimGuard(1, 2, 3, 4, lockedScope)}
|
|
2487
|
+
FOR UPDATE SKIP LOCKED`, lockedParams);
|
|
2340
2488
|
const lockedIDs = new Set(lockedResult.rows.map((row) => row.workflow_uuid));
|
|
2341
2489
|
// Preserve partition order for submission.
|
|
2342
2490
|
const claimIDs = candidateIDs.filter((id) => lockedIDs.has(id));
|
|
@@ -2344,6 +2492,18 @@ class SystemDatabase {
|
|
|
2344
2492
|
await client.query('COMMIT');
|
|
2345
2493
|
return [];
|
|
2346
2494
|
}
|
|
2495
|
+
const flipParams = [
|
|
2496
|
+
workflow_1.StatusString.PENDING,
|
|
2497
|
+
executorID,
|
|
2498
|
+
claimIDs,
|
|
2499
|
+
workflow_1.StatusString.ENQUEUED,
|
|
2500
|
+
queue.name,
|
|
2501
|
+
appVersion,
|
|
2502
|
+
startTimeMs,
|
|
2503
|
+
// Claim the row, as the unpartitioned dequeue does.
|
|
2504
|
+
this.appName ?? null,
|
|
2505
|
+
];
|
|
2506
|
+
const flipScope = this.#appNameFilter('application_name', this.appName, flipParams);
|
|
2347
2507
|
// Start the workflows by marking them PENDING; RETURNING reports exactly the rows this statement flipped.
|
|
2348
2508
|
const flippedResult = await client.query(`UPDATE "${this.schemaName}".workflow_status
|
|
2349
2509
|
SET status = $1,
|
|
@@ -2351,13 +2511,14 @@ class SystemDatabase {
|
|
|
2351
2511
|
application_version = $6,
|
|
2352
2512
|
started_at_epoch_ms = $7,
|
|
2353
2513
|
rate_limited = FALSE,
|
|
2514
|
+
application_name = COALESCE(application_name, $8),
|
|
2354
2515
|
workflow_deadline_epoch_ms = CASE
|
|
2355
2516
|
WHEN workflow_timeout_ms IS NOT NULL AND workflow_deadline_epoch_ms IS NULL
|
|
2356
2517
|
THEN (EXTRACT(epoch FROM now()) * 1000)::bigint + workflow_timeout_ms
|
|
2357
2518
|
ELSE workflow_deadline_epoch_ms
|
|
2358
2519
|
END
|
|
2359
|
-
WHERE ${claimGuard(3, 4, 5, 6)}
|
|
2360
|
-
RETURNING workflow_uuid`,
|
|
2520
|
+
WHERE ${claimGuard(3, 4, 5, 6, flipScope)}
|
|
2521
|
+
RETURNING workflow_uuid`, flipParams);
|
|
2361
2522
|
await client.query('COMMIT');
|
|
2362
2523
|
const flippedIDs = new Set(flippedResult.rows.map((row) => row.workflow_uuid));
|
|
2363
2524
|
const claimedIDs = claimIDs.filter((id) => flippedIDs.has(id));
|
|
@@ -2408,6 +2569,7 @@ class SystemDatabase {
|
|
|
2408
2569
|
'schedule_name',
|
|
2409
2570
|
'debounce_deadline_epoch_ms',
|
|
2410
2571
|
'is_debounced',
|
|
2572
|
+
'application_name',
|
|
2411
2573
|
];
|
|
2412
2574
|
input.loadInput = input.loadInput ?? true;
|
|
2413
2575
|
input.loadOutput = input.loadOutput ?? true;
|
|
@@ -2452,6 +2614,15 @@ class SystemDatabase {
|
|
|
2452
2614
|
addFilter('name', input.workflowName);
|
|
2453
2615
|
addFilter('queue_name', input.queueName);
|
|
2454
2616
|
addFilter('schedule_name', input.scheduleName);
|
|
2617
|
+
// A workflow ID is a global address, so an ID-keyed read is an identity read: it takes an
|
|
2618
|
+
// explicit filter but is never defaulted to this one. Otherwise unset scopes to this
|
|
2619
|
+
// application, as on every other observability query.
|
|
2620
|
+
// Falsy, not just undefined: a Conductor request carries an omitted list as JSON null.
|
|
2621
|
+
const idKeyed = (input.workflowIDs?.length ?? 0) > 0;
|
|
2622
|
+
whereClauses.push(idKeyed
|
|
2623
|
+
? this.#appNameFilter('application_name', input.applicationName, params)
|
|
2624
|
+
: this.#observabilityFilter('application_name', input.applicationName, params));
|
|
2625
|
+
paramCounter = params.length + 1;
|
|
2455
2626
|
if (input.workflow_id_prefix) {
|
|
2456
2627
|
if (Array.isArray(input.workflow_id_prefix)) {
|
|
2457
2628
|
const likeClauses = input.workflow_id_prefix.map((_, i) => `workflow_uuid LIKE $${paramCounter + i}`);
|
|
@@ -2554,6 +2725,7 @@ class SystemDatabase {
|
|
|
2554
2725
|
['queue_name', input.groupByQueueName ?? false, 'queue_name'],
|
|
2555
2726
|
['executor_id', input.groupByExecutorId ?? false, 'executor_id'],
|
|
2556
2727
|
['application_version', input.groupByApplicationVersion ?? false, 'application_version'],
|
|
2728
|
+
['application_name', input.groupByApplicationName ?? false, 'application_name'],
|
|
2557
2729
|
];
|
|
2558
2730
|
const groupNames = [];
|
|
2559
2731
|
const groupColumns = [];
|
|
@@ -2624,6 +2796,9 @@ class SystemDatabase {
|
|
|
2624
2796
|
addFilter('forked_from', input.forkedFrom);
|
|
2625
2797
|
addFilter('parent_workflow_id', input.parentWorkflowID);
|
|
2626
2798
|
addFilter('schedule_name', input.scheduleName);
|
|
2799
|
+
// Unset scopes to this application, as on every other observability query.
|
|
2800
|
+
whereClauses.push(this.#observabilityFilter('application_name', input.applicationName, params));
|
|
2801
|
+
paramIdx = params.length + 1;
|
|
2627
2802
|
// Only workflows that are actively enqueued.
|
|
2628
2803
|
if (input.queuesOnly) {
|
|
2629
2804
|
whereClauses.push(`queue_name IS NOT NULL`);
|
|
@@ -2786,6 +2961,9 @@ class SystemDatabase {
|
|
|
2786
2961
|
params.push(new Date(input.completedBefore).getTime());
|
|
2787
2962
|
paramIdx++;
|
|
2788
2963
|
}
|
|
2964
|
+
// Unset scopes to this application, as on every other observability query.
|
|
2965
|
+
whereClauses.push(this.#observabilityFilter('application_name', input.applicationName, params));
|
|
2966
|
+
paramIdx = params.length + 1;
|
|
2789
2967
|
const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
|
|
2790
2968
|
const groupByClause = groupColumns.join(', ');
|
|
2791
2969
|
const selectClause = [...groupSelectColumns, ...selectColumns].join(', ');
|
|
@@ -2814,10 +2992,13 @@ class SystemDatabase {
|
|
|
2814
2992
|
async garbageCollect(cutoffEpochTimestampMs, rowsThreshold) {
|
|
2815
2993
|
if (rowsThreshold !== undefined && rowsThreshold !== null) {
|
|
2816
2994
|
// Get the created_at timestamp of the rows_threshold newest row
|
|
2995
|
+
const params = [rowsThreshold - 1];
|
|
2996
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
2817
2997
|
const result = await this.pool.query(`SELECT created_at
|
|
2818
2998
|
FROM "${this.schemaName}".workflow_status
|
|
2999
|
+
WHERE ${scope}
|
|
2819
3000
|
ORDER BY created_at DESC
|
|
2820
|
-
LIMIT 1 OFFSET $1`,
|
|
3001
|
+
LIMIT 1 OFFSET $1`, params);
|
|
2821
3002
|
if (result.rows.length > 0) {
|
|
2822
3003
|
const rowsBasedCutoff = result.rows[0].created_at;
|
|
2823
3004
|
// Use the more restrictive cutoff (higher timestamp = more recent = more deletion)
|
|
@@ -2831,21 +3012,51 @@ class SystemDatabase {
|
|
|
2831
3012
|
if (cutoffEpochTimestampMs === undefined || cutoffEpochTimestampMs === null) {
|
|
2832
3013
|
return;
|
|
2833
3014
|
}
|
|
3015
|
+
const deleteParams = [
|
|
3016
|
+
cutoffEpochTimestampMs,
|
|
3017
|
+
workflow_1.StatusString.PENDING,
|
|
3018
|
+
workflow_1.StatusString.ENQUEUED,
|
|
3019
|
+
workflow_1.StatusString.DELAYED,
|
|
3020
|
+
];
|
|
3021
|
+
// Unclaimed rows included: excluding them would leak pre-upgrade rows forever.
|
|
3022
|
+
const deleteScope = this.#appNameFilter('application_name', this.appName, deleteParams);
|
|
2834
3023
|
// Delete all workflows older than cutoff that are NOT PENDING, ENQUEUED, or DELAYED
|
|
2835
3024
|
await this.pool.query(`DELETE FROM "${this.schemaName}".workflow_status
|
|
2836
3025
|
WHERE created_at < $1
|
|
2837
|
-
AND status NOT IN ($2, $3, $4)
|
|
3026
|
+
AND status NOT IN ($2, $3, $4)
|
|
3027
|
+
AND ${deleteScope}`, deleteParams);
|
|
2838
3028
|
return;
|
|
2839
3029
|
}
|
|
2840
|
-
|
|
3030
|
+
/**
|
|
3031
|
+
* IDs of this application's in-flight workflows created at or before the cutoff.
|
|
3032
|
+
* Claiming-scoped, so an upgrade still times out its own unclaimed workflows.
|
|
3033
|
+
*/
|
|
3034
|
+
async listTimedOutWorkflowIds(cutoffEpochTimestampMs) {
|
|
3035
|
+
const params = [
|
|
3036
|
+
cutoffEpochTimestampMs,
|
|
3037
|
+
workflow_1.StatusString.PENDING,
|
|
3038
|
+
workflow_1.StatusString.ENQUEUED,
|
|
3039
|
+
workflow_1.StatusString.DELAYED,
|
|
3040
|
+
];
|
|
3041
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
3042
|
+
const { rows } = await this.pool.query(`SELECT workflow_uuid
|
|
3043
|
+
FROM "${this.schemaName}".workflow_status
|
|
3044
|
+
WHERE created_at <= $1
|
|
3045
|
+
AND status IN ($2, $3, $4)
|
|
3046
|
+
AND ${scope}`, params);
|
|
3047
|
+
return rows.map((row) => row.workflow_uuid);
|
|
3048
|
+
}
|
|
3049
|
+
async getMetrics(startTime, endTime, applicationName) {
|
|
2841
3050
|
const startEpochMs = new Date(startTime).getTime();
|
|
2842
3051
|
const endEpochMs = new Date(endTime).getTime();
|
|
2843
3052
|
const metrics = [];
|
|
2844
3053
|
// Query workflow metrics
|
|
3054
|
+
const workflowParams = [startEpochMs, endEpochMs];
|
|
3055
|
+
const workflowScope = this.#observabilityFilter('application_name', applicationName, workflowParams);
|
|
2845
3056
|
const workflowResult = await this.pool.query(`SELECT name, COUNT(workflow_uuid) as count
|
|
2846
3057
|
FROM "${this.schemaName}".workflow_status
|
|
2847
|
-
WHERE created_at >= $1 AND created_at < $2
|
|
2848
|
-
GROUP BY name`,
|
|
3058
|
+
WHERE created_at >= $1 AND created_at < $2 AND ${workflowScope}
|
|
3059
|
+
GROUP BY name`, workflowParams);
|
|
2849
3060
|
for (const row of workflowResult.rows) {
|
|
2850
3061
|
metrics.push({
|
|
2851
3062
|
metricType: 'workflow_count',
|
|
@@ -2854,10 +3065,12 @@ class SystemDatabase {
|
|
|
2854
3065
|
});
|
|
2855
3066
|
}
|
|
2856
3067
|
// Query step metrics
|
|
3068
|
+
const stepParams = [startEpochMs, endEpochMs];
|
|
3069
|
+
const stepScope = this.#observabilityFilter('application_name', applicationName, stepParams);
|
|
2857
3070
|
const stepResult = await this.pool.query(`SELECT function_name, COUNT(*) as count
|
|
2858
3071
|
FROM "${this.schemaName}".operation_outputs
|
|
2859
|
-
WHERE completed_at_epoch_ms >= $1 AND completed_at_epoch_ms < $2
|
|
2860
|
-
GROUP BY function_name`,
|
|
3072
|
+
WHERE completed_at_epoch_ms >= $1 AND completed_at_epoch_ms < $2 AND ${stepScope}
|
|
3073
|
+
GROUP BY function_name`, stepParams);
|
|
2861
3074
|
for (const row of stepResult.rows) {
|
|
2862
3075
|
metrics.push({
|
|
2863
3076
|
metricType: 'step_count',
|
|
@@ -2870,10 +3083,11 @@ class SystemDatabase {
|
|
|
2870
3083
|
// ==================== Scheduling ====================
|
|
2871
3084
|
async createSchedule(schedule, client) {
|
|
2872
3085
|
const q = client ?? this.pool;
|
|
3086
|
+
const owner = await this.#resolveRowOwner(q, 'workflow_schedules', 'schedule_name', schedule.scheduleName, schedule.applicationName, 'Schedule');
|
|
2873
3087
|
try {
|
|
2874
3088
|
await q.query(`INSERT INTO "${this.schemaName}".workflow_schedules
|
|
2875
|
-
(schedule_id, schedule_name, workflow_name, workflow_class_name, schedule, status, context, last_fired_at, automatic_backfill, cron_timezone, queue_name)
|
|
2876
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [
|
|
3089
|
+
(schedule_id, schedule_name, workflow_name, workflow_class_name, schedule, status, context, last_fired_at, automatic_backfill, cron_timezone, queue_name, application_name)
|
|
3090
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, [
|
|
2877
3091
|
schedule.scheduleId,
|
|
2878
3092
|
schedule.scheduleName,
|
|
2879
3093
|
schedule.workflowName,
|
|
@@ -2885,6 +3099,7 @@ class SystemDatabase {
|
|
|
2885
3099
|
schedule.automaticBackfill,
|
|
2886
3100
|
schedule.cronTimezone,
|
|
2887
3101
|
schedule.queueName,
|
|
3102
|
+
owner ?? null,
|
|
2888
3103
|
]);
|
|
2889
3104
|
}
|
|
2890
3105
|
catch (e) {
|
|
@@ -2894,6 +3109,10 @@ class SystemDatabase {
|
|
|
2894
3109
|
throw e;
|
|
2895
3110
|
}
|
|
2896
3111
|
}
|
|
3112
|
+
/**
|
|
3113
|
+
* List only schedules owned by these applications, plus unclaimed ones.
|
|
3114
|
+
* By default, only list this application's schedules.
|
|
3115
|
+
*/
|
|
2897
3116
|
async listSchedules(filters, client) {
|
|
2898
3117
|
const q = client ?? this.pool;
|
|
2899
3118
|
const conditions = [];
|
|
@@ -2925,45 +3144,23 @@ class SystemDatabase {
|
|
|
2925
3144
|
});
|
|
2926
3145
|
conditions.push(`(${likeClauses.join(' OR ')})`);
|
|
2927
3146
|
}
|
|
3147
|
+
// Unset scopes to this application, as on every other observability query.
|
|
3148
|
+
conditions.push(this.#observabilityFilter('application_name', filters?.applicationName, params));
|
|
3149
|
+
paramIdx = params.length + 1;
|
|
2928
3150
|
const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
2929
|
-
const result = await q.query(`SELECT
|
|
3151
|
+
const result = await q.query(`SELECT ${SCHEDULE_COLUMNS}
|
|
2930
3152
|
FROM "${this.schemaName}".workflow_schedules${where}
|
|
2931
3153
|
ORDER BY schedule_name`, params);
|
|
2932
|
-
return result.rows.map((row) => (
|
|
2933
|
-
scheduleId: row.schedule_id,
|
|
2934
|
-
scheduleName: row.schedule_name,
|
|
2935
|
-
workflowName: row.workflow_name,
|
|
2936
|
-
workflowClassName: row.workflow_class_name,
|
|
2937
|
-
schedule: row.schedule,
|
|
2938
|
-
status: row.status,
|
|
2939
|
-
context: row.context,
|
|
2940
|
-
lastFiredAt: row.last_fired_at ?? null,
|
|
2941
|
-
automaticBackfill: !!row.automatic_backfill,
|
|
2942
|
-
cronTimezone: row.cron_timezone ?? null,
|
|
2943
|
-
queueName: row.queue_name ?? null,
|
|
2944
|
-
}));
|
|
3154
|
+
return result.rows.map((row) => mapWorkflowSchedule(row));
|
|
2945
3155
|
}
|
|
2946
3156
|
async getSchedule(name, client) {
|
|
2947
3157
|
const q = client ?? this.pool;
|
|
2948
|
-
const result = await q.query(`SELECT
|
|
3158
|
+
const result = await q.query(`SELECT ${SCHEDULE_COLUMNS}
|
|
2949
3159
|
FROM "${this.schemaName}".workflow_schedules
|
|
2950
3160
|
WHERE schedule_name = $1`, [name]);
|
|
2951
3161
|
if (result.rows.length === 0)
|
|
2952
3162
|
return null;
|
|
2953
|
-
|
|
2954
|
-
return {
|
|
2955
|
-
scheduleId: row.schedule_id,
|
|
2956
|
-
scheduleName: row.schedule_name,
|
|
2957
|
-
workflowName: row.workflow_name,
|
|
2958
|
-
workflowClassName: row.workflow_class_name,
|
|
2959
|
-
schedule: row.schedule,
|
|
2960
|
-
status: row.status,
|
|
2961
|
-
context: row.context,
|
|
2962
|
-
lastFiredAt: row.last_fired_at ?? null,
|
|
2963
|
-
automaticBackfill: !!row.automatic_backfill,
|
|
2964
|
-
cronTimezone: row.cron_timezone ?? null,
|
|
2965
|
-
queueName: row.queue_name ?? null,
|
|
2966
|
-
};
|
|
3163
|
+
return mapWorkflowSchedule(result.rows[0]);
|
|
2967
3164
|
}
|
|
2968
3165
|
async deleteSchedule(name, client) {
|
|
2969
3166
|
const q = client ?? this.pool;
|
|
@@ -3019,10 +3216,11 @@ class SystemDatabase {
|
|
|
3019
3216
|
try {
|
|
3020
3217
|
await client.query('BEGIN');
|
|
3021
3218
|
for (const sched of schedules) {
|
|
3219
|
+
const owner = await this.#resolveRowOwner(client, 'workflow_schedules', 'schedule_name', sched.scheduleName, sched.applicationName, 'Schedule');
|
|
3022
3220
|
// Upsert on schedule_name; on conflict, preserve schedule_id and runtime state (status, last_fired_at) and update only the declared definition fields, so an unchanged re-apply is a no-op.
|
|
3023
3221
|
await client.query(`INSERT INTO "${this.schemaName}".workflow_schedules
|
|
3024
|
-
(schedule_id, schedule_name, workflow_name, workflow_class_name, schedule, status, context, last_fired_at, automatic_backfill, cron_timezone, queue_name)
|
|
3025
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
3222
|
+
(schedule_id, schedule_name, workflow_name, workflow_class_name, schedule, status, context, last_fired_at, automatic_backfill, cron_timezone, queue_name, application_name)
|
|
3223
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
3026
3224
|
ON CONFLICT (schedule_name) DO UPDATE SET
|
|
3027
3225
|
workflow_name = EXCLUDED.workflow_name,
|
|
3028
3226
|
workflow_class_name = EXCLUDED.workflow_class_name,
|
|
@@ -3030,7 +3228,9 @@ class SystemDatabase {
|
|
|
3030
3228
|
context = EXCLUDED.context,
|
|
3031
3229
|
automatic_backfill = EXCLUDED.automatic_backfill,
|
|
3032
3230
|
cron_timezone = EXCLUDED.cron_timezone,
|
|
3033
|
-
queue_name = EXCLUDED.queue_name
|
|
3231
|
+
queue_name = EXCLUDED.queue_name,
|
|
3232
|
+
-- Claim only an unclaimed row, so a registration landing between the check above and this write keeps the name it took.
|
|
3233
|
+
application_name = COALESCE("${this.schemaName}".workflow_schedules.application_name, EXCLUDED.application_name)`, [
|
|
3034
3234
|
sched.scheduleId,
|
|
3035
3235
|
sched.scheduleName,
|
|
3036
3236
|
sched.workflowName,
|
|
@@ -3042,7 +3242,10 @@ class SystemDatabase {
|
|
|
3042
3242
|
sched.automaticBackfill,
|
|
3043
3243
|
sched.cronTimezone,
|
|
3044
3244
|
sched.queueName,
|
|
3245
|
+
owner ?? null,
|
|
3045
3246
|
]);
|
|
3247
|
+
// Read back, since the guard above is silent about why it declined to claim.
|
|
3248
|
+
await this.#resolveRowOwner(client, 'workflow_schedules', 'schedule_name', sched.scheduleName, sched.applicationName, 'Schedule');
|
|
3046
3249
|
}
|
|
3047
3250
|
await client.query('COMMIT');
|
|
3048
3251
|
}
|
|
@@ -3055,56 +3258,109 @@ class SystemDatabase {
|
|
|
3055
3258
|
}
|
|
3056
3259
|
}
|
|
3057
3260
|
// ==================== Application Versions ====================
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3261
|
+
/**
|
|
3262
|
+
* Register this version, claiming the row if nobody owns it yet so a pinned version
|
|
3263
|
+
* does not stay unclaimed. A peer's name is a collision, which is why this throws.
|
|
3264
|
+
*/
|
|
3265
|
+
async createApplicationVersion(versionName, applicationName) {
|
|
3266
|
+
const owner = applicationName ?? this.appName;
|
|
3267
|
+
const client = await this.pool.connect();
|
|
3268
|
+
try {
|
|
3269
|
+
await client.query('BEGIN');
|
|
3270
|
+
// Claim a pre-upgrade row in place, so the version is not recreated or retimed.
|
|
3271
|
+
const claimed = await client.query(`UPDATE "${this.schemaName}".application_versions
|
|
3272
|
+
SET application_name = $1
|
|
3273
|
+
WHERE version_name = $2 AND application_name IS NULL`, [owner ?? null, versionName]);
|
|
3274
|
+
if ((claimed.rowCount ?? 0) === 0) {
|
|
3275
|
+
// Targetless DO NOTHING: names no arbiter, so it survives version_name's uniqueness being dropped while still absorbing a concurrent registrar.
|
|
3276
|
+
await client.query(`INSERT INTO "${this.schemaName}".application_versions (version_id, version_name, application_name)
|
|
3277
|
+
VALUES ($1, $2, $3)
|
|
3278
|
+
ON CONFLICT DO NOTHING`, [(0, crypto_1.randomUUID)(), versionName, owner ?? null]);
|
|
3279
|
+
}
|
|
3280
|
+
// Read back, since the writes above are silent about why they declined to claim.
|
|
3281
|
+
await this.#resolveRowOwner(client, 'application_versions', 'version_name', versionName, owner, 'Application version');
|
|
3282
|
+
await client.query('COMMIT');
|
|
3283
|
+
}
|
|
3284
|
+
catch (e) {
|
|
3285
|
+
await client.query('ROLLBACK');
|
|
3286
|
+
throw e;
|
|
3287
|
+
}
|
|
3288
|
+
finally {
|
|
3289
|
+
client.release();
|
|
3290
|
+
}
|
|
3063
3291
|
}
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3292
|
+
/**
|
|
3293
|
+
* Promote a version to latest. Promoting a peer's is a collision, not a retiming;
|
|
3294
|
+
* promotion claims an unclaimed row, which would otherwise be every peer's latest.
|
|
3295
|
+
*/
|
|
3296
|
+
async updateApplicationVersionTimestamp(versionName, newTimestamp, applicationName) {
|
|
3297
|
+
const owner = applicationName ?? this.appName;
|
|
3298
|
+
const client = await this.pool.connect();
|
|
3299
|
+
try {
|
|
3300
|
+
await client.query('BEGIN');
|
|
3301
|
+
const resolved = await this.#resolveRowOwner(client, 'application_versions', 'version_name', versionName, owner, 'Application version');
|
|
3302
|
+
// Scoped to the row this writer resolved to: once version_name is no longer globally unique, a bare name match would retime every peer's version.
|
|
3303
|
+
const scope = resolved === undefined ? 'application_name IS NULL' : '(application_name = $3 OR application_name IS NULL)';
|
|
3304
|
+
const params = [newTimestamp, versionName];
|
|
3305
|
+
if (resolved !== undefined)
|
|
3306
|
+
params.push(resolved);
|
|
3307
|
+
await client.query(`UPDATE "${this.schemaName}".application_versions
|
|
3308
|
+
SET version_timestamp = $1, application_name = ${resolved === undefined ? 'application_name' : '$3'}
|
|
3309
|
+
WHERE version_name = $2 AND ${scope}`, params);
|
|
3310
|
+
await client.query('COMMIT');
|
|
3311
|
+
}
|
|
3312
|
+
catch (e) {
|
|
3313
|
+
await client.query('ROLLBACK');
|
|
3314
|
+
throw e;
|
|
3315
|
+
}
|
|
3316
|
+
finally {
|
|
3317
|
+
client.release();
|
|
3318
|
+
}
|
|
3068
3319
|
}
|
|
3069
3320
|
async listApplicationVersions() {
|
|
3070
|
-
const
|
|
3321
|
+
const params = [];
|
|
3322
|
+
const scope = this.#appNameFilter('application_name', this.appName, params);
|
|
3323
|
+
const { rows } = await this.pool.query(`SELECT version_id, version_name, version_timestamp, created_at, application_name
|
|
3071
3324
|
FROM "${this.schemaName}".application_versions
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
versionName: r.version_name,
|
|
3076
|
-
versionTimestamp: Number(r.version_timestamp),
|
|
3077
|
-
createdAt: Number(r.created_at),
|
|
3078
|
-
}));
|
|
3325
|
+
WHERE ${scope}
|
|
3326
|
+
ORDER BY version_timestamp DESC`, params);
|
|
3327
|
+
return rows.map(mapVersionInfo);
|
|
3079
3328
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3329
|
+
/**
|
|
3330
|
+
* The latest version registered by an application. Defaults to this handle's, so a
|
|
3331
|
+
* caller acting for another one — firing its schedule — must name it.
|
|
3332
|
+
*/
|
|
3333
|
+
async getLatestApplicationVersion(applicationName) {
|
|
3334
|
+
const owner = applicationName ?? this.appName;
|
|
3335
|
+
const params = [];
|
|
3336
|
+
const scope = this.#appNameFilter('application_name', owner, params);
|
|
3337
|
+
const { rows } = await this.pool.query(`SELECT version_id, version_name, version_timestamp, created_at, application_name
|
|
3082
3338
|
FROM "${this.schemaName}".application_versions
|
|
3339
|
+
WHERE ${scope}
|
|
3083
3340
|
ORDER BY version_timestamp DESC
|
|
3084
|
-
LIMIT 1
|
|
3341
|
+
LIMIT 1`, params);
|
|
3085
3342
|
if (rows.length === 0) {
|
|
3086
3343
|
throw new error_1.DBOSInitializationError('No application versions found');
|
|
3087
3344
|
}
|
|
3088
|
-
|
|
3089
|
-
return {
|
|
3090
|
-
versionId: r.version_id,
|
|
3091
|
-
versionName: r.version_name,
|
|
3092
|
-
versionTimestamp: Number(r.version_timestamp),
|
|
3093
|
-
createdAt: Number(r.created_at),
|
|
3094
|
-
};
|
|
3345
|
+
return mapVersionInfo(rows[0]);
|
|
3095
3346
|
}
|
|
3096
3347
|
// ==================== Queues ====================
|
|
3097
3348
|
async getQueue(name) {
|
|
3098
|
-
const { rows } = await this.pool.query(`SELECT
|
|
3099
|
-
priority_enabled, partition_queue, polling_interval_sec
|
|
3349
|
+
const { rows } = await this.pool.query(`SELECT ${QUEUE_COLUMNS}
|
|
3100
3350
|
FROM "${this.schemaName}".queues
|
|
3101
3351
|
WHERE name = $1`, [name]);
|
|
3102
3352
|
return rows.length === 0 ? null : queueRecordFromRow(rows[0]);
|
|
3103
3353
|
}
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3354
|
+
/**
|
|
3355
|
+
* List only queues owned by these applications, plus unclaimed ones.
|
|
3356
|
+
* By default, only list this application's queues.
|
|
3357
|
+
*/
|
|
3358
|
+
async listQueues(applicationName) {
|
|
3359
|
+
const params = [];
|
|
3360
|
+
const scope = this.#observabilityFilter('application_name', applicationName, params);
|
|
3361
|
+
const { rows } = await this.pool.query(`SELECT ${QUEUE_COLUMNS}
|
|
3362
|
+
FROM "${this.schemaName}".queues
|
|
3363
|
+
WHERE ${scope}`, params);
|
|
3108
3364
|
return rows.map(queueRecordFromRow);
|
|
3109
3365
|
}
|
|
3110
3366
|
async deleteQueue(name) {
|
|
@@ -3140,16 +3396,21 @@ class SystemDatabase {
|
|
|
3140
3396
|
priority_enabled = EXCLUDED.priority_enabled,
|
|
3141
3397
|
partition_queue = EXCLUDED.partition_queue,
|
|
3142
3398
|
polling_interval_sec = EXCLUDED.polling_interval_sec,
|
|
3143
|
-
updated_at = EXCLUDED.updated_at
|
|
3399
|
+
updated_at = EXCLUDED.updated_at,
|
|
3400
|
+
-- Claim only an unclaimed row, so a registration landing between the check above and this write keeps the name it just took.
|
|
3401
|
+
application_name = COALESCE("${this.schemaName}".queues.application_name, EXCLUDED.application_name)`
|
|
3144
3402
|
: `ON CONFLICT (name) DO NOTHING`;
|
|
3403
|
+
const owner = record.applicationName ?? this.appName;
|
|
3145
3404
|
const client = await this.pool.connect();
|
|
3146
3405
|
try {
|
|
3147
3406
|
await client.query('BEGIN');
|
|
3148
3407
|
const existed = await client.query(`SELECT name FROM "${this.schemaName}".queues WHERE name = $1`, [record.name]);
|
|
3408
|
+
// A name collision is a conflict in every mode: the name is the queue's address.
|
|
3409
|
+
const resolvedOwner = await this.#resolveRowOwner(client, 'queues', 'name', record.name, owner, 'Queue');
|
|
3149
3410
|
await client.query(`INSERT INTO "${this.schemaName}".queues
|
|
3150
3411
|
(name, concurrency, worker_concurrency, rate_limit_max, rate_limit_period_sec,
|
|
3151
|
-
priority_enabled, partition_queue, polling_interval_sec, updated_at)
|
|
3152
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
3412
|
+
priority_enabled, partition_queue, polling_interval_sec, updated_at, application_name)
|
|
3413
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
3153
3414
|
${onConflict}`, [
|
|
3154
3415
|
record.name,
|
|
3155
3416
|
record.concurrency,
|
|
@@ -3160,7 +3421,10 @@ class SystemDatabase {
|
|
|
3160
3421
|
record.partitionQueue,
|
|
3161
3422
|
record.pollingIntervalSec,
|
|
3162
3423
|
now,
|
|
3424
|
+
resolvedOwner ?? null,
|
|
3163
3425
|
]);
|
|
3426
|
+
// Read back, since the guard above is silent about why it declined to claim.
|
|
3427
|
+
await this.#resolveRowOwner(client, 'queues', 'name', record.name, owner, 'Queue');
|
|
3164
3428
|
await client.query('COMMIT');
|
|
3165
3429
|
return existed.rowCount === 0;
|
|
3166
3430
|
}
|
|
@@ -3172,6 +3436,123 @@ class SystemDatabase {
|
|
|
3172
3436
|
client.release();
|
|
3173
3437
|
}
|
|
3174
3438
|
}
|
|
3439
|
+
// ==================== Application Rename ====================
|
|
3440
|
+
/**
|
|
3441
|
+
* Rows a rename moves: an application's own, unclaimed ones, or both. Unlike
|
|
3442
|
+
* the claiming scope, unclaimed rows are not implied; they move only when asked.
|
|
3443
|
+
*/
|
|
3444
|
+
#renameSource(oldName, adoptUnclaimedRows, params) {
|
|
3445
|
+
const clauses = [];
|
|
3446
|
+
if (oldName !== undefined) {
|
|
3447
|
+
params.push(oldName);
|
|
3448
|
+
clauses.push(`application_name = $${params.length}`);
|
|
3449
|
+
}
|
|
3450
|
+
if (adoptUnclaimedRows) {
|
|
3451
|
+
clauses.push('application_name IS NULL');
|
|
3452
|
+
}
|
|
3453
|
+
// Callers validate that at least one source is named.
|
|
3454
|
+
return `(${clauses.join(' OR ')})`;
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Re-own a table's rows in half-open key ranges, so a long history neither moves in
|
|
3458
|
+
* one transaction nor rescans what it already moved; a re-run resumes.
|
|
3459
|
+
*/
|
|
3460
|
+
async #renameRowsInBatches(table, keyColumn, oldName, newName, batchSize, adoptUnclaimedRows) {
|
|
3461
|
+
if (batchSize === undefined) {
|
|
3462
|
+
const params = [newName];
|
|
3463
|
+
const predicate = this.#renameSource(oldName, adoptUnclaimedRows, params);
|
|
3464
|
+
const res = await this.pool.query(`UPDATE "${this.schemaName}".${table} SET application_name = $1 WHERE ${predicate}`, params);
|
|
3465
|
+
return res.rowCount ?? 0;
|
|
3466
|
+
}
|
|
3467
|
+
let total = 0;
|
|
3468
|
+
// Ranges, not LIMIT: a LIMIT repages every row already moved, and an IN list of keys plans as a whole-table hash join.
|
|
3469
|
+
let watermark = undefined;
|
|
3470
|
+
for (;;) {
|
|
3471
|
+
const boundParams = [];
|
|
3472
|
+
const predicate = this.#renameSource(oldName, adoptUnclaimedRows, boundParams);
|
|
3473
|
+
let scope = predicate;
|
|
3474
|
+
if (watermark !== undefined) {
|
|
3475
|
+
boundParams.push(watermark);
|
|
3476
|
+
scope = `${predicate} AND ${keyColumn} > $${boundParams.length}`;
|
|
3477
|
+
}
|
|
3478
|
+
// The batchSize-th matching key bounds this range; distinct, so a key's rows are never split across batches.
|
|
3479
|
+
const upperResult = await this.pool.query(`SELECT DISTINCT ${keyColumn} FROM "${this.schemaName}".${table}
|
|
3480
|
+
WHERE ${scope} ORDER BY ${keyColumn} LIMIT 1 OFFSET ${batchSize - 1}`, boundParams);
|
|
3481
|
+
const upper = upperResult.rows[0]?.[keyColumn];
|
|
3482
|
+
const updateParams = [newName];
|
|
3483
|
+
const updatePredicate = this.#renameSource(oldName, adoptUnclaimedRows, updateParams);
|
|
3484
|
+
let batch = updatePredicate;
|
|
3485
|
+
if (upper !== undefined) {
|
|
3486
|
+
if (watermark !== undefined) {
|
|
3487
|
+
updateParams.push(watermark);
|
|
3488
|
+
batch = `${batch} AND ${keyColumn} > $${updateParams.length}`;
|
|
3489
|
+
}
|
|
3490
|
+
updateParams.push(upper);
|
|
3491
|
+
batch = `${batch} AND ${keyColumn} <= $${updateParams.length}`;
|
|
3492
|
+
}
|
|
3493
|
+
// The final batch drops the watermark, so rows that appeared below it still move.
|
|
3494
|
+
const res = await this.pool.query(`UPDATE "${this.schemaName}".${table} SET application_name = $1 WHERE ${batch}`, updateParams);
|
|
3495
|
+
total += res.rowCount ?? 0;
|
|
3496
|
+
// Fewer than a full batch remained, so that update took the rest.
|
|
3497
|
+
if (upper === undefined)
|
|
3498
|
+
return total;
|
|
3499
|
+
watermark = upper;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
/**
|
|
3503
|
+
* Give `newName` ownership of rows `oldName` holds, of unclaimed rows, or of both.
|
|
3504
|
+
* The renamed application must be stopped, or its dequeues race this.
|
|
3505
|
+
*/
|
|
3506
|
+
async renameApplication(oldName, newName, options = {}) {
|
|
3507
|
+
const adoptUnclaimedRows = options.adoptUnclaimedRows ?? false;
|
|
3508
|
+
const batchSize = options.batchSize === null ? undefined : (options.batchSize ?? exports.DEFAULT_RENAME_BATCH_SIZE);
|
|
3509
|
+
if (oldName !== undefined && oldName === '') {
|
|
3510
|
+
throw new error_1.DBOSError("The application's previous name cannot be empty.");
|
|
3511
|
+
}
|
|
3512
|
+
if (oldName === undefined && !adoptUnclaimedRows) {
|
|
3513
|
+
throw new error_1.DBOSError('Nothing to re-own: name the application to rename, adopt unclaimed rows, or both.');
|
|
3514
|
+
}
|
|
3515
|
+
if (oldName === newName) {
|
|
3516
|
+
throw new error_1.DBOSError(`Application '${newName}' already holds that name; nothing to rename.`);
|
|
3517
|
+
}
|
|
3518
|
+
// A NaN survives a bare `< 1` test and would only fail once it reached SQL, leaving the rename half-applied.
|
|
3519
|
+
if (batchSize !== undefined && (!Number.isInteger(batchSize) || batchSize < 1)) {
|
|
3520
|
+
throw new error_1.DBOSError(`batchSize must be a positive integer, got ${batchSize}`);
|
|
3521
|
+
}
|
|
3522
|
+
// Never a merge: queue, schedule, and version names are globally unique whatever their owner, so this cannot collide.
|
|
3523
|
+
const client = await this.pool.connect();
|
|
3524
|
+
let queues, schedules, versions, inFlight;
|
|
3525
|
+
try {
|
|
3526
|
+
await client.query('BEGIN');
|
|
3527
|
+
const move = async (table, statuses) => {
|
|
3528
|
+
const params = [newName];
|
|
3529
|
+
let where = this.#renameSource(oldName, adoptUnclaimedRows, params);
|
|
3530
|
+
if (statuses !== undefined) {
|
|
3531
|
+
params.push(statuses);
|
|
3532
|
+
where = `${where} AND status = ANY($${params.length})`;
|
|
3533
|
+
}
|
|
3534
|
+
const res = await client.query(`UPDATE "${this.schemaName}".${table} SET application_name = $1 WHERE ${where}`, params);
|
|
3535
|
+
return res.rowCount ?? 0;
|
|
3536
|
+
};
|
|
3537
|
+
// A half-owned application dequeues work whose version row it can no longer see, so these move together.
|
|
3538
|
+
queues = await move('queues');
|
|
3539
|
+
schedules = await move('workflow_schedules');
|
|
3540
|
+
versions = await move('application_versions');
|
|
3541
|
+
inFlight = await move('workflow_status', [workflow_1.StatusString.PENDING, workflow_1.StatusString.ENQUEUED, workflow_1.StatusString.DELAYED]);
|
|
3542
|
+
await client.query('COMMIT');
|
|
3543
|
+
}
|
|
3544
|
+
catch (e) {
|
|
3545
|
+
await client.query('ROLLBACK');
|
|
3546
|
+
throw e;
|
|
3547
|
+
}
|
|
3548
|
+
finally {
|
|
3549
|
+
client.release();
|
|
3550
|
+
}
|
|
3551
|
+
// Only terminal rows are left to match, and they scope observability and GC alone, so they may lag behind the commit above.
|
|
3552
|
+
const terminal = await this.#renameRowsInBatches('workflow_status', 'workflow_uuid', oldName, newName, batchSize, adoptUnclaimedRows);
|
|
3553
|
+
const steps = await this.#renameRowsInBatches('operation_outputs', 'workflow_uuid', oldName, newName, batchSize, adoptUnclaimedRows);
|
|
3554
|
+
return { queues, schedules, versions, workflows: inFlight + terminal, steps };
|
|
3555
|
+
}
|
|
3175
3556
|
// ==================== Internal ====================
|
|
3176
3557
|
async insertWorkflowStatus(client, initStatus, ownerXid, incrementAttempts = false) {
|
|
3177
3558
|
try {
|
|
@@ -3206,8 +3587,9 @@ class SystemDatabase {
|
|
|
3206
3587
|
attributes,
|
|
3207
3588
|
schedule_name,
|
|
3208
3589
|
debounce_deadline_epoch_ms,
|
|
3209
|
-
is_debounced
|
|
3210
|
-
|
|
3590
|
+
is_debounced,
|
|
3591
|
+
application_name
|
|
3592
|
+
) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $26, $27, $28, $29, $30, $31, $32, $33)
|
|
3211
3593
|
ON CONFLICT (workflow_uuid)
|
|
3212
3594
|
DO UPDATE SET
|
|
3213
3595
|
recovery_attempts = CASE
|
|
@@ -3255,6 +3637,8 @@ class SystemDatabase {
|
|
|
3255
3637
|
initStatus.scheduleName ?? null,
|
|
3256
3638
|
initStatus.debounceDeadlineEpochMS ?? null,
|
|
3257
3639
|
initStatus.isDebounced ?? false,
|
|
3640
|
+
// Absent from the conflict update: a re-enqueue must not re-own a claimed row.
|
|
3641
|
+
initStatus.applicationName ?? null,
|
|
3258
3642
|
]);
|
|
3259
3643
|
if (rows.length === 0) {
|
|
3260
3644
|
throw new Error(`Attempt to insert workflow ${initStatus.workflowUUID} failed`);
|
|
@@ -3342,8 +3726,8 @@ class SystemDatabase {
|
|
|
3342
3726
|
async recordOperationResultInternal(client, workflowID, functionID, functionName, checkConflict, startTimeEpochMs, endTimeEpochMs, options = {}) {
|
|
3343
3727
|
try {
|
|
3344
3728
|
const out = await client.query(`INSERT INTO ${this.schemaName}.operation_outputs
|
|
3345
|
-
(workflow_uuid, function_id, output, error, function_name, child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms, serialization)
|
|
3346
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
3729
|
+
(workflow_uuid, function_id, output, error, function_name, child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms, serialization, application_name)
|
|
3730
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
3347
3731
|
ON CONFLICT (workflow_uuid, function_id) DO UPDATE
|
|
3348
3732
|
SET completed_at_epoch_ms = operation_outputs.completed_at_epoch_ms
|
|
3349
3733
|
RETURNING completed_at_epoch_ms;`, [
|
|
@@ -3356,6 +3740,8 @@ class SystemDatabase {
|
|
|
3356
3740
|
startTimeEpochMs,
|
|
3357
3741
|
endTimeEpochMs,
|
|
3358
3742
|
options.serialization ?? null,
|
|
3743
|
+
// Mirrors the parent: only the running application records its steps.
|
|
3744
|
+
this.appName ?? null,
|
|
3359
3745
|
]);
|
|
3360
3746
|
if (checkConflict &&
|
|
3361
3747
|
(out?.rowCount ?? 0) > 0 &&
|
|
@@ -3697,7 +4083,13 @@ __decorate([
|
|
|
3697
4083
|
__decorate([
|
|
3698
4084
|
dbRetry(),
|
|
3699
4085
|
__metadata("design:type", Function),
|
|
3700
|
-
__metadata("design:paramtypes", [
|
|
4086
|
+
__metadata("design:paramtypes", [Number]),
|
|
4087
|
+
__metadata("design:returntype", Promise)
|
|
4088
|
+
], SystemDatabase.prototype, "listTimedOutWorkflowIds", null);
|
|
4089
|
+
__decorate([
|
|
4090
|
+
dbRetry(),
|
|
4091
|
+
__metadata("design:type", Function),
|
|
4092
|
+
__metadata("design:paramtypes", [String, String, Array]),
|
|
3701
4093
|
__metadata("design:returntype", Promise)
|
|
3702
4094
|
], SystemDatabase.prototype, "getMetrics", null);
|
|
3703
4095
|
//# sourceMappingURL=system_database.js.map
|