tardie 0.47.2-rc.495 → 0.47.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tardie",
3
- "version": "0.47.2-rc.495",
3
+ "version": "0.47.2",
4
4
  "license": "MIT",
5
5
  "author": "Clavia, Inc.",
6
6
  "description": "Actor, agent, client, and host APIs for Tardigrade.",
@@ -16,7 +16,7 @@
16
16
  "access": "public"
17
17
  },
18
18
  "tardigrade": {
19
- "sourceTree": "1cdd75351fedad878fb634173bfb109d56e3ddff"
19
+ "sourceTree": "464249e5baa63a2b89b31edfb59bf6917bd82fc7"
20
20
  },
21
21
  "files": [
22
22
  "src",
@@ -1,3 +1,4 @@
1
+ import type { RetryOptions } from "../services/effect-execution"
1
2
  import type { WatchdogTarget } from "../services/watchdog"
2
3
  import { prepareInitialState } from "./initialisation"
3
4
  import type { ThreadCoordinate, ChildPlacement } from "../actor/thread"
@@ -31,6 +32,7 @@ export function createThreadHost<Event extends object, Services, State, Contract
31
32
  readonly actorTransport?: ActorMessageTransport
32
33
  readonly transports?: Readonly<Record<string, MessageTransport>>
33
34
  readonly delivery?: { readonly retryIntervalMs?: number }
35
+ readonly provisionRetry?: RetryOptions<Error>
34
36
  readonly generateName?: () => string
35
37
  readonly defaultChildPlacement?: ChildPlacement
36
38
  }) {
@@ -49,6 +51,7 @@ export function createThreadHost<Event extends object, Services, State, Contract
49
51
  ...(options.checkpointPolicy ? { checkpointPolicy: options.checkpointPolicy } : {}),
50
52
  ...(options.canDrive ? { canDrive: (instance: string) => options.canDrive!({ actor: options.actor.actorName, instance }) } : {}),
51
53
  ...(options.promises ? { promises: options.promises } : {}),
54
+ ...(options.provisionRetry ? { provisionRetry: options.provisionRetry } : {}),
52
55
  journal: instance => options.storage.supervisor(options.actor.actorName, instance),
53
56
  validateInitialState: state => prepareInitialState(options.initialStateAtoms ?? [], state).pipe(Effect.asVoid),
54
57
  provision: allocation => actors.provision({
@@ -62,7 +65,7 @@ export function createThreadHost<Event extends object, Services, State, Contract
62
65
  run,
63
66
  })
64
67
  const services: ActorExecutionOptions<Event, Services, State, Contracts>["services"] = (coordinate, runtime) => options.services(coordinate, runtime).pipe(Layer.provideMerge(Layer.merge(Layer.succeed(Supervisor, supervisor), Layer.succeed(Invocation, invocation.forSender(coordinate)))))
65
- const actors: ReturnType<typeof createActorExecution<Event, Services, State, Contracts>> = createActorExecution({
68
+ const actors: ReturnType<typeof createActorExecution<Event, Services, State, Contracts>> = createActorExecution<Event, Services, State, Contracts>({
66
69
  ...options, executionStreamBus: executionStream, run, from: options.from ?? DEFAULT_EXTERNAL_SENDER,
67
70
  delivery: coordinate => ({ ...options.delivery, address: coordinate, send: invocation.forSender(coordinate).send }),
68
71
  services,
@@ -16,6 +16,7 @@ const Allocation = Schema.Struct({
16
16
  initialState: Schema.optionalKey(InitialState),
17
17
  })
18
18
  export type ThreadAllocation = typeof Allocation.Type
19
+ const sameAllocation = Schema.toEquivalence(Allocation)
19
20
  const Thread = Schema.Struct({ ...Allocation.fields, origin: Schema.Finite, status: Schema.Literals(["requested", "registered", "failed"]), reason: Schema.optionalKey(Schema.String) })
20
21
  export const SupervisorEvent = Schema.Union([
21
22
  Schema.Struct({ type: Schema.Literal("ThreadRequested"), allocation: Allocation }),
@@ -24,7 +25,7 @@ export const SupervisorEvent = Schema.Union([
24
25
  ])
25
26
  export type SupervisorEvent = typeof SupervisorEvent.Type
26
27
 
27
- // threads projects allocation state; provisioning must tolerate repetition after interrupted registration.
28
+ // threads preserves allocation identity across failed re-requests (packages/platform/test/workerd/thread-layout.workers.ts). Provisioning must tolerate repetition after interrupted registration.
28
29
  export const threads = durableAtom({ name: "host.supervisor.threads",
29
30
  input: SupervisorEvent,
30
31
  schema: Schema.Array(Thread),
@@ -32,12 +33,14 @@ export const threads = durableAtom({ name: "host.supervisor.threads",
32
33
  reduce: (state, event: SupervisorEvent, _metadata, position) => {
33
34
  if (event.type === "ThreadRequested") {
34
35
  const next = event.allocation
35
- if (state.some(entry => entry.coordinate.thread === next.coordinate.thread || (entry.parent === next.parent && entry.name === next.name))) throw new Error("Duplicate thread allocation")
36
+ const existing = state.find(entry => entry.coordinate.thread === next.coordinate.thread || (entry.parent === next.parent && entry.name === next.name))
37
+ if (existing && (existing.status !== "failed" || !sameAllocation(existing, next))) throw new Error("Duplicate thread allocation")
36
38
  const parent = next.parent === null ? undefined : state.find(entry => entry.coordinate.thread === next.parent && entry.status === "registered")
37
39
  if (next.parent !== null && !parent) throw new Error("Unknown parent thread")
38
40
  if (next.depth !== (parent ? parent.depth + 1 : 0)) throw new Error("Invalid thread depth")
39
41
  if (state.some(entry => entry.coordinate.actor !== next.coordinate.actor || entry.coordinate.instance !== next.coordinate.instance)) throw new Error("Supervisor instance mismatch")
40
- return [...state, { ...next, origin: position, status: "requested" as const }]
42
+ const requested = { ...next, origin: position, status: "requested" as const }
43
+ return existing ? state.map(entry => entry === existing ? requested : entry) : [...state, requested]
41
44
  }
42
45
  const current = state.find(entry => entry.coordinate.thread === event.coordinate.thread)
43
46
  if (!current || current.status !== "requested" || current.coordinate.actor !== event.coordinate.actor || current.coordinate.instance !== event.coordinate.instance) throw new Error("Unknown thread registration")
@@ -54,7 +57,7 @@ export const supervisorActor = defineActor("supervisor", Effect.sync(() => {
54
57
  const acts = Object.fromEntries(directory.filter(entry => entry.status === "requested").map(allocation => {
55
58
  const key = allocation.coordinate.thread
56
59
  let request = requests.get(key)
57
- if (!request) {
60
+ if (!request || request.origin !== allocation.origin) {
58
61
  request = Provision.request({ origin: allocation.origin, input: allocation, onSettled: result => {
59
62
  if (result.status === "rejected") return [{ type: "ThreadFailed", coordinate: allocation.coordinate, reason: typeof result.reason === "string" ? result.reason : JSON.stringify(result.reason) } satisfies SupervisorEvent]
60
63
  return [{ type: "ThreadRegistered", coordinate: allocation.coordinate } satisfies SupervisorEvent]
@@ -2,9 +2,9 @@ import { isDeepStrictEqual } from "node:util"
2
2
  import type { RecoveryState } from "./watchdog"
3
3
  import type { PromisePolicy } from "./promises"
4
4
  import type { InitialState } from "../initialise"
5
+ import { EffectExecution, type RetryOptions } from "./effect-execution"
5
6
  import { Context, Deferred, Effect, Exit, Random, Schema, Semaphore } from "effect"
6
7
  import { durablePromise } from "../atoms/promise"
7
- import { EffectExecution } from "../atoms/effect"
8
8
  import { RuntimeError } from "../runtime/effects"
9
9
  import type { ChildPlacement, ThreadCoordinate } from "../actor/thread"
10
10
  import type { Journal } from "./journal"
@@ -13,6 +13,13 @@ import { createActorStore, type CheckpointPolicy } from "../runtime/execution"
13
13
  import type { ActorRuntime } from "../runtime/contracts"
14
14
  import { createSupervisorStore, type SupervisorStore } from "../runtime/stores/supervisor"
15
15
 
16
+ export const DEFAULT_PROVISION_RETRY_LIMIT = 2
17
+ export const DEFAULT_PROVISION_RETRY_INTERVAL_MS = 1_000
18
+ // DEFAULT_PROVISION_RETRY bounds typed provisioning failures using durable retries (platform/test/workerd/thread-layout.workers.ts).
19
+ export const DEFAULT_PROVISION_RETRY: RetryOptions<Error> = {
20
+ decide: (failure, attempt) => Effect.succeed(attempt < DEFAULT_PROVISION_RETRY_LIMIT ? { delayMs: DEFAULT_PROVISION_RETRY_INTERVAL_MS, reason: String(failure) } : undefined),
21
+ }
22
+
16
23
  export interface ThreadRequest {
17
24
  readonly instance: string
18
25
  readonly parent?: ThreadCoordinate
@@ -36,6 +43,7 @@ export function createSupervisor(options: {
36
43
  readonly promises?: Partial<PromisePolicy>
37
44
  readonly canDrive?: (instance: string) => Effect.Effect<boolean, Error>
38
45
  readonly journal: (instance: string) => Journal<SupervisorEvent>
46
+ readonly provisionRetry?: RetryOptions<Error>
39
47
  readonly provision: (allocation: ThreadAllocation) => Effect.Effect<void, Error>
40
48
  readonly validateInitialState?: (state: InitialState) => Effect.Effect<void, Error>
41
49
  readonly generateName?: () => string
@@ -59,7 +67,7 @@ export function createSupervisor(options: {
59
67
  return Provision.layer(allocation => Effect.gen(function* () {
60
68
  const execution = yield* EffectExecution
61
69
  const reply = durablePromise(execution.ref, { success: Schema.Null, error: Schema.String })
62
- const handle = yield* execution.fork(options.provision(allocation).pipe(
70
+ const handle = yield* execution.fork(execution.retry(options.provision(allocation), options.provisionRetry ?? DEFAULT_PROVISION_RETRY).pipe(
63
71
  Effect.match({ onFailure: error => reply.fail(String(error)), onSuccess: () => reply.succeed(null) }),
64
72
  ))
65
73
  return Provision.defer(handle)
@@ -107,7 +115,10 @@ export function createSupervisor(options: {
107
115
  if (initialState !== undefined && !isDeepStrictEqual(existing.initialState, initialState)) return yield* Effect.fail(new RuntimeError("Named thread already has different initial state"))
108
116
  if (requestedPlacement !== undefined && existing.placement !== requestedPlacement) return yield* Effect.fail(new RuntimeError("Named thread already has different placement"))
109
117
  if (suppliedName === undefined) return yield* Effect.fail(new RuntimeError("Generated thread name already exists; supply a different name or generator"))
110
- yield* supervisor.resume
118
+ if (existing.status === "failed") {
119
+ const { coordinate, name, parent, depth, placement, initialState } = existing
120
+ yield* supervisor.send([{ type: "ThreadRequested", allocation: { coordinate, name, parent, depth, placement, ...(initialState === undefined ? {} : { initialState }) } }])
121
+ } else yield* supervisor.resume
111
122
  return { supervisor, coordinate: existing.coordinate }
112
123
  }
113
124
  const thread = directory.some(entry => entry.coordinate.thread === name) ? `${name}-${yield* randomName}` : name
@@ -38,7 +38,7 @@ export interface CloudflareObjectBindings {
38
38
  }
39
39
 
40
40
  type ExecutionOptions<Event extends object, Services, State, Contracts extends ActorMethods<Event>> = ActorExecutionOptions<Event, Services, State, Contracts>
41
- export type ActorObjectOptions<Env extends object, Event extends object, Services, State, Contracts extends ActorMethods<Event>> = Omit<ExecutionOptions<Event, Services, State, Contracts>, "storage" | "services" | "from" | "delivery" | "actorContext" | "executionStreamBus"> & Pick<Parameters<typeof createThreadHost<Event, Services, State, Contracts>>[0], "from" | "delivery" | "transports" | "actorTransport" | "telemetry" | "defaultChildPlacement"> & {
41
+ export type ActorObjectOptions<Env extends object, Event extends object, Services, State, Contracts extends ActorMethods<Event>> = Omit<ExecutionOptions<Event, Services, State, Contracts>, "storage" | "services" | "from" | "delivery" | "actorContext" | "executionStreamBus"> & Pick<Parameters<typeof createThreadHost<Event, Services, State, Contracts>>[0], "from" | "delivery" | "transports" | "actorTransport" | "telemetry" | "defaultChildPlacement" | "provisionRetry"> & {
42
42
  readonly actorContext?: ExecutionOptions<Event, Services, State, Contracts>["actorContext"]
43
43
  // services receives the current Durable Object storage for local cache layers.
44
44
  readonly services: (env: Env, ...args: [...Parameters<ExecutionOptions<Event, Services, State, Contracts>["services"]>, DurableObjectStorage]) => Layer.Layer<Layer.Success<ReturnType<ExecutionOptions<Event, Services, State, Contracts>["services"]>>, Error, Supervisor | Invocation>
@@ -146,6 +146,7 @@ export function createActorObjects<Env extends object = Record<string, unknown>,
146
146
  if (!this.directory) {
147
147
  this.journal = objectJournal({ storage: this.ctx.storage, key: JSON.stringify([options.actor.actorName, instance, "supervisor"]), checkpointChunkBytes: options.checkpointChunkBytes, eventInlineBytes: options.eventInlineBytes, target: { actor: options.actor.actorName, instance }, watchdog: this.watchdog, ...(options.alarms ? { alarms: options.alarms } : {}) })
148
148
  this.directory = createSupervisor({ actor: options.actor.actorName, journal: () => this.journal!, run: this.run,
149
+ ...(options.provisionRetry ? { provisionRetry: options.provisionRetry } : {}),
149
150
  ...(options.attemptTimeoutMs === undefined ? {} : { attemptTimeoutMs: options.attemptTimeoutMs }),
150
151
  ...(options.checkpointPolicy ? { checkpointPolicy: options.checkpointPolicy } : {}),
151
152
  canDrive: () => this.watchdog.status.pipe(Effect.flatMap(entries => entries.get(watchdogKey({ actor: options.actor.actorName, instance }))?.status === "blocked" ? Effect.succeed(false) : options.canDrive ? options.canDrive({ actor: options.actor.actorName, instance }) : Effect.succeed(true))),