Skip to content

fix: detach from the interpreter while creating a list stream - #787

Merged
kylebarron merged 2 commits into
mainfrom
kyle/fix-list-deadlock
Sep 29, 2026
Merged

kylebarron merged 2 commits into
mainfrom
kyle/fix-list-deadlock

Conversation

@kylebarron

@kylebarron kylebarron commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Note

This PR was written by Claude (Claude Code), not by @kylebarron.

obs.list() deadlocks when another thread calls put() or delete() on the same MemoryStore. This PR creates the list stream inside py.detach, as list_with_delimiter() already does.

Closes #785

Cause

list() called store.list(...) while still attached to the interpreter, and InMemory::list takes the store's read lock right away. Meanwhile, put() keeps the caller's buffer zero-copy, and InMemory::put_opts drops the value it replaces while holding the write lock. Dropping that PyBuffer has to re-attach so it can release the buffer.

A stack sample of the hung process shows both sides:

  • lister: _obstore::list::list → InMemory::list → RwLock::read, parked while holding the GIL
  • writer: InMemory::put_opts → PyUntypedBuffer::drop → Python::try_attach → take_gil, parked while holding the write lock

On free-threaded builds, the attached lister instead blocks a pending stop-the-world (for example, gc.collect()), and the writer's attach waits on that stop-the-world.

list() was the only top-level function that called into the store while attached. Every other sync entry point already runs its store call inside py.detach.

Verification

Local results (macOS arm64, debug build) with the issue's reproduction script (20k iterations):

unfixed fixed
3.14 hangs 3/3 ~2.3 s, 3/3
3.14t hangs 3/3 ~1.9 s, 3/3

🤖 Written by Claude Code

`list()` created the stream while still attached. `MemoryStore` takes its
read lock at that point, and a concurrent `put()` or `delete()` releases
the replaced Python buffer while holding the write lock, which needs to
re-attach. On GIL builds the lister holds the GIL while waiting for the
lock; on free-threaded builds it blocks a pending stop-the-world. Either
way both threads wait on each other forever.

Create the stream inside `py.detach`, as `list_with_delimiter()` already
does. The regression test runs in a subprocess because on failure the
stuck lister holds the GIL and would hang the test process itself.

Closes #785

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ds-release-bot ds-release-bot Bot added the fix label Sep 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kylebarron
kylebarron enabled auto-merge (squash) September 29, 2026 23:16
@kylebarron
kylebarron merged commit 62f8487 into main Sep 29, 2026
10 checks passed
@kylebarron
kylebarron deleted the kyle/fix-list-deadlock branch September 29, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

list() deadlocks against a concurrent put() on one MemoryStore

1 participant