@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.
- package/CHANGELOG.md +24 -0
- package/dist/actor/descriptor.js +3 -0
- package/dist/actor/handles.d.ts +1 -1
- package/dist/actor/layers.js +2 -0
- package/dist/actor/reads.js +2 -1
- package/dist/contexts/command.d.ts +2 -0
- package/dist/identity/caller.d.ts +4 -4
- package/dist/identity/caller.js +11 -2
- package/dist/identity/mint.d.ts +1 -1
- package/dist/policies/command.d.ts +3 -0
- package/dist/policies/command.js +1 -0
- package/dist/runtime/connections/owner.js +60 -13
- package/dist/runtime/connections/protocol.d.ts +2 -2
- package/dist/runtime/connections/streams.js +2 -1
- package/dist/runtime/database/bounded.js +17 -2
- package/dist/runtime/database/connection.d.ts +3 -2
- package/dist/runtime/database/connection.js +9 -4
- package/dist/runtime/database/coordination.d.ts +6 -3
- package/dist/runtime/database/coordination.js +19 -18
- package/dist/runtime/database/migrations.d.ts +4 -0
- package/dist/runtime/database/migrations.js +72 -1
- package/dist/runtime/entity/mailbox.d.ts +18 -10
- package/dist/runtime/entity/mailbox.js +42 -34
- package/dist/runtime/entity/register.d.ts +3 -3
- package/dist/runtime/entity/register.js +280 -271
- package/dist/runtime/events/append.d.ts +3 -3
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/layer.d.ts +24 -1
- package/dist/runtime/layer.js +64 -9
- package/dist/runtime/members.d.ts +2 -0
- package/dist/runtime/operators/export.d.ts +3 -1
- package/dist/runtime/operators/export.js +42 -11
- package/dist/runtime/queries.d.ts +1 -1
- package/dist/runtime/queries.js +23 -6
- package/dist/runtime/registration.js +27 -1
- package/dist/runtime/request.d.ts +1 -1
- package/dist/runtime/runner.d.ts +18 -1
- package/dist/runtime/runner.js +24 -4
- package/dist/runtime/storage/cold-storage.d.ts +56 -0
- package/dist/runtime/storage/cold-storage.js +138 -0
- package/dist/runtime/storage/cold-tier.d.ts +141 -0
- package/dist/runtime/storage/cold-tier.js +355 -0
- package/dist/runtime/storage/generation.d.ts +14 -0
- package/dist/runtime/storage/generation.js +2 -0
- package/dist/runtime/storage/placements.js +1 -0
- package/dist/runtime/telemetry/metrics.d.ts +4 -0
- package/dist/runtime/telemetry/metrics.js +20 -4
- package/dist/runtime/telemetry/sampler.js +4 -3
- package/dist/runtime/telemetry/spans.d.ts +1 -1
- package/dist/runtime/topology/locks.js +8 -6
- package/dist/runtime/turn/blobs.d.ts +4 -1
- package/dist/runtime/turn/blobs.js +19 -2
- package/dist/runtime/turn/execute.d.ts +3 -1
- package/dist/runtime/turn/execute.js +272 -20
- package/dist/runtime/turn/hooks.d.ts +1 -1
- package/dist/runtime/turn/outbox.d.ts +1 -1
- package/dist/runtime/turn/receipt.d.ts +1 -1
- package/dist/runtime/turn/relay.d.ts +2 -2
- package/dist/runtime/turn/relay.js +53 -7
- package/dist/testing/actor-test.d.ts +1 -0
- package/dist/testing/actor-test.js +4 -1
- 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
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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(() =>
|
|
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;
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/runtime/index.js
CHANGED
|
@@ -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";
|