@effect-agent/platform-cloudflare 0.1.0-beta.44 → 0.1.0-beta.45
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/index.d.mts +30 -30
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{prepared-admission-BKp_Upw2.mjs → prepared-admission-BhW2eT_a.mjs} +3 -3
- package/dist/prepared-admission-BhW2eT_a.mjs.map +1 -0
- package/dist/scheduling.mjs +1 -1
- package/dist/subscriptions.mjs +1 -1
- package/package.json +1 -1
- package/src/alarm.ts +259 -265
- package/src/layers.ts +3 -3
- package/dist/prepared-admission-BKp_Upw2.mjs.map +0 -1
package/src/alarm.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type ResolvedBinding,
|
|
3
2
|
DurableAgentRuntime,
|
|
4
3
|
SubmissionLedger,
|
|
5
4
|
type DurableBindingFailure,
|
|
@@ -310,9 +309,7 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
310
309
|
) => Effect.Effect<A, E | DurableAlarmError, R>;
|
|
311
310
|
}
|
|
312
311
|
>()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
|
|
313
|
-
static readonly layer
|
|
314
|
-
bindings: ReadonlyArray<ResolvedBinding>,
|
|
315
|
-
): Layer.Layer<
|
|
312
|
+
static readonly layer: Layer.Layer<
|
|
316
313
|
ThreadMaintenance,
|
|
317
314
|
never,
|
|
318
315
|
| DurableAgentRuntime
|
|
@@ -322,288 +319,285 @@ export class ThreadMaintenance extends Context.Service<
|
|
|
322
319
|
| CloudflareDurableRuntimeConfig
|
|
323
320
|
| ThreadObjectIdentity
|
|
324
321
|
| DurableObjectContext
|
|
325
|
-
>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
catch: alarmFailure(operation),
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* () {
|
|
365
|
-
yield* failpoint.hit("maintenance:dirty:before");
|
|
366
|
-
const now = yield* Clock.currentTimeMillis;
|
|
367
|
-
|
|
368
|
-
yield* runTransaction("advance maintenance generation", () =>
|
|
369
|
-
ctx.storage.transaction(async (transaction) => {
|
|
370
|
-
const { state } = await readMaintenanceState(transaction);
|
|
371
|
-
|
|
372
|
-
const next = ThreadMaintenanceState.make({
|
|
373
|
-
...state,
|
|
374
|
-
dirty: state.dirty + 1n,
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
378
|
-
// The earliest configured retry bounds a newly actionable mutation without relying
|
|
379
|
-
// on its best-effort immediate wake hint.
|
|
380
|
-
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
381
|
-
}),
|
|
382
|
-
);
|
|
383
|
-
yield* failpoint.hit("maintenance:dirty:after");
|
|
384
|
-
yield* Ref.update(activeMutations, (active) => active + 1);
|
|
322
|
+
> = Layer.effect(ThreadMaintenance)(
|
|
323
|
+
Effect.gen(function* () {
|
|
324
|
+
const runtime = yield* DurableAgentRuntime;
|
|
325
|
+
const ledger = yield* SubmissionLedger;
|
|
326
|
+
const alarm = yield* DurableAlarmService;
|
|
327
|
+
const config = yield* CloudflareDurableRuntimeConfig;
|
|
328
|
+
const identity = yield* ThreadObjectIdentity;
|
|
329
|
+
const { ctx } = yield* DurableObjectContext;
|
|
330
|
+
const failpoint = yield* ThreadMaintenanceFailpoint;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
|
|
334
|
+
* restarts at zero and merely re-arms sooner than a long-lived one would have.
|
|
335
|
+
*/
|
|
336
|
+
const stalls = yield* Ref.make(0);
|
|
337
|
+
/**
|
|
338
|
+
* Incarnation-local mutation count guarded with the generation transactions below. It is
|
|
339
|
+
* deliberately not durable: after eviction every begun mutation has stopped, while its
|
|
340
|
+
* pre-armed dirty generation remains durable for recovery. The short gate never spans the
|
|
341
|
+
* caller's mutation or cross-Object I/O.
|
|
342
|
+
*/
|
|
343
|
+
const activeMutations = yield* Ref.make(0);
|
|
344
|
+
const generationGate = yield* Semaphore.make(1);
|
|
345
|
+
// At-least-once deliveries are idempotent, but overlapping pass bodies could otherwise
|
|
346
|
+
// acknowledge state while a sibling pass is still mutating it. Port/RPC mutations do not
|
|
347
|
+
// take this permit, so cross-Object I/O cannot deadlock the maintenance serialization.
|
|
348
|
+
const maintenancePassGate = yield* Semaphore.make(1);
|
|
349
|
+
const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
|
|
350
|
+
|
|
351
|
+
const runTransaction = <A>(
|
|
352
|
+
operation: string,
|
|
353
|
+
transaction: () => Promise<A>,
|
|
354
|
+
): Effect.Effect<A, DurableAlarmError> =>
|
|
355
|
+
Effect.tryPromise({
|
|
356
|
+
try: transaction,
|
|
357
|
+
catch: alarmFailure(operation),
|
|
385
358
|
});
|
|
386
359
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
360
|
+
const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* () {
|
|
361
|
+
yield* failpoint.hit("maintenance:dirty:before");
|
|
362
|
+
const now = yield* Clock.currentTimeMillis;
|
|
390
363
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
Effect.acquireUseRelease(
|
|
395
|
-
generationGate.withPermit(beginMutation()),
|
|
396
|
-
() =>
|
|
397
|
-
failpoint.hit("maintenance:mutation:armed").pipe(
|
|
398
|
-
Effect.andThen(body),
|
|
399
|
-
Effect.tap(() => failpoint.hit("maintenance:mutation:finished")),
|
|
400
|
-
),
|
|
401
|
-
() => endMutation,
|
|
402
|
-
);
|
|
364
|
+
yield* runTransaction("advance maintenance generation", () =>
|
|
365
|
+
ctx.storage.transaction(async (transaction) => {
|
|
366
|
+
const { state } = await readMaintenanceState(transaction);
|
|
403
367
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
368
|
+
const next = ThreadMaintenanceState.make({
|
|
369
|
+
...state,
|
|
370
|
+
dirty: state.dirty + 1n,
|
|
371
|
+
});
|
|
407
372
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
373
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
374
|
+
// The earliest configured retry bounds a newly actionable mutation without relying
|
|
375
|
+
// on its best-effort immediate wake hint.
|
|
376
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
yield* failpoint.hit("maintenance:dirty:after");
|
|
380
|
+
yield* Ref.update(activeMutations, (active) => active + 1);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const endMutation = generationGate.withPermit(
|
|
384
|
+
Ref.update(activeMutations, (active) => Math.max(0, active - 1)),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const withMutation = <A, E, R>(
|
|
388
|
+
body: Effect.Effect<A, E, R>,
|
|
389
|
+
): Effect.Effect<A, E | DurableAlarmError, R> =>
|
|
390
|
+
Effect.acquireUseRelease(
|
|
391
|
+
generationGate.withPermit(beginMutation()),
|
|
392
|
+
() =>
|
|
393
|
+
failpoint.hit("maintenance:mutation:armed").pipe(
|
|
394
|
+
Effect.andThen(body),
|
|
395
|
+
Effect.tap(() => failpoint.hit("maintenance:mutation:finished")),
|
|
396
|
+
),
|
|
397
|
+
() => endMutation,
|
|
398
|
+
);
|
|
411
399
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
400
|
+
const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
|
|
401
|
+
yield* failpoint.hit("maintenance:ensure:before");
|
|
402
|
+
const now = yield* Clock.currentTimeMillis;
|
|
403
|
+
|
|
404
|
+
yield* runTransaction("ensure maintenance alarm", () =>
|
|
405
|
+
ctx.storage.transaction(async (transaction) => {
|
|
406
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
407
|
+
|
|
408
|
+
if (!initialized) {
|
|
409
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
410
|
+
}
|
|
411
|
+
if (state.dirty > state.processed) {
|
|
412
|
+
await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
|
|
413
|
+
}
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
yield* failpoint.hit("maintenance:ensure:after");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
|
|
420
|
+
yield* failpoint.hit("maintenance:begin:before");
|
|
421
|
+
const now = yield* Clock.currentTimeMillis;
|
|
422
|
+
|
|
423
|
+
const result = yield* runTransaction("begin maintenance pass", () =>
|
|
424
|
+
ctx.storage.transaction(async (transaction) => {
|
|
425
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
426
|
+
|
|
427
|
+
if (!initialized) {
|
|
428
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
429
|
+
}
|
|
430
|
+
if (state.processed >= state.dirty) {
|
|
431
|
+
await transaction.deleteAlarm();
|
|
432
|
+
|
|
433
|
+
return { _tag: "CaughtUp" as const, nonterminal: state.nonterminal };
|
|
434
|
+
}
|
|
435
|
+
// Pre-arm the earliest retry before recovery. A successful finish may move this slot
|
|
436
|
+
// LATER to its bounded backoff, which does not cancel the running handler.
|
|
437
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
438
|
+
|
|
439
|
+
return { _tag: "Actionable" as const, generation: state.dirty };
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
422
442
|
|
|
423
|
-
|
|
424
|
-
yield* failpoint.hit("maintenance:begin:before");
|
|
425
|
-
const now = yield* Clock.currentTimeMillis;
|
|
443
|
+
yield* failpoint.hit("maintenance:begin:after");
|
|
426
444
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const { state, initialized } = await readMaintenanceState(transaction);
|
|
445
|
+
return result;
|
|
446
|
+
});
|
|
430
447
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
await transaction.deleteAlarm();
|
|
448
|
+
const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed: boolean) {
|
|
449
|
+
const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>
|
|
450
|
+
progressed ? 0 : count + 1,
|
|
451
|
+
);
|
|
436
452
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
453
|
+
if (progressed) return config.alarmBackoffBase;
|
|
454
|
+
const exponent = Math.min(priorStalls, 30);
|
|
455
|
+
const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
|
|
456
|
+
const jitter = yield* Random.next;
|
|
457
|
+
// Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever
|
|
458
|
+
// waiting longer than the deterministic bound.
|
|
459
|
+
const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);
|
|
460
|
+
|
|
461
|
+
return Math.min(jittered, config.wakeScanInterval);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const pass = Effect.fn("ThreadMaintenance.pass")(function* (): Effect.fn.Return<
|
|
465
|
+
MaintenancePassReport,
|
|
466
|
+
MaintenancePassFailure
|
|
467
|
+
> {
|
|
468
|
+
const annotate = (report: MaintenancePassReport) =>
|
|
469
|
+
Effect.annotateCurrentSpan({
|
|
470
|
+
phase: report.phase,
|
|
471
|
+
recovered: report.recovered,
|
|
472
|
+
settled: report.settled,
|
|
473
|
+
nonterminal: report.nonterminal,
|
|
474
|
+
alarm: report.alarm,
|
|
475
|
+
}).pipe(Effect.as(report));
|
|
476
|
+
|
|
477
|
+
const started = yield* generationGate.withPermit(
|
|
478
|
+
Effect.gen(function* () {
|
|
479
|
+
const activeAtStart = yield* Ref.get(activeMutations);
|
|
480
|
+
const generation = yield* beginPass();
|
|
481
|
+
|
|
482
|
+
return { ...generation, activeAtStart };
|
|
483
|
+
}),
|
|
484
|
+
);
|
|
442
485
|
|
|
443
|
-
|
|
486
|
+
if (started._tag === "CaughtUp") {
|
|
487
|
+
return yield* annotate(
|
|
488
|
+
MaintenancePassReport.make({
|
|
489
|
+
phase: "caught-up",
|
|
490
|
+
recovered: 0,
|
|
491
|
+
settled: 0,
|
|
492
|
+
nonterminal: started.nonterminal,
|
|
493
|
+
alarm: "cleared",
|
|
444
494
|
}),
|
|
445
495
|
);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
496
|
+
}
|
|
497
|
+
// Step 2 — reconciliation strictly precedes new work in this pass (exit gate).
|
|
498
|
+
const recovered: ReadonlyArray<RecoveryReport> = yield* runtime.runRecovery;
|
|
499
|
+
// Step 3 — one bounded drain pass over this Object's own lane.
|
|
500
|
+
const settlements = yield* runtime.processThreadResolved(identity.threadId);
|
|
501
|
+
// Observe residual state before acknowledging this exact pass-start generation.
|
|
502
|
+
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
503
|
+
const reports = new Map(recovered.map((report) => [report.submissionId, report]));
|
|
504
|
+
const head = remaining[0];
|
|
505
|
+
const headWaiting = head !== undefined && stableExternalWait(head, reports);
|
|
506
|
+
|
|
507
|
+
const autonomous = remaining.some((snapshot, index) => {
|
|
508
|
+
// FIFO followers cannot execute through a stable external wait. Only plain queued
|
|
509
|
+
// input is dormant here; admission repairs and accepted aborts still need a pass.
|
|
510
|
+
if (
|
|
511
|
+
index > 0 &&
|
|
512
|
+
headWaiting &&
|
|
513
|
+
snapshot.state === "ready" &&
|
|
514
|
+
reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput"
|
|
515
|
+
)
|
|
516
|
+
return false;
|
|
517
|
+
|
|
518
|
+
return !stableExternalWait(snapshot, reports);
|
|
450
519
|
});
|
|
451
520
|
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
) {
|
|
455
|
-
const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>
|
|
456
|
-
progressed ? 0 : count + 1,
|
|
457
|
-
);
|
|
521
|
+
const progressed =
|
|
522
|
+
settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
|
|
458
523
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
|
|
462
|
-
const jitter = yield* Random.next;
|
|
463
|
-
// Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever
|
|
464
|
-
// waiting longer than the deterministic bound.
|
|
465
|
-
const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);
|
|
524
|
+
const delay = autonomous ? yield* rearmDelay(progressed) : 0;
|
|
525
|
+
const now = yield* Clock.currentTimeMillis;
|
|
466
526
|
|
|
467
|
-
|
|
468
|
-
});
|
|
527
|
+
yield* failpoint.hit("maintenance:finish:before");
|
|
469
528
|
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
> {
|
|
474
|
-
const annotate = (report: MaintenancePassReport) =>
|
|
475
|
-
Effect.annotateCurrentSpan({
|
|
476
|
-
phase: report.phase,
|
|
477
|
-
recovered: report.recovered,
|
|
478
|
-
settled: report.settled,
|
|
479
|
-
nonterminal: report.nonterminal,
|
|
480
|
-
alarm: report.alarm,
|
|
481
|
-
}).pipe(Effect.as(report));
|
|
482
|
-
|
|
483
|
-
const started = yield* generationGate.withPermit(
|
|
484
|
-
Effect.gen(function* () {
|
|
485
|
-
const activeAtStart = yield* Ref.get(activeMutations);
|
|
486
|
-
const generation = yield* beginPass();
|
|
487
|
-
|
|
488
|
-
return { ...generation, activeAtStart };
|
|
489
|
-
}),
|
|
490
|
-
);
|
|
529
|
+
const alarmDisposition = yield* generationGate.withPermit(
|
|
530
|
+
Effect.gen(function* () {
|
|
531
|
+
const active = yield* Ref.get(activeMutations);
|
|
491
532
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
phase: "caught-up",
|
|
496
|
-
recovered: 0,
|
|
497
|
-
settled: 0,
|
|
498
|
-
nonterminal: started.nonterminal,
|
|
499
|
-
alarm: "cleared",
|
|
500
|
-
}),
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
// Step 2 — reconciliation strictly precedes new work in this pass (exit gate).
|
|
504
|
-
const recovered: ReadonlyArray<RecoveryReport> = yield* runtime.runRecovery;
|
|
505
|
-
// Step 3 — one bounded drain pass over this Object's own lane.
|
|
506
|
-
const settlements = yield* runtime.processThreadResolved(identity.threadId, bindings);
|
|
507
|
-
// Observe residual state before acknowledging this exact pass-start generation.
|
|
508
|
-
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
509
|
-
const reports = new Map(recovered.map((report) => [report.submissionId, report]));
|
|
510
|
-
const head = remaining[0];
|
|
511
|
-
const headWaiting = head !== undefined && stableExternalWait(head, reports);
|
|
512
|
-
|
|
513
|
-
const autonomous = remaining.some((snapshot, index) => {
|
|
514
|
-
// FIFO followers cannot execute through a stable external wait. Only plain queued
|
|
515
|
-
// input is dormant here; admission repairs and accepted aborts still need a pass.
|
|
516
|
-
if (
|
|
517
|
-
index > 0 &&
|
|
518
|
-
headWaiting &&
|
|
519
|
-
snapshot.state === "ready" &&
|
|
520
|
-
reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput"
|
|
521
|
-
)
|
|
522
|
-
return false;
|
|
523
|
-
|
|
524
|
-
return !stableExternalWait(snapshot, reports);
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
const progressed =
|
|
528
|
-
settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
|
|
529
|
-
|
|
530
|
-
const delay = autonomous ? yield* rearmDelay(progressed) : 0;
|
|
531
|
-
const now = yield* Clock.currentTimeMillis;
|
|
532
|
-
|
|
533
|
-
yield* failpoint.hit("maintenance:finish:before");
|
|
533
|
+
return yield* runTransaction("finish maintenance pass", () =>
|
|
534
|
+
ctx.storage.transaction(async (transaction) => {
|
|
535
|
+
const { state } = await readMaintenanceState(transaction);
|
|
534
536
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const { state } = await readMaintenanceState(transaction);
|
|
542
|
-
|
|
543
|
-
// Autonomous work and in-flight mutations intentionally leave the observed
|
|
544
|
-
// generation dirty. Otherwise acknowledge only the pass-start generation.
|
|
545
|
-
const processed =
|
|
546
|
-
autonomous || started.activeAtStart > 0 || active > 0
|
|
537
|
+
// Autonomous work and in-flight mutations intentionally leave the observed
|
|
538
|
+
// generation dirty. Otherwise acknowledge only the pass-start generation.
|
|
539
|
+
const processed =
|
|
540
|
+
autonomous || started.activeAtStart > 0 || active > 0
|
|
541
|
+
? state.processed
|
|
542
|
+
: state.processed > started.generation
|
|
547
543
|
? state.processed
|
|
548
|
-
:
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
await transaction.deleteAlarm();
|
|
578
|
-
|
|
579
|
-
return "cleared" as const;
|
|
580
|
-
}),
|
|
581
|
-
);
|
|
582
|
-
}),
|
|
583
|
-
);
|
|
584
|
-
|
|
585
|
-
yield* failpoint.hit("maintenance:finish:after");
|
|
586
|
-
if (alarmDisposition === "cleared") {
|
|
587
|
-
yield* Ref.set(stalls, 0);
|
|
588
|
-
}
|
|
544
|
+
: started.generation;
|
|
545
|
+
|
|
546
|
+
const next = ThreadMaintenanceState.make({
|
|
547
|
+
...state,
|
|
548
|
+
processed,
|
|
549
|
+
nonterminal: remaining.length,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
553
|
+
if (autonomous) {
|
|
554
|
+
// Replace the crash-fallback slot with this pass's bounded backoff. The target
|
|
555
|
+
// is never earlier than the begin-pass fallback, so workerd does not cancel
|
|
556
|
+
// this running alarm handler before its report/span can complete.
|
|
557
|
+
await transaction.setAlarm(now + delay);
|
|
558
|
+
|
|
559
|
+
return "rearmed" as const;
|
|
560
|
+
}
|
|
561
|
+
if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
|
|
562
|
+
// A mutation overlapped this pass's observation window or raced
|
|
563
|
+
// acknowledgement. It stays dirty and its pre-armed bounded alarm survives;
|
|
564
|
+
// unseen effects are never acknowledged. Do not accelerate that future alarm
|
|
565
|
+
// from inside the current handler: workerd cancels a running handler when it
|
|
566
|
+
// writes an earlier slot.
|
|
567
|
+
await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
|
|
568
|
+
|
|
569
|
+
return "rearmed" as const;
|
|
570
|
+
}
|
|
571
|
+
await transaction.deleteAlarm();
|
|
589
572
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
nonterminal: remaining.length,
|
|
596
|
-
alarm: alarmDisposition,
|
|
597
|
-
}),
|
|
598
|
-
);
|
|
599
|
-
});
|
|
573
|
+
return "cleared" as const;
|
|
574
|
+
}),
|
|
575
|
+
);
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
600
578
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
579
|
+
yield* failpoint.hit("maintenance:finish:after");
|
|
580
|
+
if (alarmDisposition === "cleared") {
|
|
581
|
+
yield* Ref.set(stalls, 0);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return yield* annotate(
|
|
585
|
+
MaintenancePassReport.make({
|
|
586
|
+
phase: "actionable",
|
|
587
|
+
recovered: recovered.length,
|
|
588
|
+
settled: settlements.length,
|
|
589
|
+
nonterminal: remaining.length,
|
|
590
|
+
alarm: alarmDisposition,
|
|
591
|
+
}),
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
return ThreadMaintenance.of({
|
|
596
|
+
// A mid-pass immediate hint is droppable; durable dirty state decides the final alarm.
|
|
597
|
+
pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),
|
|
598
|
+
ensureAlarm: ensureAlarm(),
|
|
599
|
+
withMutation,
|
|
600
|
+
});
|
|
601
|
+
}),
|
|
602
|
+
);
|
|
609
603
|
}
|
package/src/layers.ts
CHANGED
|
@@ -70,7 +70,7 @@ import { cloudflareWakeSchedulerLayer } from "./wake-scheduler.ts";
|
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Raw (unvalidated) construction options for `ThreadObject.make`, mirroring
|
|
73
|
-
* `
|
|
73
|
+
* `NodeDurableAgentRuntimeOptions`. Optional fields default to the documented production values
|
|
74
74
|
* (`CLOUDFLARE_RUNTIME_DEFAULTS`); everything is schema-decoded into
|
|
75
75
|
* `CloudflareDurableRuntimeConfigValue` before any resource opens (deployment §5 gate 1).
|
|
76
76
|
*/
|
|
@@ -367,7 +367,7 @@ export const layerFromBindings = (
|
|
|
367
367
|
|
|
368
368
|
const base = Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer);
|
|
369
369
|
|
|
370
|
-
const runtimeStack = DurableAgentRuntime.
|
|
370
|
+
const runtimeStack = DurableAgentRuntime.layerWithBindings(bindings).pipe(
|
|
371
371
|
Layer.provideMerge(routedPorts),
|
|
372
372
|
Layer.provideMerge(cloudflareWakeSchedulerLayer),
|
|
373
373
|
Layer.provideMerge(base),
|
|
@@ -375,7 +375,7 @@ export const layerFromBindings = (
|
|
|
375
375
|
|
|
376
376
|
return Layer.mergeAll(
|
|
377
377
|
runtimeStack,
|
|
378
|
-
ThreadMaintenance.layer
|
|
378
|
+
ThreadMaintenance.layer.pipe(Layer.provide(runtimeStack)),
|
|
379
379
|
portsEndpointLayer,
|
|
380
380
|
);
|
|
381
381
|
}),
|