Repository navigation
feat: run phase time limits and result file Status (#124) - #191
Merged
Merged
Conversation
The result file was only written when a run reached the end, so a calling TI process couldn't tell an aborted run from one that never ran. Every exit of a run now writes it, with Overall Success = False on abort: fatal execution errors, missing --config, invalid taskfile, failed TM1 taskfile read and exclusive-mode timeout. Settings now load before config.ini resolution so the result path (--result > settings.ini) is known for the earliest exits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hung RushTI run blocked the TI process that called it with ExecuteCommand. Each run phase (task_retrieval, connection, task_execution, push_results) can now have a time limit: [timeouts] in settings.ini, settings.timeouts in the taskfile, or --timeout-<phase> on run and resume. Off by default. When a phase goes past its limit, a watchdog thread logs the phase, writes the result file with Status = TimedOut, keeps the checkpoint, flushes logs, writes stats to local SQLite only, logs out of TM1 in daemon threads (at most ~5s, same preserve rule as logout()) and exits with os._exit(1). The exclusive-mode wait doesn't count against the connection phase. The --result file gains a trailing Status column for every outcome: Success, Failed, Aborted or TimedOut. Also stops a failed }rushti.load.results from overwriting the run's own success flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exclusive-mode timeout exits with code 1 (Status = Aborted), not 5. Checked on TM1 v11.8 and v12.6: logging out on a hang doesn't roll back a TI process that's already running; it runs to completion and commits. The docs now say so, and that its real outcome has to be traced on the server before resuming, since the checkpoint shows it as in progress. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closed
- A hang already being handled now blocks watchdog start/stop/paused, so the main thread can't run its normal end-of-run cleanup alongside it (restoring a deleted checkpoint, overwriting TimedOut, racing exits). - CheckpointManager takes a lock: a save from the watchdog thread could hit "dictionary changed size during iteration" while the scheduler recorded results, leaving a stale checkpoint behind. - task_execution now also covers the end-of-run logout of the task instances, which can block on the server too. - task_retrieval's CLI > settings.ini precedence reuses get_effective_settings instead of re-implementing it. - Drop the no-op reassignment of the hang handler's session dicts, and the duplicate outcomes list in work_through_tasks_dag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) - Once a hang is declared, the scheduler starts no new task and stops early session release, so no TI starts that the result file and checkpoint don't count. - The watchdog checks for a hang in progress under its lock; before, a stop() racing the timer could return while the handler ran. - Only the sleeps between exclusive-mode session polls are paused; each poll calls TM1 and now counts against the connection phase, so a poll that blocks is caught. - The hang handler only writes stats once the run row exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) assertNoLogs is Python 3.10+, so the test failed on 3.9. Saving in a loop until the writer finished could starve the writer of the lock on the Linux runner, which hung the 3.13 job. Watch logger.warning instead, and do a fixed number of saves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
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.
Closes #124.
Summary
Each run phase can now have an optional time limit. A phase that goes past its limit is a hang: RushTI writes a
TimedOutresult and exits, so the TI process that called it stops waiting. Limits are off by default.On a hang, the watchdog thread (
HangHandlerin the newrushti/timeouts.py):Once a hang is declared, the scheduler starts no new task and stops early session release. While the hang is being handled,
watchdog.start/stop/pausedblock until the handler is done, so the main thread can't run its own end-of-run cleanup at the same time.CheckpointManagertakes a lock, so the watchdog thread's save can't collide with the scheduler recording results.Settings follow the
max_workersprecedence:--timeout-<phase>> taskfilesettings.timeouts>settings.ini [timeouts]> no limit.rushti resumeforwards the flags.The
--resultfile gets a trailingStatuscolumn on every outcome:Success,Failed,Aborted(including the exclusive-mode wait running out) orTimedOut.Follow-ups from review: #192 (result file on every early exit), #193 and #194 (cleanups).
Also included:
}rushti.load.resultsno longer overwrites the run's own success flag.cancel_at_timeoutdoesn't cancel on TM1 v12, because TM1py blocks its own thread calls there. The docs now say so wherever it's documented.docs/features/timeouts.md.Evidence
After: a real process with
setup_tm1_servicesblocking and--timeout-connection 2exits with code 1 after about 2s:TimedOut, exit within limit + 5s);task_retrievalignored for local taskfiles; the exclusive-mode wait not counted; the checkpoint kept and SQLite stats written; preserved connections handled; a logout that never returns not holding up the exit;Statusfor each outcome; precedence and validation in all three tiers; resume forwarding; a hang already being handled blocksstop()/start(); the checkpoint saved from another thread while results are recorded (this reproduceddictionary changed size during iterationbefore the lock); a final logout that blocks counting againsttask_execution;stop()racing the timer never returning during a hang; no new task starting after a hang; an exclusive-mode session poll that blocks counting againstconnection.test_v11_chore_kind.py::test_mixed_dag_executes_in_order, fails the same way onmaster:}bedrock.server.waitwithpWaitSec=0aborts on tm1srv01.Status=TimedOut. Passes on both; a negative control fails as it should.rushti runwith a TI that writes a cell and then sleeps, and--timeout-task-execution 8: exit code 1 after about 8–10s,TimedOutwritten, checkpoint kept, no RushTI session left behind.Merge Danger
Door: one-way
The
[timeouts]/settings.timeouts/--timeout-*settings and theStatuscolumn are new public surface. Once released they're hard to remove.Blast Radius: result-file readers
--resultfile now sees an extra trailing column. TM1 ignores it with a fixed set of variables (checked on v11 and v12). A reader that parses the line some other way (e.g. counting delimiters) could break.setup_tm1_servicesgained two optional parameters, andwork_through_tasks_daggained an optionalon_first_task_startcallback; the defaults keep existing callers working as before.🤖 Generated with Claude Code