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
|
|
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": "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))),
|