test: use Effect runtime in runner deadlock case (#25045)

This commit is contained in:
Kit Langton
2026-04-30 12:45:30 -04:00
committed by GitHub
parent ffe0314c47
commit fef7981942
+32 -38
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test" import { describe, expect } from "bun:test"
import { Deferred, Effect, Exit, Fiber, Ref, Scope } from "effect" import { Deferred, Effect, Exit, Fiber, Ref, Scope } from "effect"
import { Runner } from "@/effect/runner" import { Runner } from "@/effect/runner"
import { it } from "../lib/effect" import { it } from "../lib/effect"
@@ -198,58 +198,52 @@ describe("Runner", () => {
}), }),
) )
test("cancel does not deadlock when replacement work starts before interrupted run exits", async () => { it.live(
function defer() { "cancel does not deadlock when replacement work starts before interrupted run exits",
let resolve!: () => void Effect.gen(function* () {
const promise = new Promise<void>((done) => { const s = yield* Scope.Scope
resolve = done const hit = yield* Deferred.make<void>()
}) const hold = yield* Deferred.make<void>()
return { promise, resolve } const done = yield* Deferred.make<void>()
}
function fail(ms: number, msg: string) { yield* Effect.gen(function* () {
return new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(msg)), ms)
})
}
const s = await Effect.runPromise(Scope.make())
const hit = defer()
const hold = defer()
const done = defer()
try {
const runner = Runner.make<string>(s) const runner = Runner.make<string>(s)
const first = Effect.never.pipe( const first = Effect.never.pipe(
Effect.onInterrupt(() => Effect.sync(() => hit.resolve())), Effect.onInterrupt(() => Deferred.succeed(hit, undefined)),
Effect.ensuring(Effect.promise(() => hold.promise)), Effect.ensuring(Deferred.await(hold)),
Effect.as("first"), Effect.as("first"),
) )
const a = Effect.runPromiseExit(runner.ensureRunning(first)) const a = yield* runner.ensureRunning(first).pipe(Effect.exit, Effect.forkChild)
await Bun.sleep(10) yield* Effect.sleep("10 millis")
const stop = Effect.runPromise(runner.cancel) const stop = yield* runner.cancel.pipe(Effect.forkChild)
await Promise.race([hit.promise, fail(250, "cancel did not interrupt running work")]) yield* Deferred.await(hit).pipe(Effect.timeout("250 millis"))
const b = Effect.runPromise(runner.ensureRunning(Effect.promise(() => done.promise).pipe(Effect.as("second")))) const b = yield* runner.ensureRunning(Deferred.await(done).pipe(Effect.as("second"))).pipe(Effect.forkChild)
yield* Effect.yieldNow
expect(runner.busy).toBe(true) expect(runner.busy).toBe(true)
hold.resolve() yield* Deferred.succeed(hold, undefined)
await Promise.race([stop, fail(250, "cancel deadlocked while replacement run was active")]) const stopExit = yield* Fiber.await(stop).pipe(Effect.timeout("250 millis"))
expect(Exit.isSuccess(stopExit)).toBe(true)
expect(runner.busy).toBe(true) expect(runner.busy).toBe(true)
done.resolve() yield* Deferred.succeed(done, undefined)
expect(await b).toBe("second") expect(yield* Fiber.join(b).pipe(Effect.timeout("250 millis"))).toBe("second")
expect(runner.busy).toBe(false) expect(runner.busy).toBe(false)
const exit = await a const exit = yield* Fiber.join(a)
expect(Exit.isFailure(exit)).toBe(true) expect(Exit.isFailure(exit)).toBe(true)
} finally { }).pipe(
hold.resolve() Effect.ensuring(
done.resolve() Effect.all([Deferred.succeed(hold, undefined), Deferred.succeed(done, undefined)], { discard: true }).pipe(
await Promise.race([Effect.runPromise(Scope.close(s, Exit.void)), fail(1000, "runner scope did not close")]) Effect.ignore,
} ),
}) ),
)
}),
)
// --- shell semantics --- // --- shell semantics ---