@effect-agent/platform-cloudflare 0.1.0-beta.13 → 0.1.0-beta.131
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/Alarm.d.mts +244 -0
- package/dist/Alarm.mjs +947 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/BrowserCredentials.d.mts +91 -0
- package/dist/BrowserCredentials.mjs +120 -0
- package/dist/BrowserCredentials.mjs.map +1 -0
- package/dist/BrowserRestCapture.d.mts +34 -0
- package/dist/BrowserRestCapture.mjs +238 -0
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/BrowserRestCrawl.d.mts +16 -0
- package/dist/BrowserRestCrawl.mjs +244 -0
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/BrowserSession-DP6b-cpL.d.mts +125 -0
- package/dist/BrowserSession-DWoRC_iM.mjs +505 -0
- package/dist/BrowserSession-DWoRC_iM.mjs.map +1 -0
- package/dist/BrowserSession.d.mts +3 -0
- package/dist/BrowserSession.mjs +4 -0
- package/dist/CloudflareAiGateway.d.mts +64 -0
- package/dist/CloudflareAiGateway.mjs +70 -0
- package/dist/CloudflareAiGateway.mjs.map +1 -0
- package/dist/CloudflareBindings.d.mts +115 -0
- package/dist/CloudflareBindings.mjs +113 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
- package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +616 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-DKXGo8L3.d.mts +106 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +128 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +142 -0
- package/dist/CloudflareMemory.mjs +202 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +396 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +530 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1752 -0
- package/dist/CloudflareThreadClient.mjs +382 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser-BnqY_Y-F.d.mts +228 -0
- package/dist/InteractiveBrowser-CxFT-Nwm.mjs +1923 -0
- package/dist/InteractiveBrowser-CxFT-Nwm.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +2 -0
- package/dist/InteractiveBrowser.mjs +2 -0
- package/dist/ThreadObject-CVhZC-7l.mjs +849 -0
- package/dist/ThreadObject-CVhZC-7l.mjs.map +1 -0
- package/dist/ThreadObject-D8GzJxJB.d.mts +839 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +57 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/index.d.mts +15 -1550
- package/dist/index.mjs +15 -1716
- package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
- package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -56
- package/src/Alarm.ts +1979 -0
- package/src/BrowserCredentials.ts +146 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/BrowserSession.ts +624 -0
- package/src/CloudflareAiGateway.ts +170 -0
- package/src/CloudflareBindings.ts +199 -0
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +308 -283
- package/src/{config.ts → CloudflareConfig.ts} +19 -12
- package/src/CloudflareMemory.ts +420 -0
- package/src/CloudflareScheduling.ts +747 -0
- package/src/CloudflareSubscriptions.ts +1041 -0
- package/src/CloudflareThreadClient.ts +809 -0
- package/src/InteractiveBrowser.ts +3246 -0
- package/src/ThreadObject.ts +1190 -0
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
- package/src/index.ts +14 -23
- package/src/internal/boundary.ts +55 -0
- package/src/internal/browser-binding.ts +322 -0
- package/src/internal/browser-credentials.ts +362 -0
- package/src/internal/browser-failure.ts +92 -0
- package/src/internal/browser-file-selection.ts +126 -0
- package/src/internal/browser-quick-action.ts +857 -0
- package/src/internal/browser-readonly-live-view.ts +111 -0
- package/src/internal/browser-session-lifecycle.ts +193 -0
- package/src/internal/layers.ts +773 -0
- package/src/internal/message-delivery.ts +131 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +44 -0
- package/dist/index.mjs.map +0 -1
- package/src/alarm.ts +0 -334
- package/src/bindings.ts +0 -145
- package/src/client.ts +0 -646
- package/src/conversation-object.ts +0 -768
- package/src/layers.ts +0 -376
- package/src/transport.ts +0 -38
package/dist/Alarm.mjs
ADDED
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { DurableObjectContext } from "./CloudflareBindings.mjs";
|
|
3
|
+
import { AuxiliaryDispatchMillis, CloudflareDurableRuntimeConfig } from "./CloudflareConfig.mjs";
|
|
4
|
+
import { r as safeCauseMessage } from "./boundary-BguazVkh.mjs";
|
|
5
|
+
import { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Random, Ref, Result, Schema, Scope, Semaphore, Stream, Struct } from "effect";
|
|
6
|
+
import "effect-agent/agent-registration";
|
|
7
|
+
import { DurableAgentRuntime, DurableRuntimeConfig, RecoveryBlocked, RecoveryFailure, RecoverySweepResult, isolateRecovery } from "effect-agent/durable-agent-runtime";
|
|
8
|
+
import { SubmissionId, ThreadId } from "effect-agent/identifiers";
|
|
9
|
+
import { OperationAuthorizationRequest, OperationAuthorizer } from "effect-agent/operation-authorizer";
|
|
10
|
+
import { AbortIntentRequest, SubmissionLedger } from "effect-agent/submission-ledger";
|
|
11
|
+
import { ThreadProjectionMaintenance, drainDue } from "effect-agent/thread-projection-maintenance";
|
|
12
|
+
import { WakeScheduler } from "effect-agent/wake-scheduler";
|
|
13
|
+
import { DurableObjectStorage } from "effect-cf";
|
|
14
|
+
import { SqlClient } from "effect/unstable/sql/SqlClient";
|
|
15
|
+
//#region src/Alarm.ts
|
|
16
|
+
var Alarm_exports = /* @__PURE__ */ __exportAll({
|
|
17
|
+
DurableAlarmError: () => DurableAlarmError,
|
|
18
|
+
DurableAlarmService: () => DurableAlarmService,
|
|
19
|
+
MaintenancePassReport: () => MaintenancePassReport,
|
|
20
|
+
ThreadHostMaintenance: () => ThreadHostMaintenance,
|
|
21
|
+
ThreadMaintenance: () => ThreadMaintenance,
|
|
22
|
+
ThreadMaintenanceFailpoint: () => ThreadMaintenanceFailpoint,
|
|
23
|
+
ThreadMessageDelivery: () => ThreadMessageDelivery,
|
|
24
|
+
ThreadMutationGate: () => ThreadMutationGate,
|
|
25
|
+
ThreadPublication: () => ThreadPublication,
|
|
26
|
+
ThreadRecoveryFault: () => ThreadRecoveryFault,
|
|
27
|
+
publishCommitted: () => publishCommitted
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE
|
|
31
|
+
* alarm slot; every cadence the Node host ran on fibers (wake scan, lease expiry, settlement
|
|
32
|
+
* and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
|
|
33
|
+
* the slot always holds the EARLIEST deadline any caller asked for.
|
|
34
|
+
*
|
|
35
|
+
* The alarm invariant (plan §1.4): every committed actionable mutation carries a newer durable
|
|
36
|
+
* maintenance generation and a committed alarm. Stable externally-driven waits may be
|
|
37
|
+
* nonterminal without retaining an alarm; their resolving mutation advances the generation and
|
|
38
|
+
* restores the alarm atomically.
|
|
39
|
+
*/
|
|
40
|
+
/** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */
|
|
41
|
+
var DurableAlarmError = class extends Schema.TaggedError()("DurableAlarmError", {
|
|
42
|
+
operation: Schema.String,
|
|
43
|
+
message: Schema.String,
|
|
44
|
+
cause: Schema.optionalKey(Schema.Defect())
|
|
45
|
+
}) {};
|
|
46
|
+
const alarmFailure = (operation) => (cause) => DurableAlarmError.make({
|
|
47
|
+
operation,
|
|
48
|
+
message: safeCauseMessage(cause, "The Cloudflare alarm API failed without a diagnostic"),
|
|
49
|
+
cause
|
|
50
|
+
});
|
|
51
|
+
const makeStorageEffect = Effect.map(SqlClient, (sql) => (operation, execute) => Effect.flatMap(Effect.serviceOption(sql.transactionService), (current) => {
|
|
52
|
+
const body = Effect.uninterruptible(execute);
|
|
53
|
+
return current._tag === "Some" ? body : Effect.scoped(Effect.andThen(sql.reserve.pipe(Effect.mapError(alarmFailure(operation))), body));
|
|
54
|
+
}));
|
|
55
|
+
const makeStorageOperation = Effect.map(makeStorageEffect, (run) => (operation, execute) => run(operation, Effect.tryPromise({
|
|
56
|
+
try: execute,
|
|
57
|
+
catch: alarmFailure(operation)
|
|
58
|
+
})));
|
|
59
|
+
/** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */
|
|
60
|
+
var DurableAlarmService = class DurableAlarmService extends Context.Service()("@effect-agent/platform-cloudflare/DurableAlarmService") {
|
|
61
|
+
static layer = Layer.effect(DurableAlarmService)(Effect.gen(function* () {
|
|
62
|
+
const { ctx } = yield* DurableObjectContext;
|
|
63
|
+
/**
|
|
64
|
+
* In-memory pass bookkeeping — a pure CACHE, never state: a fresh incarnation has no
|
|
65
|
+
* running pass, and a deferred wake lost to eviction was only ever a promptness hint
|
|
66
|
+
* on top of the already-committed pre-armed alarm.
|
|
67
|
+
*/
|
|
68
|
+
const runningPasses = yield* Ref.make(0);
|
|
69
|
+
const storageOperation = yield* makeStorageOperation;
|
|
70
|
+
const scheduled = storageOperation("get alarm", () => ctx.storage.getAlarm()).pipe(Effect.map((deadline) => deadline === null ? Option.none() : Option.some(deadline)));
|
|
71
|
+
const scheduleAt = (epochMillis) => storageOperation("set alarm", () => ctx.storage.setAlarm(epochMillis));
|
|
72
|
+
const ensureScheduledBy = (epochMillis) => storageOperation("ensure alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
73
|
+
const existing = await transaction.getAlarm();
|
|
74
|
+
if (existing === null || existing > epochMillis) await transaction.setAlarm(epochMillis);
|
|
75
|
+
}));
|
|
76
|
+
const armNow = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => ensureScheduledBy(now)));
|
|
77
|
+
const scheduleNow = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Effect.void : armNow));
|
|
78
|
+
const withWakesDeferred = (body) => Ref.update(runningPasses, (passes) => passes + 1).pipe(Effect.andThen(body), Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1)));
|
|
79
|
+
const cancel = storageOperation("delete alarm", () => ctx.storage.deleteAlarm());
|
|
80
|
+
return DurableAlarmService.of({
|
|
81
|
+
scheduled,
|
|
82
|
+
scheduleAt,
|
|
83
|
+
ensureScheduledBy,
|
|
84
|
+
scheduleNow,
|
|
85
|
+
withWakesDeferred,
|
|
86
|
+
cancel
|
|
87
|
+
});
|
|
88
|
+
}));
|
|
89
|
+
};
|
|
90
|
+
/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
|
|
91
|
+
var MaintenancePassReport = class extends Schema.Class("@effect-agent/platform-cloudflare/MaintenancePassReport")({
|
|
92
|
+
/** `caught-up` ran no runtime work (publication may be pending); `actionable` ran recovery. */
|
|
93
|
+
phase: Schema.Literals(["caught-up", "actionable"]),
|
|
94
|
+
/** Recovery decisions and Thread faults observed during this event. */
|
|
95
|
+
recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
96
|
+
/** Head Attempts settled during the event. Joined input may settle with each head. */
|
|
97
|
+
settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
98
|
+
/** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
|
|
99
|
+
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
100
|
+
/** `rearmed` for dirty/autonomous work, `cleared` for stable waits or settlement. */
|
|
101
|
+
alarm: Schema.Literals(["rearmed", "cleared"])
|
|
102
|
+
}) {};
|
|
103
|
+
/** Test-only fault authority; production uses the inert layer. */
|
|
104
|
+
var ThreadMaintenanceFailpoint = class extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint") {
|
|
105
|
+
static layer = Layer.succeed(this)({ hit: () => Effect.void });
|
|
106
|
+
};
|
|
107
|
+
/** Opt in with `ThreadObject.layer(registrations, { publication: Layer.effect(ThreadPublication)(...) })`. */
|
|
108
|
+
var ThreadPublication = class extends Context.Service()("@effect-agent/platform-cloudflare/ThreadPublication") {
|
|
109
|
+
static layer = Layer.succeed(this)({
|
|
110
|
+
invalidate: Effect.void,
|
|
111
|
+
prepareGeneration: () => Effect.void,
|
|
112
|
+
drain: Effect.void,
|
|
113
|
+
pendingDeadline: Effect.succeed(Option.none())
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Host-assembled native message recovery. Preparation is a bounded local selection; the
|
|
118
|
+
* driver bounds each actual Claim and persists its timeout/retry before `run` returns.
|
|
119
|
+
* The alarm must not add a timer starting at selection: Claim setup and retry commits belong
|
|
120
|
+
* to the driver. The prepared allowance decides only whether a wave fits this event.
|
|
121
|
+
*/
|
|
122
|
+
const ThreadMessageDelivery = Context.Reference("@effect-agent/platform-cloudflare/ThreadMessageDelivery", { defaultValue: () => ({
|
|
123
|
+
prepare: Effect.succeed({
|
|
124
|
+
timeoutMillis: 1,
|
|
125
|
+
run: Effect.void
|
|
126
|
+
}),
|
|
127
|
+
pendingDeadline: Effect.succeed(Option.none())
|
|
128
|
+
}) });
|
|
129
|
+
/** Independent lanes share the existing alarm; compose hosts by concatenating their lanes. */
|
|
130
|
+
const ThreadHostMaintenance = Context.Reference("@effect-agent/platform-cloudflare/ThreadHostMaintenance", { defaultValue: () => ({ lanes: [] }) });
|
|
131
|
+
const earliestDeadline = (left, right) => Option.isSome(left) ? Option.isSome(right) ? Option.some(Math.min(left.value, right.value)) : left : right;
|
|
132
|
+
/** @internal A committed source operation must not become a failed operation because delivery failed. */
|
|
133
|
+
const publishCommitted = Effect.gen(function* () {
|
|
134
|
+
const publication = yield* ThreadPublication;
|
|
135
|
+
yield* publication.invalidate.pipe(Effect.andThen(publication.drain));
|
|
136
|
+
}).pipe(Effect.catchCause((cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.logError("Thread publication deferred after source commit", cause)));
|
|
137
|
+
const MaintenanceGeneration = Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n));
|
|
138
|
+
var BindingRetry = class extends Schema.Class("BindingRetry")({
|
|
139
|
+
threadId: ThreadId,
|
|
140
|
+
submissionId: SubmissionId,
|
|
141
|
+
attempts: Schema.Natural,
|
|
142
|
+
notBefore: Schema.Finite,
|
|
143
|
+
reportedAt: Schema.Finite
|
|
144
|
+
}) {};
|
|
145
|
+
/**
|
|
146
|
+
* A durable observation of blocked recovery, independent of the execution journal. This is
|
|
147
|
+
* neither a Settlement nor proof that external effects did not happen. A successful recovery
|
|
148
|
+
* sweep clears it; repair must preserve canonical history and the original admission identity.
|
|
149
|
+
*/
|
|
150
|
+
var ThreadRecoveryFault = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadRecoveryFault")({
|
|
151
|
+
schemaVersion: Schema.Literal(1),
|
|
152
|
+
threadId: ThreadId,
|
|
153
|
+
firstFailedAt: Schema.Finite,
|
|
154
|
+
lastFailedAt: Schema.Finite,
|
|
155
|
+
/** Earliest automatic recovery retry; new admissions do not erase this deadline. */
|
|
156
|
+
retryAt: Schema.Finite,
|
|
157
|
+
/** Saturates at 2^31 - 1; one observation per Thread per recovery sweep. */
|
|
158
|
+
attempts: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(2147483647)),
|
|
159
|
+
failure: RecoveryFailure
|
|
160
|
+
}) {};
|
|
161
|
+
const recoveryFaultKey = (threadId) => `effect-agent:thread-recovery-fault:v1:${threadId}`;
|
|
162
|
+
const decodeRecoveryFaultValue = Schema.decodeUnknownSync(ThreadRecoveryFault);
|
|
163
|
+
const decodeRecoveryFault = (threadId, encoded) => {
|
|
164
|
+
const fault = decodeRecoveryFaultValue(encoded);
|
|
165
|
+
if (fault.threadId !== threadId) throw new Error("Recovery status does not match its Thread key");
|
|
166
|
+
return fault;
|
|
167
|
+
};
|
|
168
|
+
const encodeRecoveryFault = Schema.encodeSync(ThreadRecoveryFault);
|
|
169
|
+
const nativeDispatchConcurrency = 2;
|
|
170
|
+
var MaintenanceRetry = class extends Schema.Class("MaintenanceRetry")({
|
|
171
|
+
generation: MaintenanceGeneration,
|
|
172
|
+
notBefore: Schema.Finite,
|
|
173
|
+
nativeOnly: Schema.Boolean,
|
|
174
|
+
stalls: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(30))
|
|
175
|
+
}) {};
|
|
176
|
+
/** Versioned, platform-private maintenance state stored through Durable Object KV. */
|
|
177
|
+
var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadMaintenanceState")({
|
|
178
|
+
schemaVersion: Schema.Literal(1),
|
|
179
|
+
dirty: MaintenanceGeneration,
|
|
180
|
+
processed: MaintenanceGeneration,
|
|
181
|
+
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
182
|
+
/** One physical-owner cursor; old single-lane records need no conversion. */
|
|
183
|
+
lastServedThreadId: Schema.optionalKey(ThreadId),
|
|
184
|
+
/** Rotate old recovery independently of dispatch, including after eviction or timeout. */
|
|
185
|
+
lastRecoveredThreadId: Schema.optionalKey(ThreadId),
|
|
186
|
+
bindingRetries: Schema.optionalKey(Schema.Array(BindingRetry)),
|
|
187
|
+
/** Absent on older records. A newer mutation makes this retry obsolete. */
|
|
188
|
+
retry: Schema.optionalKey(MaintenanceRetry)
|
|
189
|
+
}) {};
|
|
190
|
+
const MAINTENANCE_STATE_KEY = "effect-agent:thread-maintenance:v1";
|
|
191
|
+
const decodeMaintenanceState = Schema.decodeUnknownSync(ThreadMaintenanceState);
|
|
192
|
+
const encodeMaintenanceState = Schema.encodeSync(ThreadMaintenanceState);
|
|
193
|
+
const initialMaintenanceState = () => ThreadMaintenanceState.make({
|
|
194
|
+
schemaVersion: 1,
|
|
195
|
+
dirty: 1n,
|
|
196
|
+
processed: 0n,
|
|
197
|
+
nonterminal: 0
|
|
198
|
+
});
|
|
199
|
+
const readMaintenanceState = async (transaction) => {
|
|
200
|
+
const encoded = await transaction.get(MAINTENANCE_STATE_KEY);
|
|
201
|
+
return encoded === void 0 ? {
|
|
202
|
+
state: initialMaintenanceState(),
|
|
203
|
+
initialized: false
|
|
204
|
+
} : {
|
|
205
|
+
state: decodeMaintenanceState(encoded),
|
|
206
|
+
initialized: true
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
const ensureTransactionAlarmBy = async (transaction, deadline) => {
|
|
210
|
+
const scheduled = await transaction.getAlarm();
|
|
211
|
+
if (scheduled === null || scheduled > deadline) await transaction.setAlarm(deadline);
|
|
212
|
+
};
|
|
213
|
+
const stableExternalWait = (snapshot, reports) => {
|
|
214
|
+
const report = reports.get(snapshot.submissionId);
|
|
215
|
+
if (report?.decision._tag === "SettleAborted") return false;
|
|
216
|
+
switch (snapshot.state) {
|
|
217
|
+
case "suspended":
|
|
218
|
+
case "joined": return true;
|
|
219
|
+
case "unknown": return report?.disposition === "unknown";
|
|
220
|
+
case "admitted": return reports.get(snapshot.submissionId)?.decision._tag === "AwaitParentEstablishment";
|
|
221
|
+
case "input-applied":
|
|
222
|
+
case "joining":
|
|
223
|
+
case "ready":
|
|
224
|
+
case "running":
|
|
225
|
+
case "settled":
|
|
226
|
+
case "terminalizing": return false;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Shared prearm/acknowledgement boundary for ingress and runtime-owned producers.
|
|
231
|
+
* `ThreadObject.layer` provides this same instance in its Services. Rebuilt runtime/maintenance
|
|
232
|
+
* Layers must reuse that instance; a second gate cannot observe the native producers' activity.
|
|
233
|
+
*/
|
|
234
|
+
var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@effect-agent/platform-cloudflare/internal/ThreadMutationGate") {
|
|
235
|
+
static layer = Layer.effect(this)(Effect.gen(function* () {
|
|
236
|
+
const { ctx } = yield* DurableObjectContext;
|
|
237
|
+
const config = yield* CloudflareDurableRuntimeConfig;
|
|
238
|
+
const failpoint = yield* ThreadMaintenanceFailpoint;
|
|
239
|
+
const activeMutations = yield* Ref.make(0);
|
|
240
|
+
const generationGate = yield* Semaphore.make(1);
|
|
241
|
+
const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
|
|
242
|
+
const runTransaction = yield* makeStorageOperation;
|
|
243
|
+
const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* (invalidatesRecovery) {
|
|
244
|
+
yield* failpoint.hit("maintenance:dirty:before");
|
|
245
|
+
const now = yield* Clock.currentTimeMillis;
|
|
246
|
+
yield* runTransaction("advance maintenance generation", () => ctx.storage.transaction(async (transaction) => {
|
|
247
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
248
|
+
const next = ThreadMaintenanceState.make({
|
|
249
|
+
...state,
|
|
250
|
+
dirty: state.dirty + (invalidatesRecovery ? 1n : 0n)
|
|
251
|
+
});
|
|
252
|
+
if (invalidatesRecovery || !initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
253
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
254
|
+
}));
|
|
255
|
+
yield* failpoint.hit("maintenance:dirty:after");
|
|
256
|
+
yield* Ref.update(activeMutations, (active) => active + 1);
|
|
257
|
+
});
|
|
258
|
+
const endMutation = generationGate.withPermit(Ref.update(activeMutations, (active) => Math.max(0, active - 1)));
|
|
259
|
+
const withMutation = (body, options) => Effect.acquireUseRelease(generationGate.withPermit(beginMutation(options?.invalidatesRecovery ?? true)), () => failpoint.hit("maintenance:mutation:armed").pipe(Effect.andThen(body), Effect.tap(() => failpoint.hit("maintenance:mutation:finished"))), () => endMutation);
|
|
260
|
+
return ThreadMutationGate.of({
|
|
261
|
+
withMutation,
|
|
262
|
+
withSnapshot: (body) => generationGate.withPermit(Effect.flatMap(Ref.get(activeMutations), body))
|
|
263
|
+
});
|
|
264
|
+
}));
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Incremental, quiescent maintenance over a durable dirty/processed generation (issue #93).
|
|
268
|
+
*
|
|
269
|
+
* One physical event owns native scheduling, auxiliary delivery and final alarm rearming.
|
|
270
|
+
*
|
|
271
|
+
* 1. Prearm before any work. A caught-up native step uses the O(1) generation record without
|
|
272
|
+
* recovery, ledger scans or canonical-history reads.
|
|
273
|
+
* 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
|
|
274
|
+
* producer keeps its generation dirty and immediately eligible. Quiescent native retries
|
|
275
|
+
* retain their durable backoff.
|
|
276
|
+
* 3. Admit at most two independent Thread Attempts, one active head per Thread.
|
|
277
|
+
* Keep native and delivery admission open together while finite waves remain active, so
|
|
278
|
+
* fresh replies and abort controls can progress during unrelated cleanup. Close atomically
|
|
279
|
+
* at quiescence, or at the original ten-minute yield deadline, then join admitted waves.
|
|
280
|
+
* 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill waves are
|
|
281
|
+
* bounded independently; incoming native work never restarts or cancels their attempts.
|
|
282
|
+
* Auxiliary failures are reported after the current native opportunity.
|
|
283
|
+
* 5. Close every event resource before the final gated deadline snapshot and alarm decision.
|
|
284
|
+
* The whole event retains one fourteen-minute cooperative timeout.
|
|
285
|
+
*/
|
|
286
|
+
var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
|
|
287
|
+
static layer = Layer.effect(ThreadMaintenance)(Effect.gen(function* () {
|
|
288
|
+
const runtime = yield* DurableAgentRuntime;
|
|
289
|
+
const recoveryConfig = yield* DurableRuntimeConfig;
|
|
290
|
+
const ledger = yield* SubmissionLedger;
|
|
291
|
+
const wakes = yield* WakeScheduler;
|
|
292
|
+
const alarm = yield* DurableAlarmService;
|
|
293
|
+
const config = yield* CloudflareDurableRuntimeConfig;
|
|
294
|
+
const { ctx } = yield* DurableObjectContext;
|
|
295
|
+
const storage = DurableObjectStorage.fromDurableObjectStorage(ctx.storage);
|
|
296
|
+
const runStorage = yield* makeStorageEffect;
|
|
297
|
+
const failpoint = yield* ThreadMaintenanceFailpoint;
|
|
298
|
+
const mutations = yield* ThreadMutationGate;
|
|
299
|
+
const publication = yield* ThreadPublication;
|
|
300
|
+
const projection = yield* ThreadProjectionMaintenance;
|
|
301
|
+
const messages = yield* ThreadMessageDelivery;
|
|
302
|
+
const host = yield* ThreadHostMaintenance;
|
|
303
|
+
const authorizer = yield* OperationAuthorizer;
|
|
304
|
+
const projectionDeadline = projection.pendingDeadline.pipe(Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError("Thread projection deadline unavailable", cause).pipe(Effect.as(Option.some(0)))));
|
|
305
|
+
const pendingDeadline = Effect.gen(function* () {
|
|
306
|
+
return earliestDeadline(earliestDeadline(yield* publication.pendingDeadline, yield* messages.pendingDeadline), earliestDeadline(yield* projectionDeadline, (yield* Effect.forEach(host.lanes, (lane) => lane.pendingDeadline)).reduce(earliestDeadline, Option.none())));
|
|
307
|
+
});
|
|
308
|
+
const maintenancePassGate = yield* Semaphore.make(1);
|
|
309
|
+
const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
|
|
310
|
+
const runTransaction = yield* makeStorageOperation;
|
|
311
|
+
const recoveryStatus = Effect.fn("ThreadMaintenance.recoveryStatus")(function* (threadId) {
|
|
312
|
+
yield* authorizer.authorize(OperationAuthorizationRequest.make({
|
|
313
|
+
operation: "explain",
|
|
314
|
+
threadId
|
|
315
|
+
}));
|
|
316
|
+
return yield* runTransaction("read Thread recovery status", async () => {
|
|
317
|
+
const encoded = await ctx.storage.get(recoveryFaultKey(threadId));
|
|
318
|
+
return encoded === void 0 ? Option.none() : Option.some(decodeRecoveryFault(threadId, encoded));
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
const recordRecoveryStatus = Effect.fn("ThreadMaintenance.recordRecoveryStatus")(function* (result) {
|
|
322
|
+
const threads = /* @__PURE__ */ new Map();
|
|
323
|
+
for (const report of result.reports) threads.set(report.threadId, void 0);
|
|
324
|
+
for (const blocked of result.blocked) threads.set(blocked.threadId, blocked.failure);
|
|
325
|
+
if (threads.size === 0) return /* @__PURE__ */ new Map();
|
|
326
|
+
const now = yield* Clock.currentTimeMillis;
|
|
327
|
+
yield* failpoint.hit("maintenance:recovery-status:before");
|
|
328
|
+
const retained = yield* runTransaction("record Thread recovery status", () => ctx.storage.transaction(async (transaction) => {
|
|
329
|
+
const changed = [];
|
|
330
|
+
const newlyBlocked = [];
|
|
331
|
+
const faults = /* @__PURE__ */ new Map();
|
|
332
|
+
for (const [threadId, failure] of threads) {
|
|
333
|
+
const key = recoveryFaultKey(threadId);
|
|
334
|
+
const encoded = await transaction.get(key);
|
|
335
|
+
const previous = encoded === void 0 ? void 0 : decodeRecoveryFault(threadId, encoded);
|
|
336
|
+
if (failure === void 0) {
|
|
337
|
+
if (previous !== void 0) {
|
|
338
|
+
await transaction.delete(key);
|
|
339
|
+
changed.push(threadId);
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const fault = ThreadRecoveryFault.make({
|
|
344
|
+
schemaVersion: 1,
|
|
345
|
+
threadId,
|
|
346
|
+
firstFailedAt: previous?.firstFailedAt ?? now,
|
|
347
|
+
lastFailedAt: now,
|
|
348
|
+
attempts: Math.min(2147483647, (previous?.attempts ?? 0) + 1),
|
|
349
|
+
retryAt: now + Math.min(6e4, 5e3 * 2 ** Math.min(30, previous?.attempts ?? 0)),
|
|
350
|
+
failure
|
|
351
|
+
});
|
|
352
|
+
await transaction.put(key, encodeRecoveryFault(fault));
|
|
353
|
+
changed.push(threadId);
|
|
354
|
+
faults.set(threadId, fault);
|
|
355
|
+
if (previous === void 0) newlyBlocked.push(fault);
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
changed,
|
|
359
|
+
newlyBlocked,
|
|
360
|
+
faults
|
|
361
|
+
};
|
|
362
|
+
}));
|
|
363
|
+
yield* failpoint.hit("maintenance:recovery-status:after");
|
|
364
|
+
for (const threadId of retained.changed) yield* wakes.notify(threadId);
|
|
365
|
+
for (const fault of retained.newlyBlocked) yield* Effect.logError("Native Thread recovery blocked; accepted work remains pending", fault.failure.reason === "defect" ? Cause.die(fault.failure) : Cause.fail(fault.failure)).pipe(Effect.annotateLogs({ threadId: fault.threadId }));
|
|
366
|
+
return retained.faults;
|
|
367
|
+
});
|
|
368
|
+
const recoverThread = Effect.fn("ThreadMaintenance.recoverThread")(function* (threadId, recovery) {
|
|
369
|
+
const result = yield* runtime.runRecovery({ threadId });
|
|
370
|
+
const faults = yield* recordRecoveryStatus(result);
|
|
371
|
+
for (const report of result.reports) recovery.reports.set(report.submissionId, report);
|
|
372
|
+
recovery.faults.delete(threadId);
|
|
373
|
+
for (const [id, fault] of faults) recovery.faults.set(id, fault);
|
|
374
|
+
recovery.recovered += result.reports.length + result.blocked.length;
|
|
375
|
+
recovery.repaired ||= result.reports.some((report) => report.disposition === "repaired");
|
|
376
|
+
});
|
|
377
|
+
const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
|
|
378
|
+
yield* failpoint.hit("maintenance:ensure:before");
|
|
379
|
+
const now = yield* Clock.currentTimeMillis;
|
|
380
|
+
const retry = yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
381
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
382
|
+
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
383
|
+
if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, state.retry?.generation === state.dirty ? Math.max(now + minimumAlarmDelay, state.retry.notBefore) : now + config.wakeScanInterval);
|
|
384
|
+
return state.retry?.generation === state.dirty ? state.retry : void 0;
|
|
385
|
+
}));
|
|
386
|
+
const deadline = yield* pendingDeadline;
|
|
387
|
+
if (Option.isSome(deadline)) yield* runTransaction("ensure publication alarm", () => ctx.storage.transaction((transaction) => ensureTransactionAlarmBy(transaction, Math.max(now + minimumAlarmDelay, deadline.value <= now && retry !== void 0 && !retry.nativeOnly ? Math.max(deadline.value, retry.notBefore) : deadline.value))));
|
|
388
|
+
yield* failpoint.hit("maintenance:ensure:after");
|
|
389
|
+
});
|
|
390
|
+
const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* (observed) {
|
|
391
|
+
yield* failpoint.hit("maintenance:begin:before");
|
|
392
|
+
const now = yield* Clock.currentTimeMillis;
|
|
393
|
+
const result = yield* runTransaction("begin maintenance pass", () => ctx.storage.transaction(async (transaction) => {
|
|
394
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
395
|
+
observed.generation = state.dirty;
|
|
396
|
+
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
397
|
+
const retryAt = state.retry?.generation === state.dirty ? state.retry.notBefore : 0;
|
|
398
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
399
|
+
if (state.processed >= state.dirty || retryAt > now) return {
|
|
400
|
+
_tag: "CaughtUp",
|
|
401
|
+
nonterminal: state.nonterminal
|
|
402
|
+
};
|
|
403
|
+
return {
|
|
404
|
+
_tag: "Actionable",
|
|
405
|
+
generation: state.dirty,
|
|
406
|
+
nonterminal: state.nonterminal,
|
|
407
|
+
stalls: state.retry?.generation === state.dirty ? state.retry.stalls : 0
|
|
408
|
+
};
|
|
409
|
+
}));
|
|
410
|
+
yield* failpoint.hit("maintenance:begin:after");
|
|
411
|
+
return result;
|
|
412
|
+
});
|
|
413
|
+
const backoffDelay = (priorStalls, jitter) => {
|
|
414
|
+
const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** Math.min(priorStalls, 30));
|
|
415
|
+
return Math.ceil(backoff / 2 + backoff / 2 * jitter);
|
|
416
|
+
};
|
|
417
|
+
const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed, priorStalls) {
|
|
418
|
+
return progressed ? config.alarmBackoffBase : backoffDelay(priorStalls, yield* Random.next);
|
|
419
|
+
});
|
|
420
|
+
const rearmFailure = Effect.fn("ThreadMaintenance.rearmFailure")(function* (observed) {
|
|
421
|
+
const { generation, nativeOnly } = observed;
|
|
422
|
+
if (generation === void 0) return;
|
|
423
|
+
yield* failpoint.hit("maintenance:retry:before");
|
|
424
|
+
yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
425
|
+
const deadline = yield* pendingDeadline.pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
426
|
+
const now = yield* Clock.currentTimeMillis;
|
|
427
|
+
const jitter = yield* Random.next;
|
|
428
|
+
yield* runTransaction("back off failed maintenance", () => ctx.storage.transaction(async (transaction) => {
|
|
429
|
+
const { state } = await readMaintenanceState(transaction);
|
|
430
|
+
const previous = state.retry?.generation === generation && state.retry.nativeOnly === nativeOnly ? state.retry : void 0;
|
|
431
|
+
const retry = MaintenanceRetry.make({
|
|
432
|
+
generation,
|
|
433
|
+
notBefore: Math.max(previous?.notBefore ?? 0, now + backoffDelay(previous?.stalls ?? 0, jitter)),
|
|
434
|
+
nativeOnly,
|
|
435
|
+
stalls: Math.min(30, (previous?.stalls ?? 0) + 1)
|
|
436
|
+
});
|
|
437
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
|
|
438
|
+
...state,
|
|
439
|
+
retry
|
|
440
|
+
})));
|
|
441
|
+
const nativeDeadline = active > 0 || state.dirty !== generation ? now + minimumAlarmDelay : retry.notBefore;
|
|
442
|
+
await transaction.setAlarm(Math.max(now + minimumAlarmDelay, Option.isSome(deadline) && (nativeOnly || deadline.value > now) ? Math.min(nativeDeadline, deadline.value) : nativeDeadline));
|
|
443
|
+
}));
|
|
444
|
+
}));
|
|
445
|
+
yield* failpoint.hit("maintenance:retry:after");
|
|
446
|
+
});
|
|
447
|
+
const beginNative = Effect.fn("ThreadMaintenance.beginNative")(function* (observed) {
|
|
448
|
+
return yield* mutations.withSnapshot((activeAtStart) => Effect.gen(function* () {
|
|
449
|
+
const generation = yield* beginPass(observed);
|
|
450
|
+
if (generation._tag === "Actionable" && activeAtStart === 0) yield* publication.prepareGeneration(generation.generation);
|
|
451
|
+
return {
|
|
452
|
+
...generation,
|
|
453
|
+
activeAtStart
|
|
454
|
+
};
|
|
455
|
+
}));
|
|
456
|
+
});
|
|
457
|
+
const attemptSelected = Effect.fn("ThreadMaintenance.attemptSelected")(function* (selected, previousRetries, yieldAfter, recovery) {
|
|
458
|
+
yield* recoverThread(selected.threadId, recovery);
|
|
459
|
+
let bindingFailure;
|
|
460
|
+
const settlement = recovery.faults.has(selected.threadId) ? Option.none() : yield* runtime.processThreadHead(selected.threadId, { yieldAfter }).pipe(Effect.catchTag("BindingUnavailable", (failure) => {
|
|
461
|
+
bindingFailure = failure;
|
|
462
|
+
return Effect.succeed(Option.none());
|
|
463
|
+
}));
|
|
464
|
+
const previous = previousRetries.find((retry) => retry.submissionId === selected.submissionId);
|
|
465
|
+
let retry;
|
|
466
|
+
let reportBindingFailure = false;
|
|
467
|
+
if (bindingFailure !== void 0) {
|
|
468
|
+
const now = yield* Clock.currentTimeMillis;
|
|
469
|
+
const attempts = Math.min(30, (previous?.attempts ?? 0) + 1);
|
|
470
|
+
reportBindingFailure = previous === void 0 || now - previous.reportedAt >= 9e5;
|
|
471
|
+
retry = BindingRetry.make({
|
|
472
|
+
threadId: selected.threadId,
|
|
473
|
+
submissionId: selected.submissionId,
|
|
474
|
+
attempts,
|
|
475
|
+
notBefore: now + Math.min(6e4, 5e3 * 2 ** (attempts - 1)),
|
|
476
|
+
reportedAt: reportBindingFailure ? now : previous?.reportedAt ?? now
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
if (retry !== void 0 || previous !== void 0) {
|
|
480
|
+
yield* failpoint.hit("maintenance:binding-retry:before");
|
|
481
|
+
yield* runStorage("record submission binding retry", storage.transaction((transaction) => Effect.gen(function* () {
|
|
482
|
+
const encoded = yield* transaction.get(MAINTENANCE_STATE_KEY);
|
|
483
|
+
const state = encoded === void 0 ? initialMaintenanceState() : yield* Schema.decodeUnknownEffect(ThreadMaintenanceState)(encoded);
|
|
484
|
+
const bindingRetries = [...(state.bindingRetries ?? []).filter((entry) => entry.submissionId !== selected.submissionId), ...retry === void 0 ? [] : [retry]];
|
|
485
|
+
yield* transaction.put(MAINTENANCE_STATE_KEY, yield* Schema.encodeEffect(ThreadMaintenanceState)(ThreadMaintenanceState.make({
|
|
486
|
+
...state,
|
|
487
|
+
bindingRetries
|
|
488
|
+
})));
|
|
489
|
+
})).pipe(Effect.mapError(alarmFailure("record submission binding retry"))));
|
|
490
|
+
yield* failpoint.hit("maintenance:binding-retry:after");
|
|
491
|
+
}
|
|
492
|
+
if (bindingFailure !== void 0) yield* reportBindingFailure ? Effect.logError("Thread awaits a current agent binding; original work remains pending", Cause.fail(bindingFailure)) : Effect.logDebug("Thread binding retry remains pending", Cause.fail(bindingFailure));
|
|
493
|
+
return Option.isSome(settlement) ? 1 : 0;
|
|
494
|
+
});
|
|
495
|
+
const advance = Effect.fn("ThreadMaintenance.advance")(function* (started, yieldAfter, observed, recovery, native, dispatch = true, reserved = new Set(native.active.keys())) {
|
|
496
|
+
const deadline = yield* publication.pendingDeadline;
|
|
497
|
+
if (started._tag === "Actionable" || Option.isSome(deadline) && deadline.value <= (yield* Clock.currentTimeMillis)) yield* publication.drain;
|
|
498
|
+
const pending = yield* publication.pendingDeadline;
|
|
499
|
+
if (started._tag === "CaughtUp" || Option.isSome(pending)) {
|
|
500
|
+
const nextAttemptAt = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
501
|
+
const now = yield* Clock.currentTimeMillis;
|
|
502
|
+
const { state } = yield* runTransaction("read native maintenance deadline", () => ctx.storage.transaction((transaction) => readMaintenanceState(transaction)));
|
|
503
|
+
const native = active > 0 || state.dirty > state.processed ? state.retry?.generation === state.dirty && active === 0 ? state.retry.notBefore : now + minimumAlarmDelay : Infinity;
|
|
504
|
+
const next = Option.isSome(pending) ? pending.value : native;
|
|
505
|
+
return Number.isFinite(next) ? Math.max(now + minimumAlarmDelay, next) : void 0;
|
|
506
|
+
}));
|
|
507
|
+
return {
|
|
508
|
+
phase: "caught-up",
|
|
509
|
+
nonterminal: started.nonterminal,
|
|
510
|
+
nextAttemptAt
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (native.generation !== started.generation) {
|
|
514
|
+
native.deferred.clear();
|
|
515
|
+
native.generation = started.generation;
|
|
516
|
+
}
|
|
517
|
+
if (native.active.size > 0 && native.scanGeneration === started.generation) return {
|
|
518
|
+
phase: "actionable",
|
|
519
|
+
nonterminal: started.nonterminal,
|
|
520
|
+
nextAttemptAt: void 0
|
|
521
|
+
};
|
|
522
|
+
native.scanGeneration = started.activeAtStart === 0 ? started.generation : void 0;
|
|
523
|
+
observed.nativeOnly = true;
|
|
524
|
+
const observation = recovery.observation ??= {
|
|
525
|
+
generation: started.generation,
|
|
526
|
+
activeAtStart: started.activeAtStart
|
|
527
|
+
};
|
|
528
|
+
const current = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
529
|
+
const selectionTime = yield* Clock.currentTimeMillis;
|
|
530
|
+
yield* runTransaction("read Thread recovery deadlines", async () => {
|
|
531
|
+
for (const threadId of new Set(current.map((row) => row.threadId))) {
|
|
532
|
+
if (recovery.loaded.has(threadId)) continue;
|
|
533
|
+
const encoded = await ctx.storage.get(recoveryFaultKey(threadId));
|
|
534
|
+
if (encoded !== void 0) recovery.faults.set(threadId, decodeRecoveryFault(threadId, encoded));
|
|
535
|
+
recovery.loaded.add(threadId);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
const reports = recovery.reports;
|
|
539
|
+
const recoveryFaults = recovery.faults;
|
|
540
|
+
const waiting = (row) => !recovery.pending.has(row.threadId) && !recoveryFaults.has(row.threadId) && stableExternalWait(row, reports);
|
|
541
|
+
const heads = /* @__PURE__ */ new Map();
|
|
542
|
+
for (const row of current) {
|
|
543
|
+
if (row.state === "unknown" && waiting(row)) continue;
|
|
544
|
+
if (!heads.has(row.threadId)) heads.set(row.threadId, row);
|
|
545
|
+
}
|
|
546
|
+
const stopping = /* @__PURE__ */ new Set();
|
|
547
|
+
for (const head of heads.values()) {
|
|
548
|
+
if (head.state !== "ready" || native.active.has(head.threadId) || recovery.pending.has(head.threadId) || recoveryFaults.has(head.threadId)) continue;
|
|
549
|
+
const intent = yield* isolateRecovery(ledger.readAbortIntent(AbortIntentRequest.make({ submissionId: head.submissionId })), {
|
|
550
|
+
timeout: recoveryConfig.recoveryTimeout,
|
|
551
|
+
phase: () => "recovery",
|
|
552
|
+
operation: "read abort intent"
|
|
553
|
+
});
|
|
554
|
+
if (Result.isSuccess(intent)) {
|
|
555
|
+
if (intent.success !== void 0) stopping.add(head.threadId);
|
|
556
|
+
} else {
|
|
557
|
+
const faults = yield* recordRecoveryStatus(RecoverySweepResult.make({
|
|
558
|
+
reports: [],
|
|
559
|
+
blocked: [RecoveryBlocked.make({
|
|
560
|
+
threadId: head.threadId,
|
|
561
|
+
failure: intent.failure
|
|
562
|
+
})]
|
|
563
|
+
}));
|
|
564
|
+
for (const [threadId, fault] of faults) recoveryFaults.set(threadId, fault);
|
|
565
|
+
recovery.recovered++;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const eligible = [...heads.values()].filter((head) => !recovery.pending.has(head.threadId) && !stopping.has(head.threadId) && !recoveryFaults.has(head.threadId) && !waiting(head) && (head.state === "ready" || reports.has(head.submissionId))).map((head) => head.threadId).sort();
|
|
569
|
+
yield* failpoint.hit("maintenance:select:before");
|
|
570
|
+
const selection = yield* runTransaction("select maintenance lane", () => ctx.storage.transaction(async (transaction) => {
|
|
571
|
+
const { state } = await readMaintenanceState(transaction);
|
|
572
|
+
const retries = (state.bindingRetries ?? []).filter((retry) => heads.get(retry.threadId)?.submissionId === retry.submissionId);
|
|
573
|
+
const runnable = eligible.filter((threadId) => !reserved.has(threadId) && !native.active.has(threadId) && !native.deferred.has(threadId) && !retries.some((retry) => retry.threadId === threadId && retry.notBefore > selectionTime));
|
|
574
|
+
const pivot = Math.max(0, runnable.findIndex((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId));
|
|
575
|
+
const selected = dispatch ? [...runnable.slice(pivot), ...runnable.slice(0, pivot)].slice(0, nativeDispatchConcurrency - reserved.size) : [];
|
|
576
|
+
const next = selected.at(-1);
|
|
577
|
+
if (next !== void 0) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
|
|
578
|
+
...state,
|
|
579
|
+
lastServedThreadId: next
|
|
580
|
+
})));
|
|
581
|
+
const backlog = recovery.started ? [] : [...heads.keys()].filter((threadId) => {
|
|
582
|
+
const fault = recoveryFaults.get(threadId);
|
|
583
|
+
return !eligible.includes(threadId) && (fault === void 0 || fault.retryAt <= selectionTime);
|
|
584
|
+
});
|
|
585
|
+
const after = backlog.filter((threadId) => state.lastRecoveredThreadId === void 0 || threadId > state.lastRecoveredThreadId);
|
|
586
|
+
const before = backlog.filter((threadId) => state.lastRecoveredThreadId !== void 0 && threadId <= state.lastRecoveredThreadId);
|
|
587
|
+
return {
|
|
588
|
+
selected,
|
|
589
|
+
retries,
|
|
590
|
+
backlog: [...after, ...before]
|
|
591
|
+
};
|
|
592
|
+
}));
|
|
593
|
+
yield* failpoint.hit("maintenance:select:after");
|
|
594
|
+
if (!recovery.started) {
|
|
595
|
+
recovery.started = true;
|
|
596
|
+
recovery.needsCheckpoint = selection.backlog.length > 0;
|
|
597
|
+
for (const threadId of selection.backlog) recovery.pending.add(threadId);
|
|
598
|
+
yield* Deferred.succeed(recovery.queue, selection.backlog);
|
|
599
|
+
}
|
|
600
|
+
for (const threadId of selection.selected) {
|
|
601
|
+
const selected = heads.get(threadId);
|
|
602
|
+
if (selected === void 0) continue;
|
|
603
|
+
native.dispatched++;
|
|
604
|
+
native.active.set(threadId, yield* Effect.forkIn(attemptSelected(selected, selection.retries, yieldAfter, recovery), native.scope));
|
|
605
|
+
}
|
|
606
|
+
if (native.active.size > 0) return {
|
|
607
|
+
phase: "actionable",
|
|
608
|
+
nonterminal: current.length,
|
|
609
|
+
nextAttemptAt: void 0,
|
|
610
|
+
dispatched: selection.selected.length > 0
|
|
611
|
+
};
|
|
612
|
+
const retries = selection.retries;
|
|
613
|
+
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
614
|
+
const waitingHeads = /* @__PURE__ */ new Map();
|
|
615
|
+
const autonomous = remaining.some((snapshot) => {
|
|
616
|
+
if (snapshot.state === "unknown" && waiting(snapshot)) return false;
|
|
617
|
+
const headWaiting = waitingHeads.get(snapshot.threadId);
|
|
618
|
+
if (headWaiting === void 0) waitingHeads.set(snapshot.threadId, waiting(snapshot));
|
|
619
|
+
if (headWaiting === true && snapshot.state === "ready" && reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput") return false;
|
|
620
|
+
return !waiting(snapshot);
|
|
621
|
+
});
|
|
622
|
+
const progressed = native.progressed || recovery.repaired;
|
|
623
|
+
const now = yield* Clock.currentTimeMillis;
|
|
624
|
+
const ordinaryDelay = autonomous ? yield* rearmDelay(progressed, started.stalls) : 0;
|
|
625
|
+
const nextEligible = [...recoveryFaults.values()].map((fault) => fault.retryAt).concat(eligible.map((threadId) => retries.find((retry) => retry.submissionId === heads.get(threadId)?.submissionId)?.notBefore ?? now));
|
|
626
|
+
const retryDelay = nextEligible.length === 0 ? 0 : Math.max(0, Math.min(...nextEligible) - now);
|
|
627
|
+
const delay = Math.max(ordinaryDelay, retryDelay);
|
|
628
|
+
yield* failpoint.hit("maintenance:checkpoint:before");
|
|
629
|
+
const nextAttemptAt = yield* mutations.withSnapshot((active) => runTransaction("checkpoint native maintenance", () => ctx.storage.transaction(async (transaction) => {
|
|
630
|
+
const { state } = await readMaintenanceState(transaction);
|
|
631
|
+
const mutationOverlap = observation.activeAtStart > 0 || started.activeAtStart > 0 || active > 0;
|
|
632
|
+
const processed = autonomous || mutationOverlap ? state.processed : state.processed > observation.generation ? state.processed : observation.generation;
|
|
633
|
+
const canBackoff = !mutationOverlap && state.dirty === started.generation;
|
|
634
|
+
const next = ThreadMaintenanceState.make({
|
|
635
|
+
...Struct.omit(state, ["retry"]),
|
|
636
|
+
processed,
|
|
637
|
+
nonterminal: remaining.length,
|
|
638
|
+
bindingRetries: (state.bindingRetries ?? []).filter((retry) => remaining.some((row) => row.submissionId === retry.submissionId)),
|
|
639
|
+
...autonomous && !progressed && canBackoff ? { retry: MaintenanceRetry.make({
|
|
640
|
+
generation: started.generation,
|
|
641
|
+
notBefore: now + delay,
|
|
642
|
+
nativeOnly: true,
|
|
643
|
+
stalls: Math.min(30, started.stalls + 1)
|
|
644
|
+
}) } : {}
|
|
645
|
+
});
|
|
646
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
647
|
+
if (autonomous) return now + (canBackoff ? delay : minimumAlarmDelay);
|
|
648
|
+
return mutationOverlap || next.dirty > next.processed ? now + minimumAlarmDelay : void 0;
|
|
649
|
+
})));
|
|
650
|
+
yield* failpoint.hit("maintenance:checkpoint:after");
|
|
651
|
+
native.deferred.clear();
|
|
652
|
+
native.progressed = false;
|
|
653
|
+
native.needsCheckpoint = false;
|
|
654
|
+
return {
|
|
655
|
+
phase: "actionable",
|
|
656
|
+
nonterminal: remaining.length,
|
|
657
|
+
nextAttemptAt
|
|
658
|
+
};
|
|
659
|
+
});
|
|
660
|
+
const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter, dispatchUntil, observed) {
|
|
661
|
+
const notified = (yield* Stream.toPull(wakes.wakes)).pipe(Effect.asVoid, Effect.catch(() => Effect.never));
|
|
662
|
+
const started = yield* beginNative(observed);
|
|
663
|
+
const auxiliaryScope = yield* Effect.acquireRelease(Scope.make("parallel"), (scope, exit) => Scope.close(scope, exit));
|
|
664
|
+
const fork = (work) => Effect.forkIn(work, auxiliaryScope);
|
|
665
|
+
const lanes = host.lanes.map((lane) => ({
|
|
666
|
+
...lane,
|
|
667
|
+
initial: true,
|
|
668
|
+
check: true,
|
|
669
|
+
exhausted: false,
|
|
670
|
+
fiber: void 0
|
|
671
|
+
}));
|
|
672
|
+
let messageExhausted = false;
|
|
673
|
+
let delivery;
|
|
674
|
+
let failure;
|
|
675
|
+
const recovery = {
|
|
676
|
+
queue: yield* Deferred.make(),
|
|
677
|
+
pending: /* @__PURE__ */ new Set(),
|
|
678
|
+
loaded: /* @__PURE__ */ new Set(),
|
|
679
|
+
reports: /* @__PURE__ */ new Map(),
|
|
680
|
+
faults: /* @__PURE__ */ new Map(),
|
|
681
|
+
started: false,
|
|
682
|
+
needsCheckpoint: false,
|
|
683
|
+
recovered: 0,
|
|
684
|
+
repaired: false
|
|
685
|
+
};
|
|
686
|
+
const recoveryFiber = yield* fork(Effect.gen(function* () {
|
|
687
|
+
for (const threadId of yield* Deferred.await(recovery.queue)) {
|
|
688
|
+
yield* failpoint.hit("maintenance:select:before");
|
|
689
|
+
yield* runTransaction("select old recovery lane", () => ctx.storage.transaction(async (transaction) => {
|
|
690
|
+
const { state } = await readMaintenanceState(transaction);
|
|
691
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
|
|
692
|
+
...state,
|
|
693
|
+
lastRecoveredThreadId: threadId
|
|
694
|
+
})));
|
|
695
|
+
}));
|
|
696
|
+
yield* failpoint.hit("maintenance:select:after");
|
|
697
|
+
yield* recoverThread(threadId, recovery);
|
|
698
|
+
recovery.pending.delete(threadId);
|
|
699
|
+
yield* wakes.notify(threadId);
|
|
700
|
+
}
|
|
701
|
+
}));
|
|
702
|
+
const backfill = yield* fork(drainDue.pipe(Effect.provideService(ThreadProjectionMaintenance, projection), Effect.timeoutOption(config.projectionDispatchTimeoutMillis)));
|
|
703
|
+
let backfillObserved = false;
|
|
704
|
+
let recoveryObserved = false;
|
|
705
|
+
const dispatch = {
|
|
706
|
+
scope: auxiliaryScope,
|
|
707
|
+
active: /* @__PURE__ */ new Map(),
|
|
708
|
+
deferred: /* @__PURE__ */ new Set(),
|
|
709
|
+
dispatched: 0,
|
|
710
|
+
settled: 0,
|
|
711
|
+
progressed: false,
|
|
712
|
+
needsCheckpoint: false
|
|
713
|
+
};
|
|
714
|
+
let native = yield* fork(advance(started, yieldAfter, observed, recovery, dispatch));
|
|
715
|
+
let result = {
|
|
716
|
+
phase: "caught-up",
|
|
717
|
+
nonterminal: started.nonterminal,
|
|
718
|
+
nextAttemptAt: void 0
|
|
719
|
+
};
|
|
720
|
+
let phase = result.phase;
|
|
721
|
+
let nativeCheck = false;
|
|
722
|
+
const until = DateTime.toEpochMillis(yieldAfter);
|
|
723
|
+
const dispatchEnd = DateTime.toEpochMillis(dispatchUntil);
|
|
724
|
+
const checkLanes = () => {
|
|
725
|
+
for (const lane of lanes) lane.check = true;
|
|
726
|
+
};
|
|
727
|
+
while (true) {
|
|
728
|
+
if (native?.pollUnsafe() !== void 0) {
|
|
729
|
+
result = yield* Fiber.join(native);
|
|
730
|
+
native = void 0;
|
|
731
|
+
if (result.phase === "actionable") phase = "actionable";
|
|
732
|
+
observed.nativeOnly = dispatch.active.size > 0;
|
|
733
|
+
if (result.dispatched === true && dispatch.active.size < nativeDispatchConcurrency) {
|
|
734
|
+
dispatch.scanGeneration = void 0;
|
|
735
|
+
nativeCheck = true;
|
|
736
|
+
}
|
|
737
|
+
recovery.started = true;
|
|
738
|
+
yield* Deferred.succeed(recovery.queue, []);
|
|
739
|
+
checkLanes();
|
|
740
|
+
}
|
|
741
|
+
for (const [threadId, attempt] of dispatch.active) {
|
|
742
|
+
const exit = attempt.pollUnsafe();
|
|
743
|
+
if (exit === void 0) continue;
|
|
744
|
+
dispatch.active.delete(threadId);
|
|
745
|
+
dispatch.scanGeneration = void 0;
|
|
746
|
+
if (Exit.isFailure(exit)) return yield* Effect.failCause(exit.cause);
|
|
747
|
+
dispatch.settled += exit.value;
|
|
748
|
+
dispatch.progressed ||= exit.value > 0;
|
|
749
|
+
dispatch.needsCheckpoint = true;
|
|
750
|
+
if (exit.value === 0) dispatch.deferred.add(threadId);
|
|
751
|
+
nativeCheck = true;
|
|
752
|
+
checkLanes();
|
|
753
|
+
}
|
|
754
|
+
if (!recoveryObserved && recoveryFiber.pollUnsafe() !== void 0) {
|
|
755
|
+
yield* Fiber.join(recoveryFiber);
|
|
756
|
+
recoveryObserved = true;
|
|
757
|
+
dispatch.scanGeneration = void 0;
|
|
758
|
+
nativeCheck ||= recovery.needsCheckpoint;
|
|
759
|
+
}
|
|
760
|
+
if (!backfillObserved) {
|
|
761
|
+
const exit = backfill.pollUnsafe();
|
|
762
|
+
if (exit !== void 0) {
|
|
763
|
+
backfillObserved = true;
|
|
764
|
+
if (Exit.isFailure(exit)) failure ??= exit.cause;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
for (const lane of lanes) {
|
|
768
|
+
const exit = lane.fiber?.pollUnsafe();
|
|
769
|
+
if (exit === void 0) continue;
|
|
770
|
+
lane.fiber = void 0;
|
|
771
|
+
if (Exit.isFailure(exit)) {
|
|
772
|
+
failure ??= exit.cause;
|
|
773
|
+
lane.exhausted = true;
|
|
774
|
+
} else lane.check ||= exit.value;
|
|
775
|
+
}
|
|
776
|
+
const deliveryExit = delivery?.pollUnsafe();
|
|
777
|
+
if (deliveryExit !== void 0) {
|
|
778
|
+
delivery = void 0;
|
|
779
|
+
if (Exit.isFailure(deliveryExit)) {
|
|
780
|
+
failure ??= deliveryExit.cause;
|
|
781
|
+
messageExhausted = true;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const now = yield* Clock.currentTimeMillis;
|
|
785
|
+
if (now >= until) break;
|
|
786
|
+
for (const lane of lanes) {
|
|
787
|
+
if (lane.fiber !== void 0 || lane.exhausted || !lane.check) continue;
|
|
788
|
+
lane.check = false;
|
|
789
|
+
lane.fiber = yield* fork(Effect.gen(function* () {
|
|
790
|
+
yield* Schema.decodeEffect(AuxiliaryDispatchMillis)(lane.dispatchTimeoutMillis).pipe(Effect.mapError((cause) => DurableAlarmError.make({
|
|
791
|
+
operation: "host dispatch allowance",
|
|
792
|
+
message: "Declare an integer whole-wave allowance between 1 and 300000 milliseconds",
|
|
793
|
+
cause
|
|
794
|
+
})));
|
|
795
|
+
if ((yield* Clock.currentTimeMillis) + lane.dispatchTimeoutMillis > dispatchEnd) {
|
|
796
|
+
lane.exhausted = true;
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
return yield* Effect.gen(function* () {
|
|
800
|
+
if (!lane.initial) {
|
|
801
|
+
const deadline = yield* lane.pendingDeadline;
|
|
802
|
+
if (Option.isNone(deadline) || deadline.value > (yield* Clock.currentTimeMillis)) return false;
|
|
803
|
+
}
|
|
804
|
+
const selectedAt = yield* Clock.currentTimeMillis;
|
|
805
|
+
if (selectedAt >= until || selectedAt + lane.dispatchTimeoutMillis > dispatchEnd) {
|
|
806
|
+
lane.exhausted = true;
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
lane.initial = false;
|
|
810
|
+
yield* lane.run;
|
|
811
|
+
return true;
|
|
812
|
+
}).pipe(Effect.scoped, Effect.timeoutOrElse({
|
|
813
|
+
duration: lane.dispatchTimeoutMillis,
|
|
814
|
+
orElse: () => DurableAlarmError.make({
|
|
815
|
+
operation: "host dispatch allowance",
|
|
816
|
+
message: "The admitted host wave exceeded its allowance; durable work remains pending"
|
|
817
|
+
})
|
|
818
|
+
}));
|
|
819
|
+
}));
|
|
820
|
+
}
|
|
821
|
+
if (delivery === void 0 && !messageExhausted) {
|
|
822
|
+
const selected = yield* Effect.gen(function* () {
|
|
823
|
+
const deadline = yield* messages.pendingDeadline;
|
|
824
|
+
return Option.isSome(deadline) && deadline.value <= (yield* Clock.currentTimeMillis) ? Option.some(yield* messages.prepare) : Option.none();
|
|
825
|
+
}).pipe(Effect.exit);
|
|
826
|
+
if (Exit.isFailure(selected)) {
|
|
827
|
+
failure ??= selected.cause;
|
|
828
|
+
messageExhausted = true;
|
|
829
|
+
} else if (Option.isSome(selected.value)) {
|
|
830
|
+
const wave = selected.value.value;
|
|
831
|
+
const selectedAt = yield* Clock.currentTimeMillis;
|
|
832
|
+
if (selectedAt >= until || selectedAt + wave.timeoutMillis > dispatchEnd) messageExhausted = true;
|
|
833
|
+
else delivery = yield* fork(wave.run);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (native === void 0 && dispatch.active.size === 0 && recoveryObserved && !recovery.needsCheckpoint && backfillObserved && delivery === void 0 && lanes.every((lane) => lane.fiber === void 0)) break;
|
|
837
|
+
if (native === void 0 && nativeCheck && dispatch.active.size < nativeDispatchConcurrency) {
|
|
838
|
+
nativeCheck = false;
|
|
839
|
+
const nativeCheckpoint = dispatch.needsCheckpoint && dispatch.active.size === 0;
|
|
840
|
+
const checkpoint = recoveryObserved && recovery.needsCheckpoint || nativeCheckpoint;
|
|
841
|
+
if (checkpoint) recovery.needsCheckpoint = false;
|
|
842
|
+
const reserved = new Set(dispatch.active.keys());
|
|
843
|
+
native = yield* fork(Effect.gen(function* () {
|
|
844
|
+
const awakened = checkpoint ? started : yield* beginNative(observed);
|
|
845
|
+
return yield* advance(awakened, yieldAfter, observed, recovery, dispatch, !nativeCheckpoint && (!checkpoint || dispatch.settled === 0), reserved);
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
const next = Math.min(native === void 0 ? result.nextAttemptAt ?? Infinity : Infinity, now + config.wakeScanInterval, until);
|
|
849
|
+
const completing = [
|
|
850
|
+
...native === void 0 ? [] : [native],
|
|
851
|
+
...dispatch.active.values(),
|
|
852
|
+
...recoveryObserved ? [] : [recoveryFiber],
|
|
853
|
+
...backfillObserved ? [] : [backfill],
|
|
854
|
+
...delivery === void 0 ? [] : [delivery],
|
|
855
|
+
...lanes.flatMap((lane) => lane.fiber === void 0 ? [] : [lane.fiber])
|
|
856
|
+
];
|
|
857
|
+
if ((yield* Effect.raceAllFirst([...completing.map((fiber) => Fiber.await(fiber).pipe(Effect.as("completed"))), Effect.raceFirst(notified, Effect.sleep(Math.max(0, next - now))).pipe(Effect.as("scan"))])) === "scan") {
|
|
858
|
+
nativeCheck = true;
|
|
859
|
+
checkLanes();
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (native !== void 0) {
|
|
863
|
+
result = yield* Fiber.join(native);
|
|
864
|
+
if (result.phase === "actionable") phase = "actionable";
|
|
865
|
+
observed.nativeOnly = dispatch.active.size > 0;
|
|
866
|
+
recovery.started = true;
|
|
867
|
+
yield* Deferred.succeed(recovery.queue, []);
|
|
868
|
+
}
|
|
869
|
+
for (const attempt of dispatch.active.values()) {
|
|
870
|
+
const settled = yield* Fiber.join(attempt);
|
|
871
|
+
dispatch.settled += settled;
|
|
872
|
+
dispatch.progressed ||= settled > 0;
|
|
873
|
+
}
|
|
874
|
+
dispatch.active.clear();
|
|
875
|
+
observed.nativeOnly = false;
|
|
876
|
+
for (const fiber of [
|
|
877
|
+
recoveryFiber,
|
|
878
|
+
backfill,
|
|
879
|
+
...delivery === void 0 ? [] : [delivery],
|
|
880
|
+
...lanes.flatMap((lane) => lane.fiber === void 0 ? [] : [lane.fiber])
|
|
881
|
+
]) {
|
|
882
|
+
const exit = yield* Fiber.await(fiber);
|
|
883
|
+
if (Exit.isFailure(exit)) failure ??= exit.cause;
|
|
884
|
+
}
|
|
885
|
+
if (failure !== void 0) return yield* Effect.failCause(failure);
|
|
886
|
+
if (recovery.needsCheckpoint || dispatch.dispatched > 0) {
|
|
887
|
+
result = yield* advance(started, yieldAfter, observed, recovery, dispatch, false);
|
|
888
|
+
if (result.phase === "actionable") phase = "actionable";
|
|
889
|
+
}
|
|
890
|
+
yield* Scope.close(auxiliaryScope, Exit.void);
|
|
891
|
+
yield* failpoint.hit("maintenance:finish:before");
|
|
892
|
+
const disposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
893
|
+
const latest = yield* pendingDeadline;
|
|
894
|
+
const now = yield* Clock.currentTimeMillis;
|
|
895
|
+
return yield* runTransaction("finish maintenance event", () => ctx.storage.transaction(async (transaction) => {
|
|
896
|
+
const { state } = await readMaintenanceState(transaction);
|
|
897
|
+
const native = active > 0 || state.dirty > state.processed ? state.retry?.generation === state.dirty && active === 0 ? Math.max(now + minimumAlarmDelay, state.retry.notBefore) : state.dirty === observed.generation && active === 0 ? result.nextAttemptAt ?? now + config.wakeScanInterval : now + minimumAlarmDelay : Infinity;
|
|
898
|
+
const next = Option.isSome(latest) ? Math.min(native, latest.value) : native;
|
|
899
|
+
if (Number.isFinite(next)) {
|
|
900
|
+
await transaction.setAlarm(Math.max(now + minimumAlarmDelay, next));
|
|
901
|
+
return "rearmed";
|
|
902
|
+
}
|
|
903
|
+
await transaction.deleteAlarm();
|
|
904
|
+
return "cleared";
|
|
905
|
+
}));
|
|
906
|
+
}));
|
|
907
|
+
yield* failpoint.hit("maintenance:finish:after");
|
|
908
|
+
const report = MaintenancePassReport.make({
|
|
909
|
+
phase,
|
|
910
|
+
recovered: recovery.recovered,
|
|
911
|
+
settled: dispatch.settled,
|
|
912
|
+
nonterminal: result.nonterminal,
|
|
913
|
+
alarm: disposition
|
|
914
|
+
});
|
|
915
|
+
yield* Effect.annotateCurrentSpan({
|
|
916
|
+
phase: report.phase,
|
|
917
|
+
recovered: report.recovered,
|
|
918
|
+
settled: report.settled,
|
|
919
|
+
nonterminal: report.nonterminal,
|
|
920
|
+
alarm: report.alarm
|
|
921
|
+
});
|
|
922
|
+
return report;
|
|
923
|
+
});
|
|
924
|
+
return ThreadMaintenance.of({
|
|
925
|
+
pass: Effect.gen(function* () {
|
|
926
|
+
const now = yield* Clock.currentTimeMillis;
|
|
927
|
+
const yieldAfter = DateTime.makeUnsafe(now + 6e5);
|
|
928
|
+
const dispatchUntil = DateTime.makeUnsafe(now + 84e4);
|
|
929
|
+
const observed = { nativeOnly: false };
|
|
930
|
+
return yield* alarm.withWakesDeferred(maintenancePassGate.withPermit(Effect.scoped(pass(yieldAfter, dispatchUntil, observed)).pipe(Effect.onErrorIf(() => true, () => rearmFailure(observed)))));
|
|
931
|
+
}).pipe(Effect.timeoutOrElse({
|
|
932
|
+
duration: "14 minutes",
|
|
933
|
+
orElse: () => DurableAlarmError.make({
|
|
934
|
+
operation: "maintenance pass deadline",
|
|
935
|
+
message: "The maintenance event exceeded its 14 minute deadline; durable recovery remains pending"
|
|
936
|
+
})
|
|
937
|
+
})),
|
|
938
|
+
ensureAlarm: mutations.withSnapshot(() => ensureAlarm()),
|
|
939
|
+
recoveryStatus,
|
|
940
|
+
withMutation: (body) => mutations.withMutation(body.pipe(Effect.tap(() => publishCommitted.pipe(Effect.provideService(ThreadPublication, publication)))))
|
|
941
|
+
});
|
|
942
|
+
}));
|
|
943
|
+
};
|
|
944
|
+
//#endregion
|
|
945
|
+
export { DurableAlarmError, DurableAlarmService, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadRecoveryFault, publishCommitted, Alarm_exports as t };
|
|
946
|
+
|
|
947
|
+
//# sourceMappingURL=Alarm.mjs.map
|