t3-code-android-nightly/.repos/effect-smol/packages/effect/test/workflow/WorkflowEngine.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

616 lines
25 KiB
TypeScript

import { assert, describe, it } from "@effect/vitest"
import { Cause, Duration, Effect, Exit, Fiber, Latch, Layer, Option, Ref, Schema, Scope } from "effect"
import { TestClock } from "effect/testing"
import { Activity, DurableClock, DurableDeferred, Workflow, WorkflowEngine } from "effect/workflow"
describe("deferred self-completion", () => {
for (const failure of [false, true]) {
it.live(failure ? "failure" : "success", () =>
Effect.gen(function*() {
const signal = DurableDeferred.make("signal", { success: Schema.String, error: Schema.String })
const read = yield* Latch.make()
const cleanup = yield* Latch.make()
const release = yield* Latch.make()
const events: Array<string> = []
let runs = 0
const workflow = Workflow.make("SelfCompletion", {
payload: {},
success: Schema.String,
error: Schema.String,
idempotencyKey: () => "one"
})
const layer = workflow.toLayer(() =>
Effect.gen(function*() {
const run = ++runs
events.push(`start-${run}`)
const engine = yield* WorkflowEngine.WorkflowEngine
return yield* DurableDeferred.raceAll({
name: "race",
success: Schema.String,
error: Schema.String,
effects: [
DurableDeferred.await(signal),
read.await.pipe(
Effect.andThen(Effect.yieldNow),
Effect.andThen(failure ? Effect.fail("boom") : Effect.succeed("ok")),
DurableDeferred.into(signal),
// Successful completion must preempt this producer and replay the run.
Effect.andThen(Effect.never),
Effect.onInterrupt(() =>
run === 1
? Effect.gen(function*() {
events.push("cleanup-start")
yield* cleanup.open
yield* release.await
events.push("cleanup-end")
})
: Effect.void
)
)
]
}).pipe(
Effect.provideService(WorkflowEngine.WorkflowEngine, {
...engine,
deferredResult: (deferred) =>
engine.deferredResult(deferred).pipe(
Effect.tap(() => deferred.name === signal.name ? read.open : Effect.void)
)
}),
Effect.ensuring(Effect.sync(() => events.push(`end-${run}`)))
)
})
).pipe(Layer.provideMerge(WorkflowEngine.layerMemory))
yield* Effect.gen(function*() {
const execution = yield* workflow.execute({}).pipe(Effect.exit, Effect.forkChild({ startImmediately: true }))
if (!failure) {
yield* cleanup.await
for (let i = 0; i < 20; i++) yield* Effect.yieldNow
assert.deepStrictEqual(events, ["start-1", "cleanup-start"], "replay must wait for cleanup")
yield* release.open
}
const result = yield* Fiber.join(execution)
if (failure) {
assert.ok(Exit.isFailure(result))
assert.strictEqual(result.cause.reasons.length, 1)
const reason = result.cause.reasons[0]
assert.ok(Cause.isFailReason(reason))
assert.strictEqual(reason.error, "boom")
} else {
assert.deepStrictEqual(result, Exit.succeed("ok"))
assert.deepStrictEqual(events, ["start-1", "cleanup-start", "cleanup-end", "end-1", "start-2", "end-2"])
}
}).pipe(Effect.provide(layer))
}), 5_000)
}
})
describe("WorkflowEngine", () => {
const IncrementWorkflow = Workflow.make("WorkflowEngine/IncrementWorkflow", {
payload: { value: Schema.Number },
success: Schema.Number,
idempotencyKey: ({ value }) => String(value)
})
const IncrementWorkflowLayer = IncrementWorkflow.toLayer(({ value }) => Effect.succeed(value + 1))
class ClassWorkflow extends Workflow.make("WorkflowEngine/ClassWorkflow", {
payload: { value: Schema.Number },
success: Schema.Number,
idempotencyKey: ({ value }) => String(value)
}) {}
const ClassWorkflowLayer = ClassWorkflow.toLayer(({ value }) => Effect.succeed(value + 1))
const DeferredRaceWorkflow = Workflow.make("WorkflowEngine/DeferredRaceWorkflow", {
payload: { id: Schema.String },
success: Schema.String,
idempotencyKey: ({ id }) => id
})
const DeferredRaceGate = DurableDeferred.make("WorkflowEngine/DeferredRaceGate", {
success: Schema.String
})
let deferredRaceRuns = 0
const DeferredRaceWorkflowLayer = DeferredRaceWorkflow.toLayer(() =>
Effect.suspend(() => {
deferredRaceRuns++
return DurableDeferred.raceAll({
name: "memory-deferred-race",
success: Schema.String,
error: Schema.Never,
effects: [
DurableDeferred.await(DeferredRaceGate),
Effect.sleep("10 seconds").pipe(Effect.as("activity"))
]
})
})
)
it.effect("layer executes and polls workflows", () =>
Effect.gen(function*() {
const executionId = yield* IncrementWorkflow.execute({ value: 1 }, { discard: true })
const result = yield* IncrementWorkflow.execute({ value: 1 })
const polled = yield* IncrementWorkflow.poll(executionId)
assert.strictEqual(result, 2)
assert(Option.isSome(polled) && polled.value._tag === "Complete" && Exit.isSuccess(polled.value.exit))
assert.strictEqual(polled.value.exit.value, 2)
}).pipe(
Effect.provide(IncrementWorkflowLayer.pipe(
Layer.provideMerge(WorkflowEngine.layerMemory)
))
))
for (
const { childCount, concurrency, waves } of [
{ childCount: 3, concurrency: "unbounded" as const, waves: [3] },
{ childCount: 5, concurrency: 2, waves: [2, 4, 5] }
]
) {
it.effect(`layerMemory replays suspended activity fan-out with concurrency ${concurrency}`, () =>
Effect.gen(function*() {
const Parent = Workflow.make("WorkflowEngine/FanOutParent", {
payload: {},
success: Schema.Array(Schema.Number),
idempotencyKey: () => "parent"
})
const Child = Workflow.make("WorkflowEngine/FanOutChild", {
payload: { index: Schema.Number },
success: Schema.Number,
idempotencyKey: ({ index }) => String(index)
})
const started = new Set<number>()
let parentRuns = 0
let activityRuns = 0
let released = 0
const ParentLayer = Parent.toLayer(() =>
Effect.suspend(() => {
parentRuns++
return Activity.make({
name: "fan-out",
success: Schema.Array(Schema.Number),
execute: Effect.suspend(() => {
activityRuns++
return Effect.forEach(
Array.from({ length: childCount }, (_, index) => index),
(index) => Child.execute({ index }),
{ concurrency }
)
}).pipe(Effect.ensuring(Effect.sync(() => {
released++
})))
})
})
)
const ChildLayer = Child.toLayer(Effect.fnUntraced(function*({ index }) {
started.add(index)
yield* DurableClock.sleep({ name: "child", duration: "2 seconds", inMemoryThreshold: Duration.zero })
return index
}))
yield* Effect.gen(function*() {
const executionId = yield* Parent.execute({}, { discard: true })
// Execution-ID digests use real promises, so wait for state rather than clock ticks.
for (const expected of waves) {
let result = yield* Parent.poll(executionId)
while (Option.isNone(result) || result.value._tag !== "Suspended" || started.size !== expected) {
yield* Effect.yieldNow
result = yield* Parent.poll(executionId)
}
assert.deepStrictEqual([...started].sort(), Array.from({ length: expected }, (_, index) => index))
assert.strictEqual(released, activityRuns)
assert.isAtMost(activityRuns, parentRuns)
if (expected === waves[0]) assert.strictEqual(parentRuns, 1)
yield* TestClock.adjust("2 seconds")
}
let result = yield* Parent.poll(executionId)
while (Option.isNone(result) || result.value._tag !== "Complete") {
yield* Effect.yieldNow
result = yield* Parent.poll(executionId)
}
assert.deepStrictEqual(
result.value,
new Workflow.Complete({ exit: Exit.succeed(Array.from({ length: childCount }, (_, index) => index)) })
)
assert.isAtLeast(parentRuns, waves.length + 1)
assert.isAtMost(activityRuns, parentRuns)
}).pipe(
Effect.provide(Layer.mergeAll(ParentLayer, ChildLayer).pipe(Layer.provideMerge(WorkflowEngine.layerMemory)))
)
}))
}
it.effect("layerMemory resumes when children complete during activity cleanup", () =>
Effect.gen(function*() {
const cleaningUp = yield* Latch.make()
const release = yield* Latch.make()
const Parent = Workflow.make("WorkflowEngine/CleanupParent", {
payload: {},
success: Schema.Array(Schema.Number),
idempotencyKey: () => "parent"
})
const Child = Workflow.make("WorkflowEngine/CleanupChild", {
payload: { index: Schema.Number },
success: Schema.Number,
idempotencyKey: ({ index }) => String(index)
})
const ParentLayer = Parent.toLayer(() =>
Activity.make({
name: "children",
success: Schema.Array(Schema.Number),
execute: Effect.forEach([0, 1], (index) => Child.execute({ index }), { concurrency: "unbounded" }).pipe(
Effect.ensuring(Effect.andThen(cleaningUp.open, release.await))
)
})
)
const ChildLayer = Child.toLayer(({ index }) =>
DurableClock.sleep({ name: "wait", duration: "2 seconds", inMemoryThreshold: Duration.zero }).pipe(
Effect.as(index)
)
)
yield* Effect.gen(function*() {
const executionId = yield* Parent.execute({}, { discard: true })
yield* cleaningUp.await
yield* TestClock.adjust("2 seconds")
yield* release.open
let result = yield* Parent.poll(executionId)
while (Option.isNone(result) || result.value._tag !== "Complete") {
yield* Effect.yieldNow
result = yield* Parent.poll(executionId)
}
assert.deepStrictEqual(
result.value,
new Workflow.Complete({ exit: Exit.succeed([0, 1]) })
)
}).pipe(
Effect.provide(Layer.mergeAll(ParentLayer, ChildLayer).pipe(Layer.provideMerge(WorkflowEngine.layerMemory)))
)
}))
for (const mode of ["failure", "interruption"] as const) {
it.effect(`layerMemory releases a pending child registration on ${mode}`, () =>
Effect.gen(function*() {
const computingId = yield* Latch.make()
const started = new Set<number>()
const Parent = Workflow.make(`WorkflowEngine/PendingParent/${mode}`, {
payload: {},
success: Schema.Array(Schema.Number),
idempotencyKey: () => "parent"
})
const Child = Workflow.make(`WorkflowEngine/PendingChild/${mode}`, {
payload: { index: Schema.Number },
success: Schema.Number,
idempotencyKey: ({ index }) => {
if (index === 1) {
if (mode === "failure") throw new Error("cannot compute the execution id")
computingId.openUnsafe()
}
return String(index)
}
})
const ParentLayer = Parent.toLayer(() =>
Activity.make({
name: "children",
success: Schema.Array(Schema.Number),
execute: Effect.forEach([0, 1], (index) => {
const execute = Child.execute({ index })
if (index === 0) return execute
return mode === "failure"
? Effect.catchDefect(execute, () => Effect.succeed(-1))
: Effect.raceFirst(execute, Effect.as(computingId.await, -1))
}, { concurrency: "unbounded" })
})
)
const ChildLayer = Child.toLayer(Effect.fnUntraced(function*({ index }) {
started.add(index)
yield* DurableClock.sleep({ name: "wait", duration: "2 seconds", inMemoryThreshold: Duration.zero })
return index
}))
yield* Effect.gen(function*() {
const executionId = yield* Parent.execute({}, { discard: true })
let result = yield* Parent.poll(executionId)
while (Option.isNone(result) || result.value._tag !== "Suspended") {
yield* Effect.yieldNow
result = yield* Parent.poll(executionId)
}
assert.deepStrictEqual([...started], [0])
yield* TestClock.adjust("2 seconds")
result = yield* Parent.poll(executionId)
while (Option.isNone(result) || result.value._tag !== "Complete") {
yield* Effect.yieldNow
result = yield* Parent.poll(executionId)
}
assert.deepStrictEqual(
result.value,
new Workflow.Complete({ exit: Exit.succeed([0, -1]) })
)
}).pipe(
Effect.provide(Layer.mergeAll(ParentLayer, ChildLayer).pipe(Layer.provideMerge(WorkflowEngine.layerMemory)))
)
}))
}
it.effect("discard returns the deterministic execution ID", () =>
Effect.gen(function*() {
const executionId = yield* IncrementWorkflow.executionId({ value: 1 })
const discardedExecutionId = yield* IncrementWorkflow.execute({ value: 1 }, { discard: true })
assert.strictEqual(discardedExecutionId, executionId)
}).pipe(
Effect.provide(IncrementWorkflowLayer.pipe(
Layer.provideMerge(WorkflowEngine.layerMemory)
))
))
it.effect("distinguishes workflow tags and idempotency keys at string boundaries", () =>
Effect.gen(function*() {
const makeWorkflow = (tag: string) =>
Workflow.make(tag, {
payload: { key: Schema.String, value: Schema.Number },
idempotencyKey: ({ key }) => key
})
for (
const [leftTag, leftKey, rightTag, rightKey] of [
["Order", "Pay-42", "Order-Pay", "42"],
["Order", "Pay:42", "Order:Pay", "42"],
["", "-", "-", ""],
["A\"", "B\\", "A\"-B", "\\"]
]
) {
const left = makeWorkflow(leftTag)
const right = makeWorkflow(rightTag)
const leftId = yield* left.executionId({ key: leftKey, value: 1 })
const rightId = yield* right.executionId({ key: rightKey, value: 1 })
assert.notStrictEqual(leftId, rightId)
assert.strictEqual(yield* left.executionId({ key: leftKey, value: 1 }), leftId)
assert.strictEqual(yield* left.executionId({ key: leftKey, value: 2 }), leftId)
}
}))
it.effect("supports class extension", () =>
Effect.gen(function*() {
const result = yield* ClassWorkflow.execute({ value: 1 })
assert.strictEqual(ClassWorkflow._tag, "WorkflowEngine/ClassWorkflow")
assert.strictEqual(result, 2)
}).pipe(
Effect.provide(ClassWorkflowLayer.pipe(
Layer.provideMerge(WorkflowEngine.layerMemory)
))
))
it.effect("layerMemory wakes an active workflow when a durable deferred completes", () =>
Effect.gen(function*() {
const payload = { id: "memory-deferred-race" }
const executionId = yield* DeferredRaceWorkflow.executionId(payload)
const fiber = yield* DeferredRaceWorkflow.execute(payload).pipe(
Effect.forkChild({ startImmediately: true })
)
// Park the deferred branch before completing it.
yield* TestClock.adjust(1)
yield* TestClock.adjust(1)
const token = DurableDeferred.tokenFromExecutionId(DeferredRaceGate, {
workflow: DeferredRaceWorkflow,
executionId
})
yield* DurableDeferred.succeed(DeferredRaceGate, { token, value: "signal" })
// Require the wake to settle before the sleeper.
let polled = yield* DeferredRaceWorkflow.poll(executionId)
while (Option.isNone(polled) || polled.value._tag !== "Complete") {
yield* Effect.yieldNow
polled = yield* DeferredRaceWorkflow.poll(executionId)
}
// Let the caller's suspended-retry loop pick up a replayed result.
yield* TestClock.adjust("1 second")
assert.strictEqual(yield* Fiber.join(fiber), "signal")
// Usually the completion preempts the parked run (2 runs); under load
// it can land before the branch parks and is read directly (1 run).
assert(deferredRaceRuns === 1 || deferredRaceRuns === 2)
}).pipe(
Effect.provide(DeferredRaceWorkflowLayer.pipe(
Layer.provideMerge(WorkflowEngine.layerMemory)
))
))
it.effect("layerMemory propagates interruption when the engine is shut down", () =>
Effect.gen(function*() {
const Stuck = Workflow.make("WorkflowEngine/ShutdownWorkflow", {
payload: { id: Schema.String },
idempotencyKey: ({ id }) => id
})
const layer = Stuck.toLayer(() => Effect.never).pipe(
Layer.provideMerge(WorkflowEngine.layerMemory)
)
const scope = yield* Scope.make()
const context = yield* Scope.provide(Layer.build(layer), scope)
const fiber = yield* Stuck.execute({ id: "one" }).pipe(
Effect.provideContext(context),
Effect.forkChild({ startImmediately: true })
)
yield* Effect.yieldNow
yield* Effect.yieldNow
// Shutting down must interrupt the caller, not report a suspension.
yield* Scope.close(scope, Exit.void)
const exit = yield* Fiber.await(fiber)
assert(Exit.hasInterrupts(exit))
}))
it.effect("layerMemory runs compensations when the engine is shut down", () =>
Effect.gen(function*() {
const ready = Latch.makeUnsafe()
const compensated: Array<string> = []
const Stuck = Workflow.make("WorkflowEngine/ShutdownCompensation", {
payload: { id: Schema.String },
idempotencyKey: ({ id }) => id
})
const layer = Stuck.toLayer(() =>
Effect.gen(function*() {
yield* Effect.succeed("a").pipe(
Stuck.withCompensation((value) => Effect.sync(() => compensated.push(value)))
)
ready.openUnsafe()
return yield* Effect.never
})
).pipe(
Layer.provideMerge(WorkflowEngine.layerMemory)
)
const scope = yield* Scope.make()
const context = yield* Scope.provide(Layer.build(layer), scope)
const fiber = yield* Stuck.execute({ id: "one" }).pipe(
Effect.provideContext(context),
Effect.forkChild({ startImmediately: true })
)
yield* ready.await
yield* Scope.close(scope, Exit.void)
const exit = yield* Fiber.await(fiber)
assert(Exit.hasInterrupts(exit))
assert.deepStrictEqual(compensated, ["a"])
}))
it.effect("layerMemory closes finalizers registered before suspension", () =>
Effect.gen(function*() {
const gate = DurableDeferred.make("WorkflowEngine/SuspendedScope/Gate")
const finalized: Array<number> = []
let runs = 0
const Suspends = Workflow.make("WorkflowEngine/SuspendedScope", {
payload: { id: Schema.String },
idempotencyKey: ({ id }) => id
})
const layer = Suspends.toLayer(() =>
Effect.gen(function*() {
const run = ++runs
yield* Workflow.addFinalizer(() => Effect.sync(() => finalized.push(run)))
yield* DurableDeferred.await(gate)
})
).pipe(Layer.provideMerge(WorkflowEngine.layerMemory))
yield* Effect.gen(function*() {
const payload = { id: "one" }
const executionId = yield* Suspends.execute(payload, { discard: true })
let result = yield* Suspends.poll(executionId)
while (Option.isNone(result) || result.value._tag !== "Suspended") {
yield* Effect.yieldNow
result = yield* Suspends.poll(executionId)
}
const token = DurableDeferred.tokenFromExecutionId(gate, { workflow: Suspends, executionId })
yield* DurableDeferred.succeed(gate, { token, value: void 0 })
yield* Suspends.execute(payload)
assert.deepStrictEqual(finalized, [2, 1])
}).pipe(Effect.provide(layer))
}))
it.effect("layerMemory hides deposited interrupts from body finalizers", () =>
Effect.gen(function*() {
const gate = DurableDeferred.make("WorkflowEngine/InterruptBodyFinalizer/Gate")
const observed = yield* Ref.make<ReadonlyArray<boolean>>([])
const Suspends = Workflow.make("WorkflowEngine/InterruptBodyFinalizer", {
payload: { id: Schema.String },
idempotencyKey: ({ id }) => id
})
const layer = Suspends.toLayer(Effect.fnUntraced(function*() {
const instance = yield* WorkflowEngine.WorkflowInstance
return yield* DurableDeferred.await(gate).pipe(
Effect.onExit(() => Ref.update(observed, (values) => [...values, instance.interrupted]))
)
})).pipe(Layer.provideMerge(WorkflowEngine.layerMemory))
yield* Effect.gen(function*() {
const payload = { id: "one" }
const executionId = yield* Suspends.execute(payload, { discard: true })
let result = yield* Suspends.poll(executionId)
while (Option.isNone(result) || result.value._tag !== "Suspended") {
yield* Effect.yieldNow
result = yield* Suspends.poll(executionId)
}
yield* Suspends.interrupt(executionId)
while (Option.isNone(result) || result.value._tag !== "Complete") {
yield* Effect.yieldNow
result = yield* Suspends.poll(executionId)
}
assert.deepStrictEqual(yield* Ref.get(observed), [false, false])
}).pipe(Effect.provide(layer))
}))
it.effect("layerMemory exposes deposited interrupts to workflow finalizers", () =>
Effect.gen(function*() {
const gate = DurableDeferred.make("WorkflowEngine/InterruptWorkflowFinalizer/Gate")
const observed = yield* Ref.make<ReadonlyArray<boolean>>([])
const runs = yield* Ref.make(0)
const Suspends = Workflow.make("WorkflowEngine/InterruptWorkflowFinalizer", {
payload: { id: Schema.String },
idempotencyKey: ({ id }) => id
})
const layer = Suspends.toLayer(Effect.fnUntraced(function*() {
const run = yield* Ref.getAndUpdate(runs, (run) => run + 1)
const instance = yield* WorkflowEngine.WorkflowInstance
if (run === 1) {
yield* Workflow.addFinalizer(() => Ref.update(observed, (values) => [...values, instance.interrupted]))
}
return yield* DurableDeferred.await(gate)
})).pipe(Layer.provideMerge(WorkflowEngine.layerMemory))
yield* Effect.gen(function*() {
const payload = { id: "one" }
const executionId = yield* Suspends.execute(payload, { discard: true })
let result = yield* Suspends.poll(executionId)
while (Option.isNone(result) || result.value._tag !== "Suspended") {
yield* Effect.yieldNow
result = yield* Suspends.poll(executionId)
}
yield* Suspends.interrupt(executionId)
while (Option.isNone(result) || result.value._tag !== "Complete") {
yield* Effect.yieldNow
result = yield* Suspends.poll(executionId)
}
assert.deepStrictEqual(yield* Ref.get(observed), [true])
}).pipe(Effect.provide(layer))
}))
it.effect("layerMemory preserves interruptUnsafe across a suspended replay", () =>
Effect.gen(function*() {
const gate = DurableDeferred.make("WorkflowEngine/InterruptUnsafeReplay/Gate")
const Suspends = Workflow.make("WorkflowEngine/InterruptUnsafeReplay", {
payload: { id: Schema.String },
idempotencyKey: ({ id }) => id
})
const layer = Suspends.toLayer(() => DurableDeferred.await(gate)).pipe(
Layer.provideMerge(WorkflowEngine.layerMemory)
)
yield* Effect.gen(function*() {
const engine = yield* WorkflowEngine.WorkflowEngine
const payload = { id: "one" }
const executionId = yield* Suspends.execute(payload, { discard: true })
let result = yield* Suspends.poll(executionId)
while (Option.isNone(result) || result.value._tag !== "Suspended") {
yield* Effect.yieldNow
result = yield* Suspends.poll(executionId)
}
yield* engine.interruptUnsafe(Suspends, executionId)
const token = DurableDeferred.tokenFromExecutionId(gate, { workflow: Suspends, executionId })
yield* DurableDeferred.succeed(gate, { token, value: void 0 })
while (Option.isNone(result) || result.value._tag !== "Complete") {
yield* Effect.yieldNow
result = yield* Suspends.poll(executionId)
}
assert(Option.isSome(result) && result.value._tag === "Complete")
assert(Exit.hasInterrupts(result.value.exit))
}).pipe(Effect.provide(layer))
}))
})