@effect-agent/platform-cloudflare 0.1.0-beta.95 → 0.1.0-beta.97
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 +3 -3
- package/dist/Alarm.mjs +141 -51
- package/dist/Alarm.mjs.map +1 -1
- package/dist/{CloudflareConfig-D472JOQA.d.mts → CloudflareConfig-DKXGo8L3.d.mts} +5 -5
- package/dist/CloudflareConfig.d.mts +1 -1
- package/dist/CloudflareConfig.mjs +4 -4
- package/dist/CloudflareConfig.mjs.map +1 -1
- package/dist/CloudflareThreadClient.d.mts +126 -46
- package/dist/{ThreadObject-CZxuDWOb.d.mts → ThreadObject-COli4r4g.d.mts} +20 -20
- package/dist/ThreadObject-DXavwaU-.mjs.map +1 -1
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/index.d.mts +2 -2
- package/package.json +1 -1
- package/src/Alarm.ts +285 -67
- package/src/CloudflareConfig.ts +4 -4
- package/src/internal/layers.ts +2 -2
package/dist/Alarm.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DurableObjectContext } from "./CloudflareBindings.mjs";
|
|
2
|
-
import { s as CloudflareDurableRuntimeConfig } from "./CloudflareConfig-
|
|
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
5
|
import { DurableAgentRuntime, DurableWorkerFailure } from "effect-agent/durable-agent-runtime";
|
|
@@ -75,7 +75,7 @@ declare const MaintenancePassReport_base: Schema.Class<MaintenancePassReport, Sc
|
|
|
75
75
|
/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
|
|
76
76
|
declare class MaintenancePassReport extends MaintenancePassReport_base {}
|
|
77
77
|
/** Fault boundaries around every maintenance-owned durable mutation. */
|
|
78
|
-
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:finish:before" | "maintenance:finish:after";
|
|
78
|
+
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:finish:before" | "maintenance:finish:after";
|
|
79
79
|
type ThreadMaintenanceFailpointHandler = (location: ThreadMaintenanceFailpointLocation) => Effect.Effect<void>;
|
|
80
80
|
declare const ThreadMaintenanceFailpoint_base: Context.ServiceClass<ThreadMaintenanceFailpoint, "@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint", {
|
|
81
81
|
readonly hit: ThreadMaintenanceFailpointHandler;
|
|
@@ -167,7 +167,7 @@ declare class ThreadMutationGate extends ThreadMutationGate_base {
|
|
|
167
167
|
}
|
|
168
168
|
type MaintenancePassFailure = DurableWorkerFailure | DurableBindingFailure | DurableAlarmError | ThreadProjectionError;
|
|
169
169
|
declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@effect-agent/platform-cloudflare/ThreadMaintenance", {
|
|
170
|
-
/** One idempotent
|
|
170
|
+
/** One idempotent pass; failures propagate after durably scheduling bounded recovery. */
|
|
171
171
|
readonly pass: Effect.Effect<MaintenancePassReport, MaintenancePassFailure>;
|
|
172
172
|
/**
|
|
173
173
|
* Constructor gate: initialize/inspect only the O(1) maintenance record and ensure a dirty
|
package/dist/Alarm.mjs
CHANGED
|
@@ -2,10 +2,10 @@ 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 } from "effect";
|
|
5
|
+
import { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Random, Ref, Schema, Scope, Semaphore, Stream, Struct } from "effect";
|
|
6
6
|
import "effect-agent/agent-registration";
|
|
7
7
|
import { DurableAgentRuntime } from "effect-agent/durable-agent-runtime";
|
|
8
|
-
import { ThreadId } from "effect-agent/identifiers";
|
|
8
|
+
import { SubmissionId, ThreadId } from "effect-agent/identifiers";
|
|
9
9
|
import { SubmissionLedger } from "effect-agent/submission-ledger";
|
|
10
10
|
import { ThreadProjectionMaintenance, drainDue } from "effect-agent/thread-projection-maintenance";
|
|
11
11
|
import { SqlClient } from "effect/unstable/sql/SqlClient";
|
|
@@ -147,6 +147,19 @@ const publishCommitted = Effect.gen(function* () {
|
|
|
147
147
|
yield* publication.invalidate.pipe(Effect.andThen(publication.drain));
|
|
148
148
|
}).pipe(Effect.catchCause((cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.logError("Thread publication deferred after source commit", cause)));
|
|
149
149
|
const MaintenanceGeneration = Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n));
|
|
150
|
+
var BindingRetry = class extends Schema.Class("BindingRetry")({
|
|
151
|
+
threadId: ThreadId,
|
|
152
|
+
submissionId: SubmissionId,
|
|
153
|
+
attempts: Schema.Natural,
|
|
154
|
+
notBefore: Schema.Finite,
|
|
155
|
+
reportedAt: Schema.Finite
|
|
156
|
+
}) {};
|
|
157
|
+
var MaintenanceRetry = class extends Schema.Class("MaintenanceRetry")({
|
|
158
|
+
generation: MaintenanceGeneration,
|
|
159
|
+
notBefore: Schema.Finite,
|
|
160
|
+
nativeOnly: Schema.Boolean,
|
|
161
|
+
stalls: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(30))
|
|
162
|
+
}) {};
|
|
150
163
|
/** Versioned, platform-private maintenance state stored through Durable Object KV. */
|
|
151
164
|
var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadMaintenanceState")({
|
|
152
165
|
schemaVersion: Schema.Literal(1),
|
|
@@ -154,7 +167,10 @@ var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-
|
|
|
154
167
|
processed: MaintenanceGeneration,
|
|
155
168
|
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
156
169
|
/** One physical-owner cursor; old single-lane records need no conversion. */
|
|
157
|
-
lastServedThreadId: Schema.optionalKey(ThreadId)
|
|
170
|
+
lastServedThreadId: Schema.optionalKey(ThreadId),
|
|
171
|
+
bindingRetries: Schema.optionalKey(Schema.Array(BindingRetry)),
|
|
172
|
+
/** Absent on older records. A newer mutation makes this retry obsolete. */
|
|
173
|
+
retry: Schema.optionalKey(MaintenanceRetry)
|
|
158
174
|
}) {};
|
|
159
175
|
const MAINTENANCE_STATE_KEY = "effect-agent:thread-maintenance:v1";
|
|
160
176
|
const decodeMaintenanceState = Schema.decodeUnknownSync(ThreadMaintenanceState);
|
|
@@ -258,11 +274,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
258
274
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
259
275
|
const { ctx } = yield* DurableObjectContext;
|
|
260
276
|
const failpoint = yield* ThreadMaintenanceFailpoint;
|
|
261
|
-
/**
|
|
262
|
-
* Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
|
|
263
|
-
* restarts at zero and merely re-arms sooner than a long-lived one would have.
|
|
264
|
-
*/
|
|
265
|
-
const stalls = yield* Ref.make(0);
|
|
266
277
|
const mutations = yield* ThreadMutationGate;
|
|
267
278
|
const publication = yield* ThreadPublication;
|
|
268
279
|
const projection = yield* ThreadProjectionMaintenance;
|
|
@@ -278,48 +289,74 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
278
289
|
const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
|
|
279
290
|
yield* failpoint.hit("maintenance:ensure:before");
|
|
280
291
|
const now = yield* Clock.currentTimeMillis;
|
|
281
|
-
yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
292
|
+
const retry = yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
282
293
|
const { state, initialized } = await readMaintenanceState(transaction);
|
|
283
294
|
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
284
|
-
if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
|
|
295
|
+
if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, state.retry?.generation === state.dirty ? Math.max(now + minimumAlarmDelay, state.retry.notBefore) : now + config.wakeScanInterval);
|
|
296
|
+
return state.retry?.generation === state.dirty ? state.retry : void 0;
|
|
285
297
|
}));
|
|
286
298
|
const deadline = yield* pendingDeadline;
|
|
287
|
-
if (Option.isSome(deadline)) yield* runTransaction("ensure publication alarm", () => ctx.storage.transaction((transaction) => ensureTransactionAlarmBy(transaction, Math.max(now + minimumAlarmDelay, deadline.value))));
|
|
299
|
+
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))));
|
|
288
300
|
yield* failpoint.hit("maintenance:ensure:after");
|
|
289
301
|
});
|
|
290
|
-
const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
|
|
302
|
+
const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* (observed) {
|
|
291
303
|
yield* failpoint.hit("maintenance:begin:before");
|
|
292
304
|
const now = yield* Clock.currentTimeMillis;
|
|
293
305
|
const result = yield* runTransaction("begin maintenance pass", () => ctx.storage.transaction(async (transaction) => {
|
|
294
306
|
const { state, initialized } = await readMaintenanceState(transaction);
|
|
307
|
+
observed.generation = state.dirty;
|
|
295
308
|
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
296
|
-
|
|
297
|
-
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
298
|
-
return {
|
|
299
|
-
_tag: "CaughtUp",
|
|
300
|
-
nonterminal: state.nonterminal
|
|
301
|
-
};
|
|
302
|
-
}
|
|
309
|
+
const retryAt = state.retry?.generation === state.dirty ? state.retry.notBefore : 0;
|
|
303
310
|
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
311
|
+
if (state.processed >= state.dirty || retryAt > now) return {
|
|
312
|
+
_tag: "CaughtUp",
|
|
313
|
+
nonterminal: state.nonterminal
|
|
314
|
+
};
|
|
304
315
|
return {
|
|
305
316
|
_tag: "Actionable",
|
|
306
317
|
generation: state.dirty,
|
|
307
|
-
nonterminal: state.nonterminal
|
|
318
|
+
nonterminal: state.nonterminal,
|
|
319
|
+
stalls: state.retry?.generation === state.dirty ? state.retry.stalls : 0
|
|
308
320
|
};
|
|
309
321
|
}));
|
|
310
322
|
yield* failpoint.hit("maintenance:begin:after");
|
|
311
323
|
return result;
|
|
312
324
|
});
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const jittered = Math.ceil(backoff / 2 + backoff / 2 * jitter);
|
|
320
|
-
return Math.min(jittered, config.wakeScanInterval);
|
|
325
|
+
const backoffDelay = (priorStalls, jitter) => {
|
|
326
|
+
const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** Math.min(priorStalls, 30));
|
|
327
|
+
return Math.ceil(backoff / 2 + backoff / 2 * jitter);
|
|
328
|
+
};
|
|
329
|
+
const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed, priorStalls) {
|
|
330
|
+
return progressed ? config.alarmBackoffBase : backoffDelay(priorStalls, yield* Random.next);
|
|
321
331
|
});
|
|
322
|
-
const
|
|
332
|
+
const rearmFailure = Effect.fn("ThreadMaintenance.rearmFailure")(function* (observed) {
|
|
333
|
+
const { generation, nativeOnly } = observed;
|
|
334
|
+
if (generation === void 0) return;
|
|
335
|
+
yield* failpoint.hit("maintenance:retry:before");
|
|
336
|
+
yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
337
|
+
const deadline = yield* pendingDeadline.pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
338
|
+
const now = yield* Clock.currentTimeMillis;
|
|
339
|
+
const jitter = yield* Random.next;
|
|
340
|
+
yield* runTransaction("back off failed maintenance", () => ctx.storage.transaction(async (transaction) => {
|
|
341
|
+
const { state } = await readMaintenanceState(transaction);
|
|
342
|
+
const previous = state.retry?.generation === generation ? state.retry : void 0;
|
|
343
|
+
const retry = MaintenanceRetry.make({
|
|
344
|
+
generation,
|
|
345
|
+
notBefore: Math.max(previous?.notBefore ?? 0, now + backoffDelay(previous?.stalls ?? 0, jitter)),
|
|
346
|
+
nativeOnly,
|
|
347
|
+
stalls: Math.min(30, (previous?.stalls ?? 0) + 1)
|
|
348
|
+
});
|
|
349
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
|
|
350
|
+
...state,
|
|
351
|
+
retry
|
|
352
|
+
})));
|
|
353
|
+
const nativeDeadline = active > 0 || state.dirty !== generation ? now + minimumAlarmDelay : retry.notBefore;
|
|
354
|
+
await transaction.setAlarm(Math.max(now + minimumAlarmDelay, Option.isSome(deadline) && (nativeOnly || deadline.value > now) ? Math.min(nativeDeadline, deadline.value) : nativeDeadline));
|
|
355
|
+
}));
|
|
356
|
+
}));
|
|
357
|
+
yield* failpoint.hit("maintenance:retry:after");
|
|
358
|
+
});
|
|
359
|
+
const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter, dispatchUntil, observed) {
|
|
323
360
|
const annotate = (report) => Effect.annotateCurrentSpan({
|
|
324
361
|
phase: report.phase,
|
|
325
362
|
recovered: report.recovered,
|
|
@@ -328,7 +365,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
328
365
|
alarm: report.alarm
|
|
329
366
|
}).pipe(Effect.as(report));
|
|
330
367
|
const started = yield* mutations.withSnapshot((activeAtStart) => Effect.gen(function* () {
|
|
331
|
-
const generation = yield* beginPass();
|
|
368
|
+
const generation = yield* beginPass(observed);
|
|
332
369
|
if (generation._tag === "Actionable" && activeAtStart === 0) yield* publication.prepareGeneration(generation.generation);
|
|
333
370
|
return {
|
|
334
371
|
...generation,
|
|
@@ -370,7 +407,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
370
407
|
const now = yield* Clock.currentTimeMillis;
|
|
371
408
|
return yield* runTransaction("finish publication pass", () => ctx.storage.transaction(async (transaction) => {
|
|
372
409
|
const { state } = await readMaintenanceState(transaction);
|
|
373
|
-
const nativeDeadline = active > 0 || state.dirty > state.processed ? now + config.wakeScanInterval : Infinity;
|
|
410
|
+
const nativeDeadline = active > 0 || state.dirty > state.processed ? state.retry?.generation === state.dirty && active === 0 ? Math.max(now + minimumAlarmDelay, state.retry.notBefore) : now + config.wakeScanInterval : Infinity;
|
|
374
411
|
const next = Option.isSome(latest) ? Math.min(nativeDeadline, latest.value) : nativeDeadline;
|
|
375
412
|
if (Number.isFinite(next)) {
|
|
376
413
|
await transaction.setAlarm(Math.max(now + minimumAlarmDelay, next));
|
|
@@ -389,27 +426,69 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
389
426
|
alarm: disposition
|
|
390
427
|
}));
|
|
391
428
|
}
|
|
429
|
+
observed.nativeOnly = true;
|
|
392
430
|
const recovered = yield* runtime.runRecovery;
|
|
393
431
|
const reports = new Map(recovered.map((report) => [report.submissionId, report]));
|
|
394
432
|
const current = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
395
433
|
const heads = /* @__PURE__ */ new Map();
|
|
396
434
|
for (const row of current) if (!heads.has(row.threadId)) heads.set(row.threadId, row);
|
|
397
435
|
const eligible = [...heads.values()].filter((head) => !stableExternalWait(head, reports)).map((head) => head.threadId).sort();
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
436
|
+
const selectionTime = yield* Clock.currentTimeMillis;
|
|
437
|
+
yield* failpoint.hit("maintenance:select:before");
|
|
438
|
+
const selection = yield* runTransaction("select maintenance lane", () => ctx.storage.transaction(async (transaction) => {
|
|
439
|
+
const { state } = await readMaintenanceState(transaction);
|
|
440
|
+
const retries = (state.bindingRetries ?? []).filter((retry) => heads.get(retry.threadId)?.submissionId === retry.submissionId);
|
|
441
|
+
const runnable = eligible.filter((threadId) => !retries.some((retry) => retry.threadId === threadId && retry.notBefore > selectionTime));
|
|
442
|
+
const next = runnable.find((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId) ?? runnable[0];
|
|
443
|
+
if (next !== void 0) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
|
|
444
|
+
...state,
|
|
445
|
+
lastServedThreadId: next
|
|
446
|
+
})));
|
|
447
|
+
return {
|
|
448
|
+
selected: next,
|
|
449
|
+
retries
|
|
450
|
+
};
|
|
451
|
+
}));
|
|
452
|
+
yield* failpoint.hit("maintenance:select:after");
|
|
453
|
+
const selected = selection.selected === void 0 ? void 0 : heads.get(selection.selected);
|
|
454
|
+
let retries = selection.retries;
|
|
455
|
+
let bindingFailure;
|
|
456
|
+
const settlement = selected === void 0 ? Option.none() : yield* runtime.processThreadHead(selected.threadId, { yieldAfter }).pipe(Effect.catchTag(["BindingUnavailable", "BindingDigestMismatch"], (failure) => {
|
|
457
|
+
bindingFailure = failure;
|
|
458
|
+
return Effect.succeed(Option.none());
|
|
459
|
+
}));
|
|
460
|
+
if (selected !== void 0) {
|
|
461
|
+
const previous = retries.find((retry) => retry.submissionId === selected.submissionId);
|
|
462
|
+
let retry;
|
|
463
|
+
let reportBindingFailure = false;
|
|
464
|
+
if (bindingFailure !== void 0) {
|
|
465
|
+
const now = yield* Clock.currentTimeMillis;
|
|
466
|
+
const attempts = Math.min(30, (previous?.attempts ?? 0) + 1);
|
|
467
|
+
reportBindingFailure = previous === void 0 || now - previous.reportedAt >= 9e5;
|
|
468
|
+
retry = BindingRetry.make({
|
|
469
|
+
threadId: selected.threadId,
|
|
470
|
+
submissionId: selected.submissionId,
|
|
471
|
+
attempts,
|
|
472
|
+
notBefore: now + Math.min(6e4, 5e3 * 2 ** (attempts - 1)),
|
|
473
|
+
reportedAt: reportBindingFailure ? now : previous?.reportedAt ?? now
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (retry !== void 0 || previous !== void 0) {
|
|
477
|
+
yield* failpoint.hit("maintenance:binding-retry:before");
|
|
478
|
+
retries = yield* runTransaction("record submission binding retry", () => ctx.storage.transaction(async (transaction) => {
|
|
479
|
+
const { state } = await readMaintenanceState(transaction);
|
|
480
|
+
const bindingRetries = [...(state.bindingRetries ?? []).filter((entry) => entry.submissionId !== selected.submissionId), ...retry === void 0 ? [] : [retry]];
|
|
481
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
|
|
482
|
+
...state,
|
|
483
|
+
bindingRetries
|
|
484
|
+
})));
|
|
485
|
+
return bindingRetries;
|
|
486
|
+
}));
|
|
487
|
+
yield* failpoint.hit("maintenance:binding-retry:after");
|
|
488
|
+
}
|
|
489
|
+
if (bindingFailure !== void 0) yield* reportBindingFailure ? Effect.logError("Thread awaits a compatible binding; original work remains pending", Cause.fail(bindingFailure)) : Effect.logDebug("Thread binding retry remains pending", Cause.fail(bindingFailure));
|
|
411
490
|
}
|
|
412
|
-
|
|
491
|
+
observed.nativeOnly = false;
|
|
413
492
|
yield* finishAuxiliary;
|
|
414
493
|
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
415
494
|
const waitingHeads = /* @__PURE__ */ new Map();
|
|
@@ -420,8 +499,11 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
420
499
|
return !stableExternalWait(snapshot, reports);
|
|
421
500
|
});
|
|
422
501
|
const progressed = Option.isSome(settlement) || recovered.some((report) => report.disposition === "repaired");
|
|
423
|
-
const delay = autonomous ? yield* rearmDelay(progressed) : 0;
|
|
424
502
|
const now = yield* Clock.currentTimeMillis;
|
|
503
|
+
const ordinaryDelay = autonomous ? yield* rearmDelay(progressed, started.stalls) : 0;
|
|
504
|
+
const nextEligible = eligible.map((threadId) => retries.find((retry) => retry.submissionId === heads.get(threadId)?.submissionId)?.notBefore ?? now);
|
|
505
|
+
const bindingDelay = nextEligible.length === 0 ? 0 : Math.max(0, Math.min(...nextEligible) - now);
|
|
506
|
+
const delay = Math.max(ordinaryDelay, bindingDelay);
|
|
425
507
|
yield* failpoint.hit("maintenance:finish:before");
|
|
426
508
|
const alarmDisposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
427
509
|
const publicationDeadline = yield* pendingDeadline;
|
|
@@ -429,13 +511,21 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
429
511
|
const { state } = await readMaintenanceState(transaction);
|
|
430
512
|
const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > started.generation ? state.processed : started.generation;
|
|
431
513
|
const next = ThreadMaintenanceState.make({
|
|
432
|
-
...state,
|
|
514
|
+
...Struct.omit(state, ["retry"]),
|
|
433
515
|
processed,
|
|
434
|
-
nonterminal: remaining.length
|
|
516
|
+
nonterminal: remaining.length,
|
|
517
|
+
bindingRetries: (state.bindingRetries ?? []).filter((retry) => remaining.some((row) => row.submissionId === retry.submissionId)),
|
|
518
|
+
...autonomous && !progressed ? { retry: MaintenanceRetry.make({
|
|
519
|
+
generation: started.generation,
|
|
520
|
+
notBefore: now + delay,
|
|
521
|
+
nativeOnly: true,
|
|
522
|
+
stalls: Math.min(30, started.stalls + 1)
|
|
523
|
+
}) } : {}
|
|
435
524
|
});
|
|
436
525
|
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
437
526
|
if (autonomous) {
|
|
438
|
-
|
|
527
|
+
const nativeDeadline = started.activeAtStart > 0 || active > 0 || state.dirty !== started.generation ? now + minimumAlarmDelay : now + delay;
|
|
528
|
+
await transaction.setAlarm(Option.isSome(publicationDeadline) ? Math.max(now + minimumAlarmDelay, Math.min(nativeDeadline, publicationDeadline.value)) : nativeDeadline);
|
|
439
529
|
return "rearmed";
|
|
440
530
|
}
|
|
441
531
|
if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
|
|
@@ -451,7 +541,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
451
541
|
}));
|
|
452
542
|
}));
|
|
453
543
|
yield* failpoint.hit("maintenance:finish:after");
|
|
454
|
-
if (alarmDisposition === "cleared") yield* Ref.set(stalls, 0);
|
|
455
544
|
return yield* annotate(MaintenancePassReport.make({
|
|
456
545
|
phase: "actionable",
|
|
457
546
|
recovered: recovered.length,
|
|
@@ -465,7 +554,8 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
465
554
|
const now = yield* Clock.currentTimeMillis;
|
|
466
555
|
const yieldAfter = DateTime.makeUnsafe(now + 6e5);
|
|
467
556
|
const dispatchUntil = DateTime.makeUnsafe(now + 84e4);
|
|
468
|
-
|
|
557
|
+
const observed = { nativeOnly: false };
|
|
558
|
+
return yield* alarm.withWakesDeferred(maintenancePassGate.withPermit(Effect.scoped(pass(yieldAfter, dispatchUntil, observed)).pipe(Effect.onErrorIf(() => true, () => rearmFailure(observed)))));
|
|
469
559
|
}).pipe(Effect.timeoutOrElse({
|
|
470
560
|
duration: "14 minutes",
|
|
471
561
|
orElse: () => DurableAlarmError.make({
|