Overview

This PR addresses two critical race conditions in ZIO that can cause data loss and unexpected interruption behavior in high-concurrency scenarios:

  • Issue #9973: Queue race condition where items could disappear during interrupted take operations
  • Issue #9974: Interruption gap in uninterruptibleMask where interruption could occur at the boundary between async operations and their continuations

Queue Race Condition Fix (#9973)

Problem

Items could disappear when Queue.take operations were interrupted during concurrent offer operations. This happened when:

  1. A fiber calls queue.take() on an empty queue, creating a promise and adding it to the takers deque
  2. Another fiber calls queue.offer(item), polls the promise from takers, and attempts to complete it
  3. The take fiber gets interrupted and removes its promise from takers
  4. The offer fiber completes the promise, but since the take fiber is already interrupted, the item is lost

Root Cause

The unsafeCompletePromise method ignored the return value of Promise.unsafe.done(), which indicates whether promise completion was successful (returns false if the promise was already completed due to interruption).

Solution

  • Modified unsafeCompletePromise to return a Boolean indicating completion success
  • Updated offer, offerAll, and unsafeCompleteTakers methods to check promise completion success
  • If completion fails (due to interruption), items are put back into the queue
  • Added comprehensive test cases to verify the fix

Interruption Gap Fix (#9974)

Problem

There was an interruption “gap” in uninterruptibleMask where interruption could occur at the boundary between restore and subsequent operations like flatMap, even when the restored operation itself was uninterruptible.

This happened when:

  1. uninterruptibleMask sets the fiber to uninterruptible mode
  2. restore temporarily restores interruptibility for the inner operation
  3. asyncMaybe or asyncInterrupt returns an immediate result instead of suspending
  4. The gap: After the async operation returns the immediate result, but before the continuation is processed, there’s a window where interruption can occur
  5. The flatMap continuation should continue with the result, but if interruption happens in the gap, the continuation is lost

Root Cause

In FiberRuntime.runLoop, after initiateAsync returns an immediate result from asyncMaybe/asyncInterrupt, the code checks for interruption before processing the continuation:

if (shouldInterrupt()) {
cur = Exit.failCause(getInterruptedCause())
}

This check happens after getting the immediate result but before continuing with the next operation, creating the interruption gap.

Solution

  • Capture the interruptibility status before calling initiateAsync
  • Only check for interruption if the fiber was interruptible when the async operation started
  • This preserves the uninterruptible semantics even when async operations return immediate results
  • Added test case to verify uninterruptibleMask semantics are preserved

Changes Made

Core Implementation

  • core/shared/src/main/scala/zio/Queue.scala: Queue race condition fix
  • core/shared/src/main/scala/zio/internal/FiberRuntime.scala: Interruption gap fix

Test Coverage

  • core-tests/shared/src/test/scala/zio/QueueSpec.scala: Added tests for Queue race conditions
  • core-tests/shared/src/test/scala/zio/ZIOSpec.scala: Added test for interruption gap fix

Compatibility and Impact

Both fixes:

  • ✅ Are internal implementation changes with no breaking API changes
  • ✅ Maintain all existing behavior and performance characteristics
  • ✅ Include comprehensive test coverage
  • ✅ Are compatible with each other and don’t introduce regressions
  • ✅ Follow ZIO project conventions

Testing

The fixes include comprehensive test cases that:

  • Reproduce the original race conditions described in both issues
  • Verify that the fixes resolve the problems
  • Ensure no regressions are introduced
  • Test edge cases and concurrent scenarios

Closes #9973 Closes #9974


/claim #9973

Claim

Total prize pool $1,000
Total paid $0
Status Pending
Submitted July 28, 2025
Last updated July 28, 2025

Contributors

VI

Vishwanath Martur

@vishwamartur

100%

Sponsors

ZI

ZIO

@ZIO

$1,000