@rikalabs/akter 0.1.0-alpha.4 → 0.1.0-alpha.6

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/actor/descriptor.js +3 -0
  3. package/dist/actor/handles.d.ts +1 -1
  4. package/dist/actor/layers.js +2 -0
  5. package/dist/actor/reads.js +2 -1
  6. package/dist/contexts/command.d.ts +2 -0
  7. package/dist/identity/caller.d.ts +4 -4
  8. package/dist/identity/caller.js +11 -2
  9. package/dist/identity/mint.d.ts +1 -1
  10. package/dist/policies/command.d.ts +3 -0
  11. package/dist/policies/command.js +1 -0
  12. package/dist/runtime/connections/owner.js +60 -13
  13. package/dist/runtime/connections/protocol.d.ts +2 -2
  14. package/dist/runtime/connections/streams.js +2 -1
  15. package/dist/runtime/database/bounded.js +17 -2
  16. package/dist/runtime/database/connection.d.ts +3 -2
  17. package/dist/runtime/database/connection.js +9 -4
  18. package/dist/runtime/database/coordination.d.ts +6 -3
  19. package/dist/runtime/database/coordination.js +19 -18
  20. package/dist/runtime/database/migrations.d.ts +4 -0
  21. package/dist/runtime/database/migrations.js +72 -1
  22. package/dist/runtime/entity/mailbox.d.ts +18 -10
  23. package/dist/runtime/entity/mailbox.js +42 -34
  24. package/dist/runtime/entity/register.d.ts +3 -3
  25. package/dist/runtime/entity/register.js +280 -271
  26. package/dist/runtime/events/append.d.ts +3 -3
  27. package/dist/runtime/index.d.ts +4 -1
  28. package/dist/runtime/index.js +2 -1
  29. package/dist/runtime/layer.d.ts +24 -1
  30. package/dist/runtime/layer.js +64 -9
  31. package/dist/runtime/members.d.ts +2 -0
  32. package/dist/runtime/operators/export.d.ts +3 -1
  33. package/dist/runtime/operators/export.js +42 -11
  34. package/dist/runtime/queries.d.ts +1 -1
  35. package/dist/runtime/queries.js +23 -6
  36. package/dist/runtime/registration.js +27 -1
  37. package/dist/runtime/request.d.ts +1 -1
  38. package/dist/runtime/runner.d.ts +18 -1
  39. package/dist/runtime/runner.js +24 -4
  40. package/dist/runtime/storage/cold-storage.d.ts +56 -0
  41. package/dist/runtime/storage/cold-storage.js +138 -0
  42. package/dist/runtime/storage/cold-tier.d.ts +141 -0
  43. package/dist/runtime/storage/cold-tier.js +355 -0
  44. package/dist/runtime/storage/generation.d.ts +14 -0
  45. package/dist/runtime/storage/generation.js +2 -0
  46. package/dist/runtime/storage/placements.js +1 -0
  47. package/dist/runtime/telemetry/metrics.d.ts +4 -0
  48. package/dist/runtime/telemetry/metrics.js +20 -4
  49. package/dist/runtime/telemetry/sampler.js +4 -3
  50. package/dist/runtime/telemetry/spans.d.ts +1 -1
  51. package/dist/runtime/topology/locks.js +8 -6
  52. package/dist/runtime/turn/blobs.d.ts +4 -1
  53. package/dist/runtime/turn/blobs.js +19 -2
  54. package/dist/runtime/turn/execute.d.ts +3 -1
  55. package/dist/runtime/turn/execute.js +272 -20
  56. package/dist/runtime/turn/hooks.d.ts +1 -1
  57. package/dist/runtime/turn/outbox.d.ts +1 -1
  58. package/dist/runtime/turn/receipt.d.ts +1 -1
  59. package/dist/runtime/turn/relay.d.ts +2 -2
  60. package/dist/runtime/turn/relay.js +53 -7
  61. package/dist/testing/actor-test.d.ts +1 -0
  62. package/dist/testing/actor-test.js +4 -1
  63. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { Cause, Clock, Crypto, Effect, Exit, Option, Predicate, Result, Schema, Scope, } from "effect";
1
+ import { Cause, Clock, Context, Crypto, Effect, Exit, Option, Predicate, Result, Schema, Scope, } from "effect";
2
2
  import { SqlClient, SqlError } from "effect/sql";
3
3
  import { ActorError, CommandExpired, NotCreated, Unauthorized } from "../../errors/actor.js";
4
4
  import {} from "../members.js";
@@ -12,7 +12,8 @@ import { eventsStatement, notifyEvents } from "../events/append.js";
12
12
  import { COMMIT_VERSION } from "../database/replica.js";
13
13
  import { isPoolRefusal } from "../database/bounded.js";
14
14
  import { compress, decompress } from "../storage/codec.js";
15
- import { Metrics, record } from "../telemetry/metrics.js";
15
+ import { ColdTier, FetchCold } from "../storage/cold-tier.js";
16
+ import { count, Metrics, record } from "../telemetry/metrics.js";
16
17
  import { SpanNames } from "../telemetry/spans.js";
17
18
  import { receiptMarginMs } from "../storage/retention.js";
18
19
  import { hashedPayload } from "../subscriptions/identity.js";
@@ -26,6 +27,10 @@ import { asSqlConnection, awaitReplies, queueStatements, sendPipelined, sendSequ
26
27
  import { MERGE_CAP, merges } from "../entity/mailbox.js";
27
28
  import { checkReceipt, encodeOutcome, hashCanonical } from "./receipt.js";
28
29
  const isSystem = Schema.is(System);
30
+ /** Tests can exercise the ordinary admission pipeline independently of warm speculation. */
31
+ export const WarmTurnFastPath = Context.Reference("akter/WarmTurnFastPath", {
32
+ defaultValue: () => true,
33
+ });
29
34
  /**
30
35
  * The cursor row once `delivery` applied, as its commit writes it: a routed
31
36
  * delivery creates the epoch-0 row or raises its position, a dynamic one moves
@@ -210,9 +215,12 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
210
215
  const sql = yield* SqlClient.SqlClient;
211
216
  const hooks = yield* TurnHooks;
212
217
  const clock = yield* FrameworkClock;
218
+ const tier = yield* ColdTier;
213
219
  const scope = yield* TenantScope;
220
+ const warmTurns = yield* WarmTurnFastPath;
214
221
  const { ref } = run.first[0].request;
215
222
  const { tenant, actor, id } = ref;
223
+ let coldStartedAtMs;
216
224
  const role = scope.role ?? (scope.adoption?.enforced.has(actor) === true ? scope.adoption.role : undefined);
217
225
  const actorRow = rowOf({ sql, actor: { key: routingKey, ref } });
218
226
  const canonicalsOf = (batch) => Effect.forEach(batch, ({ request }) => hashedPayload(request));
@@ -239,10 +247,10 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
239
247
  role === undefined ? "-" : `+${encodeURIComponent(role)}`,
240
248
  role === undefined ? "-" : `+${encodeURIComponent(tenant)}`,
241
249
  ].join(":");
242
- const admit = (batch, canonicals, view, session, begin, grouped = false) => {
250
+ const admit = (batch, canonicals, view, session, begin, grouped = false, optimistic) => {
243
251
  const cold = view.generation === undefined;
244
252
  const readsState = cold || view.state === undefined;
245
- let admissions = [];
253
+ let admissions = optimistic === undefined ? [] : [optimistic];
246
254
  let bumped;
247
255
  let stored = [];
248
256
  const subscribed = batch.some(({ request }) => request.delivery !== undefined);
@@ -278,7 +286,8 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
278
286
  Effect.map(sql `
279
287
  WITH locked AS MATERIALIZED (
280
288
  SELECT g.routing_key, g.tenant_id, g.actor_type, g.actor_id,
281
- g.generation, g.created, g.event_sequence
289
+ g.generation, g.created, g.event_sequence,
290
+ g.cold_ref, g.cold_digest, g.cold_state_version
282
291
  FROM actor_generations g
283
292
  WHERE ${rowOf({ sql, actor: { key: routingKey, ref }, alias: "g" })}
284
293
  FOR UPDATE OF g ${grouped ? sql.literal("SKIP LOCKED") : sql.literal("")}
@@ -286,7 +295,8 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
286
295
  SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::text AS now,
287
296
  g.generation::text AS generation, g.created,
288
297
  c.payload::jsonb::text AS canonical,
289
- r.caller_key, r.command, r.payload_hash, r.outcome, g.event_sequence::text AS head
298
+ r.caller_key, r.command, r.payload_hash, r.outcome, g.event_sequence::text AS head,
299
+ g.cold_ref, g.cold_digest, g.cold_state_version
290
300
  ${cursorColumns}
291
301
  FROM locked g
292
302
  CROSS JOIN ${values}
@@ -303,6 +313,7 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
303
313
  RETURNING generation::text AS generation`, (rows) => {
304
314
  bumped = rows[0]?.generation;
305
315
  }),
316
+ Effect.asVoid(sql `DELETE FROM actor_outbox WHERE ${actorRow} AND timer_key = '$cold'`),
306
317
  ]
307
318
  : []),
308
319
  ...(readsState
@@ -325,6 +336,9 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
325
336
  }
326
337
  const current = cold ? bumped : first.generation;
327
338
  const now = Number(first.now) + clock.offsetMillis();
339
+ if (first.cold_ref === null)
340
+ cache.cold = undefined;
341
+ let restored = false;
328
342
  const { retryWindowMs } = yield* OutboxRuntime;
329
343
  const expiryMarginMs = receiptMarginMs({
330
344
  keepReceiptsMs: policy.keepReceiptsMs,
@@ -342,6 +356,7 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
342
356
  let creates = false;
343
357
  let replayed = false;
344
358
  let wake = false;
359
+ let needsAdmissionClock = false;
345
360
  const broadcasts = [];
346
361
  let events = 0;
347
362
  let intents = 0;
@@ -430,6 +445,8 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
430
445
  : Effect.map(committedMintIntent(request, parent), (committed) => committed ? hash : refuse());
431
446
  };
432
447
  const admitOne = (index) => {
448
+ if (optimistic !== undefined)
449
+ return admitHashed(index, "");
433
450
  const request = batch[index].request;
434
451
  const identityNow = Number(admissions[index].now) + (request.clockOffset ?? clock.offsetMillis());
435
452
  const hashed = Effect.flatMap(hashCanonical(admissions[index].canonical), (hash) => admitHashed(index, hash));
@@ -453,6 +470,19 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
453
470
  index += 1;
454
471
  continue;
455
472
  }
473
+ if (first.cold_ref !== null)
474
+ coldStartedAtMs ??= Number(first.now);
475
+ if (first.cold_ref !== null &&
476
+ (cache.cold?.ref !== first.cold_ref ||
477
+ cache.cold.digest !== first.cold_digest ||
478
+ cache.cold.version !== first.cold_state_version))
479
+ return yield* Effect.die(new FetchCold({
480
+ pointer: {
481
+ ref: first.cold_ref,
482
+ digest: first.cold_digest,
483
+ version: first.cold_state_version,
484
+ },
485
+ }));
456
486
  const start = index;
457
487
  const members = [{ index, request, hash }];
458
488
  index += 1;
@@ -465,23 +495,35 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
465
495
  index += 1;
466
496
  }
467
497
  next ??= readsState
468
- ? new Map(stored.map(({ key, value }) => [key, decompress(value)]))
498
+ ? new Map(first.cold_ref === null
499
+ ? stored.map(({ key, value }) => [key, decompress(value)])
500
+ : cache.cold.envelope.state)
469
501
  : new Map(view.state);
470
502
  const given = next;
471
503
  const head = String(BigInt(first.head) + BigInt(events));
472
504
  const savepoint = `SAVEPOINT ${HANDLER_SAVEPOINT}`;
473
- if (statements && start > 0)
505
+ const restoring = first.cold_ref !== null && !restored;
506
+ const savepoints = statements || restoring;
507
+ if (savepoints && (start > 0 || !statements))
474
508
  yield* session.defer(savepoint);
509
+ if (restoring) {
510
+ if (tier === undefined)
511
+ return yield* Effect.die(new Error("Cold actor needs coldStorage"));
512
+ yield* tier.restore({ key: routingKey, ref }, cache.cold);
513
+ yield* hooks.at("afterColdWriteBack", request);
514
+ }
475
515
  const business = yield* hooks.at("beforeHandler", request).pipe(Effect.andThen(() => members.length === 1
476
516
  ? command.run(request, [...given], { head, connections })
477
517
  : command.merge(members.map((member) => member.request), [...given])), Effect.catchIf(SqlError.isSqlError, Effect.die), Effect.result);
478
518
  const result = Result.isSuccess(business)
479
519
  ? business.success
480
520
  : business.failure;
481
- if (statements && (start === 0 || !session.withdraw(savepoint)))
521
+ if (savepoints && ((statements && start === 0) || !session.withdraw(savepoint)))
482
522
  yield* session.defer(Result.isSuccess(business)
483
523
  ? `RELEASE SAVEPOINT ${HANDLER_SAVEPOINT}`
484
524
  : `ROLLBACK TO SAVEPOINT ${HANDLER_SAVEPOINT}`);
525
+ if (restoring && Result.isSuccess(business))
526
+ restored = true;
485
527
  const written = new Map(result.state);
486
528
  if (result.complete)
487
529
  for (const key of given.keys())
@@ -514,6 +556,10 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
514
556
  blobs.add(blob);
515
557
  intents += result.outbox.intents.length;
516
558
  jobs += result.outbox.jobs.length;
559
+ needsAdmissionClock ||=
560
+ result.outbox.intents.length > 0 ||
561
+ result.outbox.jobs.length > 0 ||
562
+ result.outbox.subscriptions.length > 0;
517
563
  if (Outcome.guards.Success(result.outcome))
518
564
  broadcasts.push(...(result.broadcasts ?? []));
519
565
  if (yield* notifyEvents(request, routingKey, result.events, waited))
@@ -563,6 +609,8 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
563
609
  settled,
564
610
  generation: current,
565
611
  state: view.state,
612
+ created,
613
+ needsAdmissionClock,
566
614
  wake: false,
567
615
  broadcasts: [],
568
616
  head: first.head,
@@ -591,7 +639,15 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
591
639
  if (creates)
592
640
  writes.push(Effect.asVoid(sql `UPDATE actor_generations SET created = true WHERE ${actorRow}`));
593
641
  if (receipts.length > 0) {
594
- writes.push(Effect.asVoid(sql `INSERT INTO actor_receipts ${sql.insert(receipts)}`));
642
+ const receipt = receipts[0];
643
+ writes.push(Effect.asVoid(optimistic === undefined
644
+ ? sql `INSERT INTO actor_receipts ${sql.insert(receipts)}`
645
+ : sql `INSERT INTO actor_receipts (routing_key, tenant_id, actor_type, actor_id,
646
+ command_id, command, payload_hash, caller_key, outcome, expires_at_ms, started_at_ms)
647
+ VALUES (${routingKey}, ${tenant}, ${actor}, ${id}, ${receipt.command_id},
648
+ ${receipt.command}, encode(sha256(convert_to(${batch[0].request.payload}::jsonb::text, 'UTF8')), 'hex'),
649
+ ${receipt.caller_key}, ${receipt.outcome}, ${receipt.expires_at_ms},
650
+ current_setting('durable.admitted_at_ms')::bigint)`));
595
651
  if (accounting !== undefined)
596
652
  writes.push(accounting.commands({
597
653
  ref,
@@ -602,9 +658,12 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
602
658
  writes.push(...(yield* ticks));
603
659
  return {
604
660
  writes,
661
+ restored,
605
662
  settled,
606
663
  generation: current,
607
- state: next,
664
+ state: first.cold_ref !== null && !restored ? undefined : next,
665
+ created,
666
+ needsAdmissionClock,
608
667
  wake,
609
668
  broadcasts,
610
669
  head: String(BigInt(first.head) + BigInt(events)),
@@ -645,9 +704,40 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
645
704
  if (plan.writes !== undefined) {
646
705
  cache.generation = plan.generation;
647
706
  cache.state = plan.state;
707
+ if (plan.restored === true)
708
+ cache.cold = undefined;
709
+ if (plan.state !== undefined) {
710
+ const previous = cache.committed;
711
+ const receipts = previous?.generation === plan.generation ? previous.receipts : new Set();
712
+ let replayBefore = previous?.generation === plan.generation
713
+ ? previous.replayBefore
714
+ : plan.startedAtMs + clock.offsetMillis();
715
+ for (const [index, settled] of plan.settled.entries())
716
+ if (Result.isSuccess(settled))
717
+ receipts.add(batch[index].request.commandId);
718
+ while (receipts.size > 1_024) {
719
+ const oldest = receipts.values().next().value;
720
+ replayBefore = Math.max(replayBefore, commandTimes(oldest).issuedAt);
721
+ receipts.delete(oldest);
722
+ }
723
+ cache.committed = {
724
+ generation: plan.generation,
725
+ state: plan.state,
726
+ created: plan.created,
727
+ head: plan.head,
728
+ now: endedAtMs,
729
+ replayBefore,
730
+ receipts,
731
+ };
732
+ }
733
+ else
734
+ cache.committed = undefined;
648
735
  }
736
+ const coldStarted = coldStartedAtMs;
737
+ coldStartedAtMs = undefined;
649
738
  answering = true;
650
- return run.committed(batch, {
739
+ return run
740
+ .committed(batch, {
651
741
  settled: plan.settled,
652
742
  broadcasts: plan.broadcasts,
653
743
  head: plan.head,
@@ -667,7 +757,15 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
667
757
  plan.outbox.some((replies) => replies.wake) ||
668
758
  plan.emitted.some((stamp) => stamp.fed),
669
759
  cancelled: plan.outbox.some((replies) => replies.cancelled),
670
- });
760
+ })
761
+ .pipe(Effect.tap(() => Effect.all([
762
+ plan.restored === true
763
+ ? count(Metrics.coldRestorations, { actor_type: actor }, 1)
764
+ : Effect.void,
765
+ coldStarted === undefined
766
+ ? Effect.void
767
+ : record(Metrics.coldWakeDuration, { actor_type: actor }, endedAtMs - coldStarted),
768
+ ], { discard: true })));
671
769
  }).pipe(Effect.andThen(Effect.sync(() => {
672
770
  answering = false;
673
771
  })));
@@ -810,8 +908,118 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
810
908
  }));
811
909
  });
812
910
  };
813
- const alone = (batch, { admission, admitted }, taken) => awaitReplies(admitted).pipe(Effect.andThen(() => admission.resume()), Effect.flatMap((plan) => Effect.flatMap(taken === undefined ? run.next : Effect.succeed(taken), (following) => commitPlan(batch, plan, following))), inTurn, bounded, Effect.filterOrElse((stepped) => stepped.ending !== "COMMIT" || stepped.tag === "COMMIT", () => Effect.die(RetryTurn.make({ message: "Turn commit rolled back" }))), Effect.tap((stepped) => (stepped.chained === undefined ? release : Effect.void)));
911
+ const alone = (batch, { admission, admitted }, taken) => awaitReplies(admitted).pipe(Effect.andThen(() => admission.resume()), Effect.flatMap((plan) => Effect.flatMap(taken === undefined ? run.next : Effect.succeed(taken), (following) => commitPlan(batch, plan, following))), inTurn, bounded, Effect.filterOrElse((stepped) => stepped.ending !== "COMMIT" || stepped.tag === "COMMIT", () => Effect.die(RetryTurn.make({ message: "Turn commit rolled back" }))), Effect.tap((stepped) => (stepped.chained === undefined ? release : Effect.void)), Effect.catchCause((cause) => {
912
+ const fetching = Cause.squash(cause);
913
+ if (!(fetching instanceof FetchCold))
914
+ return Effect.failCause(cause);
915
+ return Effect.gen(function* () {
916
+ deferred.length = 0;
917
+ const rollback = yield* control("ROLLBACK");
918
+ if (rollback.command !== "ROLLBACK")
919
+ return yield* Effect.die(RetryTurn.make({ message: "Cold admission rollback unconfirmed" }));
920
+ open = false;
921
+ yield* release;
922
+ yield* hooks.at("afterColdRollback", batch[0].request);
923
+ if (tier === undefined)
924
+ return yield* Effect.die(new Error("Cold actor needs coldStorage"));
925
+ cache.state = undefined;
926
+ cache.committed = undefined;
927
+ cache.cold = yield* tier
928
+ .fetch({ key: routingKey, ref }, fetching.pointer)
929
+ .pipe(Effect.catch((error) => Effect.die(RetryTurn.make({ message: error.message }))));
930
+ yield* hooks.at("afterColdFetch", batch[0].request);
931
+ return yield* alone(batch, yield* openAlone(batch), taken);
932
+ });
933
+ }));
814
934
  const openAlone = (batch) => leased.pipe(Effect.andThen(queue(batch, view(), []).pipe(inTurn)), Effect.map(({ admission, flight }) => ({ admission, admitted: flight })));
935
+ /**
936
+ * A speculative plan uses only an already committed snapshot. The
937
+ * guard's deliberate division by zero aborts every pipelined write on
938
+ * a miss; the receipt's unique constraint also catches a commit that
939
+ * became visible after the guard's statement snapshot. Recovery then
940
+ * uses ordinary admission, never trusting an unconfirmed outcome.
941
+ */
942
+ const optimistic = (batch, admitting, committed) => {
943
+ const { request } = batch[0];
944
+ const { issuedAt, expiresAt } = commandTimes(request.commandId);
945
+ const identityOffset = request.clockOffset ?? clock.offsetMillis();
946
+ const ordinary = () => Effect.flatMap(openAlone(batch), (opened) => alone(batch, opened));
947
+ return admitting.admission.resume().pipe(Effect.catchDefect((defect) => Schema.is(RetryTurn)(defect) || SqlError.isSqlError(defect)
948
+ ? Effect.die(defect)
949
+ : Effect.void), Effect.flatMap((plan) => {
950
+ if (plan === undefined || plan.writes === undefined || plan.needsAdmissionClock)
951
+ return ordinary();
952
+ let missed = false;
953
+ let tag = "";
954
+ let version = "";
955
+ let endedAtMs = 0;
956
+ let startedAtMs = 0;
957
+ const guard = sql `
958
+ WITH locked AS MATERIALIZED (
959
+ SELECT g.generation, g.created, g.event_sequence,
960
+ to_jsonb(g)->>'cold_ref' AS cold_ref
961
+ FROM actor_generations g
962
+ WHERE ${rowOf({ sql, actor: { key: routingKey, ref }, alias: "g" })}
963
+ FOR UPDATE OF g
964
+ ), checked AS MATERIALIZED (
965
+ SELECT *, floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint AS now
966
+ FROM locked
967
+ )
968
+ SELECT 1 / CASE WHEN EXISTS (
969
+ SELECT 1 FROM checked g
970
+ WHERE g.generation = ${committed.generation} AND g.created = ${committed.created}
971
+ AND g.event_sequence = ${committed.head}
972
+ AND g.cold_ref IS NULL
973
+ AND NOT EXISTS (SELECT 1 FROM actor_receipts WHERE ${actorRow}
974
+ AND command_id = ${request.commandId})
975
+ AND (${request.external !== true} OR
976
+ (g.now + ${identityOffset} >= ${issuedAt} AND g.now + ${identityOffset} < ${expiresAt}))
977
+ ) THEN 1 ELSE 0 END AS accepted,
978
+ (SELECT set_config('durable.admitted_at_ms', now::text, true) FROM checked) AS now`;
979
+ const flight = [
980
+ begin,
981
+ Effect.asVoid(sql `SELECT ${timeouts}`),
982
+ Effect.map(guard, (rows) => {
983
+ startedAtMs = Number(rows[0].now);
984
+ if (request.external === true)
985
+ batch[0].admitted = true;
986
+ }).pipe(Effect.tapError((error) => Effect.sync(() => {
987
+ missed = Schema.is(Schema.Struct({ code: Schema.Literal("22012") }))(error.reason.cause);
988
+ }))),
989
+ ...plan.writes,
990
+ Effect.map(control("COMMIT"), (result) => {
991
+ tag = result.command;
992
+ open = false;
993
+ }),
994
+ Effect.map(session().query(COMMIT_VERSION, [], true), (result) => {
995
+ const ended = result.rows[0];
996
+ version = ended.version;
997
+ endedAtMs = Number(ended.now);
998
+ }),
999
+ ];
1000
+ return sendPipelined(flight).pipe(Effect.withSpan(SpanNames.commit), Effect.as(true), Effect.catch((error) => {
1001
+ const duplicate = Schema.is(Schema.Struct({
1002
+ code: Schema.Literal("23505"),
1003
+ constraint: Schema.Literal("actor_receipts_pkey"),
1004
+ }))(error.reason.cause);
1005
+ return (missed || duplicate) && tag === "ROLLBACK"
1006
+ ? Effect.succeed(false)
1007
+ : Effect.fail(error);
1008
+ }), Effect.flatMap((accepted) => {
1009
+ if (!accepted) {
1010
+ forget(cache);
1011
+ return ordinary();
1012
+ }
1013
+ if (tag !== "COMMIT")
1014
+ return Effect.die(RetryTurn.make({ message: "Warm turn commit rolled back" }));
1015
+ return Effect.as(release, {
1016
+ plan: { ...plan, startedAtMs },
1017
+ version,
1018
+ endedAtMs,
1019
+ });
1020
+ }));
1021
+ }), inTurn, bounded);
1022
+ };
815
1023
  const onMember = (member) => (effect) => Effect.suspend(() => Effect.provideService(effect, sql.transactionService, [
816
1024
  asSqlConnection({ connection: member.connection(), send: member.send }),
817
1025
  0,
@@ -906,17 +1114,60 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
906
1114
  };
907
1115
  }).pipe(Effect.catchDefect((defect) => defect instanceof Unseated ? Effect.undefined : Effect.die(defect)), Effect.onExit(() => member.leave(false)), bounded, Effect.map((ended) => ({ ended, following })));
908
1116
  };
909
- const transact = (batch, admitting) => admitting.member === undefined
910
- ? alone(batch, admitting)
911
- : Effect.flatMap(grouped(batch, admitting.admission, admitting.member), (outcome) => outcome.ended !== undefined
912
- ? Effect.succeed(outcome.ended)
913
- : Effect.flatMap(openAlone(batch), (opened) => alone(batch, opened, outcome.following)));
1117
+ const transact = (batch, admitting) => admitting.optimistic !== undefined
1118
+ ? optimistic(batch, admitting, admitting.optimistic)
1119
+ : admitting.member === undefined
1120
+ ? alone(batch, admitting)
1121
+ : Effect.flatMap(grouped(batch, admitting.admission, admitting.member), (outcome) => outcome.ended !== undefined
1122
+ ? Effect.succeed(outcome.ended)
1123
+ : Effect.flatMap(openAlone(batch), (opened) => alone(batch, opened, outcome.following)));
914
1124
  let first = true;
915
1125
  return yield* drive((batch) => {
916
- const joining = first && view().generation !== undefined && view().state !== undefined
1126
+ const snapshot = cache.committed;
1127
+ const { request, command } = batch[0];
1128
+ const fast = warmTurns &&
1129
+ batch.length === 1 &&
1130
+ cache.cold === undefined &&
1131
+ snapshot !== undefined &&
1132
+ snapshot.generation === cache.generation &&
1133
+ snapshot.state === cache.state &&
1134
+ !statements &&
1135
+ waited.size === 0 &&
1136
+ connections === undefined &&
1137
+ !run.publishesUnderLock(batch) &&
1138
+ !command.handler &&
1139
+ (!command.internal || isSystem(request.caller)) &&
1140
+ request.delivery === undefined &&
1141
+ request.redelivered !== true &&
1142
+ commandTimes(request.commandId).issuedAt > snapshot.replayBefore &&
1143
+ !snapshot.receipts.has(request.commandId) &&
1144
+ (policy.createdBy === undefined || snapshot.created);
1145
+ const joining = first &&
1146
+ cache.cold === undefined &&
1147
+ view().generation !== undefined &&
1148
+ view().state !== undefined
917
1149
  ? groups
918
1150
  : undefined;
919
1151
  first = false;
1152
+ if (fast)
1153
+ return leased.pipe(Effect.map(() => ({
1154
+ admission: admit(batch, [], view(), pipeline, [], false, {
1155
+ now: String(snapshot.now),
1156
+ generation: snapshot.generation,
1157
+ created: snapshot.created,
1158
+ cold_ref: null,
1159
+ cold_digest: null,
1160
+ cold_state_version: null,
1161
+ canonical: request.payload,
1162
+ caller_key: null,
1163
+ command: null,
1164
+ payload_hash: null,
1165
+ outcome: null,
1166
+ head: snapshot.head,
1167
+ }),
1168
+ admitted: [],
1169
+ optimistic: snapshot,
1170
+ })));
920
1171
  return joining === undefined ? openAlone(batch) : openGrouped(batch, joining);
921
1172
  }, transact).pipe(Effect.onExit((exit) => {
922
1173
  if (Exit.isSuccess(exit) || !open)
@@ -960,6 +1211,7 @@ export const executeBatches = Effect.fnUntraced(function* (run, cache, routingKe
960
1211
  : Option.getOrUndefined(yield* Effect.serviceOption(TurnGroups));
961
1212
  const exit = yield* (Option.isSome(turns) ? pipelined(turns.value) : sequential).pipe(Effect.onError(() => Effect.sync(() => {
962
1213
  cache.state = undefined;
1214
+ cache.committed = undefined;
963
1215
  })), Effect.exit);
964
1216
  if (Exit.isSuccess(exit))
965
1217
  return undefined;
@@ -4,7 +4,7 @@ import type { Request } from "../request.ts";
4
4
  * A named moment in command delivery, turn execution, and the outbox and
5
5
  * subscription relays where a test can pause, fail, or crash the runtime.
6
6
  */
7
- export type TurnPoint = "beforeDelivery" | "queued" | "beforeHandler" | "beforeCommit" | "afterCommit" | "beforeFlush" | "afterClaim" | "beforeOutboxDelete" | "beforeExecute" | "afterExecute" | "beforeRenew" | "beforeDeadLetterCommit" | "beforeSettle" | "afterSettleSnapshot" | "afterExpand" | "beforeWorkflowSuspend";
7
+ export type TurnPoint = "beforeDelivery" | "queued" | "beforeHandler" | "afterColdRollback" | "afterColdFetch" | "afterColdWriteBack" | "beforeCommit" | "afterCommit" | "beforeFlush" | "afterClaim" | "beforeOutboxDelete" | "beforeExecute" | "afterExecute" | "beforeRenew" | "beforeDeadLetterCommit" | "beforeSettle" | "afterSettleSnapshot" | "afterExpand" | "beforeWorkflowSuspend";
8
8
  declare const RetryTurn_base: Schema.Class<RetryTurn, Schema.TaggedStruct<"RetryTurn", {
9
9
  readonly message: Schema.String;
10
10
  }>, import("effect/Cause").YieldableError>;
@@ -53,7 +53,7 @@ export declare const ControlPayload: Schema.fromJsonString<Schema.Struct<{
53
53
  export declare const CallerJson: Schema.fromJsonString<Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"User", {
54
54
  readonly subject: Schema.NonEmptyString;
55
55
  }>, Schema.TaggedStruct<"Anonymous", {}>, Schema.TaggedStruct<"System", {
56
- readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription"]>;
56
+ readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription", "cold"]>;
57
57
  readonly ref: Schema.optional<Schema.Struct<{
58
58
  readonly tenant: Schema.NonEmptyString;
59
59
  readonly actor: Schema.NonEmptyString;
@@ -28,7 +28,7 @@ export declare const checkReceipt: (request: {
28
28
  readonly _tag: "Anonymous";
29
29
  } | {
30
30
  readonly _tag: "System";
31
- readonly source: "actor" | "cron" | "job" | "process" | "subscription" | "timer" | "workflow";
31
+ readonly source: "actor" | "cold" | "cron" | "job" | "process" | "subscription" | "timer" | "workflow";
32
32
  readonly ref?: {
33
33
  readonly tenant: string;
34
34
  readonly actor: string;
@@ -44,7 +44,7 @@ interface LocalExecutor {
44
44
  * subscription work in `work`.
45
45
  */
46
46
  interface ClaimedRow extends ClaimedJob {
47
- readonly kind: "intent" | "job" | "skipped-intent" | "skipped-job" | "work";
47
+ readonly kind: "intent" | "job" | "cold" | "skipped-intent" | "skipped-job" | "skipped-cold" | "work";
48
48
  readonly target_type: string;
49
49
  readonly target_id: string;
50
50
  readonly timer_key: string | null;
@@ -62,7 +62,7 @@ interface ClaimedRow extends ClaimedJob {
62
62
  */
63
63
  export declare const candidates: ({ sql, kind, now, limit, only, range, }: {
64
64
  readonly sql: SqlClient.SqlClient;
65
- readonly kind: "intent" | "job" | "feed" | "control";
65
+ readonly kind: "intent" | "job" | "feed" | "control" | "cold";
66
66
  readonly now: Statement.Fragment;
67
67
  readonly limit: number;
68
68
  readonly only?: Statement.Fragment;