Repository navigation
Queue Hugging Face staging uploads with one commit per repository per cycle - #23
Merged
Merged
Conversation
anth-volk
added this pull request to stack #25
October 19, 2026 21:21
This was referenced Oct 19, 2026
anth-volk
force-pushed
the
feat/huggingface-staging-queue
branch
from
October 20, 2026 13:00
f438369 to
473b576
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The hand-over tables (staged run files, run deliveries, repository schedules, jobs and copied files) move from the shared spool database to a separate SQLite file with its own Alembic history: - core/handover_models.py holds them on their own declarative base, and core/handover_alembic holds their one migration, renamed handovers_20261020_01 so neither history accepts the other's file; - core/migrations.py applies either history through one function; - the spool database's history ends at 20261008_02 again, the revision every released emitter (0.2.0 to 0.4.0) knows. HuggingFaceSettings now sends handover_database_path: by default events.handovers.sqlite3 next to the spool, or a file the build names, never the spool file itself (invalid_configuration). The module opens only that file. The unreleased configuration fixture and its digest follow, the installed-interface smoke check confirms the wheel creates the file, and the README and changelog fragment describe it. Green: 31 passed (the three red files); 188 passed across #23's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/huggingface-staging-queue
branch
from
October 20, 2026 17:47
04a612a to
0393d26
Compare
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The host gives each module one worker thread, and runs the module's close (the commit at shutdown) on it only after the current tick returns. Bundle uploads ran inside that tick, so a long one held up every other repository's commits, the shutdown commit and read-backs. - workers.DeliveryWorker runs one kind of delivery cycle on a thread of its own. It can tell an upload in progress from an idle wait, so close knows when the queue is safe to release. Later kinds (release changes, collector submissions) use the same class. - HuggingFaceDelivery splits its cycle: run_file_cycle commits run files only, and job_cycle(kinds) delivers jobs of those kinds only. run_cycle still does both on the calling thread. - HuggingFaceModule starts a dataset-bundle worker. The host's tick commits run files only. close first commits every repository's pending run files that no worker is uploading to, then gives an upload in progress until the deadline, and commits that repository's run files after it if time remains. Leases in the hand-over database still keep two writers off one repository. - background_workers=False keeps every delivery on tick, for tests that drive the module by hand. The README's statement that the module commits pending files once more at shutdown now says which repository can miss that commit and why. Green: 4 passed; 192 passed across #23'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 #23 review's code-quality notes: - retry_delay capped its exponent at 8, so retries never waited the 300 seconds MAX_RETRY_SECONDS and the README promise; - bytes equal to the last committed ones, handed over again while other bytes for the path were being committed, were marked not pending and their copy cleared, so the repository kept the older commit's bytes; - the interface's wait for a bundle to settle indexed the receipt's handover_id outside its error handling. Red: 3 failed, 34 passed. "assert [] == [b'X']"; "At index 3 diff: 256.0 != 300.0"; "KeyError: 'handover_id'". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #23 fix review found that after `close` calls the bundle worker's `stop`, the cycle already in progress still leases, claims and uploads the remaining repositories on its list. When the upload to a second repository outlasts the shutdown period, `wait` returns false, the second shutdown commit is skipped, and the first repository's run files miss it although that repository's upload finished in time; the README says they are committed. The test queues bundles for two repositories before the worker starts, holds the first upload, begins shutdown, then releases it after half a second. It asserts that no upload to the second repository starts and that the first repository's run files land at shutdown. Red: 1 failed, 4 passed (`AssertionError: an upload to the second repository started after shutdown began`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The #23 fix review found that the 10-second spacing between this machine's commits to one repository is checked only when a cycle builds its list of due repositories. Since bundles moved to their own worker, the host's thread can commit to a repository on the worker's list (or the other way round) while the worker uploads elsewhere, and the second commit then follows at once. Two tests, each with the clock standing still, make the other thread's commit from inside the first commit of a cycle: - a bundle cycle lists the private and staging repositories, and the staging run files are committed during the private upload: the staging bundle must wait for the spacing; - a run-file cycle lists both repositories, and the staging bundle is committed during the private run-file commit: the staging run files must wait for the spacing. Red: 2 failed, 5 passed (`AssertionError: the staging bundle was committed less than 10 seconds after its run files`; `AssertionError: the staging run files were committed less than 10 seconds after the bundle`). 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 #23 found one window left after the stop checks: the bundle cycle checks for shutdown before each repository and once the repository is leased, but not after it claims the job. A shutdown that begins between the post-lease check and the claim still starts that upload. The new test begins shutdown from inside the job claim and expects no upload, and the job left pending with its lease released and its attempt count unchanged. Red: 1 failed, 7 passed. The failure is the stated reason: `AssertionError: a bundle upload started after shutdown began, once its job was claimed`. 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 #23 asked for three changes to the README's repair note for a spool database an earlier, unreleased build moved to `20261020_03`: - The revision reset now runs in one transaction that first inserts the file's revision into a temporary table whose check accepts only `20261020_03`. With `sqlite3 -bail`, any other revision (a later spool revision, or the hand-over database, whose tables have the same names) stops the command with nothing changed. The `UPDATE` also carries `WHERE version_num = '20261020_03'`. - The same transaction drops the earlier build's tables, as that migration's own `downgrade()` did. Checked against the migration file at every earlier head (`f438369`, `473b576`, `04a612a`): it only created `staged_run_files`, `staged_run_deliveries`, `repository_commit_schedules`, `handover_jobs` and, from `04a612a`'s form on, `handover_files`, with no index. No spool revision at any branch of the stack or any release uses these names. - The note says four or five tables, and that an emitter started with only the `huggingface` module never opens the spool database. Documentation only; no red step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
`ReleaseReceipt` gains `current: str | None`. On a `published` receipt it is the release's current build in `releases.json` once the change was applied (or, when nothing needed writing, as the writer found it), and `None` when the release has no current build; on `pending`, `refused` and `unavailable` receipts it is `None`. A publication with `make_current=True` made its build current exactly when its receipt is `published` and `current` equals its build, so H2c can tell "published and current" from "published, not current" without reading `message`. The writer sets `ReleaseOutcome.current` on its three `published` paths (a commit, a change that needed no commit, and a build already published), and the delivery copies it into the receipt. The hand-over receipts the module answers with (pending, and refused at hand-over) and the interface's `unavailable_release_receipt` carry `current: None`. The `publish_release` docstring now states the one-hour expiry and the cases where a make-current publication is `published` but not current. The README and the changelog fragment describe the field. The protocol fixtures record only request messages and the answer's top-level `receipt` key, so no fixture describes receipt fields and none changed. The behaviour of an overtaken make-current publication (it adds its build and leaves `current` with the newer request's build) is unchanged; it awaits the user's acceptance. Green: `test_release_writer.py` 56 passed, `test_release_client.py` 20 passed; every test file this branch changes, plus #23's `test_delivery_workers.py`: 224 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
Request times in writer commit titles come from each machine's clock, but every change is requested before its commit. Two changes bound what one fast clock can do, without changing the `releases.json` schema or the one-hour expiry: - The writer now reads the Hub's date for the commit that last changed `releases.json` from the same `get_paths_info` answer as its title (`DatasetRepository.last_commit_title_and_date`; no extra request). A time in that title more than `CLOCK_TOLERANCE_SECONDS` (5 minutes) after that date is ignored, and is not carried into the next title. A machine whose clock runs fast therefore no longer refuses later changes to its release on other machines until real time catches up; its own change is ordered only by its own queue and the expiry. - The two-hour window that drops old times from the titles now ends at that Hub date instead of at the newest time listed, so no machine's clock decides which other releases' times are dropped. Every change requested before a dropped time is more than two hours older than a commit that has already landed, so it has expired, as before. When the Hub reports no date, both fall back to the earlier behaviour. Clock differences within the tolerance still allow the misordering of changes requested closer together than the difference, as before; the module docstring and the README say so. Green: `test_release_writer.py` 59 passed; every test file this branch changes, plus #23's `test_delivery_workers.py` and `test_hub.py`: 244 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/huggingface-staging-queue
branch
from
October 20, 2026 22:47
817d017 to
67fa198
Compare
anth-volk
force-pushed
the
feat/huggingface-staging-queue
branch
2 times, most recently
from
October 21, 2026 19:03
67fa198 to
bd78514
Compare
anth-volk
marked this pull request as draft
October 21, 2026 21:35
- Migration 20261020_03 adds four tables to the spool database: staged_run_files (the newest handed-over bytes per repository path), staged_run_deliveries (per-run counts and the pending runs-index entry), repository_commit_schedules (commit spacing, backoff and a per-repository lease) and handover_jobs (one row per hand-over and destination, with leases, retries and a receipt, like graph jobs). - `HandoverQueue` records hand-overs without touching the Hub: bytes equal to those last committed are not pending, unforced hand-overs wait for their interval after the repository's last commit, forced ones keep a 10-second spacing, and failures back off. - `DatasetRepository` ports the calls of Microcosm's HuggingFaceDatasetStorage that the emitter needs, including credential_can_write, and recognises parent-commit conflicts (HTTP 409 or 412). `hugging_face_api()` uses the build's credential in the same order as core/auth.py. - Tests use an in-memory Hugging Face API (tests/support). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new `huggingface` emitter module takes over the transport of Microcosm's staging writers (staging.py, staging_v2.py): - `stage_run_files` copies the handed-over files' bytes into the queue before answering. They are written at `<prefix>/<run_id>/<path>` with the bytes the build wrote; staging contract version 1 also writes `latest_staging.json` and merges its run summary into the repository's `runs.json` with Microcosm's ordering and formatting. - Each module tick makes at most one commit per due repository, with the newest bytes of every pending path. Unforced hand-overs wait for the build's upload interval after the last commit; forced ones go at the next cycle, at least 10 seconds after the previous commit. - Failures keep files queued and back off; without a credential one warning is printed. Close commits pending files once more. - A `runs.json` merge commits with `parent_commit` and, on a conflict, reads the index again and merges again. - `read_back_run_files` compares the Hub's copies with the committed bytes. - Requests are checked with the interface's shared path rules; staging never writes under `releases/`. - Protocol fixtures for the new messages; the installed-wheel check starts the module without a credential. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The transport of Microcosm's staging_dataset.stage_bundle (used by uk_runtime/rowwise_staging.py and the Orrery evidence bundle) moves to the emitter as a durable job: - `StageDatasetBundle` names each file with its role (dataset, calibration_diagnostics, build_manifest, staged_manifest, checksums, ...), SHA-256 and size; exactly one staged_manifest is required. - The worker re-hashes preserved files and refuses changed bytes, then writes every file under `<prefix>/<run_id>/` in one commit pinned to the head it checked, with Microcosm's commit message. - A matching repository inventory is `already_staged` with its own commit; a different one is `REMOTE_DIFFERS` and is never overwritten. - REPOSITORY_UNAVAILABLE and UPLOAD_FAILED are retried with backoff; a commit that loses a race is checked and tried again. - `check_write_access` ports the staged-dataset preflight. - Jobs are filtered by the kinds this emitter delivers, so a job queued by a newer emitter sharing the database waits for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The staging calls still built their messages before the client's error handling: an unconvertible `run_index_entry`, a bundle file whose path cannot be resolved, or an invalid repository or run identifier given to `run_files_receipt`, `read_back_run_files` or `check_write_access` raised into the build. `stage_dataset_bundle`'s unavailable receipt also rebuilt the message, so it raised again. Each call now passes a message builder that the client runs inside its error handling, as the telemetry and Orrery calls do since the previous pull request; a failure returns the call's unavailable result and records `invalid_arguments` in `last_failure`. The unavailable bundle receipt has an empty `handover_id` when the files cannot be converted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`commit_run_files` started `runs.json` empty whenever `hub.is_absent` matched the download error, and `is_absent` matched any error class named `EntryNotFoundError` or `FileNotFoundError` in the error's hierarchy. huggingface_hub's `LocalEntryNotFoundError` subclasses both, and `hf_hub_download` raises it for connection errors, timeouts and 429 or 503 responses when the file is not cached. A transient error therefore committed an index without every other run. `is_absent` now holds only for an entry-not-found error that carries the Hub's HTTP 404 response, and never for `LocalEntryNotFoundError`, a missing revision or repository, or a local `FileNotFoundError`. Any other error fails the commit, keeps the run files and index entry queued, and retries after the backoff. Tested with real huggingface_hub error classes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Microcosm's `full_build_cli` (at 41209b38c) rewrites three bundle files right after `stage_dataset` returns: the build manifest (`replace_manifest`), the checksum listing (`refresh_sha256sums_entry`) and, in national builds, the build record. With the queued upload the worker re-hashed those files and failed the hand-over permanently with `PRESERVED_BYTES_CHANGED`. - Before answering, the module copies the bundle's files smallest first, each of at most 32 MiB and together at most 64 MiB, after checking that each holds its declared size and SHA-256 (`FILE_SIZE_DIFFERS`, `FILE_DIGEST_DIFFERS`). The copies are stored with the job in a new `handover_files` table (added to the unreleased revision `20261020_03`) and named in the receipt's `copied_files`. - The worker uploads the copied bytes as they were handed over, and still re-hashes and refuses changed bytes of the larger files it uploads from their paths (the datasets, which Microcosm does not rewrite). - The copies are deleted once every destination of the hand-over has settled. A test follows Microcosm's order: hand over, rewrite the manifest atomically, append to the checksum listing and rewrite the build record, then run the cycle; the commit carries the handed-over bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_read_back_run_files` returned `passed` with `files: 0` for a run without committed rows, for example after a refused hand-over or when the run was staged through another spool database. Microcosm's `validate_staging_delivery` rejects a passed read-back with zero uploads. Such a read-back is now `failed` with the error code `NO_COMMITTED_FILES`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every repository and job lease lasted 900 seconds and was never renewed, so an emitter that was killed during a delivery kept the next emitter on the same machine from committing to that repository for up to 15 minutes. Leases now last 120 seconds. While a delivery runs, `HeldLeases` renews the repository lease and the claimed job's lease every 30 seconds on a background thread, and stops before the job's attempt is recorded and the repository is released, so both use the renewed lease. A long upload keeps its leases; a killed emitter's lease expires within 120 seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documentation only: - README: `runs.json` starts empty only when the Hub reports it missing; other download errors retry the commit (Microcosm started empty on any error). The 10-second spacing applies to every commit from this machine to a repository, of run files or bundles, forced or not, except the last commit at shutdown; it does not count other machines' commits. - `HandoverQueue`: rows are kept after delivery and nothing deletes them, except the copied hand-over bytes once the hand-over settles. The earlier docstring said records were kept only until delivered. - `StageRunFiles` and `MIN_COMMIT_SPACING_SECONDS` say the same about the interval and spacing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Released emitters (0.2.0 to 0.4.0) refuse a spool database at a revision they don't know. Opening this branch's spool, or starting the Hugging Face module on it, moved the shared database to 20261020_03, so a build pinned to 0.2.1 on the same machine would start with no telemetry and no Orrery queue. The tests ask for the hand-over tables in a separate database next to the spool, with its own migration history, and for HuggingFaceSettings to name that file explicitly and refuse the spool file itself. Red: 6 failed, 25 passed. The compatibility test fails with "CommandError: Can't locate revision identified by '20261020_03'". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hand-over tables (staged run files, run deliveries, repository schedules, jobs and copied files) move from the shared spool database to a separate SQLite file with its own Alembic history: - core/handover_models.py holds them on their own declarative base, and core/handover_alembic holds their one migration, renamed handovers_20261020_01 so neither history accepts the other's file; - core/migrations.py applies either history through one function; - the spool database's history ends at 20261008_02 again, the revision every released emitter (0.2.0 to 0.4.0) knows. HuggingFaceSettings now sends handover_database_path: by default events.handovers.sqlite3 next to the spool, or a file the build names, never the spool file itself (invalid_configuration). The module opens only that file. The unreleased configuration fixture and its digest follow, the installed-interface smoke check confirms the wheel creates the file, and the README and changelog fragment describe it. Green: 31 passed (the three red files); 188 passed across #23's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All repositories' commits ran one after another on the module's single worker thread, and the commit at shutdown ran on that thread after the current cycle. While a bundle upload to one repository was in progress, run files for another repository waited, the shutdown commit never ran before the host gave up, and a requested read-back answered pending. The tests run the module in a host runtime with a 2-second shutdown period and an in-memory Hub whose bundle commit blocks on an event. Red: 3 failed, 1 passed. Run files never reached the staging repository; the runtime reported "LocalServiceError: Service worker failed (TimeoutError)" at shutdown without the staging commit; the read-back answered "pending". The passing test checks that the blocked bundle still lands once released. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host gives each module one worker thread, and runs the module's close (the commit at shutdown) on it only after the current tick returns. Bundle uploads ran inside that tick, so a long one held up every other repository's commits, the shutdown commit and read-backs. - workers.DeliveryWorker runs one kind of delivery cycle on a thread of its own. It can tell an upload in progress from an idle wait, so close knows when the queue is safe to release. Later kinds (release changes, collector submissions) use the same class. - HuggingFaceDelivery splits its cycle: run_file_cycle commits run files only, and job_cycle(kinds) delivers jobs of those kinds only. run_cycle still does both on the calling thread. - HuggingFaceModule starts a dataset-bundle worker. The host's tick commits run files only. close first commits every repository's pending run files that no worker is uploading to, then gives an upload in progress until the deadline, and commits that repository's run files after it if time remains. Leases in the hand-over database still keep two writers off one repository. - background_workers=False keeps every delivery on tick, for tests that drive the module by hand. The README's statement that the module commits pending files once more at shutdown now says which repository can miss that commit and why. Green: 4 passed; 192 passed across #23's changed test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the #23 review's code-quality notes: - retry_delay capped its exponent at 8, so retries never waited the 300 seconds MAX_RETRY_SECONDS and the README promise; - bytes equal to the last committed ones, handed over again while other bytes for the path were being committed, were marked not pending and their copy cleared, so the repository kept the older commit's bytes; - the interface's wait for a bundle to settle indexed the receipt's handover_id outside its error handling. Red: 3 failed, 34 passed. "assert [] == [b'X']"; "At index 3 diff: 256.0 != 300.0"; "KeyError: 'handover_id'". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- retry_delay lets its exponent grow until MAX_RETRY_SECONDS (300 s) applies, as the README says. - A path handed over again with the bytes last committed, while other bytes for it are pending, keeps those bytes; when the in-progress commit is recorded and the row's latest bytes differ from what it committed, the row is pending again, so the next commit restores the newest bytes. - The interface waits for a hand-over to settle only when its receipt names a hand-over ID, and otherwise returns the receipt as answered. Green: 37 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #23 fix review found that after `close` calls the bundle worker's `stop`, the cycle already in progress still leases, claims and uploads the remaining repositories on its list. When the upload to a second repository outlasts the shutdown period, `wait` returns false, the second shutdown commit is skipped, and the first repository's run files miss it although that repository's upload finished in time; the README says they are committed. The test queues bundles for two repositories before the worker starts, holds the first upload, begins shutdown, then releases it after half a second. It asserts that no upload to the second repository starts and that the first repository's run files land at shutdown. Red: 1 failed, 4 passed (`AssertionError: an upload to the second repository started after shutdown began`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`DeliveryWorker` now calls its cycle with `stop_requested`, which turns true when `close` calls `stop`. The job cycle checks it before each repository on its list and again once that repository is leased, so after shutdown begins the bundle worker starts no new upload. Only an upload already under way can then hold a repository during the commit at shutdown, which is the one exception the README names; that repository's run files get the second shutdown commit when the upload finishes in time. Cycles that no worker runs (the host's tick, and modules driven by hand) use `never_stop`. Green: 5 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #23 fix review found that the 10-second spacing between this machine's commits to one repository is checked only when a cycle builds its list of due repositories. Since bundles moved to their own worker, the host's thread can commit to a repository on the worker's list (or the other way round) while the worker uploads elsewhere, and the second commit then follows at once. Two tests, each with the clock standing still, make the other thread's commit from inside the first commit of a cycle: - a bundle cycle lists the private and staging repositories, and the staging run files are committed during the private upload: the staging bundle must wait for the spacing; - a run-file cycle lists both repositories, and the staging bundle is committed during the private run-file commit: the staging run files must wait for the spacing. Red: 2 failed, 5 passed (`AssertionError: the staging bundle was committed less than 10 seconds after its run files`; `AssertionError: the staging run files were committed less than 10 seconds after the bundle`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bundle worker and the host's thread each list due repositories and then deliver them in turn, so a list can be minutes old when a delivery starts. After the lease is taken, `_claim_job` now refuses to claim a job, and `run_files_due` reports no run files due, until `MIN_COMMIT_SPACING_SECONDS` have passed since this machine's last commit to the repository. The commit at shutdown still ignores the spacing. The helpers `_spacing_elapsed`, `commit_spacing_elapsed` and `_run_files_allowed` match the ones #24 uses, which also exempts release changes from the spacing. Green: 7 passed; every test file this branch adds or changes: 199 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A developer machine that ran an earlier, unreleased build of this branch has its spool database at revision `20261020_03`, which no migration history contains now, so every emitter there refuses to start. The README now names the file to delete (and its rollback journal), says what deleting it loses, and gives the revision reset that keeps the queued telemetry events and graph publication jobs. No repair code is added for an unreleased revision. Documentation only; no red step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #23 found one window left after the stop checks: the bundle cycle checks for shutdown before each repository and once the repository is leased, but not after it claims the job. A shutdown that begins between the post-lease check and the claim still starts that upload. The new test begins shutdown from inside the job claim and expects no upload, and the job left pending with its lease released and its attempt count unchanged. Red: 1 failed, 7 passed. The failure is the stated reason: `AssertionError: a bundle upload started after shutdown began, once its job was claimed`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_deliver_repository` now checks for shutdown once more after it claims a job, immediately before the upload begins. A job claimed after shutdown began is given back with the new `HandoverQueue.release_job`, which clears its lease without counting an attempt, so the job stays pending and due for the next emitter. The README, the `workers` module docstring and the delivery docstrings now state the exact guarantee: the worker checks before each repository, once the repository is leased, and once the job is claimed. During the commit at shutdown a repository can be held only by an upload that passed that last check before shutdown began, or, for the few database operations it takes to give it back, by a lease taken just before. Green: 8 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 review of #23 asked for three changes to the README's repair note for a spool database an earlier, unreleased build moved to `20261020_03`: - The revision reset now runs in one transaction that first inserts the file's revision into a temporary table whose check accepts only `20261020_03`. With `sqlite3 -bail`, any other revision (a later spool revision, or the hand-over database, whose tables have the same names) stops the command with nothing changed. The `UPDATE` also carries `WHERE version_num = '20261020_03'`. - The same transaction drops the earlier build's tables, as that migration's own `downgrade()` did. Checked against the migration file at every earlier head (`f438369`, `473b576`, `04a612a`): it only created `staged_run_files`, `staged_run_deliveries`, `repository_commit_schedules`, `handover_jobs` and, from `04a612a`'s form on, `handover_files`, with no index. No spool revision at any branch of the stack or any release uses these names. - The note says four or five tables, and that an emitter started with only the `huggingface` module never opens the spool database. Documentation only; no red step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/huggingface-staging-queue
branch
from
October 21, 2026 21:58
bd78514 to
8fa62eb
Compare
anth-volk
marked this pull request as ready for review
October 21, 2026 22:30
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 #19
Third of four stacked pull requests (base: #22). Moves the transport of Microcosm's staging uploads into a new
huggingfaceemitter module behind the interface. Microcosm keeps building and validating its staging documents and bundles; the emitter writes the bytes it is given, with the build's own credential. Nothing in Microcosm calls this yet (H2a and H2b switch it over).What is ported
41209b38c)staging.py(contract version 1) andstaging_v2.pyuploads underruns/<run_id>/, throughstaging_storage.HuggingFaceDatasetStorageandBestEffortUploadSessionstage_run_files(StageRunFiles(...)),run_files_receipt,read_back_run_fileslatest_staging.jsonis written at the root, and its run summary is merged into the repository'sruns.jsonwith Microcosm's ordering and formatting (staging._write_json).staging_dataset.stage_bundle, used byuk_runtime/rowwise_staging.pyand the Orrery evidence bundlestage_dataset_bundle(StageDatasetBundle(...)),dataset_bundle_receiptstaged/<run_id>/<file>), one commit pinned to the head it checked, the same commit message, and the same idempotency: a matching repositorystaged_manifest.jsonisalready_stagedwith its own commit; a different one isREMOTE_DIFFERSand never overwritten. Each file is handed over with its role (dataset,calibration_diagnostics,build_manifest,staged_manifest,checksums, ...).rowwise_staging.preflight_staged_datasetandHuggingFaceDatasetStorage.credential_can_writecheck_write_accessNot ported:
staging_dataset.fetch_bundleandtools/fetch_uk_staged_dataset.py(downloads, not hand-overs).How the transport works
events.sqlite3→events.handovers.sqlite3) with its own Alembic history (handovers_20261020_01). The shared spool database stays at20261008_02, which every released emitter accepts (see the second round of review fixes below).staged_run_fileskeeps the newest handed-over bytes per repository path;staged_run_deliverieskeeps per-run counts and the pendingruns.jsonentry;repository_commit_schedulesspaces commits, backs off and leases each repository to one writer on the machine;handover_jobsholds bundles (and, in the next pull request, release changes) with leases, retries and receipts, likegraph_publication_jobs;handover_filesholds the bundle files copied at hand-over. Rows are kept after delivery and nothing deletes them yet, except the copied bundle bytes, which are deleted once the hand-over settles.upload_filecommit, up to eight per upload cycle, each triggering the dataset repository's webhook. Each delivery leases its repository first, so this machine never commits to one repository twice at once.unavailable; nothing raises into the build. A failed commit keeps everything queued and backs off (2 s doubling up to 5 minutes), replacingBestEffortUploadSession's pause after three consecutive failures. Without a credential, work stays queued and one warning is printed. On shutdown the module commits every repository's pending run files once more, before it waits for any bundle upload; only a repository a bundle upload holds at that moment can miss that commit, if the upload outlasts the shutdown period. Warnings carry only error type names, never Hub messages.runs.jsonmerge commits withparent_commit; if another build commits first, the index is read and merged again. A bundle commit that loses a race is checked again (it may now bealready_staged) and retried.FILE_SIZE_DIFFERS,FILE_DIGEST_DIFFERS); the receipt'scopied_filesnames them, and those are uploaded as handed over. The worker re-hashes every other file before uploading; changed bytes are refused (PRESERVED_BYTES_CHANGED), missing files wait (PRESERVED_FILES_MISSING).HfApi(token=…)with the token found in the same order as telemetry (core/auth.py), read again every cycle.microcosm_emitter_interface.identifiers) are checked by both the interface and the module; staging never writes underreleases/.Behaviour differences to note
runs.json: Microcosm started from an empty index on any download error, which could overwrite other runs. The module starts empty only when the Hub answers HTTP 404 for the file; every other error, including huggingface_hub'sLocalEntryNotFoundErrorfor connection errors, timeouts and 429/5xx responses, retries the cycle.read_back_run_filesof a run with no file this emitter committed (a refused hand-over, or another spool database) isfailedwithNO_COMMITTED_FILES, neverpassed.REPOSITORY_UNAVAILABLEandUPLOAD_FAILEDwere final after one attempt; they are now retried from the queue (the receipt keeps the last code whilepending).upload_attempts,upload_successes) can be filled from the emitter's receipt in H2a.Release
Three
addedfragments: merging releases the next minor version of both distributions.Validation
Every test uses an in-memory Hugging Face API (
tests/support/fake_hugging_face.py): no network, no real repositories.cd31388Add the durable Hugging Face hand-over queue and repository callstests/huggingface/test_hub.py,test_queue.py: 29 of 29 fail (module missing). Modifiedtests/telemetry/test_persistence.py(new head revision and tables), run without the migration: 2 failed, 1 passed04f9805Stage run files with one Hugging Face commit per repository per cycletests/huggingface/test_run_files.py,tests/interface/test_staging_client.py: 38 of 38 fail. Fixture tests with the new fixtures expected: 8 failed, 30 passedtest_package_names.py80 passed;check-artifacts.shexit 0; re-run: 80 passed473b576Stage dataset bundles with each file's role through the queuetests/huggingface/test_bundles.py,tests/interface/test_bundle_client.py: 36 of 36 fail. Fixture tests: 5 failed, 36 passedtests/huggingfaceandtests/interface: 156 passedFor the next steps
handover_jobsis keyed by hand-over anddestination(today onlyhuggingface), so the same hand-over can get a collector row without a new table; the bundle hand-over already carries each file's role.Overlap with open pull requests
Max's #5–#7 edit
README.md, including the "Delivery and persistence" section, where this changes one sentence about migrations. #5 editspyproject.tomlanduv.lock.Review fixes (2026-10-20)
Each fix was written red, then green; only the test files this pull request adds or changes were run. All tests use the in-memory Hub; none contacts a real repository.
09cf78bBuild staging and bundle messages inside the client's error handlingstage_run_files,run_files_receipt,read_back_run_files,stage_dataset_bundleandcheck_write_accessbuilt their messages before the error handling. (When this branch was restacked, these call sites were first adapted to the new message-builder argument without changing behaviour; this commit makes them lazy.)tests/interface/test_client_errors.py: 4 failed, 12 passedbdd659cStart runs.json empty only when the Hub reports the file missingis_absentmatched anyEntryNotFoundError/FileNotFoundErrorsubclass, includingLocalEntryNotFoundError, so a transient error wipedruns.json.tests/huggingface/test_hub.py,test_run_files.py: 5 failed, 42 passed (with realhuggingface_huberror classes)e67f60eCopy a bundle's small files into the queue when it is handed overfull_build_cli(at41209b38c) rewrites the build manifest,sha256sums.txtand the national build record right afterstage_datasetreturns, so the queued upload failed withPRESERVED_BYTES_CHANGED.test_bundles.py,test_persistence.py: 5 failed, 2 errors, 24 passedd13e186Never pass a read-back of a run with no committed files_read_back_run_filesreturnedpassedwithfiles: 0.test_run_files.py: 2 failed, 30 passedtest_staging_client.py6a277f7Renew repository and job leases during deliveries; shorten them to 120 stests/huggingface/test_leases.py: 3 failedtests/huggingface: 93 passed04a612aDescribe the runs.json merge, commit spacing and row retention exactlyruns.jsonand the 10-second spacing;HandoverQueue's docstring said rows are kept only until delivered.Re-run after the last commit, every test file this pull request adds or changes: 182 passed.
ruff checkandruff format --checkpass.Finding 9: the design, and what Microcosm's H2b must do
Design (emitter side). The bundle's small files are copied into the hand-over database when the build hands the bundle over, as run files are; only files too large to copy (the datasets, which Microcosm never rewrites) are referenced by path and re-hashed before upload. The copy is checked against the declared size and SHA-256 when it is taken, so the upload carries exactly the bytes Microcosm verified with
StagedDatasetBundle.from_manifestand listed in its sidecars.tests/huggingface/test_bundles.py::test_files_microcosm_rewrites_after_staging_are_uploaded_as_handed_overfollowsfull_build_cli's order: hand over, rewrite the manifest atomically asreplace_manifestdoes, append tosha256sums.txtasrefresh_sha256sums_entrydoes, rewrite the build record, then run the cycle; the commit carries the handed-over bytes. That is also what Hugging Face held before, because the synchronousstage_bundleuploaded before those rewrites.What H2b must do (
uk_runtime/rowwise_staging.stage_dataset, used by_close_attemptand_close_national_attempt):client.stage_dataset_bundle(StageDatasetBundle(...))wherestage_bundle(bundle, storage=..., prefix=UK_STAGED_DATASET_PREFIX)is called today: afterwrite_sidecars, insidestage_dataset, so beforereplace_manifest,refresh_sha256sums_entryand the national build-record rewrite. Do not rewrite any handed-over file before the call returns.dataset,calibration_diagnostics,build_manifest(rowwise_candidate_manifest.json),staged_manifest,checksums(sha256sums.txt) and, in national builds, the build record as its own output role (for examplebuild_record).sha256sums.txt,build_record.json) is in the receipt'scopied_files. If one is not (a file above the copy limits, or an emitter that does not copy), wait for the hand-over to settle before rewriting it.copied_filesunchanged and in place until the receipt settles (uploaded,already_stagedorfailed). On a machine that ends with the build (Modal), wait for that before the build exits (wait_seconds, or polldataset_bundle_receipt): the queue lives in the machine's hand-over database, and a host started later elsewhere cannot finish it.staged_dataset's delivery record:revisionfrom the receipt;pendingis not yet staged;refusedcarries its code. Keep the preflight (preflight_staged_dataset) throughcheck_write_access.Review fixes, second round (2026-10-20)
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. All tests use the in-memory Hub; none contacts a real repository.
3330414→2709cafKeep hand-overs in their own database so the shared spool stays released20261020_03upgraded the shared spool database (by default~/.cache/microcosm/telemetry/events.sqlite3). Released emitters 0.2.0 to 0.3.0, and #21's 0.4.0, refuse an Alembic revision they don't know, so a Microcosm build pinned to 0.2.1 on that machine would start with no telemetry and no Orrery queue.tests/test_queue_compatibility.py,tests/huggingface/test_queue.py,tests/interface/test_staging_client.py: 6 failed, 25 passed (CommandError: Can't locate revision identified by '20261020_03')5071bcc→904c1c0Upload bundles on their own worker so other commits never waitpending.tests/huggingface/test_delivery_workers.py(a host runtime with a 2-second shutdown period, and a bundle commit held on an event): 3 failed, 1 passed. Run files never reached the staging repository; the runtime reportedLocalServiceError: Service worker failed (TimeoutError); the read-back answeredpending. The passing test checks the held bundle still lands once released.cfcb2f6→0393d26Reach the five-minute retry cap, recommit re-handed bytes, guard waitsretry_delaycapped at 256 s, soMAX_RETRY_SECONDS(300) and the README's "up to five minutes" never applied; bytes equal to the last committed ones, handed over again while other bytes for the path were being committed, were left uncommitted;_wait_until_settledindexedreceipt["handover_id"]outside the error handling.test_queue.py,test_client_errors.py: 3 failed, 34 passedDesign: a separate hand-over database (finding on the shared spool). The five tables this stack adds (
staged_run_files,staged_run_deliveries,repository_commit_schedules,handover_jobs,handover_files; #24 and #27 add none) live in their own SQLite file with their own Alembic history.core/handover_models.pyholds them on their own declarative base,core/handover_alembicholds their one migration (renamedhandovers_20261020_01, so neither history accepts the other's file), andcore/migrations.pyapplies either history through one function. The migration was unreleased, so it was moved and renamed, not kept for compatibility. The path is explicit:HuggingFaceSettings(handover_database_path=…)names the file; by default it isevents.handovers.sqlite3next to the spool database. The settings refuse the spool file itself (invalid_configuration), and the module opens only that file. The unreleased configuration fixture and its digest changed with it. Tests: a spool database at20261008_02, opened by this branch'sEventSpoolwith the module started on the samespool_path, still resolves in a migration history built from only the two released migration files and stays at20261008_02; a new hand-over database starts empty and migrates on first open (alembic checkpasses); a spool database is refused as a hand-over database. The installed-interface smoke check confirms the wheel creates the file.Design: bundle uploads on their own worker (finding on a slow upload). The host gives each module one worker thread and runs
close(the commit at shutdown) on it only after the current tick returns, so nothing that can take minutes may run inside the tick.DeliveryWorker(huggingface/service/workers.py) runs one kind of delivery cycle on a thread of its own and can tell an upload in progress from an idle wait. The module starts one for dataset bundles; the tick commits run files only.closestops the workers, commits every repository's pending run files that no worker is uploading to, gives an upload in progress until the shutdown deadline, then commits that repository's run files too if time remains. A repository's lease still keeps two writers off it, so run files for the repository a bundle is uploading to wait for that upload. #24's release changes and #27's collector submissions get their own workers through the same class.background_workers=Falsekeeps every delivery on the tick, for tests that drive the module by hand. The README's shutdown sentence now says which repository can miss the shutdown commit.Re-run after the last commit, every test file this pull request adds or changes: 196 passed.
tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass.Not changed here: copied bundle bytes kept while a job stays pending (retention is undecided); run-file commits recorded without rechecking the repository lease (the commit has landed, so recording it is right); the module's single request thread for read-backs and write-access checks.
Fix round 2 (2026-10-20)
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, against the in-memory Hub.
092b5d8→2a752c4Start no bundle upload once shutdown beginsclosecalled the bundle worker'sstop, the cycle in progress still leased, claimed and uploaded the other repositories on its list. When such an upload outlasted the shutdown period, the second shutdown commit was skipped, so the run files of a repository whose upload had finished in time missed it, which the README says cannot happen.tests/huggingface/test_delivery_workers.py: 1 failed, 4 passed (AssertionError: an upload to the second repository started after shutdown began)a20a596→8d5a589Check the commit spacing again once a repository is leasedtest_delivery_workers.py: 2 failed, 5 passed (the staging bundle was committed less than 10 seconds after its run files;the staging run files were committed less than 10 seconds after the bundle)0e810f2Say how to repair a spool database an earlier build moved to 20261020_03Design: no new upload once shutdown begins.
DeliveryWorkernow passes its stop check (stop_requested) to each cycle. The job cycle checks it before each repository on its list and again once that repository is leased, so afterclosecallsstopthe bundle worker starts no new upload. Only an upload already under way can then hold a repository during the commit at shutdown, which is the one exception the README names; that repository's run files get the second shutdown commit when the upload finishes in time. Cycles that no worker runs usenever_stop.Design: the spacing after the lease. Once a repository is leased,
_claim_jobclaims no job andrun_files_duereports no run files due untilMIN_COMMIT_SPACING_SECONDShave passed since this machine's last commit to it. The helpers (_spacing_elapsed,commit_spacing_elapsed,_run_files_allowed) are the ones #24 uses, where release changes are exempt. The commit at shutdown still ignores the spacing.Repair for a developer machine that ran an earlier, unreleased build of this branch. Before the hand-over database existed, this branch's migration
20261020_03moved the shared spool database to that revision. No migration history contains that revision now, so on such a machine every emitter, this one and the released ones, refuses the spool database at startup (Can't locate revision identified by '20261020_03'), and builds run without telemetry or hand-overs. Nothing repairs it automatically. Stop every Microcosm build and emitter on the machine, then delete the spool database:$XDG_CACHE_HOME/microcosm/telemetry/events.sqlite3, or~/.cache/microcosm/telemetry/events.sqlite3whenXDG_CACHE_HOMEis unset (or thespool_paththe build set), together withevents.sqlite3-journalbeside it if that file exists. Deleting it loses everything queued in it and not yet delivered: telemetry events not yet sent to the collector, graph publication jobs not yet published, and the run files and dataset bundles the earlier build handed over and had not committed. The next emitter creates a new spool database at20261008_02. To keep the queued telemetry events and graph publication jobs instead, reset the revision withsqlite3 <spool file> "UPDATE alembic_version SET version_num = '20261008_02'": the earlier build added only the five hand-over tables, which every emitter then ignores, so their undelivered hand-overs are lost either way.Re-run after the last commit, every test file this pull request adds or changes: 199 passed.
tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass.Fix round 3 (2026-10-20)
Each code fix is a commit that adds a test failing because of the bug, then a commit with the code that leaves that test unchanged. Only the test files this pull request adds or changes were run, against the in-memory Hub. This section replaces the round-2 statements that the bundle worker "starts no new upload" and the round-2 repair command.
7ccbf7a→ebf18c9Give back a job claimed after shutdown began instead of uploading ittests/huggingface/test_delivery_workers.py: 1 failed, 7 passed (AssertionError: a bundle upload started after shutdown began, once its job was claimed)817d017Guard the spool repair to revision 20261020_03 and drop its tablesUPDATEhad noWHERE, kept the earlier build's tables, said "five tables", and said every emitter is affected.Design: a check after the claim.
_deliver_repositorynow checks for shutdown once more after it claims a job, immediately before the upload begins. A job claimed after shutdown began is given back with the newHandoverQueue.release_job, which clears its lease without counting an attempt, so it stays pending and due for the next emitter. The test begins shutdown from inside the job claim, after both earlier checks, so it covers this check alone. The README now states the exact guarantee: the worker checks before each repository, once the repository is leased, and once the job is claimed; during the commit at shutdown a repository can be held only by an upload that passed that last check before shutdown began, or, for the few database operations it takes to give it back, by a lease taken just before.Repair note (replaces the round-2 note). On a developer machine that ran an earlier, unreleased build of this branch, the spool database is at revision
20261020_03, which no migration history contains now. An emitter started with thetelemetryororrerymodule, this one or a released one, refuses it at startup; an emitter started with only thehuggingfacemodule never opens the spool database and is not affected. After stopping every build and emitter on the machine, either delete the spool database (andevents.sqlite3-journalbeside it), losing everything queued in it, or keep the queued telemetry events and graph publication jobs with:handover_filesonly in its later form), checked against that migration at every earlier head (f438369,473b576,04a612a); it created no index. No spool revision on any branch of the stack or in any release uses these names, so dropping them does what that migration's owndowngrade()did.20261020_03, the guard row is refused,-bailstopssqlite3at that error, and the open transaction is rolled back. So the command changes nothing in a spool database at any other revision, nor in the hand-over database (events.handovers.sqlite3), whose tables have the same names. I checked this withsqlite3on scratch files at20261020_03, at a later revision, at the hand-over revision20261020_01, and with an empty version table.Re-run after the last commit, every test file this pull request adds or changes: 200 passed.
tools/check-artifacts.shexit 0;ruff check,ruff format --checkandtools/check-changelog.shpass.🤖 Generated with Claude Code