@nylorun/runtime 0.22.0-beta → 0.23.0-beta
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/CHANGELOG.md +13 -0
- package/README.md +17 -3
- package/dist/adapters/execution/restate.d.ts +15 -3
- package/dist/adapters/execution/restate.js +77 -83
- package/dist/adapters/streams/s2.js +1 -13
- package/dist/core/flow-host.d.ts +1 -19
- package/dist/core/flow-host.js +8 -37
- package/dist/execution/types.d.ts +10 -8
- package/dist/execution/types.js +5 -7
- package/dist/host/execution.d.ts +8 -6
- package/dist/host/main.js +10 -16
- package/dist/host/stack-config.d.ts +6 -0
- package/dist/host/stack-config.js +6 -0
- package/dist/infra/execution.d.ts +18 -1
- package/dist/infra/execution.js +28 -4
- package/dist/infra/index.d.ts +1 -1
- package/dist/management-openapi.json +1 -1
- package/dist/openapi.json +1 -1
- package/dist/retry.d.ts +21 -0
- package/dist/retry.js +34 -0
- package/dist/sandbox/join.js +6 -7
- package/dist/sandbox/pods/reconcile.js +3 -3
- package/dist/store/postgres/control.js +8 -1
- package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
- package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
- package/dist/store/postgres/schema.d.ts +263 -2
- package/dist/store/postgres/schema.js +34 -3
- package/dist/store/postgres/store.js +165 -52
- package/dist/store/types.d.ts +96 -39
- package/dist/streams/relay/core.js +1 -13
- package/dist/tenant/advance.js +6 -7
- package/dist/tenant/commands.js +1 -2
- package/dist/tenant/context.d.ts +19 -11
- package/dist/tenant/control.d.ts +8 -1
- package/dist/tenant/control.js +2 -0
- package/dist/tenant/runtime.js +23 -3
- package/dist/tenant/sandboxes.d.ts +2 -1
- package/dist/tenant/sandboxes.js +23 -19
- package/dist/tenant/scheduler.d.ts +7 -0
- package/dist/tenant/scheduler.js +38 -7
- package/dist/tenant/session-streams.d.ts +0 -1
- package/dist/tenant/session-streams.js +1 -13
- package/dist/tenant/streams.js +14 -29
- package/dist/tenant/sweep.d.ts +39 -12
- package/dist/tenant/sweep.js +157 -58
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
package/dist/tenant/sweep.js
CHANGED
|
@@ -4,26 +4,34 @@
|
|
|
4
4
|
* and startup scan the Tenant runtime used to keep. It reconciles the Session Store with what
|
|
5
5
|
* Durable Session Execution was asked to do; it never decides when a session advances:
|
|
6
6
|
*
|
|
7
|
-
* 1. **
|
|
8
|
-
* after commit that failed, or that a crash between the commit and the send cut
|
|
9
|
-
* first pass on this process takes every row, so a previous process's are not
|
|
10
|
-
* waiting; later passes take
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* 1. **Outbox.** Outbox rows (`Tx.wake`, `Tx.signalSandbox`) are delivered again and deleted:
|
|
8
|
+
* a delivery after commit that failed, or that a crash between the commit and the send cut
|
|
9
|
+
* off. The first pass on this process takes every row, so a previous process's are not
|
|
10
|
+
* left waiting; later passes take those whose delivery after commit failed and those older
|
|
11
|
+
* than `OUTBOX_GRACE_MS` (the database's clock), batch after batch within
|
|
12
|
+
* `DRAIN_BUDGET_MS`. Each row carries the idempotency key it was first sent with, so a
|
|
13
|
+
* request the execution did accept before the crash, or one still being sent after its
|
|
14
|
+
* commit, runs once. Rows are delivered one by one: a row that fails is retried with
|
|
15
|
+
* backoff and parked after `MAX_OUTBOX_ATTEMPTS` (`deliverOutbox`), and no delivery fails
|
|
16
|
+
* the step.
|
|
13
17
|
* 2. **Linked agents.** Pending workflow `agent` effects whose linked turn already settled
|
|
14
|
-
* are completed and the workflow is woken
|
|
15
|
-
*
|
|
18
|
+
* are completed and the workflow is woken. A linked turn's settle does both in its own
|
|
19
|
+
* transaction; this is the backstop for one that settle left pending, such as an effect
|
|
20
|
+
* an older Runtime wrote.
|
|
16
21
|
* 3. **Orphaned sessions.** On this process's first pass for the Tenant, then at most every
|
|
17
22
|
* `ORPHAN_SCAN_MS`: `running` or `runnable` sessions with no owner, or an owner whose lease
|
|
18
|
-
* expired, are woken with reason `recover
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* expired, are woken with reason `recover`, page by page within `DRAIN_BUDGET_MS`; a scan
|
|
24
|
+
* the budget cut short goes on at the next pass. Neither a lost wake (step 1) nor a Worker
|
|
25
|
+
* that died mid-advance (the execution retries that advance, which takes over, §11.4)
|
|
26
|
+
* needs it: it is the backstop for what the execution itself lost or ended without an
|
|
27
|
+
* advance, such as an in-process execution's queue when its process stopped, an advance
|
|
28
|
+
* for a Tenant not open on its Worker, an invocation an operator killed, Restate's state
|
|
29
|
+
* wiped (§17.11), or a parked outbox row.
|
|
23
30
|
* 4. **Sandboxes.** Idle sandboxes are stopped, records of compute no longer held are marked
|
|
24
31
|
* stopped, and sandboxes whose session or sandbox resource is gone are removed, by the
|
|
25
32
|
* workspace capability (`harness-api/workspace.ts`: here, or in the harness that serves
|
|
26
|
-
* workspaces). Idle MCP connections of the in-process harness are closed.
|
|
33
|
+
* workspaces). Idle MCP connections of the in-process harness are closed. As often as the
|
|
34
|
+
* orphan scan, pod sandboxes whose change stalled get a reconcile (`reconcileStalePods`).
|
|
27
35
|
* 5. **Control signals.** Signals on the control bus older than `SIGNAL_RETENTION_MS` are
|
|
28
36
|
* deleted: followers read back only the last two minutes (`store/postgres/control.ts`).
|
|
29
37
|
* 6. **Hooks.** Callbacks registered with `ctx.onSweep`.
|
|
@@ -38,14 +46,28 @@ const BATCH = 100;
|
|
|
38
46
|
/** How long a control signal is kept after it was written. */
|
|
39
47
|
const SIGNAL_RETENTION_MS = 60 * 60 * 1000;
|
|
40
48
|
/**
|
|
41
|
-
* How old
|
|
42
|
-
*
|
|
43
|
-
*
|
|
49
|
+
* How old an outbox row is before the sweep delivers it, unless its delivery after commit
|
|
50
|
+
* already failed (then it is due at once): longer than that delivery can take with its
|
|
51
|
+
* retries, so the sweep does not send again, and double the load on, an execution that is
|
|
52
|
+
* only slow. Sending one twice is harmless either way (the idempotency key).
|
|
53
|
+
*/
|
|
54
|
+
export const OUTBOX_GRACE_MS = 30_000;
|
|
55
|
+
/**
|
|
56
|
+
* How often the orphan scan, and the stale pod sandbox check, run after the first pass of a
|
|
57
|
+
* Tenant opened on this process.
|
|
44
58
|
*/
|
|
45
|
-
export const WAKE_GRACE_MS = 2000;
|
|
46
|
-
/** How often the orphan scan runs after the first pass of a Tenant opened on this process. */
|
|
47
59
|
export const ORPHAN_SCAN_MS = 60_000;
|
|
48
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* How long one pass may spend draining the outbox, and paging through orphaned sessions.
|
|
62
|
+
* A backlog is worked off over several passes rather than holding one (and with it the
|
|
63
|
+
* Tenant's sweep object in the execution) for long.
|
|
64
|
+
*/
|
|
65
|
+
export const DRAIN_BUDGET_MS = 2000;
|
|
66
|
+
/**
|
|
67
|
+
* Each open Tenant's sweep state on this process: when its last orphan scan finished, the
|
|
68
|
+
* last session id of one that ran out of budget, which the next pass goes on from, and when
|
|
69
|
+
* the stale pod sandbox check last ran.
|
|
70
|
+
*/
|
|
49
71
|
const passes = new WeakMap();
|
|
50
72
|
export async function sweep(ctx) {
|
|
51
73
|
if (ctx.closing || ctx.closed)
|
|
@@ -54,20 +76,22 @@ export async function sweep(ctx) {
|
|
|
54
76
|
const first = !passes.has(ctx);
|
|
55
77
|
const pass = passes.get(ctx) ?? {};
|
|
56
78
|
passes.set(ctx, pass);
|
|
57
|
-
const scanOrphans = pass.orphansAt === undefined ||
|
|
79
|
+
const scanOrphans = pass.orphansAt === undefined ||
|
|
80
|
+
pass.orphansAfter !== undefined ||
|
|
81
|
+
now.getTime() - pass.orphansAt >= ORPHAN_SCAN_MS;
|
|
82
|
+
const checkPods = pass.podsAt === undefined || now.getTime() - pass.podsAt >= ORPHAN_SCAN_MS;
|
|
58
83
|
const steps = [
|
|
59
|
-
[
|
|
60
|
-
"wakes",
|
|
61
|
-
() => deliverPendingWakes(ctx, first ? now : new Date(now.getTime() - WAKE_GRACE_MS)),
|
|
62
|
-
],
|
|
84
|
+
["outbox", () => deliverOutbox(ctx, first ? 0 : OUTBOX_GRACE_MS)],
|
|
63
85
|
["linked", () => reconcileLinkedAgents(ctx)],
|
|
64
86
|
...(scanOrphans
|
|
65
87
|
? [
|
|
66
88
|
[
|
|
67
89
|
"orphans",
|
|
68
90
|
async () => {
|
|
69
|
-
await wakeOrphanedSessions(ctx, now);
|
|
70
|
-
pass.
|
|
91
|
+
const scan = await wakeOrphanedSessions(ctx, now, pass.orphansAfter);
|
|
92
|
+
pass.orphansAfter = scan.next;
|
|
93
|
+
if (scan.next === undefined)
|
|
94
|
+
pass.orphansAt = now.getTime();
|
|
71
95
|
},
|
|
72
96
|
],
|
|
73
97
|
]
|
|
@@ -80,7 +104,17 @@ export async function sweep(ctx) {
|
|
|
80
104
|
sandboxExists: async (id) => (await ctx.store.tx((t) => t.sandboxResource(id))) !== undefined,
|
|
81
105
|
}),
|
|
82
106
|
],
|
|
83
|
-
|
|
107
|
+
...(checkPods
|
|
108
|
+
? [
|
|
109
|
+
[
|
|
110
|
+
"pods",
|
|
111
|
+
async () => {
|
|
112
|
+
await reconcileStalePods(ctx, now);
|
|
113
|
+
pass.podsAt = now.getTime();
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
]
|
|
117
|
+
: []),
|
|
84
118
|
// A harness elsewhere closes its own idle MCP connections.
|
|
85
119
|
["mcp", async () => ctx.mcp?.sweep()],
|
|
86
120
|
[
|
|
@@ -107,53 +141,118 @@ export async function sweep(ctx) {
|
|
|
107
141
|
if (failure)
|
|
108
142
|
throw failure.error;
|
|
109
143
|
}
|
|
144
|
+
/** Failed deliveries after which the sweep parks an outbox row (`Tx.failOutbox`). */
|
|
145
|
+
export const MAX_OUTBOX_ATTEMPTS = 10;
|
|
146
|
+
/** How long after its first failed delivery the sweep tries a row again; doubled each time. */
|
|
147
|
+
const RETRY_MS = 5000;
|
|
148
|
+
const RETRY_MAX_MS = 10 * 60_000;
|
|
110
149
|
/**
|
|
111
|
-
* Deliver the
|
|
112
|
-
*
|
|
113
|
-
*
|
|
150
|
+
* Deliver the outbox rows that are due (`Tx.pendingOutbox`: older than `graceMs`, or whose
|
|
151
|
+
* delivery failed), oldest first, batch after batch until none is left or `budgetMs` is
|
|
152
|
+
* spent, and delete the delivered ones. Each row is delivered on its own: one that fails
|
|
153
|
+
* (Restate refuses it, or a Runtime that does not know a wake's reason) is tried again later,
|
|
154
|
+
* with backoff, so it never holds back the rows behind it, and after `MAX_OUTBOX_ATTEMPTS`
|
|
155
|
+
* failures it is parked: logged once, at error, and kept in the outbox for an operator. A
|
|
156
|
+
* parked wake's session is still re-woken by the orphan scan while it is runnable, and a
|
|
157
|
+
* parked sandbox signal's pod by the stale pod check. A row the Tenant declines (it is
|
|
158
|
+
* closing) stays as it is, and ends the drain. Never throws for a delivery; returns the rows
|
|
159
|
+
* delivered.
|
|
114
160
|
*/
|
|
115
|
-
export async function
|
|
116
|
-
const
|
|
161
|
+
export async function deliverOutbox(ctx, graceMs = 0, budgetMs = DRAIN_BUDGET_MS) {
|
|
162
|
+
const deadline = Date.now() + budgetMs;
|
|
117
163
|
const delivered = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
164
|
+
for (;;) {
|
|
165
|
+
const pending = await ctx.store.tx((t) => t.pendingOutbox(graceMs, BATCH));
|
|
166
|
+
const batch = [];
|
|
167
|
+
let done = pending.length < BATCH;
|
|
168
|
+
try {
|
|
169
|
+
for (const row of pending) {
|
|
170
|
+
if (Date.now() >= deadline) {
|
|
171
|
+
done = true;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
let accepted;
|
|
175
|
+
try {
|
|
176
|
+
accepted = await ctx.send(row.request);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
await failed(ctx, row, error);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (accepted)
|
|
183
|
+
batch.push(row);
|
|
184
|
+
else
|
|
185
|
+
done = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
// Delivered ones go even when a later step fails, so they are not sent again.
|
|
190
|
+
if (batch.length > 0)
|
|
191
|
+
await ctx.store.tx((t) => t.deleteOutbox(batch));
|
|
192
|
+
}
|
|
193
|
+
delivered.push(...batch);
|
|
194
|
+
if (done)
|
|
195
|
+
return delivered;
|
|
127
196
|
}
|
|
128
|
-
|
|
197
|
+
}
|
|
198
|
+
/** Records a failed delivery of `row`: retried with backoff, parked after the last attempt. */
|
|
199
|
+
async function failed(ctx, row, error) {
|
|
200
|
+
const attempts = row.attempts + 1;
|
|
201
|
+
const park = attempts >= MAX_OUTBOX_ATTEMPTS;
|
|
202
|
+
const retryInMs = Math.min(RETRY_MS * 2 ** (attempts - 1), RETRY_MAX_MS);
|
|
203
|
+
await ctx.store.tx((t) => t.failOutbox(row, { retryInMs, park }));
|
|
204
|
+
const { request } = row;
|
|
205
|
+
const fields = {
|
|
206
|
+
rowId: row.id,
|
|
207
|
+
...(request.kind === "wake"
|
|
208
|
+
? { sessionId: request.sessionId, reason: request.wake.reason }
|
|
209
|
+
: { sandboxId: request.sandboxId, signal: request.signal.kind }),
|
|
210
|
+
attempts,
|
|
211
|
+
message: error instanceof Error ? error.message : String(error),
|
|
212
|
+
};
|
|
213
|
+
if (park)
|
|
214
|
+
ctx.config.logger.error("outbox row parked: its delivery keeps failing", fields);
|
|
215
|
+
else
|
|
216
|
+
ctx.config.logger.warn("outbox delivery failed; the sweep tries it again", fields);
|
|
129
217
|
}
|
|
130
218
|
/** Settle pending workflow `agent` effects whose linked turn already finished. */
|
|
131
219
|
export async function reconcileLinkedAgents(ctx) {
|
|
132
220
|
const { store } = ctx;
|
|
133
221
|
const effects = await store.tx((t) => pendingAgentEffects(t));
|
|
134
222
|
for (const effect of effects)
|
|
135
|
-
await store.tx((t) => reconcilePendingAgentEffect({
|
|
136
|
-
t,
|
|
137
|
-
effectId: effect.request.effectId,
|
|
138
|
-
schedule: ctx.wake,
|
|
139
|
-
}));
|
|
223
|
+
await store.tx((t) => reconcilePendingAgentEffect({ t, effectId: effect.request.effectId }));
|
|
140
224
|
}
|
|
141
|
-
/**
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Wake `running`/`runnable` sessions without a live owner, page after page from the first id
|
|
227
|
+
* after `after`, until none is left or `budgetMs` is spent. Returns the ids woken, and the last
|
|
228
|
+
* id when the budget ran out first (`next`, where the next scan goes on).
|
|
229
|
+
*/
|
|
230
|
+
export async function wakeOrphanedSessions(ctx, now = new Date(), after, budgetMs = DRAIN_BUDGET_MS) {
|
|
231
|
+
const deadline = Date.now() + budgetMs;
|
|
232
|
+
const woken = [];
|
|
233
|
+
let cursor = after;
|
|
234
|
+
for (;;) {
|
|
235
|
+
const page = await ctx.store.tx((t) => t.orphanedSessions(now, BATCH, cursor));
|
|
236
|
+
for (const session of page) {
|
|
237
|
+
await ctx.wake(session.id, { reason: "recover" });
|
|
238
|
+
woken.push(session.id);
|
|
239
|
+
cursor = session.id;
|
|
240
|
+
}
|
|
241
|
+
if (page.length < BATCH)
|
|
242
|
+
return { woken };
|
|
243
|
+
if (Date.now() >= deadline)
|
|
244
|
+
return { woken, next: cursor };
|
|
245
|
+
}
|
|
148
246
|
}
|
|
149
247
|
/** A pod sandbox in a change this long without a reconcile gets one from the sweep. */
|
|
150
248
|
const STALE_POD_MS = 60_000;
|
|
151
249
|
/**
|
|
152
250
|
* Pod sandboxes (F7.2) whose change has not finished (creating, deleting, an old volume still
|
|
153
251
|
* to delete, an expiry not yet recorded), or that should run but whose engine is not connected,
|
|
154
|
-
* and whose row has not moved for a minute
|
|
155
|
-
*
|
|
156
|
-
*
|
|
252
|
+
* and whose row has not moved for a minute, get a reconcile: a pod or volume deleted outside
|
|
253
|
+
* the Runtime is found, and a change goes on whose reconcile the execution itself lost (its
|
|
254
|
+
* retry timer, an invocation an operator killed, Restate's state wiped) or whose outbox row
|
|
255
|
+
* was parked. A reconcile a commit asked for is never lost: it is in the outbox.
|
|
157
256
|
*/
|
|
158
257
|
async function reconcileStalePods(ctx, now) {
|
|
159
258
|
if (!ctx.pods)
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.23.0-beta";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0-beta",
|
|
4
4
|
"description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"@nylorun/core": "0.19.0-beta",
|
|
69
69
|
"@nylorun/harness": "0.25.1-beta",
|
|
70
70
|
"@restatedev/restate-sdk": "1.17.2",
|
|
71
|
+
"@restatedev/restate-sdk-clients": "1.17.2",
|
|
71
72
|
"@s2-dev/streamstore": "0.28.0",
|
|
72
73
|
"aws4fetch": "1.0.20",
|
|
73
74
|
"drizzle-orm": "^0.45.3",
|