Skip to content

Commit cc303f5

Browse files
Compact compiled terminal coordination
1 parent 1fa7b71 commit cc303f5

3 files changed

Lines changed: 114 additions & 86 deletions

File tree

.changeset/calm-machines-finish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typeonce/effect-machine": patch
3+
---
4+
5+
Reduce compiled-machine lifecycle and retained-memory overhead by using owner-local terminal arbitration while preserving public completion, cleanup, and first-terminal-wins semantics.

src/internal/machineRuntime.ts

Lines changed: 48 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,17 +1194,32 @@ const startCompiledInternal: <
11941194
const shutdownMailbox = compactMailbox === undefined
11951195
? Queue.shutdown(queue!)
11961196
: Effect.sync(() => closeCompactMailbox(compactMailbox))
1197-
const termination = yield* Deferred.make<ProcessTermination>()
11981197
const done = yield* Deferred.make<Output, Error | StoppedError>()
11991198
const awaitCompletion = Deferred.await(done).pipe(Effect.exit, Effect.asVoid)
12001199
const drainServices = onDemand ? yield* Effect.context<Requirements>() : undefined
12011200
let worker: Fiber.Fiber<any, never> | undefined
12021201
let draining = false
12031202
let offerRevision = 0
12041203
let interruptRequested = false
1205-
let terminationRequested = false
1206-
let requestRuntimeTermination = (requested: ProcessTermination): Effect.Effect<boolean> =>
1207-
Deferred.succeed(termination, requested)
1204+
let requestedTermination: ProcessTermination | undefined
1205+
let reservedTerminationSnapshot: RuntimeSnapshot<State, Error, Output> | undefined
1206+
let reserveRequestedTermination:
1207+
| ((requested: ProcessTermination) => RuntimeSnapshot<State, Error, Output> | undefined)
1208+
| undefined
1209+
// A compiled process never suspends waiting for a terminal request: its
1210+
// active drain owns completion, while stop/failure either interrupts that
1211+
// owner or terminalizes an idle drain directly. An owner-local cell can
1212+
// therefore arbitrate the first request atomically without retaining a
1213+
// second Deferred in every compiled machine.
1214+
const requestRuntimeTermination = (requested: ProcessTermination): Effect.Effect<boolean> =>
1215+
Effect.sync(() => {
1216+
if (requestedTermination !== undefined) {
1217+
return false
1218+
}
1219+
requestedTermination = requested
1220+
reservedTerminationSnapshot = reserveRequestedTermination?.(requested)
1221+
return true
1222+
})
12081223
let sendEvent = compactMailbox !== undefined
12091224
? (event: Event): Effect.Effect<void, StoppedError> => Effect.sync(() => offerCompactMailbox(compactMailbox, event))
12101225
: (event: Event): Effect.Effect<void, StoppedError> =>
@@ -1408,26 +1423,6 @@ const startCompiledInternal: <
14081423
Effect.asVoid
14091424
)
14101425

1411-
const reserveTerminalSnapshot = (
1412-
f: (
1413-
snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "active" }>
1414-
) => RuntimeSnapshot<State, Error, Output>
1415-
): Effect.Effect<RuntimeSnapshot<State, Error, Output> | undefined> =>
1416-
Effect.sync(() => {
1417-
const latest = MutableRef.get(current)
1418-
if (latest.terminalizing || latest.snapshot.status !== "active") {
1419-
return undefined
1420-
}
1421-
const reserved = {
1422-
revision: latest.revision + 1,
1423-
snapshot: f(latest.snapshot),
1424-
terminalizing: true,
1425-
changes: latest.changes
1426-
}
1427-
MutableRef.set(current, { ...latest, terminalizing: true })
1428-
return reserved.snapshot
1429-
})
1430-
14311426
const setAndPublishSnapshot = (
14321427
snapshot: RuntimeSnapshot<State, Error, Output>
14331428
): Effect.Effect<void> =>
@@ -1469,24 +1464,19 @@ const startCompiledInternal: <
14691464
)
14701465
}
14711466

1472-
const reserveStoppedSnapshot = reserveTerminalSnapshot((snapshot) => ({
1473-
status: "stopped",
1474-
state: snapshot.state
1475-
}))
1476-
1477-
const reserveFailureSnapshot = (cause: Cause.Cause<Error>) =>
1478-
reserveTerminalSnapshot((snapshot) => ({
1479-
status: "error",
1480-
state: snapshot.state,
1481-
cause
1482-
}))
1483-
1484-
const reserveSuccessSnapshot = (output: Output) =>
1485-
reserveTerminalSnapshot((snapshot) => ({
1486-
status: "done",
1487-
state: snapshot.state,
1488-
output
1489-
}))
1467+
reserveRequestedTermination = (termination) => {
1468+
const latest = MutableRef.get(current)
1469+
if (latest.terminalizing || latest.snapshot.status !== "active") {
1470+
return undefined
1471+
}
1472+
const snapshot: RuntimeSnapshot<State, Error, Output> = termination._tag === "Stopped"
1473+
? { status: "stopped", state: latest.snapshot.state }
1474+
: termination._tag === "Done"
1475+
? { status: "done", state: latest.snapshot.state, output: termination.output }
1476+
: { status: "error", state: latest.snapshot.state, cause: termination.cause }
1477+
MutableRef.set(current, { ...latest, terminalizing: true })
1478+
return snapshot
1479+
}
14901480

14911481
const terminalizeReservedStop = (
14921482
snapshot: RuntimeSnapshot<State, Error, Output>
@@ -1515,16 +1505,8 @@ const startCompiledInternal: <
15151505
return terminalizeWith(snapshot, exit, Deferred.succeed(done, output))
15161506
}
15171507

1518-
const reserveTermination = (termination: ProcessTermination) => {
1519-
switch (termination._tag) {
1520-
case "Stopped":
1521-
return reserveStoppedSnapshot
1522-
case "Done":
1523-
return reserveSuccessSnapshot(termination.output)
1524-
case "Failure":
1525-
return reserveFailureSnapshot(termination.cause)
1526-
}
1527-
}
1508+
const reserveTermination = (termination: ProcessTermination) =>
1509+
Effect.sync(() => reserveRequestedTermination!(termination))
15281510

15291511
const completeTermination = (
15301512
termination: ProcessTermination,
@@ -1540,24 +1522,6 @@ const startCompiledInternal: <
15401522
}
15411523
}
15421524

1543-
let reservedTerminationSnapshot: RuntimeSnapshot<State, Error, Output> | undefined
1544-
requestRuntimeTermination = (requested) =>
1545-
Deferred.succeed(termination, requested).pipe(
1546-
Effect.flatMap((accepted) =>
1547-
accepted
1548-
? reserveTermination(requested).pipe(
1549-
Effect.tap((snapshot) =>
1550-
Effect.sync(() => {
1551-
terminationRequested = true
1552-
reservedTerminationSnapshot = snapshot
1553-
})
1554-
),
1555-
Effect.as(true)
1556-
)
1557-
: Effect.succeed(false)
1558-
)
1559-
)
1560-
15611525
const finishRequestedTermination = (requested: ProcessTermination): Effect.Effect<void> =>
15621526
Effect.gen(function*() {
15631527
const snapshot = reservedTerminationSnapshot ?? (yield* reserveTermination(requested))
@@ -1680,26 +1644,24 @@ const startCompiledInternal: <
16801644
Effect.gen(function*() {
16811645
let observedRevision = offerRevision
16821646
while (true) {
1683-
const pending = yield* Deferred.poll(termination)
1684-
if (Option.isSome(pending)) {
1685-
return yield* finishRequestedTermination(yield* pending.value)
1647+
if (requestedTermination !== undefined) {
1648+
return yield* finishRequestedTermination(requestedTermination)
16861649
}
16871650

16881651
const exit = yield* restore(Effect.suspend(() => logic.drain!(context))).pipe(Effect.exit)
16891652
if (Exit.isFailure(exit)) {
16901653
const completed: ProcessTermination = { _tag: "Failure", cause: exit.cause }
1691-
yield* Deferred.succeed(termination, completed)
1692-
return yield* finishRequestedTermination(yield* Deferred.await(termination))
1654+
yield* requestRuntimeTermination(completed)
1655+
return yield* finishRequestedTermination(requestedTermination!)
16931656
}
16941657
if (Option.isSome(exit.value)) {
16951658
const completed: ProcessTermination = { _tag: "Done", output: exit.value.value }
1696-
yield* Deferred.succeed(termination, completed)
1697-
return yield* finishRequestedTermination(yield* Deferred.await(termination))
1659+
yield* requestRuntimeTermination(completed)
1660+
return yield* finishRequestedTermination(requestedTermination!)
16981661
}
16991662

1700-
const requested = yield* Deferred.poll(termination)
1701-
if (Option.isSome(requested)) {
1702-
return yield* finishRequestedTermination(yield* requested.value)
1663+
if (requestedTermination !== undefined) {
1664+
return yield* finishRequestedTermination(requestedTermination)
17031665
}
17041666

17051667
const continueDraining = yield* Effect.sync(() => {
@@ -1732,7 +1694,7 @@ const startCompiledInternal: <
17321694
sendEvent = (event) =>
17331695
Effect.uninterruptible(
17341696
Effect.suspend(() => {
1735-
if (compactMailbox!.closed || terminationRequested) {
1697+
if (compactMailbox!.closed || requestedTermination !== undefined) {
17361698
return Effect.fail(new StoppedError())
17371699
}
17381700
offerCompactMailbox(compactMailbox!, event)
@@ -1763,19 +1725,19 @@ const startCompiledInternal: <
17631725
return ref
17641726
}
17651727

1766-
const pendingTermination = yield* Deferred.poll(termination)
1728+
const pendingTermination = requestedTermination
17671729
const compiledRuntime: Effect.Effect<void, never, Requirements> = Effect.uninterruptibleMask((restore) =>
17681730
Effect.gen(function*() {
17691731
let requested: ProcessTermination
1770-
if (Option.isSome(pendingTermination)) {
1771-
requested = yield* pendingTermination.value
1732+
if (pendingTermination !== undefined) {
1733+
requested = pendingTermination
17721734
} else {
17731735
const exit = yield* restore(Effect.suspend(() => logic.run(context))).pipe(Effect.exit)
17741736
const completed: ProcessTermination = Exit.isFailure(exit)
17751737
? { _tag: "Failure", cause: exit.cause }
17761738
: { _tag: "Done", output: exit.value }
1777-
yield* Deferred.succeed(termination, completed)
1778-
requested = yield* Deferred.await(termination)
1739+
yield* requestRuntimeTermination(completed)
1740+
requested = requestedTermination!
17791741
}
17801742

17811743
const snapshot = reservedTerminationSnapshot ?? (yield* reserveTermination(requested))

test/MachineProcessLifecycle.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,67 @@ describe("machine process lifecycle", () => {
169169
assert.instanceOf(yield* Effect.flip(ref.join), Machine.StoppedError)
170170
}))
171171

172+
it.effect("arbitrates on-demand completion and stop exactly once", () =>
173+
Effect.gen(function*() {
174+
yield* Effect.forEach(
175+
Array.from({ length: 100 }, (_, index) => index),
176+
(index) =>
177+
Effect.gen(function*() {
178+
const started = yield* Deferred.make<void>()
179+
const release = yield* Deferred.make<void>()
180+
const race = yield* Deferred.make<void>()
181+
const cleanupCount = yield* Ref.make(0)
182+
const ref = yield* MachineRuntime.startProcess<number, void, never, never, number>({
183+
[MachineRuntime.childlessProcess]: true,
184+
[MachineRuntime.compiledProcess]: true,
185+
initial: () => Effect.succeed(index),
186+
run: () => Effect.never,
187+
drain: (context) =>
188+
Effect.gen(function*() {
189+
const event = yield* context.poll!
190+
if (Option.isNone(event)) {
191+
return Option.none()
192+
}
193+
yield* Deferred.succeed(started, undefined)
194+
return yield* Deferred.await(release).pipe(
195+
Effect.as(Option.some(index)),
196+
Effect.ensuring(Ref.update(cleanupCount, (count) => count + 1))
197+
)
198+
})
199+
})
200+
201+
yield* ref.send(undefined)
202+
yield* Deferred.await(started)
203+
const stopFiber = yield* Deferred.await(race).pipe(
204+
Effect.andThen(ref.stop),
205+
Effect.forkChild
206+
)
207+
const completionFiber = yield* Deferred.await(race).pipe(
208+
Effect.andThen(Deferred.succeed(release, undefined)),
209+
Effect.forkChild
210+
)
211+
212+
yield* Deferred.succeed(race, undefined)
213+
yield* Fiber.join(stopFiber)
214+
yield* Fiber.join(completionFiber)
215+
216+
const snapshot = yield* ref.snapshot
217+
const joined = yield* Effect.exit(ref.join)
218+
assert.strictEqual(yield* Ref.get(cleanupCount), 1)
219+
assert(snapshot.status === "done" || snapshot.status === "stopped")
220+
if (snapshot.status === "done") {
221+
assert.deepStrictEqual(joined, Exit.succeed(index))
222+
} else {
223+
assert(Exit.isFailure(joined))
224+
if (Exit.isFailure(joined)) {
225+
assert.instanceOf(joined.cause.reasons.find(Cause.isFailReason)?.error, Machine.StoppedError)
226+
}
227+
}
228+
}),
229+
{ concurrency: "unbounded" }
230+
)
231+
}))
232+
172233
it.effect("terminalizes once when concurrent callers stop the same process", () =>
173234
Effect.gen(function*() {
174235
const cleanupCount = yield* Ref.make(0)

0 commit comments

Comments
 (0)