@effect-agent/platform-cloudflare 0.1.0-beta.94 → 0.1.0-beta.96
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 +39 -20
- package/dist/Alarm.mjs +200 -77
- package/dist/Alarm.mjs.map +1 -1
- package/dist/{CloudflareConfig-f3CqTel1.d.mts → CloudflareConfig-DKXGo8L3.d.mts} +12 -7
- package/dist/CloudflareConfig.d.mts +2 -2
- package/dist/CloudflareConfig.mjs +11 -5
- package/dist/CloudflareConfig.mjs.map +1 -1
- package/dist/CloudflareThreadClient.d.mts +99 -30
- package/dist/{ThreadObject-DvRG0dDz.mjs → ThreadObject-DXavwaU-.mjs} +34 -16
- package/dist/ThreadObject-DXavwaU-.mjs.map +1 -0
- package/dist/{ThreadObject-8LfGzzz4.d.mts → ThreadObject-DiqVDdJE.d.mts} +39 -37
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/ThreadObject.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +409 -117
- package/src/CloudflareConfig.ts +10 -4
- package/src/internal/layers.ts +9 -4
- package/src/internal/message-delivery.ts +69 -40
- package/dist/ThreadObject-DvRG0dDz.mjs.map +0 -1
package/dist/Alarm.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DurableObjectContext } from "./CloudflareBindings.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { Cause, Context,
|
|
2
|
+
import { s as CloudflareDurableRuntimeConfig } from "./CloudflareConfig-DKXGo8L3.mjs";
|
|
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";
|
|
6
6
|
import { SubmissionLedger } from "effect-agent/submission-ledger";
|
|
@@ -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;
|
|
@@ -92,6 +92,8 @@ declare class ThreadMaintenanceFailpoint extends ThreadMaintenanceFailpoint_base
|
|
|
92
92
|
* `invalidate`, `prepareGeneration` and `pendingDeadline` must be bounded local operations.
|
|
93
93
|
* `prepareGeneration` durably invalidates a scan only when its generation changes; repeated
|
|
94
94
|
* calls must preserve partial scan progress. It runs with no source mutation in flight.
|
|
95
|
+
* Use this gate only for publication required before dependent native execution. Independent
|
|
96
|
+
* UI relays and outboxes belong to ThreadHostMaintenance.
|
|
95
97
|
* `drain` performs bounded delivery and persists retries before returning. A pending deadline
|
|
96
98
|
* defers runtime recovery/Attempts, allowing committed host publications to drain first.
|
|
97
99
|
* Unexpected hook failures leave the prearmed generation for retry. Hooks acquire per-call
|
|
@@ -110,33 +112,46 @@ declare class ThreadPublication extends ThreadPublication_base {
|
|
|
110
112
|
static readonly layer: Layer.Layer<ThreadPublication, never, never>;
|
|
111
113
|
}
|
|
112
114
|
/**
|
|
113
|
-
* Host-assembled message recovery
|
|
114
|
-
*
|
|
115
|
-
*
|
|
115
|
+
* Host-assembled native message recovery. The driver bounds each actual Claim and persists its
|
|
116
|
+
* timeout/retry before this pump returns. Do not add a second timer starting at batch selection:
|
|
117
|
+
* local Claim setup may take time, and expiration still owes the driver's local retry commit.
|
|
116
118
|
*/
|
|
117
119
|
declare const ThreadMessageDelivery: Context.Reference<{
|
|
118
|
-
readonly
|
|
119
|
-
/** Drain inserts and due retries during source work, finishing the bounded wave on completion. */
|
|
120
|
-
readonly drainUntil?: (finished: Deferred.Deferred<void>) => Effect.Effect<void, DurableAlarmError>;
|
|
120
|
+
readonly drainUntil: (sourceFinished: Effect.Effect<void>, dispatchUntil: DateTime.Utc) => Effect.Effect<void, DurableAlarmError, Scope.Scope>;
|
|
121
121
|
readonly pendingDeadline: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
122
122
|
}>;
|
|
123
123
|
/**
|
|
124
|
-
* Application obligations sharing this Object's alarm.
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
124
|
+
* Application obligations sharing this Object's alarm. Admit one initial external wave even on
|
|
125
|
+
* a caught-up pass, then respond to wakes while native work runs. sourceFinished closes only
|
|
126
|
+
* admission of NEW external waves. Keep local admission/hub subscriptions in the supplied event
|
|
127
|
+
* Scope until maintenance tears it down; do not scope them to sourceFinished or to this Effect.
|
|
128
|
+
* No deadline sleeps or automatic retry loops. Return after already-admitted waves finish.
|
|
129
|
+
*
|
|
130
|
+
* Declare a finite whole-wave allowance (1..300000ms): maximum for parallel lanes, sum for
|
|
131
|
+
* sequential operations. Admit a wave only if its allowance fits before dispatchUntil. Later
|
|
132
|
+
* arrivals cannot renew the retirement window. Maintenance bounds the join after sourceFinished,
|
|
133
|
+
* interrupts and joins event Scope, then reads local deadlines under the mutation gate.
|
|
134
|
+
*
|
|
135
|
+
* Setup and pendingDeadline are bounded local operations. Persist claims/envelopes before
|
|
136
|
+
* dispatch; interrupted waits leave exact retries/receipts recoverable. Cancellation is local,
|
|
137
|
+
* not remote rollback. Retry accounting is unchanged. Network waits/finalizers must be
|
|
138
|
+
* interruptible; short local atomic commits may be uninterruptible. Hooks never write the raw
|
|
139
|
+
* alarm slot. Required native publication gates belong to ThreadPublication.
|
|
130
140
|
*/
|
|
131
141
|
declare const ThreadHostMaintenance: Context.Reference<{
|
|
142
|
+
readonly dispatchTimeoutMillis: number;
|
|
143
|
+
readonly drainUntil: (sourceFinished: Effect.Effect<void>, dispatchUntil: DateTime.Utc) => Effect.Effect<void, DurableAlarmError, Scope.Scope>;
|
|
132
144
|
readonly pendingDeadline: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
133
|
-
readonly drainUntil: (finished: Deferred.Deferred<void>) => Effect.Effect<void, DurableAlarmError>;
|
|
134
145
|
}>;
|
|
135
146
|
/** @internal A committed source operation must not become a failed operation because delivery failed. */
|
|
136
147
|
declare const publishCommitted: Effect.Effect<void, never, ThreadPublication>;
|
|
137
148
|
declare const ThreadMutationGate_base: Context.ServiceClass<ThreadMutationGate, "@effect-agent/platform-cloudflare/internal/ThreadMutationGate", {
|
|
138
149
|
readonly withMutation: <A, E, R>(body: Effect.Effect<A, E, R>,
|
|
139
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Native admission, approval, abort and unknown resolution keep the default true.
|
|
152
|
+
* Projection/relay/reply receipt-only bookkeeping must use false: its local durable
|
|
153
|
+
* pendingDeadline owns scheduling without creating native recovery debt.
|
|
154
|
+
*/
|
|
140
155
|
options?: {
|
|
141
156
|
readonly invalidatesRecovery: boolean;
|
|
142
157
|
}) => Effect.Effect<A, E | DurableAlarmError, R>;
|
|
@@ -152,7 +167,7 @@ declare class ThreadMutationGate extends ThreadMutationGate_base {
|
|
|
152
167
|
}
|
|
153
168
|
type MaintenancePassFailure = DurableWorkerFailure | DurableBindingFailure | DurableAlarmError | ThreadProjectionError;
|
|
154
169
|
declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@effect-agent/platform-cloudflare/ThreadMaintenance", {
|
|
155
|
-
/** One idempotent
|
|
170
|
+
/** One idempotent pass; failures propagate after durably scheduling bounded recovery. */
|
|
156
171
|
readonly pass: Effect.Effect<MaintenancePassReport, MaintenancePassFailure>;
|
|
157
172
|
/**
|
|
158
173
|
* Constructor gate: initialize/inspect only the O(1) maintenance record and ensure a dirty
|
|
@@ -175,9 +190,13 @@ declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@
|
|
|
175
190
|
* alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.
|
|
176
191
|
* 2. Recovery strictly precedes a new claim. One head Attempt advances the lane and requests
|
|
177
192
|
* a safe yield after ten minutes. The whole event has a fourteen-minute cooperative timeout.
|
|
178
|
-
* 3.
|
|
193
|
+
* 3. Auxiliary dispatch and listeners belong to the event Scope. After native completion,
|
|
194
|
+
* stop new external waves, join native delivery through its driver-owned Claim deadline,
|
|
195
|
+
* and bound host/backfill work by explicit allowances before closing and joining Scope.
|
|
196
|
+
* Ordinary auxiliary setup failures are reported after the one native opportunity.
|
|
197
|
+
* 4. The final transaction acknowledges only the generation observed at pass start. A racing
|
|
179
198
|
* mutation therefore remains `dirty > processed` and retains its atomically-established alarm.
|
|
180
|
-
*
|
|
199
|
+
* 5. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease
|
|
181
200
|
* recovery states leave their generation dirty and retain bounded backoff rearming.
|
|
182
201
|
*/
|
|
183
202
|
declare class ThreadMaintenance extends ThreadMaintenance_base {
|
package/dist/Alarm.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import { DurableObjectContext } from "./CloudflareBindings.mjs";
|
|
3
|
-
import { CloudflareDurableRuntimeConfig } from "./CloudflareConfig.mjs";
|
|
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, 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";
|
|
@@ -109,25 +109,36 @@ var ThreadPublication = class extends Context.Service()("@effect-agent/platform-
|
|
|
109
109
|
});
|
|
110
110
|
};
|
|
111
111
|
/**
|
|
112
|
-
* Host-assembled message recovery
|
|
113
|
-
*
|
|
114
|
-
*
|
|
112
|
+
* Host-assembled native message recovery. The driver bounds each actual Claim and persists its
|
|
113
|
+
* timeout/retry before this pump returns. Do not add a second timer starting at batch selection:
|
|
114
|
+
* local Claim setup may take time, and expiration still owes the driver's local retry commit.
|
|
115
115
|
*/
|
|
116
116
|
const ThreadMessageDelivery = Context.Reference("@effect-agent/platform-cloudflare/ThreadMessageDelivery", { defaultValue: () => ({
|
|
117
|
-
|
|
117
|
+
drainUntil: () => Effect.void,
|
|
118
118
|
pendingDeadline: Effect.succeed(Option.none())
|
|
119
119
|
}) });
|
|
120
120
|
/**
|
|
121
|
-
* Application obligations sharing this Object's alarm.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
121
|
+
* Application obligations sharing this Object's alarm. Admit one initial external wave even on
|
|
122
|
+
* a caught-up pass, then respond to wakes while native work runs. sourceFinished closes only
|
|
123
|
+
* admission of NEW external waves. Keep local admission/hub subscriptions in the supplied event
|
|
124
|
+
* Scope until maintenance tears it down; do not scope them to sourceFinished or to this Effect.
|
|
125
|
+
* No deadline sleeps or automatic retry loops. Return after already-admitted waves finish.
|
|
126
|
+
*
|
|
127
|
+
* Declare a finite whole-wave allowance (1..300000ms): maximum for parallel lanes, sum for
|
|
128
|
+
* sequential operations. Admit a wave only if its allowance fits before dispatchUntil. Later
|
|
129
|
+
* arrivals cannot renew the retirement window. Maintenance bounds the join after sourceFinished,
|
|
130
|
+
* interrupts and joins event Scope, then reads local deadlines under the mutation gate.
|
|
131
|
+
*
|
|
132
|
+
* Setup and pendingDeadline are bounded local operations. Persist claims/envelopes before
|
|
133
|
+
* dispatch; interrupted waits leave exact retries/receipts recoverable. Cancellation is local,
|
|
134
|
+
* not remote rollback. Retry accounting is unchanged. Network waits/finalizers must be
|
|
135
|
+
* interruptible; short local atomic commits may be uninterruptible. Hooks never write the raw
|
|
136
|
+
* alarm slot. Required native publication gates belong to ThreadPublication.
|
|
127
137
|
*/
|
|
128
138
|
const ThreadHostMaintenance = Context.Reference("@effect-agent/platform-cloudflare/ThreadHostMaintenance", { defaultValue: () => ({
|
|
129
|
-
|
|
130
|
-
drainUntil: () => Effect.void
|
|
139
|
+
dispatchTimeoutMillis: 1,
|
|
140
|
+
drainUntil: () => Effect.void,
|
|
141
|
+
pendingDeadline: Effect.succeed(Option.none())
|
|
131
142
|
}) });
|
|
132
143
|
const earliestDeadline = (left, right) => Option.isSome(left) ? Option.isSome(right) ? Option.some(Math.min(left.value, right.value)) : left : right;
|
|
133
144
|
/** @internal A committed source operation must not become a failed operation because delivery failed. */
|
|
@@ -136,6 +147,19 @@ const publishCommitted = Effect.gen(function* () {
|
|
|
136
147
|
yield* publication.invalidate.pipe(Effect.andThen(publication.drain));
|
|
137
148
|
}).pipe(Effect.catchCause((cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.logError("Thread publication deferred after source commit", cause)));
|
|
138
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
|
+
}) {};
|
|
139
163
|
/** Versioned, platform-private maintenance state stored through Durable Object KV. */
|
|
140
164
|
var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadMaintenanceState")({
|
|
141
165
|
schemaVersion: Schema.Literal(1),
|
|
@@ -143,7 +167,10 @@ var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-
|
|
|
143
167
|
processed: MaintenanceGeneration,
|
|
144
168
|
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
145
169
|
/** One physical-owner cursor; old single-lane records need no conversion. */
|
|
146
|
-
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)
|
|
147
174
|
}) {};
|
|
148
175
|
const MAINTENANCE_STATE_KEY = "effect-agent:thread-maintenance:v1";
|
|
149
176
|
const decodeMaintenanceState = Schema.decodeUnknownSync(ThreadMaintenanceState);
|
|
@@ -230,9 +257,13 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
|
|
|
230
257
|
* alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.
|
|
231
258
|
* 2. Recovery strictly precedes a new claim. One head Attempt advances the lane and requests
|
|
232
259
|
* a safe yield after ten minutes. The whole event has a fourteen-minute cooperative timeout.
|
|
233
|
-
* 3.
|
|
260
|
+
* 3. Auxiliary dispatch and listeners belong to the event Scope. After native completion,
|
|
261
|
+
* stop new external waves, join native delivery through its driver-owned Claim deadline,
|
|
262
|
+
* and bound host/backfill work by explicit allowances before closing and joining Scope.
|
|
263
|
+
* Ordinary auxiliary setup failures are reported after the one native opportunity.
|
|
264
|
+
* 4. The final transaction acknowledges only the generation observed at pass start. A racing
|
|
234
265
|
* mutation therefore remains `dirty > processed` and retains its atomically-established alarm.
|
|
235
|
-
*
|
|
266
|
+
* 5. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease
|
|
236
267
|
* recovery states leave their generation dirty and retain bounded backoff rearming.
|
|
237
268
|
*/
|
|
238
269
|
var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
|
|
@@ -243,11 +274,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
243
274
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
244
275
|
const { ctx } = yield* DurableObjectContext;
|
|
245
276
|
const failpoint = yield* ThreadMaintenanceFailpoint;
|
|
246
|
-
/**
|
|
247
|
-
* Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
|
|
248
|
-
* restarts at zero and merely re-arms sooner than a long-lived one would have.
|
|
249
|
-
*/
|
|
250
|
-
const stalls = yield* Ref.make(0);
|
|
251
277
|
const mutations = yield* ThreadMutationGate;
|
|
252
278
|
const publication = yield* ThreadPublication;
|
|
253
279
|
const projection = yield* ThreadProjectionMaintenance;
|
|
@@ -263,48 +289,74 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
263
289
|
const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
|
|
264
290
|
yield* failpoint.hit("maintenance:ensure:before");
|
|
265
291
|
const now = yield* Clock.currentTimeMillis;
|
|
266
|
-
yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
292
|
+
const retry = yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
267
293
|
const { state, initialized } = await readMaintenanceState(transaction);
|
|
268
294
|
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
269
|
-
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;
|
|
270
297
|
}));
|
|
271
298
|
const deadline = yield* pendingDeadline;
|
|
272
|
-
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))));
|
|
273
300
|
yield* failpoint.hit("maintenance:ensure:after");
|
|
274
301
|
});
|
|
275
|
-
const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
|
|
302
|
+
const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* (observed) {
|
|
276
303
|
yield* failpoint.hit("maintenance:begin:before");
|
|
277
304
|
const now = yield* Clock.currentTimeMillis;
|
|
278
305
|
const result = yield* runTransaction("begin maintenance pass", () => ctx.storage.transaction(async (transaction) => {
|
|
279
306
|
const { state, initialized } = await readMaintenanceState(transaction);
|
|
307
|
+
observed.generation = state.dirty;
|
|
280
308
|
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
281
|
-
|
|
282
|
-
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
283
|
-
return {
|
|
284
|
-
_tag: "CaughtUp",
|
|
285
|
-
nonterminal: state.nonterminal
|
|
286
|
-
};
|
|
287
|
-
}
|
|
309
|
+
const retryAt = state.retry?.generation === state.dirty ? state.retry.notBefore : 0;
|
|
288
310
|
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
311
|
+
if (state.processed >= state.dirty || retryAt > now) return {
|
|
312
|
+
_tag: "CaughtUp",
|
|
313
|
+
nonterminal: state.nonterminal
|
|
314
|
+
};
|
|
289
315
|
return {
|
|
290
316
|
_tag: "Actionable",
|
|
291
317
|
generation: state.dirty,
|
|
292
|
-
nonterminal: state.nonterminal
|
|
318
|
+
nonterminal: state.nonterminal,
|
|
319
|
+
stalls: state.retry?.generation === state.dirty ? state.retry.stalls : 0
|
|
293
320
|
};
|
|
294
321
|
}));
|
|
295
322
|
yield* failpoint.hit("maintenance:begin:after");
|
|
296
323
|
return result;
|
|
297
324
|
});
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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);
|
|
331
|
+
});
|
|
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");
|
|
306
358
|
});
|
|
307
|
-
const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter) {
|
|
359
|
+
const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter, dispatchUntil, observed) {
|
|
308
360
|
const annotate = (report) => Effect.annotateCurrentSpan({
|
|
309
361
|
phase: report.phase,
|
|
310
362
|
recovered: report.recovered,
|
|
@@ -313,32 +365,49 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
313
365
|
alarm: report.alarm
|
|
314
366
|
}).pipe(Effect.as(report));
|
|
315
367
|
const started = yield* mutations.withSnapshot((activeAtStart) => Effect.gen(function* () {
|
|
316
|
-
const generation = yield* beginPass();
|
|
368
|
+
const generation = yield* beginPass(observed);
|
|
317
369
|
if (generation._tag === "Actionable" && activeAtStart === 0) yield* publication.prepareGeneration(generation.generation);
|
|
318
370
|
return {
|
|
319
371
|
...generation,
|
|
320
372
|
activeAtStart
|
|
321
373
|
};
|
|
322
374
|
}));
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
|
|
375
|
+
const auxiliaryScope = yield* Effect.acquireRelease(Scope.make("parallel"), (scope, exit) => Scope.close(scope, exit));
|
|
376
|
+
const sourceFinished = yield* Deferred.make();
|
|
377
|
+
const finished = Deferred.await(sourceFinished);
|
|
378
|
+
const deliveryFiber = yield* Effect.forkIn(Scope.provide(auxiliaryScope)(messages.drainUntil(finished, dispatchUntil)), auxiliaryScope);
|
|
379
|
+
const hostFiber = yield* Effect.forkIn(Scope.provide(auxiliaryScope)(Effect.gen(function* () {
|
|
380
|
+
yield* Schema.decodeEffect(AuxiliaryDispatchMillis)(host.dispatchTimeoutMillis).pipe(Effect.mapError((cause) => DurableAlarmError.make({
|
|
381
|
+
operation: "host dispatch allowance",
|
|
382
|
+
message: "Declare an integer whole-wave allowance between 1 and 300000 milliseconds",
|
|
383
|
+
cause
|
|
384
|
+
})));
|
|
385
|
+
yield* host.drainUntil(finished, dispatchUntil);
|
|
386
|
+
})), auxiliaryScope);
|
|
387
|
+
const backfill = yield* Effect.forkIn(drainDue.pipe(Effect.provideService(ThreadProjectionMaintenance, projection), Effect.timeoutOption(config.projectionDispatchTimeoutMillis)), auxiliaryScope);
|
|
388
|
+
const finishAuxiliary = Effect.gen(function* () {
|
|
389
|
+
yield* Deferred.succeed(sourceFinished, void 0);
|
|
390
|
+
const remaining = Math.max(1, DateTime.toEpochMillis(dispatchUntil) - (yield* Clock.currentTimeMillis));
|
|
391
|
+
const hostJoin = yield* Effect.forkIn(Fiber.join(hostFiber).pipe(Effect.timeoutOption(Math.min(host.dispatchTimeoutMillis, remaining)), Effect.tap((result) => Effect.annotateCurrentSpan({ "host.timedOut": Option.isNone(result) }))), auxiliaryScope);
|
|
392
|
+
yield* Fiber.joinAll([
|
|
393
|
+
deliveryFiber,
|
|
394
|
+
hostJoin,
|
|
395
|
+
backfill
|
|
396
|
+
]);
|
|
397
|
+
yield* Scope.close(auxiliaryScope, Exit.void);
|
|
398
|
+
});
|
|
329
399
|
const deadline = yield* publication.pendingDeadline;
|
|
330
400
|
if (started._tag === "Actionable" || Option.isSome(deadline) && deadline.value <= (yield* Clock.currentTimeMillis)) yield* publication.drain;
|
|
331
401
|
const pending = yield* publication.pendingDeadline;
|
|
332
402
|
if (started._tag === "CaughtUp" || Option.isSome(pending)) {
|
|
333
|
-
yield*
|
|
334
|
-
if (Exit.isFailure(projected)) return yield* Effect.failCause(projected.cause);
|
|
403
|
+
yield* finishAuxiliary;
|
|
335
404
|
yield* failpoint.hit("maintenance:finish:before");
|
|
336
405
|
const disposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
337
406
|
const latest = yield* pendingDeadline;
|
|
338
407
|
const now = yield* Clock.currentTimeMillis;
|
|
339
408
|
return yield* runTransaction("finish publication pass", () => ctx.storage.transaction(async (transaction) => {
|
|
340
409
|
const { state } = await readMaintenanceState(transaction);
|
|
341
|
-
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;
|
|
342
411
|
const next = Option.isSome(latest) ? Math.min(nativeDeadline, latest.value) : nativeDeadline;
|
|
343
412
|
if (Number.isFinite(next)) {
|
|
344
413
|
await transaction.setAlarm(Math.max(now + minimumAlarmDelay, next));
|
|
@@ -357,29 +426,70 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
357
426
|
alarm: disposition
|
|
358
427
|
}));
|
|
359
428
|
}
|
|
429
|
+
observed.nativeOnly = true;
|
|
360
430
|
const recovered = yield* runtime.runRecovery;
|
|
361
431
|
const reports = new Map(recovered.map((report) => [report.submissionId, report]));
|
|
362
432
|
const current = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
363
433
|
const heads = /* @__PURE__ */ new Map();
|
|
364
434
|
for (const row of current) if (!heads.has(row.threadId)) heads.set(row.threadId, row);
|
|
365
435
|
const eligible = [...heads.values()].filter((head) => !stableExternalWait(head, reports)).map((head) => head.threadId).sort();
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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));
|
|
379
490
|
}
|
|
380
|
-
|
|
381
|
-
yield*
|
|
382
|
-
if (Exit.isFailure(projected)) return yield* Effect.failCause(projected.cause);
|
|
491
|
+
observed.nativeOnly = false;
|
|
492
|
+
yield* finishAuxiliary;
|
|
383
493
|
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
384
494
|
const waitingHeads = /* @__PURE__ */ new Map();
|
|
385
495
|
const autonomous = remaining.some((snapshot) => {
|
|
@@ -389,8 +499,11 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
389
499
|
return !stableExternalWait(snapshot, reports);
|
|
390
500
|
});
|
|
391
501
|
const progressed = Option.isSome(settlement) || recovered.some((report) => report.disposition === "repaired");
|
|
392
|
-
const delay = autonomous ? yield* rearmDelay(progressed) : 0;
|
|
393
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);
|
|
394
507
|
yield* failpoint.hit("maintenance:finish:before");
|
|
395
508
|
const alarmDisposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
396
509
|
const publicationDeadline = yield* pendingDeadline;
|
|
@@ -398,13 +511,21 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
398
511
|
const { state } = await readMaintenanceState(transaction);
|
|
399
512
|
const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > started.generation ? state.processed : started.generation;
|
|
400
513
|
const next = ThreadMaintenanceState.make({
|
|
401
|
-
...state,
|
|
514
|
+
...Struct.omit(state, ["retry"]),
|
|
402
515
|
processed,
|
|
403
|
-
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
|
+
}) } : {}
|
|
404
524
|
});
|
|
405
525
|
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
406
526
|
if (autonomous) {
|
|
407
|
-
|
|
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);
|
|
408
529
|
return "rearmed";
|
|
409
530
|
}
|
|
410
531
|
if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
|
|
@@ -420,7 +541,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
420
541
|
}));
|
|
421
542
|
}));
|
|
422
543
|
yield* failpoint.hit("maintenance:finish:after");
|
|
423
|
-
if (alarmDisposition === "cleared") yield* Ref.set(stalls, 0);
|
|
424
544
|
return yield* annotate(MaintenancePassReport.make({
|
|
425
545
|
phase: "actionable",
|
|
426
546
|
recovered: recovered.length,
|
|
@@ -431,8 +551,11 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
|
|
|
431
551
|
});
|
|
432
552
|
return ThreadMaintenance.of({
|
|
433
553
|
pass: Effect.gen(function* () {
|
|
434
|
-
const
|
|
435
|
-
|
|
554
|
+
const now = yield* Clock.currentTimeMillis;
|
|
555
|
+
const yieldAfter = DateTime.makeUnsafe(now + 6e5);
|
|
556
|
+
const dispatchUntil = DateTime.makeUnsafe(now + 84e4);
|
|
557
|
+
const observed = { nativeOnly: false };
|
|
558
|
+
return yield* alarm.withWakesDeferred(maintenancePassGate.withPermit(Effect.scoped(pass(yieldAfter, dispatchUntil, observed)).pipe(Effect.onErrorIf(() => true, () => rearmFailure(observed)))));
|
|
436
559
|
}).pipe(Effect.timeoutOrElse({
|
|
437
560
|
duration: "14 minutes",
|
|
438
561
|
orElse: () => DurableAlarmError.make({
|