effect-cf 0.54.0 → 0.55.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
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# effect-cf
|
|
2
2
|
|
|
3
|
+
## 0.55.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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.
|
|
8
|
+
|
|
3
9
|
## 0.54.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -21,6 +21,8 @@ declare const DEFAULT_MIN_REPEAT_INTERVAL_MS = 60000;
|
|
|
21
21
|
interface ScheduleConfiguration {
|
|
22
22
|
/** Check for due alarms after RPC/fetch responses unwind. Native alarms remain recovery authority. */
|
|
23
23
|
readonly dispatchAfterEvent?: boolean;
|
|
24
|
+
/** Positive recovery deadline while dispatch or deferWakes is active. Defaults to one second. */
|
|
25
|
+
readonly inFlightRecovery?: Duration.Input;
|
|
24
26
|
/** Retry floor, at least one second. Defaults to one second. */
|
|
25
27
|
readonly minimumRetryDelay?: Duration.Input;
|
|
26
28
|
/** Positive safe integer. Defaults to eight unchanged attempts. */
|
|
@@ -184,7 +186,7 @@ type AlarmScheduler = {
|
|
|
184
186
|
* object's concurrent and nested scopes. Source/checkpoint transactions still commit;
|
|
185
187
|
* the last scope reconciles their current deadlines on success, failure or interruption.
|
|
186
188
|
* Run outside transactions, around bounded maintenance or inline work. The first scope's
|
|
187
|
-
*
|
|
189
|
+
* inFlightRecovery bounds the shared guard; renew it if it expires and is consumed while
|
|
188
190
|
* scopes remain active. A process loss before exit recovers at that retained alarm.
|
|
189
191
|
*/
|
|
190
192
|
readonly deferWakes: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | InvalidScheduleConfigurationError | StorageOperationError, R>;
|
|
@@ -68,6 +68,7 @@ const DEFAULT_PROCESS_DUE_ALARMS_LIMIT = 100;
|
|
|
68
68
|
const MIN_RETRY_DELAY_MS = 1e3;
|
|
69
69
|
/** Minimum and default parked recovery interval. */
|
|
70
70
|
const PARKED_RETRY_DELAY_MS = 36e5;
|
|
71
|
+
const DEFAULT_IN_FLIGHT_RECOVERY_MS = 1e3;
|
|
71
72
|
/** Absolute product repeat floor. */
|
|
72
73
|
const MIN_REPEAT_INTERVAL_MS = 1e3;
|
|
73
74
|
/** Default product schedule floor. Never use repeats to poll state. */
|
|
@@ -78,6 +79,7 @@ const safeIntegerAtLeast = (minimum) => S.Int.check(S.isGreaterThanOrEqualTo(min
|
|
|
78
79
|
const AlarmLifecycleSchema = S.Literals(["automatic", "manual"]);
|
|
79
80
|
const ScheduleConfigurationSchema = S.Struct({
|
|
80
81
|
dispatchAfterEvent: S.Boolean,
|
|
82
|
+
inFlightRecovery: safeIntegerAtLeast(1),
|
|
81
83
|
minimumRetryDelay: safeIntegerAtLeast(MIN_RETRY_DELAY_MS),
|
|
82
84
|
unchangedAttemptBudget: safeIntegerAtLeast(1),
|
|
83
85
|
parkedRetryDelay: safeIntegerAtLeast(PARKED_RETRY_DELAY_MS),
|
|
@@ -142,6 +144,7 @@ const getScheduleConfiguration = Effect.fnUntraced(function* (defaults) {
|
|
|
142
144
|
return yield* Effect.try({
|
|
143
145
|
try: () => S.decodeUnknownSync(ScheduleConfigurationSchema)({
|
|
144
146
|
dispatchAfterEvent: input.dispatchAfterEvent ?? false,
|
|
147
|
+
inFlightRecovery: Math.ceil(Duration.toMillis(input.inFlightRecovery ?? DEFAULT_IN_FLIGHT_RECOVERY_MS)),
|
|
145
148
|
minimumRetryDelay: Math.ceil(Duration.toMillis(input.minimumRetryDelay ?? 1e3)),
|
|
146
149
|
unchangedAttemptBudget: input.unchangedAttemptBudget ?? 8,
|
|
147
150
|
parkedRetryDelay: Math.ceil(Duration.toMillis(input.parkedRetryDelay ?? 36e5)),
|
|
@@ -533,7 +536,7 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
533
536
|
const configuration = yield* getScheduleConfiguration(configurationDefaults);
|
|
534
537
|
const now = yield* Clock.currentTimeMillis;
|
|
535
538
|
if (wakeDeferrals === 0) {
|
|
536
|
-
deferredRecoveryDelay = configuration.
|
|
539
|
+
deferredRecoveryDelay = configuration.inFlightRecovery;
|
|
537
540
|
deferredRecoveryAt = now + deferredRecoveryDelay;
|
|
538
541
|
wakesReady ??= Deferred.makeUnsafe();
|
|
539
542
|
}
|
|
@@ -566,6 +569,16 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
566
569
|
})).pipe(Effect.onExit((exit) => Effect.gen(function* () {
|
|
567
570
|
const reconcile = dispatchNeedsReconcile || dispatchStarted || Exit.isFailure(exit);
|
|
568
571
|
dispatchActive = Exit.isFailure(exit) && !dispatchStarted;
|
|
572
|
+
if (dispatchActive) {
|
|
573
|
+
const recoveryDelay = yield* getScheduleConfiguration(configurationDefaults).pipe(Effect.match({
|
|
574
|
+
onFailure: () => PARKED_RETRY_DELAY_MS,
|
|
575
|
+
onSuccess: (configuration) => configuration.parkedRetryDelay
|
|
576
|
+
}));
|
|
577
|
+
if (wakeDeferrals === 0) {
|
|
578
|
+
deferredRecoveryDelay = recoveryDelay;
|
|
579
|
+
deferredRecoveryAt = 0;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
569
582
|
const reconciled = yield* (reconcile ? transaction(() => Effect.void) : Effect.void).pipe(Effect.exit);
|
|
570
583
|
dispatchActive = false;
|
|
571
584
|
pendingAlarm = void 0;
|
|
@@ -607,7 +620,10 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
607
620
|
const dueRows = yield* select("automatic", limit);
|
|
608
621
|
const manualRows = yield* select("manual", DEFAULT_PROCESS_DUE_ALARMS_LIMIT);
|
|
609
622
|
if (manualRows.length > 0) yield* transaction(() => Effect.gen(function* () {
|
|
610
|
-
for (const row of manualRows)
|
|
623
|
+
for (const row of manualRows) {
|
|
624
|
+
const recoveryAt = now + (options?.[ManualTags]?.has(row.tag) ? configuration.inFlightRecovery : configuration.parkedRetryDelay);
|
|
625
|
+
yield* state.storage.sql.exec(`UPDATE effect_cf_scheduled_alarms SET run_at = ?, wake_at = ? WHERE ${sameRevisionSql}`, recoveryAt, recoveryAt, row.storage_id, row.storage_id, row.revision);
|
|
626
|
+
}
|
|
611
627
|
}));
|
|
612
628
|
const handled = [];
|
|
613
629
|
const failed = [];
|
|
@@ -665,7 +681,10 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
|
|
|
665
681
|
const permits = dispatch?.permits ?? (yield* Semaphore.make(4));
|
|
666
682
|
const automatic = Effect.forEach(dueRows, handleRow, { discard: true }).pipe(Effect.exit);
|
|
667
683
|
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.
|
|
684
|
+
yield* transaction(() => Effect.gen(function* () {
|
|
685
|
+
const recoveryAt = (yield* Clock.currentTimeMillis) + configuration.parkedRetryDelay;
|
|
686
|
+
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.storage_id, row.revision);
|
|
687
|
+
}));
|
|
669
688
|
yield* Exit.asVoidAll(exits);
|
|
670
689
|
return {
|
|
671
690
|
failed,
|
package/package.json
CHANGED
|
@@ -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),
|
|
@@ -230,6 +234,9 @@ const getScheduleConfiguration = Effect.fnUntraced(function* (defaults: Schedule
|
|
|
230
234
|
try: () =>
|
|
231
235
|
S.decodeUnknownSync(ScheduleConfigurationSchema)({
|
|
232
236
|
dispatchAfterEvent: input.dispatchAfterEvent ?? false,
|
|
237
|
+
inFlightRecovery: Math.ceil(
|
|
238
|
+
Duration.toMillis(input.inFlightRecovery ?? DEFAULT_IN_FLIGHT_RECOVERY_MS),
|
|
239
|
+
),
|
|
233
240
|
minimumRetryDelay: Math.ceil(
|
|
234
241
|
Duration.toMillis(input.minimumRetryDelay ?? MIN_RETRY_DELAY_MS),
|
|
235
242
|
),
|
|
@@ -385,7 +392,7 @@ export type AlarmScheduler = {
|
|
|
385
392
|
* object's concurrent and nested scopes. Source/checkpoint transactions still commit;
|
|
386
393
|
* the last scope reconciles their current deadlines on success, failure or interruption.
|
|
387
394
|
* Run outside transactions, around bounded maintenance or inline work. The first scope's
|
|
388
|
-
*
|
|
395
|
+
* inFlightRecovery bounds the shared guard; renew it if it expires and is consumed while
|
|
389
396
|
* scopes remain active. A process loss before exit recovers at that retained alarm.
|
|
390
397
|
*/
|
|
391
398
|
readonly deferWakes: <A, E, R>(
|
|
@@ -1369,7 +1376,7 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1369
1376
|
const now = yield* Clock.currentTimeMillis;
|
|
1370
1377
|
|
|
1371
1378
|
if (wakeDeferrals === 0) {
|
|
1372
|
-
deferredRecoveryDelay = configuration.
|
|
1379
|
+
deferredRecoveryDelay = configuration.inFlightRecovery;
|
|
1373
1380
|
deferredRecoveryAt = now + deferredRecoveryDelay;
|
|
1374
1381
|
wakesReady ??= Deferred.makeUnsafe<void>();
|
|
1375
1382
|
}
|
|
@@ -1448,6 +1455,22 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1448
1455
|
dispatchNeedsReconcile || dispatchStarted || Exit.isFailure(exit);
|
|
1449
1456
|
|
|
1450
1457
|
dispatchActive = Exit.isFailure(exit) && !dispatchStarted;
|
|
1458
|
+
if (dispatchActive) {
|
|
1459
|
+
const recoveryDelay = yield* getScheduleConfiguration(
|
|
1460
|
+
configurationDefaults,
|
|
1461
|
+
).pipe(
|
|
1462
|
+
Effect.match({
|
|
1463
|
+
onFailure: () => PARKED_RETRY_DELAY_MS,
|
|
1464
|
+
onSuccess: (configuration) => configuration.parkedRetryDelay,
|
|
1465
|
+
}),
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
// A concurrent deferred scope retains ownership of its short guard.
|
|
1469
|
+
if (wakeDeferrals === 0) {
|
|
1470
|
+
deferredRecoveryDelay = recoveryDelay;
|
|
1471
|
+
deferredRecoveryAt = 0;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1451
1474
|
const reconciled = yield* (
|
|
1452
1475
|
reconcile ? transaction(() => Effect.void) : Effect.void
|
|
1453
1476
|
).pipe(Effect.exit);
|
|
@@ -1570,11 +1593,17 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1570
1593
|
yield* transaction(() =>
|
|
1571
1594
|
Effect.gen(function* () {
|
|
1572
1595
|
for (const row of manualRows) {
|
|
1573
|
-
// Keep
|
|
1596
|
+
// Keep work eligible after eviction; unknown registrations stay parked.
|
|
1597
|
+
const recoveryAt =
|
|
1598
|
+
now +
|
|
1599
|
+
(options?.[ManualTags]?.has(row.tag)
|
|
1600
|
+
? configuration.inFlightRecovery
|
|
1601
|
+
: configuration.parkedRetryDelay);
|
|
1602
|
+
|
|
1574
1603
|
yield* state.storage.sql.exec(
|
|
1575
1604
|
`UPDATE effect_cf_scheduled_alarms SET run_at = ?, wake_at = ? WHERE ${sameRevisionSql}`,
|
|
1576
|
-
|
|
1577
|
-
|
|
1605
|
+
recoveryAt,
|
|
1606
|
+
recoveryAt,
|
|
1578
1607
|
row.storage_id,
|
|
1579
1608
|
row.storage_id,
|
|
1580
1609
|
row.revision,
|
|
@@ -1686,7 +1715,23 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
|
|
|
1686
1715
|
{ concurrency: 2 },
|
|
1687
1716
|
);
|
|
1688
1717
|
|
|
1689
|
-
yield* transaction(() =>
|
|
1718
|
+
yield* transaction(() =>
|
|
1719
|
+
Effect.gen(function* () {
|
|
1720
|
+
const recoveryAt = (yield* Clock.currentTimeMillis) + configuration.parkedRetryDelay;
|
|
1721
|
+
|
|
1722
|
+
for (const row of manualRows) {
|
|
1723
|
+
// Only a returned handler with no newer checkpoint falls back to parked recovery.
|
|
1724
|
+
yield* state.storage.sql.exec(
|
|
1725
|
+
`UPDATE effect_cf_scheduled_alarms SET run_at = ?, wake_at = ? WHERE ${sameRevisionSql}`,
|
|
1726
|
+
recoveryAt,
|
|
1727
|
+
recoveryAt,
|
|
1728
|
+
row.storage_id,
|
|
1729
|
+
row.storage_id,
|
|
1730
|
+
row.revision,
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
}),
|
|
1734
|
+
);
|
|
1690
1735
|
yield* Exit.asVoidAll(exits);
|
|
1691
1736
|
|
|
1692
1737
|
return { failed, handled, parked };
|