Repository navigation
Stream missing annexed content with remfile, cached by annex key - #1955
Draft
CodyCBakerPhD wants to merge 15 commits into
Draft
CodyCBakerPhD wants to merge 15 commits into
CodyCBakerPhD wants to merge 15 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1955 +/- ##
==========================================
+ Coverage 90.83% 91.16% +0.33%
==========================================
Files 92 96 +4
Lines 14190 14648 +458
==========================================
+ Hits 12889 13354 +465
+ Misses 1301 1294 -7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
CodyCBakerPhD
added this pull request to stack #1959
October 12, 2026 21:30
CodyCBakerPhD
force-pushed
the
claude/datalad-fuse-readable
branch
2 times, most recently
from
October 15, 2026 17:58
2d461dd to
608f3ab
Compare
CodyCBakerPhD
removed this pull request from stack #1959
October 15, 2026 18:34
CodyCBakerPhD
added this pull request to stack #1965
October 15, 2026 19:28
Add `dandi.support.datalad_fuse.AnnexedReadableFile`, a `Readable` for a locked annexed file whose content is not present locally, streamed with the adapter of datalad-fuse (no FUSE mount), which asks git-annex where the content is. It is available with the new `datalad` extra. Cache NWB metadata of locked annexed files, streamed or local, under their git-annex key paired with their path, with fscacher 0.5.0's `annex_key_fingerprint`. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
annex_fingerprint is not specific to NWB files, and living in dandi.support.datalad_fuse it needs no deferred import. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
As documented in datalad/datalad-fuse#138. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
fscacher imports joblib, which imports numpy, so importing it with the module made the CLI (via dandi.validate in the stacked PR) import numpy, failing test_no_heavy_imports. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
h5py holds its global lock while reading from a Python file object, and the read waits for another thread to fetch the data over HTTP (fsspec's I/O thread, or an HTTP server in the same process as in the tests). A garbage collection in that thread could then run the finalizer of an unreferenced h5py-backed object (e.g., hdmf's HDF5IO.__del__), which waits for the same lock: a deadlock, as seen in CI in test_validate_stream, ended by pytest-timeout with a segfault. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
datalad-fuse opens the files with fsspec's defaults: 5 MiB of read-ahead, discarded whenever a read jumps elsewhere. h5py reads many small pieces scattered over an NWB file, so each open downloaded about as much as the whole file (64 MB for the neurodata types of a 16 MB file in the new test), and dandi opens each file several times during validation. Read in cached blocks of 256 KiB instead (1.3 MB for the same file). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
fsspec's "blockcache" fetches each block of a read with its own request, so a large read (e.g., of a dataset's data, by NWB Inspector) took one round trip per 256 KiB, i.e., a couple of MB/s however fast the connection. Use instead a block cache that fetches each run of consecutive missing blocks with a single request, and keeps up to 512 blocks (128 MiB) per open file rather than 32, which h5py's scattered reads of metadata kept evicting and fetching again. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
Rather than through datalad-fuse's adapter (and fsspec), the content of a locked annexed file whose content is missing is now streamed by dandi with remfile, from the URLs git-annex knows for its key: - The key, and the size of the content, are taken from the target of the (broken) symbolic link, and the URLs from `git annex whereis --json --key`, run only in repositories in which git-annex is initialized (as any `git annex` command would initialize it). - The content is read from the first URL answering a range request, with remfile, which reads in the calling thread, in requests sized for HDF5's access pattern: reading the structure of a 16 MB NWB file downloads 0.6 MB. No fsspec I/O thread means no deadlock with h5py's lock, so garbage collection is no longer paused (`_GCPausedFile`), and the coalescing fsspec cache is gone. - The module is renamed `dandi.support.annex_stream`, and the `datalad` extra (datalad-fuse) is replaced by a `streaming` extra (remfile); the CI job is renamed accordingly. - The tests' HTTP server runs in a separate process (dandi.tests. range_http_server), so that no thread of the tests' process serving requests can run h5py finalizers while h5py's lock is held. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
…rver The HTTP server for streaming tests now exits normally when stopped, so that coverage saves the data of its process. Test that annexed content is not streamed when it cannot be (no remfile, not annexed content, no size in the key, no URL), and that URLs which fail to connect are skipped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
…ckend Rather than dandi reading the keys and URLs of annexed files from git-annex and streaming them with remfile itself, the content is again streamed with datalad-fuse's adapter, now that it has a remfile backend and can be told not to initialize git-annex (datalad/datalad-fuse#131, and CodyCBakerPhD/datalad-fuse's remfile-dev branch, which CI installs until those are released): - The adapter (RemoteFilesystemAdapter, with caching=False and init_annex=False) gives the state and key of a file, the URLs git-annex knows for it, and opens it from the first URL that can be read, with remfile for HDF5-based files such as NWB. dandi only wraps the file object it returns as a raw binary file, for h5py. - git-annex being initialized is still checked first (`git config annex.uuid`), so that a DataLad dataset is not loaded for each broken symbolic link of a fresh clone. - The `streaming` extra is again a `datalad` extra, requiring datalad-fuse[remfile], and the CI job is named `datalad-fuse`. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
…ackend Until datalad-fuse releases its remfile backend and the adapter's `init_annex` option (datalad/datalad-fuse#131), the `datalad` extra requires them from CodyCBakerPhD/datalad-fuse at a fixed commit, so that `pip install "dandi[datalad]"` gives working streaming, and CI installs just `.[datalad]`. This must be replaced by a release requirement before merging, as PyPI rejects distributions with direct URL dependencies. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
Pass `backends="remfile,fsspec"` to the adapter rather than leaving it to a dataset's `datalad.fusefs.backends` configuration: a dataset configured to use fsspec would otherwise have NWB files read by fsspec, which fetches in another thread, where a garbage collection running h5py finalizers while h5py's lock is held waiting for the data would deadlock. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
Pin the datalad extra to the head commit of datalad/datalad-fuse#131 rather than a fork combining it with an init_annex option of the adapter: dandi hands files to the adapter only in repositories in which git-annex is initialized, so the adapter never initializes it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
h5py holds its global lock while it reads from a Python file object, and a read of a RemoteReadableAsset waits for fsspec's IO thread to fetch the data. If a garbage collection runs in that thread meanwhile, the finalizer of an unreferenced object closing an HDF5 file (e.g., hdmf's HDMFIO.__del__ -> HDF5IO.close() -> h5py's File.close()) waits for the same lock, and neither thread can proceed. The aiohttp timeouts cannot end this, as the event loop thread itself is blocked. This is the hang of test_nwb2asset_remote_asset (#1450), and it made test_reextract_metadata time out in the lowest-deps CI job of #1955. RemoteReadableAsset.open() now wraps fsspec's file object in a _GCPausedFile, which disables automatic garbage collection while any such file is open (and restores the previous state once all are closed). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
CodyCBakerPhD
force-pushed
the
claude/datalad-fuse-readable
branch
from
October 16, 2026 03:23
478fb93 to
b75c824
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR and the one stacked on it (#1956) are an alternative to #1941 and #1933. The goal is the same:
dandi validateon a DataLad clone of a Dandiset, streaming file content without downloading it. Here, dandi leaves the git-annex and HTTP work to datalad-fuse's adapter, and datalad-fuse reads NWB/HDF5 content with remfile.Changes
dandi.support.annex_stream.AnnexedReadableFileis aReadablefor a locked annexed file whose content isn't present locally.RemoteFilesystemAdapter, used directly with no FUSE mount. One adapter is shared and closed at exit.caching=False: nothing is written into the clone.backends="remfile,fsspec": NWB/HDF5 files are read with remfile even if a dataset configuresdatalad.fusefs.backendsotherwise.git annex whereis, in the order git-annex lists them. For a DataLad Dandiset that is the DANDI Archive's API download URL, which redirects to S3, then the direct S3 URL;.nwb/.h5files go through datalad-fuse's remfile backend.io.RawIOBasewrapper around the file object the adapter returns, so it works with h5py (which reads withreadinto()) and pynwb.get_annexed_readable(path)returnsNonewhen:git annex init. Before handing a file to datalad-fuse, dandi checksgit config annex.uuid. So the adapter, which would otherwise rungit annexcommands that initialize it, is never used in such a clone. This also avoids loading a DataLad dataset for every broken symlink of a freshgit clone. The tests check that a fresh clone keeps no annex UUID.get_metadata,get_neurodata_typesandnwb_has_external_linksuseannex_fingerprintas theircustom_fingerprint.annex_key_fingerprint, which keys on the path paired with the annex key.WORMandURLkeys fall back tostat().Readable.dataladextra: datalad-fuse with remfile, for now pinned to a commit (see below). git-annex is also required.fscacher >= 0.5.0.make_git_annex_dandiset()builds a real git-annex repository: content added, one or more URLs registered, then dropped.range_http_serverfixture serves files with range requests from a separate process (dandi/tests/range_http_server.py). No thread of the test process serves requests, so a garbage collection there can't run h5py finalizers while h5py's lock is held waiting for a response. The server exits normally when stopped, so coverage records its process.seek/read/end-of-file behavior;skipif.no_git_annexcondition.mode: datalad-fuse, ubuntu, Python 3.12) installsgit-annexand.[datalad]. The other jobs skip these tests.Depends on unreleased datalad-fuse
This uses the remfile backend and pluggable backends of datalad/datalad-fuse#131, which datalad-fuse hasn't released yet. It works with that PR as it is. Until it is released, the
dataladextra pins the PR's head commit in datalad/datalad-fuse:"datalad-fuse[remfile] @ git+https://github.com/datalad/datalad-fuse@b4dbee656eeb8ed54b1396c8d7e5c8b369b64a4a"With the pin,
pip install "dandi[datalad]"gives working streaming, and CI just installs.[datalad].Before merging, the pin must be replaced by a requirement on the first datalad-fuse release with #131. PyPI rejects distributions with direct URL dependencies, so a release of dandi with the pin would fail to upload.
With a released datalad-fuse that lacks #131, nothing breaks:
get_annexed_readable()returnsNoneand the content is reported as missing.An
init_annexoption of the adapter (datalad/datalad-fuse#141), to stop it from initializing git-annex in an uninitialized clone, would be useful to other users of the adapter, but dandi doesn't need it: it checksannex.uuiditself.Why datalad-fuse's remfile backend
An earlier version of this PR streamed through datalad-fuse's fsspec adapter. That needed three workarounds in dandi:
The garbage collection pause was needed because fsspec fetches data in a separate I/O thread. If a collection runs in that thread while h5py holds its global lock waiting for the data, any finalizer that calls h5py deadlocks. A patched h5py wasn't enough: hdmf 6.2.0's
HDMFIO.__del__callsis_open(), which waits for the same lock.remfile fetches data in the reading thread, so with datalad-fuse's remfile backend none of these workarounds is needed. dandi keeps no streaming code of its own beyond a file-object wrapper.
Compared to #1941
746 added lines, 257 excluding tests and the test server, against #1941's 990. It drops
AnnexKey,AnnexRepo, the URL-log parsing, the fake-annex fixtures and theReadable.get_fingerprinthook. In exchange, streaming requires git-annex and datalad-fuse with remfile.🤖 Generated with Claude Code
https://claude.ai/code/session_01Qn5WBSiQgoZoL4fytF6nEr
Generated by Claude Code