@dbos-inc/dbos-sdk 5.1.10 → 5.2.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 +2 -1
- package/dist/schemas/system_db_schema.d.ts.map +1 -1
- package/dist/schemas/system_db_schema.js.map +1 -1
- package/dist/src/client.d.ts +3 -1
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +5 -4
- package/dist/src/client.js.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +2 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/context.d.ts +3 -0
- package/dist/src/context.d.ts.map +1 -1
- package/dist/src/context.js +13 -1
- package/dist/src/context.js.map +1 -1
- package/dist/src/dbos-executor.d.ts +15 -2
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +33 -32
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +5 -0
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/error.d.ts +8 -0
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +17 -2
- package/dist/src/error.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/scheduler/crontab.d.ts +9 -2
- package/dist/src/scheduler/crontab.d.ts.map +1 -1
- package/dist/src/scheduler/crontab.js +418 -181
- package/dist/src/scheduler/crontab.js.map +1 -1
- package/dist/src/scheduler/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler/scheduler.js +14 -8
- package/dist/src/scheduler/scheduler.js.map +1 -1
- package/dist/src/sysdb_migrations/internal/migrations.js +5 -0
- package/dist/src/sysdb_migrations/internal/migrations.js.map +1 -1
- package/dist/src/system_database.d.ts +30 -14
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +291 -131
- package/dist/src/system_database.js.map +1 -1
- package/dist/src/wfqueue.d.ts.map +1 -1
- package/dist/src/wfqueue.js +12 -8
- package/dist/src/wfqueue.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -9,12 +9,13 @@ 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.verifySystemDatabase = exports.ensureSystemDatabase = exports.retentionLockKey = exports.grantDbosSchemaPermissions = exports.getDbosSchemaPermissionsSql = exports.DEFAULT_GC_BATCH_SIZE = exports.DEFAULT_RENAME_BATCH_SIZE = exports.validateObservabilityQueryTimeoutMs = exports.DEFAULT_OBSERVABILITY_QUERY_TIMEOUT_MS = exports.DEFAULT_NOTIFICATION_COALESCE_MS = exports.DBOS_STREAMS_CHANNEL = exports.DBOS_WORKFLOW_EVENTS_CHANNEL = exports.DBOS_NOTIFICATIONS_CHANNEL = exports.isLegacyClosedSentinel = exports.isStreamClosedSentinel = exports.DBOS_STREAM_CLOSED_SENTINEL_SERIALIZED = exports.DBOS_STREAM_CLOSED_SENTINEL = exports.DEFAULT_POOL_SIZE = exports.DBOS_FUNCNAME_READSTREAMOFFSET = exports.DBOS_FUNCNAME_READSTREAM = 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.retriablePostgresException = exports.verifySystemDatabase = exports.ensureSystemDatabase = exports.retentionLockKey = exports.grantDbosSchemaPermissions = exports.getDbosSchemaPermissionsSql = exports.DEFAULT_GC_BATCH_SIZE = exports.DEFAULT_RENAME_BATCH_SIZE = exports.validateIdleTransactionTimeoutMs = exports.DEFAULT_IDLE_TRANSACTION_TIMEOUT_MS = exports.validateObservabilityQueryTimeoutMs = exports.DEFAULT_OBSERVABILITY_QUERY_TIMEOUT_MS = exports.DEFAULT_NOTIFICATION_COALESCE_MS = exports.DBOS_STREAMS_CHANNEL = exports.DBOS_WORKFLOW_EVENTS_CHANNEL = exports.DBOS_NOTIFICATIONS_CHANNEL = exports.isLegacyClosedSentinel = exports.isStreamClosedSentinel = exports.DBOS_STREAM_CLOSED_SENTINEL_SERIALIZED = exports.DBOS_STREAM_CLOSED_SENTINEL = exports.DEFAULT_POOL_SIZE = exports.DBOS_FUNCNAME_READSTREAMOFFSET = exports.DBOS_FUNCNAME_READSTREAM = 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");
|
|
16
16
|
const workflow_1 = require("./workflow");
|
|
17
17
|
const utils_1 = require("./utils");
|
|
18
|
+
const context_1 = require("./context");
|
|
18
19
|
const async_hooks_1 = require("async_hooks");
|
|
19
20
|
const crypto_1 = require("crypto");
|
|
20
21
|
const utils_2 = require("./utils");
|
|
@@ -67,6 +68,24 @@ function validateObservabilityQueryTimeoutMs(value) {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
exports.validateObservabilityQueryTimeoutMs = validateObservabilityQueryTimeoutMs;
|
|
71
|
+
// Bounds how long a frozen or unreachable client can hold system database locks.
|
|
72
|
+
exports.DEFAULT_IDLE_TRANSACTION_TIMEOUT_MS = 60_000;
|
|
73
|
+
/** The timeout is rendered into a `SET idle_in_transaction_session_timeout`, which takes only an in-range integer. */
|
|
74
|
+
function validateIdleTransactionTimeoutMs(value) {
|
|
75
|
+
if (value !== undefined && (!Number.isFinite(value) || value > MAX_STATEMENT_TIMEOUT_MS)) {
|
|
76
|
+
throw new Error(`systemDatabaseIdleTransactionTimeoutMs must be a finite number no greater than ${MAX_STATEMENT_TIMEOUT_MS}, got ${value}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.validateIdleTransactionTimeoutMs = validateIdleTransactionTimeoutMs;
|
|
80
|
+
/** Whether the connection string already sets idle_in_transaction_session_timeout through libpq options. */
|
|
81
|
+
function setsIdleTransactionTimeout(databaseUrl) {
|
|
82
|
+
try {
|
|
83
|
+
return new URL(databaseUrl).searchParams.get('options')?.includes('idle_in_transaction_session_timeout') ?? false;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
70
89
|
// Workflows re-owned per transaction by a rename.
|
|
71
90
|
exports.DEFAULT_RENAME_BATCH_SIZE = 10_000;
|
|
72
91
|
// Rows deleted per transaction by garbage collection.
|
|
@@ -344,10 +363,12 @@ const RETRY_SQLSTATE_PREFIXES = new Set([
|
|
|
344
363
|
]);
|
|
345
364
|
const RETRY_SQLSTATE_CODES = new Set([
|
|
346
365
|
'40003', // statement_completion_unknown
|
|
366
|
+
'25P03', // idle_in_transaction_session_timeout, when the kill lands on an in-flight query; an idle kill surfaces as a message match below
|
|
367
|
+
'40P01', // deadlock_detected: the victim's transaction rolled back, so rerunning it is safe
|
|
347
368
|
]);
|
|
348
369
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
370
|
+
* Bulk maintenance retries these a bounded number of times. 40001 is kept out of the sets above,
|
|
371
|
+
* which feed `dbRetry` and so retry forever; every other path runs READ COMMITTED and never sees it.
|
|
351
372
|
*/
|
|
352
373
|
const SERIALIZATION_SQLSTATE_CODES = new Set([
|
|
353
374
|
'40001', // serialization_failure (MVCC conflict)
|
|
@@ -442,6 +463,7 @@ function retriablePostgresException(err) {
|
|
|
442
463
|
}
|
|
443
464
|
return false;
|
|
444
465
|
}
|
|
466
|
+
exports.retriablePostgresException = retriablePostgresException;
|
|
445
467
|
/** 57014 is query_canceled, which is how statement_timeout cancels a query. */
|
|
446
468
|
function isStatementTimeout(err) {
|
|
447
469
|
return !!err && typeof err === 'object' && err.code === '57014';
|
|
@@ -575,6 +597,8 @@ class SystemDatabase {
|
|
|
575
597
|
notificationCoalesceMs = exports.DEFAULT_NOTIFICATION_COALESCE_MS;
|
|
576
598
|
// Statement timeout for observability reads, in ms; undefined disables the cap.
|
|
577
599
|
observabilityQueryTimeoutMs;
|
|
600
|
+
// idle_in_transaction_session_timeout set on each connection of a pool we own, in ms; undefined leaves the server's.
|
|
601
|
+
idleTransactionTimeoutMs;
|
|
578
602
|
// Coalesced NOTIFY payloads keyed by channel, flushed by the notifier loop; soft-private so tests can drive a flush.
|
|
579
603
|
pendingNotifications = new Map();
|
|
580
604
|
#notifierActive = false;
|
|
@@ -588,7 +612,8 @@ class SystemDatabase {
|
|
|
588
612
|
* client and starve control-plane operations. See {@link #pollWithLimiter}.
|
|
589
613
|
*/
|
|
590
614
|
pollLimiter;
|
|
591
|
-
|
|
615
|
+
/** Executions running in this process, by workflow ID; a re-dispatch runs alongside its stale execution, which parks at its next write. */
|
|
616
|
+
runningWorkflowMap = new Map();
|
|
592
617
|
#cancelWatchers = new Map();
|
|
593
618
|
#cancelPollerLoop = undefined;
|
|
594
619
|
#cancelPollerWake = null;
|
|
@@ -605,7 +630,7 @@ class SystemDatabase {
|
|
|
605
630
|
#retentionClients = new Set();
|
|
606
631
|
constructor(systemDatabaseUrl, logger, serializer, sysDbPoolSize = exports.DEFAULT_POOL_SIZE, systemDatabasePool, schemaName = 'dbos', useListenNotify = true, pollingConcurrency, notificationCoalesceMs = exports.DEFAULT_NOTIFICATION_COALESCE_MS,
|
|
607
632
|
// The application this handle acts for; undefined writes unclaimed rows.
|
|
608
|
-
appName, observabilityQueryTimeoutMs = exports.DEFAULT_OBSERVABILITY_QUERY_TIMEOUT_MS) {
|
|
633
|
+
appName, observabilityQueryTimeoutMs = exports.DEFAULT_OBSERVABILITY_QUERY_TIMEOUT_MS, idleTransactionTimeoutMs = exports.DEFAULT_IDLE_TRANSACTION_TIMEOUT_MS) {
|
|
609
634
|
this.systemDatabaseUrl = systemDatabaseUrl;
|
|
610
635
|
this.logger = logger;
|
|
611
636
|
this.serializer = serializer;
|
|
@@ -614,6 +639,12 @@ class SystemDatabase {
|
|
|
614
639
|
this.shouldUseDBNotifications = useListenNotify;
|
|
615
640
|
this.notificationCoalesceMs = notificationCoalesceMs;
|
|
616
641
|
validateObservabilityQueryTimeoutMs(observabilityQueryTimeoutMs);
|
|
642
|
+
validateIdleTransactionTimeoutMs(idleTransactionTimeoutMs);
|
|
643
|
+
// Floor at 1ms: PostgreSQL reads 0 as "no timeout".
|
|
644
|
+
this.idleTransactionTimeoutMs =
|
|
645
|
+
idleTransactionTimeoutMs > 0 && !systemDatabasePool && !setsIdleTransactionTimeout(systemDatabaseUrl)
|
|
646
|
+
? Math.max(1, Math.round(idleTransactionTimeoutMs))
|
|
647
|
+
: undefined;
|
|
617
648
|
// Floor at 1ms: PostgreSQL reads 0 as "no timeout", the loosest cap rather than the tightest.
|
|
618
649
|
this.observabilityQueryTimeoutMs =
|
|
619
650
|
observabilityQueryTimeoutMs > 0 ? Math.max(1, Math.round(observabilityQueryTimeoutMs)) : undefined;
|
|
@@ -641,6 +672,15 @@ class SystemDatabase {
|
|
|
641
672
|
this.pool.on('error', (err) => {
|
|
642
673
|
this.logger.warn(`Unexpected error in pool: ${err}`);
|
|
643
674
|
});
|
|
675
|
+
const timeoutMs = this.idleTransactionTimeoutMs;
|
|
676
|
+
if (timeoutMs !== undefined) {
|
|
677
|
+
// Queued ahead of the borrower's first query; outside any transaction, so it persists for the session.
|
|
678
|
+
this.pool.on('connect', (client) => {
|
|
679
|
+
client.query(`SET idle_in_transaction_session_timeout = ${timeoutMs}`).catch((err) => {
|
|
680
|
+
this.logger.warn(`Could not set idle_in_transaction_session_timeout on a system database connection: ${err}`);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
}
|
|
644
684
|
}
|
|
645
685
|
}
|
|
646
686
|
#onClientError = (err) => {
|
|
@@ -808,18 +848,18 @@ class SystemDatabase {
|
|
|
808
848
|
}
|
|
809
849
|
// ==================== Workflow Status ====================
|
|
810
850
|
/** Runs on `client` if given, joining its transaction; otherwise in its own retried transaction. */
|
|
811
|
-
async initWorkflowStatus(initStatus,
|
|
851
|
+
async initWorkflowStatus(initStatus, creatorXid, client, reusePolicy = 'return-existing') {
|
|
812
852
|
if (client !== undefined) {
|
|
813
|
-
return await this.#initWorkflowStatusInternal(client, initStatus,
|
|
853
|
+
return await this.#initWorkflowStatusInternal(client, initStatus, creatorXid, reusePolicy);
|
|
814
854
|
}
|
|
815
|
-
return await this.initWorkflowStatusStandalone(initStatus,
|
|
855
|
+
return await this.initWorkflowStatusStandalone(initStatus, creatorXid, reusePolicy);
|
|
816
856
|
}
|
|
817
|
-
async initWorkflowStatusStandalone(initStatus,
|
|
857
|
+
async initWorkflowStatusStandalone(initStatus, creatorXid, reusePolicy) {
|
|
818
858
|
const client = await this.#connect();
|
|
819
859
|
let shouldCommit = false;
|
|
820
860
|
try {
|
|
821
861
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
822
|
-
const result = await this.#initWorkflowStatusInternal(client, initStatus,
|
|
862
|
+
const result = await this.#initWorkflowStatusInternal(client, initStatus, creatorXid, reusePolicy);
|
|
823
863
|
// If there is an existing DB record and we aren't here to recover it, leave it be.
|
|
824
864
|
shouldCommit = result.shouldExecuteOnThisExecutor;
|
|
825
865
|
return result;
|
|
@@ -840,13 +880,17 @@ class SystemDatabase {
|
|
|
840
880
|
}
|
|
841
881
|
}
|
|
842
882
|
/** Insert a child workflow's row and record it as the parent's step in one transaction, so a crash leaves both or neither. */
|
|
843
|
-
async initChildWorkflowStatus(initStatus,
|
|
883
|
+
async initChildWorkflowStatus(initStatus, creatorXid, parentWorkflowID, parentFunctionID, startTimeEpochMs, endTimeEpochMs, reusePolicy = 'return-existing') {
|
|
844
884
|
const client = await this.#connect();
|
|
845
885
|
let shouldCommit = false;
|
|
846
886
|
try {
|
|
847
887
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
848
|
-
const
|
|
849
|
-
|
|
888
|
+
const parentOwnerXid = (0, context_1.currentOwnerXid)(parentWorkflowID);
|
|
889
|
+
if (parentOwnerXid !== undefined) {
|
|
890
|
+
await this.#checkOwner(client, parentWorkflowID, parentOwnerXid);
|
|
891
|
+
}
|
|
892
|
+
const result = await this.#initWorkflowStatusInternal(client, initStatus, creatorXid, reusePolicy);
|
|
893
|
+
await this.recordOperationResultInternal(client, parentWorkflowID, parentFunctionID, initStatus.workflowName, true, startTimeEpochMs, endTimeEpochMs, { childWorkflowID: initStatus.workflowUUID, ownerChecked: true });
|
|
850
894
|
shouldCommit = true;
|
|
851
895
|
return result;
|
|
852
896
|
}
|
|
@@ -865,9 +909,9 @@ class SystemDatabase {
|
|
|
865
909
|
}
|
|
866
910
|
}
|
|
867
911
|
}
|
|
868
|
-
async #initWorkflowStatusInternal(client, initStatus,
|
|
869
|
-
const resRow = await this.insertWorkflowStatus(client, initStatus,
|
|
870
|
-
if (reusePolicy === 'reject' && (
|
|
912
|
+
async #initWorkflowStatusInternal(client, initStatus, creatorXid, reusePolicy) {
|
|
913
|
+
const resRow = await this.insertWorkflowStatus(client, initStatus, creatorXid);
|
|
914
|
+
if (reusePolicy === 'reject' && (creatorXid === null || resRow.creator_xid !== creatorXid)) {
|
|
871
915
|
throw new error_1.DBOSWorkflowIDInUseError(initStatus.workflowUUID, resRow.status, resRow.name);
|
|
872
916
|
}
|
|
873
917
|
if (resRow.name !== initStatus.workflowName) {
|
|
@@ -889,7 +933,7 @@ class SystemDatabase {
|
|
|
889
933
|
// The upsert above already set executor assignment for a row we own.
|
|
890
934
|
return {
|
|
891
935
|
status: resRow.status,
|
|
892
|
-
shouldExecuteOnThisExecutor:
|
|
936
|
+
shouldExecuteOnThisExecutor: creatorXid === resRow.creator_xid,
|
|
893
937
|
serialization: resRow.serialization,
|
|
894
938
|
};
|
|
895
939
|
}
|
|
@@ -899,6 +943,7 @@ class SystemDatabase {
|
|
|
899
943
|
return;
|
|
900
944
|
await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
901
945
|
SET status = $1,
|
|
946
|
+
owner_xid = NULL,
|
|
902
947
|
deduplication_id = NULL,
|
|
903
948
|
started_at_epoch_ms = NULL,
|
|
904
949
|
queue_name = NULL,
|
|
@@ -1007,7 +1052,7 @@ class SystemDatabase {
|
|
|
1007
1052
|
'queue_partition_key',
|
|
1008
1053
|
'parent_workflow_id',
|
|
1009
1054
|
'serialization',
|
|
1010
|
-
'
|
|
1055
|
+
'creator_xid',
|
|
1011
1056
|
'delay_until_epoch_ms',
|
|
1012
1057
|
'attributes',
|
|
1013
1058
|
'schedule_name',
|
|
@@ -1065,42 +1110,42 @@ class SystemDatabase {
|
|
|
1065
1110
|
}
|
|
1066
1111
|
return inserted;
|
|
1067
1112
|
}
|
|
1068
|
-
async recordWorkflowOutput(workflowID, status) {
|
|
1113
|
+
async recordWorkflowOutput(workflowID, status, ownerXid) {
|
|
1069
1114
|
const client = await this.#connect();
|
|
1070
1115
|
try {
|
|
1071
|
-
return await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.SUCCESS, { output: status.output });
|
|
1116
|
+
return await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.SUCCESS, { output: status.output }, ownerXid);
|
|
1072
1117
|
}
|
|
1073
1118
|
finally {
|
|
1074
1119
|
client.release();
|
|
1075
1120
|
}
|
|
1076
1121
|
}
|
|
1077
|
-
async recordWorkflowError(workflowID, status) {
|
|
1122
|
+
async recordWorkflowError(workflowID, status, ownerXid) {
|
|
1078
1123
|
const client = await this.#connect();
|
|
1079
1124
|
try {
|
|
1080
|
-
return await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.ERROR, { error: status.error });
|
|
1125
|
+
return await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.ERROR, { error: status.error }, ownerXid);
|
|
1081
1126
|
}
|
|
1082
1127
|
finally {
|
|
1083
1128
|
client.release();
|
|
1084
1129
|
}
|
|
1085
1130
|
}
|
|
1086
1131
|
// Record a workflow's terminal outcome (SUCCESS or ERROR), reporting whether
|
|
1087
|
-
// the write landed. The write applies only to a PENDING row
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
// concurrent execution is already running and the status is PENDING. However,
|
|
1091
|
-
// both executions should be deterministic and idempotent.)
|
|
1132
|
+
// the write landed. The write applies only to a PENDING row still owned by
|
|
1133
|
+
// ownerXid, which defaults to the calling execution's token; a caller
|
|
1134
|
+
// outside the workflow's context passes it explicitly or goes unchecked.
|
|
1092
1135
|
//
|
|
1093
1136
|
// Returning false means the row was CANCELLED, dead-lettered, already
|
|
1094
|
-
// terminal, handed to another execution (
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
async #recordWorkflowOutcome(client, workflowID, status, outcome) {
|
|
1137
|
+
// terminal, handed to another execution (e.g. by a concurrent resume or
|
|
1138
|
+
// recovery), or deleted; the caller resolves which by awaiting the recorded
|
|
1139
|
+
// outcome.
|
|
1140
|
+
async #recordWorkflowOutcome(client, workflowID, status, outcome, ownerXid) {
|
|
1141
|
+
ownerXid ??= (0, context_1.currentOwnerXid)(workflowID);
|
|
1098
1142
|
let committed = false;
|
|
1099
1143
|
try {
|
|
1100
1144
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
1101
1145
|
const rowCount = await this.updateWorkflowStatus(client, workflowID, status, {
|
|
1102
|
-
|
|
1103
|
-
|
|
1146
|
+
// A finished workflow has no owner.
|
|
1147
|
+
update: { resetDeduplicationID: true, setCompletedAt: true, ownerXid: null },
|
|
1148
|
+
where: { status: workflow_1.StatusString.PENDING, ownerXid },
|
|
1104
1149
|
throwOnFailure: false,
|
|
1105
1150
|
});
|
|
1106
1151
|
if (rowCount === 0) {
|
|
@@ -1142,6 +1187,7 @@ class SystemDatabase {
|
|
|
1142
1187
|
const result = await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
1143
1188
|
SET started_at_epoch_ms = NULL,
|
|
1144
1189
|
status = $1,
|
|
1190
|
+
owner_xid = NULL,
|
|
1145
1191
|
updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint,
|
|
1146
1192
|
queue_name = COALESCE(queue_name, $2)
|
|
1147
1193
|
WHERE status = $3
|
|
@@ -1161,7 +1207,7 @@ class SystemDatabase {
|
|
|
1161
1207
|
const client = await this.#connect();
|
|
1162
1208
|
try {
|
|
1163
1209
|
// Check if the operation has been done before for OAOO (only do this inside a workflow).
|
|
1164
|
-
const json = await this.#runAndRecordResult(client, exports.DBOS_FUNCNAME_GETSTATUS, callerID, callerFN, funcGetStatus);
|
|
1210
|
+
const json = await this.#inTransaction(client, () => this.#runAndRecordResult(client, exports.DBOS_FUNCNAME_GETSTATUS, callerID, callerFN, funcGetStatus));
|
|
1165
1211
|
return parseStatus(json);
|
|
1166
1212
|
}
|
|
1167
1213
|
finally {
|
|
@@ -1202,6 +1248,7 @@ class SystemDatabase {
|
|
|
1202
1248
|
resetNameTo: internalOptions?.updateName,
|
|
1203
1249
|
queueName: internalOptions?.queueName,
|
|
1204
1250
|
resetStartedAtEpochMs: internalOptions?.resetStartedAtEpochMs,
|
|
1251
|
+
ownerXid: internalOptions?.ownerXid,
|
|
1205
1252
|
},
|
|
1206
1253
|
});
|
|
1207
1254
|
}
|
|
@@ -1236,7 +1283,7 @@ class SystemDatabase {
|
|
|
1236
1283
|
async recordOperationResult(workflowID, functionID, functionName, checkConflict, startTimeEpochMs, endTimeEpochMs, options = {}) {
|
|
1237
1284
|
const client = await this.#connect();
|
|
1238
1285
|
try {
|
|
1239
|
-
await this.recordOperationResultInternal(client, workflowID, functionID, functionName, checkConflict, startTimeEpochMs, endTimeEpochMs, options);
|
|
1286
|
+
await this.#inTransaction(client, () => this.recordOperationResultInternal(client, workflowID, functionID, functionName, checkConflict, startTimeEpochMs, endTimeEpochMs, options));
|
|
1240
1287
|
}
|
|
1241
1288
|
finally {
|
|
1242
1289
|
client.release();
|
|
@@ -1274,31 +1321,38 @@ class SystemDatabase {
|
|
|
1274
1321
|
if (functionID === undefined)
|
|
1275
1322
|
throw new TypeError('functionID must be defined');
|
|
1276
1323
|
patchName = `DBOS.patch-${patchName}`;
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1279
|
-
|
|
1324
|
+
const readCheckpointName = async (queryable) => {
|
|
1325
|
+
const { rows } = await queryable.query(`SELECT function_name
|
|
1326
|
+
FROM "${this.schemaName}".operation_outputs
|
|
1327
|
+
WHERE workflow_uuid=$1 AND function_id=$2`, [workflowID, functionID]);
|
|
1328
|
+
return rows.length === 0 ? undefined : rows[0].function_name;
|
|
1329
|
+
};
|
|
1280
1330
|
if (deprecated) {
|
|
1281
1331
|
// Deprecated does not write anything. We skip any existing matching patch marker if it matches
|
|
1282
|
-
|
|
1332
|
+
const checkpointName = await readCheckpointName(this.pool);
|
|
1333
|
+
if (checkpointName === undefined) {
|
|
1283
1334
|
return { isPatched: true, hasEntry: false };
|
|
1284
1335
|
}
|
|
1285
|
-
return { isPatched: true, hasEntry:
|
|
1336
|
+
return { isPatched: true, hasEntry: checkpointName === patchName };
|
|
1286
1337
|
}
|
|
1287
1338
|
// Nondeprecated - skip matching entry, unpatched if nonmatching entry,
|
|
1288
|
-
// If there is no entry, we insert one that indicates it is patched.
|
|
1289
|
-
|
|
1290
|
-
|
|
1339
|
+
// If there is no entry, we insert one that indicates it is patched, as an owner-checked checkpoint.
|
|
1340
|
+
const client = await this.#connect();
|
|
1341
|
+
try {
|
|
1342
|
+
return await this.#inTransaction(client, async () => {
|
|
1343
|
+
const checkpointName = await readCheckpointName(client);
|
|
1344
|
+
if (checkpointName !== undefined) {
|
|
1345
|
+
const matches = checkpointName === patchName;
|
|
1346
|
+
return { isPatched: matches, hasEntry: matches };
|
|
1347
|
+
}
|
|
1348
|
+
const dn = Date.now();
|
|
1349
|
+
await this.recordOperationResultInternal(client, workflowID, functionID, patchName, true, dn, dn);
|
|
1291
1350
|
return { isPatched: true, hasEntry: true };
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
finally {
|
|
1354
|
+
client.release();
|
|
1294
1355
|
}
|
|
1295
|
-
// Insert a patchmarker
|
|
1296
|
-
const dn = Date.now();
|
|
1297
|
-
await this.pool.query(`INSERT INTO "${this.schemaName}".operation_outputs
|
|
1298
|
-
(workflow_uuid, function_id, output, error, function_name, child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms, application_name, retention_timestamp)
|
|
1299
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, (EXTRACT(EPOCH FROM now()) * 1000)::bigint)
|
|
1300
|
-
ON CONFLICT DO NOTHING;`, [workflowID, functionID, null, null, patchName, null, dn, dn, this.appName ?? null]);
|
|
1301
|
-
return { isPatched: true, hasEntry: true };
|
|
1302
1356
|
}
|
|
1303
1357
|
// ==================== Workflow Management ====================
|
|
1304
1358
|
async cancelWorkflows(workflowIDs, cancelChildren = false) {
|
|
@@ -1320,7 +1374,7 @@ class SystemDatabase {
|
|
|
1320
1374
|
}
|
|
1321
1375
|
async #cancelWorkflows(workflowIDs) {
|
|
1322
1376
|
await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
1323
|
-
SET status = $1, queue_name = NULL, deduplication_id = NULL, started_at_epoch_ms = NULL,
|
|
1377
|
+
SET status = $1, owner_xid = NULL, queue_name = NULL, deduplication_id = NULL, started_at_epoch_ms = NULL,
|
|
1324
1378
|
updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint,
|
|
1325
1379
|
completed_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint
|
|
1326
1380
|
WHERE workflow_uuid = ANY($2)
|
|
@@ -1331,7 +1385,7 @@ class SystemDatabase {
|
|
|
1331
1385
|
}
|
|
1332
1386
|
async resumeWorkflows(workflowIDs, queueName) {
|
|
1333
1387
|
await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
1334
|
-
SET status = $1, queue_name = $2, recovery_attempts = 0,
|
|
1388
|
+
SET status = $1, owner_xid = NULL, queue_name = $2, recovery_attempts = 0,
|
|
1335
1389
|
workflow_deadline_epoch_ms = NULL, deduplication_id = NULL,
|
|
1336
1390
|
started_at_epoch_ms = NULL,
|
|
1337
1391
|
updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint,
|
|
@@ -1583,7 +1637,7 @@ class SystemDatabase {
|
|
|
1583
1637
|
params.push(options.applicationVersion);
|
|
1584
1638
|
}
|
|
1585
1639
|
const { rowCount } = await client.query(`UPDATE "${schema}".workflow_status
|
|
1586
|
-
SET status = $1, queue_name = $2, queue_partition_key = $3, recovery_attempts = 0,
|
|
1640
|
+
SET status = $1, owner_xid = NULL, queue_name = $2, queue_partition_key = $3, recovery_attempts = 0,
|
|
1587
1641
|
workflow_deadline_epoch_ms = NULL, deduplication_id = NULL, started_at_epoch_ms = NULL,
|
|
1588
1642
|
completed_at = NULL, output = NULL, error = NULL,
|
|
1589
1643
|
updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint${setVersion}
|
|
@@ -1832,9 +1886,9 @@ class SystemDatabase {
|
|
|
1832
1886
|
for (const wfID of workflowIDs) {
|
|
1833
1887
|
// Export workflow_status
|
|
1834
1888
|
const statusResult = await client.query(
|
|
1835
|
-
// owner_xid
|
|
1836
|
-
//
|
|
1837
|
-
// meaningless in the target.
|
|
1889
|
+
// creator_xid and owner_xid are intentionally omitted: they are transient
|
|
1890
|
+
// tokens, not logical workflow state, and a source database's
|
|
1891
|
+
// tokens are meaningless in the target.
|
|
1838
1892
|
`SELECT
|
|
1839
1893
|
ws.workflow_uuid, ws.status, ws.name, ws.authenticated_user, ws.assumed_role,
|
|
1840
1894
|
ws.authenticated_roles,
|
|
@@ -2004,43 +2058,63 @@ class SystemDatabase {
|
|
|
2004
2058
|
}
|
|
2005
2059
|
}
|
|
2006
2060
|
// ==================== Awaiting Workflows ====================
|
|
2007
|
-
|
|
2061
|
+
/** Register one execution; the returned release is idempotent, so it can run both before a park and once the run settles. */
|
|
2062
|
+
registerRunningWorkflow(workflowID, queueName, queuePartitionKey) {
|
|
2063
|
+
const entry = {
|
|
2064
|
+
promise: Promise.resolve(),
|
|
2065
|
+
queueName,
|
|
2066
|
+
queuePartitionKey,
|
|
2067
|
+
release: () => {
|
|
2068
|
+
const entries = this.runningWorkflowMap.get(workflowID);
|
|
2069
|
+
const index = entries?.indexOf(entry) ?? -1;
|
|
2070
|
+
if (index < 0)
|
|
2071
|
+
return;
|
|
2072
|
+
entries.splice(index, 1);
|
|
2073
|
+
if (entries.length === 0)
|
|
2074
|
+
this.runningWorkflowMap.delete(workflowID);
|
|
2075
|
+
},
|
|
2076
|
+
};
|
|
2077
|
+
const entries = this.runningWorkflowMap.get(workflowID);
|
|
2078
|
+
if (entries)
|
|
2079
|
+
entries.push(entry);
|
|
2080
|
+
else
|
|
2081
|
+
this.runningWorkflowMap.set(workflowID, [entry]);
|
|
2082
|
+
return entry;
|
|
2083
|
+
}
|
|
2084
|
+
/** Track an execution's promise for shutdown, releasing its entry when it settles. */
|
|
2085
|
+
trackRunningWorkflow(workflowID, entry, workflowPromise) {
|
|
2008
2086
|
// Need to await for the workflow and capture errors.
|
|
2009
|
-
|
|
2087
|
+
entry.promise = workflowPromise
|
|
2010
2088
|
.catch((error) => {
|
|
2011
2089
|
const outcome = this.#destroyed ? 'was abandoned by shutdown' : 'failed';
|
|
2012
2090
|
this.logger.debug(`Workflow ${workflowID} ${outcome}: ${error}`);
|
|
2013
2091
|
})
|
|
2014
2092
|
.finally(() => {
|
|
2015
|
-
|
|
2016
|
-
});
|
|
2017
|
-
this.runningWorkflowMap.set(workflowID, {
|
|
2018
|
-
promise: awaitWorkflowPromise,
|
|
2019
|
-
queueName,
|
|
2020
|
-
queuePartitionKey,
|
|
2093
|
+
entry.release();
|
|
2021
2094
|
});
|
|
2022
2095
|
}
|
|
2023
2096
|
checkForRunningWorkflow(workflowID) {
|
|
2024
2097
|
return this.runningWorkflowMap.has(workflowID);
|
|
2025
2098
|
}
|
|
2026
|
-
clearRunningWorkflow(workflowID) {
|
|
2027
|
-
this.runningWorkflowMap.delete(workflowID);
|
|
2028
|
-
}
|
|
2029
2099
|
/** Workflows this worker is running for a queue, across every partition of it. */
|
|
2030
2100
|
countRunningWorkflowsForQueue(queueName) {
|
|
2031
2101
|
let count = 0;
|
|
2032
|
-
for (const
|
|
2033
|
-
|
|
2034
|
-
|
|
2102
|
+
for (const entries of this.runningWorkflowMap.values()) {
|
|
2103
|
+
for (const entry of entries) {
|
|
2104
|
+
if (entry.queueName === queueName)
|
|
2105
|
+
count++;
|
|
2106
|
+
}
|
|
2035
2107
|
}
|
|
2036
2108
|
return count;
|
|
2037
2109
|
}
|
|
2038
2110
|
/** Workflows this worker is running for one partition of a queue. */
|
|
2039
2111
|
countRunningWorkflowsForPartition(queueName, queuePartitionKey) {
|
|
2040
2112
|
let count = 0;
|
|
2041
|
-
for (const
|
|
2042
|
-
|
|
2043
|
-
|
|
2113
|
+
for (const entries of this.runningWorkflowMap.values()) {
|
|
2114
|
+
for (const entry of entries) {
|
|
2115
|
+
if (entry.queueName === queueName && entry.queuePartitionKey === queuePartitionKey)
|
|
2116
|
+
count++;
|
|
2117
|
+
}
|
|
2044
2118
|
}
|
|
2045
2119
|
return count;
|
|
2046
2120
|
}
|
|
@@ -2054,7 +2128,10 @@ class SystemDatabase {
|
|
|
2054
2128
|
// Each pass picks up workflows a draining workflow started, and awaits any given run only once.
|
|
2055
2129
|
const awaited = new Set();
|
|
2056
2130
|
for (;;) {
|
|
2057
|
-
const pending = Array.from(this.runningWorkflowMap.values()
|
|
2131
|
+
const pending = Array.from(this.runningWorkflowMap.values())
|
|
2132
|
+
.flat()
|
|
2133
|
+
.map((entry) => entry.promise)
|
|
2134
|
+
.filter((promise) => !awaited.has(promise));
|
|
2058
2135
|
if (pending.length === 0)
|
|
2059
2136
|
break;
|
|
2060
2137
|
for (const promise of pending)
|
|
@@ -2074,12 +2151,6 @@ class SystemDatabase {
|
|
|
2074
2151
|
if (this.runningWorkflowMap.size > 0) {
|
|
2075
2152
|
this.logger.warn(`Shutting down while ${this.runningWorkflowMap.size} workflows are still running: ${Array.from(this.runningWorkflowMap.keys()).join(', ')}`);
|
|
2076
2153
|
}
|
|
2077
|
-
if (this.workflowEventsMap.map.size > 0) {
|
|
2078
|
-
this.logger.warn('Workflow events map is not empty - shutdown is not clean.');
|
|
2079
|
-
}
|
|
2080
|
-
if (this.notificationsMap.map.size > 0) {
|
|
2081
|
-
this.logger.warn('Message notification map is not empty - shutdown is not clean.');
|
|
2082
|
-
}
|
|
2083
2154
|
}
|
|
2084
2155
|
/**
|
|
2085
2156
|
* Run a DB-backed polling read under the polling concurrency limiter, so that
|
|
@@ -2306,6 +2377,23 @@ class SystemDatabase {
|
|
|
2306
2377
|
}
|
|
2307
2378
|
return await this.sendDirectStandalone(destinationID, message, topic, serialization, idempotencyKey);
|
|
2308
2379
|
}
|
|
2380
|
+
/** A send from inside a step of `workflowID`: not recorded, but it lands only while the caller still owns the workflow. */
|
|
2381
|
+
async sendFromStep(workflowID, destinationID, message, topic, serialization, idempotencyKey) {
|
|
2382
|
+
const ownerXid = (0, context_1.currentOwnerXid)(workflowID);
|
|
2383
|
+
const client = await this.#connect();
|
|
2384
|
+
try {
|
|
2385
|
+
await this.#inTransaction(client, async () => {
|
|
2386
|
+
await this.#sendDirectInternal(client, destinationID, message, topic, serialization, idempotencyKey);
|
|
2387
|
+
// After the insert, in the order the recorded send locks, so they cannot deadlock.
|
|
2388
|
+
if (ownerXid !== undefined) {
|
|
2389
|
+
await this.#checkOwner(client, workflowID, ownerXid);
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
finally {
|
|
2394
|
+
client.release();
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2309
2397
|
async sendDirectStandalone(destinationID, message, topic, serialization, idempotencyKey) {
|
|
2310
2398
|
return await this.#sendDirectInternal(this.pool, destinationID, message, topic, serialization, idempotencyKey);
|
|
2311
2399
|
}
|
|
@@ -2527,27 +2615,41 @@ class SystemDatabase {
|
|
|
2527
2615
|
}
|
|
2528
2616
|
// ==================== Streams ====================
|
|
2529
2617
|
async writeStreamFromStep(workflowID, functionID, key, serializedValue, serialization) {
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2618
|
+
const ownerXid = (0, context_1.currentOwnerXid)(workflowID);
|
|
2619
|
+
const client = await this.#connect();
|
|
2620
|
+
try {
|
|
2621
|
+
while (true) {
|
|
2622
|
+
try {
|
|
2623
|
+
await this.#inTransaction(client, async () => {
|
|
2624
|
+
// Derives the first unused offset inside the insert; two writers can still pick the same one.
|
|
2625
|
+
await client.query(`INSERT INTO "${this.schemaName}".streams (workflow_uuid, key, value, "offset", function_id, serialization)
|
|
2626
|
+
SELECT $1::text, $2::text, $3::text, COALESCE(MAX(s."offset"), -1) + 1, $4::int, $5::text
|
|
2627
|
+
FROM "${this.schemaName}".streams s
|
|
2628
|
+
WHERE s.workflow_uuid = $1 AND s.key = $2`, [workflowID, key, serializedValue, functionID, serialization]);
|
|
2629
|
+
// After the insert, in the order the workflow-level writes lock, so they cannot deadlock.
|
|
2630
|
+
if (ownerXid !== undefined) {
|
|
2631
|
+
await this.#checkOwner(client, workflowID, ownerXid);
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2544
2634
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2635
|
+
catch (e) {
|
|
2636
|
+
// Only an offset conflict resolves on retry; anything else would spin forever.
|
|
2637
|
+
if (e instanceof pg_1.DatabaseError && e.code === '23505') {
|
|
2638
|
+
this.logger.warn(`Stream offset conflict for workflow ${workflowID}, key ${key}; retrying`);
|
|
2639
|
+
await (0, utils_1.sleepms)(100);
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
if (!(e instanceof error_1.DBOSWorkflowConflictError))
|
|
2643
|
+
this.logger.error(e);
|
|
2644
|
+
throw e;
|
|
2645
|
+
}
|
|
2646
|
+
// Notify only after commit, so a woken reader sees the value.
|
|
2647
|
+
this.#signalNotification(exports.DBOS_STREAMS_CHANNEL, `${workflowID}::${key}`);
|
|
2648
|
+
return;
|
|
2547
2649
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2650
|
+
}
|
|
2651
|
+
finally {
|
|
2652
|
+
client.release();
|
|
2551
2653
|
}
|
|
2552
2654
|
}
|
|
2553
2655
|
async writeStreamFromWorkflow(workflowID, functionID, key, serializedValue, serialization, functionName) {
|
|
@@ -2763,7 +2865,7 @@ class SystemDatabase {
|
|
|
2763
2865
|
// Literal statuses let the planner match idx_workflow_status_deadline under any plan mode.
|
|
2764
2866
|
// SKIP LOCKED leaves a row a dequeue or a peer's sweep holds for the next sweep.
|
|
2765
2867
|
const { rows } = await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
2766
|
-
SET status = $1, queue_name = NULL, deduplication_id = NULL, started_at_epoch_ms = NULL,
|
|
2868
|
+
SET status = $1, owner_xid = NULL, queue_name = NULL, deduplication_id = NULL, started_at_epoch_ms = NULL,
|
|
2767
2869
|
updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint,
|
|
2768
2870
|
completed_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint
|
|
2769
2871
|
WHERE workflow_uuid IN (
|
|
@@ -2805,7 +2907,9 @@ class SystemDatabase {
|
|
|
2805
2907
|
SELECT pk FROM partitions WHERE pk IS NOT NULL`, params);
|
|
2806
2908
|
return rows.map((row) => row.pk);
|
|
2807
2909
|
}
|
|
2808
|
-
async findAndMarkStartableWorkflows(queue, executorID, appVersion, queuePartitionKey, localRunningCount = 0, partitionLocalRunningCount = 0
|
|
2910
|
+
async findAndMarkStartableWorkflows(queue, executorID, appVersion, queuePartitionKey, localRunningCount = 0, partitionLocalRunningCount = 0,
|
|
2911
|
+
// Written to every claimed row: the dispatches of this claim own their executions.
|
|
2912
|
+
ownerXid = null) {
|
|
2809
2913
|
const claimedIDs = [];
|
|
2810
2914
|
const partitionParams = queuePartitionKey !== undefined ? [queuePartitionKey] : [];
|
|
2811
2915
|
// Shares a concurrency or rate limit budget with other executors.
|
|
@@ -2938,12 +3042,14 @@ class SystemDatabase {
|
|
|
2938
3042
|
workflow_1.StatusString.ENQUEUED,
|
|
2939
3043
|
// Claim an unclaimed row for this application; a nameless dequeuer leaves ownership untouched.
|
|
2940
3044
|
this.appName ?? null,
|
|
3045
|
+
ownerXid,
|
|
2941
3046
|
];
|
|
2942
3047
|
// Re-check ownership alongside status, as the partitioned claim guard does.
|
|
2943
3048
|
const claimScope = this.#appNameFilter('application_name', this.appName, updateParams);
|
|
2944
3049
|
// RETURNING reports exactly the rows this statement flipped, so a row another worker won is absent.
|
|
2945
3050
|
const flippedResult = await client.query(`UPDATE "${this.schemaName}".workflow_status
|
|
2946
3051
|
SET status = $1,
|
|
3052
|
+
owner_xid = $8,
|
|
2947
3053
|
executor_id = $2,
|
|
2948
3054
|
application_version = $3,
|
|
2949
3055
|
started_at_epoch_ms = (EXTRACT(epoch FROM now()) * 1000)::bigint,
|
|
@@ -2979,7 +3085,9 @@ class SystemDatabase {
|
|
|
2979
3085
|
/** Max heads admitted per sweep: bounds dispatch, not the partition walk; an unconstrained sweep takes the lowest keys, so higher keys can wait under sustained load. */
|
|
2980
3086
|
partitionedDequeueSweepCap = 8192;
|
|
2981
3087
|
/** Dequeue each partition's head-of-line workflow in one transaction, at most {@link partitionedDequeueSweepCap} per sweep; only valid for partition-concurrency-1 queues with no queue-wide limit. */
|
|
2982
|
-
async findAndMarkStartablePartitionedWorkflows(queue, executorID, appVersion, maxTasks = Infinity
|
|
3088
|
+
async findAndMarkStartablePartitionedWorkflows(queue, executorID, appVersion, maxTasks = Infinity,
|
|
3089
|
+
// Written to every claimed row: the dispatches of this claim own their executions.
|
|
3090
|
+
ownerXid = null) {
|
|
2983
3091
|
if (queue.partitionConcurrency !== 1 ||
|
|
2984
3092
|
queue.concurrency !== undefined ||
|
|
2985
3093
|
queue.rateLimit !== undefined ||
|
|
@@ -3085,11 +3193,13 @@ class SystemDatabase {
|
|
|
3085
3193
|
appVersion,
|
|
3086
3194
|
// Claim the row, as the unpartitioned dequeue does.
|
|
3087
3195
|
this.appName ?? null,
|
|
3196
|
+
ownerXid,
|
|
3088
3197
|
];
|
|
3089
3198
|
const flipScope = this.#appNameFilter('application_name', this.appName, flipParams);
|
|
3090
3199
|
// Start the workflows by marking them PENDING; RETURNING reports exactly the rows this statement flipped.
|
|
3091
3200
|
const flippedResult = await client.query(`UPDATE "${this.schemaName}".workflow_status
|
|
3092
3201
|
SET status = $1,
|
|
3202
|
+
owner_xid = $8,
|
|
3093
3203
|
executor_id = $2,
|
|
3094
3204
|
application_version = $6,
|
|
3095
3205
|
started_at_epoch_ms = (EXTRACT(epoch FROM now()) * 1000)::bigint,
|
|
@@ -4429,7 +4539,7 @@ class SystemDatabase {
|
|
|
4429
4539
|
return { queues, schedules, versions, workflows: inFlight + terminal, steps };
|
|
4430
4540
|
}
|
|
4431
4541
|
// ==================== Internal ====================
|
|
4432
|
-
async insertWorkflowStatus(client, initStatus,
|
|
4542
|
+
async insertWorkflowStatus(client, initStatus, creatorXid) {
|
|
4433
4543
|
try {
|
|
4434
4544
|
const { rows } = await client.query(`INSERT INTO "${this.schemaName}".workflow_status (
|
|
4435
4545
|
workflow_uuid,
|
|
@@ -4454,18 +4564,19 @@ class SystemDatabase {
|
|
|
4454
4564
|
forked_from,
|
|
4455
4565
|
parent_workflow_id,
|
|
4456
4566
|
serialization,
|
|
4457
|
-
|
|
4567
|
+
creator_xid,
|
|
4458
4568
|
delay_until_epoch_ms,
|
|
4459
4569
|
attributes,
|
|
4460
4570
|
schedule_name,
|
|
4461
4571
|
debounce_deadline_epoch_ms,
|
|
4462
4572
|
is_debounced,
|
|
4463
|
-
application_name
|
|
4464
|
-
|
|
4573
|
+
application_name,
|
|
4574
|
+
owner_xid
|
|
4575
|
+
) 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)
|
|
4465
4576
|
ON CONFLICT (workflow_uuid)
|
|
4466
4577
|
-- A no-op update, so an existing row comes back unchanged for the caller to inspect.
|
|
4467
|
-
DO UPDATE SET
|
|
4468
|
-
RETURNING status, name, class_name, config_name, queue_name, executor_id,
|
|
4578
|
+
DO UPDATE SET creator_xid = workflow_status.creator_xid
|
|
4579
|
+
RETURNING status, name, class_name, config_name, queue_name, executor_id, creator_xid, serialization`, [
|
|
4469
4580
|
initStatus.workflowUUID,
|
|
4470
4581
|
initStatus.status,
|
|
4471
4582
|
initStatus.workflowName,
|
|
@@ -4490,13 +4601,15 @@ class SystemDatabase {
|
|
|
4490
4601
|
initStatus.forkedFrom ?? null,
|
|
4491
4602
|
initStatus.parentWorkflowID ?? null,
|
|
4492
4603
|
initStatus.serialization,
|
|
4493
|
-
|
|
4604
|
+
creatorXid,
|
|
4494
4605
|
initStatus.delayUntilEpochMS ?? null,
|
|
4495
4606
|
initStatus.attributes ? JSON.stringify(initStatus.attributes) : null,
|
|
4496
4607
|
initStatus.scheduleName ?? null,
|
|
4497
4608
|
initStatus.debounceDeadlineEpochMS ?? null,
|
|
4498
4609
|
initStatus.isDebounced ?? false,
|
|
4499
4610
|
initStatus.applicationName ?? null,
|
|
4611
|
+
// A direct start runs at once, so its creator token also owns the execution.
|
|
4612
|
+
initStatus.status === workflow_1.StatusString.PENDING ? creatorXid : null,
|
|
4500
4613
|
]);
|
|
4501
4614
|
if (rows.length === 0) {
|
|
4502
4615
|
throw new Error(`Attempt to insert workflow ${initStatus.workflowUUID} failed`);
|
|
@@ -4561,6 +4674,10 @@ class SystemDatabase {
|
|
|
4561
4674
|
else if (update.clearCompletedAt) {
|
|
4562
4675
|
setClause += `, completed_at = NULL`;
|
|
4563
4676
|
}
|
|
4677
|
+
if (update.ownerXid !== undefined) {
|
|
4678
|
+
const param = args.push(update.ownerXid);
|
|
4679
|
+
setClause += `, owner_xid=$${param}`;
|
|
4680
|
+
}
|
|
4564
4681
|
const where = options.where ?? {};
|
|
4565
4682
|
if (where.status) {
|
|
4566
4683
|
const param = args.push(where.status);
|
|
@@ -4570,6 +4687,10 @@ class SystemDatabase {
|
|
|
4570
4687
|
const param = args.push(where.notStatus);
|
|
4571
4688
|
whereClause += ` AND status!=$${param}`;
|
|
4572
4689
|
}
|
|
4690
|
+
if (where.ownerXid !== undefined) {
|
|
4691
|
+
const param = args.push(where.ownerXid);
|
|
4692
|
+
whereClause += ` AND owner_xid=$${param}`;
|
|
4693
|
+
}
|
|
4573
4694
|
const result = await client.query(`UPDATE "${this.schemaName}".workflow_status ${setClause} ${whereClause}`, args);
|
|
4574
4695
|
const throwOnFailure = options.throwOnFailure ?? true;
|
|
4575
4696
|
if (throwOnFailure && result.rowCount !== 1) {
|
|
@@ -4578,6 +4699,10 @@ class SystemDatabase {
|
|
|
4578
4699
|
return result.rowCount ?? 0;
|
|
4579
4700
|
}
|
|
4580
4701
|
async recordOperationResultInternal(client, workflowID, functionID, functionName, checkConflict, startTimeEpochMs, endTimeEpochMs, options = {}) {
|
|
4702
|
+
const ownerXid = options.ownerChecked ? undefined : (0, context_1.currentOwnerXid)(workflowID);
|
|
4703
|
+
if (ownerXid !== undefined) {
|
|
4704
|
+
await this.#checkOwner(client, workflowID, ownerXid);
|
|
4705
|
+
}
|
|
4581
4706
|
try {
|
|
4582
4707
|
const out = await client.query(`INSERT INTO "${this.schemaName}".operation_outputs
|
|
4583
4708
|
(workflow_uuid, function_id, output, error, function_name, child_workflow_id, started_at_epoch_ms, completed_at_epoch_ms, serialization, application_name, retention_timestamp)
|
|
@@ -4597,30 +4722,47 @@ class SystemDatabase {
|
|
|
4597
4722
|
// Mirrors the parent: only the running application records its steps.
|
|
4598
4723
|
this.appName ?? null,
|
|
4599
4724
|
]);
|
|
4725
|
+
// A row already there with another completion time is this execution's own doing
|
|
4726
|
+
// (a retry carries the same time), so it is nondeterminism, not a duplicate.
|
|
4600
4727
|
if (checkConflict &&
|
|
4601
4728
|
(out?.rowCount ?? 0) > 0 &&
|
|
4602
4729
|
Number(out?.rows?.[0]?.completed_at_epoch_ms) !== endTimeEpochMs) {
|
|
4603
|
-
|
|
4604
|
-
throw new error_1.DBOSWorkflowConflictError(workflowID);
|
|
4605
|
-
}
|
|
4606
|
-
if (Number(out?.rows?.[0]?.completed_at_epoch_ms) === endTimeEpochMs) {
|
|
4607
|
-
// Winning the checkpoint proves this executor is advancing the workflow:
|
|
4608
|
-
// claim the executor_id marker (skips the write when already ours).
|
|
4609
|
-
await client.query(`UPDATE "${this.schemaName}".workflow_status
|
|
4610
|
-
SET executor_id = $1
|
|
4611
|
-
WHERE workflow_uuid = $2 AND executor_id IS DISTINCT FROM $1`, [utils_1.globalParams.executorID, workflowID]);
|
|
4730
|
+
throw new error_1.DBOSStepNondeterminismError(workflowID, functionID);
|
|
4612
4731
|
}
|
|
4613
4732
|
}
|
|
4614
4733
|
catch (error) {
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
// Serialization and primary key conflict (Postgres).
|
|
4618
|
-
throw new error_1.DBOSWorkflowConflictError(workflowID);
|
|
4619
|
-
}
|
|
4620
|
-
else {
|
|
4621
|
-
throw err;
|
|
4734
|
+
if (error.code === '23505') {
|
|
4735
|
+
throw new error_1.DBOSStepNondeterminismError(workflowID, functionID);
|
|
4622
4736
|
}
|
|
4737
|
+
throw error;
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
/** Run `fn` in a READ COMMITTED transaction on `client`, so an ownership check holds its row lock until the write commits. */
|
|
4741
|
+
async #inTransaction(client, fn) {
|
|
4742
|
+
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
4743
|
+
let result;
|
|
4744
|
+
try {
|
|
4745
|
+
result = await fn();
|
|
4746
|
+
}
|
|
4747
|
+
catch (e) {
|
|
4748
|
+
// Swallowed: a rollback failure must not mask why we are rolling back.
|
|
4749
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
4750
|
+
throw e;
|
|
4623
4751
|
}
|
|
4752
|
+
await client.query('COMMIT');
|
|
4753
|
+
return result;
|
|
4754
|
+
}
|
|
4755
|
+
/** Throw DBOSWorkflowConflictError unless ownerXid still owns the workflow; the row lock holds until commit, so no hand-off lands in between. */
|
|
4756
|
+
async #checkOwner(client, workflowID, ownerXid) {
|
|
4757
|
+
const { rows } = await client.query(`SELECT owner_xid FROM "${this.schemaName}".workflow_status WHERE workflow_uuid = $1 FOR NO KEY UPDATE`, [workflowID]);
|
|
4758
|
+
if (rows[0]?.owner_xid !== ownerXid) {
|
|
4759
|
+
throw new error_1.DBOSWorkflowConflictError(workflowID);
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
/** The workflow's current ownership token; null if unowned or missing. */
|
|
4763
|
+
async getWorkflowOwner(workflowID) {
|
|
4764
|
+
const { rows } = await this.pool.query(`SELECT owner_xid FROM "${this.schemaName}".workflow_status WHERE workflow_uuid = $1`, [workflowID]);
|
|
4765
|
+
return rows[0]?.owner_xid ?? null;
|
|
4624
4766
|
}
|
|
4625
4767
|
async #getOperationResultAndThrowIfCancelled(client, workflowID, functionID) {
|
|
4626
4768
|
await this.#checkIfCanceled(client, workflowID);
|
|
@@ -4682,10 +4824,10 @@ class SystemDatabase {
|
|
|
4682
4824
|
}
|
|
4683
4825
|
return JSON.parse(res.output);
|
|
4684
4826
|
}
|
|
4685
|
-
await this.recordOperationResultInternal(client, workflowID, functionID, exports.DBOS_FUNCNAME_SLEEP, false, startTimeMs, recordCompletionAtDeadline ? endTimeMs : startTimeMs, {
|
|
4827
|
+
await this.#inTransaction(client, () => this.recordOperationResultInternal(client, workflowID, functionID, exports.DBOS_FUNCNAME_SLEEP, false, startTimeMs, recordCompletionAtDeadline ? endTimeMs : startTimeMs, {
|
|
4686
4828
|
output: serialization_1.DBOSPortableJSON.stringify(endTimeMs),
|
|
4687
4829
|
serialization: serialization_1.DBOSPortableJSON.name(),
|
|
4688
|
-
});
|
|
4830
|
+
}));
|
|
4689
4831
|
return endTimeMs;
|
|
4690
4832
|
}
|
|
4691
4833
|
finally {
|
|
@@ -4838,13 +4980,13 @@ __decorate([
|
|
|
4838
4980
|
__decorate([
|
|
4839
4981
|
dbRetry(),
|
|
4840
4982
|
__metadata("design:type", Function),
|
|
4841
|
-
__metadata("design:paramtypes", [String, Object]),
|
|
4983
|
+
__metadata("design:paramtypes", [String, Object, String]),
|
|
4842
4984
|
__metadata("design:returntype", Promise)
|
|
4843
4985
|
], SystemDatabase.prototype, "recordWorkflowOutput", null);
|
|
4844
4986
|
__decorate([
|
|
4845
4987
|
dbRetry(),
|
|
4846
4988
|
__metadata("design:type", Function),
|
|
4847
|
-
__metadata("design:paramtypes", [String, Object]),
|
|
4989
|
+
__metadata("design:paramtypes", [String, Object, String]),
|
|
4848
4990
|
__metadata("design:returntype", Promise)
|
|
4849
4991
|
], SystemDatabase.prototype, "recordWorkflowError", null);
|
|
4850
4992
|
__decorate([
|
|
@@ -4871,6 +5013,12 @@ __decorate([
|
|
|
4871
5013
|
__metadata("design:paramtypes", [String, Number, String, Boolean, Number, Number, Object]),
|
|
4872
5014
|
__metadata("design:returntype", Promise)
|
|
4873
5015
|
], SystemDatabase.prototype, "recordOperationResult", null);
|
|
5016
|
+
__decorate([
|
|
5017
|
+
dbRetry(),
|
|
5018
|
+
__metadata("design:type", Function),
|
|
5019
|
+
__metadata("design:paramtypes", [String, Number, String, Function]),
|
|
5020
|
+
__metadata("design:returntype", Promise)
|
|
5021
|
+
], SystemDatabase.prototype, "runTransactionalStep", null);
|
|
4874
5022
|
__decorate([
|
|
4875
5023
|
dbRetry(),
|
|
4876
5024
|
__metadata("design:type", Function),
|
|
@@ -4931,6 +5079,12 @@ __decorate([
|
|
|
4931
5079
|
__metadata("design:paramtypes", [String, Number, String, Object, Object, Object, String]),
|
|
4932
5080
|
__metadata("design:returntype", Promise)
|
|
4933
5081
|
], SystemDatabase.prototype, "send", null);
|
|
5082
|
+
__decorate([
|
|
5083
|
+
dbRetry(),
|
|
5084
|
+
__metadata("design:type", Function),
|
|
5085
|
+
__metadata("design:paramtypes", [String, String, Object, Object, Object, String]),
|
|
5086
|
+
__metadata("design:returntype", Promise)
|
|
5087
|
+
], SystemDatabase.prototype, "sendFromStep", null);
|
|
4934
5088
|
__decorate([
|
|
4935
5089
|
dbRetry(),
|
|
4936
5090
|
__metadata("design:type", Function),
|
|
@@ -5009,4 +5163,10 @@ __decorate([
|
|
|
5009
5163
|
__metadata("design:paramtypes", [String]),
|
|
5010
5164
|
__metadata("design:returntype", Promise)
|
|
5011
5165
|
], SystemDatabase.prototype, "getLatestApplicationVersion", null);
|
|
5166
|
+
__decorate([
|
|
5167
|
+
dbRetry(),
|
|
5168
|
+
__metadata("design:type", Function),
|
|
5169
|
+
__metadata("design:paramtypes", [String]),
|
|
5170
|
+
__metadata("design:returntype", Promise)
|
|
5171
|
+
], SystemDatabase.prototype, "getWorkflowOwner", null);
|
|
5012
5172
|
//# sourceMappingURL=system_database.js.map
|