Repository navigation
feat(taskrunner): replacement, cancellation, batching and retries - #331
Conversation
93d67eb to
611d661
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Retry scheduling breaks cancellation guarantees and same-subject ordering, while non-atomic batch failure updates violate the fail-together contract.
3 open issues
What changed in this PR
Adds scheduling features to the standalone Postgres task runner, building on #330 toward #328’s replacement of specialized locks and sweepers. The runner remains unwired from the server.
Changes:
- Adds pending-task replacement and cancellation, async batching, and configurable retries.
- Introduces a second schema migration for scheduling state.
- Adds live database tests and usage documentation.
| File | Description |
|---|---|
taskrunner/worker.go |
Claims individual tasks or batches before execution. |
taskrunner/tree.go |
Executes batches and integrates retries and cancellation notifications. |
taskrunner/schema.go |
Adds scheduling columns, cancellation state, and a replacement index. |
taskrunner/scheduling.go |
Implements replacement, cancellation, batch claiming, and retries. |
taskrunner/scheduling_live_test.go |
Tests scheduling features against Postgres. |
taskrunner/runner.go |
Exposes scheduling options and validates async-only batching. |
taskrunner/README.md |
Documents runner behavior and feature usage. |
CLAUDE.md |
Updates the task runner package description. |
Review effort: Balanced
…old their subject From review on #331: - a batch's failures and retries are recorded in one transaction under its lease, and waiters are woken only after it commits - RetryPolicy.HoldsSubject keeps the tasks behind a retry waiting, so an add that fails and retries still runs before a remove submitted after it; it is opt-in because it also holds up the subject's sync tasks - a task waiting to retry stays cancellable, which the docs now say Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
611d661 to
89176f7
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A critical finding remains in taskrunner/tree.go: handlerless batch failures can commit task outcomes individually, leaving the batch partially failed instead of enforcing the documented fail-together behavior.
1 open issue
3 resolved since last review
Review effort: Lite
…old their subject From review on #331: - a batch's failures and retries are recorded in one transaction under its lease, and waiters are woken only after it commits - RetryPolicy.HoldsSubject keeps the tasks behind a retry waiting, so an add that fails and retries still runs before a remove submitted after it; it is opt-in because it also holds up the subject's sync tasks - a task waiting to retry stays cancellable, which the docs now say Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89176f7 to
880fc71
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A critical active issue remains because batch expiry is not atomic, allowing some tasks to be committed as expired while others remain pending and later run independently, violating the fail-together contract.
1 open issue
1 resolved since last review
Review effort: Lite
From review on #331: finish takes the tasks to record and writes them in one lease-checked transaction, so a batch's expiries land together or not at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…old their subject From review on #331: - a batch's failures and retries are recorded in one transaction under its lease, and waiters are woken only after it commits - RetryPolicy.HoldsSubject keeps the tasks behind a retry waiting, so an add that fails and retries still runs before a remove submitted after it; it is opt-in because it also holds up the subject's sync tasks - a task waiting to retry stays cancellable, which the docs now say Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e7f6453 to
e6e230b
Compare
From review on #331: finish takes the tasks to record and writes them in one lease-checked transaction, so a batch's expiries land together or not at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Active critical and moderate findings remain: mixed batch outcomes can violate the fail-together contract when expiry is handled separately, and DrainLimit can be exceeded by batched claims.
2 open issues
1 resolved since last review
Review effort: Lite
…r-task expiry in batches From review on #331: - a drain counts every task a batch claims toward DrainLimit, and cuts the next batch short to fit, so a worker never runs more than DrainLimit tasks on one subject before releasing its lease - a task already past its Expires is recorded as expired and left out of its batch; HandleBatch and BatchHandler now say so, and a test pins it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4224daa to
8de6457
Compare
8de6457 to
995d77e
Compare
Async tasks can replace a pending one (the later NotBefore wins), be cancelled before they start, run in batches of one action, and be retried under a per-action policy. Adds a README with usage examples. Refs #328 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…old their subject From review on #331: - a batch's failures and retries are recorded in one transaction under its lease, and waiters are woken only after it commits - RetryPolicy.HoldsSubject keeps the tasks behind a retry waiting, so an add that fails and retries still runs before a remove submitted after it; it is opt-in because it also holds up the subject's sync tasks - a task waiting to retry stays cancellable, which the docs now say Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review on #331: finish takes the tasks to record and writes them in one lease-checked transaction, so a batch's expiries land together or not at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ase lost Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r-task expiry in batches From review on #331: - a drain counts every task a batch claims toward DrainLimit, and cuts the next batch short to fit, so a worker never runs more than DrainLimit tasks on one subject before releasing its lease - a task already past its Expires is recorded as expired and left out of its batch; HandleBatch and BatchHandler now say so, and a test pins it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There is no deployment to carry forward, so the columns for replacement, cancellation and retries are part of the one step that creates the table rather than a step of their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A retry marked its task not replaceable, so a replacement submitted during the backoff was added beside it and the stale retry still ran: a cached copy renewed while its eviction backed off was removed before its new time. A handler fails before acting, so a task waiting to retry has done nothing, as Cancel already assumes. A replacement now takes its place, starting its attempts afresh and clearing any hold on the subject. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DrainLimit bounded a lease's work by counting a batch's tasks and cutting the next batch short to fit, so it also capped every batch at DrainLimit: queued registrations went out 100 at a time against their own limit of 256. It now counts handler runs, so a batch's size is only its action's limit, and a lease still gives way after DrainLimit runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
995d77e to
033db23
Compare
…#334) The third and last runner step of #328, stacked on #331: placing tasks on one instance, and metrics, as the [task runner RFC](fil-one/RFC#40) describes. Nothing in the server uses the runner yet; the next pull requests start moving Ingot onto it. **Placement.** A task can be tied to one instance, for work on that instance's local state such as a cached file. Only that instance runs it, and other instances also leave the tasks queued behind it on the same subject until it has run, so the subject's order holds. A sync task tied to another instance is refused, since it can only run in its caller. Ingot runs as one instance today, so this changes nothing yet; it is there so a multi-instance design can place work without changing the runner again. **Metrics.** The runner reports through OpenTelemetry, labelled by queue and action, so the deployment's pool sizes can be tuned from real numbers: how many tasks are ready and how many are delayed, how long the oldest ready task has waited, how long callers wait for a subject, how long tasks take by outcome, how busy each queue's workers are, and how often a lease is taken over from a consumer that stopped renewing it. The queue counts are read from Postgres each time the exporter collects. With no meter provider configured, every instrument does nothing. The README gains a section on each. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sets every staging pin in `nodes/staging/eu-central-3/apps/versions.env` to what `nodes/dev/apps/versions.env` pins on `main`. Merging this is the promotion: the node picks it up on its next reconcile pass, waits for a safe proving window and restarts each service that changed. This pull request is rebuilt on every push to `main` and closed once staging matches dev, so edits to it are overwritten unless they are pushed to `bot/promote-staging` by hand. ## piri [`sha256:d0e88a9`](https://github.com/orgs/fil-forge/packages/container/piri/1506614110) → [`sha256:e83df92`](https://github.com/orgs/fil-forge/packages/container/piri/1535715713) What fil-forge/piri brings over staging's pin, [`029d86d..f62b605`](fil-forge/piri@029d86d...f62b605): - fil-forge/piri#215 ([3d07e1a](fil-forge/piri@3d07e1a)) - fil-forge/piri#214 ([6c47937](fil-forge/piri@6c47937)) - fil-forge/piri#216 ([6f2be19](fil-forge/piri@6f2be19)) - fil-forge/piri#218 ([67efb3b](fil-forge/piri@67efb3b)) - fil-forge/piri#217 ([a53055c](fil-forge/piri@a53055c)) - fil-forge/piri#228 ([f62b605](fil-forge/piri@f62b605)) ## ingot [`sha256:ac6dc48`](https://github.com/orgs/fil-forge/packages/container/ingot/1530171943) → [`sha256:8fc72f3`](https://github.com/orgs/fil-forge/packages/container/ingot/1537185609) What fil-forge/ingot brings over staging's pin, [`af89169..7bfca91`](fil-forge/ingot@af89169...7bfca91): - fil-forge/ingot#361 ([7d9f32b](fil-forge/ingot@7d9f32b)) - fil-forge/ingot#330 ([cddc726](fil-forge/ingot@cddc726)) - fil-forge/ingot#331 ([ce9e35c](fil-forge/ingot@ce9e35c)) - fil-forge/ingot#334 ([b9fd991](fil-forge/ingot@b9fd991)) - fil-forge/ingot#336 ([c054d14](fil-forge/ingot@c054d14)) - fil-forge/ingot#337 ([26ee5e9](fil-forge/ingot@26ee5e9)) - fil-forge/ingot#340 ([155d969](fil-forge/ingot@155d969)) - fil-forge/ingot#341 ([0aff04e](fil-forge/ingot@0aff04e)) - fil-forge/ingot#342 ([24d752a](fil-forge/ingot@24d752a)) - fil-forge/ingot#343 ([e04c724](fil-forge/ingot@e04c724)) - fil-forge/ingot#344 ([8c0cd13](fil-forge/ingot@8c0cd13)) - fil-forge/ingot#345 ([c8cb626](fil-forge/ingot@c8cb626)) - fil-forge/ingot#346 ([ff68601](fil-forge/ingot@ff68601)) - fil-forge/ingot#347 ([c7f3d8d](fil-forge/ingot@c7f3d8d)) - fil-forge/ingot#348 ([e0bd021](fil-forge/ingot@e0bd021)) - fil-forge/ingot#349 ([a4b8dd7](fil-forge/ingot@a4b8dd7)) - fil-forge/ingot#350 ([2ea37b7](fil-forge/ingot@2ea37b7)) - fil-forge/ingot#351 ([d330171](fil-forge/ingot@d330171)) - fil-forge/ingot#352 ([6172383](fil-forge/ingot@6172383)) - fil-forge/ingot#362 ([bfd2961](fil-forge/ingot@bfd2961)) - fil-forge/ingot#363 ([a2e9841](fil-forge/ingot@a2e9841)) - fil-forge/ingot#365 ([7bfca91](fil-forge/ingot@7bfca91))
Sets every staging pin in `nodes/staging/eu-central-3/apps/versions.env` to what `nodes/dev/apps/versions.env` pins on `main`. Merging this is the promotion: the node picks it up on its next reconcile pass, waits for a safe proving window and restarts each service that changed. This pull request is rebuilt on every push to `main` and closed once staging matches dev, so edits to it are overwritten unless they are pushed to `bot/promote-staging` by hand. ## piri [`sha256:d0e88a9`](https://github.com/orgs/fil-forge/packages/container/piri/1506614110) → [`sha256:e83df92`](https://github.com/orgs/fil-forge/packages/container/piri/1535715713) What fil-forge/piri brings over staging's pin, [`029d86d..f62b605`](fil-forge/piri@029d86d...f62b605): - fil-forge/piri#215 ([3d07e1a](fil-forge/piri@3d07e1a)) - fil-forge/piri#214 ([6c47937](fil-forge/piri@6c47937)) - fil-forge/piri#216 ([6f2be19](fil-forge/piri@6f2be19)) - fil-forge/piri#218 ([67efb3b](fil-forge/piri@67efb3b)) - fil-forge/piri#217 ([a53055c](fil-forge/piri@a53055c)) - fil-forge/piri#228 ([f62b605](fil-forge/piri@f62b605)) ## ingot [`sha256:ac6dc48`](https://github.com/orgs/fil-forge/packages/container/ingot/1530171943) → [`sha256:86a32ca`](https://github.com/orgs/fil-forge/packages/container/ingot/1538499073) What fil-forge/ingot brings over staging's pin, [`af89169..82a70c9`](fil-forge/ingot@af89169...82a70c9): - fil-forge/ingot#361 ([7d9f32b](fil-forge/ingot@7d9f32b)) - fil-forge/ingot#330 ([cddc726](fil-forge/ingot@cddc726)) - fil-forge/ingot#331 ([ce9e35c](fil-forge/ingot@ce9e35c)) - fil-forge/ingot#334 ([b9fd991](fil-forge/ingot@b9fd991)) - fil-forge/ingot#336 ([c054d14](fil-forge/ingot@c054d14)) - fil-forge/ingot#337 ([26ee5e9](fil-forge/ingot@26ee5e9)) - fil-forge/ingot#340 ([155d969](fil-forge/ingot@155d969)) - fil-forge/ingot#341 ([0aff04e](fil-forge/ingot@0aff04e)) - fil-forge/ingot#342 ([24d752a](fil-forge/ingot@24d752a)) - fil-forge/ingot#343 ([e04c724](fil-forge/ingot@e04c724)) - fil-forge/ingot#344 ([8c0cd13](fil-forge/ingot@8c0cd13)) - fil-forge/ingot#345 ([c8cb626](fil-forge/ingot@c8cb626)) - fil-forge/ingot#346 ([ff68601](fil-forge/ingot@ff68601)) - fil-forge/ingot#347 ([c7f3d8d](fil-forge/ingot@c7f3d8d)) - fil-forge/ingot#348 ([e0bd021](fil-forge/ingot@e0bd021)) - fil-forge/ingot#349 ([a4b8dd7](fil-forge/ingot@a4b8dd7)) - fil-forge/ingot#350 ([2ea37b7](fil-forge/ingot@2ea37b7)) - fil-forge/ingot#351 ([d330171](fil-forge/ingot@d330171)) - fil-forge/ingot#352 ([6172383](fil-forge/ingot@6172383)) - fil-forge/ingot#362 ([bfd2961](fil-forge/ingot@bfd2961)) - fil-forge/ingot#363 ([a2e9841](fil-forge/ingot@a2e9841)) - fil-forge/ingot#365 ([7bfca91](fil-forge/ingot@7bfca91)) - fil-forge/ingot#319 ([3d5520b](fil-forge/ingot@3d5520b)) - fil-forge/ingot#353 ([82a70c9](fil-forge/ingot@82a70c9))
Sets every staging pin in `nodes/staging/eu-central-3/apps/versions.env` to what `nodes/dev/apps/versions.env` pins on `main`. Merging this is the promotion: the node picks it up on its next reconcile pass, waits for a safe proving window and restarts each service that changed. This pull request is rebuilt on every push to `main` and closed once staging matches dev, so edits to it are overwritten unless they are pushed to `bot/promote-staging` by hand. ## piri [`sha256:d0e88a9`](https://github.com/orgs/fil-forge/packages/container/piri/1506614110) → [`sha256:e83df92`](https://github.com/orgs/fil-forge/packages/container/piri/1535715713) What fil-forge/piri brings over staging's pin, [`029d86d..f62b605`](fil-forge/piri@029d86d...f62b605): - fil-forge/piri#215 ([3d07e1a](fil-forge/piri@3d07e1a)) - fil-forge/piri#214 ([6c47937](fil-forge/piri@6c47937)) - fil-forge/piri#216 ([6f2be19](fil-forge/piri@6f2be19)) - fil-forge/piri#218 ([67efb3b](fil-forge/piri@67efb3b)) - fil-forge/piri#217 ([a53055c](fil-forge/piri@a53055c)) - fil-forge/piri#228 ([f62b605](fil-forge/piri@f62b605)) ## ingot [`sha256:ac6dc48`](https://github.com/orgs/fil-forge/packages/container/ingot/1530171943) → [`sha256:86a32ca`](https://github.com/orgs/fil-forge/packages/container/ingot/1538499073) What fil-forge/ingot brings over staging's pin, [`af89169..82a70c9`](fil-forge/ingot@af89169...82a70c9): - fil-forge/ingot#361 ([7d9f32b](fil-forge/ingot@7d9f32b)) - fil-forge/ingot#330 ([cddc726](fil-forge/ingot@cddc726)) - fil-forge/ingot#331 ([ce9e35c](fil-forge/ingot@ce9e35c)) - fil-forge/ingot#334 ([b9fd991](fil-forge/ingot@b9fd991)) - fil-forge/ingot#336 ([c054d14](fil-forge/ingot@c054d14)) - fil-forge/ingot#337 ([26ee5e9](fil-forge/ingot@26ee5e9)) - fil-forge/ingot#340 ([155d969](fil-forge/ingot@155d969)) - fil-forge/ingot#341 ([0aff04e](fil-forge/ingot@0aff04e)) - fil-forge/ingot#342 ([24d752a](fil-forge/ingot@24d752a)) - fil-forge/ingot#343 ([e04c724](fil-forge/ingot@e04c724)) - fil-forge/ingot#344 ([8c0cd13](fil-forge/ingot@8c0cd13)) - fil-forge/ingot#345 ([c8cb626](fil-forge/ingot@c8cb626)) - fil-forge/ingot#346 ([ff68601](fil-forge/ingot@ff68601)) - fil-forge/ingot#347 ([c7f3d8d](fil-forge/ingot@c7f3d8d)) - fil-forge/ingot#348 ([e0bd021](fil-forge/ingot@e0bd021)) - fil-forge/ingot#349 ([a4b8dd7](fil-forge/ingot@a4b8dd7)) - fil-forge/ingot#350 ([2ea37b7](fil-forge/ingot@2ea37b7)) - fil-forge/ingot#351 ([d330171](fil-forge/ingot@d330171)) - fil-forge/ingot#352 ([6172383](fil-forge/ingot@6172383)) - fil-forge/ingot#362 ([bfd2961](fil-forge/ingot@bfd2961)) - fil-forge/ingot#363 ([a2e9841](fil-forge/ingot@a2e9841)) - fil-forge/ingot#365 ([7bfca91](fil-forge/ingot@7bfca91)) - fil-forge/ingot#319 ([3d5520b](fil-forge/ingot@3d5520b)) - fil-forge/ingot#353 ([82a70c9](fil-forge/ingot@82a70c9))
Sets every staging pin in `nodes/staging/eu-central-3/apps/versions.env` to what `nodes/dev/apps/versions.env` pins on `main`. Merging this is the promotion: the node picks it up on its next reconcile pass, waits for a safe proving window and restarts each service that changed. This pull request is rebuilt on every push to `main` and closed once staging matches dev, so edits to it are overwritten unless they are pushed to `bot/promote-staging` by hand. ## piri [`sha256:d0e88a9`](https://github.com/orgs/fil-forge/packages/container/piri/1506614110) → [`sha256:e83df92`](https://github.com/orgs/fil-forge/packages/container/piri/1535715713) What fil-forge/piri brings over staging's pin, [`029d86d..f62b605`](fil-forge/piri@029d86d...f62b605): - fil-forge/piri#215 ([3d07e1a](fil-forge/piri@3d07e1a)) - fil-forge/piri#214 ([6c47937](fil-forge/piri@6c47937)) - fil-forge/piri#216 ([6f2be19](fil-forge/piri@6f2be19)) - fil-forge/piri#218 ([67efb3b](fil-forge/piri@67efb3b)) - fil-forge/piri#217 ([a53055c](fil-forge/piri@a53055c)) - fil-forge/piri#228 ([f62b605](fil-forge/piri@f62b605)) ## ingot [`sha256:ac6dc48`](https://github.com/orgs/fil-forge/packages/container/ingot/1530171943) → [`sha256:86a32ca`](https://github.com/orgs/fil-forge/packages/container/ingot/1538499073) What fil-forge/ingot brings over staging's pin, [`af89169..82a70c9`](fil-forge/ingot@af89169...82a70c9): - fil-forge/ingot#361 ([7d9f32b](fil-forge/ingot@7d9f32b)) - fil-forge/ingot#330 ([cddc726](fil-forge/ingot@cddc726)) - fil-forge/ingot#331 ([ce9e35c](fil-forge/ingot@ce9e35c)) - fil-forge/ingot#334 ([b9fd991](fil-forge/ingot@b9fd991)) - fil-forge/ingot#336 ([c054d14](fil-forge/ingot@c054d14)) - fil-forge/ingot#337 ([26ee5e9](fil-forge/ingot@26ee5e9)) - fil-forge/ingot#340 ([155d969](fil-forge/ingot@155d969)) - fil-forge/ingot#341 ([0aff04e](fil-forge/ingot@0aff04e)) - fil-forge/ingot#342 ([24d752a](fil-forge/ingot@24d752a)) - fil-forge/ingot#343 ([e04c724](fil-forge/ingot@e04c724)) - fil-forge/ingot#344 ([8c0cd13](fil-forge/ingot@8c0cd13)) - fil-forge/ingot#345 ([c8cb626](fil-forge/ingot@c8cb626)) - fil-forge/ingot#346 ([ff68601](fil-forge/ingot@ff68601)) - fil-forge/ingot#347 ([c7f3d8d](fil-forge/ingot@c7f3d8d)) - fil-forge/ingot#348 ([e0bd021](fil-forge/ingot@e0bd021)) - fil-forge/ingot#349 ([a4b8dd7](fil-forge/ingot@a4b8dd7)) - fil-forge/ingot#350 ([2ea37b7](fil-forge/ingot@2ea37b7)) - fil-forge/ingot#351 ([d330171](fil-forge/ingot@d330171)) - fil-forge/ingot#352 ([6172383](fil-forge/ingot@6172383)) - fil-forge/ingot#362 ([bfd2961](fil-forge/ingot@bfd2961)) - fil-forge/ingot#363 ([a2e9841](fil-forge/ingot@a2e9841)) - fil-forge/ingot#365 ([7bfca91](fil-forge/ingot@7bfca91)) - fil-forge/ingot#319 ([3d5520b](fil-forge/ingot@3d5520b)) - fil-forge/ingot#353 ([82a70c9](fil-forge/ingot@82a70c9))
Sets every staging pin in `nodes/staging/eu-central-3/apps/versions.env` to what `nodes/dev/apps/versions.env` pins on `main`. Merging this is the promotion: the node picks it up on its next reconcile pass, waits for a safe proving window and restarts each service that changed. This pull request is rebuilt on every push to `main` and closed once staging matches dev, so edits to it are overwritten unless they are pushed to `bot/promote-staging` by hand. ## piri [`sha256:d0e88a9`](https://github.com/orgs/fil-forge/packages/container/piri/1506614110) → [`sha256:e83df92`](https://github.com/orgs/fil-forge/packages/container/piri/1535715713) What fil-forge/piri brings over staging's pin, [`029d86d..f62b605`](fil-forge/piri@029d86d...f62b605): - fil-forge/piri#215 ([3d07e1a](fil-forge/piri@3d07e1a)) - fil-forge/piri#214 ([6c47937](fil-forge/piri@6c47937)) - fil-forge/piri#216 ([6f2be19](fil-forge/piri@6f2be19)) - fil-forge/piri#218 ([67efb3b](fil-forge/piri@67efb3b)) - fil-forge/piri#217 ([a53055c](fil-forge/piri@a53055c)) - fil-forge/piri#228 ([f62b605](fil-forge/piri@f62b605)) ## ingot [`sha256:ac6dc48`](https://github.com/orgs/fil-forge/packages/container/ingot/1530171943) → [`sha256:86a32ca`](https://github.com/orgs/fil-forge/packages/container/ingot/1538499073) What fil-forge/ingot brings over staging's pin, [`af89169..82a70c9`](fil-forge/ingot@af89169...82a70c9): - fil-forge/ingot#361 ([7d9f32b](fil-forge/ingot@7d9f32b)) - fil-forge/ingot#330 ([cddc726](fil-forge/ingot@cddc726)) - fil-forge/ingot#331 ([ce9e35c](fil-forge/ingot@ce9e35c)) - fil-forge/ingot#334 ([b9fd991](fil-forge/ingot@b9fd991)) - fil-forge/ingot#336 ([c054d14](fil-forge/ingot@c054d14)) - fil-forge/ingot#337 ([26ee5e9](fil-forge/ingot@26ee5e9)) - fil-forge/ingot#340 ([155d969](fil-forge/ingot@155d969)) - fil-forge/ingot#341 ([0aff04e](fil-forge/ingot@0aff04e)) - fil-forge/ingot#342 ([24d752a](fil-forge/ingot@24d752a)) - fil-forge/ingot#343 ([e04c724](fil-forge/ingot@e04c724)) - fil-forge/ingot#344 ([8c0cd13](fil-forge/ingot@8c0cd13)) - fil-forge/ingot#345 ([c8cb626](fil-forge/ingot@c8cb626)) - fil-forge/ingot#346 ([ff68601](fil-forge/ingot@ff68601)) - fil-forge/ingot#347 ([c7f3d8d](fil-forge/ingot@c7f3d8d)) - fil-forge/ingot#348 ([e0bd021](fil-forge/ingot@e0bd021)) - fil-forge/ingot#349 ([a4b8dd7](fil-forge/ingot@a4b8dd7)) - fil-forge/ingot#350 ([2ea37b7](fil-forge/ingot@2ea37b7)) - fil-forge/ingot#351 ([d330171](fil-forge/ingot@d330171)) - fil-forge/ingot#352 ([6172383](fil-forge/ingot@6172383)) - fil-forge/ingot#362 ([bfd2961](fil-forge/ingot@bfd2961)) - fil-forge/ingot#363 ([a2e9841](fil-forge/ingot@a2e9841)) - fil-forge/ingot#365 ([7bfca91](fil-forge/ingot@7bfca91)) - fil-forge/ingot#319 ([3d5520b](fil-forge/ingot@3d5520b)) - fil-forge/ingot#353 ([82a70c9](fil-forge/ingot@82a70c9))
Sets every staging pin in `nodes/staging/eu-central-3/apps/versions.env` to what `nodes/dev/apps/versions.env` pins on `main`. Merging this is the promotion: the node picks it up on its next reconcile pass, waits for a safe proving window and restarts each service that changed. This pull request is rebuilt on every push to `main` and closed once staging matches dev, so edits to it are overwritten unless they are pushed to `bot/promote-staging` by hand. ## piri [`sha256:d0e88a9`](https://github.com/orgs/fil-forge/packages/container/piri/1506614110) → [`sha256:d5b2a71`](https://github.com/orgs/fil-forge/packages/container/piri/1542539277) What fil-forge/piri brings over staging's pin, [`029d86d..5e80c10`](fil-forge/piri@029d86d...5e80c10): - fil-forge/piri#215 ([3d07e1a](fil-forge/piri@3d07e1a)) - fil-forge/piri#214 ([6c47937](fil-forge/piri@6c47937)) - fil-forge/piri#216 ([6f2be19](fil-forge/piri@6f2be19)) - fil-forge/piri#218 ([67efb3b](fil-forge/piri@67efb3b)) - fil-forge/piri#217 ([a53055c](fil-forge/piri@a53055c)) - fil-forge/piri#228 ([f62b605](fil-forge/piri@f62b605)) - fil-forge/piri#220 ([5e80c10](fil-forge/piri@5e80c10)) ## ingot [`sha256:ac6dc48`](https://github.com/orgs/fil-forge/packages/container/ingot/1530171943) → [`sha256:86a32ca`](https://github.com/orgs/fil-forge/packages/container/ingot/1538499073) What fil-forge/ingot brings over staging's pin, [`af89169..82a70c9`](fil-forge/ingot@af89169...82a70c9): - fil-forge/ingot#361 ([7d9f32b](fil-forge/ingot@7d9f32b)) - fil-forge/ingot#330 ([cddc726](fil-forge/ingot@cddc726)) - fil-forge/ingot#331 ([ce9e35c](fil-forge/ingot@ce9e35c)) - fil-forge/ingot#334 ([b9fd991](fil-forge/ingot@b9fd991)) - fil-forge/ingot#336 ([c054d14](fil-forge/ingot@c054d14)) - fil-forge/ingot#337 ([26ee5e9](fil-forge/ingot@26ee5e9)) - fil-forge/ingot#340 ([155d969](fil-forge/ingot@155d969)) - fil-forge/ingot#341 ([0aff04e](fil-forge/ingot@0aff04e)) - fil-forge/ingot#342 ([24d752a](fil-forge/ingot@24d752a)) - fil-forge/ingot#343 ([e04c724](fil-forge/ingot@e04c724)) - fil-forge/ingot#344 ([8c0cd13](fil-forge/ingot@8c0cd13)) - fil-forge/ingot#345 ([c8cb626](fil-forge/ingot@c8cb626)) - fil-forge/ingot#346 ([ff68601](fil-forge/ingot@ff68601)) - fil-forge/ingot#347 ([c7f3d8d](fil-forge/ingot@c7f3d8d)) - fil-forge/ingot#348 ([e0bd021](fil-forge/ingot@e0bd021)) - fil-forge/ingot#349 ([a4b8dd7](fil-forge/ingot@a4b8dd7)) - fil-forge/ingot#350 ([2ea37b7](fil-forge/ingot@2ea37b7)) - fil-forge/ingot#351 ([d330171](fil-forge/ingot@d330171)) - fil-forge/ingot#352 ([6172383](fil-forge/ingot@6172383)) - fil-forge/ingot#362 ([bfd2961](fil-forge/ingot@bfd2961)) - fil-forge/ingot#363 ([a2e9841](fil-forge/ingot@a2e9841)) - fil-forge/ingot#365 ([7bfca91](fil-forge/ingot@7bfca91)) - fil-forge/ingot#319 ([3d5520b](fil-forge/ingot@3d5520b)) - fil-forge/ingot#353 ([82a70c9](fil-forge/ingot@82a70c9))
Sets every staging pin in `nodes/staging/eu-central-3/apps/versions.env` to what `nodes/dev/apps/versions.env` pins on `main`. Merging this is the promotion: the node picks it up on its next reconcile pass, waits for a safe proving window and restarts each service that changed. This pull request is rebuilt on every push to `main` and closed once staging matches dev, so edits to it are overwritten unless they are pushed to `bot/promote-staging` by hand. ## piri [`sha256:d0e88a9`](https://github.com/orgs/fil-forge/packages/container/piri/1506614110) → [`sha256:cf21468`](https://github.com/orgs/fil-forge/packages/container/piri/1542621852) What fil-forge/piri brings over staging's pin, [`029d86d..e663f38`](fil-forge/piri@029d86d...e663f38): - fil-forge/piri#215 ([3d07e1a](fil-forge/piri@3d07e1a)) - fil-forge/piri#214 ([6c47937](fil-forge/piri@6c47937)) - fil-forge/piri#216 ([6f2be19](fil-forge/piri@6f2be19)) - fil-forge/piri#218 ([67efb3b](fil-forge/piri@67efb3b)) - fil-forge/piri#217 ([a53055c](fil-forge/piri@a53055c)) - fil-forge/piri#228 ([f62b605](fil-forge/piri@f62b605)) - fil-forge/piri#220 ([5e80c10](fil-forge/piri@5e80c10)) - fil-forge/piri#225 ([e663f38](fil-forge/piri@e663f38)) ## ingot [`sha256:ac6dc48`](https://github.com/orgs/fil-forge/packages/container/ingot/1530171943) → [`sha256:86a32ca`](https://github.com/orgs/fil-forge/packages/container/ingot/1538499073) What fil-forge/ingot brings over staging's pin, [`af89169..82a70c9`](fil-forge/ingot@af89169...82a70c9): - fil-forge/ingot#361 ([7d9f32b](fil-forge/ingot@7d9f32b)) - fil-forge/ingot#330 ([cddc726](fil-forge/ingot@cddc726)) - fil-forge/ingot#331 ([ce9e35c](fil-forge/ingot@ce9e35c)) - fil-forge/ingot#334 ([b9fd991](fil-forge/ingot@b9fd991)) - fil-forge/ingot#336 ([c054d14](fil-forge/ingot@c054d14)) - fil-forge/ingot#337 ([26ee5e9](fil-forge/ingot@26ee5e9)) - fil-forge/ingot#340 ([155d969](fil-forge/ingot@155d969)) - fil-forge/ingot#341 ([0aff04e](fil-forge/ingot@0aff04e)) - fil-forge/ingot#342 ([24d752a](fil-forge/ingot@24d752a)) - fil-forge/ingot#343 ([e04c724](fil-forge/ingot@e04c724)) - fil-forge/ingot#344 ([8c0cd13](fil-forge/ingot@8c0cd13)) - fil-forge/ingot#345 ([c8cb626](fil-forge/ingot@c8cb626)) - fil-forge/ingot#346 ([ff68601](fil-forge/ingot@ff68601)) - fil-forge/ingot#347 ([c7f3d8d](fil-forge/ingot@c7f3d8d)) - fil-forge/ingot#348 ([e0bd021](fil-forge/ingot@e0bd021)) - fil-forge/ingot#349 ([a4b8dd7](fil-forge/ingot@a4b8dd7)) - fil-forge/ingot#350 ([2ea37b7](fil-forge/ingot@2ea37b7)) - fil-forge/ingot#351 ([d330171](fil-forge/ingot@d330171)) - fil-forge/ingot#352 ([6172383](fil-forge/ingot@6172383)) - fil-forge/ingot#362 ([bfd2961](fil-forge/ingot@bfd2961)) - fil-forge/ingot#363 ([a2e9841](fil-forge/ingot@a2e9841)) - fil-forge/ingot#365 ([7bfca91](fil-forge/ingot@7bfca91)) - fil-forge/ingot#319 ([3d5520b](fil-forge/ingot@3d5520b)) - fil-forge/ingot#353 ([82a70c9](fil-forge/ingot@82a70c9))
Sets every staging pin in `nodes/staging/eu-central-3/apps/versions.env` to what `nodes/dev/apps/versions.env` pins on `main`. Merging this is the promotion: the node picks it up on its next reconcile pass, waits for a safe proving window and restarts each service that changed. This pull request is rebuilt on every push to `main` and closed once staging matches dev, so edits to it are overwritten unless they are pushed to `bot/promote-staging` by hand. ## piri [`sha256:d0e88a9`](https://github.com/orgs/fil-forge/packages/container/piri/1506614110) → [`sha256:cf21468`](https://github.com/orgs/fil-forge/packages/container/piri/1542621852) What fil-forge/piri brings over staging's pin, [`029d86d..e663f38`](fil-forge/piri@029d86d...e663f38): - fil-forge/piri#215 ([3d07e1a](fil-forge/piri@3d07e1a)) - fil-forge/piri#214 ([6c47937](fil-forge/piri@6c47937)) - fil-forge/piri#216 ([6f2be19](fil-forge/piri@6f2be19)) - fil-forge/piri#218 ([67efb3b](fil-forge/piri@67efb3b)) - fil-forge/piri#217 ([a53055c](fil-forge/piri@a53055c)) - fil-forge/piri#228 ([f62b605](fil-forge/piri@f62b605)) - fil-forge/piri#220 ([5e80c10](fil-forge/piri@5e80c10)) - fil-forge/piri#225 ([e663f38](fil-forge/piri@e663f38)) ## ingot [`sha256:ac6dc48`](https://github.com/orgs/fil-forge/packages/container/ingot/1530171943) → [`sha256:70a32a8`](https://github.com/orgs/fil-forge/packages/container/ingot/1542745662) What fil-forge/ingot brings over staging's pin, [`af89169..2a460d7`](fil-forge/ingot@af89169...2a460d7): - fil-forge/ingot#361 ([7d9f32b](fil-forge/ingot@7d9f32b)) - fil-forge/ingot#330 ([cddc726](fil-forge/ingot@cddc726)) - fil-forge/ingot#331 ([ce9e35c](fil-forge/ingot@ce9e35c)) - fil-forge/ingot#334 ([b9fd991](fil-forge/ingot@b9fd991)) - fil-forge/ingot#336 ([c054d14](fil-forge/ingot@c054d14)) - fil-forge/ingot#337 ([26ee5e9](fil-forge/ingot@26ee5e9)) - fil-forge/ingot#340 ([155d969](fil-forge/ingot@155d969)) - fil-forge/ingot#341 ([0aff04e](fil-forge/ingot@0aff04e)) - fil-forge/ingot#342 ([24d752a](fil-forge/ingot@24d752a)) - fil-forge/ingot#343 ([e04c724](fil-forge/ingot@e04c724)) - fil-forge/ingot#344 ([8c0cd13](fil-forge/ingot@8c0cd13)) - fil-forge/ingot#345 ([c8cb626](fil-forge/ingot@c8cb626)) - fil-forge/ingot#346 ([ff68601](fil-forge/ingot@ff68601)) - fil-forge/ingot#347 ([c7f3d8d](fil-forge/ingot@c7f3d8d)) - fil-forge/ingot#348 ([e0bd021](fil-forge/ingot@e0bd021)) - fil-forge/ingot#349 ([a4b8dd7](fil-forge/ingot@a4b8dd7)) - fil-forge/ingot#350 ([2ea37b7](fil-forge/ingot@2ea37b7)) - fil-forge/ingot#351 ([d330171](fil-forge/ingot@d330171)) - fil-forge/ingot#352 ([6172383](fil-forge/ingot@6172383)) - fil-forge/ingot#362 ([bfd2961](fil-forge/ingot@bfd2961)) - fil-forge/ingot#363 ([a2e9841](fil-forge/ingot@a2e9841)) - fil-forge/ingot#365 ([7bfca91](fil-forge/ingot@7bfca91)) - fil-forge/ingot#319 ([3d5520b](fil-forge/ingot@3d5520b)) - fil-forge/ingot#353 ([82a70c9](fil-forge/ingot@82a70c9)) - fil-forge/ingot#378 ([2a460d7](fil-forge/ingot@2a460d7))


The second step of #328, stacked on #330: the scheduling features the task runner RFC describes for async tasks. Like #330, nothing in the server uses them yet.
The package also gets a README with an example of every feature, including the earlier ones.
🤖 Generated with Claude Code