Repository navigation
feat(backend): clean up old task history in the task manager and during the upgrade [INFP-507] - #10869
Closed
saltas888 wants to merge 41 commits into
Closed
feat(backend): clean up old task history in the task manager and during the upgrade [INFP-507]#10869saltas888 wants to merge 41 commits into
saltas888 wants to merge 41 commits into
Conversation
2 of 5 tasks
Contributor
There was a problem hiding this comment.
2 issues found across 26 files
Confidence score: 4/5
test_cleanup.pychanges both the day and run count, so it doesn’t verify that a count-only increase reports progress. Add a poll with the same day and a higherdeleted_runscount.task-manager-api.mdsaysrewrittencan be true even when no table was rewritten, including when all tables are skipped for lock timeouts. Clarify that it indicates the rewrite pass completed.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="backend/tests/unit/task_manager/flow_run/test_cleanup.py">
<violation number="1" location="backend/tests/unit/task_manager/flow_run/test_cleanup.py:84">
P2: This poll changes both the day and run count, so it does not verify that a count-only increase reports progress. Add a poll that keeps the same day while increasing `deleted_runs`, and assert that notification.</violation>
</file>
<file name="dev/specs/infp-507-task-history-retention/contracts/task-manager-api.md">
<violation number="1" location="dev/specs/infp-507-task-history-retention/contracts/task-manager-api.md:32">
P2: `rewritten` can be true even when no table was rewritten: the job sets it after the rewrite pass returns, including when every table is skipped for lock timeouts. Document it as indicating that the rewrite pass completed, and let `not_rewritten` identify skipped tables.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
This was referenced Oct 4, 2026
saltas888
force-pushed
the
task-history-cleanup-infp-507
branch
2 times, most recently
from
October 11, 2026 18:00
4779e71 to
e65cadc
Compare
saltas888
force-pushed
the
task-history-cleanup-infp-507
branch
from
October 14, 2026 13:27
20256f2 to
9655ff6
Compare
saltas888
force-pushed
the
task-history-cleanup-infp-507
branch
from
October 27, 2026 22:07
9655ff6 to
54873d0
Compare
3 of 5 tasks
saltas888
force-pushed
the
task-history-cleanup-infp-507
branch
from
October 28, 2026 09:39
54873d0 to
77aae1f
Compare
…routes
POST /infrahub/task-history/cleanup starts a background job in the task
manager that deletes the finished top-level runs older than Prefect's
flow-run vacuum retention, one day of end times per transaction: the runs
first (DELETE ... RETURNING id, selected FOR UPDATE SKIP LOCKED), then the
logs and artifacts of those runs, batched under the bind-parameter limit.
Further passes catch subflows whose parent was deleted. No table, temporary
table or index is created in Prefect's database.
On Postgres the job holds an advisory lock on a connection of its own (409
from another replica), and with rewrite=true runs VACUUM FULL on the six
task history tables when the deletes freed more than half of the runs,
with a 60s lock timeout and 3 retries per table. SQLite uses a process
lock and never rewrites. GET /infrahub/task-history/cleanup/{id} reports
the job. Stale artifact collections are left to Prefect's daily orphan
cleanup, as after Prefect's own vacuum.
Component tests compare the end state with Prefect's vacuum_old_flow_runs
on a copy of the same database, run both concurrently, and cover the
routes. The contract now spells out the rewrite, size and error fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cleanup request's rewrite becomes a mode instead of a boolean: "never" (default), "if_freed" (rewrite only when the runs left are fewer than half of the runs counted at the start, so Prefect's own deletes during the job count too) and "always". The job reports the requested mode in "rewrite" and the outcome in a new "rewritten" field; SQLite never rewrites. A condition on the cleanup's own deletes skipped the rewrite exactly when the disk had to come back: after a Helm rollout or after lowering the retention, Prefect's hourly cleanup deletes the old runs first and a later maintenance run of flush flow-runs --rewrite deleted too little to rewrite. The command's rewrite option will send "always" and the upgrade "if_freed". The seeded task history scenario moves into the seed helpers so that a Postgres test can reuse it. The spec set is updated for the new modes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p job infrahub tasks flush flow-runs now starts the task manager's task history cleanup and polls it to its end, printing a line each time its day or its number of deleted runs changes, then a summary: runs deleted and the cutoff, the table sizes before and after on Postgres, and whether the tables were rewritten. --rewrite asks for the rewrite "always", and the command asks for "never" without it. --days-to-keep and --batch-size are gone: the task manager reads its own retention. Exit code 0 on success and when the task manager has no cleanup route (an older release), 1 when the cleanup fails. A cleanup running on another replica (409) or unknown to the replica that answers (404 while polling) is started again after a 2s wait; committed days stay deleted. The client gives up with exit code 1 after 3 hours without an answer that shows a cleanup. infrahub upgrade gets step 6/7 "Task history cleanup" after the task manager step, with the rewrite "if_freed", and --no-task-history-cleanup to leave it out on an instance that still serves (the Helm upgrade hook). A failed cleanup cancels the upgrade, as a failed migration does. Unit tests drive the client, the command and the upgrade step against a scripted task manager on an httpx mock transport; an integration-docker test seeds Postgres through the task manager container and runs the command with --rewrite against the full stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The committed days stay deleted and a failed rewrite leaves its table intact, so the upgrade reports the failure with the command that finishes the cleanup and continues to the branch rebase instead of stopping with exit code 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ger restart The cleanup client now orders the rewrite modes never < if_freed < always. When the job it followed was started by another caller with a weaker mode, it posts again with its own once that job completes, so `infrahub tasks flush flow-runs --rewrite` no longer exits 0 without a rewrite after adopting an interrupted upgrade's if_freed job. A transport error while the client follows a job (a task manager restart that outlasts Prefect's client retries) is treated like a 404: wait and post again inside the same give-up window. A transport error on the first request still propagates. `flush flow-runs` now handles the same failures as the upgrade step (TaskHistoryCleanupError and httpx errors, PrefectHTTPStatusError included): one error line and exit 1 instead of a traceback. Both share the handling through clean_task_history, which returns the failure. The route test that held the cleanup lock in a try/finally now takes it from a yield fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holds an ACCESS SHARE lock on the log table from another psql session in the task-manager-db container, runs the cleanup in the task manager container with a 1 s lock timeout and no retries, and asserts that only log is reported not rewritten while the five other tables get a new relfilenode. The lock holder is terminated on fixture teardown, and the test runs last in the class so a leaked lock cannot stall the flush test. The in-container script's exit code is now checked. Without the lock the test fails with `[] != ['log']`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ewrite decision The job started with no decision, so the loop's first hold of the start lock decides the rewrite instead of a separate call before it; the job still ends in the same hold as its last check of the mode. The comment on the maintenance statement timeout now says what a timeout of None does: Prefect turns it into the API's 10-second timeout, and the engine cache then hands back the API's own engine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…deletes of a task history cleanup A cleanup whose cutoff falls at noon deletes a run that ended at 06:00 that day and keeps one that ended at 18:00. With the cap on the day's end removed, this test fails as the 18:00 run is deleted, and no other test noticed. The comparison with Prefect's vacuum now seeds three old runs that ended on one day and runs with one run per statement as well, so a day's log and artifact deletes span several statements. With the batching loop replaced by a single statement over the first batch, the one-run case fails on the logs and artifacts left behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d one committed While the task manager answered that a cleanup runs elsewhere, that it no longer knows the cleanup, or could not be reached, the client waited up to three hours without a word, and its give-up error merged the three conditions. It now reports each wait through a second callback when the wait starts, when its reason changes, and when it starts again after a job showed, and the give-up error names the reason seen last. The flush command and the upgrade print one line per reported wait. A job that ends failed now raises with the job as it ended, and both print a second line with what it had committed: the runs it deleted, and the rewrite when that had finished. The job records no table of a rewrite cut short, so none is named then. The client builds its request with the condition it reuses, and the upgrade test of an unexpected answer pins the whole line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… --check` The check said nothing about the new step, which deletes finished task runs irreversibly. It now prints a "Task history" section before "Branches": the upgrade deletes the finished task runs older than the task manager's task history retention, rewrites the tables if the deletes free more than half of their runs, and --no-task-history-cleanup leaves this out. It does not query the task manager. The check has no test of its own to extend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…leanup A script run in the task manager container builds three advisory locks on a key of the test's own and checks, in order, that the first acquires, the second is refused, the second acquires once the first releases, and a third acquires once both released. The key is not the cleanup's, so a cleanup that another test runs on the shared stack never holds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e is free The upgrade's cleanup decided the `if_freed` rewrite against the runs it counted when it started. On the Compose upgrade path, `docker compose run` starts the task manager fresh, and Prefect's flow-run vacuum deletes the old runs at startup, before the cleanup starts. The cleanup then counted only the runs left, deleted none and never rewrote: with 120,305 old runs the tables stayed at 166.5 MB, so the disk space never came back. `if_freed` now rewrites when the live rows of `flow_run` hold less than half of its size on disk, whoever deleted the runs and when. The size on disk is the main data and the TOAST table, without indexes, free-space map and visibility map; the live size sums `pg_column_size` of each column plus 28 bytes a row, so no value stored out of line is read. It runs on the maintenance pool, as it scans the whole table. `flow_run` stands in for the six tables, as the other five lose their rows with their runs. The job no longer counts runs: `delete` returns nothing, `rewrite` takes no count, and `TaskHistoryTables.count_runs` is gone. A decision taken again after a stronger start measures the space again. The upgrade docstring, the `--check` line, the `if_freed` docstring, the changelog fragment and the spec documents state the new rule. The rewrite tests script the measured space through the recording rewriter, including a cleanup that deleted nothing from a mostly free table, and a mostly live table raised to `always` after the decision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n Postgres A script run in the task manager container inserts 2,000 finished runs that ended 35 days ago and 20 recent ones after them, and plain SQL deletes the old ones before any cleanup starts, as Prefect's vacuum does when the task manager starts. A cleanup started through the route with `if_freed` then deletes no run and rewrites all six tables (each gets a new file), and the tables are smaller after it than before. The recent runs keep the freed pages from ending the table, where a vacuum could cut them off before the cleanup measures. Against an image of the earlier cleanup, which counted runs, the test fails on the job reporting `rewritten` False. A second test inserts 500 recent runs, compacts the runs table with `VACUUM FULL`, and checks that an `if_freed` cleanup with no run to delete leaves every table's file as it was. The scripts use only Prefect, SQLAlchemy and httpx, so they run in an image of any version of the cleanup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… each freed table Follows the design doc's changes of 8 and 11 Oct 2026. The upgrade no longer runs the one-off task history cleanup: its task manager step prints how much disk space the task history takes and the command that returns it, read from a new GET /infrahub/task-history/size route with a 10 s bound, and the command alone when the task manager answers late, with an error or without a size. The "Task history cleanup" step, the 1/7 to 7/7 numbering, --no-task-history-cleanup and the --check text are gone. `infrahub tasks flush flow-runs --rewrite` now asks for `if_freed`, the only rewrite mode left besides `never`. The job rewrites each of the six task history tables and events, event_resources whose live rows hold less than half of its disk space, measured from a random sample of pages above 10,000 pages, and records and prints the tables it rewrote, left because they are mostly live, and could not lock. The spec set, contracts and changelog fragments follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hint now says the size could not be read, with the error type, before the command, so the failure it swallows is reported. Its comments no longer name the upgrade, and the --rewrite help starts with "After the deletes". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hint The design doc suggests that the hint report how many old runs are left, so an operator can wait until the hourly cleanup has worked through the backlog, when the cleanup step is mostly the rewrite. The size route now also answers the finished top-level runs older than the task history retention, counted up to 100,000, and whether there are more, so the count stays fast on millions of runs. The hint reads, for example, "Task history takes 24.8 GB, with 1,234 runs older than the retention." Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…upports the new flush flow-runs The tool passes --days-to-keep and --batch-size, which this change removes; opsmill/infrahub-backup#179 makes it use the task manager's retention. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…production cleanup Each VACUUM FULL of the rewrite now follows the limits of the manual cleanup of the event tables of OpsMill's production task manager: - lock_timeout of 3 s, retried up to 4 times 20 s apart, instead of 60 s with 3 retries: while a rewrite waits for its lock, every later query on the table queues behind it; - statement_timeout of 60 s: a stopped rewrite leaves the table as it was, and the job lists it in a new timed_out field that the command prints; - a plain VACUUM (ANALYZE) of each rewritten table; - before the next table, a pause of ten times as long as the previous table was locked, up to 60 s, so Prefect catches up on the writes that waited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
saltas888
force-pushed
the
task-history-cleanup-infp-507
branch
from
October 28, 2026 11:01
77aae1f to
7aefafe
Compare
…pgrade hint The spec set still gave the old 60 s lock timeout with 3 retries, left out the timed-out tables, said that other queries never queue behind a rewrite waiting for its lock, and gave the upgrade hint without the count of old runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…tgreSQL On a SQLite task manager database the cleanup has no rewriter, so --rewrite deletes the old runs and rewrites nothing. The upgrade also prints its task history hint only on PostgreSQL, so the retention fragment says so too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…inished The job recorded the rewrite outcomes only after the last table, so a cleanup that failed partway through its rewrite named none of the tables it had already rewritten in autocommit. Each table now joins its list as soon as its rewrite ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…its SQLSTATE The rewrite told a lock timeout from a statement timeout by the asyncpg exception that SQLAlchemy chains as __cause__, which SQLAlchemy does not document. It now compares the SQLSTATE in the documented .sqlstate of the adapter's error, and the backend no longer imports asyncpg directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…fails A rewrite that the database rejected, for example because the disk had no room for the copy, failed the whole cleanup and left the tables after it untried. The server rolls back only that table's copy, so the table is now listed as failed and the job goes on; a lost connection or a client error still fails the job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…its lock The advisory lock lived on a session that sat idle for the whole cleanup, so an idle timeout, a terminated session or a failover could drop it while the job ran and let another replica start a second cleanup. The session now checks every 30 s that it still holds the lock, and a job that cannot confirm it stops its current step and fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…saction A cleanup deleted a whole day of runs, with their task runs, logs and artifacts, in one transaction, which held its locks and blocked vacuum for as long as the day took, showed no progress within the day and rolled the whole day back on a failure. Each transaction now deletes at most 1,000 runs of the day, the earliest ended first, with their logs and artifacts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…eted runs grows Within a day, a cleanup now commits its deletes in several transactions, so the count of deleted runs can grow while the day stays the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…nother session Only the runs themselves were selected with SKIP LOCKED; their task runs, states, logs, artifacts and subflows could keep a delete waiting until the 24 h maintenance timeout, holding the cleanup lock all along. Each delete transaction now waits at most 60 s for a lock, and the job then fails with an error that names the day. The comment on the maintenance timeout now gives the reason for its 24 h value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…tch keeps old task runs With PREFECT_SERVER_SERVICES_DB_VACUUM_ENABLED set to a value without flow_runs, Prefect deletes no old task runs and the task history retention is not applied, yet the command still deleted them and the upgrade hint still advised it. The task manager now refuses the cleanup with the reason, the command prints it and exits 1, and the hint says why old runs stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
An answer to the start or to a read of the cleanup that held no cleanup, such as a sign-in page or a state from another release, ended the command with a pydantic traceback. The client now raises its own error with the status and each validation error, which the command prints on one line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
… cleanup is followed Behind an ingress or a service mesh, a restarting task manager answers 502, 503 or 504 instead of failing the connection, and the task manager answers 503 while its database starts, so the command failed where it would have waited for a transport error. Once the task manager has answered, these answers now lead to the same wait and new start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
…mmary When the task manager lost the cleanup the command followed, for example in a restart, the command started a new one and reported only that one's count, so the progress fell and the summary left out the runs already deleted. The client now adds the last-seen count of each lost cleanup, and the summary says 'at least' when it does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
The data model said that only a lost connection or a client error fails the job during the rewrite, but a job that cannot confirm its cleanup lock, or a server error outside a table's rewrite, fails it too. The free-disk edge case now also says what happens when the write-ahead log has no room either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
… answer The check of the cleanup lock was bounded by cancelling its query, and asyncpg waits without a limit for the server's answer to a cancelled query. A lock session that stopped answering, through a stopped server or a lost network, therefore kept the job running, and another replica could start a second cleanup once the server ended that session. A check without an answer within the interval now closes the lock's connection at once, so the job stops and fails. Releasing the lock waits for a check in progress instead of cancelling it. The integration test of a lost lock now checks every 3 s instead of every second, so a slow answer on a busy runner is not taken for a lost lock, and a new one silences the lock's connection through a proxy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwJNFw8rccyxc4Wp9eZFbH
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.
Why
The large instances known to OpsMill hold 20 to 35 GB of task history. Prefect's own cleanup took 2 h 26 min on 25 GB in the benchmark, and Postgres does not shrink its files after deletes, so the disk only comes back after a rewrite of the tables. Design doc: Task history and Activity log retention (D9 in the numbering this stack uses, D7 since the doc renumbered its decisions on 11 Oct); spec set in
dev/specs/infp-507-task-history-retention/.Goal: delete the backlog fast with SQL that follows Prefect's own rules, rewrite each table that is mostly free space, and tell operators about it during the upgrade without slowing the upgrade.
The design doc's Design changes for implementation list asked for two changes to this PR, both done:
--no-task-history-cleanupand the--checktext are removed, and the upgrade prints a hint instead.Closes part of INFP-507.
What changed
Behavioural changes:
infrahub tasks flush flow-runs [--rewrite]deletes, in the task manager, the finished top-level runs older than the task history retention with their task runs, states, logs and artifacts, one day of end times per transaction, and prints progress per day.--rewritethen rewrites, withVACUUM FULL, each offlow_run,flow_run_state,task_run,task_run_state,log,artifact,eventsandevent_resourceswhose live rows hold less than half of its disk space, whoever deleted the rows. The instance does not need to be stopped: tasks that write to a table wait while it is rewritten.Rewrote …,Left … as they are, because their live rows hold at least half of their disk space,Did not rewrite …, which stayed locked by other sessions,Did not rewrite …, whose rewrite took longer than its time limit and was stopped.--days-to-keepand--batch-sizeare removed; the cutoff comes from the task manager's retention, set fromtask_manager.retention.task_history(changelog+flush-flow-runs.changed.md).infrahub upgradedeletes no task history and keeps its six steps. In its task manager step it printsTask history takes 24.8 GB, with 1,234 runs older than the retention. To get the space back, run: infrahub tasks flush flow-runs --rewrite. The count stops at 100,000 (more than 100,000 runs), so it stays fast on millions of runs; withno runs older than the retention, the step only shrinks the tables. The size and the count are read within 10 s. A task manager that answers late, answers with an error (404from a release without the route) or answers without a size getsCould not read the task history size from the task manager (<error type>).before the command, and the upgrade goes on.POST /infrahub/task-history/cleanup({"rewrite": "never" | "if_freed"}) andGET /infrahub/task-history/cleanup/{id}, a background job per task manager;GET /infrahub/task-history/size, the disk space of the six task history tables with their indexes (nullon SQLite), and the finished top-level runs older than the retention, counted up to 100,000 (old_runs,old_runs_limit_reached).Implementation notes:
vacuum_old_flow_runs(top-level, terminal state,end_timebefore the cutoff, which is Prefect's owndb_vacuum.retention_period), same order (runs first, then their logs and artifacts), runs selected withFOR UPDATE SKIP LOCKED, so it never waits on Prefect's own cleanup. Passes repeat until nothing is eligible, which catches subflows left without a parent.409; the CLI waits and posts again, and also waits out a task-manager restart while it follows a job. Committed days are never redone.if_freedwhen it asks for it, and the job decides again before it completes; it never rewrites twice.pg_column_sizeplus 28 bytes a row, so no value stored out of line is read). It measures space rather than counting deleted runs, because Prefect's own cleanup usually deletes the old records before the command runs.TABLESAMPLE SYSTEMover about 10,000 random pages, which is built into Postgres. On a restored copy of Fatih's 1.10 large backup (12 GBevents, 12 GBevent_resources), the estimate was within 2 % of the full read and took 0.2 to 0.7 s against 4 to 6 s. A fully live table measures about 0.9 of its disk size, so it is left as it is.pg_total_relation_size, which reads no table data.VACUUM FULLruns in autocommit with the limits of the manual cleanup of the event tables of OpsMill's production task manager on 26 Oct (changed on 27 Oct, fromlock_timeout = 60swith 3 retries):lock_timeout = 3s, retried up to 4 times 20 s apart, then listed as not rewritten. While aVACUUM FULLwaits for its lock, every later query on the table queues behind it, so the wait stays short.statement_timeout = 60s: a stoppedVACUUM FULLleaves the table as it was, and the job lists it in a newtimed_outfield.VACUUM (ANALYZE)of each rewritten table, which blocks nothing.prefect_server/task_history_models.py, so the CLI client imports them without loading Prefect's database layer orasyncpg(a unit test checks it).The design doc's two suggestions are implemented: the command names each table it skipped and why, and the hint counts the old runs. The count selects the runs as the cleanup does and stops at 100,001, because an exact count would read every old run, millions on the instances this is for.
Needs approval
backend/AGENTS.mdlists new REST endpoints under "Ask first".GET /infrahub/task-history/sizeis a new read-only route on the task manager, not on the Infrahub API, so it is not inschema/openapi.json; like the other task manager routes, it has no authentication. The design doc's hint needs it, because the upgrade runs in the Infrahub server container, which reaches the task manager only over HTTP.Known limitations
--rewritemeasures each table again and rewrites those still mostly free. With a Kubernetes Service and keep-alive connections, reads stay on one pod.How to review
backend/infrahub/prefect_server/task_history.py(job, lock, deletes, per-table rewrite, routes).backend/infrahub/task_manager/flow_run/cleanup.py(client loop and size read), thencli/tasks.pyandcli/upgrade.py.tests/component/task_manager/test_task_history_cleanup.pyruns Prefect'svacuum_old_flow_runson one copy of a seeded database and the new cleanup on another, and compares every remaining run, task run, state, log and artifact ID (plus a concurrent run of both), and checks the per-table rewrite decision;test_task_history_routes.py;tests/integration_docker/test_task_history_cleanup.pyon Postgres through the real command, including the sampled measurement and a locked table; unit tests for the client, the CLI and the upgrade's hint.How to test
Impact & rollout
flush flow-runsoptions removed (breaking, changelog). Theinfrahub-taskmanager flush flow-runsmaintenance tool passes both options, and fails on this release without opsmill/infrahub-backup#179, which runs the command without a number of days from 1.13 on; the changelog points the tool's users to it. Deletion is irreversible; set a longertask_historybefore upgrading to keep older runs.Validated locally on 27 Oct, after the change to the rewrite limits and the rebase onto
develop: unit and component tests (576 passed), the integration-docker tests against an image built from the rebased stack (6 passed, including a rewrite stopped at its time limit), this PR alone without the PRs above it (169 tests passed, ruff), mypy and the generated docs.Validated locally on 11 Oct: unit, component and integration-docker tests (the integration tests against an image built from this branch, 5 passed); the free-space sampling against a restored copy of Fatih's 1.10 large backup; quickstart Part 1 steps 5 and 6 on a dev stack with 30,000 old runs (results in #10879);
/pre-cichecks (ruff, mypy, generated docs, test shards, changelog style). Earlier, on 4 to 8 Oct: the Compose upgrade guide run end to end with 120,305 old runs, a Postgres 14 and 18 probe of the advisory lock, concurrency and the lock timeout, and cubic review (three passes). Release evidence on restored 25 GB and 100 GB backups comes from opsmill/infrahub-private-tests, run separately.Checklist
🤖 Generated with Claude Code