Repository navigation
[-] fix active_chain row leak when a chain fails before its tasks run - #862
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Roll back partially started transactions before removing run status to avoid leaking pool connections.
1 open finding
What changed in this PR
Fixes leaked active_chain rows when chain startup or task loading fails.
Changes:
- Cleans up run status on early-return paths.
- Uses cancellation-independent cleanup contexts.
- Adds tests for failure scenarios.
| File | Summary |
|---|---|
internal/scheduler/chain.go |
Adds cleanup for early chain failures. |
internal/scheduler/chain_test.go |
Tests active-chain cleanup behavior. |
🧠 Review effort: Lite
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
Coverage Report for CI Build 38762415067Coverage increased (+0.4%) to 88.288%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
active_chain row leak when a chain fails before its tasks run
|
@dependabot rebase |
`executeChain()` returned early on `StartTransaction()` and `GetChainElements()` errors without calling `RemoveChainRunStatus()`, so the row inserted by the worker stayed in `timetable.active_chain`. Once the leaked rows reached `max_instances` the chain was never started again. Remove the run status on both paths using a context detached from cancellation, as the task failure path already does.
… can leak it Both workers now call `removeChainRunStatus` right after `executeChain` returns, pairing the delete with the insert in one place. Cleanup runs on a detached context bounded by `log.DetachedTimeout` so a cancelled scheduler context cannot skip it and a dead database cannot block a worker forever. Chains that fail before any task runs now count as failed: span status, `RecordChainFailed` and the `on_error` handler fire on every failure path. `StartTransaction` rolls back its own transaction when the vxid lookup fails instead of handing a live `tx` back with an error.
9d56709 to
974381b
Compare
|
The 30s
To reproduce: No rows ever land in Creating the detached context where cleanup starts, on each exit path, should fix it. I can push that if you'd like. |
|
Sure! Go ahead! That's you PR after all! :) Thanks for all the help! |
… starts `executeChain` created its detached cleanup context before the chain's transaction began, so `DetachedTimeout` was counted from the start of the chain. Any chain running longer than 30s committed on an expired context: pgx never sent `COMMIT`, the chain's transaction was rolled back, and the chain was still logged as successful. Rollback, `on_error` and self-destruct after 30s hit the same expired context. Each exit path now creates its own detached context when its cleanup starts. `DetachedTimeout` is a variable so the new test can shorten it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… chains - commit has no deadline, `DetachedTimeout` bounds only rollback and `DeleteChain` - `on_error` runs on the chain context without a deadline - remove `CommitTransaction` wrapper, log early failures once
SummaryReview fixes in a729814 and 259ae7a. executeChain
StartTransaction / GetChainElements / executeTask
- on failure: chainFailed(bctx: WithoutCancel + 30s) # rollback, on_error share 30s
+ on failure: chainFailed(l, tx, err, msg) # logs once, via task logger
+ rollback (DetachedTimeout)
+ executeOnErrorHandler(ctx) # no deadline; skipped if chain was stopped
- CommitTransaction(bctx: 30s) # error only logged
+ tx.Commit(WithoutCancel(ctx)) # no deadline
+ on error: chainFailed("Chain failed to commit") # on_error runs, chain not completed
- DeleteChain(bctx)
+ DeleteChain(DetachedTimeout) # self-destruct only after a successful commit RemoveChainRunStatus(chain_id, client_name)
- DELETE every active_chain row of the chain # frees other instances' max_instances slots
+ DELETE one row: ctid = (SELECT ... LIMIT 1 FOR UPDATE SKIP LOCKED)
Evidence
Merge DangerDoor: two-way Code-only change, no migration; revert restores the previous behaviour. Blast Radius: chain execution A chain stopped by |
|
hey @olaberglund, do these changes look good to you? |
|
Hi @pashagolub ! Yes, I agree with your new changes. Especially treating a failed commit as a failure was a good addition. |

What
Fixes an
active_chainrow leak when a chain cannot start a transaction or load its tasks. Each such early return fromexecuteChainleft onetimetable.active_chainrow behind. Aftermax_instancesof them the chain was never scheduled again until restart or reconnect.Seen on pg_timetable 7.0.0 with a chain scheduled every second under
query_wait_timeout.How
chainWorkerandintervalChainWorkercallremoveChainRunStatusright afterexecuteChainreturns, beforeUnlockand before anyRepeatAfterreschedule. No exit path insideexecuteChaincan leak the row any more, and the slot now also covers theon_errorhandler.on_error, self-destruct) runs onlog.DetachedContext: detached from cancellation sonotify_chain_stop()cannot skip it, and bounded by a 30slog.DetachedTimeoutso a dead database cannot block a worker goroutine forever.RecordChainFailedand theon_errorhandler fire on every failure path. Thectx.Err()guard inexecuteOnErrorHandleris gone since it always receives the detached context now.StartTransactionrolls back its own transaction when the vxid lookup fails and returns a niltx, instead of handing a live transaction back together with an error.Tests
TestExecuteChainEarlyReturncoversBeginfailing,Beginfailing under a cancelled context, the vxid lookup failing, andGetChainElementsfailing. Each case asserts rollback where a transaction exists and thaton_errorruns.TestChainWorkerRemovesRunStatusOnEarlyReturnproves the worker deletes the row after an early return.TestStartTransactioncovers the vxid failure rollback.TestExecuteChainCancelledContextCleansUpnow drives the realchainWorkeragainst a Postgres container instead of simulating it by hand.Investigation and implementation were aided by an LLM (Claude). Targets v7.2.0.