Skip to content

Retry once when the batch signer returns a partial map - #310

Merged
stevevanhooser merged 1 commit into
mainfrom
claude/batch-signer-partial-map-retry
Sep 15, 2026
Merged

stevevanhooser merged 1 commit into
mainfrom
claude/batch-signer-partial-map-retry

Conversation

@stevevanhooser

Copy link
Copy Markdown
Contributor

Closes #309.

The gap

BatchSignedUrlLookup.lookup treated "the map does not name this uid" as one thing. It is two:

  1. Data drift — the file was never in this scope. Fall back per uid, warn once, move on.
  2. A lagged index — the file IS in the scope on the server, but the batch endpoint's map has not caught up with a just-completed bulk upload. Falling back per uid here defeats the batch design; a 28,000-member series that hit this would cost 28,000 getFileDetails calls where the batch would have taken one.

Run 229 of test-cloud-api.yml on TEST_USER_1 / prod, on the merge commit that fixed #306 (#308), showed case 2: the batch answered signer_calls=1 with a 2-entry map for a 4-member series; the two missing uids each took a per-uid round trip. Retrying the same scope 37 minutes later on run_attempt: 2 returned the full 4-entry map, so the endpoint DOES eventually catch up — just not by the time waitForAllBulkUploads reports done.

The fix

When a cached scope answers with a URL but a subsequent uid in the same scope misses, drop the cache entry, sleep DEFAULT_PARTIAL_MAP_RETRY_SECONDS (500 ms), and re-fetch once. If the second map covers the uid, take it and every subsequent uid in that scope resolves from the refreshed cache. If not, warn and fall back exactly as today.

Bounded to one retry per scope, tracked in a new Stats.partial_map_retries counter — so a genuine data-drift miss still costs one extra signer call for the whole scope, not one per uid, and a run's own stats say whether the retry path fired.

A test seam (sleep= and partial_map_retry_seconds=) lets offline tests exercise the retry without paying the wall-clock cost.

Tests

Four new tests in TestBatchLookupPartialMapRetry:

  • test_a_lagged_scope_settles_on_the_retry — first map has 2 of 4 uids, second has all 4. All four members resolve; signer_calls=2, partial_map_retries=1, uid_misses=0.
  • test_the_retry_pauses_before_re_fetching — the 500 ms sleep fires exactly once, on the retry path.
  • test_a_scope_is_only_retried_once — a scope whose map stays partial produces at most one retry regardless of how many misses follow.
  • test_a_retry_that_returns_no_map_at_all_still_falls_back — retry raises → per-uid fallback, no double-count of the miss.

Existing test test_a_uid_absent_from_the_batch_map_falls_back updated to acknowledge the retry path: partial_map_retries=1, signer_calls=2, same fallback outcome. Retry sleep disabled in that test for speed.

Live-cloud round-trip test's assertion signer_calls == 1 relaxed to signer_calls == 1 + partial_map_retries, with a separate partial_map_retries <= 1 cap. The "one batch call per series' worth of members" contract survives; the retry is admitted.

Not changed

  • filehandler.download_file_from_cloud — no change; it consults the same lookup.
  • The signer itself — the fetch shape is untouched. The retry uses the existing _fetch_scope code path, populating the cache the same way.
  • The negative-cache path (a signer that raised, or that returned an unusable payload) — still suppresses per-uid re-hammering exactly as before. The retry only fires when the FIRST answer was successful-but-partial.

Files

  • src/ndi/cloud/batch_signed_url.py — retry logic + partial_map_retries stat.
  • tests/test_cloud_batch_signed_url.py — new test class, one existing test refreshed.
  • tests/test_cloud_file_series_round_trip.py — assertion admits one retry.

Offline tests pass (22 batch, 47 in the wider batch+series suite). Bridge check passes. The 7 pre-existing keyring failures in test_cloud_profile_secret_field_names.py reproduce on main; unrelated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AdGZ8fRPeY9JSpUHgXm9ao


Generated by Claude Code

BatchSignedUrlLookup.lookup collapsed two distinct causes of "this uid
is not in the scope's map" into one silent per-uid fallback:

1. Data drift: the file was never in this scope. The right answer is
   to fall back and warn.
2. A lagged index: the file IS in the scope on the server, but the
   batch endpoint's map has not caught up with a just-completed bulk
   upload. Falling back per uid here defeats the batch design -- a
   28,000-member series that took the fallback would issue 28,000
   getFileDetails calls where the batch would have taken one.

TEST_USER_1/prod on run 229 of test-cloud-api showed the second case
after #308 first let a series manifest
reach the server: the batch answered signer_calls=1 with a 2-entry
map for a 4-member series. The two missing uids fell back per uid.
Retrying the same scope 37 minutes later on run_attempt=2 returned
the full 4-entry map -- so the endpoint does eventually catch up,
just not by the time waitForAllBulkUploads reported done.

Fix: when a cached scope answers with a URL but a subsequent uid in
that scope misses, drop the entry, sleep for DEFAULT_PARTIAL_MAP_
RETRY_SECONDS (500 ms), and re-fetch once. If the second map covers
the uid, take it and every subsequent uid in the scope resolves from
the refreshed cache; if not, warn and fall back as before. Bounded
to one retry per scope, so a genuine data-drift miss still costs one
extra signer call for the whole scope, not one per uid. Tracked in a
new Stats.partial_map_retries counter so tests can distinguish the
retry path from a plain data-drift miss.

The live-cloud round-trip test's `signer_calls == 1` assertion is
relaxed to `signer_calls == 1 + partial_map_retries`, keeping the
"one batch call per series' worth of members" contract while
allowing the retry.

Closes #309.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdGZ8fRPeY9JSpUHgXm9ao
@stevevanhooser
stevevanhooser merged commit 7f27c6e into main Sep 15, 2026
9 checks passed
@stevevanhooser
stevevanhooser deleted the claude/batch-signer-partial-map-retry branch September 15, 2026 13:58
stevevanhooser pushed a commit that referenced this pull request Sep 15, 2026
NDI-matlab moved the file since 5804cd80 with one commit that has
already been ported on the Python side:

  9120c002  Retry once when the batch signer returns a partial map
            (VH-Lab/NDI-matlab#991)

#310 landed the corresponding Python
retry on 2026-09-15 (src/ndi/cloud/batch_signed_url.py:
_partial_map_retry_seconds / STATS.partial_map_retries). The Python
side is caught up; nothing else to port here.

Purely a bookkeeping bump to unblock this PR's CI. Included on this
branch because the drift check gates every push, not because it is
part of the mfdaq change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DiZHVBoGVBfHRYCxvoAWmo
stevevanhooser pushed a commit that referenced this pull request Sep 16, 2026
Picks up:
- Sample-index-base as a third indexing rule (WDS/NDI-python#314)
- mfdaq ingested-clamp-past-edges companion (#312)
- Bridge hash bumps for mfdaq_reader (770c1eb3) and the accurate
  decision_log for batchSignedUrlLookup 9120c002.

Conflict: src/ndi/cloud/ndi_matlab_python_bridge.yaml. Both this branch
and main added a decision_log paragraph for the 9120c002 hash bump,
but main's is the correct one: the Python side landed the partial-map
retry FIRST (#310 with
_partial_map_retry_seconds and STATS.partial_map_retries), so this is
a hash-only bump. My earlier note incorrectly said the Python side
had no equivalent. Took main's version and dropped mine.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N67xH9BejMzZp4GNAq8m7w
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants