effect-cf 0.54.0 → 0.56.0
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/CHANGELOG.md +12 -0
- package/dist/DurableObjectAlarm.d.mts +14 -6
- package/dist/DurableObjectAlarm.mjs +92 -42
- package/package.json +1 -1
- package/src/DurableObjectAlarm.ts +199 -85
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# effect-cf
|
|
2
2
|
|
|
3
|
+
## 0.56.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#213](https://github.com/danieljvdm/effect-cf/pull/213) [`b0e1201`](https://github.com/danieljvdm/effect-cf/commit/b0e1201663c3769fa2e63daad6f46a8a8b0d9b05) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Allow alarm transactions to share a Durable Object SQL client's connection and commit boundary. Reduce manual alarm checkpoint writes while preserving recovery guards and stale-dispatch fencing.
|
|
8
|
+
|
|
9
|
+
## 0.55.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#211](https://github.com/danieljvdm/effect-cf/pull/211) [`2764143`](https://github.com/danieljvdm/effect-cf/commit/276414374c3fe71f619c26ef1c447ab0f52f9ab4) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Keep in-flight Durable Object work recoverable through a configurable short alarm deadline while preserving hourly parked recovery. Coalesce native deliveries with active processing without starting another pass.
|
|
14
|
+
|
|
3
15
|
## 0.54.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -6,8 +6,9 @@ import * as S from "effect/Schema";
|
|
|
6
6
|
import * as Layer from "effect/Layer";
|
|
7
7
|
import * as Duration from "effect/Duration";
|
|
8
8
|
import * as DateTime from "effect/DateTime";
|
|
9
|
+
import { SqlClient } from "effect/sql/SqlClient";
|
|
9
10
|
declare namespace DurableObjectAlarm_d_exports {
|
|
10
|
-
export { AlarmDefinitionConfig, AlarmDefinitionEntry, AlarmDefinitionPayload, AlarmDefinitions, AlarmFailurePolicy, AlarmLifecycle, AlarmParked, AlarmPayload, AlarmPayloadSchema, AlarmRef, AlarmRegistration, AlarmReporter, AlarmRetryPolicy, AlarmScheduler, AlarmService, AlarmStatus, AlarmTagClass, AlarmTransaction, DEFAULT_MIN_REPEAT_INTERVAL_MS, DefinedAlarmEvent, DefinedAlarmHandlers, DefinedAlarmScheduler, DefinedAlarmTransaction, DefinedScheduleAlarmInput, DispatchAfterEvent, DurableObjectAlarm, DurableObjectAlarmError, DurableObjectAlarmEvent, HasDueAlarms, InvalidAlarmPayloadError, InvalidAlarmRefError, InvalidAlarmRegistrationError, InvalidProcessDueAlarmsOptionsError, InvalidRepeatEveryError, InvalidScheduleConfigurationError, MIN_REPEAT_INTERVAL_MS, MIN_RETRY_DELAY_MS, PARKED_RETRY_DELAY_MS, ProcessDueAlarmsFailure, ProcessDueAlarmsFailureAction, ProcessDueAlarmsHandler, ProcessDueAlarmsOptions, ProcessDueAlarmsResult, RunAlarm, ScheduleAlarmInput, ScheduleConfiguration, StoredAlarmDecodeError, Tag, UNCHANGED_ATTEMPT_BUDGET, define, dispatchRawAlarm, mergeAll, processDue };
|
|
11
|
+
export { AlarmDefinitionConfig, AlarmDefinitionEntry, AlarmDefinitionPayload, AlarmDefinitions, AlarmFailurePolicy, AlarmLifecycle, AlarmParked, AlarmPayload, AlarmPayloadSchema, AlarmRef, AlarmRegistration, AlarmReporter, AlarmRetryPolicy, AlarmScheduler, AlarmService, AlarmStatus, AlarmTagClass, AlarmTransaction, AlarmTransactionOptions, DEFAULT_MIN_REPEAT_INTERVAL_MS, DefinedAlarmEvent, DefinedAlarmHandlers, DefinedAlarmScheduler, DefinedAlarmTransaction, DefinedScheduleAlarmInput, DispatchAfterEvent, DurableObjectAlarm, DurableObjectAlarmError, DurableObjectAlarmEvent, HasDueAlarms, InvalidAlarmPayloadError, InvalidAlarmRefError, InvalidAlarmRegistrationError, InvalidProcessDueAlarmsOptionsError, InvalidRepeatEveryError, InvalidScheduleConfigurationError, MIN_REPEAT_INTERVAL_MS, MIN_RETRY_DELAY_MS, PARKED_RETRY_DELAY_MS, ProcessDueAlarmsFailure, ProcessDueAlarmsFailureAction, ProcessDueAlarmsHandler, ProcessDueAlarmsOptions, ProcessDueAlarmsResult, RunAlarm, ScheduleAlarmInput, ScheduleConfiguration, StoredAlarmDecodeError, Tag, UNCHANGED_ATTEMPT_BUDGET, define, dispatchRawAlarm, mergeAll, processDue };
|
|
11
12
|
}
|
|
12
13
|
declare const MIN_RETRY_DELAY_MS = 1000;
|
|
13
14
|
/** Default unchanged-attempt budget. */
|
|
@@ -21,6 +22,8 @@ declare const DEFAULT_MIN_REPEAT_INTERVAL_MS = 60000;
|
|
|
21
22
|
interface ScheduleConfiguration {
|
|
22
23
|
/** Check for due alarms after RPC/fetch responses unwind. Native alarms remain recovery authority. */
|
|
23
24
|
readonly dispatchAfterEvent?: boolean;
|
|
25
|
+
/** Positive recovery deadline while dispatch or deferWakes is active. Defaults to one second. */
|
|
26
|
+
readonly inFlightRecovery?: Duration.Input;
|
|
24
27
|
/** Retry floor, at least one second. Defaults to one second. */
|
|
25
28
|
readonly minimumRetryDelay?: Duration.Input;
|
|
26
29
|
/** Positive safe integer. Defaults to eight unchanged attempts. */
|
|
@@ -159,6 +162,11 @@ type ProcessDueAlarmsHandler<R = never, E = never> = (event: DurableObjectAlarmE
|
|
|
159
162
|
* fiber; forked work and use after the callback ends fail with StorageOperationError.
|
|
160
163
|
*/
|
|
161
164
|
type AlarmTransaction = Pick<AlarmScheduler, "scheduleAlarm" | "scheduleAlarmEarlier" | "cancelAlarm">;
|
|
165
|
+
/** Share the same native commit and connection permit as the caller's SQL writes. */
|
|
166
|
+
interface AlarmTransactionOptions {
|
|
167
|
+
/** Must be a storage-backed @effect/sql-sqlite-do client for this Durable Object. */
|
|
168
|
+
readonly sqlClient: SqlClient;
|
|
169
|
+
}
|
|
162
170
|
interface AlarmStatus {
|
|
163
171
|
readonly attempts: number;
|
|
164
172
|
readonly parked: boolean;
|
|
@@ -184,14 +192,14 @@ type AlarmScheduler = {
|
|
|
184
192
|
* object's concurrent and nested scopes. Source/checkpoint transactions still commit;
|
|
185
193
|
* the last scope reconciles their current deadlines on success, failure or interruption.
|
|
186
194
|
* Run outside transactions, around bounded maintenance or inline work. The first scope's
|
|
187
|
-
*
|
|
195
|
+
* inFlightRecovery bounds the shared guard; renew it if it expires and is consumed while
|
|
188
196
|
* scopes remain active. A process loss before exit recovers at that retained alarm.
|
|
189
197
|
*/
|
|
190
198
|
readonly deferWakes: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | InvalidScheduleConfigurationError | StorageOperationError, R>;
|
|
191
199
|
/**
|
|
192
200
|
* Commits local application storage and logical alarms in one native SQLite
|
|
193
201
|
* Durable Object transaction, reconciling the native alarm before commit.
|
|
194
|
-
* Use the supplied mutations
|
|
202
|
+
* Use the supplied mutations inside the callback instead of standalone alarm methods.
|
|
195
203
|
* SqlClient queries must use this same Durable Object's storage.
|
|
196
204
|
*
|
|
197
205
|
* Failure, defects and interruption before commit roll back. A lost reply or
|
|
@@ -200,7 +208,7 @@ type AlarmScheduler = {
|
|
|
200
208
|
* Cloudflare's native retries are bounded; composition does not remove the
|
|
201
209
|
* pre-arm requirement or promise infinite retry liveness.
|
|
202
210
|
*/
|
|
203
|
-
readonly transaction: <A, E, R>(closure: (alarms: AlarmTransaction) => Effect.Effect<A, E, R
|
|
211
|
+
readonly transaction: <A, E, R>(closure: (alarms: AlarmTransaction) => Effect.Effect<A, E, R>, options?: AlarmTransactionOptions) => Effect.Effect<A, E | StorageOperationError, R>;
|
|
204
212
|
readonly cancelAlarm: (input: AlarmRef) => Effect.Effect<void, InvalidAlarmRefError | StorageOperationError>;
|
|
205
213
|
/** Inspect retained work, including hourly recovery deadlines. */
|
|
206
214
|
readonly getAlarmStatus: (input: AlarmRef) => Effect.Effect<AlarmStatus | undefined, InvalidAlarmRefError | StorageOperationError>;
|
|
@@ -265,7 +273,7 @@ interface DefinedAlarmScheduler<Definitions extends AlarmDefinitions> extends De
|
|
|
265
273
|
readonly [TypedAlarmSchedulerTypeId]: typeof TypedAlarmSchedulerTypeId;
|
|
266
274
|
readonly deferWakes: AlarmScheduler["deferWakes"];
|
|
267
275
|
readonly getAlarmStatus: (input: AlarmRef<keyof Definitions & string>) => ReturnType<AlarmScheduler["getAlarmStatus"]>;
|
|
268
|
-
readonly transaction: <A, E, R>(closure: (alarms: DefinedAlarmTransaction<Definitions>) => Effect.Effect<A, E, R
|
|
276
|
+
readonly transaction: <A, E, R>(closure: (alarms: DefinedAlarmTransaction<Definitions>) => Effect.Effect<A, E, R>, options?: AlarmTransactionOptions) => Effect.Effect<A, E | StorageOperationError, R>;
|
|
269
277
|
}
|
|
270
278
|
/** Registers a dispatcher and the service authorized to schedule its alarms. */
|
|
271
279
|
interface AlarmRegistration<Self, R = never, E = never> {
|
|
@@ -302,4 +310,4 @@ declare class DurableObjectAlarm extends DurableObjectAlarm_base {
|
|
|
302
310
|
static readonly layer: Layer.Layer<DurableObjectAlarm, never, DurableObjectState>;
|
|
303
311
|
}
|
|
304
312
|
//#endregion
|
|
305
|
-
export { AlarmDefinitionConfig, AlarmDefinitionEntry, AlarmDefinitionPayload, AlarmDefinitions, AlarmFailurePolicy, AlarmLifecycle, AlarmParked, AlarmPayload, AlarmPayloadSchema, AlarmRef, AlarmRegistration, AlarmReporter, AlarmRetryPolicy, AlarmScheduler, AlarmService, AlarmStatus, AlarmTagClass, AlarmTransaction, DEFAULT_MIN_REPEAT_INTERVAL_MS, DefinedAlarmEvent, DefinedAlarmHandlers, DefinedAlarmScheduler, DefinedAlarmTransaction, DefinedScheduleAlarmInput, DispatchAfterEvent, DurableObjectAlarm, DurableObjectAlarmError, DurableObjectAlarmEvent, DurableObjectAlarm_d_exports, HasDueAlarms, InvalidAlarmPayloadError, InvalidAlarmRefError, InvalidAlarmRegistrationError, InvalidProcessDueAlarmsOptionsError, InvalidRepeatEveryError, InvalidScheduleConfigurationError, MIN_REPEAT_INTERVAL_MS, MIN_RETRY_DELAY_MS, PARKED_RETRY_DELAY_MS, ProcessDueAlarmsFailure, ProcessDueAlarmsFailureAction, ProcessDueAlarmsHandler, ProcessDueAlarmsOptions, ProcessDueAlarmsResult, RunAlarm, ScheduleAlarmInput, ScheduleConfiguration, StoredAlarmDecodeError, Tag, UNCHANGED_ATTEMPT_BUDGET, define, dispatchRawAlarm, mergeAll, processDue };
|
|
313
|
+
export { AlarmDefinitionConfig, AlarmDefinitionEntry, AlarmDefinitionPayload, AlarmDefinitions, AlarmFailurePolicy, AlarmLifecycle, AlarmParked, AlarmPayload, AlarmPayloadSchema, AlarmRef, AlarmRegistration, AlarmReporter, AlarmRetryPolicy, AlarmScheduler, AlarmService, AlarmStatus, AlarmTagClass, AlarmTransaction, AlarmTransactionOptions, DEFAULT_MIN_REPEAT_INTERVAL_MS, DefinedAlarmEvent, DefinedAlarmHandlers, DefinedAlarmScheduler, DefinedAlarmTransaction, DefinedScheduleAlarmInput, DispatchAfterEvent, DurableObjectAlarm, DurableObjectAlarmError, DurableObjectAlarmEvent, DurableObjectAlarm_d_exports, HasDueAlarms, InvalidAlarmPayloadError, InvalidAlarmRefError, InvalidAlarmRegistrationError, InvalidProcessDueAlarmsOptionsError, InvalidRepeatEveryError, InvalidScheduleConfigurationError, MIN_REPEAT_INTERVAL_MS, MIN_RETRY_DELAY_MS, PARKED_RETRY_DELAY_MS, ProcessDueAlarmsFailure, ProcessDueAlarmsFailureAction, ProcessDueAlarmsHandler, ProcessDueAlarmsOptions, ProcessDueAlarmsResult, RunAlarm, ScheduleAlarmInput, ScheduleConfiguration, StoredAlarmDecodeError, Tag, UNCHANGED_ATTEMPT_BUDGET, define, dispatchRawAlarm, mergeAll, processDue };
|
|
@@ -11,9 +11,11 @@ import * as Predicate from "effect/Predicate";
|
|
|
11
11
|
import * as Exit from "effect/Exit";
|
|
12
12
|
import * as Duration from "effect/Duration";
|
|
13
13
|
import * as Clock from "effect/Clock";
|
|
14
|
+
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
14
15
|
import * as DateTime from "effect/DateTime";
|
|
15
16
|
import * as Deferred from "effect/Deferred";
|
|
16
17
|
import * as Semaphore from "effect/Semaphore";
|
|
18
|
+
import { SqlError } from "effect/sql/SqlError";
|
|
17
19
|
//#region src/DurableObjectAlarm.ts
|
|
18
20
|
var DurableObjectAlarm_exports = /* @__PURE__ */ __exportAll({
|
|
19
21
|
AlarmParked: () => AlarmParked,
|
|
@@ -51,23 +53,21 @@ CREATE TABLE IF NOT EXISTS effect_cf_scheduled_alarms (
|
|
|
51
53
|
run_at INTEGER NOT NULL,
|
|
52
54
|
wake_at INTEGER NOT NULL DEFAULT 0,
|
|
53
55
|
repeat_every_ms INTEGER,
|
|
54
|
-
payload TEXT NOT NULL
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
revision TEXT NOT NULL,
|
|
59
|
-
attempts INTEGER NOT NULL,
|
|
60
|
-
parked INTEGER NOT NULL,
|
|
56
|
+
payload TEXT NOT NULL,
|
|
57
|
+
revision TEXT NOT NULL DEFAULT '',
|
|
58
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
parked INTEGER NOT NULL DEFAULT 0,
|
|
61
60
|
retry_at INTEGER,
|
|
62
|
-
progress INTEGER NOT NULL
|
|
61
|
+
progress INTEGER NOT NULL DEFAULT -1
|
|
63
62
|
);
|
|
64
63
|
`;
|
|
65
64
|
const WAKE_INDEX = "idx_effect_cf_scheduled_alarms_wake_at_storage_id";
|
|
66
|
-
const LIFECYCLE_INDEX = "
|
|
65
|
+
const LIFECYCLE_INDEX = "idx_effect_cf_scheduled_alarms_lifecycle_wake_at_storage_id_v2";
|
|
67
66
|
const DEFAULT_PROCESS_DUE_ALARMS_LIMIT = 100;
|
|
68
67
|
const MIN_RETRY_DELAY_MS = 1e3;
|
|
69
68
|
/** Minimum and default parked recovery interval. */
|
|
70
69
|
const PARKED_RETRY_DELAY_MS = 36e5;
|
|
70
|
+
const DEFAULT_IN_FLIGHT_RECOVERY_MS = 1e3;
|
|
71
71
|
/** Absolute product repeat floor. */
|
|
72
72
|
const MIN_REPEAT_INTERVAL_MS = 1e3;
|
|
73
73
|
/** Default product schedule floor. Never use repeats to poll state. */
|
|
@@ -78,6 +78,7 @@ const safeIntegerAtLeast = (minimum) => S.Int.check(S.isGreaterThanOrEqualTo(min
|
|
|
78
78
|
const AlarmLifecycleSchema = S.Literals(["automatic", "manual"]);
|
|
79
79
|
const ScheduleConfigurationSchema = S.Struct({
|
|
80
80
|
dispatchAfterEvent: S.Boolean,
|
|
81
|
+
inFlightRecovery: safeIntegerAtLeast(1),
|
|
81
82
|
minimumRetryDelay: safeIntegerAtLeast(MIN_RETRY_DELAY_MS),
|
|
82
83
|
unchangedAttemptBudget: safeIntegerAtLeast(1),
|
|
83
84
|
parkedRetryDelay: safeIntegerAtLeast(PARKED_RETRY_DELAY_MS),
|
|
@@ -85,14 +86,8 @@ const ScheduleConfigurationSchema = S.Struct({
|
|
|
85
86
|
}).check(S.makeFilter((configuration) => configuration.parkedRetryDelay >= configuration.minimumRetryDelay ? void 0 : "parkedRetryDelay must be at least minimumRetryDelay"));
|
|
86
87
|
const retryDelay = (attempts, configuration, parked, initialDelay = configuration.minimumRetryDelay) => parked ? configuration.parkedRetryDelay : Math.min(configuration.parkedRetryDelay, initialDelay * 2 ** (attempts - 1));
|
|
87
88
|
const getScheduledEventId = (input) => `effect-cf-alarm:${encodeURIComponent(input.tag)}:${encodeURIComponent(input.id)}`;
|
|
88
|
-
const alarmRowsSql =
|
|
89
|
-
|
|
90
|
-
COALESCE(s.attempts, 0) AS attempts, COALESCE(s.parked, 0) AS parked,
|
|
91
|
-
s.retry_at, COALESCE(s.progress, -1) AS progress
|
|
92
|
-
FROM effect_cf_scheduled_alarms a
|
|
93
|
-
LEFT JOIN effect_cf_alarm_attempts s USING (storage_id)`;
|
|
94
|
-
const sameRevisionSql = `storage_id = ? AND COALESCE(
|
|
95
|
-
(SELECT revision FROM effect_cf_alarm_attempts WHERE storage_id = ?), '') = ?`;
|
|
89
|
+
const alarmRowsSql = "SELECT a.* FROM effect_cf_scheduled_alarms a";
|
|
90
|
+
const sameRevisionSql = "storage_id = ? AND revision = ?";
|
|
96
91
|
/** Content-free: no alarm identifiers, payload, error text or consumer state. */
|
|
97
92
|
const AlarmParked = S.TaggedStruct("AlarmParked", {
|
|
98
93
|
attempts: S.Int,
|
|
@@ -142,6 +137,7 @@ const getScheduleConfiguration = Effect.fnUntraced(function* (defaults) {
|
|
|
142
137
|
return yield* Effect.try({
|
|
143
138
|
try: () => S.decodeUnknownSync(ScheduleConfigurationSchema)({
|
|
144
139
|
dispatchAfterEvent: input.dispatchAfterEvent ?? false,
|
|
140
|
+
inFlightRecovery: Math.ceil(Duration.toMillis(input.inFlightRecovery ?? DEFAULT_IN_FLIGHT_RECOVERY_MS)),
|
|
145
141
|
minimumRetryDelay: Math.ceil(Duration.toMillis(input.minimumRetryDelay ?? 1e3)),
|
|
146
142
|
unchangedAttemptBudget: input.unchangedAttemptBudget ?? 8,
|
|
147
143
|
parkedRetryDelay: Math.ceil(Duration.toMillis(input.parkedRetryDelay ?? 36e5)),
|
|
@@ -167,6 +163,7 @@ const AlarmRefSchema = S.Struct({
|
|
|
167
163
|
tag: S.NonEmptyString
|
|
168
164
|
});
|
|
169
165
|
const decodeAlarmRef = (input) => S.decodeUnknownEffect(AlarmRefSchema)(input).pipe(Effect.mapError((cause) => new InvalidAlarmRefError({ cause })));
|
|
166
|
+
const StorageSqlClient = S.declare((value) => Predicate.hasProperty(value, SqliteClient.TypeId));
|
|
170
167
|
/** @internal */
|
|
171
168
|
const RunAlarm = Symbol("effect-cf/DurableObjectAlarm/RunAlarm");
|
|
172
169
|
/** @internal */
|
|
@@ -181,15 +178,30 @@ const decodeStoredPayload = (row) => S.decodeUnknownEffect(StoredPayloadString)(
|
|
|
181
178
|
})));
|
|
182
179
|
const encodeStoredPayload = (payload) => S.encodeEffect(StoredPayloadString)(payload).pipe(Effect.mapError((cause) => new InvalidAlarmPayloadError({ cause })));
|
|
183
180
|
const ensureTable = Effect.fnUntraced(function* (state) {
|
|
184
|
-
yield* state.storage.sql.exec(INIT_TABLE_SQL);
|
|
185
181
|
if ((yield* (yield* state.storage.sql.exec("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?", LIFECYCLE_INDEX)).toArray()).length > 0) return;
|
|
182
|
+
yield* state.storage.sql.exec(INIT_TABLE_SQL);
|
|
186
183
|
const columns = yield* state.storage.sql.exec("SELECT name FROM pragma_table_info('effect_cf_scheduled_alarms')");
|
|
187
184
|
const names = new Set((yield* columns.toArray()).map((column) => column.name));
|
|
188
185
|
if (!names.has("lifecycle")) yield* state.storage.sql.exec("ALTER TABLE effect_cf_scheduled_alarms ADD COLUMN lifecycle TEXT NOT NULL DEFAULT 'automatic'");
|
|
189
186
|
if (!names.has("wake_at")) yield* state.storage.sql.exec("ALTER TABLE effect_cf_scheduled_alarms ADD COLUMN wake_at INTEGER NOT NULL DEFAULT 0");
|
|
187
|
+
for (const [column, definition] of Object.entries({
|
|
188
|
+
revision: "TEXT NOT NULL DEFAULT ''",
|
|
189
|
+
attempts: "INTEGER NOT NULL DEFAULT 0",
|
|
190
|
+
parked: "INTEGER NOT NULL DEFAULT 0",
|
|
191
|
+
retry_at: "INTEGER",
|
|
192
|
+
progress: "INTEGER NOT NULL DEFAULT -1"
|
|
193
|
+
})) if (!names.has(column)) yield* state.storage.sql.exec(`ALTER TABLE effect_cf_scheduled_alarms ADD COLUMN ${column} ${definition}`);
|
|
194
|
+
if ((yield* (yield* state.storage.sql.exec("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'effect_cf_alarm_attempts'")).toArray()).length > 0) {
|
|
195
|
+
yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET
|
|
196
|
+
(revision, attempts, parked, retry_at, progress) =
|
|
197
|
+
(SELECT revision, attempts, parked, retry_at, progress FROM effect_cf_alarm_attempts s
|
|
198
|
+
WHERE s.storage_id = effect_cf_scheduled_alarms.storage_id)
|
|
199
|
+
WHERE storage_id IN (SELECT storage_id FROM effect_cf_alarm_attempts)`);
|
|
200
|
+
yield* state.storage.sql.exec("DROP TABLE effect_cf_alarm_attempts");
|
|
201
|
+
}
|
|
190
202
|
yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET
|
|
191
|
-
wake_at = MAX(run_at, COALESCE(
|
|
192
|
-
|
|
203
|
+
wake_at = MAX(run_at, COALESCE(retry_at, run_at))`);
|
|
204
|
+
yield* state.storage.sql.exec("DROP INDEX IF EXISTS idx_effect_cf_scheduled_alarms_lifecycle_wake_at_storage_id");
|
|
193
205
|
yield* state.storage.sql.exec("DROP INDEX IF EXISTS idx_effect_cf_scheduled_alarms_run_at_storage_id");
|
|
194
206
|
yield* state.storage.sql.exec(`CREATE INDEX IF NOT EXISTS ${WAKE_INDEX}
|
|
195
207
|
ON effect_cf_scheduled_alarms (wake_at, storage_id)`);
|
|
@@ -343,7 +355,7 @@ const makeDefinition = (definitions) => {
|
|
|
343
355
|
...bind(alarms),
|
|
344
356
|
deferWakes: alarms.deferWakes,
|
|
345
357
|
getAlarmStatus: (input) => alarms.getAlarmStatus(input),
|
|
346
|
-
transaction: (closure) => alarms.transaction((tx) => closure(bind(tx)))
|
|
358
|
+
transaction: (closure, options) => alarms.transaction((tx) => closure(bind(tx)), options)
|
|
347
359
|
}),
|
|
348
360
|
handlers: (handlers, options) => processDue((event) => Effect.gen(function* () {
|
|
349
361
|
const definition = definitionFor(event.tag);
|
|
@@ -440,18 +452,14 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
440
452
|
yield* writeNativeAlarm(next?.run_at ?? null);
|
|
441
453
|
});
|
|
442
454
|
const writeAttempts = Effect.fnUntraced(function* (storageId, attempts, parked, retryAt, progress) {
|
|
443
|
-
yield* state.storage.sql.exec(`
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms
|
|
447
|
-
SET wake_at = MAX(run_at, COALESCE(?, run_at))
|
|
448
|
-
WHERE storage_id = ?`, retryAt, storageId);
|
|
455
|
+
yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET revision = ?, attempts = ?, parked = ?,
|
|
456
|
+
retry_at = ?, progress = ?, wake_at = MAX(run_at, COALESCE(?, run_at))
|
|
457
|
+
WHERE storage_id = ?`, crypto.randomUUID(), attempts, parked, retryAt, progress, retryAt, storageId);
|
|
449
458
|
});
|
|
450
459
|
const cancelAlarm = Effect.fn("DurableObjectAlarm.cancelAlarm")(function* (input) {
|
|
451
460
|
const ref = yield* decodeAlarmRef(input);
|
|
452
461
|
const storageId = getScheduledEventId(ref);
|
|
453
462
|
yield* state.storage.sql.exec(`DELETE FROM effect_cf_scheduled_alarms WHERE storage_id = ?`, storageId);
|
|
454
|
-
yield* state.storage.sql.exec(`DELETE FROM effect_cf_alarm_attempts WHERE storage_id = ?`, storageId);
|
|
455
463
|
});
|
|
456
464
|
const scheduleAlarm = Effect.fn("DurableObjectAlarm.scheduleAlarm")(function* (input, earlier, reports) {
|
|
457
465
|
const configuration = yield* getScheduleConfiguration(configurationDefaults);
|
|
@@ -461,6 +469,19 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
461
469
|
const repeatEveryMillis = yield* toRepeatEveryMillis(input.repeatEvery, configuration.minimumRepeatInterval);
|
|
462
470
|
const payload = yield* encodeStoredPayload(input.payload);
|
|
463
471
|
const storageId = getScheduledEventId(ref);
|
|
472
|
+
const runAt = DateTime.toEpochMillis(input.runAt);
|
|
473
|
+
if (lifecycle === "manual") {
|
|
474
|
+
const deadline = earlier ? "MIN(run_at, excluded.run_at)" : "excluded.run_at";
|
|
475
|
+
const nextPayload = earlier ? "CASE WHEN run_at <= excluded.run_at THEN payload ELSE excluded.payload END" : "excluded.payload";
|
|
476
|
+
yield* state.storage.sql.exec(`INSERT INTO effect_cf_scheduled_alarms
|
|
477
|
+
(storage_id, alarm_id, tag, lifecycle, run_at, wake_at, payload, revision)
|
|
478
|
+
VALUES (?, ?, ?, 'manual', ?, ?, ?, ?)
|
|
479
|
+
ON CONFLICT(storage_id) DO UPDATE SET
|
|
480
|
+
lifecycle = 'manual', run_at = ${deadline}, wake_at = ${deadline},
|
|
481
|
+
payload = ${nextPayload}, revision = excluded.revision,
|
|
482
|
+
repeat_every_ms = NULL, attempts = 0, parked = 0, retry_at = NULL, progress = -1`, storageId, ref.id, ref.tag, runAt, runAt, payload, crypto.randomUUID());
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
464
485
|
const existing = yield* readRow(storageId);
|
|
465
486
|
const pass = yield* CurrentAlarmPass;
|
|
466
487
|
const source = lifecycle === "automatic" && pass?.row.lifecycle === "automatic" ? pass.row : void 0;
|
|
@@ -473,16 +494,15 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
473
494
|
const parked = attempts > 0 && (attempts >= configuration.unchangedAttemptBudget || source?.parked === 1 || existing?.parked === 1) ? 1 : 0;
|
|
474
495
|
const now = yield* Clock.currentTimeMillis;
|
|
475
496
|
const retryAt = attempts === 0 ? null : now + retryDelay(attempts, configuration, parked === 1);
|
|
476
|
-
const runAt = DateTime.toEpochMillis(input.runAt);
|
|
477
497
|
const keepEarlier = earlier && existing !== void 0 && existing.run_at <= runAt;
|
|
478
498
|
const scheduledAt = keepEarlier ? existing.run_at : runAt;
|
|
479
499
|
const scheduledPayload = keepEarlier ? existing.payload : payload;
|
|
480
|
-
const scheduledRepeat =
|
|
481
|
-
const nextProgress =
|
|
500
|
+
const scheduledRepeat = keepEarlier ? existing.repeat_every_ms : repeatEveryMillis;
|
|
501
|
+
const nextProgress = Math.max(progress, input.progress ?? -1);
|
|
482
502
|
yield* state.storage.sql.exec(`INSERT OR REPLACE INTO effect_cf_scheduled_alarms
|
|
483
|
-
(storage_id, alarm_id, tag, lifecycle, run_at, repeat_every_ms, payload
|
|
484
|
-
|
|
485
|
-
|
|
503
|
+
(storage_id, alarm_id, tag, lifecycle, run_at, repeat_every_ms, payload,
|
|
504
|
+
wake_at, revision, attempts, parked, retry_at, progress)
|
|
505
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, storageId, ref.id, ref.tag, lifecycle, scheduledAt, scheduledRepeat, scheduledPayload, Math.max(scheduledAt, retryAt ?? scheduledAt), crypto.randomUUID(), attempts, parked, retryAt, nextProgress);
|
|
486
506
|
if (parked === 1 && retryAt !== null && (existing?.parked ?? source?.parked ?? 0) === 0) reports.push(storageId);
|
|
487
507
|
});
|
|
488
508
|
const reportParked = Effect.fnUntraced(function* (storageIds) {
|
|
@@ -500,14 +520,14 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
500
520
|
yield* Effect.exit(Effect.suspend(() => report(event)));
|
|
501
521
|
}
|
|
502
522
|
});
|
|
503
|
-
const transaction = Effect.fnUntraced(function* (closure) {
|
|
523
|
+
const transaction = Effect.fnUntraced(function* (closure, options) {
|
|
504
524
|
const pass = yield* CurrentAlarmPass;
|
|
505
525
|
if (pass !== void 0 && !pass.active) return yield* Effect.fail(new StorageOperationError({
|
|
506
526
|
operation: "alarm.transaction",
|
|
507
527
|
cause: /* @__PURE__ */ new Error("Alarm handlers cannot mutate detached work after their pass ends")
|
|
508
528
|
}));
|
|
509
529
|
const reports = [];
|
|
510
|
-
const
|
|
530
|
+
const body = Effect.withFiber((owner) => {
|
|
511
531
|
let active = true;
|
|
512
532
|
const requireActive = (effect) => Effect.withFiber((fiber) => active && fiber === owner && (pass === void 0 || pass.active) ? effect : Effect.fail(new StorageOperationError({
|
|
513
533
|
operation: "alarm.transaction",
|
|
@@ -525,7 +545,21 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
525
545
|
yield* reconcileAlarm();
|
|
526
546
|
return result;
|
|
527
547
|
});
|
|
528
|
-
})
|
|
548
|
+
});
|
|
549
|
+
const result = yield* options === void 0 ? state.storage.transaction(() => body) : Effect.gen(function* () {
|
|
550
|
+
const client = yield* S.decodeUnknownEffect(StorageSqlClient)(options.sqlClient).pipe(Effect.mapError((cause) => new StorageOperationError({
|
|
551
|
+
operation: "alarm.transaction",
|
|
552
|
+
cause
|
|
553
|
+
})));
|
|
554
|
+
if (client.config.storage !== state.raw.storage) return yield* Effect.fail(new StorageOperationError({
|
|
555
|
+
operation: "alarm.transaction",
|
|
556
|
+
cause: /* @__PURE__ */ new Error("Alarm transactions require this Durable Object's storage-backed SQL client")
|
|
557
|
+
}));
|
|
558
|
+
return yield* client.withTransaction(body).pipe(Effect.mapError((cause) => cause instanceof SqlError ? new StorageOperationError({
|
|
559
|
+
operation: "alarm.transaction",
|
|
560
|
+
cause
|
|
561
|
+
}) : cause));
|
|
562
|
+
});
|
|
529
563
|
yield* reportParked(reports);
|
|
530
564
|
return result;
|
|
531
565
|
});
|
|
@@ -533,7 +567,7 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
533
567
|
const configuration = yield* getScheduleConfiguration(configurationDefaults);
|
|
534
568
|
const now = yield* Clock.currentTimeMillis;
|
|
535
569
|
if (wakeDeferrals === 0) {
|
|
536
|
-
deferredRecoveryDelay = configuration.
|
|
570
|
+
deferredRecoveryDelay = configuration.inFlightRecovery;
|
|
537
571
|
deferredRecoveryAt = now + deferredRecoveryDelay;
|
|
538
572
|
wakesReady ??= Deferred.makeUnsafe();
|
|
539
573
|
}
|
|
@@ -566,6 +600,16 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
566
600
|
})).pipe(Effect.onExit((exit) => Effect.gen(function* () {
|
|
567
601
|
const reconcile = dispatchNeedsReconcile || dispatchStarted || Exit.isFailure(exit);
|
|
568
602
|
dispatchActive = Exit.isFailure(exit) && !dispatchStarted;
|
|
603
|
+
if (dispatchActive) {
|
|
604
|
+
const recoveryDelay = yield* getScheduleConfiguration(configurationDefaults).pipe(Effect.match({
|
|
605
|
+
onFailure: () => PARKED_RETRY_DELAY_MS,
|
|
606
|
+
onSuccess: (configuration) => configuration.parkedRetryDelay
|
|
607
|
+
}));
|
|
608
|
+
if (wakeDeferrals === 0) {
|
|
609
|
+
deferredRecoveryDelay = recoveryDelay;
|
|
610
|
+
deferredRecoveryAt = 0;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
569
613
|
const reconciled = yield* (reconcile ? transaction(() => Effect.void) : Effect.void).pipe(Effect.exit);
|
|
570
614
|
dispatchActive = false;
|
|
571
615
|
pendingAlarm = void 0;
|
|
@@ -584,11 +628,11 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
584
628
|
});
|
|
585
629
|
const acknowledgeAlarm = Effect.fnUntraced(function* (row, configuration) {
|
|
586
630
|
if (row.repeat_every_ms === null) {
|
|
587
|
-
|
|
631
|
+
yield* state.storage.sql.exec(`DELETE FROM effect_cf_scheduled_alarms WHERE ${sameRevisionSql}`, row.storage_id, row.revision);
|
|
588
632
|
return;
|
|
589
633
|
}
|
|
590
634
|
const now = yield* Clock.currentTimeMillis;
|
|
591
|
-
if ((yield* (yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET run_at = ? WHERE ${sameRevisionSql}`, now + Math.max(configuration.minimumRepeatInterval, row.repeat_every_ms), row.storage_id, row.
|
|
635
|
+
if ((yield* (yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET run_at = ? WHERE ${sameRevisionSql}`, now + Math.max(configuration.minimumRepeatInterval, row.repeat_every_ms), row.storage_id, row.revision)).rowsWritten) > 0) yield* writeAttempts(row.storage_id, 0, 0, null, row.progress);
|
|
592
636
|
});
|
|
593
637
|
const processDueAlarms = Effect.fn("DurableObjectAlarm.processDueAlarms")(function* (handle, options) {
|
|
594
638
|
const configuration = yield* getScheduleConfiguration(configurationDefaults);
|
|
@@ -607,7 +651,10 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
607
651
|
const dueRows = yield* select("automatic", limit);
|
|
608
652
|
const manualRows = yield* select("manual", DEFAULT_PROCESS_DUE_ALARMS_LIMIT);
|
|
609
653
|
if (manualRows.length > 0) yield* transaction(() => Effect.gen(function* () {
|
|
610
|
-
for (const row of manualRows)
|
|
654
|
+
for (const row of manualRows) {
|
|
655
|
+
const recoveryAt = now + (options?.[ManualTags]?.has(row.tag) ? configuration.inFlightRecovery : configuration.parkedRetryDelay);
|
|
656
|
+
yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET run_at = ?, wake_at = ? WHERE ${sameRevisionSql}`, recoveryAt, recoveryAt, row.storage_id, row.revision);
|
|
657
|
+
}
|
|
611
658
|
}));
|
|
612
659
|
const handled = [];
|
|
613
660
|
const failed = [];
|
|
@@ -665,7 +712,10 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
665
712
|
const permits = dispatch?.permits ?? (yield* Semaphore.make(4));
|
|
666
713
|
const automatic = Effect.forEach(dueRows, handleRow, { discard: true }).pipe(Effect.exit);
|
|
667
714
|
const exits = yield* manualRows.length === 0 ? automatic.pipe(Effect.map((exit) => [exit])) : Effect.all([automatic, Effect.forEach(manualRows, (row) => permits.withPermits(1)(handleRow(row)).pipe(Effect.exit), { concurrency: 4 }).pipe(Effect.flatMap(Exit.asVoidAll), Effect.exit)], { concurrency: 2 });
|
|
668
|
-
yield* transaction(() => Effect.
|
|
715
|
+
yield* transaction(() => Effect.gen(function* () {
|
|
716
|
+
const recoveryAt = (yield* Clock.currentTimeMillis) + configuration.parkedRetryDelay;
|
|
717
|
+
for (const row of manualRows) yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET run_at = ?, wake_at = ? WHERE ${sameRevisionSql}`, recoveryAt, recoveryAt, row.storage_id, row.revision);
|
|
718
|
+
}));
|
|
669
719
|
yield* Exit.asVoidAll(exits);
|
|
670
720
|
return {
|
|
671
721
|
failed,
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
1
2
|
import * as Clock from "effect/Clock";
|
|
2
3
|
import * as Context from "effect/Context";
|
|
3
4
|
import * as Data from "effect/Data";
|
|
@@ -10,6 +11,8 @@ import * as Layer from "effect/Layer";
|
|
|
10
11
|
import * as Predicate from "effect/Predicate";
|
|
11
12
|
import * as S from "effect/Schema";
|
|
12
13
|
import * as Semaphore from "effect/Semaphore";
|
|
14
|
+
import type { SqlClient } from "effect/sql/SqlClient";
|
|
15
|
+
import { SqlError } from "effect/sql/SqlError";
|
|
13
16
|
|
|
14
17
|
import { DurableObjectState } from "./DurableObjectState";
|
|
15
18
|
import { type SqlStorageValue, StorageOperationError } from "./DurableObjectStorage";
|
|
@@ -24,20 +27,17 @@ CREATE TABLE IF NOT EXISTS effect_cf_scheduled_alarms (
|
|
|
24
27
|
run_at INTEGER NOT NULL,
|
|
25
28
|
wake_at INTEGER NOT NULL DEFAULT 0,
|
|
26
29
|
repeat_every_ms INTEGER,
|
|
27
|
-
payload TEXT NOT NULL
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
revision TEXT NOT NULL,
|
|
32
|
-
attempts INTEGER NOT NULL,
|
|
33
|
-
parked INTEGER NOT NULL,
|
|
30
|
+
payload TEXT NOT NULL,
|
|
31
|
+
revision TEXT NOT NULL DEFAULT '',
|
|
32
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
parked INTEGER NOT NULL DEFAULT 0,
|
|
34
34
|
retry_at INTEGER,
|
|
35
|
-
progress INTEGER NOT NULL
|
|
35
|
+
progress INTEGER NOT NULL DEFAULT -1
|
|
36
36
|
);
|
|
37
37
|
`;
|
|
38
38
|
|
|
39
39
|
const WAKE_INDEX = "idx_effect_cf_scheduled_alarms_wake_at_storage_id";
|
|
40
|
-
const LIFECYCLE_INDEX = "
|
|
40
|
+
const LIFECYCLE_INDEX = "idx_effect_cf_scheduled_alarms_lifecycle_wake_at_storage_id_v2";
|
|
41
41
|
|
|
42
42
|
const DEFAULT_PROCESS_DUE_ALARMS_LIMIT = 100;
|
|
43
43
|
|
|
@@ -46,6 +46,7 @@ export const MIN_RETRY_DELAY_MS = 1_000;
|
|
|
46
46
|
export const UNCHANGED_ATTEMPT_BUDGET = 8;
|
|
47
47
|
/** Minimum and default parked recovery interval. */
|
|
48
48
|
export const PARKED_RETRY_DELAY_MS = 3_600_000;
|
|
49
|
+
const DEFAULT_IN_FLIGHT_RECOVERY_MS = 1_000;
|
|
49
50
|
|
|
50
51
|
/** Absolute product repeat floor. */
|
|
51
52
|
export const MIN_REPEAT_INTERVAL_MS = 1_000;
|
|
@@ -55,6 +56,8 @@ export const DEFAULT_MIN_REPEAT_INTERVAL_MS = 60_000;
|
|
|
55
56
|
export interface ScheduleConfiguration {
|
|
56
57
|
/** Check for due alarms after RPC/fetch responses unwind. Native alarms remain recovery authority. */
|
|
57
58
|
readonly dispatchAfterEvent?: boolean;
|
|
59
|
+
/** Positive recovery deadline while dispatch or deferWakes is active. Defaults to one second. */
|
|
60
|
+
readonly inFlightRecovery?: Duration.Input;
|
|
58
61
|
/** Retry floor, at least one second. Defaults to one second. */
|
|
59
62
|
readonly minimumRetryDelay?: Duration.Input;
|
|
60
63
|
/** Positive safe integer. Defaults to eight unchanged attempts. */
|
|
@@ -81,6 +84,7 @@ const AlarmLifecycleSchema = S.Literals(["automatic", "manual"]);
|
|
|
81
84
|
|
|
82
85
|
const ScheduleConfigurationSchema = S.Struct({
|
|
83
86
|
dispatchAfterEvent: S.Boolean,
|
|
87
|
+
inFlightRecovery: safeIntegerAtLeast(1),
|
|
84
88
|
minimumRetryDelay: safeIntegerAtLeast(MIN_RETRY_DELAY_MS),
|
|
85
89
|
unchangedAttemptBudget: safeIntegerAtLeast(1),
|
|
86
90
|
parkedRetryDelay: safeIntegerAtLeast(PARKED_RETRY_DELAY_MS),
|
|
@@ -126,15 +130,8 @@ interface AlarmRow extends Record<string, SqlStorageValue> {
|
|
|
126
130
|
readonly progress: number;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
const alarmRowsSql =
|
|
130
|
-
|
|
131
|
-
COALESCE(s.attempts, 0) AS attempts, COALESCE(s.parked, 0) AS parked,
|
|
132
|
-
s.retry_at, COALESCE(s.progress, -1) AS progress
|
|
133
|
-
FROM effect_cf_scheduled_alarms a
|
|
134
|
-
LEFT JOIN effect_cf_alarm_attempts s USING (storage_id)`;
|
|
135
|
-
|
|
136
|
-
const sameRevisionSql = `storage_id = ? AND COALESCE(
|
|
137
|
-
(SELECT revision FROM effect_cf_alarm_attempts WHERE storage_id = ?), '') = ?`;
|
|
133
|
+
const alarmRowsSql = "SELECT a.* FROM effect_cf_scheduled_alarms a";
|
|
134
|
+
const sameRevisionSql = "storage_id = ? AND revision = ?";
|
|
138
135
|
|
|
139
136
|
/** Content-free: no alarm identifiers, payload, error text or consumer state. */
|
|
140
137
|
export const AlarmParked = S.TaggedStruct("AlarmParked", {
|
|
@@ -230,6 +227,9 @@ const getScheduleConfiguration = Effect.fnUntraced(function* (defaults: Schedule
|
|
|
230
227
|
try: () =>
|
|
231
228
|
S.decodeUnknownSync(ScheduleConfigurationSchema)({
|
|
232
229
|
dispatchAfterEvent: input.dispatchAfterEvent ?? false,
|
|
230
|
+
inFlightRecovery: Math.ceil(
|
|
231
|
+
Duration.toMillis(input.inFlightRecovery ?? DEFAULT_IN_FLIGHT_RECOVERY_MS),
|
|
232
|
+
),
|
|
233
233
|
minimumRetryDelay: Math.ceil(
|
|
234
234
|
Duration.toMillis(input.minimumRetryDelay ?? MIN_RETRY_DELAY_MS),
|
|
235
235
|
),
|
|
@@ -351,6 +351,16 @@ export type AlarmTransaction = Pick<
|
|
|
351
351
|
"scheduleAlarm" | "scheduleAlarmEarlier" | "cancelAlarm"
|
|
352
352
|
>;
|
|
353
353
|
|
|
354
|
+
/** Share the same native commit and connection permit as the caller's SQL writes. */
|
|
355
|
+
export interface AlarmTransactionOptions {
|
|
356
|
+
/** Must be a storage-backed @effect/sql-sqlite-do client for this Durable Object. */
|
|
357
|
+
readonly sqlClient: SqlClient;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const StorageSqlClient = S.declare<SqliteClient.SqliteClient>(
|
|
361
|
+
(value): value is SqliteClient.SqliteClient => Predicate.hasProperty(value, SqliteClient.TypeId),
|
|
362
|
+
);
|
|
363
|
+
|
|
354
364
|
export interface AlarmStatus {
|
|
355
365
|
readonly attempts: number;
|
|
356
366
|
readonly parked: boolean;
|
|
@@ -385,7 +395,7 @@ export type AlarmScheduler = {
|
|
|
385
395
|
* object's concurrent and nested scopes. Source/checkpoint transactions still commit;
|
|
386
396
|
* the last scope reconciles their current deadlines on success, failure or interruption.
|
|
387
397
|
* Run outside transactions, around bounded maintenance or inline work. The first scope's
|
|
388
|
-
*
|
|
398
|
+
* inFlightRecovery bounds the shared guard; renew it if it expires and is consumed while
|
|
389
399
|
* scopes remain active. A process loss before exit recovers at that retained alarm.
|
|
390
400
|
*/
|
|
391
401
|
readonly deferWakes: <A, E, R>(
|
|
@@ -394,7 +404,7 @@ export type AlarmScheduler = {
|
|
|
394
404
|
/**
|
|
395
405
|
* Commits local application storage and logical alarms in one native SQLite
|
|
396
406
|
* Durable Object transaction, reconciling the native alarm before commit.
|
|
397
|
-
* Use the supplied mutations
|
|
407
|
+
* Use the supplied mutations inside the callback instead of standalone alarm methods.
|
|
398
408
|
* SqlClient queries must use this same Durable Object's storage.
|
|
399
409
|
*
|
|
400
410
|
* Failure, defects and interruption before commit roll back. A lost reply or
|
|
@@ -405,6 +415,7 @@ export type AlarmScheduler = {
|
|
|
405
415
|
*/
|
|
406
416
|
readonly transaction: <A, E, R>(
|
|
407
417
|
closure: (alarms: AlarmTransaction) => Effect.Effect<A, E, R>,
|
|
418
|
+
options?: AlarmTransactionOptions,
|
|
408
419
|
) => Effect.Effect<A, E | StorageOperationError, R>;
|
|
409
420
|
|
|
410
421
|
readonly cancelAlarm: (
|
|
@@ -458,13 +469,13 @@ const encodeStoredPayload = (payload: AlarmPayload) =>
|
|
|
458
469
|
);
|
|
459
470
|
|
|
460
471
|
const ensureTable = Effect.fnUntraced(function* (state: DurableObjectState["Service"]) {
|
|
461
|
-
yield* state.storage.sql.exec(INIT_TABLE_SQL);
|
|
462
472
|
const index = yield* state.storage.sql.exec(
|
|
463
473
|
"SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?",
|
|
464
474
|
LIFECYCLE_INDEX,
|
|
465
475
|
);
|
|
466
476
|
|
|
467
477
|
if ((yield* index.toArray()).length > 0) return;
|
|
478
|
+
yield* state.storage.sql.exec(INIT_TABLE_SQL);
|
|
468
479
|
const columns = yield* state.storage.sql.exec<{ name: string }>(
|
|
469
480
|
"SELECT name FROM pragma_table_info('effect_cf_scheduled_alarms')",
|
|
470
481
|
);
|
|
@@ -480,11 +491,36 @@ const ensureTable = Effect.fnUntraced(function* (state: DurableObjectState["Serv
|
|
|
480
491
|
"ALTER TABLE effect_cf_scheduled_alarms ADD COLUMN wake_at INTEGER NOT NULL DEFAULT 0",
|
|
481
492
|
);
|
|
482
493
|
}
|
|
483
|
-
|
|
484
|
-
|
|
494
|
+
for (const [column, definition] of Object.entries({
|
|
495
|
+
revision: "TEXT NOT NULL DEFAULT ''",
|
|
496
|
+
attempts: "INTEGER NOT NULL DEFAULT 0",
|
|
497
|
+
parked: "INTEGER NOT NULL DEFAULT 0",
|
|
498
|
+
retry_at: "INTEGER",
|
|
499
|
+
progress: "INTEGER NOT NULL DEFAULT -1",
|
|
500
|
+
})) {
|
|
501
|
+
if (!names.has(column))
|
|
502
|
+
yield* state.storage.sql.exec(
|
|
503
|
+
`ALTER TABLE effect_cf_scheduled_alarms ADD COLUMN ${column} ${definition}`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
const attempts = yield* state.storage.sql.exec(
|
|
507
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'effect_cf_alarm_attempts'",
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
if ((yield* attempts.toArray()).length > 0) {
|
|
511
|
+
yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET
|
|
512
|
+
(revision, attempts, parked, retry_at, progress) =
|
|
513
|
+
(SELECT revision, attempts, parked, retry_at, progress FROM effect_cf_alarm_attempts s
|
|
514
|
+
WHERE s.storage_id = effect_cf_scheduled_alarms.storage_id)
|
|
515
|
+
WHERE storage_id IN (SELECT storage_id FROM effect_cf_alarm_attempts)`);
|
|
516
|
+
yield* state.storage.sql.exec("DROP TABLE effect_cf_alarm_attempts");
|
|
517
|
+
}
|
|
518
|
+
// The replacement lifecycle index marks the committed single-row migration.
|
|
485
519
|
yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET
|
|
486
|
-
wake_at = MAX(run_at, COALESCE(
|
|
487
|
-
|
|
520
|
+
wake_at = MAX(run_at, COALESCE(retry_at, run_at))`);
|
|
521
|
+
yield* state.storage.sql.exec(
|
|
522
|
+
"DROP INDEX IF EXISTS idx_effect_cf_scheduled_alarms_lifecycle_wake_at_storage_id",
|
|
523
|
+
);
|
|
488
524
|
yield* state.storage.sql.exec(
|
|
489
525
|
"DROP INDEX IF EXISTS idx_effect_cf_scheduled_alarms_run_at_storage_id",
|
|
490
526
|
);
|
|
@@ -676,6 +712,7 @@ export interface DefinedAlarmScheduler<
|
|
|
676
712
|
) => ReturnType<AlarmScheduler["getAlarmStatus"]>;
|
|
677
713
|
readonly transaction: <A, E, R>(
|
|
678
714
|
closure: (alarms: DefinedAlarmTransaction<Definitions>) => Effect.Effect<A, E, R>,
|
|
715
|
+
options?: AlarmTransactionOptions,
|
|
679
716
|
) => Effect.Effect<A, E | StorageOperationError, R>;
|
|
680
717
|
}
|
|
681
718
|
|
|
@@ -943,7 +980,7 @@ const makeDefinition = <const Definitions extends AlarmDefinitions>(definitions:
|
|
|
943
980
|
...bind(alarms),
|
|
944
981
|
deferWakes: alarms.deferWakes,
|
|
945
982
|
getAlarmStatus: (input) => alarms.getAlarmStatus(input),
|
|
946
|
-
transaction: (closure) => alarms.transaction((tx) => closure(bind(tx))),
|
|
983
|
+
transaction: (closure, options) => alarms.transaction((tx) => closure(bind(tx)), options),
|
|
947
984
|
}),
|
|
948
985
|
handlers: <R = never, E = never>(
|
|
949
986
|
handlers: DefinedAlarmHandlers<Definitions, R, E>,
|
|
@@ -1154,21 +1191,14 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1154
1191
|
progress: number,
|
|
1155
1192
|
) {
|
|
1156
1193
|
yield* state.storage.sql.exec(
|
|
1157
|
-
`
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
storageId,
|
|
1194
|
+
`UPDATE effect_cf_scheduled_alarms SET revision = ?, attempts = ?, parked = ?,
|
|
1195
|
+
retry_at = ?, progress = ?, wake_at = MAX(run_at, COALESCE(?, run_at))
|
|
1196
|
+
WHERE storage_id = ?`,
|
|
1161
1197
|
crypto.randomUUID(),
|
|
1162
1198
|
attempts,
|
|
1163
1199
|
parked,
|
|
1164
1200
|
retryAt,
|
|
1165
1201
|
progress,
|
|
1166
|
-
);
|
|
1167
|
-
// Materialize the effective wake in the same transaction for indexed dispatch/reconciliation.
|
|
1168
|
-
yield* state.storage.sql.exec(
|
|
1169
|
-
`UPDATE effect_cf_scheduled_alarms
|
|
1170
|
-
SET wake_at = MAX(run_at, COALESCE(?, run_at))
|
|
1171
|
-
WHERE storage_id = ?`,
|
|
1172
1202
|
retryAt,
|
|
1173
1203
|
storageId,
|
|
1174
1204
|
);
|
|
@@ -1182,10 +1212,6 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1182
1212
|
`DELETE FROM effect_cf_scheduled_alarms WHERE storage_id = ?`,
|
|
1183
1213
|
storageId,
|
|
1184
1214
|
);
|
|
1185
|
-
yield* state.storage.sql.exec(
|
|
1186
|
-
`DELETE FROM effect_cf_alarm_attempts WHERE storage_id = ?`,
|
|
1187
|
-
storageId,
|
|
1188
|
-
);
|
|
1189
1215
|
});
|
|
1190
1216
|
|
|
1191
1217
|
const scheduleAlarm = Effect.fn("DurableObjectAlarm.scheduleAlarm")(function* (
|
|
@@ -1215,6 +1241,34 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1215
1241
|
);
|
|
1216
1242
|
const payload = yield* encodeStoredPayload(input.payload);
|
|
1217
1243
|
const storageId = getScheduledEventId(ref);
|
|
1244
|
+
const runAt = DateTime.toEpochMillis(input.runAt);
|
|
1245
|
+
|
|
1246
|
+
if (lifecycle === "manual") {
|
|
1247
|
+
// The queue owns attempts. One row carries the aggregate deadline and dispatch fence.
|
|
1248
|
+
const deadline = earlier ? "MIN(run_at, excluded.run_at)" : "excluded.run_at";
|
|
1249
|
+
const nextPayload = earlier
|
|
1250
|
+
? "CASE WHEN run_at <= excluded.run_at THEN payload ELSE excluded.payload END"
|
|
1251
|
+
: "excluded.payload";
|
|
1252
|
+
|
|
1253
|
+
yield* state.storage.sql.exec(
|
|
1254
|
+
`INSERT INTO effect_cf_scheduled_alarms
|
|
1255
|
+
(storage_id, alarm_id, tag, lifecycle, run_at, wake_at, payload, revision)
|
|
1256
|
+
VALUES (?, ?, ?, 'manual', ?, ?, ?, ?)
|
|
1257
|
+
ON CONFLICT(storage_id) DO UPDATE SET
|
|
1258
|
+
lifecycle = 'manual', run_at = ${deadline}, wake_at = ${deadline},
|
|
1259
|
+
payload = ${nextPayload}, revision = excluded.revision,
|
|
1260
|
+
repeat_every_ms = NULL, attempts = 0, parked = 0, retry_at = NULL, progress = -1`,
|
|
1261
|
+
storageId,
|
|
1262
|
+
ref.id,
|
|
1263
|
+
ref.tag,
|
|
1264
|
+
runAt,
|
|
1265
|
+
runAt,
|
|
1266
|
+
payload,
|
|
1267
|
+
crypto.randomUUID(),
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1218
1272
|
const existing = yield* readRow(storageId);
|
|
1219
1273
|
const pass = yield* CurrentAlarmPass;
|
|
1220
1274
|
const source =
|
|
@@ -1252,22 +1306,17 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1252
1306
|
const now = yield* Clock.currentTimeMillis;
|
|
1253
1307
|
const retryAt =
|
|
1254
1308
|
attempts === 0 ? null : now + retryDelay(attempts, configuration, parked === 1);
|
|
1255
|
-
const runAt = DateTime.toEpochMillis(input.runAt);
|
|
1256
1309
|
const keepEarlier = earlier && existing !== undefined && existing.run_at <= runAt;
|
|
1257
1310
|
const scheduledAt = keepEarlier ? existing.run_at : runAt;
|
|
1258
1311
|
const scheduledPayload = keepEarlier ? existing.payload : payload;
|
|
1259
|
-
const scheduledRepeat =
|
|
1260
|
-
|
|
1261
|
-
? null
|
|
1262
|
-
: keepEarlier
|
|
1263
|
-
? existing.repeat_every_ms
|
|
1264
|
-
: repeatEveryMillis;
|
|
1265
|
-
const nextProgress = lifecycle === "manual" ? -1 : Math.max(progress, input.progress ?? -1);
|
|
1312
|
+
const scheduledRepeat = keepEarlier ? existing.repeat_every_ms : repeatEveryMillis;
|
|
1313
|
+
const nextProgress = Math.max(progress, input.progress ?? -1);
|
|
1266
1314
|
|
|
1267
1315
|
yield* state.storage.sql.exec(
|
|
1268
1316
|
`INSERT OR REPLACE INTO effect_cf_scheduled_alarms
|
|
1269
|
-
(storage_id, alarm_id, tag, lifecycle, run_at, repeat_every_ms, payload
|
|
1270
|
-
|
|
1317
|
+
(storage_id, alarm_id, tag, lifecycle, run_at, repeat_every_ms, payload,
|
|
1318
|
+
wake_at, revision, attempts, parked, retry_at, progress)
|
|
1319
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1271
1320
|
storageId,
|
|
1272
1321
|
ref.id,
|
|
1273
1322
|
ref.tag,
|
|
@@ -1275,8 +1324,13 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1275
1324
|
scheduledAt,
|
|
1276
1325
|
scheduledRepeat,
|
|
1277
1326
|
scheduledPayload,
|
|
1327
|
+
Math.max(scheduledAt, retryAt ?? scheduledAt),
|
|
1328
|
+
crypto.randomUUID(),
|
|
1329
|
+
attempts,
|
|
1330
|
+
parked,
|
|
1331
|
+
retryAt,
|
|
1332
|
+
nextProgress,
|
|
1278
1333
|
);
|
|
1279
|
-
yield* writeAttempts(storageId, attempts, parked, retryAt, nextProgress);
|
|
1280
1334
|
if (parked === 1 && retryAt !== null && (existing?.parked ?? source?.parked ?? 0) === 0) {
|
|
1281
1335
|
reports.push(storageId);
|
|
1282
1336
|
}
|
|
@@ -1303,20 +1357,22 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1303
1357
|
}
|
|
1304
1358
|
});
|
|
1305
1359
|
|
|
1306
|
-
const transaction: AlarmScheduler["transaction"] = Effect.fnUntraced(
|
|
1307
|
-
|
|
1360
|
+
const transaction: AlarmScheduler["transaction"] = Effect.fnUntraced(
|
|
1361
|
+
function* (closure, options) {
|
|
1362
|
+
const pass = yield* CurrentAlarmPass;
|
|
1308
1363
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1364
|
+
if (pass !== undefined && !pass.active) {
|
|
1365
|
+
return yield* Effect.fail(
|
|
1366
|
+
new StorageOperationError({
|
|
1367
|
+
operation: "alarm.transaction",
|
|
1368
|
+
cause: new Error(
|
|
1369
|
+
"Alarm handlers cannot mutate detached work after their pass ends",
|
|
1370
|
+
),
|
|
1371
|
+
}),
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
const reports: string[] = [];
|
|
1375
|
+
const body = Effect.withFiber((owner) => {
|
|
1320
1376
|
let active = true;
|
|
1321
1377
|
const requireActive = <A, E>(effect: Effect.Effect<A, E>) =>
|
|
1322
1378
|
Effect.withFiber<A, E | StorageOperationError>((fiber) =>
|
|
@@ -1354,13 +1410,44 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1354
1410
|
|
|
1355
1411
|
return result;
|
|
1356
1412
|
});
|
|
1357
|
-
})
|
|
1358
|
-
|
|
1413
|
+
});
|
|
1414
|
+
const result = yield* options === undefined
|
|
1415
|
+
? state.storage.transaction(() => body)
|
|
1416
|
+
: Effect.gen(function* () {
|
|
1417
|
+
const client = yield* S.decodeUnknownEffect(StorageSqlClient)(
|
|
1418
|
+
options.sqlClient,
|
|
1419
|
+
).pipe(
|
|
1420
|
+
Effect.mapError(
|
|
1421
|
+
(cause) => new StorageOperationError({ operation: "alarm.transaction", cause }),
|
|
1422
|
+
),
|
|
1423
|
+
);
|
|
1359
1424
|
|
|
1360
|
-
|
|
1425
|
+
if (client.config.storage !== state.raw.storage)
|
|
1426
|
+
return yield* Effect.fail(
|
|
1427
|
+
new StorageOperationError({
|
|
1428
|
+
operation: "alarm.transaction",
|
|
1429
|
+
cause: new Error(
|
|
1430
|
+
"Alarm transactions require this Durable Object's storage-backed SQL client",
|
|
1431
|
+
),
|
|
1432
|
+
}),
|
|
1433
|
+
);
|
|
1361
1434
|
|
|
1362
|
-
|
|
1363
|
-
|
|
1435
|
+
return yield* client
|
|
1436
|
+
.withTransaction(body)
|
|
1437
|
+
.pipe(
|
|
1438
|
+
Effect.mapError((cause) =>
|
|
1439
|
+
cause instanceof SqlError
|
|
1440
|
+
? new StorageOperationError({ operation: "alarm.transaction", cause })
|
|
1441
|
+
: cause,
|
|
1442
|
+
),
|
|
1443
|
+
);
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
yield* reportParked(reports);
|
|
1447
|
+
|
|
1448
|
+
return result;
|
|
1449
|
+
},
|
|
1450
|
+
);
|
|
1364
1451
|
|
|
1365
1452
|
const deferWakes: AlarmScheduler["deferWakes"] = (body) =>
|
|
1366
1453
|
Effect.acquireUseRelease(
|
|
@@ -1369,7 +1456,7 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1369
1456
|
const now = yield* Clock.currentTimeMillis;
|
|
1370
1457
|
|
|
1371
1458
|
if (wakeDeferrals === 0) {
|
|
1372
|
-
deferredRecoveryDelay = configuration.
|
|
1459
|
+
deferredRecoveryDelay = configuration.inFlightRecovery;
|
|
1373
1460
|
deferredRecoveryAt = now + deferredRecoveryDelay;
|
|
1374
1461
|
wakesReady ??= Deferred.makeUnsafe<void>();
|
|
1375
1462
|
}
|
|
@@ -1448,6 +1535,22 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1448
1535
|
dispatchNeedsReconcile || dispatchStarted || Exit.isFailure(exit);
|
|
1449
1536
|
|
|
1450
1537
|
dispatchActive = Exit.isFailure(exit) && !dispatchStarted;
|
|
1538
|
+
if (dispatchActive) {
|
|
1539
|
+
const recoveryDelay = yield* getScheduleConfiguration(
|
|
1540
|
+
configurationDefaults,
|
|
1541
|
+
).pipe(
|
|
1542
|
+
Effect.match({
|
|
1543
|
+
onFailure: () => PARKED_RETRY_DELAY_MS,
|
|
1544
|
+
onSuccess: (configuration) => configuration.parkedRetryDelay,
|
|
1545
|
+
}),
|
|
1546
|
+
);
|
|
1547
|
+
|
|
1548
|
+
// A concurrent deferred scope retains ownership of its short guard.
|
|
1549
|
+
if (wakeDeferrals === 0) {
|
|
1550
|
+
deferredRecoveryDelay = recoveryDelay;
|
|
1551
|
+
deferredRecoveryAt = 0;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1451
1554
|
const reconciled = yield* (
|
|
1452
1555
|
reconcile ? transaction(() => Effect.void) : Effect.void
|
|
1453
1556
|
).pipe(Effect.exit);
|
|
@@ -1493,20 +1596,12 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1493
1596
|
configuration: ResolvedScheduleConfiguration,
|
|
1494
1597
|
) {
|
|
1495
1598
|
if (row.repeat_every_ms === null) {
|
|
1496
|
-
|
|
1599
|
+
yield* state.storage.sql.exec(
|
|
1497
1600
|
`DELETE FROM effect_cf_scheduled_alarms WHERE ${sameRevisionSql}`,
|
|
1498
1601
|
row.storage_id,
|
|
1499
|
-
row.storage_id,
|
|
1500
1602
|
row.revision,
|
|
1501
1603
|
);
|
|
1502
1604
|
|
|
1503
|
-
if ((yield* cursor.rowsWritten) > 0) {
|
|
1504
|
-
yield* state.storage.sql.exec(
|
|
1505
|
-
`DELETE FROM effect_cf_alarm_attempts WHERE storage_id = ?`,
|
|
1506
|
-
row.storage_id,
|
|
1507
|
-
);
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
1605
|
return;
|
|
1511
1606
|
}
|
|
1512
1607
|
const now = yield* Clock.currentTimeMillis;
|
|
@@ -1514,7 +1609,6 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1514
1609
|
`UPDATE effect_cf_scheduled_alarms SET run_at = ? WHERE ${sameRevisionSql}`,
|
|
1515
1610
|
now + Math.max(configuration.minimumRepeatInterval, row.repeat_every_ms),
|
|
1516
1611
|
row.storage_id,
|
|
1517
|
-
row.storage_id,
|
|
1518
1612
|
row.revision,
|
|
1519
1613
|
);
|
|
1520
1614
|
|
|
@@ -1570,12 +1664,17 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1570
1664
|
yield* transaction(() =>
|
|
1571
1665
|
Effect.gen(function* () {
|
|
1572
1666
|
for (const row of manualRows) {
|
|
1573
|
-
// Keep
|
|
1667
|
+
// Keep work eligible after eviction; unknown registrations stay parked.
|
|
1668
|
+
const recoveryAt =
|
|
1669
|
+
now +
|
|
1670
|
+
(options?.[ManualTags]?.has(row.tag)
|
|
1671
|
+
? configuration.inFlightRecovery
|
|
1672
|
+
: configuration.parkedRetryDelay);
|
|
1673
|
+
|
|
1574
1674
|
yield* state.storage.sql.exec(
|
|
1575
1675
|
`UPDATE effect_cf_scheduled_alarms SET run_at = ?, wake_at = ? WHERE ${sameRevisionSql}`,
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
row.storage_id,
|
|
1676
|
+
recoveryAt,
|
|
1677
|
+
recoveryAt,
|
|
1579
1678
|
row.storage_id,
|
|
1580
1679
|
row.revision,
|
|
1581
1680
|
);
|
|
@@ -1686,7 +1785,22 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1686
1785
|
{ concurrency: 2 },
|
|
1687
1786
|
);
|
|
1688
1787
|
|
|
1689
|
-
yield* transaction(() =>
|
|
1788
|
+
yield* transaction(() =>
|
|
1789
|
+
Effect.gen(function* () {
|
|
1790
|
+
const recoveryAt = (yield* Clock.currentTimeMillis) + configuration.parkedRetryDelay;
|
|
1791
|
+
|
|
1792
|
+
for (const row of manualRows) {
|
|
1793
|
+
// Only a returned handler with no newer checkpoint falls back to parked recovery.
|
|
1794
|
+
yield* state.storage.sql.exec(
|
|
1795
|
+
`UPDATE effect_cf_scheduled_alarms SET run_at = ?, wake_at = ? WHERE ${sameRevisionSql}`,
|
|
1796
|
+
recoveryAt,
|
|
1797
|
+
recoveryAt,
|
|
1798
|
+
row.storage_id,
|
|
1799
|
+
row.revision,
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
}),
|
|
1803
|
+
);
|
|
1690
1804
|
yield* Exit.asVoidAll(exits);
|
|
1691
1805
|
|
|
1692
1806
|
return { failed, handled, parked };
|