@effect-agent/platform-cloudflare 0.1.0-beta.111 → 0.1.0-beta.112
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 +34 -8
- package/dist/Alarm.mjs +231 -41
- package/dist/Alarm.mjs.map +1 -1
- package/dist/CloudflareThreadClient.d.mts +35 -28
- package/dist/CloudflareThreadClient.mjs +1 -1
- package/dist/{ThreadObject-BGla_Obk.mjs → ThreadObject-0k-SVpY_.mjs} +2 -2
- package/dist/ThreadObject-0k-SVpY_.mjs.map +1 -0
- package/dist/{ThreadObject-Cmn8pAQ3.d.mts → ThreadObject-DVxgrU7c.d.mts} +16 -9
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/ThreadObject.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +432 -72
- package/src/ThreadObject.ts +2 -2
- package/dist/ThreadObject-BGla_Obk.mjs.map +0 -1
package/dist/Alarm.d.mts
CHANGED
|
@@ -2,13 +2,15 @@ import { DurableObjectContext } from "./CloudflareBindings.mjs";
|
|
|
2
2
|
import { s as CloudflareDurableRuntimeConfig } from "./CloudflareConfig-DKXGo8L3.mjs";
|
|
3
3
|
import { Cause, Context, DateTime, Effect, Layer, Option, Schema, Scope } from "effect";
|
|
4
4
|
import { DurableBindingFailure } from "effect-agent/agent-registration";
|
|
5
|
-
import { DurableAgentRuntime, DurableWorkerFailure } from "effect-agent/durable-agent-runtime";
|
|
5
|
+
import { DurableAgentRuntime, DurableRuntimeConfig, DurableWorkerFailure, RecoveryFailure } from "effect-agent/durable-agent-runtime";
|
|
6
|
+
import { ThreadId } from "effect-agent/identifiers";
|
|
7
|
+
import { OperationDenied } from "effect-agent/operation-authorizer";
|
|
6
8
|
import { SubmissionLedger } from "effect-agent/submission-ledger";
|
|
7
9
|
import { ThreadProjectionError, ThreadProjectionMaintenance } from "effect-agent/thread-projection-maintenance";
|
|
8
10
|
import { WakeScheduler } from "effect-agent/wake-scheduler";
|
|
9
11
|
import { SqlClient } from "effect/unstable/sql/SqlClient";
|
|
10
12
|
declare namespace Alarm_d_exports {
|
|
11
|
-
export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadPublicationService, publishCommitted };
|
|
13
|
+
export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadPublicationService, ThreadRecoveryFault, publishCommitted };
|
|
12
14
|
}
|
|
13
15
|
declare const DurableAlarmError_base: Schema.Class<DurableAlarmError, Schema.TaggedStruct<"DurableAlarmError", {
|
|
14
16
|
readonly operation: Schema.String;
|
|
@@ -64,7 +66,7 @@ export declare class DurableAlarmService extends DurableAlarmService_base {
|
|
|
64
66
|
declare const MaintenancePassReport_base: Schema.Class<MaintenancePassReport, Schema.Struct<{
|
|
65
67
|
/** `caught-up` ran no runtime work (publication may be pending); `actionable` ran recovery. */
|
|
66
68
|
readonly phase: Schema.Literals<readonly ["caught-up", "actionable"]>;
|
|
67
|
-
/** Recovery decisions
|
|
69
|
+
/** Recovery decisions and Thread faults observed during this event. */
|
|
68
70
|
readonly recovered: Schema.Int;
|
|
69
71
|
/** Head Attempts settled during the event. Joined input may settle with each head. */
|
|
70
72
|
readonly settled: Schema.Int;
|
|
@@ -76,7 +78,7 @@ declare const MaintenancePassReport_base: Schema.Class<MaintenancePassReport, Sc
|
|
|
76
78
|
/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
|
|
77
79
|
export declare class MaintenancePassReport extends MaintenancePassReport_base {}
|
|
78
80
|
/** Fault boundaries around every maintenance-owned durable mutation. */
|
|
79
|
-
export type ThreadMaintenanceFailpointLocation = "maintenance:dirty:before" | "maintenance:dirty:after" | "maintenance:mutation:armed" | "maintenance:mutation:finished" | "maintenance:ensure:before" | "maintenance:ensure:after" | "maintenance:begin:before" | "maintenance:begin:after" | "maintenance:select:before" | "maintenance:select:after" | "maintenance:binding-retry:before" | "maintenance:binding-retry:after" | "maintenance:retry:before" | "maintenance:retry:after" | "maintenance:checkpoint:before" | "maintenance:checkpoint:after" | "maintenance:finish:before" | "maintenance:finish:after";
|
|
81
|
+
export type ThreadMaintenanceFailpointLocation = "maintenance:dirty:before" | "maintenance:dirty:after" | "maintenance:mutation:armed" | "maintenance:mutation:finished" | "maintenance:ensure:before" | "maintenance:ensure:after" | "maintenance:begin:before" | "maintenance:begin:after" | "maintenance:select:before" | "maintenance:select:after" | "maintenance:binding-retry:before" | "maintenance:binding-retry:after" | "maintenance:recovery-status:before" | "maintenance:recovery-status:after" | "maintenance:retry:before" | "maintenance:retry:after" | "maintenance:checkpoint:before" | "maintenance:checkpoint:after" | "maintenance:finish:before" | "maintenance:finish:after";
|
|
80
82
|
export type ThreadMaintenanceFailpointHandler = (location: ThreadMaintenanceFailpointLocation) => Effect.Effect<void>;
|
|
81
83
|
declare const ThreadMaintenanceFailpoint_base: Context.ServiceClass<ThreadMaintenanceFailpoint, "@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint", {
|
|
82
84
|
readonly hit: ThreadMaintenanceFailpointHandler;
|
|
@@ -146,6 +148,23 @@ export declare const ThreadHostMaintenance: Context.Reference<{
|
|
|
146
148
|
}>;
|
|
147
149
|
/** @internal A committed source operation must not become a failed operation because delivery failed. */
|
|
148
150
|
export declare const publishCommitted: Effect.Effect<void, never, ThreadPublication>;
|
|
151
|
+
declare const ThreadRecoveryFault_base: Schema.Class<ThreadRecoveryFault, Schema.Struct<{
|
|
152
|
+
readonly schemaVersion: Schema.Literal<1>;
|
|
153
|
+
readonly threadId: Schema.brand<Schema.NonEmptyString, "@effect-agent/core/ThreadId">;
|
|
154
|
+
readonly firstFailedAt: Schema.Finite;
|
|
155
|
+
readonly lastFailedAt: Schema.Finite;
|
|
156
|
+
/** Earliest automatic recovery retry; new admissions do not erase this deadline. */
|
|
157
|
+
readonly retryAt: Schema.Finite;
|
|
158
|
+
/** Saturates at 2^31 - 1; one observation per Thread per recovery sweep. */
|
|
159
|
+
readonly attempts: Schema.Int;
|
|
160
|
+
readonly failure: typeof RecoveryFailure;
|
|
161
|
+
}>, {}>;
|
|
162
|
+
/**
|
|
163
|
+
* A durable observation of blocked recovery, independent of the execution journal. This is
|
|
164
|
+
* neither a Settlement nor proof that external effects did not happen. A successful recovery
|
|
165
|
+
* sweep clears it; repair must preserve canonical history and the original admission identity.
|
|
166
|
+
*/
|
|
167
|
+
export declare class ThreadRecoveryFault extends ThreadRecoveryFault_base {}
|
|
149
168
|
declare const ThreadMutationGate_base: Context.ServiceClass<ThreadMutationGate, "@effect-agent/platform-cloudflare/internal/ThreadMutationGate", {
|
|
150
169
|
readonly withMutation: <A, E, R>(body: Effect.Effect<A, E, R>,
|
|
151
170
|
/**
|
|
@@ -175,6 +194,13 @@ declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@
|
|
|
175
194
|
* generation has an alarm. It never scans the ledger or canonical history.
|
|
176
195
|
*/
|
|
177
196
|
readonly ensureAlarm: Effect.Effect<void, MaintenancePassFailure>;
|
|
197
|
+
/**
|
|
198
|
+
* Authorize `explain`, then read one bounded local record without reading execution history.
|
|
199
|
+
* None means no recorded fault, not proof of health or settlement. The host authenticates
|
|
200
|
+
* callers and verifies local Thread membership before exposing this service across RPC.
|
|
201
|
+
* Provide OperationAuthorizer when constructing this Layer, as for DurableAgentRuntime.
|
|
202
|
+
*/
|
|
203
|
+
readonly recoveryStatus: (threadId: ThreadId) => Effect.Effect<Option.Option<ThreadRecoveryFault>, DurableAlarmError | OperationDenied>;
|
|
178
204
|
/**
|
|
179
205
|
* Serialize the pre-arm boundary with pass acknowledgement, advance the durable dirty
|
|
180
206
|
* generation and arm the alarm in one transaction BEFORE running the caller's mutation.
|
|
@@ -191,9 +217,9 @@ declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@
|
|
|
191
217
|
* recovery, ledger scans or canonical-history reads.
|
|
192
218
|
* 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
|
|
193
219
|
* producer keeps its newer generation dirty. Native retries retain their durable backoff.
|
|
194
|
-
* 3.
|
|
195
|
-
*
|
|
196
|
-
*
|
|
220
|
+
* 3. Keep delivery admission open while old recovery is pending, so fresh native replies can
|
|
221
|
+
* publish in this event. Close once, at latest at the original ten-minute yield deadline;
|
|
222
|
+
* native wakes can still advance heads while admitted deliveries retire.
|
|
197
223
|
* 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill joins are
|
|
198
224
|
* bounded independently; incoming native work never restarts or cancels their attempts.
|
|
199
225
|
* Auxiliary failures are reported after the current native opportunity.
|
|
@@ -201,7 +227,7 @@ declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@
|
|
|
201
227
|
* The whole event retains one fourteen-minute cooperative timeout.
|
|
202
228
|
*/
|
|
203
229
|
export declare class ThreadMaintenance extends ThreadMaintenance_base {
|
|
204
|
-
static readonly layer: Layer.Layer<ThreadMaintenance, never, ThreadMutationGate | ThreadPublication | ThreadProjectionMaintenance | DurableAgentRuntime | SubmissionLedger | WakeScheduler | DurableAlarmService | ThreadMaintenanceFailpoint | CloudflareDurableRuntimeConfig | DurableObjectContext | SqlClient>;
|
|
230
|
+
static readonly layer: Layer.Layer<ThreadMaintenance, never, ThreadMutationGate | ThreadPublication | ThreadProjectionMaintenance | DurableAgentRuntime | DurableRuntimeConfig | SubmissionLedger | WakeScheduler | DurableAlarmService | ThreadMaintenanceFailpoint | CloudflareDurableRuntimeConfig | DurableObjectContext | SqlClient>;
|
|
205
231
|
}
|
|
206
232
|
//#endregion
|
|
207
233
|
export { Alarm_d_exports as t };
|
package/dist/Alarm.mjs
CHANGED
|
@@ -2,11 +2,12 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
|
2
2
|
import { DurableObjectContext } from "./CloudflareBindings.mjs";
|
|
3
3
|
import { AuxiliaryDispatchMillis, CloudflareDurableRuntimeConfig } from "./CloudflareConfig.mjs";
|
|
4
4
|
import { r as safeCauseMessage } from "./boundary-BguazVkh.mjs";
|
|
5
|
-
import { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Random, Ref, Schema, Scope, Semaphore, Stream, Struct } from "effect";
|
|
5
|
+
import { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Random, Ref, Result, Schema, Scope, Semaphore, Stream, Struct } from "effect";
|
|
6
6
|
import "effect-agent/agent-registration";
|
|
7
|
-
import { DurableAgentRuntime } from "effect-agent/durable-agent-runtime";
|
|
7
|
+
import { DurableAgentRuntime, DurableRuntimeConfig, RecoveryBlocked, RecoveryFailure, RecoverySweepResult, isolateRecovery } from "effect-agent/durable-agent-runtime";
|
|
8
8
|
import { SubmissionId, ThreadId } from "effect-agent/identifiers";
|
|
9
|
-
import {
|
|
9
|
+
import { OperationAuthorizationRequest, OperationAuthorizer } from "effect-agent/operation-authorizer";
|
|
10
|
+
import { AbortIntentRequest, SubmissionLedger } from "effect-agent/submission-ledger";
|
|
10
11
|
import { ThreadProjectionMaintenance, drainDue } from "effect-agent/thread-projection-maintenance";
|
|
11
12
|
import { WakeScheduler } from "effect-agent/wake-scheduler";
|
|
12
13
|
import { SqlClient } from "effect/unstable/sql/SqlClient";
|
|
@@ -21,6 +22,7 @@ var Alarm_exports = /* @__PURE__ */ __exportAll({
|
|
|
21
22
|
ThreadMessageDelivery: () => ThreadMessageDelivery,
|
|
22
23
|
ThreadMutationGate: () => ThreadMutationGate,
|
|
23
24
|
ThreadPublication: () => ThreadPublication,
|
|
25
|
+
ThreadRecoveryFault: () => ThreadRecoveryFault,
|
|
24
26
|
publishCommitted: () => publishCommitted
|
|
25
27
|
});
|
|
26
28
|
/**
|
|
@@ -87,7 +89,7 @@ var DurableAlarmService = class DurableAlarmService extends Context.Service()("@
|
|
|
87
89
|
var MaintenancePassReport = class extends Schema.Class("@effect-agent/platform-cloudflare/MaintenancePassReport")({
|
|
88
90
|
/** `caught-up` ran no runtime work (publication may be pending); `actionable` ran recovery. */
|
|
89
91
|
phase: Schema.Literals(["caught-up", "actionable"]),
|
|
90
|
-
/** Recovery decisions
|
|
92
|
+
/** Recovery decisions and Thread faults observed during this event. */
|
|
91
93
|
recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
92
94
|
/** Head Attempts settled during the event. Joined input may settle with each head. */
|
|
93
95
|
settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
@@ -155,6 +157,30 @@ var BindingRetry = class extends Schema.Class("BindingRetry")({
|
|
|
155
157
|
notBefore: Schema.Finite,
|
|
156
158
|
reportedAt: Schema.Finite
|
|
157
159
|
}) {};
|
|
160
|
+
/**
|
|
161
|
+
* A durable observation of blocked recovery, independent of the execution journal. This is
|
|
162
|
+
* neither a Settlement nor proof that external effects did not happen. A successful recovery
|
|
163
|
+
* sweep clears it; repair must preserve canonical history and the original admission identity.
|
|
164
|
+
*/
|
|
165
|
+
var ThreadRecoveryFault = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadRecoveryFault")({
|
|
166
|
+
schemaVersion: Schema.Literal(1),
|
|
167
|
+
threadId: ThreadId,
|
|
168
|
+
firstFailedAt: Schema.Finite,
|
|
169
|
+
lastFailedAt: Schema.Finite,
|
|
170
|
+
/** Earliest automatic recovery retry; new admissions do not erase this deadline. */
|
|
171
|
+
retryAt: Schema.Finite,
|
|
172
|
+
/** Saturates at 2^31 - 1; one observation per Thread per recovery sweep. */
|
|
173
|
+
attempts: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(2147483647)),
|
|
174
|
+
failure: RecoveryFailure
|
|
175
|
+
}) {};
|
|
176
|
+
const recoveryFaultKey = (threadId) => `effect-agent:thread-recovery-fault:v1:${threadId}`;
|
|
177
|
+
const decodeRecoveryFaultValue = Schema.decodeUnknownSync(ThreadRecoveryFault);
|
|
178
|
+
const decodeRecoveryFault = (threadId, encoded) => {
|
|
179
|
+
const fault = decodeRecoveryFaultValue(encoded);
|
|
180
|
+
if (fault.threadId !== threadId) throw new Error("Recovery status does not match its Thread key");
|
|
181
|
+
return fault;
|
|
182
|
+
};
|
|
183
|
+
const encodeRecoveryFault = Schema.encodeSync(ThreadRecoveryFault);
|
|
158
184
|
var MaintenanceRetry = class extends Schema.Class("MaintenanceRetry")({
|
|
159
185
|
generation: MaintenanceGeneration,
|
|
160
186
|
notBefore: Schema.Finite,
|
|
@@ -169,6 +195,8 @@ var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-
|
|
|
169
195
|
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
170
196
|
/** One physical-owner cursor; old single-lane records need no conversion. */
|
|
171
197
|
lastServedThreadId: Schema.optionalKey(ThreadId),
|
|
198
|
+
/** Rotate old recovery independently of dispatch, including after eviction or timeout. */
|
|
199
|
+
lastRecoveredThreadId: Schema.optionalKey(ThreadId),
|
|
172
200
|
bindingRetries: Schema.optionalKey(Schema.Array(BindingRetry)),
|
|
173
201
|
/** Absent on older records. A newer mutation makes this retry obsolete. */
|
|
174
202
|
retry: Schema.optionalKey(MaintenanceRetry)
|
|
@@ -258,9 +286,9 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
|
|
|
258
286
|
* recovery, ledger scans or canonical-history reads.
|
|
259
287
|
* 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
|
|
260
288
|
* producer keeps its newer generation dirty. Native retries retain their durable backoff.
|
|
261
|
-
* 3.
|
|
262
|
-
*
|
|
263
|
-
*
|
|
289
|
+
* 3. Keep delivery admission open while old recovery is pending, so fresh native replies can
|
|
290
|
+
* publish in this event. Close once, at latest at the original ten-minute yield deadline;
|
|
291
|
+
* native wakes can still advance heads while admitted deliveries retire.
|
|
264
292
|
* 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill joins are
|
|
265
293
|
* bounded independently; incoming native work never restarts or cancels their attempts.
|
|
266
294
|
* Auxiliary failures are reported after the current native opportunity.
|
|
@@ -270,6 +298,7 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
|
|
|
270
298
|
var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
|
|
271
299
|
static layer = Layer.effect(ThreadMaintenance)(Effect.gen(function* () {
|
|
272
300
|
const runtime = yield* DurableAgentRuntime;
|
|
301
|
+
const recoveryConfig = yield* DurableRuntimeConfig;
|
|
273
302
|
const ledger = yield* SubmissionLedger;
|
|
274
303
|
const wakes = yield* WakeScheduler;
|
|
275
304
|
const alarm = yield* DurableAlarmService;
|
|
@@ -281,6 +310,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
281
310
|
const projection = yield* ThreadProjectionMaintenance;
|
|
282
311
|
const messages = yield* ThreadMessageDelivery;
|
|
283
312
|
const host = yield* ThreadHostMaintenance;
|
|
313
|
+
const authorizer = yield* OperationAuthorizer;
|
|
284
314
|
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)))));
|
|
285
315
|
const pendingDeadline = Effect.gen(function* () {
|
|
286
316
|
return earliestDeadline(earliestDeadline(yield* publication.pendingDeadline, yield* messages.pendingDeadline), earliestDeadline(yield* projectionDeadline, yield* host.pendingDeadline));
|
|
@@ -288,6 +318,65 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
288
318
|
const maintenancePassGate = yield* Semaphore.make(1);
|
|
289
319
|
const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
|
|
290
320
|
const runTransaction = yield* makeStorageOperation;
|
|
321
|
+
const recoveryStatus = Effect.fn("ThreadMaintenance.recoveryStatus")(function* (threadId) {
|
|
322
|
+
yield* authorizer.authorize(OperationAuthorizationRequest.make({
|
|
323
|
+
operation: "explain",
|
|
324
|
+
threadId
|
|
325
|
+
}));
|
|
326
|
+
return yield* runTransaction("read Thread recovery status", async () => {
|
|
327
|
+
const encoded = await ctx.storage.get(recoveryFaultKey(threadId));
|
|
328
|
+
return encoded === void 0 ? Option.none() : Option.some(decodeRecoveryFault(threadId, encoded));
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
const recordRecoveryStatus = Effect.fn("ThreadMaintenance.recordRecoveryStatus")(function* (result) {
|
|
332
|
+
const threads = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const report of result.reports) threads.set(report.threadId, void 0);
|
|
334
|
+
for (const blocked of result.blocked) threads.set(blocked.threadId, blocked.failure);
|
|
335
|
+
if (threads.size === 0) return /* @__PURE__ */ new Map();
|
|
336
|
+
const now = yield* Clock.currentTimeMillis;
|
|
337
|
+
yield* failpoint.hit("maintenance:recovery-status:before");
|
|
338
|
+
const retained = yield* runTransaction("record Thread recovery status", () => ctx.storage.transaction(async (transaction) => {
|
|
339
|
+
const newlyBlocked = [];
|
|
340
|
+
const faults = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const [threadId, failure] of threads) {
|
|
342
|
+
const key = recoveryFaultKey(threadId);
|
|
343
|
+
const encoded = await transaction.get(key);
|
|
344
|
+
const previous = encoded === void 0 ? void 0 : decodeRecoveryFault(threadId, encoded);
|
|
345
|
+
if (failure === void 0) {
|
|
346
|
+
if (previous !== void 0) await transaction.delete(key);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const fault = ThreadRecoveryFault.make({
|
|
350
|
+
schemaVersion: 1,
|
|
351
|
+
threadId,
|
|
352
|
+
firstFailedAt: previous?.firstFailedAt ?? now,
|
|
353
|
+
lastFailedAt: now,
|
|
354
|
+
attempts: Math.min(2147483647, (previous?.attempts ?? 0) + 1),
|
|
355
|
+
retryAt: now + Math.min(6e4, 5e3 * 2 ** Math.min(30, previous?.attempts ?? 0)),
|
|
356
|
+
failure
|
|
357
|
+
});
|
|
358
|
+
await transaction.put(key, encodeRecoveryFault(fault));
|
|
359
|
+
faults.set(threadId, fault);
|
|
360
|
+
if (previous === void 0) newlyBlocked.push(fault);
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
newlyBlocked,
|
|
364
|
+
faults
|
|
365
|
+
};
|
|
366
|
+
}));
|
|
367
|
+
yield* failpoint.hit("maintenance:recovery-status:after");
|
|
368
|
+
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 }));
|
|
369
|
+
return retained.faults;
|
|
370
|
+
});
|
|
371
|
+
const recoverThread = Effect.fn("ThreadMaintenance.recoverThread")(function* (threadId, recovery) {
|
|
372
|
+
const result = yield* runtime.runRecovery({ threadId });
|
|
373
|
+
const faults = yield* recordRecoveryStatus(result);
|
|
374
|
+
for (const report of result.reports) recovery.reports.set(report.submissionId, report);
|
|
375
|
+
recovery.faults.delete(threadId);
|
|
376
|
+
for (const [id, fault] of faults) recovery.faults.set(id, fault);
|
|
377
|
+
recovery.recovered += result.reports.length + result.blocked.length;
|
|
378
|
+
recovery.repaired ||= result.reports.some((report) => report.disposition === "repaired");
|
|
379
|
+
});
|
|
291
380
|
const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
|
|
292
381
|
yield* failpoint.hit("maintenance:ensure:before");
|
|
293
382
|
const now = yield* Clock.currentTimeMillis;
|
|
@@ -368,7 +457,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
368
457
|
};
|
|
369
458
|
}));
|
|
370
459
|
});
|
|
371
|
-
const advance = Effect.fn("ThreadMaintenance.advance")(function* (started, yieldAfter, observed) {
|
|
460
|
+
const advance = Effect.fn("ThreadMaintenance.advance")(function* (started, yieldAfter, observed, recovery, dispatch = true) {
|
|
372
461
|
const deadline = yield* publication.pendingDeadline;
|
|
373
462
|
if (started._tag === "Actionable" || Option.isSome(deadline) && deadline.value <= (yield* Clock.currentTimeMillis)) yield* publication.drain;
|
|
374
463
|
const pending = yield* publication.pendingDeadline;
|
|
@@ -382,43 +471,91 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
382
471
|
}));
|
|
383
472
|
return {
|
|
384
473
|
phase: "caught-up",
|
|
385
|
-
recovered: 0,
|
|
386
474
|
settled: 0,
|
|
387
475
|
nonterminal: started.nonterminal,
|
|
388
476
|
nextAttemptAt
|
|
389
477
|
};
|
|
390
478
|
}
|
|
391
479
|
observed.nativeOnly = true;
|
|
392
|
-
const
|
|
393
|
-
|
|
480
|
+
const observation = recovery.observation ??= {
|
|
481
|
+
generation: started.generation,
|
|
482
|
+
activeAtStart: started.activeAtStart
|
|
483
|
+
};
|
|
394
484
|
const current = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
485
|
+
const selectionTime = yield* Clock.currentTimeMillis;
|
|
486
|
+
yield* runTransaction("read Thread recovery deadlines", async () => {
|
|
487
|
+
for (const threadId of new Set(current.map((row) => row.threadId))) {
|
|
488
|
+
if (recovery.loaded.has(threadId)) continue;
|
|
489
|
+
const encoded = await ctx.storage.get(recoveryFaultKey(threadId));
|
|
490
|
+
if (encoded !== void 0) recovery.faults.set(threadId, decodeRecoveryFault(threadId, encoded));
|
|
491
|
+
recovery.loaded.add(threadId);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
const reports = recovery.reports;
|
|
495
|
+
const recoveryFaults = recovery.faults;
|
|
496
|
+
const waiting = (row) => !recovery.pending.has(row.threadId) && !recoveryFaults.has(row.threadId) && stableExternalWait(row, reports);
|
|
395
497
|
const heads = /* @__PURE__ */ new Map();
|
|
396
498
|
for (const row of current) {
|
|
397
|
-
if (row.state === "unknown" &&
|
|
499
|
+
if (row.state === "unknown" && waiting(row)) continue;
|
|
398
500
|
if (!heads.has(row.threadId)) heads.set(row.threadId, row);
|
|
399
501
|
}
|
|
400
|
-
const
|
|
401
|
-
const
|
|
502
|
+
const stopping = /* @__PURE__ */ new Set();
|
|
503
|
+
for (const head of heads.values()) {
|
|
504
|
+
if (head.state !== "ready" || recovery.pending.has(head.threadId) || recoveryFaults.has(head.threadId)) continue;
|
|
505
|
+
const intent = yield* isolateRecovery(ledger.readAbortIntent(AbortIntentRequest.make({ submissionId: head.submissionId })), {
|
|
506
|
+
timeout: recoveryConfig.recoveryTimeout,
|
|
507
|
+
phase: () => "recovery",
|
|
508
|
+
operation: "read abort intent"
|
|
509
|
+
});
|
|
510
|
+
if (Result.isSuccess(intent)) {
|
|
511
|
+
if (intent.success !== void 0) stopping.add(head.threadId);
|
|
512
|
+
} else {
|
|
513
|
+
const faults = yield* recordRecoveryStatus(RecoverySweepResult.make({
|
|
514
|
+
reports: [],
|
|
515
|
+
blocked: [RecoveryBlocked.make({
|
|
516
|
+
threadId: head.threadId,
|
|
517
|
+
failure: intent.failure
|
|
518
|
+
})]
|
|
519
|
+
}));
|
|
520
|
+
for (const [threadId, fault] of faults) recoveryFaults.set(threadId, fault);
|
|
521
|
+
recovery.recovered++;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
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();
|
|
402
525
|
yield* failpoint.hit("maintenance:select:before");
|
|
403
526
|
const selection = yield* runTransaction("select maintenance lane", () => ctx.storage.transaction(async (transaction) => {
|
|
404
527
|
const { state } = await readMaintenanceState(transaction);
|
|
405
528
|
const retries = (state.bindingRetries ?? []).filter((retry) => heads.get(retry.threadId)?.submissionId === retry.submissionId);
|
|
406
529
|
const runnable = eligible.filter((threadId) => !retries.some((retry) => retry.threadId === threadId && retry.notBefore > selectionTime));
|
|
407
|
-
const next = runnable.find((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId) ?? runnable[0];
|
|
530
|
+
const next = dispatch ? runnable.find((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId) ?? runnable[0] : void 0;
|
|
408
531
|
if (next !== void 0) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
|
|
409
532
|
...state,
|
|
410
533
|
lastServedThreadId: next
|
|
411
534
|
})));
|
|
535
|
+
const backlog = recovery.started ? [] : [...heads.keys()].filter((threadId) => {
|
|
536
|
+
const fault = recoveryFaults.get(threadId);
|
|
537
|
+
return !eligible.includes(threadId) && (fault === void 0 || fault.retryAt <= selectionTime);
|
|
538
|
+
});
|
|
539
|
+
const after = backlog.filter((threadId) => state.lastRecoveredThreadId === void 0 || threadId > state.lastRecoveredThreadId);
|
|
540
|
+
const before = backlog.filter((threadId) => state.lastRecoveredThreadId !== void 0 && threadId <= state.lastRecoveredThreadId);
|
|
412
541
|
return {
|
|
413
542
|
selected: next,
|
|
414
|
-
retries
|
|
543
|
+
retries,
|
|
544
|
+
backlog: [...after, ...before]
|
|
415
545
|
};
|
|
416
546
|
}));
|
|
417
547
|
yield* failpoint.hit("maintenance:select:after");
|
|
548
|
+
if (!recovery.started) {
|
|
549
|
+
recovery.started = true;
|
|
550
|
+
recovery.needsCheckpoint = selection.backlog.length > 0;
|
|
551
|
+
for (const threadId of selection.backlog) recovery.pending.add(threadId);
|
|
552
|
+
yield* Deferred.succeed(recovery.queue, selection.backlog);
|
|
553
|
+
}
|
|
418
554
|
const selected = selection.selected === void 0 ? void 0 : heads.get(selection.selected);
|
|
555
|
+
if (selected !== void 0) yield* recoverThread(selected.threadId, recovery);
|
|
419
556
|
let retries = selection.retries;
|
|
420
557
|
let bindingFailure;
|
|
421
|
-
const settlement = selected === void 0 ? Option.none() : yield* runtime.processThreadHead(selected.threadId, { yieldAfter }).pipe(Effect.catchTag("BindingUnavailable", (failure) => {
|
|
558
|
+
const settlement = selected === void 0 || recoveryFaults.has(selected.threadId) ? Option.none() : yield* runtime.processThreadHead(selected.threadId, { yieldAfter }).pipe(Effect.catchTag("BindingUnavailable", (failure) => {
|
|
422
559
|
bindingFailure = failure;
|
|
423
560
|
return Effect.succeed(Option.none());
|
|
424
561
|
}));
|
|
@@ -456,22 +593,22 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
456
593
|
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
457
594
|
const waitingHeads = /* @__PURE__ */ new Map();
|
|
458
595
|
const autonomous = remaining.some((snapshot) => {
|
|
459
|
-
if (snapshot.state === "unknown" &&
|
|
596
|
+
if (snapshot.state === "unknown" && waiting(snapshot)) return false;
|
|
460
597
|
const headWaiting = waitingHeads.get(snapshot.threadId);
|
|
461
|
-
if (headWaiting === void 0) waitingHeads.set(snapshot.threadId,
|
|
598
|
+
if (headWaiting === void 0) waitingHeads.set(snapshot.threadId, waiting(snapshot));
|
|
462
599
|
if (headWaiting === true && snapshot.state === "ready" && reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput") return false;
|
|
463
|
-
return !
|
|
600
|
+
return !waiting(snapshot);
|
|
464
601
|
});
|
|
465
|
-
const progressed = Option.isSome(settlement) ||
|
|
602
|
+
const progressed = Option.isSome(settlement) || recovery.repaired;
|
|
466
603
|
const now = yield* Clock.currentTimeMillis;
|
|
467
604
|
const ordinaryDelay = autonomous ? yield* rearmDelay(progressed, started.stalls) : 0;
|
|
468
|
-
const nextEligible = eligible.map((threadId) => retries.find((retry) => retry.submissionId === heads.get(threadId)?.submissionId)?.notBefore ?? now);
|
|
469
|
-
const
|
|
470
|
-
const delay = Math.max(ordinaryDelay,
|
|
605
|
+
const nextEligible = [...recoveryFaults.values()].map((fault) => fault.retryAt).concat(eligible.map((threadId) => retries.find((retry) => retry.submissionId === heads.get(threadId)?.submissionId)?.notBefore ?? now));
|
|
606
|
+
const retryDelay = nextEligible.length === 0 ? 0 : Math.max(0, Math.min(...nextEligible) - now);
|
|
607
|
+
const delay = Math.max(ordinaryDelay, retryDelay);
|
|
471
608
|
yield* failpoint.hit("maintenance:checkpoint:before");
|
|
472
609
|
const nextAttemptAt = yield* mutations.withSnapshot((active) => runTransaction("checkpoint native maintenance", () => ctx.storage.transaction(async (transaction) => {
|
|
473
610
|
const { state } = await readMaintenanceState(transaction);
|
|
474
|
-
const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed >
|
|
611
|
+
const processed = autonomous || observation.activeAtStart > 0 || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > observation.generation ? state.processed : observation.generation;
|
|
475
612
|
const next = ThreadMaintenanceState.make({
|
|
476
613
|
...Struct.omit(state, ["retry"]),
|
|
477
614
|
processed,
|
|
@@ -491,7 +628,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
491
628
|
yield* failpoint.hit("maintenance:checkpoint:after");
|
|
492
629
|
return {
|
|
493
630
|
phase: "actionable",
|
|
494
|
-
recovered: recovered.length,
|
|
495
631
|
settled: Option.isSome(settlement) ? 1 : 0,
|
|
496
632
|
nonterminal: remaining.length,
|
|
497
633
|
nextAttemptAt
|
|
@@ -499,8 +635,35 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
499
635
|
});
|
|
500
636
|
const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter, dispatchUntil, observed) {
|
|
501
637
|
const notified = (yield* Stream.toPull(wakes.wakes)).pipe(Effect.asVoid, Effect.catch(() => Effect.never));
|
|
502
|
-
|
|
638
|
+
const started = yield* beginNative(observed);
|
|
503
639
|
const auxiliaryScope = yield* Effect.acquireRelease(Scope.make("parallel"), (scope, exit) => Scope.close(scope, exit));
|
|
640
|
+
const recovery = {
|
|
641
|
+
queue: yield* Deferred.make(),
|
|
642
|
+
pending: /* @__PURE__ */ new Set(),
|
|
643
|
+
loaded: /* @__PURE__ */ new Set(),
|
|
644
|
+
reports: /* @__PURE__ */ new Map(),
|
|
645
|
+
faults: /* @__PURE__ */ new Map(),
|
|
646
|
+
started: false,
|
|
647
|
+
needsCheckpoint: false,
|
|
648
|
+
recovered: 0,
|
|
649
|
+
repaired: false
|
|
650
|
+
};
|
|
651
|
+
const recoveryFiber = yield* Effect.forkIn(Effect.gen(function* () {
|
|
652
|
+
for (const threadId of yield* Deferred.await(recovery.queue)) {
|
|
653
|
+
yield* failpoint.hit("maintenance:select:before");
|
|
654
|
+
yield* runTransaction("select old recovery lane", () => ctx.storage.transaction(async (transaction) => {
|
|
655
|
+
const { state } = await readMaintenanceState(transaction);
|
|
656
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
|
|
657
|
+
...state,
|
|
658
|
+
lastRecoveredThreadId: threadId
|
|
659
|
+
})));
|
|
660
|
+
}));
|
|
661
|
+
yield* failpoint.hit("maintenance:select:after");
|
|
662
|
+
yield* recoverThread(threadId, recovery);
|
|
663
|
+
recovery.pending.delete(threadId);
|
|
664
|
+
yield* wakes.notify(threadId);
|
|
665
|
+
}
|
|
666
|
+
}), auxiliaryScope);
|
|
504
667
|
const dispatchClosed = yield* Deferred.make();
|
|
505
668
|
const stopDispatch = Deferred.await(dispatchClosed);
|
|
506
669
|
const deliveryFiber = yield* Effect.forkIn(Scope.provide(auxiliaryScope)(messages.drainUntil(stopDispatch, dispatchUntil)), auxiliaryScope);
|
|
@@ -513,35 +676,61 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
513
676
|
yield* host.drainUntil(stopDispatch, dispatchUntil);
|
|
514
677
|
})), auxiliaryScope);
|
|
515
678
|
const backfill = yield* Effect.forkIn(drainDue.pipe(Effect.provideService(ThreadProjectionMaintenance, projection), Effect.timeoutOption(config.projectionDispatchTimeoutMillis)), auxiliaryScope);
|
|
516
|
-
let result = yield* advance(started, yieldAfter, observed);
|
|
679
|
+
let result = yield* advance(started, yieldAfter, observed, recovery);
|
|
680
|
+
recovery.started = true;
|
|
681
|
+
yield* Deferred.succeed(recovery.queue, []);
|
|
517
682
|
let phase = result.phase;
|
|
518
|
-
let recovered = result.recovered;
|
|
519
683
|
let settled = result.settled;
|
|
520
684
|
observed.nativeOnly = false;
|
|
521
|
-
yield*
|
|
522
|
-
|
|
523
|
-
|
|
685
|
+
const hostJoin = yield* Effect.forkIn(Effect.gen(function* () {
|
|
686
|
+
yield* stopDispatch;
|
|
687
|
+
const remaining = Math.max(1, DateTime.toEpochMillis(dispatchUntil) - (yield* Clock.currentTimeMillis));
|
|
688
|
+
const outcome = yield* Fiber.join(hostFiber).pipe(Effect.timeoutOption(Math.min(host.dispatchTimeoutMillis, remaining)));
|
|
689
|
+
yield* Effect.annotateCurrentSpan({ "host.timedOut": Option.isNone(outcome) });
|
|
690
|
+
}), auxiliaryScope);
|
|
524
691
|
const retired = yield* Effect.forkChild(Fiber.joinAll([
|
|
525
692
|
deliveryFiber,
|
|
526
693
|
hostJoin,
|
|
527
|
-
backfill
|
|
694
|
+
backfill,
|
|
695
|
+
recoveryFiber
|
|
528
696
|
]));
|
|
529
|
-
const auxiliaryPending = () => deliveryFiber.pollUnsafe() === void 0 || hostFiber.pollUnsafe() === void 0 || backfill.pollUnsafe() === void 0;
|
|
530
|
-
while (
|
|
697
|
+
const auxiliaryPending = () => deliveryFiber.pollUnsafe() === void 0 || hostFiber.pollUnsafe() === void 0 || backfill.pollUnsafe() === void 0 || recoveryFiber.pollUnsafe() === void 0;
|
|
698
|
+
while (true) {
|
|
699
|
+
const recoveryFinished = recoveryFiber.pollUnsafe() !== void 0;
|
|
700
|
+
if (recoveryFinished) {
|
|
701
|
+
if (recovery.needsCheckpoint) {
|
|
702
|
+
yield* Fiber.join(recoveryFiber);
|
|
703
|
+
result = yield* advance(started, yieldAfter, observed, recovery, settled === 0);
|
|
704
|
+
if (result.phase === "actionable") phase = "actionable";
|
|
705
|
+
settled += result.settled;
|
|
706
|
+
recovery.needsCheckpoint = false;
|
|
707
|
+
}
|
|
708
|
+
yield* Deferred.succeed(dispatchClosed, void 0);
|
|
709
|
+
}
|
|
710
|
+
if (retired.pollUnsafe() !== void 0 || !auxiliaryPending()) break;
|
|
531
711
|
const now = yield* Clock.currentTimeMillis;
|
|
532
712
|
const until = DateTime.toEpochMillis(yieldAfter);
|
|
533
713
|
if (now >= until) break;
|
|
534
714
|
const next = Math.min(result.nextAttemptAt ?? Infinity, now + config.wakeScanInterval, until);
|
|
535
|
-
|
|
715
|
+
const recoveryDone = recoveryFinished ? Effect.never : Fiber.await(recoveryFiber);
|
|
716
|
+
const ready = yield* Effect.raceFirst(Effect.raceFirst(notified, Effect.sleep(Math.max(0, next - now))).pipe(Effect.as("native")), Effect.raceFirst(recoveryDone.pipe(Effect.as("recovery")), Fiber.await(retired).pipe(Effect.as("retired"))));
|
|
717
|
+
if (ready === "recovery") continue;
|
|
718
|
+
if (ready === "retired" || retired.pollUnsafe() !== void 0) break;
|
|
719
|
+
if (!auxiliaryPending()) continue;
|
|
536
720
|
if ((yield* Clock.currentTimeMillis) >= until) break;
|
|
537
|
-
|
|
538
|
-
result = yield* advance(
|
|
721
|
+
const awakened = yield* beginNative(observed);
|
|
722
|
+
result = yield* advance(awakened, yieldAfter, observed, recovery);
|
|
539
723
|
if (result.phase === "actionable") phase = "actionable";
|
|
540
|
-
recovered += result.recovered;
|
|
541
724
|
settled += result.settled;
|
|
542
725
|
observed.nativeOnly = false;
|
|
543
726
|
}
|
|
727
|
+
yield* Deferred.succeed(dispatchClosed, void 0);
|
|
544
728
|
yield* Fiber.join(retired);
|
|
729
|
+
if (recovery.needsCheckpoint) {
|
|
730
|
+
result = yield* advance(started, yieldAfter, observed, recovery, false);
|
|
731
|
+
if (result.phase === "actionable") phase = "actionable";
|
|
732
|
+
settled += result.settled;
|
|
733
|
+
}
|
|
545
734
|
yield* Scope.close(auxiliaryScope, Exit.void);
|
|
546
735
|
yield* failpoint.hit("maintenance:finish:before");
|
|
547
736
|
const disposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
@@ -562,7 +751,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
562
751
|
yield* failpoint.hit("maintenance:finish:after");
|
|
563
752
|
const report = MaintenancePassReport.make({
|
|
564
753
|
phase,
|
|
565
|
-
recovered,
|
|
754
|
+
recovered: recovery.recovered,
|
|
566
755
|
settled,
|
|
567
756
|
nonterminal: result.nonterminal,
|
|
568
757
|
alarm: disposition
|
|
@@ -591,11 +780,12 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
591
780
|
})
|
|
592
781
|
})),
|
|
593
782
|
ensureAlarm: mutations.withSnapshot(() => ensureAlarm()),
|
|
783
|
+
recoveryStatus,
|
|
594
784
|
withMutation: (body) => mutations.withMutation(body.pipe(Effect.tap(() => publishCommitted.pipe(Effect.provideService(ThreadPublication, publication)))))
|
|
595
785
|
});
|
|
596
786
|
}));
|
|
597
787
|
};
|
|
598
788
|
//#endregion
|
|
599
|
-
export { DurableAlarmError, DurableAlarmService, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, publishCommitted, Alarm_exports as t };
|
|
789
|
+
export { DurableAlarmError, DurableAlarmService, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadRecoveryFault, publishCommitted, Alarm_exports as t };
|
|
600
790
|
|
|
601
791
|
//# sourceMappingURL=Alarm.mjs.map
|