Repository navigation
Conversation
anth-volk
added this pull request to stack #25
October 19, 2026 21:42
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
from
October 20, 2026 13:00
8bfcbdd to
14c0240
Compare
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
2 times, most recently
from
October 20, 2026 13:33
5948c03 to
b7c7422
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The Hugging Face module ran the collector cycle inside the host's tick, after the Hugging Face run files, and uploads had no write timeout. A slow or stalled submission therefore delayed the run-file commits a build waits for, and stopped the commit at shutdown. The fake collector can now hold uploads on an event. The tests cover the #27 review's two cases: with an upload held, run files and a release publication still land, and in a host runtime with a 2-second shutdown period the commit at shutdown still runs. Three more ask for a finite write timeout, a time limit on each upload, and a time budget per collector cycle. Red: 5 failed, 13 passed. Run files never reached the staging repository; the runtime reported "LocalServiceError: Service worker failed (TimeoutError)"; UPLOAD_TIMEOUT.write was None; upload_time_limit and MAX_CYCLE_SECONDS did not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- Collector submissions run on their own DeliveryWorker, the class the Hugging Face module already uses for bundles and release changes. tick no longer runs the collector cycle (except when the module runs without workers, for tests driven by hand), so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back. close releases the collector's HTTP client and the queue only once that worker has stopped. - Uploads get a 60-second write timeout, so a stalled upload fails within a minute. Each upload also stops after upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks and the limit is checked before each one (UploadTimeLimitError becomes a pending UPLOAD_FAILED). - A collector cycle starts no new submission after MAX_CYCLE_SECONDS (ten minutes); with the limits on every request and upload, one cycle's length is bounded. The two runtime tests use the real clock, as the host does: a submission waiting for its Hugging Face commit is retried seconds later. Green: 18 passed; 153 passed across #27's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
from
October 20, 2026 17:47
28dcec2 to
19cac1b
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
From the #27 review's code-quality notes: - an error queueing the collector row, after the Hugging Face row was queued, turned the whole bundle answer into an error; - an unusable collector setting (a non-loopback development URL) stopped the Hugging Face module from starting; - an unexpected exception during a submission left the collector row leased for the full hour. Red: 3 failed, 18 passed: "RuntimeError: database is locked", "ValueError: development collector URL must use a loopback address", "RuntimeError: synthetic". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 20, 2026
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The Hugging Face module ran the collector cycle inside the host's tick, after the Hugging Face run files, and uploads had no write timeout. A slow or stalled submission therefore delayed the run-file commits a build waits for, and stopped the commit at shutdown. The fake collector can now hold uploads on an event. The tests cover the #27 review's two cases: with an upload held, run files and a release publication still land, and in a host runtime with a 2-second shutdown period the commit at shutdown still runs. Three more ask for a finite write timeout, a time limit on each upload, and a time budget per collector cycle. Red: 5 failed, 13 passed. Run files never reached the staging repository; the runtime reported "LocalServiceError: Service worker failed (TimeoutError)"; UPLOAD_TIMEOUT.write was None; upload_time_limit and MAX_CYCLE_SECONDS did not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- Collector submissions run on their own DeliveryWorker, the class the Hugging Face module already uses for bundles and release changes. tick no longer runs the collector cycle (except when the module runs without workers, for tests driven by hand), so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back. close releases the collector's HTTP client and the queue only once that worker has stopped. - Uploads get a 60-second write timeout, so a stalled upload fails within a minute. Each upload also stops after upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks and the limit is checked before each one (UploadTimeLimitError becomes a pending UPLOAD_FAILED). - A collector cycle starts no new submission after MAX_CYCLE_SECONDS (ten minutes); with the limits on every request and upload, one cycle's length is bounded. The two runtime tests use the real clock, as the host does: a submission waiting for its Hugging Face commit is retried seconds later. Green: 18 passed; 153 passed across #27's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
from
October 20, 2026 18:47
19cac1b to
e463794
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
From the #27 review's code-quality notes: - an error queueing the collector row, after the Hugging Face row was queued, turned the whole bundle answer into an error; - an unusable collector setting (a non-loopback development URL) stopped the Hugging Face module from starting; - an unexpected exception during a submission left the collector row leased for the full hour. Red: 3 failed, 18 passed: "RuntimeError: database is locked", "ValueError: development collector URL must use a loopback address", "RuntimeError: synthetic". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #27 fix review found that `close` waits for every worker, the collector worker included, before the second shutdown commit. When a collector upload is still running at shutdown, that wait lasts until the deadline and the second commit is skipped, so the run files of the repository a bundle upload held are not committed although that upload finished in time. On Modal they are lost with the container. The test follows the review's case: a described bundle's collector upload is held, a bundle upload to the staging repository is held for half a second, run files for the staging repository are handed over, and the module closes with a three-second deadline. The staging run files must land. Red: 1 failed, 21 passed (`AssertionError: the staging run files missed the second shutdown commit`), in two runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #27 fix review found that every upload stops after `upload_time_limit(size)`, which assumes at least 2.1 Mbit/s: a large file on a slower link (399 MiB at 1.5 Mbit/s, for example) is stopped on every attempt, starts again from zero, and is never submitted. The fake collector can now give each connection a small receive buffer and read an upload's body slowly, and the client tests use a client with a small send buffer, so the client's sends wait for the fake to read. Two tests on a 2.5 MiB file: - the fake reads 64 KiB every 20 ms (about a second in all), each send makes progress well within a 1-second stall threshold, and any limit on the upload's total time derived from the file's size is set as short as the client allows (`monkeypatch.setattr(..., raising=False)`, so the test still runs once such a limit is gone): the upload must complete; - the fake reads nothing: the upload must fail with `UPLOAD_FAILED` after the 0.5-second stall threshold. This one passes today, because the per-send write timeout already detects a stall. Red: 1 failed, 41 passed (`AssertionError: an upload that kept making progress was stopped`; the outcome was `('pending', 'UPLOAD_FAILED')` after about 0.4 s, at the second 1 MiB piece), in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
claude/collector-artifact-submission
branch
from
October 20, 2026 20:01
e463794 to
423139b
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The Hugging Face module ran the collector cycle inside the host's tick, after the Hugging Face run files, and uploads had no write timeout. A slow or stalled submission therefore delayed the run-file commits a build waits for, and stopped the commit at shutdown. The fake collector can now hold uploads on an event. The tests cover the #27 review's two cases: with an upload held, run files and a release publication still land, and in a host runtime with a 2-second shutdown period the commit at shutdown still runs. Three more ask for a finite write timeout, a time limit on each upload, and a time budget per collector cycle. Red: 5 failed, 13 passed. Run files never reached the staging repository; the runtime reported "LocalServiceError: Service worker failed (TimeoutError)"; UPLOAD_TIMEOUT.write was None; upload_time_limit and MAX_CYCLE_SECONDS did not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- Collector submissions run on their own DeliveryWorker, the class the Hugging Face module already uses for bundles and release changes. tick no longer runs the collector cycle (except when the module runs without workers, for tests driven by hand), so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back. close releases the collector's HTTP client and the queue only once that worker has stopped. - Uploads get a 60-second write timeout, so a stalled upload fails within a minute. Each upload also stops after upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks and the limit is checked before each one (UploadTimeLimitError becomes a pending UPLOAD_FAILED). - A collector cycle starts no new submission after MAX_CYCLE_SECONDS (ten minutes); with the limits on every request and upload, one cycle's length is bounded. The two runtime tests use the real clock, as the host does: a submission waiting for its Hugging Face commit is retried seconds later. Green: 18 passed; 153 passed across #27's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
From the #27 review's code-quality notes: - an error queueing the collector row, after the Hugging Face row was queued, turned the whole bundle answer into an error; - an unusable collector setting (a non-loopback development URL) stopped the Hugging Face module from starting; - an unexpected exception during a submission left the collector row leased for the full hour. Red: 3 failed, 18 passed: "RuntimeError: database is locked", "ValueError: development collector URL must use a loopback address", "RuntimeError: synthetic". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #27 fix review found that `close` waits for every worker, the collector worker included, before the second shutdown commit. When a collector upload is still running at shutdown, that wait lasts until the deadline and the second commit is skipped, so the run files of the repository a bundle upload held are not committed although that upload finished in time. On Modal they are lost with the container. The test follows the review's case: a described bundle's collector upload is held, a bundle upload to the staging repository is held for half a second, run files for the staging repository are handed over, and the module closes with a three-second deadline. The staging run files must land. Red: 1 failed, 21 passed (`AssertionError: the staging run files missed the second shutdown commit`), in two runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #27 fix review found that every upload stops after `upload_time_limit(size)`, which assumes at least 2.1 Mbit/s: a large file on a slower link (399 MiB at 1.5 Mbit/s, for example) is stopped on every attempt, starts again from zero, and is never submitted. The fake collector can now give each connection a small receive buffer and read an upload's body slowly, and the client tests use a client with a small send buffer, so the client's sends wait for the fake to read. Two tests on a 2.5 MiB file: - the fake reads 64 KiB every 20 ms (about a second in all), each send makes progress well within a 1-second stall threshold, and any limit on the upload's total time derived from the file's size is set as short as the client allows (`monkeypatch.setattr(..., raising=False)`, so the test still runs once such a limit is gone): the upload must complete; - the fake reads nothing: the upload must fail with `UPLOAD_FAILED` after the 0.5-second stall threshold. This one passes today, because the per-send write timeout already detects a stall. Red: 1 failed, 41 passed (`AssertionError: an upload that kept making progress was stopped`; the outcome was `('pending', 'UPLOAD_FAILED')` after about 0.4 s, at the second 1 MiB piece), in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The round-2 review of #27 (NEW BUGS 1) found that over HTTPS the upload's 60-second write timeout does not measure progress. CPython's `ssl` does no partial writes, and its write timeout covers each whole call, so the timeout covers each whole 1 MiB piece the client sends. A link slower than about 140 kbit/s therefore fails partway and starts again from zero, though the upload keeps making progress. The fake collector gains `tls_uploads`: it signs upload URLs for a second loopback server that speaks TLS, as Cloud Storage does, while the collector stays plain HTTP. The server has no certificate: both sides use only anonymous TLS 1.2 cipher suites, so no key material is generated or committed. The new test reads uploads at about 1 MiB/s, sets the stall threshold to 0.5 s and uploads 1.5 MiB: the link keeps making progress, but a 1 MiB piece takes about a second to write. Red: 1 failed, 42 passed, the same in three runs. The failure is the stated reason: `an HTTPS upload that kept making progress was stopped` (`('pending', 'UPLOAD_FAILED')`). With the threshold raised to 5 s the same test passes on this code, so the TLS connection works and the failure is the write timeout on the first piece. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The round-2 review of #27 (NEW BUGS 2) found that the collector row's one-hour lease is never renewed during an attempt. Every emitter of a user on one machine shares the hand-over database by default, so once an attempt outlasts the lease, a second emitter claims the row and uploads the same file in parallel, and the first attempt's result is dropped. The new tests shorten the lease to 1 s (and a renewal interval, which the code before the fix does not have, to 0.2 s), and run a second collector delivery on the same hand-over database with a client that records each submission and sends nothing: - while the first emitter's upload is held for two and a half leases, the second emitter must claim nothing; once released, the first emitter's result must be recorded; - a guard: after shutdown abandons the held upload, the second emitter must be able to claim the row within three leases. It passes before the fix (nothing renews the lease) and must keep passing after it, so renewal has to stop at shutdown. `described_bundle_with_a_held_upload` now returns the hand-over it stages, so a test can read its receipt. Red: 1 failed, 24 passed. The failure is the stated reason: `a second emitter claimed the collector row while its upload was running`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add CollectorProvenance to the interface: the country, the build (the attempt ID), whether the build used licensed microdata, and the release, or None for a staged build. It is checked with the same rules the collector's submission contract applies. StageDatasetBundle takes it as an optional collector field. The message carries it only when given, so a bundle without it keeps its message and its hand-over ID. The interface also names the bundle roles of the Orrery inputs the collector projects from: orrery_schema, execution_overlay, orrery_upstream and orrery_evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bundle that describes its build for the collector produces one submission per kind whose required roles it names. calibration_diagnostics gives a calibration-diagnostics submission, with any build_manifest, release_manifest and demographics file. orrery_schema and execution_overlay together give an orrery-inputs submission, with any orrery_upstream and orrery_evidence files, under the collector's role names. Dataset files and the bundle's own sidecars are not submitted. The provenance is the bundle's run, the described build, country and release, and the Hugging Face repo, the commit that staged the bundle, and its folder. Planning selects files by role only; it never reads them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CollectorSubmissionClient makes one attempt at the collector's submission contract: exchange the build's Hugging Face token for an artifacts:submit credential, create the submission, PUT each file the collector still lacks to its signed URL with exactly the headers it gave, then complete it. - Before contacting the collector it checks every preserved file's size and SHA-256: a missing file is retried later, changed bytes fail. - Files go only to https://storage.googleapis.com, or to a loopback address when the caller allows it for development; any other upload URL fails. - A 412 from storage means the file is already there. - The 404 a collector answers while intake is off, outages, 408, 429 and 5xx leave the submission pending; a 401 drops the session token first. A rejected Hugging Face credential and other 4xx answers are final, and a 409 at completion retries the whole attempt. CollectorSession gains an optional scope; without one the exchange body is still {}, so telemetry credentials are unchanged. Tests run against a loopback fake collector. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mmit When collector submissions are switched on, the huggingface module gives a bundle hand-over that describes its build a second row in the hand-over queue, for the collector destination; handover_jobs already has a row per hand-over and destination, so no table changes. That row waits until the Hugging Face row has staged the bundle, since the provenance names that commit, then submits each planned submission until the collector accepts or refuses it. A bundle Hugging Face refuses is skipped. The module's tick runs collector delivery after Hugging Face delivery, and collector_submission_receipt reports the result. Collector submissions are off unless the module configuration sets collector_submissions to true, which no interface release does, so this emitter changes nothing for production builds. development_collector_url points the module at a loopback collector. The request parser checks the optional collector description with the interface's rules and refuses a bundle whose description is invalid. A new protocol fixture carries a described bundle; the released fixtures are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… restack The release writer's pull request now defines `UPLOAD_FAILED`, `PRESERVED_FILES_MISSING` and `PRESERVED_BYTES_CHANGED` once, in `huggingface.service.hub`, file hashing once, in the queue, and makes the credential lookup public (`core.auth.huggingface_token`). The collector submission client and the module's collector credential now use them; the module no longer imported at all after the rename. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Hugging Face module now copies a bundle's small files into the queue at hand-over, because Microcosm rewrites its build manifest, checksum listing and build record right after staging. The collector submission still re-hashed every file from its path, so a calibration-diagnostics submission, which includes the build manifest, would fail with `PRESERVED_BYTES_CHANGED` for every Microcosm build. The collector row now uses the same copies: copied files are uploaded from the queue and not re-hashed, and only the files too large to copy are read from their paths. The collector row is queued with the copies, so they are kept until both destinations have settled. The test of a bundle Hugging Face refuses now changes a file too large to copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failed `test_every_fixture_has_a_typed_call_that_produces_it`: the `huggingface-stage-dataset-bundle-with-collector` fixture had no typed call. The message tests now build that bundle with its `CollectorProvenance` and check that `stage_dataset_bundle` sends exactly the recorded message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contract: microcosm-collector#6 at 673df3d, "When intake is not available". A collector released before intake answers the exchange with 200 and a `telemetry:write` token, and its submission paths with 404, so the hand-over ended `failed` as `COLLECTOR_REFUSED_404`. - A scoped `CollectorSession` accepts a token only when the answer's `scope` is the one it asked for. Any other 200, or a 404 from the exchange (`intake_not_enabled`), is `intake_unavailable`. Telemetry sessions, which ask for no scope, are unchanged. - The submission client then discards the token, stays `pending` with `COLLECTOR_INTAKE_NOT_ENABLED`, and sends nothing to that collector, not even an exchange, for an hour. The collector row is attempted again after that hour (`finish_job` takes a `retry_after`). - The fake collector answers the exchange with `scope`, and can act as a collector with intake `off` or `absent`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contract: microcosm-collector#6 at 673df3d, "Collector answers". A 404 means "create again" only with the code `submission_not_found`, and only for a request about one submission (completion, status, receipt): the client stays `pending` with `SUBMISSION_NOT_FOUND`, and its next attempt creates the submission again. Every other 404, including any 404 to create, means the collector has no intake: the client discards the token and waits an hour (`COLLECTOR_INTAKE_NOT_ENABLED`). Before, every 404 was a final `COLLECTOR_REFUSED_404`. The fake collector answers an unknown submission's completion with `submission_not_found`, and tests can choose an answer's `code`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contract: microcosm-collector#6 at 673df3d, "Collector answers" and "Receipt". Completion answers 200 with the final status document, and with status `rejected` and the code `stored_object_mismatch` when a stored object has another size or recorded SHA-256. Every 200 was recorded as `accepted`. A completion whose status is `rejected` or `failed` now ends `failed`, with the rejection's code as the error code (or `COLLECTOR_REJECTED` without one) and the collector's status. The fake collector answers with the contract's status documents, including `files_sha256` and `job`, and rejects a stored-object mismatch with 200 instead of 409. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contract: microcosm-collector#6 at 673df3d, "Collector answers". A 403 without a `code` means the token's scope does not allow the request. The client now discards the token, exchanges again for `artifacts:submit` and repeats the request once; only a second such refusal is final (`COLLECTOR_REFUSED_403`). A 403 with `submission_owned_by_another_user` stays final at once. The fake collector refuses a token of another scope with a 403 without a code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contract: microcosm-collector#6 at 673df3d: `provenance.huggingface.commit` is the commit that added the folder, never a later commit. When the Hub did not report the last commit of an already-staged bundle's inventory (an error, or no commit in the answer), `stage_bundle` fell back to the repository head, and the collector submission named that head. It now stays `pending` with `REPOSITORY_UNAVAILABLE` and asks again at the next attempt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contract: microcosm-collector#6 at 673df3d: `provenance.huggingface.folder` holds at most 512 characters, and a longer one is refused with 422, which is final. A bundle's `<prefix>/<run_id>` can be longer, so its collector row is now settled as `skipped` with `COLLECTOR_FOLDER_TOO_LONG` before anything is sent; the Hugging Face upload is unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contract: microcosm-collector#6 at 673df3d, "Signed uploads": each instruction's headers now include `Host`, signed with the others, and the client sends every header unchanged. httpx sends a given `Host` as is, so the uploads already worked; an instruction whose `Host` differs from its URL's host is now refused as `UPLOAD_URL_REFUSED` before any upload. The fake collector lists `Host` in its instructions and records the `Host` each upload arrives with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documentation only: - `CollectorProvenance` and the README: only the Orrery outputs of a build that used licensed microdata stay private until cleared; calibration data may be public, licensed or not (decision D10, 2026-10-20); native evidence is never published. The docstring said everything derived from a licensed build stays private. - README: how the module treats the collector's answers under collector#6's contract at 673df3d, the commit and folder rules, and that it does not poll status or fetch receipts yet. - Changelog fragment updated to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Hugging Face module ran the collector cycle inside the host's tick, after the Hugging Face run files, and uploads had no write timeout. A slow or stalled submission therefore delayed the run-file commits a build waits for, and stopped the commit at shutdown. The fake collector can now hold uploads on an event. The tests cover the #27 review's two cases: with an upload held, run files and a release publication still land, and in a host runtime with a 2-second shutdown period the commit at shutdown still runs. Three more ask for a finite write timeout, a time limit on each upload, and a time budget per collector cycle. Red: 5 failed, 13 passed. Run files never reached the staging repository; the runtime reported "LocalServiceError: Service worker failed (TimeoutError)"; UPLOAD_TIMEOUT.write was None; upload_time_limit and MAX_CYCLE_SECONDS did not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Collector submissions run on their own DeliveryWorker, the class the Hugging Face module already uses for bundles and release changes. tick no longer runs the collector cycle (except when the module runs without workers, for tests driven by hand), so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back. close releases the collector's HTTP client and the queue only once that worker has stopped. - Uploads get a 60-second write timeout, so a stalled upload fails within a minute. Each upload also stops after upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks and the limit is checked before each one (UploadTimeLimitError becomes a pending UPLOAD_FAILED). - A collector cycle starts no new submission after MAX_CYCLE_SECONDS (ten minutes); with the limits on every request and upload, one cycle's length is bounded. The two runtime tests use the real clock, as the host does: a submission waiting for its Hugging Face commit is retried seconds later. Green: 18 passed; 153 passed across #27's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the #27 review's code-quality notes: - an error queueing the collector row, after the Hugging Face row was queued, turned the whole bundle answer into an error; - an unusable collector setting (a non-loopback development URL) stopped the Hugging Face module from starting; - an unexpected exception during a submission left the collector row leased for the full hour. Red: 3 failed, 18 passed: "RuntimeError: database is locked", "ValueError: development collector URL must use a loopback address", "RuntimeError: synthetic". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An error queueing a bundle's collector row is printed once (its type only), and the bundle's answer is its Hugging Face receipt, which is already queued. - Collector settings the module cannot use (a non-loopback development URL today) switch collector submissions off with one warning; the Hugging Face module starts as usual. - An unexpected exception during a submission is recorded as a pending attempt (LOCAL_ERROR) that keeps earlier submissions' results, so the row is retried after the usual backoff instead of staying leased for an hour. Green: 21 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #27 fix review found that `close` waits for every worker, the collector worker included, before the second shutdown commit. When a collector upload is still running at shutdown, that wait lasts until the deadline and the second commit is skipped, so the run files of the repository a bundle upload held are not committed although that upload finished in time. On Modal they are lost with the container. The test follows the review's case: a described bundle's collector upload is held, a bundle upload to the staging repository is held for half a second, run files for the staging repository are handed over, and the module closes with a three-second deadline. The staging run files must land. Red: 1 failed, 21 passed (`AssertionError: the staging run files missed the second shutdown commit`), in two runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`close` now waits for the collector worker separately: 1. it stops every worker; the collector cycle now receives the stop check and claims no further hand-over once shutdown begins; 2. it makes the first shutdown commit, waits until the deadline for the Hugging Face workers only (bundles and release changes), and makes the second shutdown commit when they stopped in time; 3. only then does it wait for a collector submission in progress, for at most `COLLECTOR_SHUTDOWN_WAIT_SECONDS` (2 s) and never past the deadline. Collector work therefore never uses up the time the Hugging Face shutdown commits need, and a busy collector worker no longer holds the process for the whole shutdown period. A submission still running is abandoned with the process, as before; its row is retried once its lease expires. The HTTP client and the queue stay open while any worker still runs. Two tests come with the fix: the bounded collector wait, and a collector cycle that claims nothing once stop is requested. The README's shutdown sentence for collector submissions now says this. Green: 24 passed (`tests/submissions/test_delivery.py`); every test file this branch adds or changes: 159 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #27 fix review found that every upload stops after `upload_time_limit(size)`, which assumes at least 2.1 Mbit/s: a large file on a slower link (399 MiB at 1.5 Mbit/s, for example) is stopped on every attempt, starts again from zero, and is never submitted. The fake collector can now give each connection a small receive buffer and read an upload's body slowly, and the client tests use a client with a small send buffer, so the client's sends wait for the fake to read. Two tests on a 2.5 MiB file: - the fake reads 64 KiB every 20 ms (about a second in all), each send makes progress well within a 1-second stall threshold, and any limit on the upload's total time derived from the file's size is set as short as the client allows (`monkeypatch.setattr(..., raising=False)`, so the test still runs once such a limit is gone): the upload must complete; - the fake reads nothing: the upload must fail with `UPLOAD_FAILED` after the 0.5-second stall threshold. This one passes today, because the per-send write timeout already detects a stall. Red: 1 failed, 41 passed (`AssertionError: an upload that kept making progress was stopped`; the outcome was `('pending', 'UPLOAD_FAILED')` after about 0.4 s, at the second 1 MiB piece), in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A signed upload is one `PUT` that stores nothing unless it completes, so a limit on its total time only makes a slow upload start again from zero. The client no longer has one: `upload_time_limit`, `chunks_until`, `UploadTimeLimitError`, `MIN_UPLOAD_SECONDS` and `MIN_UPLOAD_BYTES_PER_SECOND` are gone. Stalls are still detected by `UPLOAD_TIMEOUT`'s write timeout, which httpcore applies to each send to the connection: a send that has had no byte accepted for 60 seconds fails, and the attempt stays `pending` with `UPLOAD_FAILED`. Connecting and waiting for the answer keep their 60-second limits. Files are still sent in 1 MiB pieces with an explicit `Content-Length`. The collector cycle's ten-minute budget already only stops new submissions from starting; its comment now says it never stops one in progress. The lease comment now says an attempt can outlast its one-hour lease, which is not renewed during an attempt. `test_an_upload_stops_once_it_outlasts_its_time_limit` tested the removed functions and is removed. The README and the changelog fragment describe stall detection instead of the size-based limit. Green: 42 passed (`tests/submissions/test_client.py`); every test file this branch adds or changes: 160 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #27 (NEW BUGS 1) found that over HTTPS the upload's 60-second write timeout does not measure progress. CPython's `ssl` does no partial writes, and its write timeout covers each whole call, so the timeout covers each whole 1 MiB piece the client sends. A link slower than about 140 kbit/s therefore fails partway and starts again from zero, though the upload keeps making progress. The fake collector gains `tls_uploads`: it signs upload URLs for a second loopback server that speaks TLS, as Cloud Storage does, while the collector stays plain HTTP. The server has no certificate: both sides use only anonymous TLS 1.2 cipher suites, so no key material is generated or committed. The new test reads uploads at about 1 MiB/s, sets the stall threshold to 0.5 s and uploads 1.5 MiB: the link keeps making progress, but a 1 MiB piece takes about a second to write. Red: 1 failed, 42 passed, the same in three runs. The failure is the stated reason: `an HTTPS upload that kept making progress was stopped` (`('pending', 'UPLOAD_FAILED')`). With the threshold raised to 5 s the same test passes on this code, so the TLS connection works and the failure is the write timeout on the first piece. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`UPLOAD_CHUNK_BYTES` drops from 1 MiB to 64 KiB, the size httpx itself reads files in. Each piece is one send under the 60-second write timeout. Over HTTPS, where CPython's `ssl` writes a piece whole under one timeout, the slowest link that never fails now averages 65,536 bytes per 60 seconds over each piece: 1,093 bytes a second, about 8.7 kbit/s, instead of about 140 kbit/s. Over plain HTTP nothing changes: a send still fails only after 60 seconds in which no byte was accepted. The comment at `UPLOAD_TIMEOUT`, the cycle budget's comment, the README and the changelog fragment now state that exact rule and rate instead of "an upload that keeps making progress is never stopped, however slow the link". One comment in `test_client.py` that named the old 1 MiB piece size now describes the file size without it; the new test is unchanged. Green: 43 passed, the same in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #27 (NEW BUGS 2) found that the collector row's one-hour lease is never renewed during an attempt. Every emitter of a user on one machine shares the hand-over database by default, so once an attempt outlasts the lease, a second emitter claims the row and uploads the same file in parallel, and the first attempt's result is dropped. The new tests shorten the lease to 1 s (and a renewal interval, which the code before the fix does not have, to 0.2 s), and run a second collector delivery on the same hand-over database with a client that records each submission and sends nothing: - while the first emitter's upload is held for two and a half leases, the second emitter must claim nothing; once released, the first emitter's result must be recorded; - a guard: after shutdown abandons the held upload, the second emitter must be able to claim the row within three leases. It passes before the fix (nothing renews the lease) and must keep passing after it, so renewal has to stop at shutdown. `described_bundle_with_a_held_upload` now returns the hand-over it stages, so a test can read its receipt. Red: 1 failed, 24 passed. The failure is the stated reason: `a second emitter claimed the collector row while its upload was running`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A collector row's lease drops from a fixed hour to 120 seconds (`SUBMISSION_LEASE_SECONDS`) and is renewed every 30 seconds (`SUBMISSION_LEASE_RENEWAL_SECONDS`) while an attempt runs, through the same `HeldLeases` mechanism the Hugging Face jobs use. `HeldLeases` gains `for_job`, which holds a job without a repository lease, and an optional stop check: renewal ends at `stop` or once shutdown has begun. So: - another emitter on the machine, which shares the hand-over database, never claims the row while an upload runs, however long it takes; - the attempt finishes with the renewed lease value, so its result is recorded instead of dropped; - an attempt abandoned at shutdown, or an emitter that was killed, frees the row within two minutes instead of an hour. `_attempt` now returns an `AttemptResult`, and `run_cycle` stops the renewal before it records that result. The Hugging Face leases are unchanged: they are built without the stop check. The lease comment, the README and the changelog fragment describe the renewed two-minute lease instead of the fixed hour. Green: 25 passed, the same in three runs; `test_leases.py`, `test_delivery_workers.py` and `test_client.py`: 54 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #27 (CODE QUALITY 3) found that `submit` fetches the collector token once, before the uploads, and completes the submission with it afterwards. An upload longer than the token's life (an hour) therefore gets 401 at completion and waits for the next attempt, with a misleading `COLLECTOR_UNAVAILABLE` receipt meanwhile. The fake collector now issues tokens with a lifetime the test chooses (`token_lifetime_seconds`, 900 s by default) and answers an expired one with 401, as the collector does. The new test gives tokens one second, holds the upload for a second and a half, and expects the submission to be accepted, with a second exchange for the completion. Red: 1 failed, 43 passed. The failure is the stated reason: `the completion after a long upload used an expired token` (`('pending', 'COLLECTOR_UNAVAILABLE')`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`submit` now calls `_token()` once more after the uploads and before the completion request. The session hands back the same token while it is valid and exchanges for a new one only when it is about to expire, so a submission whose uploads outlast the token (an hour) completes in the same attempt instead of getting 401 and a `COLLECTOR_UNAVAILABLE` receipt until the next one. The README says so. Green: 44 passed, the same in three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failed the new HTTPS slow-upload test on both macOS jobs, though it passed on Ubuntu and locally. Measuring each piece's write showed why: a loopback TCP connection on macOS grows its receive buffer to about 280 KiB whatever `SO_RCVBUF` the listening socket or the accepted connection is given. The buffer then absorbs several 64 KiB pieces at once, and the next piece waits up to about 0.3 s for the window to open, close to the test's 0.5 s threshold. That is the kernel's buffering, not the client's piece size, so the test depended on the machine. The fake's TLS upload server now listens on a Unix socket, which keeps the buffer it is given. The test client sends `https://` requests over that socket (an httpx mount with `uds=`), and the upload URLs still name `https://127.0.0.1`, so the client's checks on upload URLs are the same. Each 64 KiB piece now takes a steady 70 to 80 ms against the 0.5 s threshold. Checked both ways, with the test as it stands here: with `UPLOAD_CHUNK_BYTES` temporarily set back to 1 MiB, the test fails three times out of three with `an HTTPS upload that kept making progress was stopped`; with 64 KiB it passes three times out of three. It also passes under Homebrew's Python 3.14 (OpenSSL 3.6.5), which the macOS CI jobs use for 3.14. `test_client.py`: 44 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Fixes #26
Stacked on #24 (stack #25: #21 → #22 → #23 → #24 → this PR). The base is
feat/release-writer-releases-json, so the diff shows only this change. It implements the emitter side of the collector's artifact intake: PolicyEngine/microcosm-collector#6 (the contract) and PolicyEngine/microcosm-collector#12 (the endpoints and worker).Summary
The
huggingfacemodule now also sends what a staged dataset bundle names to the collector, after the bundle's Hugging Face commit. Microcosm needs no separate call for it. Off by default.CollectorProvenance). A bundle hand-over doesn't say which country or build it is, or whether its data is licensed, and the collector needs all three.StageDatasetBundletherefore takes an optionalcollector=CollectorProvenance(country_code, build_id, licensed_data, release=None), checked with the collector contract's rules.submissions/planning.py, by role only; files are never read or interpreted):calibration_diagnosticsfile gives acalibration-diagnosticssubmission, with anybuild_manifest,release_manifestanddemographicsfile.orrery_schemaplusexecution_overlaygive anorrery-inputssubmission, with anyorrery_upstreamandorrery_evidencefiles, mapped to the collector's role names. The interface exports these bundle role names.collectorrow inhandover_jobs(one row per hand-over and destination, so no new table) waits for itshuggingfacerow (WAITING_FOR_HUGGING_FACE). A bundle Hugging Face refuses isskipped.submissions/client.py):artifacts:submitcredential.CollectorSessiongains an optionalscope; telemetry sessions still send{}. A scoped session uses a token only when the answer says"scope": "artifacts:submit".Host(412 means already stored), then complete it. An instruction whoseHostdiffers from its URL's host is refused.https://storage.googleapis.com, or to loopback when a development collector is configured.673df3d):pending, retried with the queue's backoff: outages, 408/429/5xx, a 409 at completion, and a 404submission_not_foundat completion (the next attempt creates the submission again).pendingfor an hour, with no request at all to that collector meanwhile (COLLECTOR_INTAKE_NOT_ENABLED): an exchange answered 200 without theartifacts:submitscope (a collector released before intake) or 404, and any other 404 from a submission path, including one to create. The token is discarded.code(the token's scope does not allow the request) is followed by one fresh exchange and retry; only a second such refusal is final.failed: refusals (including 403submission_owned_by_another_user), changed bytes, and completion answeringrejectedorfailed(for examplestored_object_mismatch), with the rejection's code.skipped: a bundle Hugging Face refused, or whose folder is longer than the collector's 512 characters (COLLECTOR_FOLDER_TOO_LONG).submitted: success, with each submission's ID and collector status, readable throughcollector_submission_receipt.collector_submissions: true, which no interface release sends.development_collector_urlpoints at a loopback collector. Switching production builds on is a separate emitter change that flips this default, so this release changes nothing for running builds.huggingface-stage-dataset-bundle-with-collector.json) carries a described bundle. Released fixtures are unchanged.Not in this PR
publish_release). The same mechanism would submit a release's calibration diagnostics with release provenance, which the calibration-tree worker needs to record each release publication. It is a follow-up, kept out to keep this bounded.hand_over_orrery_inputs, the graph publication queue to microcosm-runs) is unchanged. Its role set has no execution overlay, so the collector gets the Orrery inputs from the bundle instead. O1 (microcosm#1295) already registersgraph.execution.jsonas a build output.GET /v1/submissions/{id}/receipt; the emitter doesn't poll for it.Commits (red, then green)
tests/support/fake_collector.py).Each commit's own test files were run again after committing.
Testing
tests/interface/test_collector_provenance.py,tests/submissions/andtests/interface/test_protocol_fixtures.py, 66 passed. They use temporary migrated databases, the in-memory Hub, synthetic credentials and loopback servers only.tools/check-quality.shandtools/check-changelog.shpass.core/auth.py,huggingface/service/module.py,requests.py,bundles.py). CI covers them.Deployment verification
No workflow, Dockerfile or release script changes. New and reused settings:
collector_submissions(module configuration)HuggingFaceSettings.configurationsends onlyhandover_database_path, so it is off in every build.development_collector_url(module configuration)_development_collector_urlcheck).PRODUCTION_COLLECTOR_URLconstantHF_TOKEN,HUGGINGFACE_TOKENor the savedhf auth logintokenhuggingface_token(#24), and sent only to the collector's token exchange.Coordination
bundles.py,__init__.py,huggingface/service/module.py,requests.py,tests/interface/test_protocol_fixtures.py). It was restacked on their review fixes on 2026-10-20 (below).Restack and collector contract fixes (2026-10-20)
Restacked on the review fixes to #21–#24, then changed to match collector#6 (contract,
673df3d) and collector#12 (intake,30dbd1b). Each commit red, then green; only the test files this pull request adds or changes were run, with loopback services only.4598ed7Use the shared hand-over codes and public credential lookup after the restacktests/test_boundaries.py,tests/submissions: 2 failed, 10 errors, 38 passed9c5447fSubmit the bundle files copied at hand-over to the collectortests/submissions: 1 failed, 39 passedtest_bundles.pyb7c7422Send the described-bundle fixture through a typed call in the teststest_every_fixture_has_a_typed_call_that_produces_it.test_client_messages.py: 1 failed, 28 passed (as in CI)906cd4eUse a submit token only when the exchange grants that scopetests/submissions: 21 failed, 23 passed (most from the client's newclockargument)tests/telemetry/test_delivery.py42b65d2Create a submission again only for a 404 submission_not_foundtest_client.py: 5 failed, 26 passedtests/submissions: 49 passed517d1b6Fail a submission whose completion answers rejected or failedstored_object_mismatchwith 200rejected, and its status documents carryfiles_sha256andjob.0130888Exchange again for the submit scope after a 403 without a code62badd7Report an already-staged bundle only with the commit that added itlast_commitfails or reports nothing.test_bundles.py: 2 failed, 28 passedtests/submissionsaf1e35dSkip a collector submission whose folder is longer than 512 characterstest_delivery.py: 1 failed, 12 passedtests/submissions: 57 passedc3b6082Refuse an upload instruction whose Host header names another hostHost. SendingHostas given already worked (that test passed before).test_client.py: 1 failed, 39 passed28dcec2Describe the collector answers and decision D10's disclosure rulesItem 9 (the fake collector:
scope, errorcodes,files_sha256andjob,Host,stored_object_mismatchas 200rejected) came with items 1–4 and 7. Not done, as asked: polling the submission's status and fetching its receipt.Re-run after the last commit: every test file this pull request adds or changes, 148 passed; with
tests/test_boundaries.py,tests/huggingfaceandtests/telemetry/test_delivery.py, 167 passed.tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass. The earlier four commits' hashes changed in the restack.Review fixes (2026-10-20, first review of this pull request)
Each fix is a commit that adds a test failing because of the bug, then a commit with the code. Only the test files this pull request adds or changes were run, with loopback services only.
2ea1dd6→46012a2Submit to the collector on a worker of its own, with time limitstickran the collector cycle after the Hugging Face cycle on the module's one worker thread. Uploads had no write timeout (httpx.Timeout(60.0, write=None)), and uncopied files were re-hashed on that thread before every attempt. So a slow or stalled submission delayed the Hugging Face commits a build waits for and stopped the commit at shutdown.tests/submissions/test_delivery.py: 5 failed, 13 passed. With a collector upload held: run files never reached the staging repository; in a host runtime with a 2-second shutdown period,LocalServiceError: Service worker failed (TimeoutError);UPLOAD_TIMEOUT.writewasNone; the upload limit and the cycle budget did not exist.1701b3e→19cac1bKeep collector errors out of the Hugging Face answers and startuptest_delivery.py: 3 failed, 18 passedDesign (finding 7). This uses the mechanism from #23's fix:
DeliveryWorker, so a slow or stalled submission never delays a Hugging Face commit, the commit at shutdown or a read-back.tickruns the collector cycle only when the module runs without workers (tests driven by hand). At shutdown a submission still running at the deadline is abandoned with the process, and the collector's HTTP client and the queue are released only once its worker has stopped.httpx.Timeout(60.0): a stalled upload fails within a minute.upload_time_limit(size): five minutes, or longer for a file that needs it at 2 Mbit/s. The bytes are streamed in 1 MiB chunks, and the limit is checked before each one.MAX_CYCLE_SECONDS(ten minutes). With the limits on every request and upload, one cycle's length is bounded.Not done now: #29's
collector_originsetting (review finding 2). #27 still chooses the collector origin itself in_collector_delivery(the development URL, else the production constant), so once #29 and this pull request are both merged, a configuredcollector_originwould be ignored for collector submissions. The fix needs #29 merged tomainand carried up the stack first, since the resolver doesn't exist in this branch's history. It will be:configured_collector_origin(self.configuration), as the telemetry and Orrery modules do, and allow loopback uploads only when the resolved origin is the development URL;19cac1b, instead of stopping the module.Restack onto #23's and #24's fixes.
9a0dda7(submit described bundles) changed while it was rebased: the module keeps its collector wiring next to the new delivery workers, and the test fixtures usehandover_database_pathandbackground_workers=False. As before, two of this pull request's intermediate commits don't pass on their own (9a0dda7imports the old private credential lookup untilbdf9410; a submission test is fixed bydc66627); the branch head passes.Re-run after the last commit, every test file this pull request adds or changes: 156 passed; every test file the four stacked pull requests change, at this head: 480 passed.
tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass.Not changed here: the collector row's hour-long lease is not yet renewed during an attempt (an emitter killed mid-upload still blocks that row for up to an hour; unexpected errors no longer do);
Retry-After, the collector's error codes in receipts and the 1 MiB response cap (review items 3, 6, 7). Open decisions, unchanged: polling submission status and receipts, the interface role constants for H2c, andAGENTS.md's publishing rule.Fix round 2 (2026-10-20)
Restack onto #23's and #24's round-2 fixes. Every commit has a new SHA; the titles are unchanged. The sections above use the earlier SHAs:
9a0dda7is nowc4c46c5,bdf9410is4c8c360,dc66627is51aec26,2ea1dd6ise081c07,46012a2isf2c94a7,1701b3eisf901abdand19cac1biscdffe80. One change goes beyond replaying the commits: #23'sDeliveryWorkernow passes its stop check to each cycle, sof2c94a7builds the collector worker's cycle aslambda _stop_requested: self.collector_delivery.run_cycle(). Without that, the worker would fail on every cycle. The collector cycle uses the stop check fromf735a52on. As before, two intermediate commits don't pass on their own (c4c46c5imports the old private credential lookup until4c8c360; a submission test is fixed by51aec26).b30ec7e→f735a52Finish both shutdown commits before waiting briefly for the collectorclosewaited for every worker, the collector's included, before the second shutdown commit. A collector upload still running at shutdown used up the whole period, and the second commit, for the repository a bundle upload had held, was skipped even when that upload finished in time. On Modal those run files are lost.tests/submissions/test_delivery.py, the review's case: 1 failed, 21 passed (AssertionError: the staging run files missed the second shutdown commit), in two runsd838a03→e463794Stop only stalled uploads, never ones that keep making progressupload_time_limit(size), which assumes at least 2.1 Mbit/s. A large file on a slower link (399 MiB at 1.5 Mbit/s) was stopped on every attempt, started again from zero, and was never submitted.tests/submissions/test_client.py: 1 failed, 41 passed (AssertionError: an upload that kept making progress was stopped; the outcome was('pending', 'UPLOAD_FAILED'), at the second 1 MiB piece), in three runs. The stalled-upload test passes on the old code too: its write timeout already detected a stall.Design: shutdown (finding 6).
closestops every worker, and the collector cycle now claims no further hand-over once stop is requested.closethen:COLLECTOR_SHUTDOWN_WAIT_SECONDS(2 s) and never past the deadline.Collector work therefore never uses up the time the Hugging Face shutdown commits need, and a busy collector worker no longer holds the process for the whole shutdown period. A submission still running is abandoned with the process, as before, and its row is retried once its lease expires. The HTTP client and the queue stay open while any worker still runs.
Design: stall detection instead of a size-based limit (finding 7). A signed upload is one
PUTthat stores nothing unless it completes, so a limit on its total time only makes a slow upload start again from zero.upload_time_limit,chunks_until,UploadTimeLimitError,MIN_UPLOAD_SECONDSandMIN_UPLOAD_BYTES_PER_SECONDare gone.UPLOAD_TIMEOUT's write timeout, which httpcore applies to each send to the connection: a send that has had no byte accepted for 60 seconds fails the attempt (pending,UPLOAD_FAILED). Connecting and waiting for the answer keep their 60-second limits. Files are still sent in 1 MiB pieces with an explicitContent-Length.FakeCollector(receive_buffer_bytes=…)andhttpx.HTTPTransport(socket_options=…)), so the client's sends wait for the fake to read. The tests patchUPLOAD_TIMEOUTto keep the stall threshold under a second. The slow-upload test also shortens any size-based limit withmonkeypatch.setattr(..., raising=False), which has nothing to shorten after the fix.Process note (finding 8; no history rewrite). In the first round of fixes, the red commit
2ea1dd6(nowe081c07) fails at a setup step rather than for its stated reason: its two runtime tests used the manual clock, and the green commit46012a2(nowf2c94a7) also switched them to the real clock. With the manual clock, a collector row claimed before its bundle's Hugging Face commit is rescheduled 2 seconds ahead on a clock that never moves, so one of those tests fails withAssertionError: no upload startedinstead. The tests as they stand at46012a2do fail for the stated reasons against the unfixed code. Those commits are not rewritten. This round's red commits,b30ec7eandd838a03, were each run before their fix and fail for their stated reasons.Re-run after the last commit, every test file this pull request adds or changes: 160 passed; every test file #23, #24 and this pull request change, at this head: 430 passed.
tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass.Fix round 3 (2026-10-20)
Restack onto #24's round-3 fixes. Every commit has a new SHA, the same title and the same patch (
git range-diffshows=for all 23). The sections above use the round-2 SHAs:c4c46c5is now8941474,4c8c360is57c0136,51aec26is4451d7f,e081c07isc64d661,f2c94a7is394c9b5,f901abdis3380529,cdffe80is4296daa,b30ec7eis24062f1,f735a52is3f63781,d838a03isf1e40f6ande463794isdc59b69. As before,8941474does not pass on its own until57c0136.c489ad7→8ecd153Send uploads in 64 KiB pieces so HTTPS stalls are measured per piecessldoes no partial writes. A link slower than about 140 kbit/s failed partway and started again from zero.tests/submissions/test_client.py: 1 failed, 42 passed (an HTTPS upload that kept making progress was stopped, outcome('pending', 'UPLOAD_FAILED')), in three runs. With the threshold raised to 5 s the same test passes on that code, so the failure is the write timeout on the first piece.99fa0e3→6e8d912Renew the collector row's lease while an attempt runstests/submissions/test_delivery.py: 1 failed, 24 passed (a second emitter claimed the collector row while its upload was running), in two runs. A guard (the row is free again within one lease after shutdown) passes before and after.a634cfb→423139bAsk for the collector token again just before completingCOLLECTOR_UNAVAILABLEuntil the next attempt.test_client.py: 1 failed, 43 passed (the completion after a long upload used an expired token, outcome('pending', 'COLLECTOR_UNAVAILABLE'))Before pushing, I amended the unpushed red commit
99fa0e3to drop a finalattempts == 1check that raced: the row can first be attempted, and recorded asWAITING_FOR_HUGGING_FACE, before the bundle's Hugging Face commit. The amended commit still fails for the stated reason.Design: 64 KiB pieces (replaces the round-2 wording "a send that has had no byte accepted for 60 seconds" and "files are still sent in 1 MiB pieces").
UPLOAD_CHUNK_BYTESis now 64 KiB, the size httpx itself reads files in. Each piece is one send under the 60-second write timeout. Over HTTPS, where a piece is written whole under one time limit, the slowest link that never fails averages 65,536 bytes per 60 seconds over each piece: 1,093 bytes a second, about 8.7 kbit/s. Over plain HTTP (a loopback development collector) a send still fails only after 60 seconds in which no byte was accepted. Nothing else changes on the wire. The code comment, the README and the changelog fragment state this rule and rate instead of "never stopped, however slow the link".tls_uploads: a second server for uploads that speaks TLS, as Cloud Storage does, while the collector stays plain HTTP. Both sides use only anonymous TLS 1.2 cipher suites, so no certificate or key is generated or committed. The fake reads uploads at about 1 MiB/s, the threshold is 0.5 s and the file is 1.5 MiB, so a 64 KiB piece takes about 75 ms and a 1 MiB piece more than a second.383a27f(Run the HTTPS slow-upload test over a Unix socket). The first CI run at423139bfailed this test on both macOS jobs (Ubuntu passed). Measured locally: a loopback TCP connection on macOS grows its receive buffer to about 280 KiB whateverSO_RCVBUFit is given, so it absorbs several pieces at once and the next piece waits up to about 0.3 s, close to the 0.5 s threshold. The upload server now listens on a Unix socket, which keeps the buffer it is given; the test client sendshttps://requests over it (an httpx mount withuds=), and the upload URLs still namehttps://127.0.0.1. Each piece then takes a steady 70 to 80 ms. With the test as it stands after this commit, settingUPLOAD_CHUNK_BYTESback to 1 MiB makes it fail three times out of three for the stated reason, and 64 KiB passes three times out of three, also under Homebrew's Python 3.14, which the macOS CI jobs use.Design: a renewed two-minute lease (replaces the round-2 "Known gap, unchanged" bullet). The collector row's lease is now 120 s (
SUBMISSION_LEASE_SECONDS), renewed every 30 s (SUBMISSION_LEASE_RENEWAL_SECONDS) while an attempt runs, through the sameHeldLeasesthe Hugging Face jobs use.HeldLeasesgainsfor_job, which holds a job without a repository lease, and an optional stop check: renewal ends atstop, or once shutdown has begun. The Hugging Face leases are built without the stop check, so they behave as before._attemptnow returns anAttemptResult;run_cyclestops the renewal, then records the result with the renewed lease value, so the attempt's result is recorded.Design: a fresh token before completion.
submitasks the session for the token once more after the uploads. The session returns the same token while it is valid and exchanges for a new one only when it is about to expire, so uploads that outlast the token (an hour) complete in the same attempt. The fake collector now issues tokens with a lifetime the test chooses and answers an expired one with 401.Re-run after the last commit, every test file this pull request adds or changes: 164 passed; every test file #23, #24 and this pull request change, at this head: 446 passed (five runs at
423139b, once more at383a27f).tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass. No workflow, Dockerfile, Terraform, packaging or release script changed, and no new code reads an environment variable.🤖 Generated with Claude Code