Skip to content

Commit 09267d8

Browse files
Let Effect schedule machine event loops
1 parent 3848b9b commit 09267d8

3 files changed

Lines changed: 128 additions & 5 deletions

File tree

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+
Let the Effect runtime scheduler control cooperative yielding while draining machine event bursts, preserving runtime scheduler configuration and avoiding a forced scheduler turn after every event.

src/internal/machineProcess.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ const makeProcessLogic: <
173173
const current = yield* state
174174
const planned = yield* internalPlanner.plan(machine, current, event)
175175
if (planned.microsteps.length === 0) {
176-
return yield* Effect.yieldNow
176+
return
177177
}
178178

179179
yield* internalPlanner.runActions(planned.actions, liveRuntime)
@@ -185,8 +185,6 @@ const makeProcessLogic: <
185185

186186
if (planned.done) {
187187
terminal = { output: planned.output }
188-
} else {
189-
yield* Effect.yieldNow
190188
}
191189
}),
192190
step: () => undefined
@@ -409,7 +407,7 @@ const makeProcessLogic: <
409407
const current = yield* state
410408
const planned = yield* internalPlanner.plan(machine, current, event)
411409
if (planned.microsteps.length === 0) {
412-
return yield* Effect.yieldNow
410+
return
413411
}
414412
const changed = planned.microsteps.some((step) => step.changed)
415413
const exitPaths = planned.microsteps.flatMap((step) => step.exitPaths)
@@ -441,7 +439,6 @@ const makeProcessLogic: <
441439
yield* startInvokes(planned.next, [path], entryEvent)
442440
}
443441
}
444-
yield* Effect.yieldNow
445442
}
446443
}),
447444
step: () => undefined

test/MachineScheduling.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { assert, describe, it } from "@effect/vitest"
2+
import { Deferred, Effect, Fiber, Ref, References, Schema } from "effect"
3+
import { Machine } from "../src/index.js"
4+
5+
class SchedulingActive extends Schema.TaggedClass<SchedulingActive>("SchedulingActive")(
6+
"SchedulingActive",
7+
{}
8+
) {}
9+
10+
class StartBurst extends Schema.TaggedClass<StartBurst>("StartBurst")("StartBurst", {}) {}
11+
12+
class Burst extends Schema.TaggedClass<Burst>("Burst")("Burst", {}) {}
13+
14+
class ChildPing extends Schema.TaggedClass<ChildPing>("ChildPing")("ChildPing", {}) {}
15+
16+
const burstSize = 1_024
17+
const schedulerOperationBudget = 64
18+
19+
const withFrequentSchedulerYields = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
20+
effect.pipe(Effect.provideService(References.MaxOpsBeforeYield, schedulerOperationBudget))
21+
22+
describe("machine scheduling", () => {
23+
it.effect("lets the Effect scheduler run a concurrent stop during a sustained event burst", () =>
24+
withFrequentSchedulerYields(Effect.gen(function*() {
25+
const entered = yield* Deferred.make<void>()
26+
const release = yield* Deferred.make<void>()
27+
const processed = yield* Ref.make(0)
28+
const states = Machine.defineStates({ SchedulingActive })
29+
const machine = Machine.make({
30+
states: states.states,
31+
events: [StartBurst, Burst],
32+
initial: () => states.initial.SchedulingActive(new SchedulingActive({}))
33+
}).handle({
34+
SchedulingActive: {
35+
on: {
36+
StartBurst: () =>
37+
Machine.action(
38+
Deferred.succeed(entered, void 0).pipe(
39+
Effect.andThen(Deferred.await(release))
40+
)
41+
),
42+
Burst: () => Machine.action(Ref.update(processed, (count) => count + 1))
43+
}
44+
}
45+
})
46+
const ref = yield* Machine.start(machine)
47+
48+
yield* ref.send(new StartBurst({}))
49+
yield* Deferred.await(entered)
50+
yield* Effect.forEach(
51+
Array.from({ length: burstSize }),
52+
() => ref.send(new Burst({})),
53+
{ discard: true }
54+
)
55+
const stopFiber = yield* Deferred.await(release).pipe(
56+
Effect.andThen(ref.stop),
57+
Effect.forkChild
58+
)
59+
yield* Effect.yieldNow
60+
yield* Deferred.succeed(release, void 0)
61+
yield* Fiber.join(stopFiber)
62+
63+
assert.isBelow(yield* Ref.get(processed), burstSize)
64+
assert.strictEqual((yield* ref.snapshot).status, "stopped")
65+
})))
66+
67+
it.effect("lets the Effect scheduler run an invoked child during a sustained parent event burst", () =>
68+
withFrequentSchedulerYields(Effect.gen(function*() {
69+
const entered = yield* Deferred.make<void>()
70+
const release = yield* Deferred.make<void>()
71+
const processed = yield* Ref.make(0)
72+
const childObservedAt = yield* Deferred.make<number>()
73+
const Child = Machine.childAddress<ChildPing>("scheduler-child")
74+
const childLogic = Machine.logic<void, ChildPing, never>({
75+
initial: undefined,
76+
run: ({ receive }) =>
77+
receive.pipe(
78+
Effect.andThen(Ref.get(processed)),
79+
Effect.flatMap((count) => Deferred.succeed(childObservedAt, count)),
80+
Effect.andThen(Effect.never)
81+
)
82+
})
83+
const states = Machine.defineStates({ SchedulingActive })
84+
const machine = Machine.make({
85+
states: states.states,
86+
events: [StartBurst, Burst],
87+
initial: () => states.initial.SchedulingActive(new SchedulingActive({}))
88+
}).handle({
89+
SchedulingActive: {
90+
invoke: Machine.invoke({
91+
id: "scheduler-child",
92+
address: Child,
93+
src: () => childLogic
94+
}),
95+
on: {
96+
StartBurst: () =>
97+
Machine.action(
98+
Deferred.succeed(entered, void 0).pipe(
99+
Effect.andThen(Deferred.await(release)),
100+
Effect.andThen(Machine.sendTo(Child, new ChildPing({})))
101+
)
102+
),
103+
Burst: () => Machine.action(Ref.update(processed, (count) => count + 1))
104+
}
105+
}
106+
})
107+
const ref = yield* Machine.start(machine)
108+
109+
yield* ref.send(new StartBurst({}))
110+
yield* Deferred.await(entered)
111+
yield* Effect.forEach(
112+
Array.from({ length: burstSize }),
113+
() => ref.send(new Burst({})),
114+
{ discard: true }
115+
)
116+
yield* Deferred.succeed(release, void 0)
117+
118+
assert.isBelow(yield* Deferred.await(childObservedAt), burstSize)
119+
yield* ref.stop
120+
})))
121+
})

0 commit comments

Comments
 (0)