Skip to content

Snapshot Queue.All under the lock instead of holding RLock across the yield callback#624

Open
PratikDhanave wants to merge 1 commit into
microsoft:mainfrom
PratikDhanave:snapshot-queue-all-under-lock
Open

Snapshot Queue.All under the lock instead of holding RLock across the yield callback#624
PratikDhanave wants to merge 1 commit into
microsoft:mainfrom
PratikDhanave:snapshot-queue-all-under-lock

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

What

Queue.All in internal/concurrent/queue.go previously held q.mu.RLock for the entire iteration and invoked each yield(item) (arbitrary caller code) while the read lock was still held. This change takes a snapshot of the items under the lock, releases the lock, and then yields the copied values.

Why

The method's doc states that All "corresponds to a consistent snapshot of the Queue's contents at the time iteration starts", but the implementation took no copy and kept the lock live across the callback. Because sync.RWMutex is non-reentrant:

  • Any loop body that calls Enqueue, Dequeue, or Len on the same queue deadlocks.
  • A slow consumer inside the loop blocks all writers for the duration of iteration.

Snapshotting under the lock and yielding afterwards matches the documented moment-in-time semantics and removes both hazards. This aligns with the enumeration semantics used across the .NET and Python SDKs, where enumerating a concurrent collection iterates a point-in-time view rather than pinning the collection's lock across user code. The sibling Map.All explicitly documents that it is not a snapshot; Queue.All documents that it is, so the implementation should honor that contract.

Tests

Added TestQueue_AllSnapshotAllowsMutationInLoop in the canonical queue_test.go. It enqueues items, then calls Enqueue/Dequeue on the same queue from inside for v := range q.All(), asserting under a timeout that iteration completes and yields exactly the original snapshot values. The test deadlocks (times out) against the previous code and passes after the fix. Verified with go test -race ./internal/concurrent/....

Copilot AI review requested due to automatic review settings July 23, 2026 05:45
@PratikDhanave
PratikDhanave requested a review from a team as a code owner July 23, 2026 05:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Queue.All to honor its documented “consistent snapshot at iteration start” contract by copying the queue’s contents under RLock, then releasing the lock before invoking the caller’s yield callback—eliminating lock retention across arbitrary user code and preventing writer starvation / lock-order deadlocks.

Changes:

  • Change Queue.All to snapshot q.items under RLock and iterate the snapshot after unlocking.
  • Add a regression test that mutates the same queue inside for v := range q.All() and asserts no deadlock and stable snapshot values.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
internal/concurrent/queue.go Implements snapshot iteration for Queue.All by copying under RLock and yielding after unlocking.
internal/concurrent/queue_test.go Adds a timeout-based regression test ensuring All allows reentrant mutation in the loop body and yields only the original snapshot.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@PratikDhanave
PratikDhanave force-pushed the snapshot-queue-all-under-lock branch from ce18cd4 to 03d8ba3 Compare July 23, 2026 08:20
Queue.All held the read lock for the entire iteration and ran each
yield(item) (arbitrary caller code) while the lock was live. Since
sync.RWMutex is non-reentrant, any loop body that called Enqueue,
Dequeue, or Len on the same queue deadlocked, and a slow consumer
blocked all writers. This contradicted the documented contract that
All is a consistent snapshot taken when iteration starts.

Take the snapshot under the lock, release it, then yield the copied
items.
@PratikDhanave
PratikDhanave force-pushed the snapshot-queue-all-under-lock branch from 03d8ba3 to 53c081c Compare July 23, 2026 15:47
@github-actions

Copy link
Copy Markdown
Contributor

Go API Parity Review

Result: ✅ Out of scope — no parity issues

This PR only modifies internal/concurrent/queue.go (and its test), fixing Queue.All to snapshot items under the lock rather than holding the read lock across the yield callback. Both changed files are in the internal/ package tree.

Since no exported Go APIs or user-visible behaviors are changed, this PR is outside the scope of cross-repo consistency review. No comparison with the upstream .NET or Python implementations is necessary.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

Generated by Go API Consistency Review Agent · 15.1 AIC · ⌖ 3.91 AIC · ⊞ 5.7K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants