effect icon indicating copy to clipboard operation
effect copied to clipboard

FiberHandle.clear looks racy ?

Open YKX-A opened this issue 2 weeks ago • 0 comments

What version of Effect is running?

3.19.11

What steps can reproduce the bug?

See: https://github.com/Effect-TS/effect/blob/fb78c4061cec89718c49f842a91263a9bb8cf3cf/packages/effect/src/FiberHandle.ts#L317-L321

export const clear = <A, E>(self: FiberHandle<A, E>): Effect.Effect<void> =>
  Effect.uninterruptibleMask((restore) =>
    Effect.withFiberRuntime((fiber) => {
      if (self.state._tag === "Closed" || self.state.fiber === undefined) {
        return Effect.void
      }

      return Effect.zipRight(
        // This can suspend while waiting for the target fiber to actually terminate,
        // which opens a window for another fiber to call FiberHandle.run/set.
        restore(Fiber.interruptAs(self.state.fiber, FiberId.combine(fiber.id(), internalFiberId))),
        Effect.sync(() => {
          // If another fiber stored a new fiber into the handle while we were waiting above,
          // this unconditional write can wipe out the newer reference.
          if (self.state._tag === "Open") {
            self.state.fiber = undefined
          }
        })
      )
    })
  )

clear interrupts the fiber currently stored in the handle and then waits for it to finish. While it’s waiting, another fiber can call FiberHandle.run / set and put a new fiber into the handle. When clear continues, it blindly does state.fiber = undefined, which can wipe out the newer fiber reference.

So the handle can appear empty (e.g. unsafeGet returns None / get fails) even though the newer fiber is still running, meaning the handle has lost the reference and can no longer cancel/manage that fiber.

perhaps a fix like this:

export const clear = <A, E>(self: FiberHandle<A, E>): Effect.Effect<void> =>
  Effect.uninterruptibleMask((restore) =>
    Effect.withFiberRuntime((runtimeFiber) => {
      if (self.state._tag === "Closed" || self.state.fiber === undefined) {
        return Effect.void
      }

      const target = self.state.fiber

      return Effect.zipRight(
        restore(Fiber.interruptAs(target, FiberId.combine(runtimeFiber.id(), internalFiberId))),
        Effect.sync(() => {
          if (self.state._tag === "Open" && self.state.fiber === target) {
            self.state.fiber = undefined
          }
        })
      )
    })
  )

What is the expected behavior?

No response

What do you see instead?

No response

Additional information

No response

YKX-A avatar Dec 15 '25 02:12 YKX-A