Skip to content

Comprehensive audit: 20 proven bugs fixed, HTTP/2 enabled, 3-10x performance gains - #88

Merged
TheGreatAlgo merged 100 commits into
mainfrom
review-fixes-integration
Jul 21, 2026
Merged

Comprehensive audit: 20 proven bugs fixed, HTTP/2 enabled, 3-10x performance gains#88
TheGreatAlgo merged 100 commits into
mainfrom
review-fixes-integration

Conversation

@Faolain

@Faolain Faolain commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Comprehensive audit: 20 proven bugs fixed, HTTP/2 enabled, 3–10x performance gains

This PR is the output of a full library audit followed by a strict TDD pipeline. Every item below was implemented as a pair of commits — a red commit with a failing test that reproduces the defect, then a green commit with the fix — so reviewers can check out any red commit and watch the test fail. Every fix was then reviewed by two independent reviewers (Codex implemented under a verifying harness; Grok and an adversarial Claude reviewer audited each diff, with blocking findings driving remediation commits).

Results: 215 tests pass (up from 142 on main), ruff/format clean, mypy improved (7 errors vs 8 pre-existing). Two silent data-corruption bugs in ShardedZarrStore are fixed, set() is now failure-atomic, HTTP/2 is enabled (closes the investigation in #73, resolves #79 by staying on httpx), and bulk writes are ~10x faster than main.

Benchmarks (same machine, 20k-key bulk write + flush, InMemoryCAS)

Tree sets/s
main (e932534) 5,973
This PR 59,506

Reads: ~3x faster hashing (P1), ~30x faster sharded warm gets (P4), instrumentation overhead when disabled reduced from 18–22% to ≈0 (P2/P5 measurements below per item).


1. Core HAMT (py_hamt/hamt.py)

Fix Issue What was wrong → what changed
4e418cb H2 enable_write() while already writable replaced the buffer store unconditionally, orphaning root_node_id and destroying all buffered data. Now an idempotent no-op inside the lock.
07aa9db H3 A set() that failed mid-bucket-split (hash bits exhausted with short hash functions) destroyed previously committed keys: the parent bucket was replaced with a link to an empty node before evicted KVs were re-inserted. set() is now atomic on failure — prior state stays fully readable.
2c99398 H4 keys() held the non-reentrant asyncio lock across yields; calling get/set/delete inside the iteration deadlocked silently forever. Iteration now snapshots under the lock and yields lock-free.
972bb97 H5 InMemoryTreeStore.load() added every CAS-loaded node to the write buffer under a fresh uuid nobody could look up: unbounded memory growth on pure reads and a 0% cache hit rate. Loads are now cached by CAS id with dirty/clean separation.
6c9c652 H1 Deletes never merged under-full children back into parent buckets, so identical key-value sets reached different root CIDs depending on history — defeating content-addressed dedup (the determinism goal of #21). Deletes now canonicalize the path; a hypothesis property test asserts op-order-independent root CIDs. Note: root CIDs of post-delete trees that were previously non-canonical change (they were the bug).
f7864d3 P1 blake3_hashfn spent ~99% of its time in multiformats wrap/unwrap validation (~134µs vs 0.5µs raw). Calls blake3 directly; golden-digest tests prove byte-identical output (CIDs unchanged). 3.7x write / 3.2x read.
52596d2 P2 _reserialize_and_link located children via O(256) scans per level (6.2M get_link calls per 50k writes) and re-hashed keys it already hashed. Descent path is now threaded through for O(1) relinking; hashes computed once. Structural call-count test + golden root CIDs.
b0d8ff6 P5 vacate() flushed buffered nodes one await at a time. Sibling subtrees now flush concurrently (children strictly before parents); 30x faster flush at 20ms simulated RTT, byte-identical root CIDs.
bf197aa + 012fe0c H6 H3's original rollback deepcopied every path node per set() (−40% throughput net of P1/P2). Rewritten to build/validate-before-mutate: overflow subtrees are constructed in detached buffer nodes so the only failure point fires before any existing node is mutated; happy path does zero deepcopies. Adversarial review ran differential CID tests across 7 workloads with hundreds of injected failures — byte-identical trees, zero atomicity holes. Result: 3,625 → 59,506 sets/s.

2. Zarr stores (sharded_zarr_store.py, zarr_hamt_store.py, converter, encryption)

Fix Issue What was wrong → what changed
ed78667 Z1 (data corruption) The shard index ignored the array name: temp/c/0/0/0 and precip/c/0/0/0 mapped to the same slot — last write won, reads returned the other variable's data with no error. Each array now gets its own index space under a versioned root format; stores written by current main remain readable (missing version ⇒ legacy layout). The HAMT→sharded converter builds per-array indexes and round-trips multi-variable datasets.
a06afdb Z2 (data corruption) resize_store recomputed geometry without remapping existing row-major entries: growing any non-first dimension reflowed the index and scrambled all previously written chunks (append_dim="lon" silently corrupted data; time-appends only worked by coincidence). Resizes now remap old→new linear indices through chunk coordinates; only the primary array's metadata may trigger a resize.
23b29f6 Z3 Shards entered the LRU cache clean and unprotected, so the eviction loop could evict a shard during its own load (a plain read raised RuntimeError in a 6-op sequential sequence), and the clean-until-mark_dirty window could silently drop acknowledged writes at flush. Shards are now pinned while in use and marked dirty atomically with mutation; the cache overflows gracefully instead of raising.
69326ef + e9d5956 Z4 Chunk-key classification used a hardcoded coordinate-name allowlist (time, lat, lon, …); any other coordinate name (y, x, level, station) crashed writes or fell into the Z1 collision. Keys are now classified structurally against the store's actual primary array — the allowlist is gone. (Remediation commit fixes a legacy-manifest regression the adversarial reviewer proved with a failing script.)
d9e13d8 Z5 ZarrHAMTStore.delete() skipped the metadata read-cache purge (it was commented out), so get() resurrected deleted keys while exists() said False. Cache is purged on delete; the encrypted subclass is covered by the same tests.
791654d Z6 zarr Store contract violations: metadata get() rejected byte ranges, list()/list_prefix() never yielded chunk keys, list_dir() raised NotImplementedError, and root-level arrays silently bloated the root object. All implemented/made explicit with contract tests.
790f5a4 P4 Every chunk get/set round-tripped CIDs through base32 strings (~166µs/op, ~75% of warm-get CPU). CID objects now flow end-to-end (public string API preserved): warm gets 435µs → 14µs, +30% full-read throughput.

3. HTTP layer / KuboCAS (store_httpx.py) — includes #73 / #79

Fix Issue What was wrong → what changed
7810a0e K4 (data corruption) load() sent Range headers but never checked for a 206: a gateway that ignores Range returned the full body, which flowed as-if-ranged straight into zarr buffers. Responses are now validated (200 bodies sliced locally); zero-length/zero-suffix requests return b"" instead of emitting invalid bytes=-0 headers, consistently across KuboCAS and InMemoryCAS.
f0d0727 K6 Retry policy was inverted: transient 500/502/503/504/429 were never retried (Retry-After ignored) while permanently dead endpoints were retried with full backoff and re-raised as a misleading TimeoutException. Now retries transient statuses honoring Retry-After, fails fast on 404, and always re-raises the original exception type.
35b0845 K9 The concurrency semaphore was held across retry backoff and 60s timeouts — one bad CID stalled a healthy request 181s; 32 bad CIDs froze all reads. The semaphore is now held per-attempt and released during backoff.
568e384 K10 — closes the #73 investigation, resolves #79 HTTP/2 enabled on internally created HTTPS clients (httpx[http2]). The #73 ConnectionTerminated failure was empirically re-verified on httpx 0.28.1 and is absorbed by the K6/K9 retry layer — proven by a new regression test against a raw-h2 TLS server that sends GOAWAY every N streams (all concurrent loads succeed). Plaintext daemon URLs stay HTTP/1.1 via ALPN. A niquests migration (#79) was evaluated hands-on and declined: its urllib3-future dependency hijacks the urllib3 namespace, and the measured benefit doesn't justify the migration risk.
7e93a01 K1 KuboCAS(client=...) called get_running_loop() in __init__ — crashed in sync code. Client now binds lazily.
eda4335 K3 The semaphore was bound to the first event loop while clients were per-loop: second asyncio.run()RuntimeError. Semaphores are now per-loop like clients.
1da2c26 K2 On a second event loop, a user-supplied client's auth/headers were silently dropped (internal replacement client built from None defaults) and that internal client leaked. Per-loop clients now inherit the user client's configuration and are all closed by aclose().
b918bac K8 A hardcoded timeout=60.0 per request silently overrode user-configured client timeouts (0.05s configured → 60s actual). The default lives on internal clients only; user timeouts win.
7fe265a K5 Internal clients didn't follow redirects and raise_for_status() treated 301 as fatal — dweb.link, 4everland.io and w3s.link (which 301 path-style /ipfs/CID) were entirely unusable. follow_redirects=True on internal clients.
afe2d3f K7 A gateway URL ending in /ipfs (no trailing slash) built …/ipfs/ipfs/. URLs are normalized; all four spellings (host, host/, host/ipfs, host/ipfs/) work.

Compatibility notes (suggest a major version bump)

  • Z1 introduces a versioned sharded-store root format. New writes use the per-array layout; stores written by current main are still readable. Old readers cannot read new multi-variable stores.
  • H1 changes root CIDs for trees whose history included deletes that previously left non-canonical structure (that non-determinism was the bug). Insert-only trees are unaffected (golden-CID tests pin this).
  • K10 adds the h2 dependency via httpx[http2]; trustme added to dev deps for the GOAWAY TLS test.

How this was verified

  • Each red commit's test fails on its parent commit and passes after the fix; reviewers re-verified red tests were byte-identical through green (no test weakening).
  • Adversarial review ran the original repro scripts plus falsification scripts per item (concurrency races, failure injection, differential golden-CID comparisons old-vs-new). Two review findings became remediation commits (e9d5956, 012fe0c) — including one provably-unreachable line caught by sys.settrace fuzzing that would have broken the repo's 100%-coverage gate.
  • Full gates on the merged tree: pytest 215 passed / 3 skipped (IPFS-daemon and live-gateway tests excluded in this environment — please run bash run-checks.sh with a daemon before merging), ruff check and ruff format --check clean, mypy 7 pre-existing errors (was 8).

Addresses #73, #79; makes substantial progress on #56 (request amplification measured: sharded store = 2 cold RTTs vs HAMT's 3–4; CPU is no longer the bottleneck toward 1Gb/s) and #20/#21 (atomicity/determinism hardening).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added safer pinned, refcounted shard caching with concurrent flush and improved v1 primary-path inference for legacy layouts.
    • Added a client_factory option for HTTP CAS clients.
  • Bug Fixes
    • Purged stale metadata read-cache after HAMT deletes (including encrypted stores).
    • Improved HAMT write atomicity/determinism and write-iteration semantics (keys() snapshot, len() without snapshot).
    • Hardened HTTP CAS byte-range slicing, retries/backoff, redirects, and gateway URL normalization with per-event-loop concurrency.
  • Tests
    • Expanded coverage for shard eviction under load, cache correctness, HTTP range/retry behavior, and HAMT failure/atomicity guarantees.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR refactors HAMT persistence and mutation, updates sharded Zarr cache and legacy-array behavior, improves KuboCAS retries, ranges, redirects, and event-loop lifecycle handling, and adds broad regression coverage.

Changes

HAMT core behavior

Layer / File(s) Summary
HAMT storage and mutation
py_hamt/hamt.py
Direct BLAKE3 hashing, clean-node caching, concurrent flushing, canonical deletion, overflow rollback, idempotent writes, and snapshot iteration are implemented.
HAMT and metadata regression coverage
tests/test_h*.py, tests/test_p*.py, py_hamt/*zarr_hamt_store.py
Tests cover canonical roots, cache reuse, failed mutations, deadlock-free iteration, concurrent vacating, and metadata cache ordering.

Array-aware sharded Zarr storage

Layer / File(s) Summary
Shard cache and array-aware operations
py_hamt/sharded_zarr_store.py
Shard pinning, atomic entry updates, clean eviction, primary-array tracking, CID-object loading, metadata ranges, listing, deletion, and concurrent flushing are updated.
Sharded storage compatibility coverage
tests/test_z*.py, tests/fixtures/*, tests/testing_utils.py
Tests cover multi-variable manifests, legacy fixtures, resizing, coordinate round trips, cache pressure, CID reuse, metadata deletion, and store contracts.

HTTP content-addressed storage

Layer / File(s) Summary
HTTP transport and lifecycle
py_hamt/store_httpx.py
Retry delays, range slicing, lazy client binding, per-loop semaphores, URL normalization, redirects, HTTP/2 clients, timeout propagation, and cleanup are updated.
HTTP behavior regression coverage
tests/test_k*.py, tests/test_kubo_cas.py
Tests cover HTTP/2 GOAWAY recovery, multi-loop clients, semaphore scope, ranges, redirects, retries, URL normalization, and user timeouts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: 0xswego, thegreatalgo, cmanna75

Poem

A rabbit hops through bytes so bright,
BLAKE3 keeps the paths just right.
Shards stay pinned, retries sing,
Clean roots bloom on every spring.
Zarr and HTTP dance with glee. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the PR’s main themes: bug fixes, HTTP/2 support, and performance improvements, even if it is broad and promotional.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch review-fixes-integration

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 012fe0cca7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread py_hamt/sharded_zarr_store.py Outdated
Comment thread py_hamt/store_httpx.py Outdated
@codecov-commenter

codecov-commenter commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (c73fa53) to head (a414e0b).

Additional details and impacted files
@@            Coverage Diff             @@
##              main       #88    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files            8         8            
  Lines         2621      2983   +362     
==========================================
+ Hits          2621      2983   +362     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 8

🧹 Nitpick comments (9)
tests/test_z2_resize_remap.py (1)

13-50: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Property-test resize remapping across axes and geometries.

Generate dimensionality, resized axis, chunk shape, and grow/shrink sizes. The current two-dimensional examples can miss equivalent remapping defects in higher dimensions or other trailing axes.

As per coding guidelines, “Use Hypothesis property-based testing in the test suite.”

Also applies to: 108-142

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_z2_resize_remap.py` around lines 13 - 50, Expand the
resize-remapping coverage around
test_append_along_non_first_dimension_preserves_existing_chunks and the related
tests into Hypothesis property-based tests. Generate array dimensionality,
resized axis, chunk shapes, and valid grow/shrink sizes, then verify existing
data remains correctly mapped after resizing across axes and geometries,
including higher-dimensional and trailing-axis cases.

Source: Coding guidelines

tests/test_z3_shard_cache_eviction.py (1)

76-109: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Property-test eviction access sequences.

Once these keys genuinely use shards, generate write/read order, cache capacity, and flush boundaries to cover repeated eviction and reload combinations.

As per coding guidelines, “Use Hypothesis property-based testing in the test suite.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_z3_shard_cache_eviction.py` around lines 76 - 109, Expand
test_no_lost_acknowledged_writes_under_cache_pressure into a Hypothesis
property-based test that generates key write/read orders, cache capacities, and
flush boundaries. Exercise repeated shard eviction and reload sequences while
preserving the assertions that acknowledged payloads remain readable before and
after reopening the store.

Source: Coding guidelines

tests/test_sharded_zarr_store.py (1)

788-796: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Property-test byte ranges and chunk-key classification.

Generate valid/invalid range boundaries and array paths instead of covering only 10:50 and four hard-coded prefixes. This is especially valuable for zero-length ranges, optional ends, coordinate dimensionality, and primary/non-primary paths.

As per coding guidelines, “Use Hypothesis property-based testing in the test suite.”

Also applies to: 989-999

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sharded_zarr_store.py` around lines 788 - 796, Expand the tests
around the metadata range read and chunk-key classification to use
Hypothesis-generated valid and invalid byte-range boundaries, including
zero-length ranges and optional ends, rather than the fixed 10:50 case. Generate
array paths covering varying coordinate dimensionality and primary/non-primary
classifications, and assert the implementation’s expected behavior for each case
while preserving the existing full-versus-ranged byte comparison where
applicable.

Source: Coding guidelines

tests/test_z4_coord_allowlist.py (1)

11-57: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Generate coordinate and data-variable names with Hypothesis.

The prior defect was name-classification dependent, but coverage currently checks only x, y, lat, and time. Generate valid non-allowlisted names and collisions with historical allowlist names.

As per coding guidelines, “Use Hypothesis property-based testing in the test suite.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_z4_coord_allowlist.py` around lines 11 - 57, Extend the tests
around test_non_allowlisted_coordinate_names_round_trip and
test_legacy_manifest_derives_primary_array_structurally to use Hypothesis
strategies for valid coordinate and data-variable names. Generate both
non-allowlisted names and names colliding with historical allowlist entries,
then assert the existing round-trip and structural manifest behavior remains
correct for each generated case.

Source: Coding guidelines

tests/test_h1_delete_canonicalization.py (1)

14-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix RUF005 lint warning.

Ruff flags the tuple concatenation; use unpacking instead to satisfy ruff check.

🔧 Proposed fix
-KEY_POOL = COLLIDING_KEYS + ("0", "1", "2", "alpha", "omega")
+KEY_POOL = (*COLLIDING_KEYS, "0", "1", "2", "alpha", "omega")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_h1_delete_canonicalization.py` at line 14, Update the KEY_POOL
tuple definition to use tuple unpacking for COLLIDING_KEYS and the additional
string values instead of tuple concatenation, resolving the RUF005 warning while
preserving the same element order and contents.

Source: Linters/SAST tools

tests/test_h3_failed_set_atomicity.py (1)

24-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Redundant blind except Exception triggers BLE001.

If hamt.get("a") raises, pytest already fails the test with a full traceback; catching Exception just to call pytest.fail adds no value and trips the ruff BLE001 rule.

🔧 Proposed simplification
-    try:
-        committed_value = await hamt.get("a")
-    except Exception as error:
-        pytest.fail(
-            "previously committed key 'a' lost after failed set: "
-            f"{type(error).__name__}: {error}"
-        )
-
-    assert committed_value == b"value-a"
+    assert await hamt.get("a") == b"value-a", (
+        "previously committed key 'a' lost after failed set"
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_h3_failed_set_atomicity.py` around lines 24 - 32, Remove the broad
try/except around hamt.get("a") in the failed-set atomicity test, allowing any
exception to propagate naturally with its traceback. Keep the committed_value
assignment and subsequent equality assertion unchanged.

Source: Linters/SAST tools

py_hamt/store_httpx.py (1)

374-387: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid reading HTTPX internals here

client._transport, _pool, and the _max_* fields are private HTTPX details, so an upgrade can silently fall back to the hard-coded defaults and drop a user’s configured limits. If that best-effort behavior is intentional, document it; otherwise pass the limits through explicitly instead of introspecting the client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py_hamt/store_httpx.py` around lines 374 - 387, The _copy_client_limits
method relies on private HTTPX transport and pool fields that can silently lose
configured limits after upgrades. Remove this introspection and pass the
client’s configured httpx.Limits explicitly through the relevant callers and
APIs, preserving user-provided connection and keepalive settings.
py_hamt/sharded_zarr_store.py (2)

899-899: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Preserve exception context.

Chain the re-raised RuntimeError to satisfy Ruff B904.

♻️ Proposed change
-                raise RuntimeError(f"Timeout waiting for shard {shard_idx} to load.")
+                raise RuntimeError(
+                    f"Timeout waiting for shard {shard_idx} to load."
+                ) from None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py_hamt/sharded_zarr_store.py` at line 899, Update the RuntimeError raised in
the shard-loading timeout path to explicitly chain it from the caught exception
using the appropriate exception context syntax, satisfying Ruff B904 while
preserving the existing timeout message and behavior.

Source: Linters/SAST tools


1171-1178: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid blind except Exception and preserve chaining.

Catching bare Exception here (Ruff BLE001) also swallows things like asyncio.CancelledError/KeyboardInterrupt on some paths and discards the original traceback (B904). Narrow the catch or at least chain the cause.

♻️ Proposed change
-        except Exception as e:
-            raise RuntimeError(f"Failed to save data for key {key}: {e}")
+        except Exception as e:
+            raise RuntimeError(f"Failed to save data for key {key}: {e}") from e
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py_hamt/sharded_zarr_store.py` around lines 1171 - 1178, Update the exception
handling around CAS persistence in the method containing _set_pointer_cid: catch
only the expected save/pointer-update exceptions, preserving cancellation and
interruption behavior, and chain the original exception when raising
RuntimeError. If a broader catch is required, explicitly re-raise excluded
control-flow exceptions before wrapping the remaining error.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@py_hamt/sharded_zarr_store.py`:
- Around line 906-911: Ensure the pending-load event created in the
shard-loading flow around _fetch_and_cache_full_shard is removed in a finally
block, including when fetch or decoding raises. Preserve the existing
successful-load behavior while allowing subsequent accesses to retry immediately
after failure.
- Around line 178-180: Change ShardedZarrStore.__contains__ from async def to a
synchronous def so Python’s in operator performs an actual membership check
rather than receiving a coroutine. Preserve the cache-locking and shard_idx
membership behavior, using a synchronization approach compatible with
synchronous execution or provide a separately named async helper if locking
cannot remain asynchronous.

In `@py_hamt/store_httpx.py`:
- Around line 28-49: Update the retry-delay calculation in the backoff helper so
a valid Retry-After value is treated as the minimum wait, not capped by
backoff_delay; replace the min-based return with logic that waits at least the
server-directed duration while preserving jitter for cases without a valid
header. If a maximum is required, apply a separate explicit sane limit rather
than using backoff_delay as the ceiling.

In `@tests/test_k3_per_loop_semaphore.py`:
- Around line 18-28: Rename the format parameter in GatewayHandler.log_message
to avoid ruff A002’s shadowed built-in violation, while preserving the method
signature’s positional behavior and the existing no-op implementation.

In `@tests/test_z1_multivar_index.py`:
- Around line 101-122: Update test_multivar_chunks_have_per_array_indexes and
the related regression path around lines 125-147 to exercise distinct per-array
chunk grids by passing different_chunk_grids=True. Convert the coverage to a
Hypothesis property-based test using compatible distinct grid configurations,
while preserving the existing round-trip array assertions and data-chunk index
validation.

In `@tests/test_z3_shard_cache_eviction.py`:
- Around line 34-41: Rename the first parameter of CIDInMemoryCAS.load from id
to identifier and update its use in the _normalize_cid call; leave the method’s
positional behavior and all other parameters unchanged.

In `@tests/test_z4_coord_allowlist.py`:
- Around line 79-82: Add an explicit isinstance check confirming
root_obj["chunks"] is a dict before calling pop in the legacy_root_cid setup,
while preserving the existing root_obj mapping assertion and mutation behavior.

In `@tests/test_z6_store_contract.py`:
- Around line 124-139: Update test_root_level_chunk_key_no_silent_clobber to
catch only ValueError from the store.set call, matching
_validate_chunk_write_key’s rejection contract. Preserve the existing assertions
for accepted root-level writes, and make the ValueError handling explicitly
document that rejection is the expected acceptable outcome rather than silently
masking unrelated exceptions.

---

Nitpick comments:
In `@py_hamt/sharded_zarr_store.py`:
- Line 899: Update the RuntimeError raised in the shard-loading timeout path to
explicitly chain it from the caught exception using the appropriate exception
context syntax, satisfying Ruff B904 while preserving the existing timeout
message and behavior.
- Around line 1171-1178: Update the exception handling around CAS persistence in
the method containing _set_pointer_cid: catch only the expected
save/pointer-update exceptions, preserving cancellation and interruption
behavior, and chain the original exception when raising RuntimeError. If a
broader catch is required, explicitly re-raise excluded control-flow exceptions
before wrapping the remaining error.

In `@py_hamt/store_httpx.py`:
- Around line 374-387: The _copy_client_limits method relies on private HTTPX
transport and pool fields that can silently lose configured limits after
upgrades. Remove this introspection and pass the client’s configured
httpx.Limits explicitly through the relevant callers and APIs, preserving
user-provided connection and keepalive settings.

In `@tests/test_h1_delete_canonicalization.py`:
- Line 14: Update the KEY_POOL tuple definition to use tuple unpacking for
COLLIDING_KEYS and the additional string values instead of tuple concatenation,
resolving the RUF005 warning while preserving the same element order and
contents.

In `@tests/test_h3_failed_set_atomicity.py`:
- Around line 24-32: Remove the broad try/except around hamt.get("a") in the
failed-set atomicity test, allowing any exception to propagate naturally with
its traceback. Keep the committed_value assignment and subsequent equality
assertion unchanged.

In `@tests/test_sharded_zarr_store.py`:
- Around line 788-796: Expand the tests around the metadata range read and
chunk-key classification to use Hypothesis-generated valid and invalid
byte-range boundaries, including zero-length ranges and optional ends, rather
than the fixed 10:50 case. Generate array paths covering varying coordinate
dimensionality and primary/non-primary classifications, and assert the
implementation’s expected behavior for each case while preserving the existing
full-versus-ranged byte comparison where applicable.

In `@tests/test_z2_resize_remap.py`:
- Around line 13-50: Expand the resize-remapping coverage around
test_append_along_non_first_dimension_preserves_existing_chunks and the related
tests into Hypothesis property-based tests. Generate array dimensionality,
resized axis, chunk shapes, and valid grow/shrink sizes, then verify existing
data remains correctly mapped after resizing across axes and geometries,
including higher-dimensional and trailing-axis cases.

In `@tests/test_z3_shard_cache_eviction.py`:
- Around line 76-109: Expand
test_no_lost_acknowledged_writes_under_cache_pressure into a Hypothesis
property-based test that generates key write/read orders, cache capacities, and
flush boundaries. Exercise repeated shard eviction and reload sequences while
preserving the assertions that acknowledged payloads remain readable before and
after reopening the store.

In `@tests/test_z4_coord_allowlist.py`:
- Around line 11-57: Extend the tests around
test_non_allowlisted_coordinate_names_round_trip and
test_legacy_manifest_derives_primary_array_structurally to use Hypothesis
strategies for valid coordinate and data-variable names. Generate both
non-allowlisted names and names colliding with historical allowlist entries,
then assert the existing round-trip and structural manifest behavior remains
correct for each generated case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4d08240-71a1-450c-a83b-11a377cc5bd7

📥 Commits

Reviewing files that changed from the base of the PR and between c73fa53 and 012fe0c.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • py_hamt/encryption_hamt_store.py
  • py_hamt/hamt.py
  • py_hamt/hamt_to_sharded_converter.py
  • py_hamt/sharded_zarr_store.py
  • py_hamt/store_httpx.py
  • py_hamt/zarr_hamt_store.py
  • pyproject.toml
  • tests/fixtures/z1_legacy_multivar_store.json
  • tests/fixtures/z1_legacy_single_var_store.json
  • tests/test_h1_delete_canonicalization.py
  • tests/test_h2_enable_write_idempotent.py
  • tests/test_h3_failed_set_atomicity.py
  • tests/test_h4_keys_iteration_deadlock.py
  • tests/test_h5_read_cache_growth.py
  • tests/test_h6_write_perf_restore.py
  • tests/test_hamt.py
  • tests/test_k10_http2_enable.py
  • tests/test_k1_lazy_client_binding.py
  • tests/test_k2_user_client_multiloop.py
  • tests/test_k3_per_loop_semaphore.py
  • tests/test_k4_range_correctness.py
  • tests/test_k5_follow_redirects.py
  • tests/test_k6_retry_policy.py
  • tests/test_k7_url_normalization.py
  • tests/test_k8_respect_client_timeout.py
  • tests/test_k9_semaphore_scope.py
  • tests/test_kubo_cas.py
  • tests/test_p1_direct_blake3.py
  • tests/test_p2_relink_o1_hash_once.py
  • tests/test_p4_cid_objects.py
  • tests/test_p5_concurrent_vacate.py
  • tests/test_sharded_zarr_store.py
  • tests/test_z1_multivar_index.py
  • tests/test_z2_resize_remap.py
  • tests/test_z3_shard_cache_eviction.py
  • tests/test_z4_coord_allowlist.py
  • tests/test_z5_delete_stale_metadata_cache.py
  • tests/test_z6_store_contract.py
  • tests/testing_utils.py

Comment thread py_hamt/sharded_zarr_store.py Outdated
Comment thread py_hamt/sharded_zarr_store.py Outdated
Comment thread py_hamt/store_httpx.py Outdated
Comment thread tests/test_k3_per_loop_semaphore.py
Comment thread tests/test_z1_multivar_index.py Outdated
Comment thread tests/test_z3_shard_cache_eviction.py Outdated
Comment thread tests/test_z4_coord_allowlist.py Outdated
Comment thread tests/test_z6_store_contract.py
Faolain and others added 26 commits July 13, 2026 11:23
… deleted metadata from its read cache

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d metadata from its read cache

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…allowlist crash or corrupt ShardedZarrStore

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es all existing chunks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng its own load; lost-write window for clean shards

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…own load; lost-write window for clean shards

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (ShardedZarrStore)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…edZarrStore)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rough base32 strings (~75% of warm-get CPU)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ase32 strings (~75% of warm-get CPU)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rite each others chunks (per-array indexing)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…de an event loop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vent loop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… loop breaks documented multi-loop support

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reaks documented multi-loop support

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d loop: auth silently dropped + internal client leaked

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… auth silently dropped + internal client leaked

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y overrides the user clients timeout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ides the user clients timeout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(dweb.link, 4everland, w3s.link unusable)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ink, 4everland, w3s.link unusable)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iling slash) builds /ipfs/ipfs/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in write mode destroys buffered data

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e mode destroys buffered data

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ly committed keys

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…itted keys

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found 4 actionable issues.
Posted 4 inline comment(s).

Comment thread py_hamt/sharded_zarr_store.py Outdated
Comment thread py_hamt/sharded_zarr_store.py Outdated
Comment thread py_hamt/store_httpx.py Outdated
Comment thread py_hamt/sharded_zarr_store.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

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 introduces a broad correctness + performance overhaul across the HAMT core, Zarr store layers, and the HTTP-backed CAS (KuboCAS), aiming to eliminate known data-corruption/atomicity issues, improve determinism, and enable HTTP/2 with robust retry semantics.

Changes:

  • Refactors HAMT internals for faster hashing, bounded/concurrent vacate, failure-atomic set()/delete(), canonical post-delete structure, and deadlock-free key iteration.
  • Hardens sharded Zarr storage semantics (multi-variable indexing, resize remapping, cache correctness) and expands contract/compatibility test coverage (incl. fixtures).
  • Improves KuboCAS behavior (range correctness, redirects, retry/backoff, per-event-loop clients/semaphores, HTTP/2 enablement) and adds trustme for TLS testing.

Reviewed changes

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

Show a summary per file
File Description
py_hamt/hamt.py Core HAMT changes: hashing, caching, vacate concurrency, atomicity/determinism, and iteration/len behavior.
py_hamt/zarr_hamt_store.py Metadata cache correctness on set/delete in the HAMT-backed Zarr store.
py_hamt/encryption_hamt_store.py Align encrypted store metadata cache update timing with underlying write success.
pyproject.toml Adds trustme to dev dependencies for TLS/HTTP2 regression testing.
tests/testing_utils.py Adds a CID-returning in-memory CAS for offline tests and CID normalization helper.
tests/test_sharded_zarr_store.py Updates shard-cache containment checks and adds metadata range expectations.
tests/test_sharded_zarr_store_coverage.py Adds coverage for cache update/dirty-guard branches.
tests/test_z1_multivar_index.py Tests multi-variable per-array indexing + legacy fixture readability.
tests/test_z2_resize_remap.py Tests correct remapping on resize/append across non-leading dimensions.
tests/test_z3_shard_cache_eviction.py Regression tests for shard eviction safety and acknowledged-write durability.
tests/test_z4_coord_allowlist.py Ensures coordinate names are handled structurally (no hardcoded allowlist).
tests/test_z5_delete_stale_metadata_cache.py Ensures delete purges metadata read cache (plain + encrypted stores).
tests/test_z6_store_contract.py Contract tests for byte ranges + listing semantics.
tests/test_z7_delete_legacy_metadata.py Tests legacy metadata cleanup behavior and mutation sequences.
tests/test_z8_cas_save_cid_validation.py Validates CAS save() must return a CID (rejects raw multihash bytes).
tests/test_z9_cache_contains.py Ensures __contains__ works via the in operator for the LRU cache.
tests/test_z10_concurrent_flush.py Tests concurrent shard flush behavior + failure cleanup/no orphan tasks.
tests/test_z11_v1_unrecorded_coord_chunk.py Tests v1 coordinate-chunk routing across write-order permutations.
tests/test_z12_v1_legacy_primary_inference.py Property tests for conservative v1 primary-array inference behavior.
tests/test_z13_inferred_primary_write_stability.py Tests inferred-primary write semantics match recorded-primary behavior.
tests/test_p1_direct_blake3.py Golden digest + “no multiformats wrapper in hot path” performance guard.
tests/test_p2_relink_o1_hash_once.py Ensures relinking/hashing stays bounded and root IDs remain stable.
tests/test_p4_cid_objects.py Ensures warm get/set paths preserve CID objects (avoid encode/decode churn).
tests/test_p5_concurrent_vacate.py Verifies sibling subtree vacate concurrency + golden root stability.
tests/test_p6_bounded_vacate.py Ensures vacate bounded concurrency + no orphan save tasks on failure.
tests/test_p7_len_without_snapshot.py Ensures len() doesn’t materialize a key snapshot via keys().
tests/test_p8_bounded_task_creation.py Ensures bounded task creation for both HAMT vacate and shard flush.
tests/test_h1_delete_canonicalization.py Property test: operation history shouldn’t affect root CID determinism.
tests/test_h2_enable_write_idempotent.py Ensures redundant enable_write() calls are safe/idempotent.
tests/test_h3_failed_set_atomicity.py Ensures failed bucket split doesn’t corrupt previously committed keys.
tests/test_h4_keys_iteration_deadlock.py Ensures keys iteration no longer deadlocks under nested get/set.
tests/test_h5_read_cache_growth.py Ensures read caching is effective and doesn’t grow unbounded on reads.
tests/test_h6_write_perf_restore.py Guards against deepcopy regressions and validates atomic failure behavior.
tests/test_h7_delete_failure_atomicity.py Ensures delete remains atomic across CAS load failures during collapse.
tests/test_hamt.py Adjusts cache vacate assertions to account for concurrent repopulation.
tests/test_kubo_cas.py Updates retry behavior expectations (transient status retries).
tests/test_k1_lazy_client_binding.py Ensures KuboCAS(client=...) doesn’t require a running loop at init.
tests/test_k2_user_client_multiloop.py Ensures headers/auth propagate across loops; internal clients get closed.
tests/test_k3_per_loop_semaphore.py Ensures contended loads work across sequential event loops.
tests/test_k4_range_correctness.py Ensures safe handling when gateways ignore Range + zero-length/suffix cases.
tests/test_k5_follow_redirects.py Ensures internal clients follow redirects.
tests/test_k6_retry_policy.py Tests retry policy including Retry-After parsing and 404 fail-fast.
tests/test_k7_url_normalization.py Tests gateway URL normalization of /ipfs path variants.
tests/test_k8_respect_client_timeout.py Ensures user-provided httpx timeouts aren’t overridden by defaults.
tests/test_k9_semaphore_scope.py Ensures semaphore is released during retry backoff (prevents head-of-line blocking).
tests/test_k10_http2_enable.py TLS raw-h2 GOAWAY regression + verify HTTP/2 negotiation on HTTPS.
tests/test_k12_cross_loop_client_policy.py Documents/reinforces cross-loop client policy + factory constraints.
tests/test_k13_per_loop_aclose.py Ensures per-loop client cleanup works even when loops are already closed.
tests/fixtures/z1_legacy_single_var_store.json Adds a pinned legacy fixture for sharded-store backward compatibility.

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

Comment thread py_hamt/hamt.py
Comment thread py_hamt/hamt.py
@0xSwego

0xSwego commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found 4 actionable issues.
Posted 4 inline comment(s).

Comment thread py_hamt/sharded_zarr_store.py Outdated
if isinstance(recorded_path, str):
primary_path_is_exclusive = True
effective_primary_path = recorded_path
elif self._primary_array_path:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
Persist routing based on an inferred V1 primary. If a writable legacy root infers temp, then stores a same-geometry precip chunk as metadata, the inferred path is not persisted. On reopen both arrays are inference candidates, so inference becomes ambiguous and precip/c/... is parsed against the shard index, silently returning temp data instead of its metadata-backed value.

if self._manifest_version == SHARDED_ZARR_V1:
chunk_info = self._root_obj["chunks"]
if "primary_array_path" not in chunk_info:
self._primary_array_path = parsed_chunk.array_path

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
Do not let a named write rebind a legacy root-level primary. An inferred root path is "", which is indistinguishable from “not inferred” in the truthiness check. A subsequent named chunk is therefore treated as the primary, overwrites the root shard slot, and records the named path here, making existing root-array data inaccessible or incorrect. Track inference separately so an empty primary path remains exclusive.

)
evicted = True
break
self._evict_if_needed_locked()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Pin shards populated during resize before running eviction. _snapshot_shards_for_resize() calls _fetch_and_cache_full_shard() directly without a pin. With an over-budget cache whose older entries are dirty or pinned, this eviction pass can immediately remove the newly inserted clean shard; the following snapshot lookup then raises RuntimeError. Route resize loads through the pinned loader or pin cache_key around population.

chunk_prefix = (
"c" if not self._primary_array_path else f"{self._primary_array_path}/c"
)
keys.add(chunk_prefix)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Make V1 list_dir() descend into chunk directories. This synthetic candidate lets list_dir("temp") report c, but list_dir("temp/c") remains empty because _is_v2_chunk_listing_prefix() rejects V1 and the candidate set contains no coordinate keys. Enumerate _iter_chunk_keys() for V1 chunk prefixes as well.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
py_hamt/store_httpx.py (2)

724-739: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return an empty result for valid out-of-bounds ranges.

A gateway returns HTTP 416 when the requested offset is at or beyond EOF, but raise_for_status() currently raises. This diverges from InMemoryCAS and the HTTP-200 fallback’s slice semantics. Validate the 416 Content-Range total and return b"" when the start is beyond it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py_hamt/store_httpx.py` around lines 724 - 739, The HTTP gateway fetch flow
around the retry loop and response.raise_for_status() must handle valid
out-of-bounds range responses. Before raising for status, detect HTTP 416, parse
and validate its Content-Range total, and return b"" when the requested offset
is at or beyond EOF; preserve existing behavior for valid ranges and malformed
or unrelated 416 responses.

477-504: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Serialize first-use client binding across loop threads.

Two loops can simultaneously observe _supplied_client before either clears it, binding the same AsyncClient to both loops. Protect client-map creation and supplied-client consumption with a thread-safe state lock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py_hamt/store_httpx.py` around lines 477 - 504, The client selection and
per-loop binding logic must be serialized to prevent concurrent loops from
consuming the same supplied client. Add or reuse a thread-safe state lock around
the critical section in the client-acquisition method containing
`_supplied_client`, `_client_factory`, and `_client_per_loop`, including
supplied-client consumption, client creation, and map assignment; keep client
use outside the lock where possible.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@py_hamt/store_httpx.py`:
- Around line 536-545: Update the client cleanup logic around
_close_client_on_stopped_loop so the run_coroutine_threadsafe result cannot wait
indefinitely if owner_loop stops after the running check. Bound the await of
close_future, cancel the pending future on timeout, and invoke the
stopped/dead-loop fallback to ensure the client is closed.

In `@tests/test_k13_per_loop_aclose.py`:
- Line 2: Update test_k13_per_loop_aclose to assert the promised RuntimeWarning
directly during transport cleanup, rather than accepting any warning or
warning-level log; preserve the existing cleanup scenario and verify the warning
type and relevant message.

In `@tests/test_p8_bounded_task_creation.py`:
- Around line 178-257: Parameterize the four concurrency-regression
tests—test_vacate_bounds_simultaneously_live_save_tasks,
test_shard_flush_bounds_simultaneously_live_save_tasks,
test_failed_vacate_cancels_bounded_work_without_orphans, and
test_failed_shard_flush_cancels_bounded_work_without_orphans—with Hypothesis
bounded strategies for HAMT key count, shard count, and failing save ordinal.
Thread the generated values through the existing build helpers and Delayed*CAS
fail_on_save setup, while preserving the concurrency-limit and
orphan-cancellation assertions for every generated case.

In `@tests/test_z13_inferred_primary_write_stability.py`:
- Around line 214-220: Update the assertions in
tests/test_z13_inferred_primary_write_stability.py at lines 214-220 and 410-417:
require inferred.persisted_primary is None at the first site, and require
"primary_array_path" not in chunk_info at the second site, ensuring foreign
writes never persist the inferred "temp" primary.

---

Outside diff comments:
In `@py_hamt/store_httpx.py`:
- Around line 724-739: The HTTP gateway fetch flow around the retry loop and
response.raise_for_status() must handle valid out-of-bounds range responses.
Before raising for status, detect HTTP 416, parse and validate its Content-Range
total, and return b"" when the requested offset is at or beyond EOF; preserve
existing behavior for valid ranges and malformed or unrelated 416 responses.
- Around line 477-504: The client selection and per-loop binding logic must be
serialized to prevent concurrent loops from consuming the same supplied client.
Add or reuse a thread-safe state lock around the critical section in the
client-acquisition method containing `_supplied_client`, `_client_factory`, and
`_client_per_loop`, including supplied-client consumption, client creation, and
map assignment; keep client use outside the lock where possible.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cb351348-1be7-4b94-b718-2ba477c370c0

📥 Commits

Reviewing files that changed from the base of the PR and between b9e2a1e and d9e34ae.

📒 Files selected for processing (14)
  • py_hamt/hamt.py
  • py_hamt/sharded_zarr_store.py
  • py_hamt/store_httpx.py
  • tests/test_h7_delete_failure_atomicity.py
  • tests/test_k12_cross_loop_client_policy.py
  • tests/test_k13_per_loop_aclose.py
  • tests/test_p6_bounded_vacate.py
  • tests/test_p7_len_without_snapshot.py
  • tests/test_p8_bounded_task_creation.py
  • tests/test_sharded_zarr_store.py
  • tests/test_z11_v1_unrecorded_coord_chunk.py
  • tests/test_z12_v1_legacy_primary_inference.py
  • tests/test_z13_inferred_primary_write_stability.py
  • tests/test_z7_delete_legacy_metadata.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/test_p7_len_without_snapshot.py
  • tests/test_sharded_zarr_store.py
  • tests/test_k12_cross_loop_client_policy.py
  • tests/test_p6_bounded_vacate.py
  • tests/test_h7_delete_failure_atomicity.py

Comment thread py_hamt/store_httpx.py
Comment thread tests/test_k13_per_loop_aclose.py Outdated
Comment on lines +178 to +257
@pytest.mark.asyncio
async def test_vacate_bounds_simultaneously_live_save_tasks(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cas = DelayedBytesCAS()
hamt = await build_wide_hamt(cas)
node_store = cast(InMemoryTreeStore, hamt.node_store)
buffered_node_count = len(node_store.buffer)
assert buffered_node_count > _VACATE_CONCURRENCY
cas.arm()
tracker = TaskCreationTracker()

with monkeypatch.context() as scoped_monkeypatch:
tracker.install(scoped_monkeypatch)
await hamt.cache_vacate()

assert tracker.peak_live_tasks <= _VACATE_CONCURRENCY, (
"vacate must not create the entire save wave as pending tasks: "
f"peak={tracker.peak_live_tasks}, limit={_VACATE_CONCURRENCY}, "
f"buffered_nodes={buffered_node_count}"
)


@pytest.mark.asyncio
async def test_shard_flush_bounds_simultaneously_live_save_tasks(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cas = DelayedCIDCAS()
store = await build_dirty_sharded_store(cas)
cas.arm()
tracker = TaskCreationTracker()

with monkeypatch.context() as scoped_monkeypatch:
tracker.install(scoped_monkeypatch)
await store.flush()

assert tracker.peak_live_tasks <= _FLUSH_CONCURRENCY, (
"shard flush must not create every dirty shard as a pending task: "
f"peak={tracker.peak_live_tasks}, limit={_FLUSH_CONCURRENCY}, "
f"dirty_shards={_SHARD_COUNT}"
)


@pytest.mark.asyncio
async def test_failed_vacate_cancels_bounded_work_without_orphans(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cas = DelayedBytesCAS(fail_on_save=3)
hamt = await build_wide_hamt(cas)
total_nodes = len(cast(InMemoryTreeStore, hamt.node_store).buffer)
cas.arm()
tracker = TaskCreationTracker()

with monkeypatch.context() as scoped_monkeypatch:
tracker.install(scoped_monkeypatch)
with pytest.raises(ConnectionError, match="simulated HAMT save failure"):
await hamt.cache_vacate()

assert cas.started_saves < total_nodes, "failure must stop unscheduled node saves"
assert cas.in_flight_saves == 0
assert tracker.pending_tasks() == [], "vacate save tasks survived the failure"


@pytest.mark.asyncio
async def test_failed_shard_flush_cancels_bounded_work_without_orphans(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cas = DelayedCIDCAS(fail_on_save=3)
store = await build_dirty_sharded_store(cas)
cas.arm()
tracker = TaskCreationTracker()

with monkeypatch.context() as scoped_monkeypatch:
tracker.install(scoped_monkeypatch)
with pytest.raises(ConnectionError, match="simulated shard save failure"):
await store.flush()

assert cas.started_saves < _SHARD_COUNT, "failure must stop unscheduled shard saves"
assert cas.in_flight_saves == 0
assert tracker.pending_tasks() == [], "shard save tasks survived the failure"

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Locate the target test file and nearby test patterns\n'
git ls-files 'tests/**/*.py' | sed -n '1,120p'
printf '\n---\n'
ast-grep outline tests/test_p8_bounded_task_creation.py --view expanded || true

printf '\n## Inspect the relevant file sections\n'
wc -l tests/test_p8_bounded_task_creation.py
sed -n '1,320p' tests/test_p8_bounded_task_creation.py

printf '\n## Search for Hypothesis usage and async property-based test patterns\n'
rg -n "hypothesis|`@given`|strategies as st|settings\\(|pytest\\.mark\\.asyncio|pytest\\.mark\\.hypothesis" tests . -g '*.py' -g '!**/.venv/**'

printf '\n## Search for repository-wide Hypothesis configuration or async handling hooks\n'
rg -n "hypothesis|register_profile|settings\\.load_profile|deadline=None|event_loop|pytest_plugins|asyncio" . -g '*.py' -g 'pytest.ini' -g 'pyproject.toml' -g 'conftest.py' -g '!**/.venv/**'

Repository: dClimate/py-hamt

Length of output: 50372


Parameterize these concurrency-regression tests with Hypothesis.
Use bounded strategies for the HAMT key count, shard count, and failing save ordinal so these cases cover more wave sizes and cancellation points instead of a single fixed layout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_p8_bounded_task_creation.py` around lines 178 - 257, Parameterize
the four concurrency-regression
tests—test_vacate_bounds_simultaneously_live_save_tasks,
test_shard_flush_bounds_simultaneously_live_save_tasks,
test_failed_vacate_cancels_bounded_work_without_orphans, and
test_failed_shard_flush_cancels_bounded_work_without_orphans—with Hypothesis
bounded strategies for HAMT key count, shard count, and failing save ordinal.
Thread the generated values through the existing build helpers and Delayed*CAS
fail_on_save setup, while preserving the concurrency-limit and
orphan-cancellation assertions for every generated case.

Source: Coding guidelines

Comment thread tests/test_z13_inferred_primary_write_stability.py Outdated

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found 2 actionable issues.
Posted 2 inline comment(s).

# later flush of a writable open cannot seal a wrong inference.
# The flag keeps the (possibly empty-string) inferred primary
# exclusive so foreign chunk writes cannot rebind over it.
self._primary_array_path = next(iter(matching_paths))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
_primary_array_path is inferred only in memory. If this writable V1 store then adds a second array with identical shape/chunk geometry, its chunk is metadata-routed; after flush/reopen both arrays match inference, routing becomes ambiguous, and the second chunk is read from the primary array's global shard slot instead of its metadata CID. Persist an explicit routing decision or reject/migrate such writes before flushing.

else:
# V1 emits a single primary chunk tree, rooted at "c" or
# "<primary>/c" to match the recorded/inferred primary path.
yield (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
For an unrecorded V1 root whose inferred primary path is "", this yields c, while _iter_chunk_keys() emits /c/.... Because list_dir() strips the input slash and compares against c/, list_dir("c") returns empty even though list_dir("") advertises c and chunks exist. Normalize the legacy root key and prefix consistently.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found 3 actionable issues.
Posted 3 inline comment(s).

Comment thread py_hamt/sharded_zarr_store.py
Comment thread py_hamt/store_httpx.py Outdated
Comment thread py_hamt/store_httpx.py Outdated
content = response.content
response_bytes = len(content)
final_retry_count = retry_count
if headers and response.status_code == httpx.codes.OK:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Handle valid out-of-bounds ranges. A compliant gateway returns HTTP 416 when offset is at or beyond EOF, so raise_for_status() runs before this HTTP-200 fallback and raises even though InMemoryCAS and the documented Python-slice semantics return b"". Recognize a valid Content-Range: bytes */N response and return empty when offset >= N.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

One silent V1 routing corruption remains.
Posted 1 inline comment(s).


clone.array_indices = self.array_indices
clone._primary_array_path = self._primary_array_path
clone._primary_inferred = self._primary_inferred

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
Copying _primary_inferred into a writable clone leaves the inferred V1 path only in memory. If with_read_only(False) then writes a same-geometry secondary array, flush/reopen sees two inference candidates and can return the primary shard's bytes for the secondary key instead of its metadata CID. Persist chunks.primary_array_path and mark the root dirty when creating the writable clone, or reject this transition.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found two actionable HTTP range-read correctness issues.
Posted 2 inline comment(s).

Comment thread py_hamt/store_httpx.py Outdated
# compliant gateway; return b"" to match Python-slice
# semantics (and InMemoryCAS) instead of raising.
if (
offset is not None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Handle suffix ranges on empty objects. A compliant gateway may answer Range: bytes=-N for a zero-byte object with 416 Content-Range: bytes */0, but this branch only recognizes 416 responses when offset is set. Nonzero suffix reads therefore raise while InMemoryCAS returns b""; handle the suffix/zero-size case consistently.

Comment thread py_hamt/store_httpx.py Outdated
content = response.content
response_bytes = len(content)
final_retry_count = retry_count
if headers and response.status_code == httpx.codes.OK:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
Validate partial responses before trusting them. raise_for_status() accepts any 2xx response, while this branch only corrects status 200. A gateway returning a malformed 206 with a missing or mismatched Content-Range can therefore supply the full or wrong byte window, silently corrupting partial reads. Verify Content-Range and body length for 206 responses and reject unexpected successful statuses.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found 2 actionable issues: one silent V1 data-corruption path and one HTTP client resource leak.
Posted 2 inline comment(s).

Comment thread py_hamt/sharded_zarr_store.py
Comment thread py_hamt/store_httpx.py
@TheGreatAlgo
TheGreatAlgo merged commit 5a25d35 into main Jul 21, 2026
1 of 2 checks passed

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found 2 actionable issues.
Posted 2 inline comment(s).

# only true-primary chunks reach this point anyway.
chunk_info = self._root_obj["chunks"]
if "primary_array_path" not in chunk_info:
self._primary_array_path = parsed_chunk.array_path

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
Recording the V1 primary here is not used when processing later array metadata. _register_array_metadata_from_bytes() still resizes the global V1 index for any same-rank array whose shape differs, so secondary metadata can remap—or when shrinking, discard—the primary array's chunks. Gate V1 resizing on the metadata path matching _primary_array_path; the added test_secondary_array_metadata_does_not_resize_primary_geometry currently exercises this failure.

try:
data_cid_obj = await self.cas.save(raw_data_bytes, codec="raw")
await self._set_pointer(key, str(data_cid_obj), register_metadata=False)
if not isinstance(data_cid_obj, CID):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
This narrows the documented ContentAddressedStore.save() contract from any immutable IPLDKind to multiformats.CID. Backends returning a valid CID string previously worked because _set_pointer() decoded it, but now every set() fails with RuntimeError. Normalize CID strings with CID.decode() instead of rejecting them.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Two actionable correctness regressions found.
Posted 2 inline comment(s).

chunk_info = self._root_obj["chunks"]
if "primary_array_path" not in chunk_info:
self._primary_array_path = parsed_chunk.array_path
chunk_info["primary_array_path"] = parsed_chunk.array_path

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH
Recording the V1 primary here does not protect it from later metadata writes. _register_array_metadata_from_bytes() still calls _resize_store_unlocked() for any same-rank array with a different shape, so writing secondary-array metadata can remap or discard the primary array's chunks. Gate V1 resizing on array_path == _primary_array_path.

try:
data_cid_obj = await self.cas.save(raw_data_bytes, codec="raw")
await self._set_pointer(key, str(data_cid_obj), register_metadata=False)
if not isinstance(data_cid_obj, CID):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
This rejects valid CID strings returned by custom ContentAddressedStore implementations, despite save() permitting any immutable IPLDKind; the previous _set_pointer() path decoded such strings successfully. Normalize strings with CID.decode() before validating the result.

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.

Replace Httpx with niquests

5 participants