@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
@@ -344,6 +344,255 @@ recorder) {
344
344
  * batch a retryable failure aborted, each to run alone once.
345
345
  */
346
346
  const memories = new WeakMap();
347
+ const policy = registration.policy;
348
+ const statements = registration.tables.length > 0 || registration.blobs.length > 0;
349
+ const start = Effect.fnUntraced(function* (worker) {
350
+ const scope = yield* Scope.fork(worker.handler);
351
+ if (lease !== undefined)
352
+ yield* lease.holds(worker.shard).pipe(Effect.repeat({ schedule: Schedule.spaced(lease.interval), until: (held) => !held }), Effect.andThen(Effect.sync(() => {
353
+ worker.refused = leaseLostDefect;
354
+ })), Effect.andThen(Effect.forkDetach(Scope.close(scope, Exit.void))), Effect.forkIn(scope));
355
+ const owned = yield* ownedOf(worker.entityId).pipe(Scope.provide(scope));
356
+ const activated = yield* registration
357
+ .activate(ActorRef.make({ tenant: worker.tenant, actor: registration.name, id: worker.id }))
358
+ .pipe(Scope.provide(scope), Effect.exit);
359
+ if (Exit.isFailure(activated))
360
+ yield* Effect.logError("Actor activation failed", activated.cause).pipe(Effect.annotateLogs({ actor: registration.name, id: worker.id, tenant: worker.tenant }));
361
+ const started = { scope, owned, activated, engine: undefined };
362
+ return started;
363
+ });
364
+ const restart = (worker, batch, cause) => Effect.sync(() => {
365
+ worker.refused ??= restartIncomplete;
366
+ }).pipe(Effect.andThen(Effect.forEach(batch, (entry) => entry.request.external !== true
367
+ ? Deferred.failCause(entry.reply, Cause.die(Cause.squash(cause)))
368
+ : Deferred.fail(entry.reply, DeliveryFailed.make({
369
+ error: ActorError.make({
370
+ reason: ActorUnavailable.make({ cause: Cause.squash(cause) }),
371
+ }),
372
+ admitted: entry.admitted === true,
373
+ })), { discard: true })), Effect.andThen(Effect.interrupt));
374
+ const restartActivation = (worker, batch, orphan, cause) => Effect.gen(function* () {
375
+ const failures = worker.memory.restarts ?? 0;
376
+ worker.phase = "restarting";
377
+ worker.memory.restarts = failures + 1;
378
+ yield* Scope.close(worker.current.scope, Exit.void);
379
+ yield* Effect.sleep(restartDelay(failures));
380
+ if (lease !== undefined && !(yield* lease.holds(worker.shard))) {
381
+ worker.refused = leaseLostDefect;
382
+ return yield* Effect.die(leaseLostDefect);
383
+ }
384
+ worker.current = yield* start(worker).pipe(Effect.provideContext(worker.built));
385
+ worker.mailbox.requeue(orphan);
386
+ const unavailable = ActorError.make({
387
+ reason: ActorUnavailable.make({ cause: Cause.squash(cause) }),
388
+ });
389
+ yield* Effect.forEach(batch, (entry) => entry.request.external !== true
390
+ ? Deferred.fail(entry.reply, unavailable)
391
+ : Deferred.fail(entry.reply, DeliveryFailed.make({ error: unavailable, admitted: entry.admitted === true })), { discard: true });
392
+ }).pipe(Effect.uninterruptible, Effect.catchCause((failed) => restart(worker, [...batch, ...orphan], failed)));
393
+ const resolve = (worker, batch) => {
394
+ const { activated } = worker.current;
395
+ if (Exit.isSuccess(activated))
396
+ for (const entry of batch)
397
+ entry.command =
398
+ activated.value.get(entry.request.command) ??
399
+ workflowRoutes.get(entry.request.command) ??
400
+ entry.command;
401
+ return batch;
402
+ };
403
+ const following = Effect.fnUntraced(function* (worker) {
404
+ if (worker.refused !== undefined ||
405
+ !gate.open ||
406
+ Exit.isFailure(worker.current.activated) ||
407
+ Exit.isFailure(yield* Effect.exit(writable)))
408
+ return undefined;
409
+ const batch = resolve(worker, worker.mailbox.take());
410
+ worker.taken.push(...batch);
411
+ return batch.length > 0 ? batch : undefined;
412
+ });
413
+ /**
414
+ * The one owner of an ended batch's publication, in this order: wake the
415
+ * relay and running job attempts for obligations the commit made due or
416
+ * cancelled; record the batch's outcomes; end the transient progress of
417
+ * commands and jobs the batch settled; send its broadcasts, feed frames
418
+ * and watch updates to connections; then, per command in delivery order,
419
+ * run its `afterCommit` point, kick the workflow execution it started or
420
+ * resumed, and answer its caller. These steps follow the commit but are
421
+ * not atomic with it or with each other. A failure part-way stops the run
422
+ * as committed, so every command left unanswered is restarted and
423
+ * resolves through its receipt; a lost broadcast is not replayed.
424
+ */
425
+ const publish = Effect.fnUntraced(function* (worker, batch, done) {
426
+ const { owned } = worker.current;
427
+ const outbox = yield* OutboxRuntime;
428
+ if (done.wake)
429
+ yield* outbox.wake;
430
+ if (done.cancelled)
431
+ yield* outbox.cancelled;
432
+ const labels = done.settled.map((settled, index) => Result.isSuccess(settled) ? outcomeOf(settled.success, done.replays.has(index)) : "rejected");
433
+ worker.labelled.set(batch, labels);
434
+ yield* Effect.annotateCurrentSpan({ "actor.generation": done.generation });
435
+ if (batch.length === 1 && Result.isSuccess(done.settled[0]))
436
+ yield* Effect.annotateCurrentSpan({
437
+ "turn.replayed": done.replays.has(0),
438
+ "turn.outcome": labels[0],
439
+ });
440
+ yield* countWritten(done);
441
+ const live = recorder();
442
+ if (live !== undefined && done.written.receipts > 0)
443
+ recordLive(live, batch, done, yield* Clock.currentTimeMillis);
444
+ if (owner.hasProgress) {
445
+ for (const [index, settled] of done.settled.entries())
446
+ if (Result.isSuccess(settled) && !Outcome.guards.Defect(settled.success))
447
+ yield* owner.closeProgress(owned, batch[index].request.commandId);
448
+ for (const jobId of done.cancelledJobs)
449
+ yield* owner.closeProgress(owned, jobId);
450
+ }
451
+ if (owner.hasConnections || owner.hasStreams) {
452
+ for (const { request } of batch)
453
+ yield* (yield* TurnHooks).at("beforeFlush", request);
454
+ const feeds = yield* Effect.forEach(done.committed, owner.feedBroadcasts);
455
+ const watches = yield* owner.watchBroadcasts(owned, done.wrote, done.version);
456
+ yield* owner.flush(owned, [...done.broadcasts, ...feeds.flat(), ...watches], done.head);
457
+ }
458
+ for (const [index, settled] of done.settled.entries()) {
459
+ const entry = batch[index];
460
+ if (Result.isFailure(settled)) {
461
+ yield* Deferred.fail(entry.reply, settled.failure);
462
+ continue;
463
+ }
464
+ const outcome = settled.success;
465
+ worker.memory.restarts = 0;
466
+ if (!Outcome.guards.Defect(outcome)) {
467
+ yield* (yield* TurnHooks).at("afterCommit", entry.request);
468
+ const kicked = workflowRoutes.has(entry.request.command)
469
+ ? yield* kickedExecution({ request: entry.request, outcome })
470
+ : undefined;
471
+ if (kicked !== undefined) {
472
+ worker.phase = "workflow kick";
473
+ worker.current.engine ??= yield* activationEngine({
474
+ registration,
475
+ ref: entry.request.ref,
476
+ routingKey: routingKeyOf(entry.request.ref),
477
+ cache: worker.current.owned.cache,
478
+ scope: worker.current.scope,
479
+ deliveryMs: policy.deliveryMs,
480
+ });
481
+ yield* worker.current.engine.kick(kicked.executionId, kicked.interrupt);
482
+ worker.phase = "turn";
483
+ }
484
+ }
485
+ const executed = done.replays.has(index)
486
+ ? { outcome, version: done.version, endedAtMs: done.endedAtMs, replayed: true }
487
+ : { outcome, version: done.version, endedAtMs: done.endedAtMs };
488
+ yield* Deferred.succeed(entry.reply, executed);
489
+ }
490
+ });
491
+ /** Records a finished batch's turn outcomes and duration, or the defect that ended it. */
492
+ const observed = Effect.fnUntraced(function* (worker, batch, started, exit) {
493
+ const { request } = batch[0];
494
+ const lone = batch.length === 1;
495
+ const labels = worker.labelled.get(batch);
496
+ const elapsed = (yield* Clock.currentTimeMillis) - started;
497
+ if (labels === undefined && Exit.isFailure(exit)) {
498
+ if (Cause.hasInterruptsOnly(exit.cause))
499
+ return;
500
+ if (!retryable(exit.cause) && !lone)
501
+ return;
502
+ const deterministic = !retryable(exit.cause);
503
+ if (deterministic)
504
+ yield* recordDefect(request, exit.cause);
505
+ yield* count(Metrics.turns, { ...typeAttributes, outcome: deterministic ? "defect" : "retried" }, batch.length);
506
+ yield* record(Metrics.turnDuration, typeAttributes, elapsed);
507
+ return;
508
+ }
509
+ for (const label of labels ?? [])
510
+ yield* count(Metrics.turns, { ...typeAttributes, outcome: label }, 1);
511
+ yield* record(Metrics.turnDuration, typeAttributes, elapsed);
512
+ });
513
+ const observe = (worker, batch) => (effect) => Effect.flatMap(Clock.currentTimeMillis, (started) => Effect.forEach(batch, ({ request: queued }) => queued.queuedAtMs === undefined
514
+ ? Effect.void
515
+ : record(Metrics.mailboxAge, typeAttributes, Math.max(0, started - queued.queuedAtMs)), { discard: true }).pipe(Effect.andThen(effect), Effect.onExit((exit) => observed(worker, batch, started, exit)), withinTurnSpan(batch)));
516
+ const run = (worker, batch, pipelining) => {
517
+ const { owned } = worker.current;
518
+ return executeBatches({
519
+ first: batch,
520
+ next: pipelining ? following(worker) : Effect.undefined,
521
+ prepare: owner.prepare(owned),
522
+ committed: (batch, done) => publish(worker, batch, done),
523
+ publishesUnderLock: (batch) => batch.some(({ request }) => workflowRoutes.has(request.command)),
524
+ observe: (batch) => observe(worker, batch),
525
+ }, owned.cache, owned.key, policy, registration.mintable, parentPlacement(registration.placement)?.parent, statements, waited, owner.hasConnections ? owner.list(owned) : undefined, registration.cron, accounting);
526
+ };
527
+ const recover = Effect.fnUntraced(function* (worker, { batch, orphan, cause, committed, poolRefused }) {
528
+ if (poolRefused === true) {
529
+ worker.mailbox.requeue(orphan ?? []);
530
+ yield* Effect.forEach(batch, (entry) => Deferred.fail(entry.reply, overloaded("runner")), {
531
+ discard: true,
532
+ });
533
+ return;
534
+ }
535
+ if (committed || retryable(cause)) {
536
+ if (!committed && batch.length > 1)
537
+ worker.mailbox.isolate(batch.map(({ request }) => request.commandId));
538
+ if (committed || worker.refused !== undefined)
539
+ return yield* restart(worker, [...batch, ...(orphan ?? [])], cause);
540
+ return yield* restartActivation(worker, batch, orphan ?? [], cause);
541
+ }
542
+ worker.mailbox.requeue(orphan ?? []);
543
+ if (batch.length === 1) {
544
+ const { request } = batch[0];
545
+ const defect = Cause.squash(cause);
546
+ yield* Effect.logError("Deterministic actor defect", Cause.die(defect)).pipe(Effect.annotateLogs({
547
+ actor: request.ref.actor,
548
+ id: request.ref.id,
549
+ tenant: request.ref.tenant,
550
+ command: request.command,
551
+ commandId: request.commandId,
552
+ }));
553
+ worker.memory.restarts = 0;
554
+ yield* Deferred.succeed(batch[0].reply, {
555
+ outcome: Outcome.cases.Defect.make({ cause: defect }),
556
+ });
557
+ return;
558
+ }
559
+ yield* Effect.logDebug("Turn batch failed; running its commands one at a time", cause);
560
+ for (const entry of batch) {
561
+ const stopped = yield* run(worker, resolve(worker, [entry]), false);
562
+ if (stopped !== undefined)
563
+ yield* recover(worker, stopped);
564
+ }
565
+ }, Effect.provideContext(services));
566
+ const turnBatch = Effect.fnUntraced(function* (worker, batch) {
567
+ if (worker.refused !== undefined)
568
+ return yield* restart(worker, batch, Cause.die(worker.refused));
569
+ yield* writable;
570
+ const stopped = yield* run(worker, batch, true);
571
+ if (stopped !== undefined)
572
+ yield* recover(worker, stopped);
573
+ });
574
+ const work = Effect.fnUntraced(function* (worker) {
575
+ while (true) {
576
+ worker.phase = "idle";
577
+ worker.taken = [];
578
+ yield* worker.mailbox.await;
579
+ const batch = resolve(worker, worker.mailbox.take());
580
+ if (batch.length === 0)
581
+ continue;
582
+ worker.phase = "turn";
583
+ worker.taken = [...batch];
584
+ if (Exit.isFailure(worker.current.activated)) {
585
+ const { cause } = worker.current.activated;
586
+ yield* Effect.forEach(batch, (entry) => Deferred.succeed(entry.reply, {
587
+ outcome: Outcome.cases.Defect.make({ cause: Cause.squash(cause) }),
588
+ }), { discard: true });
589
+ continue;
590
+ }
591
+ yield* gate.run(turnBatch(worker, batch)).pipe(Effect.catchIf(Schema.is(ActorError), (error) => Effect.forEach(worker.taken, (entry) => Deferred.fail(entry.reply, error), {
592
+ discard: true,
593
+ })), Effect.provideContext(batch[0].context), Effect.catchCauseIf((cause) => !Cause.hasInterruptsOnly(cause), (cause) => restart(worker, batch, cause)));
594
+ }
595
+ }, Effect.provideContext(services));
347
596
  const register = sharding.registerEntity(entity, Effect.gen(function* () {
348
597
  const activation = yield* entityScope;
349
598
  const memory = memories.get(activation) ?? {
@@ -367,285 +616,45 @@ recorder) {
367
616
  : ShardId.toString(yield* entity.getShardId(ClusterEntityId.make(entityId)));
368
617
  if (lease !== undefined && !(yield* lease.holds(shard)))
369
618
  return yield* leaseLost;
370
- /**
371
- * Why this handler refuses all work: its singleton lease is lost, or an
372
- * in-place restart failed and the worker ended. Set once, never cleared;
373
- * Cluster builds a new handler for the next delivery.
374
- */
375
- let refused;
376
619
  const handler = yield* Scope.fork(activation);
377
620
  memory.handler = handler;
378
621
  yield* residentWhile(entityId, tenant, id).pipe(Scope.provide(handler));
379
- const start = Effect.gen(function* () {
380
- const scope = yield* Scope.fork(handler);
381
- if (lease !== undefined)
382
- yield* lease.holds(shard).pipe(Effect.repeat({ schedule: Schedule.spaced(lease.interval), until: (held) => !held }), Effect.andThen(Effect.sync(() => {
383
- refused = leaseLostDefect;
384
- })), Effect.andThen(Effect.forkDetach(Scope.close(scope, Exit.void))), Effect.forkIn(scope));
385
- const owned = yield* ownedOf(entityId).pipe(Scope.provide(scope));
386
- const activated = yield* registration
387
- .activate(ActorRef.make({ tenant, actor: registration.name, id }))
388
- .pipe(Scope.provide(scope), Effect.exit);
389
- if (Exit.isFailure(activated))
390
- yield* Effect.logError("Actor activation failed", activated.cause).pipe(Effect.annotateLogs({ actor: registration.name, id, tenant }));
391
- const started = { scope, owned, activated, engine: undefined };
392
- return started;
393
- });
394
- const built = yield* Effect.context();
395
- let current = yield* start;
396
- yield* Effect.addFinalizer(() => Scope.close(handler, Exit.void));
397
- const policy = registration.policy;
398
- const statements = registration.tables.length > 0 || registration.blobs.length > 0;
399
- const mailbox = activationMailbox(memory.alone);
400
- let phase = "idle";
401
- const restart = (batch, cause) => Effect.sync(() => {
402
- refused ??= restartIncomplete;
403
- }).pipe(Effect.andThen(Effect.forEach(batch, (entry) => entry.request.external !== true
404
- ? Deferred.failCause(entry.reply, Cause.die(Cause.squash(cause)))
405
- : Deferred.fail(entry.reply, DeliveryFailed.make({
406
- error: ActorError.make({
407
- reason: ActorUnavailable.make({ cause: Cause.squash(cause) }),
408
- }),
409
- admitted: entry.admitted === true,
410
- })), { discard: true })), Effect.andThen(Effect.interrupt));
411
- const restartActivation = (batch, orphan, cause) => Effect.gen(function* () {
412
- const failures = memory.restarts ?? 0;
413
- phase = "restarting";
414
- memory.restarts = failures + 1;
415
- yield* Scope.close(current.scope, Exit.void);
416
- yield* Effect.sleep(restartDelay(failures));
417
- if (lease !== undefined && !(yield* lease.holds(shard))) {
418
- refused = leaseLostDefect;
419
- return yield* Effect.die(leaseLostDefect);
420
- }
421
- current = yield* start.pipe(Effect.provideContext(built));
422
- mailbox.requeue(orphan);
423
- const unavailable = ActorError.make({
424
- reason: ActorUnavailable.make({ cause: Cause.squash(cause) }),
425
- });
426
- yield* Effect.forEach(batch, (entry) => entry.request.external !== true
427
- ? Deferred.fail(entry.reply, unavailable)
428
- : Deferred.fail(entry.reply, DeliveryFailed.make({ error: unavailable, admitted: entry.admitted === true })), { discard: true });
429
- }).pipe(Effect.uninterruptible, Effect.catchCause((failed) => restart([...batch, ...orphan], failed)));
430
- const resolve = (batch) => {
431
- const { activated } = current;
432
- if (Exit.isSuccess(activated))
433
- for (const entry of batch)
434
- entry.command =
435
- activated.value.get(entry.request.command) ??
436
- workflowRoutes.get(entry.request.command) ??
437
- entry.command;
438
- return batch;
622
+ const starting = {
623
+ memory,
624
+ entityId,
625
+ tenant,
626
+ id,
627
+ shard,
628
+ handler,
629
+ built: yield* Effect.context(),
630
+ refused: undefined,
439
631
  };
440
- let taken = [];
441
- const following = Effect.gen(function* () {
442
- if (refused !== undefined ||
443
- !gate.open ||
444
- Exit.isFailure(current.activated) ||
445
- Exit.isFailure(yield* Effect.exit(writable)))
446
- return undefined;
447
- const batch = resolve(mailbox.take());
448
- taken.push(...batch);
449
- return batch.length > 0 ? batch : undefined;
450
- });
451
- const labelled = new WeakMap();
452
- /**
453
- * The one owner of an ended batch's publication, in this order: wake the
454
- * relay and running job attempts for obligations the commit made due or
455
- * cancelled; record the batch's outcomes; end the transient progress of
456
- * commands and jobs the batch settled; send its broadcasts, feed frames
457
- * and watch updates to connections; then, per command in delivery order,
458
- * run its `afterCommit` point, kick the workflow execution it started or
459
- * resumed, and answer its caller. These steps follow the commit but are
460
- * not atomic with it or with each other. A failure part-way stops the run
461
- * as committed, so every command left unanswered is restarted and
462
- * resolves through its receipt; a lost broadcast is not replayed.
463
- */
464
- const publish = Effect.fnUntraced(function* (batch, done) {
465
- const { owned } = current;
466
- const outbox = yield* OutboxRuntime;
467
- if (done.wake)
468
- yield* outbox.wake;
469
- if (done.cancelled)
470
- yield* outbox.cancelled;
471
- const labels = done.settled.map((settled, index) => Result.isSuccess(settled)
472
- ? outcomeOf(settled.success, done.replays.has(index))
473
- : "rejected");
474
- labelled.set(batch, labels);
475
- yield* Effect.annotateCurrentSpan({ "actor.generation": done.generation });
476
- if (batch.length === 1 && Result.isSuccess(done.settled[0]))
477
- yield* Effect.annotateCurrentSpan({
478
- "turn.replayed": done.replays.has(0),
479
- "turn.outcome": labels[0],
480
- });
481
- yield* countWritten(done);
482
- const live = recorder();
483
- if (live !== undefined && done.written.receipts > 0)
484
- recordLive(live, batch, done, yield* Clock.currentTimeMillis);
485
- if (owner.hasProgress) {
486
- for (const [index, settled] of done.settled.entries())
487
- if (Result.isSuccess(settled) && !Outcome.guards.Defect(settled.success))
488
- yield* owner.closeProgress(owned, batch[index].request.commandId);
489
- for (const jobId of done.cancelledJobs)
490
- yield* owner.closeProgress(owned, jobId);
491
- }
492
- if (owner.hasConnections || owner.hasStreams) {
493
- for (const { request } of batch)
494
- yield* (yield* TurnHooks).at("beforeFlush", request);
495
- const feeds = yield* Effect.forEach(done.committed, owner.feedBroadcasts);
496
- const watches = yield* owner.watchBroadcasts(owned, done.wrote, done.version);
497
- yield* owner.flush(owned, [...done.broadcasts, ...feeds.flat(), ...watches], done.head);
498
- }
499
- for (const [index, settled] of done.settled.entries()) {
500
- const entry = batch[index];
501
- if (Result.isFailure(settled)) {
502
- yield* Deferred.fail(entry.reply, settled.failure);
503
- continue;
504
- }
505
- const outcome = settled.success;
506
- memory.restarts = 0;
507
- if (!Outcome.guards.Defect(outcome)) {
508
- yield* (yield* TurnHooks).at("afterCommit", entry.request);
509
- const kicked = workflowRoutes.has(entry.request.command)
510
- ? yield* kickedExecution({ request: entry.request, outcome })
511
- : undefined;
512
- if (kicked !== undefined) {
513
- phase = "workflow kick";
514
- current.engine ??= yield* activationEngine({
515
- registration,
516
- ref: entry.request.ref,
517
- routingKey: routingKeyOf(entry.request.ref),
518
- cache: current.owned.cache,
519
- scope: current.scope,
520
- deliveryMs: policy.deliveryMs,
521
- });
522
- yield* current.engine.kick(kicked.executionId, kicked.interrupt);
523
- phase = "turn";
524
- }
525
- }
526
- const executed = done.replays.has(index)
527
- ? { outcome, version: done.version, endedAtMs: done.endedAtMs, replayed: true }
528
- : { outcome, version: done.version, endedAtMs: done.endedAtMs };
529
- yield* Deferred.succeed(entry.reply, executed);
530
- }
632
+ const worker = Object.assign(starting, {
633
+ current: yield* start(starting),
634
+ mailbox: activationMailbox(memory.alone),
635
+ phase: "idle",
636
+ taken: [],
637
+ labelled: new WeakMap(),
531
638
  });
532
- /** Records a finished batch's turn outcomes and duration, or the defect that ended it. */
533
- const observed = Effect.fnUntraced(function* (batch, started, exit) {
534
- const { request } = batch[0];
535
- const lone = batch.length === 1;
536
- const labels = labelled.get(batch);
537
- const elapsed = (yield* Clock.currentTimeMillis) - started;
538
- if (labels === undefined && Exit.isFailure(exit)) {
539
- if (Cause.hasInterruptsOnly(exit.cause))
540
- return;
541
- if (!retryable(exit.cause) && !lone)
542
- return;
543
- const deterministic = !retryable(exit.cause);
544
- if (deterministic)
545
- yield* recordDefect(request, exit.cause);
546
- yield* count(Metrics.turns, { ...typeAttributes, outcome: deterministic ? "defect" : "retried" }, batch.length);
547
- yield* record(Metrics.turnDuration, typeAttributes, elapsed);
548
- return;
549
- }
550
- for (const label of labels ?? [])
551
- yield* count(Metrics.turns, { ...typeAttributes, outcome: label }, 1);
552
- yield* record(Metrics.turnDuration, typeAttributes, elapsed);
553
- });
554
- const observe = (batch) => (effect) => Effect.flatMap(Clock.currentTimeMillis, (started) => Effect.forEach(batch, ({ request: queued }) => queued.queuedAtMs === undefined
555
- ? Effect.void
556
- : record(Metrics.mailboxAge, typeAttributes, Math.max(0, started - queued.queuedAtMs)), { discard: true }).pipe(Effect.andThen(effect), Effect.onExit((exit) => observed(batch, started, exit)), withinTurnSpan(batch)));
557
- const run = (batch, pipelining) => {
558
- const { owned } = current;
559
- return executeBatches({
560
- first: batch,
561
- next: pipelining ? following : Effect.undefined,
562
- prepare: owner.prepare(owned),
563
- committed: publish,
564
- publishesUnderLock: (batch) => batch.some(({ request }) => workflowRoutes.has(request.command)),
565
- observe,
566
- }, owned.cache, owned.key, policy, registration.mintable, parentPlacement(registration.placement)?.parent, statements, waited, owner.hasConnections ? owner.list(owned) : undefined, registration.cron, accounting);
567
- };
568
- const recover = Effect.fnUntraced(function* ({ batch, orphan, cause, committed, poolRefused }) {
569
- if (poolRefused === true) {
570
- mailbox.requeue(orphan ?? []);
571
- yield* Effect.forEach(batch, (entry) => Deferred.fail(entry.reply, overloaded("runner")), {
572
- discard: true,
573
- });
574
- return;
575
- }
576
- if (committed || retryable(cause)) {
577
- if (!committed && batch.length > 1)
578
- mailbox.isolate(batch.map(({ request }) => request.commandId));
579
- if (committed || refused !== undefined)
580
- return yield* restart([...batch, ...(orphan ?? [])], cause);
581
- return yield* restartActivation(batch, orphan ?? [], cause);
582
- }
583
- mailbox.requeue(orphan ?? []);
584
- if (batch.length === 1) {
585
- const { request } = batch[0];
586
- const defect = Cause.squash(cause);
587
- yield* Effect.logError("Deterministic actor defect", Cause.die(defect)).pipe(Effect.annotateLogs({
588
- actor: request.ref.actor,
589
- id: request.ref.id,
590
- tenant: request.ref.tenant,
591
- command: request.command,
592
- commandId: request.commandId,
593
- }));
594
- memory.restarts = 0;
595
- return yield* Deferred.succeed(batch[0].reply, {
596
- outcome: Outcome.cases.Defect.make({ cause: defect }),
597
- });
598
- }
599
- yield* Effect.logDebug("Turn batch failed; running its commands one at a time", cause);
600
- for (const entry of batch) {
601
- const stopped = yield* run(resolve([entry]), false);
602
- if (stopped !== undefined)
603
- yield* recover(stopped);
604
- }
605
- }, Effect.provideContext(services));
606
- const turnBatch = Effect.fnUntraced(function* (batch) {
607
- if (refused !== undefined)
608
- return yield* restart(batch, Cause.die(refused));
609
- yield* writable;
610
- const stopped = yield* run(batch, true);
611
- if (stopped !== undefined)
612
- yield* recover(stopped);
639
+ yield* Effect.addFinalizer(() => Scope.close(handler, Exit.void));
640
+ yield* work(worker).pipe(Effect.forkIn(handler));
641
+ const diagnose = () => ({
642
+ worker: worker.phase,
643
+ mailbox: worker.mailbox.size(),
644
+ batch: worker.taken.length,
613
645
  });
614
- yield* Effect.gen(function* () {
615
- while (true) {
616
- phase = "idle";
617
- taken = [];
618
- yield* mailbox.await;
619
- const batch = resolve(mailbox.take());
620
- if (batch.length === 0)
621
- continue;
622
- phase = "turn";
623
- taken = [...batch];
624
- if (Exit.isFailure(current.activated)) {
625
- const { cause } = current.activated;
626
- yield* Effect.forEach(batch, (entry) => Deferred.succeed(entry.reply, {
627
- outcome: Outcome.cases.Defect.make({ cause: Cause.squash(cause) }),
628
- }), { discard: true });
629
- continue;
630
- }
631
- yield* gate.run(turnBatch(batch)).pipe(Effect.catchIf(Schema.is(ActorError), (error) => Effect.forEach(taken, (entry) => Deferred.fail(entry.reply, error), {
632
- discard: true,
633
- })), Effect.provideContext(batch[0].context), Effect.catchCauseIf((cause) => !Cause.hasInterruptsOnly(cause), (cause) => restart(batch, cause)));
634
- }
635
- }).pipe(Effect.provideContext(services), Effect.forkIn(handler));
636
- const diagnose = () => ({ worker: phase, mailbox: mailbox.size(), batch: taken.length });
637
- workers.set(entityId, diagnose);
638
- building.delete(entityId);
646
+ workers.set(worker.entityId, diagnose);
647
+ building.delete(worker.entityId);
639
648
  yield* Effect.addFinalizer(() => Effect.sync(() => {
640
- if (workers.get(entityId) === diagnose)
641
- workers.delete(entityId);
649
+ if (workers.get(worker.entityId) === diagnose)
650
+ workers.delete(worker.entityId);
642
651
  }));
643
652
  return entity.of({
644
- Wake: () => Effect.suspend(() => (refused === undefined ? Effect.void : Effect.die(refused))),
653
+ Wake: () => Effect.suspend(() => worker.refused === undefined ? Effect.void : Effect.die(worker.refused)),
645
654
  Execute: ({ payload }) => {
646
- if (refused !== undefined)
647
- return Effect.die(refused);
648
- const { activated } = current;
655
+ if (worker.refused !== undefined)
656
+ return Effect.die(worker.refused);
657
+ const { activated } = worker.current;
649
658
  if (Exit.isFailure(activated))
650
659
  return Effect.succeed({
651
660
  outcome: Outcome.cases.Defect.make({ cause: Cause.squash(activated.cause) }),
@@ -660,8 +669,8 @@ recorder) {
660
669
  context: Context.empty(),
661
670
  queued: false,
662
671
  };
663
- mailbox.offer(entry);
664
- return Rpc.fork(awaitReply(entry, mailbox));
672
+ worker.mailbox.offer(entry);
673
+ return Rpc.fork(awaitReply(entry, worker.mailbox));
665
674
  },
666
675
  });
667
676
  }), {
@@ -35,7 +35,7 @@ export declare const eventsStatement: (request: {
35
35
  readonly _tag: "Anonymous";
36
36
  } | {
37
37
  readonly _tag: "System";
38
- readonly source: "actor" | "cron" | "job" | "process" | "subscription" | "timer" | "workflow";
38
+ readonly source: "actor" | "cold" | "cron" | "job" | "process" | "subscription" | "timer" | "workflow";
39
39
  readonly ref?: {
40
40
  readonly tenant: string;
41
41
  readonly actor: string;
@@ -94,7 +94,7 @@ export declare const notifyEvents: (request: {
94
94
  readonly _tag: "Anonymous";
95
95
  } | {
96
96
  readonly _tag: "System";
97
- readonly source: "actor" | "cron" | "job" | "process" | "subscription" | "timer" | "workflow";
97
+ readonly source: "actor" | "cold" | "cron" | "job" | "process" | "subscription" | "timer" | "workflow";
98
98
  readonly ref?: {
99
99
  readonly tenant: string;
100
100
  readonly actor: string;
@@ -148,7 +148,7 @@ export declare const appendEvents: (request: {
148
148
  readonly _tag: "Anonymous";
149
149
  } | {
150
150
  readonly _tag: "System";
151
- readonly source: "actor" | "cron" | "job" | "process" | "subscription" | "timer" | "workflow";
151
+ readonly source: "actor" | "cold" | "cron" | "job" | "process" | "subscription" | "timer" | "workflow";
152
152
  readonly ref?: {
153
153
  readonly tenant: string;
154
154
  readonly actor: string;
@@ -23,7 +23,7 @@ export { actorErrorBody, closeCodeOf, statusOf } from "../protocol/wire.ts";
23
23
  export { Database } from "./layer.ts";
24
24
  export { App, AppLayerFailed } from "./app.ts";
25
25
  export type { AppActor, AppServices } from "./app.ts";
26
- export { Runner } from "./runner.ts";
26
+ export { Runner, RunnerConfigurationError } from "./runner.ts";
27
27
  export type { SocketRunnerOptions } from "./runner.ts";
28
28
  export type { MutualTlsOptions } from "./peering/transport.ts";
29
29
  export type { RunnerCredentials } from "./peering/credentials.ts";
@@ -31,6 +31,9 @@ export { RunnerAuthority } from "./peering/authority.ts";
31
31
  export type { IssueOptions } from "./peering/authority.ts";
32
32
  export { DataDirLocked, DataDirVersion } from "../errors/database.ts";
33
33
  export type { Options } from "./layer.ts";
34
+ export { ColdStorage, ColdStorageError } from "./storage/cold-storage.ts";
35
+ export type { ColdObject, S3ColdStorageOptions } from "./storage/cold-storage.ts";
36
+ export type { ColdStorageOptions } from "./storage/cold-tier.ts";
34
37
  export { RuntimeControl } from "./drain.ts";
35
38
  export type { DrainReport, Readiness } from "./drain.ts";
36
39
  export { checkWorkflows, formatIncompatibility } from "./workflows/compatibility.ts";
@@ -11,9 +11,10 @@ export { ASSERTION_SKEW_MS, AssertionClaims, AssertionKey, AssertionKeySet, KeyR
11
11
  export { actorErrorBody, closeCodeOf, statusOf } from "../protocol/wire.js";
12
12
  export { Database } from "./layer.js";
13
13
  export { App, AppLayerFailed } from "./app.js";
14
- export { Runner } from "./runner.js";
14
+ export { Runner, RunnerConfigurationError } from "./runner.js";
15
15
  export { RunnerAuthority } from "./peering/authority.js";
16
16
  export { DataDirLocked, DataDirVersion } from "../errors/database.js";
17
+ export { ColdStorage, ColdStorageError } from "./storage/cold-storage.js";
17
18
  export { RuntimeControl } from "./drain.js";
18
19
  export { checkWorkflows, formatIncompatibility } from "./workflows/compatibility.js";
19
20
  export { Inspector } from "./inspector/layer.js";