Skip to content

Read snapshot artifacts via async fsspec API - #21

Merged
jbothma merged 3 commits into
mainfrom
async-snapshot-fs
Aug 4, 2026
Merged

Read snapshot artifacts via async fsspec API#21
jbothma merged 3 commits into
mainfrom
async-snapshot-fs

Conversation

@jbothma

@jbothma jbothma commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What

Read Pravda snapshot artifacts through the async fsspec API instead of the synchronous fs.open().

Why

A kolkhoz-manual job on GKE crashed in the extraction phase reading artifacts back from the gs:// bucket:

RuntimeError: Task <GCSFileSystem._get_object()...> got Future <Future pending> attached to a different loop

Captures themselves succeeded — the crash was on the first read_artifact. Root cause is an event-loop mismatch on a shared, fsspec-cached gcsfs instance:

  • During capture, Pravda awaits gcsfs's async methods on the pipeline's asyncio.run loop, binding gcsfs's aiohttp session to that loop.
  • During extraction, read_artifact called the synchronous fs.open(), which fsspec dispatches to its own background loop — reusing the session bound to the pipeline loop from a different loop.

It only manifests on a remote async backend: the local (LocalFileSystem) path is synchronous, so dev/tests never hit it — which is why this reached the cluster.

Change

  • storage_filesystem: wrap synchronous backends in AsyncFileSystemWrapper, mirroring Pravda's own Storage.from_url, so reads use the async API on every backend.
  • read_artifact: now async, reads via await fs._cat_file(path); the three call sites in extract_snapshot await it.

This keeps all storage I/O on the running event loop, so Pravda's writes and Kolkhoz's reads share one session.

Testing

  • ruff check clean.
  • Verified the wrapped local FS and native gcsfs both expose an awaitable _cat_file.
  • ⚠️ Not yet run end-to-end against real gs:// on GKE — the definitive check is a kolkhoz-manual job run. Recommend confirming there before relying on it.

🤖 Generated with Claude Code

jbothma and others added 3 commits August 3, 2026 17:35
Reading artifacts with the synchronous fs.open() crashed on the gs://
backend with "RuntimeError: got Future attached to a different loop".
Pravda binds gcsfs's aiohttp session to the pipeline event loop by
awaiting its async API during capture; the subsequent sync fs.open()
dispatched the same cached filesystem instance's coroutine to fsspec's
background loop, mixing loops on one session.

Wrap synchronous backends in AsyncFileSystemWrapper (as Pravda's
Storage.from_url does) and read via `await fs._cat_file()`, keeping all
storage I/O on the running loop. Local (sync) backends are unaffected;
this only manifested on remote async storage in the cluster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The read-side fix moved the pipeline past capture and extraction, but it
then crashed identically ("got Future attached to a different loop") in
write_outputs, which used the synchronous fs.makedirs()/fs.open() on the
gs:// backend from inside the pipeline event loop.

Give write_outputs the same treatment as read_artifact: wrap sync
backends and write via `await fs._makedirs()` / `await fs._pipe_file()`,
keeping all storage I/O on the running loop. load_inputs is unaffected —
it runs before asyncio.run(), outside the pipeline loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jbothma
jbothma merged commit c2d5f74 into main Aug 4, 2026
1 check passed
@jbothma
jbothma deleted the async-snapshot-fs branch August 4, 2026 14:11
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.

1 participant