@dbos-inc/drizzle-datasource 4.26.5-preview → 4.26.7-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/index.d.ts.map +1 -1
- package/dist/index.js +36 -31
- package/dist/index.js.map +1 -1
- package/index.ts +43 -31
- package/package.json +1 -1
- package/tests/datasource.test.ts +93 -0
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAQ,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAUL,cAAc,EAIf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAW,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAQ1D,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,mBAAmB,EAAE,gBAAgB,GAAG,YAAY,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAI/G,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAwND,qBAAa,iBAAiB,CAAC,EAAE,GAAG,cAAc,CAAC;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC,CAC3E,YAAW,cAAc,CAAC,iBAAiB,CAAC;;IA+C1C,QAAQ,CAAC,IAAI,EAAE,MAAM;IA9BvB,MAAM,KAAK,MAAM;;OAEhB;IAED,IAAI,MAAM,OAET;WAEY,oBAAoB,CAC/B,cAAc,EAAE,YAAY,GAAG,MAAM,GAAG,UAAU,EAClD,UAAU,GAAE,MAAe,GAC1B,OAAO,CAAC,IAAI,CAAC;gBAmBL,IAAI,EAAE,MAAM,EACrB,YAAY,EAAE,UAAU,GAAG,IAAI,EAC/B,QAAQ,GAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAO,EACxC,UAAU,GAAE,MAAe;IAMvB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,iBAAiB;IAI1E,mBAAmB,CAAC,IAAI,EAAE,IAAI,SAAS,OAAO,EAAE,EAAE,MAAM,EACtD,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,EACpD,MAAM,CAAC,EAAE,iBAAiB,GAAG,YAAY,GACxC,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC;IAKjD,WAAW,CAAC,MAAM,CAAC,EAAE,iBAAiB,kDAI1B,MAAM,eACD,WAAW,cACZ,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,QAAQ,MAAM,CAAC,CAAC,oCAAxC,IAAI,WAAW,IAAI,KAAK,QAAQ,MAAM,CAAC;CAevF"}
|
package/dist/index.js
CHANGED
|
@@ -76,36 +76,34 @@ class DrizzleTransactionHandler {
|
|
|
76
76
|
return error !== null ? { error } : { output };
|
|
77
77
|
}
|
|
78
78
|
static async #recordOutput(client, workflowID, stepID, output, schemaName) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
throw new dbos_sdk_1.DBOSWorkflowConflictError(workflowID);
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
79
|
+
const statement = (0, drizzle_orm_1.sql) `
|
|
80
|
+
INSERT INTO ${drizzle_orm_1.sql.identifier(schemaName)}.transaction_completion (workflow_id, function_num, output)
|
|
81
|
+
VALUES (${workflowID}, ${stepID}, ${output})
|
|
82
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
83
|
+
RETURNING workflow_id`;
|
|
84
|
+
const { rows } = await client.execute(statement);
|
|
85
|
+
if (rows.length === 0) {
|
|
86
|
+
throw new datasource_1.DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
92
87
|
}
|
|
93
88
|
}
|
|
94
89
|
async #recordError(workflowID, stepID, error) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
const statement = (0, drizzle_orm_1.sql) `
|
|
91
|
+
INSERT INTO ${drizzle_orm_1.sql.identifier(this.schemaName)}.transaction_completion (workflow_id, function_num, error)
|
|
92
|
+
VALUES (${workflowID}, ${stepID}, ${error})
|
|
93
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
94
|
+
RETURNING workflow_id`;
|
|
95
|
+
const { rows } = await this.#drizzle.execute(statement);
|
|
96
|
+
if (rows.length === 0) {
|
|
97
|
+
throw new datasource_1.DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
100
98
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
99
|
+
}
|
|
100
|
+
// A duplicate execution won the race, so its recorded outcome is the durable one.
|
|
101
|
+
async #replayConflictingStep(workflowID, stepID) {
|
|
102
|
+
const recorded = await this.#checkExecution(workflowID, stepID);
|
|
103
|
+
if (recorded === undefined) {
|
|
104
|
+
throw new datasource_1.DBOSError(`Step ${stepID} of workflow ${workflowID} conflicted with a concurrent execution, but no recorded outcome was found`);
|
|
108
105
|
}
|
|
106
|
+
return (0, datasource_1.replayRecordedStep)(recorded);
|
|
109
107
|
}
|
|
110
108
|
/* Invoke a transaction function, called by the framework */
|
|
111
109
|
async invokeTransactionFunction(config, target, func, ...args) {
|
|
@@ -124,11 +122,7 @@ class DrizzleTransactionHandler {
|
|
|
124
122
|
// Check to see if this tx has already been executed
|
|
125
123
|
const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID) : undefined;
|
|
126
124
|
if (previousResult) {
|
|
127
|
-
|
|
128
|
-
if ('error' in previousResult) {
|
|
129
|
-
throw superjson_1.SuperJSON.parse(previousResult.error);
|
|
130
|
-
}
|
|
131
|
-
return (previousResult.output ? superjson_1.SuperJSON.parse(previousResult.output) : null);
|
|
125
|
+
return (0, datasource_1.replayRecordedStep)(previousResult);
|
|
132
126
|
}
|
|
133
127
|
try {
|
|
134
128
|
const result = await this.#drizzle.transaction(async (client) => {
|
|
@@ -145,6 +139,9 @@ class DrizzleTransactionHandler {
|
|
|
145
139
|
return result;
|
|
146
140
|
}
|
|
147
141
|
catch (error) {
|
|
142
|
+
if (saveResults && error instanceof datasource_1.DBOSStepAlreadyRecordedError) {
|
|
143
|
+
return await this.#replayConflictingStep(workflowID, stepID);
|
|
144
|
+
}
|
|
148
145
|
if ((0, datasource_1.isPGRetriableTransactionError)(error)) {
|
|
149
146
|
dbos_sdk_1.DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
|
|
150
147
|
// Retry serialization failures.
|
|
@@ -155,7 +152,15 @@ class DrizzleTransactionHandler {
|
|
|
155
152
|
else {
|
|
156
153
|
if (saveResults) {
|
|
157
154
|
const message = superjson_1.SuperJSON.stringify(error);
|
|
158
|
-
|
|
155
|
+
try {
|
|
156
|
+
await this.#recordError(workflowID, stepID, message);
|
|
157
|
+
}
|
|
158
|
+
catch (recordError) {
|
|
159
|
+
if (recordError instanceof datasource_1.DBOSStepAlreadyRecordedError) {
|
|
160
|
+
return await this.#replayConflictingStep(workflowID, stepID);
|
|
161
|
+
}
|
|
162
|
+
throw recordError;
|
|
163
|
+
}
|
|
159
164
|
}
|
|
160
165
|
throw error;
|
|
161
166
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;AAAA,2BAAwE;AACxE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;AAAA,2BAAwE;AACxE,iDAAwD;AACxD,8DAcuC;AACvC,6DAAoE;AACpE,6CAAgD;AAChD,yCAAsC;AAEtC,6CAAkC;AASlC,MAAM,aAAa,GAAG,IAAI,+BAAiB,EAAmB,CAAC;AAc/D,MAAM,yBAAyB;IAOlB;IACQ;IACA;IARV,MAAM,GAAG,SAAS,CAAC;IAC5B,WAAW,CAAgC;IAClC,UAAU,CAAS;IACnB,iBAAiB,CAAU;IAEpC,YACW,IAAY,EACJ,YAA+B,EAC/B,WAAsC,EAAE,EACzD,aAAqB,MAAM;QAHlB,SAAI,GAAJ,IAAI,CAAQ;QACJ,iBAAY,GAAZ,YAAY,CAAmB;QAC/B,aAAQ,GAAR,QAAQ,CAAgC;QAGzD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,iBAAiB,GAAG,YAAY,YAAY,SAAI,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC;QAE9B,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,YAAY,SAAI,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,SAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACnG,MAAM,EAAE,GAAG,IAAA,uBAAO,EAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,WAAW,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC;QAC7F,MAAM,IAAI,EAAE,GAAG,EAAE,CAAC;QAElB,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,iBAAG,CAAC,GAAG,CAAC,IAAA,sCAAyB,EAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAY,CAAC;YAC/F,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;gBAC5B,CAAC,CAAE,GAAuC,CAAC,CAAC,CAAC;gBAC7C,CAAC,CAAC,CAAE,GAAkD,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;oBAC7D,GAAuC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjD,SAAS,GAAG,CAAC,CAAC,GAAG,EAAE,aAAa,IAAI,CAAC,CAAC,GAAG,EAAE,YAAY,CAAC;QAC1D,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CACb,4CAA4C,IAAI,CAAC,IAAI,oCAAqC,CAAW,CAAC,OAAO,EAAE,CAChH,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,EAAE,CAAC,OAAO,CAAC,iBAAG,CAAC,GAAG,CAAC,IAAA,gDAAmC,EAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBAChF,MAAM,EAAE,CAAC,OAAO,CAAC,iBAAG,CAAC,GAAG,CAAC,IAAA,+CAAkC,EAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YACjF,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CACb,4CAA4C,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC,UAAU;0GACkB,CACjG,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC;QAE9B,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;QAE7B,MAAM,IAAI,EAAE,GAAG,EAAE,CAAC;IACpB,CAAC;IAED,IAAI,QAAQ;QACV,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC,IAAI,sBAAsB,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,UAAkB,EAClB,MAAc;QAId,MAAM,SAAS,GAAG,IAAA,iBAAG,EAAA;oCACW,iBAAG,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC;8BACrC,UAAU,uBAAuB,MAAM,EAAE,CAAC;QACpE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAS,SAAS,CAAC,CAAC;QAE9D,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACzC,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;IACjD,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,aAAa,CACxB,MAAiD,EACjD,UAAkB,EAClB,MAAc,EACd,MAAc,EACd,UAAkB;QAElB,MAAM,SAAS,GAAG,IAAA,iBAAG,EAAA;oBACL,iBAAG,CAAC,UAAU,CAAC,UAAU,CAAC;gBAC9B,UAAU,KAAK,MAAM,KAAK,MAAM;;4BAEpB,CAAC;QACzB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,yCAA4B,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,UAAkB,EAAE,MAAc,EAAE,KAAa;QAClE,MAAM,SAAS,GAAG,IAAA,iBAAG,EAAA;oBACL,iBAAG,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC;gBACnC,UAAU,KAAK,MAAM,KAAK,KAAK;;4BAEnB,CAAC;QACzB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACxD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,yCAA4B,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,kFAAkF;IAClF,KAAK,CAAC,sBAAsB,CAAS,UAAkB,EAAE,MAAc;QACrE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAChE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,IAAI,sBAAS,CACjB,QAAQ,MAAM,gBAAgB,UAAU,4EAA4E,CACrH,CAAC;QACJ,CAAC;QACD,OAAO,IAAA,+BAAkB,EAAS,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,4DAA4D;IAC5D,KAAK,CAAC,yBAAyB,CAC7B,MAAqC,EACrC,MAAY,EACZ,IAAoD,EACpD,GAAG,IAAU;QAEb,MAAM,UAAU,GAAG,eAAI,CAAC,UAAU,CAAC;QACnC,MAAM,MAAM,GAAG,eAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,UAAU,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACrD,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;QACjE,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,EAAE,UAAU,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;QACnE,MAAM,WAAW,GAAG,CAAC,QAAQ,IAAI,UAAU,KAAK,SAAS,CAAC;QAE1D,4BAA4B;QAC5B,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,MAAM,aAAa,GAAG,GAAG,CAAC;QAC1B,MAAM,cAAc,GAAG,IAAI,CAAC,CAAC,0BAA0B;QAEvD,OAAO,IAAI,EAAE,CAAC;YACZ,oDAAoD;YACpD,MAAM,cAAc,GAAG,WAAW,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,MAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACjG,IAAI,cAAc,EAAE,CAAC;gBACnB,OAAO,IAAA,+BAAkB,EAAS,cAAc,CAAC,CAAC;YACpD,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,WAAW,CAC5C,KAAK,EAAE,MAAM,EAAE,EAAE;oBACf,sCAAsC;oBACtC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI,EAAE;wBACzE,OAAO,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;oBAC1C,CAAC,CAAC,CAAC;oBAEH,6CAA6C;oBAC7C,IAAI,WAAW,EAAE,CAAC;wBAChB,MAAM,yBAAyB,CAAC,aAAa,CAC3C,MAAM,EACN,UAAU,EACV,MAAO,EACP,qBAAS,CAAC,SAAS,CAAC,MAAM,CAAC,EAC3B,IAAI,CAAC,UAAU,CAChB,CAAC;oBACJ,CAAC;oBAED,OAAO,MAAM,CAAC;gBAChB,CAAC,EACD,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE,CAC3E,CAAC;gBAEF,OAAO,MAAM,CAAC;YAChB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,WAAW,IAAI,KAAK,YAAY,yCAA4B,EAAE,CAAC;oBACjE,OAAO,MAAM,IAAI,CAAC,sBAAsB,CAAS,UAAU,EAAE,MAAO,CAAC,CAAC;gBACxE,CAAC;gBACD,IAAI,IAAA,0CAA6B,EAAC,KAAK,CAAC,EAAE,CAAC;oBACzC,eAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,2BAA2B,EAAE,EAAE,eAAe,EAAE,WAAW,EAAE,EAAE,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC;oBACtG,gCAAgC;oBAChC,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;oBACjE,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,aAAa,EAAE,cAAc,CAAC,CAAC;oBACpE,SAAS;gBACX,CAAC;qBAAM,CAAC;oBACN,IAAI,WAAW,EAAE,CAAC;wBAChB,MAAM,OAAO,GAAG,qBAAS,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;wBAC3C,IAAI,CAAC;4BACH,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,MAAO,EAAE,OAAO,CAAC,CAAC;wBACxD,CAAC;wBAAC,OAAO,WAAW,EAAE,CAAC;4BACrB,IAAI,WAAW,YAAY,yCAA4B,EAAE,CAAC;gCACxD,OAAO,MAAM,IAAI,CAAC,sBAAsB,CAAS,UAAU,EAAE,MAAO,CAAC,CAAC;4BACxE,CAAC;4BACD,MAAM,WAAW,CAAC;wBACpB,CAAC;oBACH,CAAC;oBAED,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED,MAAa,iBAAiB;IAgDjB;IA7CX,mDAAmD;IACnD,MAAM,CAAC,UAAU,CAAC,CAA6B;QAC7C,IAAI,CAAC,eAAI,CAAC,eAAe,EAAE,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;QAC3F,CAAC;QACD,MAAM,GAAG,GAAG,aAAa,CAAC,QAAQ,EAAE,CAAC;QACrC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;QAC3F,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,KAAK,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;QAC/F,CAAC;QACD,OAAO,GAAG,CAAC,MAAM,CAAC;IACpB,CAAC;IAED,MAAM,KAAK,MAAM;QACf,OAAO,iBAAiB,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,MAAM;QACR,OAAO,iBAAiB,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAO,CAAC;IAC5D,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAC/B,cAAkD,EAClD,aAAqB,MAAM;QAE3B,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,OAAO,IAAI,cAAc,EAAE,CAAC;YACpE,MAAM,cAAc,CAAC,KAAK,CAAC,IAAA,gDAAmC,EAAC,UAAU,CAAC,CAAC,CAAC;YAC5E,MAAM,cAAc,CAAC,KAAK,CAAC,IAAA,+CAAkC,EAAC,UAAU,CAAC,CAAC,CAAC;QAC7E,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,GAAG,IAAI,WAAM,CAAC,cAAc,CAAC,CAAC;YAC1C,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;gBACvB,MAAM,MAAM,CAAC,KAAK,CAAC,IAAA,gDAAmC,EAAC,UAAU,CAAC,CAAC,CAAC;gBACpE,MAAM,MAAM,CAAC,KAAK,CAAC,IAAA,+CAAkC,EAAC,UAAU,CAAC,CAAC,CAAC;YACrE,CAAC;oBAAS,CAAC;gBACT,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;IAED,SAAS,CAA4B;IAErC,YACW,IAAY,EACrB,YAA+B,EAC/B,WAAsC,EAAE,EACxC,aAAqB,MAAM;QAHlB,SAAI,GAAJ,IAAI,CAAQ;QAKrB,IAAI,CAAC,SAAS,GAAG,IAAI,yBAAyB,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;QACzF,IAAA,+BAAkB,EAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,cAAc,CAAI,IAAsB,EAAE,MAA0B;QACxE,OAAO,MAAM,IAAA,2BAAc,EAAC,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9F,CAAC;IAED,mBAAmB,CACjB,IAAoD,EACpD,MAAyC;QAEzC,OAAO,IAAA,gCAAmB,EAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtD,CAAC;IAED,YAAY;IACZ,WAAW,CAAC,MAA0B;QACpC,4DAA4D;QAC5D,MAAM,EAAE,GAAG,IAAI,CAAC;QAChB,OAAO,SAAS,SAAS,CACvB,MAAc,EACd,WAAwB,EACxB,UAAmF;YAEnF,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;gBACtB,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;YACxE,CAAC;YAED,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,mBAAmB,CAAC,UAAU,CAAC,KAAK,EAAE;gBAC1D,GAAG,MAAM;gBACT,WAAW,EAAE,MAAM;gBACnB,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,MAAM,CAAC,WAAW,CAAC;aAC1C,CAAC,CAAC;YAEH,OAAO,UAAU,CAAC;QACpB,CAAC,CAAC;IACJ,CAAC;CACF;AA1FD,8CA0FC"}
|
package/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Client, ClientConfig, Pool, PoolClient, PoolConfig } from 'pg';
|
|
2
|
-
import { DBOS,
|
|
2
|
+
import { DBOS, FunctionName } from '@dbos-inc/dbos-sdk';
|
|
3
3
|
import {
|
|
4
4
|
type DataSourceTransactionHandler,
|
|
5
5
|
createTransactionCompletionSchemaPG,
|
|
6
6
|
createTransactionCompletionTablePG,
|
|
7
7
|
isPGRetriableTransactionError,
|
|
8
|
-
|
|
8
|
+
DBOSError,
|
|
9
|
+
DBOSStepAlreadyRecordedError,
|
|
10
|
+
replayRecordedStep,
|
|
9
11
|
registerTransaction,
|
|
10
12
|
runTransaction,
|
|
11
13
|
DBOSDataSource,
|
|
@@ -132,33 +134,38 @@ class DrizzleTransactionHandler implements DataSourceTransactionHandler {
|
|
|
132
134
|
output: string,
|
|
133
135
|
schemaName: string,
|
|
134
136
|
): Promise<void> {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
} else {
|
|
144
|
-
throw error;
|
|
145
|
-
}
|
|
137
|
+
const statement = sql`
|
|
138
|
+
INSERT INTO ${sql.identifier(schemaName)}.transaction_completion (workflow_id, function_num, output)
|
|
139
|
+
VALUES (${workflowID}, ${stepID}, ${output})
|
|
140
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
141
|
+
RETURNING workflow_id`;
|
|
142
|
+
const { rows } = await client.execute(statement);
|
|
143
|
+
if (rows.length === 0) {
|
|
144
|
+
throw new DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
149
|
+
const statement = sql`
|
|
150
|
+
INSERT INTO ${sql.identifier(this.schemaName)}.transaction_completion (workflow_id, function_num, error)
|
|
151
|
+
VALUES (${workflowID}, ${stepID}, ${error})
|
|
152
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
153
|
+
RETURNING workflow_id`;
|
|
154
|
+
const { rows } = await this.#drizzle.execute(statement);
|
|
155
|
+
if (rows.length === 0) {
|
|
156
|
+
throw new DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// A duplicate execution won the race, so its recorded outcome is the durable one.
|
|
161
|
+
async #replayConflictingStep<Return>(workflowID: string, stepID: number): Promise<Return> {
|
|
162
|
+
const recorded = await this.#checkExecution(workflowID, stepID);
|
|
163
|
+
if (recorded === undefined) {
|
|
164
|
+
throw new DBOSError(
|
|
165
|
+
`Step ${stepID} of workflow ${workflowID} conflicted with a concurrent execution, but no recorded outcome was found`,
|
|
166
|
+
);
|
|
161
167
|
}
|
|
168
|
+
return replayRecordedStep<Return>(recorded);
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
/* Invoke a transaction function, called by the framework */
|
|
@@ -186,12 +193,7 @@ class DrizzleTransactionHandler implements DataSourceTransactionHandler {
|
|
|
186
193
|
// Check to see if this tx has already been executed
|
|
187
194
|
const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
|
|
188
195
|
if (previousResult) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if ('error' in previousResult) {
|
|
192
|
-
throw SuperJSON.parse(previousResult.error);
|
|
193
|
-
}
|
|
194
|
-
return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
|
|
196
|
+
return replayRecordedStep<Return>(previousResult);
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
try {
|
|
@@ -220,6 +222,9 @@ class DrizzleTransactionHandler implements DataSourceTransactionHandler {
|
|
|
220
222
|
|
|
221
223
|
return result;
|
|
222
224
|
} catch (error) {
|
|
225
|
+
if (saveResults && error instanceof DBOSStepAlreadyRecordedError) {
|
|
226
|
+
return await this.#replayConflictingStep<Return>(workflowID, stepID!);
|
|
227
|
+
}
|
|
223
228
|
if (isPGRetriableTransactionError(error)) {
|
|
224
229
|
DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
|
|
225
230
|
// Retry serialization failures.
|
|
@@ -229,7 +234,14 @@ class DrizzleTransactionHandler implements DataSourceTransactionHandler {
|
|
|
229
234
|
} else {
|
|
230
235
|
if (saveResults) {
|
|
231
236
|
const message = SuperJSON.stringify(error);
|
|
232
|
-
|
|
237
|
+
try {
|
|
238
|
+
await this.#recordError(workflowID, stepID!, message);
|
|
239
|
+
} catch (recordError) {
|
|
240
|
+
if (recordError instanceof DBOSStepAlreadyRecordedError) {
|
|
241
|
+
return await this.#replayConflictingStep<Return>(workflowID, stepID!);
|
|
242
|
+
}
|
|
243
|
+
throw recordError;
|
|
244
|
+
}
|
|
233
245
|
}
|
|
234
246
|
|
|
235
247
|
throw error;
|
package/package.json
CHANGED
package/tests/datasource.test.ts
CHANGED
|
@@ -23,6 +23,7 @@ async function createSchema(config: PoolConfig) {
|
|
|
23
23
|
await pool.query(
|
|
24
24
|
`CREATE TABLE IF NOT EXISTS greetings (name TEXT PRIMARY KEY NOT NULL, greet_count INTEGER DEFAULT 0)`,
|
|
25
25
|
);
|
|
26
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS race_side_effects (tag TEXT NOT NULL)`);
|
|
26
27
|
} finally {
|
|
27
28
|
await pool.end();
|
|
28
29
|
}
|
|
@@ -318,6 +319,57 @@ describe('DrizzleDataSource', () => {
|
|
|
318
319
|
]);
|
|
319
320
|
expect(txResults.rows.length).toBe(0);
|
|
320
321
|
});
|
|
322
|
+
|
|
323
|
+
test('duplicate execution replays the winner result', async () => {
|
|
324
|
+
await userDB.query('DELETE FROM race_side_effects');
|
|
325
|
+
raceState.callCount = 0;
|
|
326
|
+
|
|
327
|
+
// A loser whose body succeeds: the collision happens on the result-recording insert.
|
|
328
|
+
raceState.plantWinner = true;
|
|
329
|
+
raceState.plantWinnerError = false;
|
|
330
|
+
raceState.fail = false;
|
|
331
|
+
const wfid1 = randomUUID();
|
|
332
|
+
await expect(DBOS.withNextWorkflowID(wfid1, () => regRaceWorkflow())).resolves.toBe('winner-result');
|
|
333
|
+
expect(raceState.callCount).toBe(1); // the loser did run its body
|
|
334
|
+
|
|
335
|
+
// A loser whose body fails: the collision moves to the error-recording insert,
|
|
336
|
+
// and the winner's result still wins over the loser's own error.
|
|
337
|
+
raceState.plantWinner = true;
|
|
338
|
+
raceState.plantWinnerError = false;
|
|
339
|
+
raceState.fail = true;
|
|
340
|
+
const wfid2 = randomUUID();
|
|
341
|
+
await expect(DBOS.withNextWorkflowID(wfid2, () => regRaceWorkflow())).resolves.toBe('winner-result');
|
|
342
|
+
expect(raceState.callCount).toBe(2);
|
|
343
|
+
|
|
344
|
+
// A winner that recorded an error: the loser replays that throw instead of its own result.
|
|
345
|
+
raceState.plantWinner = true;
|
|
346
|
+
raceState.plantWinnerError = true;
|
|
347
|
+
raceState.fail = false;
|
|
348
|
+
const wfid3 = randomUUID();
|
|
349
|
+
const winnerError = await throws(() => DBOS.withNextWorkflowID(wfid3, () => regRaceWorkflow()));
|
|
350
|
+
expect((winnerError as Error).message).toBe('winner-error');
|
|
351
|
+
expect(raceState.callCount).toBe(3);
|
|
352
|
+
|
|
353
|
+
// Every loser's writes were discarded and the winners' records still stand.
|
|
354
|
+
const { rows: tags } = await userDB.query<{ tag: string }>('SELECT tag FROM race_side_effects');
|
|
355
|
+
expect(tags).toHaveLength(0);
|
|
356
|
+
const { rows: txOutput } = await userDB.query<transaction_completion>(
|
|
357
|
+
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = ANY($1)',
|
|
358
|
+
[[wfid1, wfid2]],
|
|
359
|
+
);
|
|
360
|
+
expect(txOutput).toHaveLength(2);
|
|
361
|
+
for (const row of txOutput) {
|
|
362
|
+
expect(row.error).toBeNull(); // no loser error was ever recorded
|
|
363
|
+
expect(SuperJSON.parse(row.output!)).toBe('winner-result');
|
|
364
|
+
}
|
|
365
|
+
const { rows: txError } = await userDB.query<transaction_completion>(
|
|
366
|
+
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
367
|
+
[wfid3],
|
|
368
|
+
);
|
|
369
|
+
expect(txError).toHaveLength(1);
|
|
370
|
+
expect(txError[0].output).toBeNull(); // the loser never overwrote the winner's error with its own result
|
|
371
|
+
expect(SuperJSON.parse<Error>(txError[0].error!).message).toBe('winner-error');
|
|
372
|
+
});
|
|
321
373
|
});
|
|
322
374
|
|
|
323
375
|
const greetingsTable = pgTable('greetings', {
|
|
@@ -366,6 +418,47 @@ async function errorFunction(user: string) {
|
|
|
366
418
|
throw new Error(`test error ${Date.now()}`);
|
|
367
419
|
}
|
|
368
420
|
|
|
421
|
+
const raceState = { callCount: 0, plantWinner: false, plantWinnerError: false, fail: false };
|
|
422
|
+
|
|
423
|
+
async function raceFunction() {
|
|
424
|
+
raceState.callCount += 1;
|
|
425
|
+
await dataSource.client.execute(sql`INSERT INTO race_side_effects(tag) VALUES (${`run-${raceState.callCount}`})`);
|
|
426
|
+
if (raceState.plantWinner) {
|
|
427
|
+
raceState.plantWinner = false;
|
|
428
|
+
await plantWinnerCompletion();
|
|
429
|
+
}
|
|
430
|
+
if (raceState.fail) {
|
|
431
|
+
throw new Error("loser's own failure");
|
|
432
|
+
}
|
|
433
|
+
return `result-${raceState.callCount}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// A duplicate execution commits its result while this transaction is still open, so this
|
|
437
|
+
// execution's pre-check missed it but its completion insert will collide with it.
|
|
438
|
+
async function plantWinnerCompletion() {
|
|
439
|
+
const winner = new Client(config);
|
|
440
|
+
try {
|
|
441
|
+
await winner.connect();
|
|
442
|
+
const [column, value] = raceState.plantWinnerError
|
|
443
|
+
? ['error', SuperJSON.stringify(new Error('winner-error'))]
|
|
444
|
+
: ['output', SuperJSON.stringify('winner-result')];
|
|
445
|
+
await winner.query(
|
|
446
|
+
`INSERT INTO dbos.transaction_completion (workflow_id, function_num, ${column}) VALUES ($1, $2, $3)`,
|
|
447
|
+
[DBOS.workflowID, DBOS.stepID, value],
|
|
448
|
+
);
|
|
449
|
+
} finally {
|
|
450
|
+
await winner.end();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const regRaceFunction = dataSource.registerTransaction(raceFunction);
|
|
455
|
+
|
|
456
|
+
async function raceWorkflow() {
|
|
457
|
+
return await regRaceFunction();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const regRaceWorkflow = DBOS.registerWorkflow(raceWorkflow);
|
|
461
|
+
|
|
369
462
|
async function readFunction(user: string) {
|
|
370
463
|
const result = await dataSource.client
|
|
371
464
|
.select({ greet_count: greetingsTable.greet_count })
|