rivetkit 2.3.23 → 2.3.24-rc.1
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/browser/client.d.ts +2 -0
- package/dist/browser/client.js +1 -1
- package/dist/browser/client.js.map +1 -1
- package/dist/browser/inspector/client.js +1 -1
- package/dist/browser/inspector/client.js.map +1 -1
- package/dist/tsup/agent-os/index.cjs +1 -1
- package/dist/tsup/agent-os/index.cjs.map +1 -1
- package/dist/tsup/agent-os/index.d.cts +2 -0
- package/dist/tsup/agent-os/index.d.ts +2 -0
- package/dist/tsup/agent-os/index.js +1 -1
- package/dist/tsup/agent-os/index.js.map +1 -1
- package/dist/tsup/{chunk-UV5AN6YV.js → chunk-64CRWLOX.js} +3 -3
- package/dist/tsup/{chunk-CIWSGFQG.js → chunk-6HW3TU7Q.js} +2 -2
- package/dist/tsup/{chunk-LKNGMRIT.js → chunk-B6WTPGOR.js} +3 -3
- package/dist/tsup/{chunk-JLM6TML2.cjs → chunk-CE6NBGOL.cjs} +3 -3
- package/dist/tsup/{chunk-JLM6TML2.cjs.map → chunk-CE6NBGOL.cjs.map} +1 -1
- package/dist/tsup/{chunk-FVVRY42Y.cjs → chunk-DAC73W53.cjs} +7 -7
- package/dist/tsup/{chunk-FVVRY42Y.cjs.map → chunk-DAC73W53.cjs.map} +1 -1
- package/dist/tsup/{chunk-VJNX5RCS.cjs → chunk-ERBNWARB.cjs} +4 -4
- package/dist/tsup/{chunk-VJNX5RCS.cjs.map → chunk-ERBNWARB.cjs.map} +1 -1
- package/dist/tsup/{chunk-674WU4L6.js → chunk-JJCUSUWO.js} +2 -2
- package/dist/tsup/chunk-JJCUSUWO.js.map +1 -0
- package/dist/tsup/{chunk-7ZDC4XEA.js → chunk-JKDAOE4T.js} +4 -4
- package/dist/tsup/{chunk-CRZJ74SA.cjs → chunk-JMO6AVUC.cjs} +3 -3
- package/dist/tsup/{chunk-CRZJ74SA.cjs.map → chunk-JMO6AVUC.cjs.map} +1 -1
- package/dist/tsup/{chunk-CSOHAAZK.js → chunk-LTGBHN5Z.js} +1 -1
- package/dist/tsup/chunk-LTGBHN5Z.js.map +1 -0
- package/dist/tsup/{chunk-Z3Z5EOSP.js → chunk-M5J3PXNU.js} +2 -2
- package/dist/tsup/{chunk-NWCAIWHL.cjs → chunk-NGJWQMUU.cjs} +1 -1
- package/dist/tsup/chunk-NGJWQMUU.cjs.map +1 -0
- package/dist/tsup/{chunk-KC3T3MMB.js → chunk-OB5JLDBM.js} +2 -2
- package/dist/tsup/{chunk-Z7QK4HTR.cjs → chunk-PFHGTLF2.cjs} +10 -10
- package/dist/tsup/{chunk-Z7QK4HTR.cjs.map → chunk-PFHGTLF2.cjs.map} +1 -1
- package/dist/tsup/{chunk-HO6UA2RJ.js → chunk-QQ454DVR.js} +2 -2
- package/dist/tsup/{chunk-GZKR7ORE.cjs → chunk-R3UI7MO3.cjs} +140 -140
- package/dist/tsup/{chunk-GZKR7ORE.cjs.map → chunk-R3UI7MO3.cjs.map} +1 -1
- package/dist/tsup/{chunk-XUCVB6BD.cjs → chunk-SWQCSORE.cjs} +3 -3
- package/dist/tsup/{chunk-XUCVB6BD.cjs.map → chunk-SWQCSORE.cjs.map} +1 -1
- package/dist/tsup/{chunk-3Y7MKDLZ.cjs → chunk-YVVFOKEP.cjs} +2 -2
- package/dist/tsup/chunk-YVVFOKEP.cjs.map +1 -0
- package/dist/tsup/client/mod.cjs +5 -5
- package/dist/tsup/client/mod.d.cts +2 -2
- package/dist/tsup/client/mod.d.ts +2 -2
- package/dist/tsup/client/mod.js +4 -4
- package/dist/tsup/common/log.cjs +2 -2
- package/dist/tsup/common/log.js +1 -1
- package/dist/tsup/common/websocket.cjs +3 -3
- package/dist/tsup/common/websocket.js +2 -2
- package/dist/tsup/{config-osdJqQwX.d.cts → config-CuKzfH58.d.cts} +4 -0
- package/dist/tsup/{config-Cv6pjCt-.d.ts → config-RV6gIa2L.d.ts} +4 -0
- package/dist/tsup/{context-DpjrM7n7.d.cts → context-DPCxyjax.d.cts} +1 -1
- package/dist/tsup/{context-CoVbKBuk.d.ts → context-DoMKZBQA.d.ts} +1 -1
- package/dist/tsup/db/drizzle.cjs +2 -2
- package/dist/tsup/db/drizzle.js +1 -1
- package/dist/tsup/db/mod.cjs +3 -3
- package/dist/tsup/db/mod.js +2 -2
- package/dist/tsup/dynamic/mod.cjs +2 -2
- package/dist/tsup/dynamic/mod.d.cts +1 -1
- package/dist/tsup/dynamic/mod.d.ts +1 -1
- package/dist/tsup/dynamic/mod.js +1 -1
- package/dist/tsup/inspector/mod.cjs +5 -5
- package/dist/tsup/inspector/mod.js +4 -4
- package/dist/tsup/inspector/workflow.cjs +4 -4
- package/dist/tsup/inspector/workflow.js +3 -3
- package/dist/tsup/inspector-tab/mod.cjs +2 -2
- package/dist/tsup/inspector-tab/mod.d.cts +2 -2
- package/dist/tsup/inspector-tab/mod.d.ts +2 -2
- package/dist/tsup/inspector-tab/mod.js +1 -1
- package/dist/tsup/mod.cjs +416 -188
- package/dist/tsup/mod.cjs.map +1 -1
- package/dist/tsup/mod.d.cts +3 -3
- package/dist/tsup/mod.d.ts +3 -3
- package/dist/tsup/mod.js +318 -90
- package/dist/tsup/mod.js.map +1 -1
- package/dist/tsup/test/mod.cjs +6 -6
- package/dist/tsup/test/mod.d.cts +1 -1
- package/dist/tsup/test/mod.d.ts +1 -1
- package/dist/tsup/test/mod.js +4 -4
- package/dist/tsup/utils.cjs +2 -2
- package/dist/tsup/utils.js +1 -1
- package/dist/tsup/workflow/mod.cjs +69 -21
- package/dist/tsup/workflow/mod.cjs.map +1 -1
- package/dist/tsup/workflow/mod.d.cts +3 -3
- package/dist/tsup/workflow/mod.d.ts +3 -3
- package/dist/tsup/workflow/mod.js +64 -16
- package/dist/tsup/workflow/mod.js.map +1 -1
- package/package.json +9 -9
- package/src/actor/config.ts +2 -0
- package/src/registry/lost-callbacks.test.ts +64 -0
- package/src/registry/napi-runtime.ts +8 -0
- package/src/registry/native.ts +332 -65
- package/src/registry/run-handler-coordinator.test.ts +102 -0
- package/src/registry/run-handler-coordinator.ts +84 -17
- package/src/registry/runtime.ts +2 -0
- package/src/registry/wasm-runtime.ts +8 -0
- package/src/workflow/context.ts +20 -0
- package/src/workflow/driver.ts +25 -1
- package/src/workflow/mod.ts +46 -12
- package/dist/tsup/chunk-3Y7MKDLZ.cjs.map +0 -1
- package/dist/tsup/chunk-674WU4L6.js.map +0 -1
- package/dist/tsup/chunk-CSOHAAZK.js.map +0 -1
- package/dist/tsup/chunk-NWCAIWHL.cjs.map +0 -1
- /package/dist/tsup/{chunk-UV5AN6YV.js.map → chunk-64CRWLOX.js.map} +0 -0
- /package/dist/tsup/{chunk-CIWSGFQG.js.map → chunk-6HW3TU7Q.js.map} +0 -0
- /package/dist/tsup/{chunk-LKNGMRIT.js.map → chunk-B6WTPGOR.js.map} +0 -0
- /package/dist/tsup/{chunk-7ZDC4XEA.js.map → chunk-JKDAOE4T.js.map} +0 -0
- /package/dist/tsup/{chunk-Z3Z5EOSP.js.map → chunk-M5J3PXNU.js.map} +0 -0
- /package/dist/tsup/{chunk-KC3T3MMB.js.map → chunk-OB5JLDBM.js.map} +0 -0
- /package/dist/tsup/{chunk-HO6UA2RJ.js.map → chunk-QQ454DVR.js.map} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test, vi } from "vitest";
|
|
2
2
|
import { defineRunHandler, type RunControl } from "@/actor/config";
|
|
3
|
+
import { workflow } from "@/workflow/mod";
|
|
3
4
|
import { RunHandlerCoordinator } from "./run-handler-coordinator";
|
|
4
5
|
|
|
5
6
|
class Deferred<T = void> {
|
|
@@ -51,6 +52,107 @@ function createCoordinator() {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
describe("RunHandlerCoordinator", () => {
|
|
55
|
+
test("workflow inspector registrations stay per generation", () => {
|
|
56
|
+
const coordinator = new RunHandlerCoordinator(workflow(async () => {}));
|
|
57
|
+
const restart = vi.fn();
|
|
58
|
+
|
|
59
|
+
const oldInspector = coordinator.getInspector(
|
|
60
|
+
"actor-1",
|
|
61
|
+
restart,
|
|
62
|
+
1,
|
|
63
|
+
)?.workflow;
|
|
64
|
+
const newInspector = coordinator.getInspector(
|
|
65
|
+
"actor-1",
|
|
66
|
+
restart,
|
|
67
|
+
2,
|
|
68
|
+
)?.workflow;
|
|
69
|
+
expect(oldInspector).toBeDefined();
|
|
70
|
+
expect(newInspector).toBeDefined();
|
|
71
|
+
expect(newInspector).not.toBe(oldInspector);
|
|
72
|
+
|
|
73
|
+
// A superseded generation cannot re-initialize or rebind the current generation's
|
|
74
|
+
// workflow registration.
|
|
75
|
+
expect(coordinator.getInspector("actor-1", restart, 1)).toBeUndefined();
|
|
76
|
+
expect(coordinator.getInspector("actor-1", restart, 2)?.workflow).toBe(
|
|
77
|
+
newInspector,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a superseded generation's run is rejected before it touches the inspector", async () => {
|
|
82
|
+
const coordinator = new RunHandlerCoordinator(workflow(async () => {}));
|
|
83
|
+
const restart = vi.fn();
|
|
84
|
+
const newInspector = coordinator.getInspector(
|
|
85
|
+
"actor-1",
|
|
86
|
+
restart,
|
|
87
|
+
2,
|
|
88
|
+
)?.workflow;
|
|
89
|
+
|
|
90
|
+
await expect(
|
|
91
|
+
coordinator.run("actor-1", restart, async () => {}, 1),
|
|
92
|
+
).resolves.toBe("closed");
|
|
93
|
+
expect(coordinator.getInspector("actor-1", restart, 2)?.workflow).toBe(
|
|
94
|
+
newInspector,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a newer generation does not wait on an older generation's stuck run", async () => {
|
|
99
|
+
const coordinator = new RunHandlerCoordinator(
|
|
100
|
+
defineRunHandler(async () => {}, {
|
|
101
|
+
inspectorKind: "workflow",
|
|
102
|
+
createInspector: () => ({
|
|
103
|
+
inspector: {
|
|
104
|
+
workflow: {
|
|
105
|
+
getHistory: () => null,
|
|
106
|
+
getState: async () => null,
|
|
107
|
+
onHistoryUpdated: () => () => {},
|
|
108
|
+
replayFromStep: async () => null,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
const oldRun = new Deferred();
|
|
115
|
+
const oldOutcome = coordinator.run(
|
|
116
|
+
"actor-1",
|
|
117
|
+
() => {},
|
|
118
|
+
() => oldRun.promise,
|
|
119
|
+
1,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// The lost generation's JS callback cannot be cancelled, so it stays active.
|
|
123
|
+
await expect(
|
|
124
|
+
coordinator.run(
|
|
125
|
+
"actor-1",
|
|
126
|
+
() => {},
|
|
127
|
+
async () => {},
|
|
128
|
+
2,
|
|
129
|
+
),
|
|
130
|
+
).resolves.toBe("ran");
|
|
131
|
+
|
|
132
|
+
// The superseded generation cannot start new runs, and its late destroy leaves the
|
|
133
|
+
// newer generation's state in place.
|
|
134
|
+
await expect(
|
|
135
|
+
coordinator.run(
|
|
136
|
+
"actor-1",
|
|
137
|
+
() => {},
|
|
138
|
+
async () => {},
|
|
139
|
+
1,
|
|
140
|
+
),
|
|
141
|
+
).resolves.toBe("closed");
|
|
142
|
+
coordinator.destroy("actor-1", 1);
|
|
143
|
+
await expect(
|
|
144
|
+
coordinator.run(
|
|
145
|
+
"actor-1",
|
|
146
|
+
() => {},
|
|
147
|
+
async () => {},
|
|
148
|
+
2,
|
|
149
|
+
),
|
|
150
|
+
).resolves.toBe("ran");
|
|
151
|
+
|
|
152
|
+
oldRun.resolve();
|
|
153
|
+
await expect(oldOutcome).resolves.toBe("ran");
|
|
154
|
+
});
|
|
155
|
+
|
|
54
156
|
test("fails explicitly when a JavaScript factory omits required workflow controls", async () => {
|
|
55
157
|
const coordinator = new RunHandlerCoordinator(
|
|
56
158
|
defineRunHandler(async () => {}, {
|
|
@@ -11,6 +11,8 @@ import { RivetError } from "@/actor/errors";
|
|
|
11
11
|
type RunHandler = ((...args: any[]) => any) | { run: (...args: any[]) => any };
|
|
12
12
|
|
|
13
13
|
interface ActorRunState {
|
|
14
|
+
/** Actor generation that owns this state, when the runtime reports one. */
|
|
15
|
+
actorGeneration: number | undefined;
|
|
14
16
|
active: boolean;
|
|
15
17
|
closed: boolean;
|
|
16
18
|
exclusive: boolean;
|
|
@@ -46,6 +48,32 @@ function notifyStateChanged(state: ActorRunState): void {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
function createRunState(actorGeneration: number | undefined): ActorRunState {
|
|
52
|
+
return {
|
|
53
|
+
actorGeneration,
|
|
54
|
+
active: false,
|
|
55
|
+
closed: false,
|
|
56
|
+
exclusive: false,
|
|
57
|
+
exclusiveGeneration: 0,
|
|
58
|
+
inspectorInitialized: false,
|
|
59
|
+
queued: 0,
|
|
60
|
+
waiters: new Set(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Closes run state. The inspector disposer is user code, so it is skipped for a generation
|
|
66
|
+
* that was declared lost.
|
|
67
|
+
*/
|
|
68
|
+
function closeRunState(state: ActorRunState, disposeInspector = true): void {
|
|
69
|
+
state.closed = true;
|
|
70
|
+
if (disposeInspector) {
|
|
71
|
+
state.inspector?.dispose?.();
|
|
72
|
+
}
|
|
73
|
+
state.inspector = undefined;
|
|
74
|
+
notifyStateChanged(state);
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
function waitForStateChange(state: ActorRunState): Promise<void> {
|
|
50
78
|
return new Promise((resolve) => {
|
|
51
79
|
state.waiters.add(resolve);
|
|
@@ -69,8 +97,14 @@ export class RunHandlerCoordinator {
|
|
|
69
97
|
actorId: string,
|
|
70
98
|
restart: () => void | Promise<void>,
|
|
71
99
|
callback: () => void | Promise<void>,
|
|
100
|
+
actorGeneration?: number,
|
|
72
101
|
): Promise<RunHandlerOutcome> {
|
|
73
|
-
const state = this.#getOrCreate(actorId);
|
|
102
|
+
const state = this.#getOrCreate(actorId, actorGeneration);
|
|
103
|
+
// Closed state belongs to a superseded generation. It must not initialize an inspector,
|
|
104
|
+
// which would rebind the workflow registration of the current generation.
|
|
105
|
+
if (state.closed) {
|
|
106
|
+
return "closed";
|
|
107
|
+
}
|
|
74
108
|
state.restart = restart;
|
|
75
109
|
this.#initializeInspector(actorId, state);
|
|
76
110
|
state.queued += 1;
|
|
@@ -110,21 +144,37 @@ export class RunHandlerCoordinator {
|
|
|
110
144
|
getInspector(
|
|
111
145
|
actorId: string,
|
|
112
146
|
restart: () => void | Promise<void>,
|
|
147
|
+
actorGeneration?: number,
|
|
113
148
|
): RunInspectorConfig | undefined {
|
|
114
|
-
const state = this.#getOrCreate(actorId);
|
|
149
|
+
const state = this.#getOrCreate(actorId, actorGeneration);
|
|
150
|
+
if (state.closed) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
115
153
|
state.restart = restart;
|
|
116
154
|
this.#initializeInspector(actorId, state);
|
|
117
155
|
return state.inspector?.inspector;
|
|
118
156
|
}
|
|
119
157
|
|
|
120
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Closes the run state for `actorId`. With `actorGeneration`, only state owned by that
|
|
160
|
+
* generation is closed, so an older generation's late cleanup cannot close a newer one.
|
|
161
|
+
*/
|
|
162
|
+
destroy(
|
|
163
|
+
actorId: string,
|
|
164
|
+
actorGeneration?: number,
|
|
165
|
+
options: { disposeInspector?: boolean } = {},
|
|
166
|
+
): void {
|
|
121
167
|
const state = this.#states.get(actorId);
|
|
122
168
|
if (!state) return;
|
|
169
|
+
if (
|
|
170
|
+
actorGeneration !== undefined &&
|
|
171
|
+
state.actorGeneration !== undefined &&
|
|
172
|
+
state.actorGeneration !== actorGeneration
|
|
173
|
+
) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
123
176
|
|
|
124
|
-
state.
|
|
125
|
-
state.inspector?.dispose?.();
|
|
126
|
-
state.inspector = undefined;
|
|
127
|
-
notifyStateChanged(state);
|
|
177
|
+
closeRunState(state, options.disposeInspector ?? true);
|
|
128
178
|
this.#states.delete(actorId);
|
|
129
179
|
}
|
|
130
180
|
|
|
@@ -173,18 +223,34 @@ export class RunHandlerCoordinator {
|
|
|
173
223
|
};
|
|
174
224
|
}
|
|
175
225
|
|
|
176
|
-
#getOrCreate(
|
|
226
|
+
#getOrCreate(
|
|
227
|
+
actorId: string,
|
|
228
|
+
actorGeneration: number | undefined,
|
|
229
|
+
): ActorRunState {
|
|
177
230
|
let state = this.#states.get(actorId);
|
|
231
|
+
if (
|
|
232
|
+
state &&
|
|
233
|
+
actorGeneration !== undefined &&
|
|
234
|
+
state.actorGeneration !== undefined &&
|
|
235
|
+
state.actorGeneration !== actorGeneration
|
|
236
|
+
) {
|
|
237
|
+
if (actorGeneration < state.actorGeneration) {
|
|
238
|
+
// A superseded generation gets closed, detached state so it cannot run or
|
|
239
|
+
// block the current generation.
|
|
240
|
+
const detached = createRunState(actorGeneration);
|
|
241
|
+
detached.closed = true;
|
|
242
|
+
return detached;
|
|
243
|
+
}
|
|
244
|
+
// A newer generation replaces state left by an older one, which may still be
|
|
245
|
+
// marked active because its JS callback cannot be cancelled. That generation did not
|
|
246
|
+
// clean up after itself, which only happens when it was lost, so its user inspector
|
|
247
|
+
// disposer is not called.
|
|
248
|
+
closeRunState(state, false);
|
|
249
|
+
this.#states.delete(actorId);
|
|
250
|
+
state = undefined;
|
|
251
|
+
}
|
|
178
252
|
if (!state) {
|
|
179
|
-
state =
|
|
180
|
-
active: false,
|
|
181
|
-
closed: false,
|
|
182
|
-
exclusive: false,
|
|
183
|
-
exclusiveGeneration: 0,
|
|
184
|
-
inspectorInitialized: false,
|
|
185
|
-
queued: 0,
|
|
186
|
-
waiters: new Set(),
|
|
187
|
-
};
|
|
253
|
+
state = createRunState(actorGeneration);
|
|
188
254
|
this.#states.set(actorId, state);
|
|
189
255
|
}
|
|
190
256
|
return state;
|
|
@@ -194,6 +260,7 @@ export class RunHandlerCoordinator {
|
|
|
194
260
|
if (state.inspectorInitialized) return;
|
|
195
261
|
const inspector = createRunInspector(this.#run, {
|
|
196
262
|
actorId,
|
|
263
|
+
actorGeneration: state.actorGeneration,
|
|
197
264
|
control: this.#control(actorId, state),
|
|
198
265
|
});
|
|
199
266
|
if (this.inspectorKind === "workflow") {
|
package/src/registry/runtime.ts
CHANGED
|
@@ -583,6 +583,8 @@ export interface CoreRuntime {
|
|
|
583
583
|
writes: RuntimeWorkflowKvWrite[],
|
|
584
584
|
): Promise<void>;
|
|
585
585
|
actorId(ctx: ActorContextHandle): string;
|
|
586
|
+
actorGeneration(ctx: ActorContextHandle): number | undefined;
|
|
587
|
+
actorIsLost(ctx: ActorContextHandle): boolean;
|
|
586
588
|
/**
|
|
587
589
|
* Runs one actor callback with `ctx` as the current invocation: operations
|
|
588
590
|
* on retained handles for the same actor resolve to it, and its Core span
|
|
@@ -538,6 +538,14 @@ export class WasmCoreRuntime implements CoreRuntime {
|
|
|
538
538
|
return callHandle(asWasmActorContext(ctx), "actorId");
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
+
actorGeneration(ctx: ActorContextHandle): number | undefined {
|
|
542
|
+
return callHandle(asWasmActorContext(ctx), "generation") ?? undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
actorIsLost(ctx: ActorContextHandle): boolean {
|
|
546
|
+
return callHandle(asWasmActorContext(ctx), "isLost");
|
|
547
|
+
}
|
|
548
|
+
|
|
541
549
|
runWithActorInvocationContext<T>(
|
|
542
550
|
_ctx: ActorContextHandle,
|
|
543
551
|
run: () => T,
|
package/src/workflow/context.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
WorkflowContextInterface,
|
|
13
13
|
WorkflowQueueMessage,
|
|
14
14
|
} from "@rivetkit/workflow-engine";
|
|
15
|
+
import { EvictedError } from "@rivetkit/workflow-engine";
|
|
15
16
|
import type {
|
|
16
17
|
QueueFilterName,
|
|
17
18
|
QueueNextBatchOptions,
|
|
@@ -19,6 +20,7 @@ import type {
|
|
|
19
20
|
QueueResultMessageForName,
|
|
20
21
|
RunContext,
|
|
21
22
|
} from "@/actor/config";
|
|
23
|
+
import { ACTOR_CONTEXT_INTERNAL_SYMBOL } from "@/actor/config";
|
|
22
24
|
import type {
|
|
23
25
|
AnyActorDefinition,
|
|
24
26
|
BaseActorDefinition,
|
|
@@ -282,6 +284,20 @@ export type WorkflowBranchConfig<
|
|
|
282
284
|
// never appears on the public surface.
|
|
283
285
|
const DEACTIVATE_STEP = Symbol("workflow.step.deactivate");
|
|
284
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Stops the workflow before it starts more user code once its actor generation was declared
|
|
289
|
+
* lost. The engine treats the eviction like a sleep and replays from history in the next
|
|
290
|
+
* generation.
|
|
291
|
+
*/
|
|
292
|
+
export function throwIfGenerationLost(runCtx: unknown): void {
|
|
293
|
+
const actor = (
|
|
294
|
+
runCtx as { [ACTOR_CONTEXT_INTERNAL_SYMBOL]?: { isLost?: boolean } }
|
|
295
|
+
)[ACTOR_CONTEXT_INTERNAL_SYMBOL];
|
|
296
|
+
if (actor?.isLost === true) {
|
|
297
|
+
throw new EvictedError();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
285
301
|
/**
|
|
286
302
|
* The context handed to a workflow step (`step` / `tryStep` callbacks). This is
|
|
287
303
|
* the only scope where actor data (state, vars, db, client) and side effects
|
|
@@ -994,6 +1010,7 @@ export class WorkflowContext<
|
|
|
994
1010
|
TQueues
|
|
995
1011
|
>(this.#runCtx, () => this.#markGuardTriggered());
|
|
996
1012
|
|
|
1013
|
+
throwIfGenerationLost(this.#runCtx);
|
|
997
1014
|
try {
|
|
998
1015
|
return await run(stepCtx);
|
|
999
1016
|
} finally {
|
|
@@ -1044,6 +1061,7 @@ export class WorkflowContext<
|
|
|
1044
1061
|
TEvents,
|
|
1045
1062
|
TQueues
|
|
1046
1063
|
>(this.#runCtx, () => this.#markGuardTriggered());
|
|
1064
|
+
throwIfGenerationLost(this.#runCtx);
|
|
1047
1065
|
try {
|
|
1048
1066
|
await rollback(stepCtx, output);
|
|
1049
1067
|
} finally {
|
|
@@ -1097,6 +1115,8 @@ export class WorkflowContext<
|
|
|
1097
1115
|
TEvents,
|
|
1098
1116
|
TQueues
|
|
1099
1117
|
> {
|
|
1118
|
+
// Every try, loop, join, and race body runs through a child context.
|
|
1119
|
+
throwIfGenerationLost(this.#runCtx);
|
|
1100
1120
|
return new WorkflowContext(ctx, this.#runCtx);
|
|
1101
1121
|
}
|
|
1102
1122
|
}
|
package/src/workflow/driver.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { RunContext } from "@/actor/config";
|
|
|
10
10
|
import type { AnyStaticActorInstance } from "@/actor/definition";
|
|
11
11
|
import { makeWorkflowKey, workflowStoragePrefix } from "@/actor/keys";
|
|
12
12
|
import type { RawAccess } from "@/common/database/config";
|
|
13
|
+
import { throwIfGenerationLost } from "./context";
|
|
13
14
|
|
|
14
15
|
const WORKFLOW_STORAGE_PREFIX = workflowStoragePrefix();
|
|
15
16
|
// Keep workflow flushes below depot's 320-dirty-page commit ceiling. The
|
|
@@ -69,6 +70,26 @@ function normalizeSqlBlob(value: unknown): Uint8Array {
|
|
|
69
70
|
throw new Error("workflow sqlite value was not a blob");
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function guardRawAccessAgainstLost(
|
|
74
|
+
access: RawAccess,
|
|
75
|
+
runCtx: RunContext<any, any, any, any, any, any, any, any> | undefined,
|
|
76
|
+
): RawAccess {
|
|
77
|
+
return {
|
|
78
|
+
...access,
|
|
79
|
+
execute: ((...args: Parameters<RawAccess["execute"]>) => {
|
|
80
|
+
throwIfGenerationLost(runCtx);
|
|
81
|
+
return access.execute(...args);
|
|
82
|
+
}) as RawAccess["execute"],
|
|
83
|
+
transaction: (callback, options) => {
|
|
84
|
+
throwIfGenerationLost(runCtx);
|
|
85
|
+
return access.transaction(
|
|
86
|
+
(tx) => callback(guardRawAccessAgainstLost(tx, runCtx)),
|
|
87
|
+
options,
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
function runtimeDbFromContext(
|
|
73
94
|
runCtx?: RunContext<any, any, any, any, any, any, any, any>,
|
|
74
95
|
): RawAccess | undefined {
|
|
@@ -79,7 +100,10 @@ function runtimeDbFromContext(
|
|
|
79
100
|
"execute" in db &&
|
|
80
101
|
"transaction" in db
|
|
81
102
|
) {
|
|
82
|
-
|
|
103
|
+
// The client can come from a user database provider. Workflow storage calls it after
|
|
104
|
+
// awaits, including inside its own transactions, so every call checks that the
|
|
105
|
+
// generation was not lost first.
|
|
106
|
+
return guardRawAccessAgainstLost(db as RawAccess, runCtx);
|
|
83
107
|
}
|
|
84
108
|
return undefined;
|
|
85
109
|
}
|
package/src/workflow/mod.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { isActorAbortedError, RivetError } from "@/actor/errors";
|
|
|
24
24
|
import type { EventSchemaConfig, QueueSchemaConfig } from "@/actor/schema";
|
|
25
25
|
import type { AnyDatabaseProvider } from "@/common/database/config";
|
|
26
26
|
import { stringifyError } from "@/utils";
|
|
27
|
-
import { WorkflowContext } from "./context";
|
|
27
|
+
import { throwIfGenerationLost, WorkflowContext } from "./context";
|
|
28
28
|
import { ActorWorkflowControlDriver, ActorWorkflowDriver } from "./driver";
|
|
29
29
|
import { createWorkflowInspectorAdapter } from "./inspector";
|
|
30
30
|
|
|
@@ -178,13 +178,28 @@ export function workflow<
|
|
|
178
178
|
type WorkflowInspectorRegistration = ReturnType<
|
|
179
179
|
typeof createWorkflowInspectorAdapter
|
|
180
180
|
> & { control?: RunControl };
|
|
181
|
+
// Keyed by actor id and generation so a lost generation that is still running cannot rebind
|
|
182
|
+
// the control or state of the generation that replaced it.
|
|
181
183
|
const workflowInspectors = new Map<string, WorkflowInspectorRegistration>();
|
|
182
184
|
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
+
function workflowInspectorKey(
|
|
186
|
+
actorId: string,
|
|
187
|
+
actorGeneration: number | undefined,
|
|
188
|
+
): string {
|
|
189
|
+
return actorGeneration === undefined
|
|
190
|
+
? actorId
|
|
191
|
+
: `${actorId}#${actorGeneration}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getWorkflowInspector(
|
|
195
|
+
actorId: string,
|
|
196
|
+
actorGeneration: number | undefined,
|
|
197
|
+
) {
|
|
198
|
+
const key = workflowInspectorKey(actorId, actorGeneration);
|
|
199
|
+
let workflowInspector = workflowInspectors.get(key);
|
|
185
200
|
if (!workflowInspector) {
|
|
186
201
|
workflowInspector = createWorkflowInspectorAdapter();
|
|
187
|
-
workflowInspectors.set(
|
|
202
|
+
workflowInspectors.set(key, workflowInspector);
|
|
188
203
|
}
|
|
189
204
|
return workflowInspector;
|
|
190
205
|
}
|
|
@@ -207,7 +222,11 @@ export function workflow<
|
|
|
207
222
|
}
|
|
208
223
|
)[ACTOR_CONTEXT_INTERNAL_SYMBOL];
|
|
209
224
|
invariant(actor, "workflow() requires an actor instance");
|
|
210
|
-
const
|
|
225
|
+
const actorGeneration = (actor as { generation?: number }).generation;
|
|
226
|
+
const workflowInspector = getWorkflowInspector(
|
|
227
|
+
actor.id,
|
|
228
|
+
actorGeneration,
|
|
229
|
+
);
|
|
211
230
|
|
|
212
231
|
const driver = new ActorWorkflowDriver(actor, runCtx);
|
|
213
232
|
const controlDriver = new ActorWorkflowControlDriver(actor, runCtx);
|
|
@@ -246,7 +265,10 @@ export function workflow<
|
|
|
246
265
|
|
|
247
266
|
const handle = runWorkflow(
|
|
248
267
|
actor.id,
|
|
249
|
-
async (ctx) =>
|
|
268
|
+
async (ctx) => {
|
|
269
|
+
throwIfGenerationLost(runCtx);
|
|
270
|
+
return await fn(new WorkflowContext(ctx, runCtx));
|
|
271
|
+
},
|
|
250
272
|
undefined,
|
|
251
273
|
driver,
|
|
252
274
|
{
|
|
@@ -256,7 +278,15 @@ export function workflow<
|
|
|
256
278
|
logger: runCtx.log as RunWorkflowOptions["logger"],
|
|
257
279
|
onHistoryUpdated: workflowInspector.update,
|
|
258
280
|
onError: onError
|
|
259
|
-
? async (event) =>
|
|
281
|
+
? async (event) => {
|
|
282
|
+
// A lost generation starts no new user hook.
|
|
283
|
+
if (
|
|
284
|
+
(actor as { isLost?: boolean }).isLost === true
|
|
285
|
+
) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
await onError(runCtx, event);
|
|
289
|
+
}
|
|
260
290
|
: undefined,
|
|
261
291
|
},
|
|
262
292
|
);
|
|
@@ -302,16 +332,20 @@ export function workflow<
|
|
|
302
332
|
return defineRunHandler(run, {
|
|
303
333
|
icon: "diagram-project",
|
|
304
334
|
inspectorKind: "workflow",
|
|
305
|
-
createInspector: ({ actorId, control }) => {
|
|
306
|
-
const
|
|
335
|
+
createInspector: ({ actorId, actorGeneration, control }) => {
|
|
336
|
+
const key = workflowInspectorKey(actorId, actorGeneration);
|
|
337
|
+
const workflowInspector = getWorkflowInspector(
|
|
338
|
+
actorId,
|
|
339
|
+
actorGeneration,
|
|
340
|
+
);
|
|
307
341
|
workflowInspector.control = control;
|
|
308
342
|
return {
|
|
309
343
|
inspector: { workflow: workflowInspector.adapter },
|
|
310
344
|
dispose: () => {
|
|
311
345
|
// Do not let a stale disposer remove a newly-created adapter for
|
|
312
|
-
// the same actor
|
|
313
|
-
if (workflowInspectors.get(
|
|
314
|
-
workflowInspectors.delete(
|
|
346
|
+
// the same actor generation.
|
|
347
|
+
if (workflowInspectors.get(key) === workflowInspector) {
|
|
348
|
+
workflowInspectors.delete(key);
|
|
315
349
|
}
|
|
316
350
|
},
|
|
317
351
|
};
|