Skip to content

Fix: surface swallowed hash-computation errors in calculate_pkg_hashes - #5249

Open
kevinemoore wants to merge 3 commits into
masterfrom
fix/pkgpush-swallowed-hash-error
Open

Fix: surface swallowed hash-computation errors in calculate_pkg_hashes#5249
kevinemoore wants to merge 3 commits into
masterfrom
fix/pkgpush-swallowed-hash-error

Conversation

@kevinemoore

@kevinemoore kevinemoore commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Problem

calculate_pkg_hashes submits per-entry hash computations to a thread pool, then calls concurrent.futures.wait(comp_futures). wait() blocks until every future is done but never reads their results, so any exception raised inside a computation is silently retained on the future and discarded. The observable damage:

  • An AccessDenied writing to a cross-region scratch bucket (or any per-entry failure) is swallowed.
  • entry.hash is left unset, and package creation falls back to an expensive download-and-hash — with no signal that anything went wrong.

This is item 4.5 of the alias-roles 26.8.0 remediation (the swallowed-error half of the cross-region scratch finding). It is filed and reviewed as a standalone PR, separate from the cross-region scratch grant fix (item 4), which is an IAM/template change in the deployment repo.

Fix

Replace wait() with an as_completed() + f.result() loop, re-raising the first exception so the failure is loud. This mirrors the pattern already used for precomp_futures a few lines above.

Test

Added test_calculate_pkg_hashes_propagates_compute_error: mocks compute_checksum_via_copy to raise a dedicated _ScratchAccessDenied, asserts the exception propagates, and asserts the failed entry's hash stays None (the actual regression symptom). Verified it fails (DID NOT RAISE) against the pre-fix wait() code before the fix was applied.

A /code-review pass ran on the branch's final head; the two test-quality findings it raised (bare-Exception assertion; missing symptom assertion) are folded into the test above.

Test environment note

The pkgpush lambda suite runs standalone (no Postgres/Docker needed). Run locally with uv run pytest under lambdas/pkgpush/58 passed including the new regression test. This differs from the registry suite, which needs Postgres + Docker.

Follow-up

  • The CHANGELOG entry currently carries a #PR placeholder link; it will be backfilled with this PR's number.

Greptile Summary

This PR makes package hash computation fail loudly when a worker future raises, rather than silently proceeding with an unset entry hash.

  • Replaces passive waiting with as_completed() and result() so worker exceptions propagate.
  • Adds a regression test covering the scratch-copy failure path and its unset-hash symptom.
  • Documents the behavior change in the pkgpush changelog.

Confidence Score: 5/5

The PR appears safe to merge because it surfaces worker failures as intended without introducing a concrete correctness, security, or lifecycle defect.

The executor context still waits for all submitted work during unwinding, while calling each completed future's result closes the existing exception-swallowing path and the regression test covers the intended failure behavior.

Important Files Changed

Filename Overview
lambdas/pkgpush/src/t4_lambda_pkgpush/init.py Replaces exception-swallowing future waiting with explicit result retrieval while preserving executor shutdown behavior.
lambdas/pkgpush/tests/test_hash_calc.py Adds focused regression coverage proving copy-path exceptions propagate and do not set the failed entry's hash.
lambdas/pkgpush/CHANGELOG.md Records the corrected package-hashing error behavior, with the acknowledged PR-link placeholder pending replacement.

Sequence Diagram

sequenceDiagram
    participant Push as Package push
    participant Pool as Hash worker pool
    participant Worker as Entry hash computation
    Push->>Pool: Submit entry computations
    Pool->>Worker: Compute checksum
    alt computation succeeds
        Worker-->>Pool: Checksum
        Pool-->>Push: Future completes
        Push->>Push: result() returns
    else computation fails
        Worker-->>Pool: Exception
        Pool-->>Push: Future completes
        Push->>Push: result() re-raises exception
    end
Loading

Reviews (1): Last reviewed commit: "Tighten swallowed-hash-error regression ..." | Re-trigger Greptile

Context used:

kevinemoore and others added 3 commits August 27, 2026 20:27
calculate_pkg_hashes submitted per-entry hash computations to a thread
pool and then called concurrent.futures.wait(comp_futures), which blocks
until every future is done but never reads their results. wait() retains
any raised exception on the future silently, so a failure inside a
computation (e.g. an AccessDenied on copy_object writing to a
cross-region scratch bucket) was discarded: entry.hash stayed unset and
package creation fell back to an expensive download-and-hash with no
signal that anything went wrong.

Replace wait() with an as_completed() loop that calls f.result() on each
future, re-raising the first exception so the failure is loud.

Regression test test_calculate_pkg_hashes_propagates_compute_error mocks
compute_checksum_via_copy to raise and asserts the exception propagates;
verified it fails ("DID NOT RAISE Exception") against the wait()-based
code before the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two review findings on the regression test:
- It asserted against bare Exception, which a match string alone does not
  make precise. Introduce a dedicated _ScratchAccessDenied subclass and
  raise/expect that, so the test binds to the intended failure path rather
  than any incidental exception whose message contains "AccessDenied".
- It only asserted that an exception propagates, not the core regression
  symptom. Add an assertion that the failed (copy-path) entry's hash stays
  None, guarding against a future change that surfaces the error but still
  leaves the entry in a bad partial state.

Verified the tightened test still fails ("DID NOT RAISE _ScratchAccessDenied")
against the wait()-based code before the fix. Full pkgpush suite: 58 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 39.67%. Comparing base (1609144) to head (161e7bb).

❗ There is a different number of reports uploaded between BASE (1609144) and HEAD (161e7bb). Click for more details.

HEAD has 11 uploads less than BASE
Flag BASE (1609144) HEAD (161e7bb)
py-shared 1 0
api-python 10 0
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #5249       +/-   ##
===========================================
- Coverage   54.70%   39.67%   -15.03%     
===========================================
  Files         869      757      -112     
  Lines       36337    26283    -10054     
  Branches     6424     6424               
===========================================
- Hits        19877    10428     -9449     
+ Misses      14716    14111      -605     
  Partials     1744     1744               
Flag Coverage Δ
api-python ?
catalog 34.87% <ø> (ø)
lambda 85.38% <100.00%> (+<0.01%) ⬆️
py-shared ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@QuiltSimon QuiltSimon mentioned this pull request Aug 31, 2026
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant