fix(scheduler): survive SQL-unavailable startup and recover hung tasks (GenRTP incident) - #48
Merged
Merged
Conversation
After the 2026-07-10 shared reboot, GenRTP hung on an unavailable SQL Server while holding its in-process lock; the stale detector alerted once and skipped ~6,700 runs for 5 days without recovering. This addresses the whole failure chain: - Enforce per-action TimeoutMinutes (persisted but never applied) on all execution paths: Executable/Batch/sqlcmd children run through RunProcessWithTimeout (kills the process tree on expiry) and the PowerShell timeout now uses the action value instead of a fixed 4 h. A scheduler-level backstop (timeout + 2 min) kills tracked child processes and releases the lock if the execution path itself hangs. - Track child processes by PID per task in TaskRunner (AsyncLocal task context) and expose KillProcessesForTask; stale-lock recovery now kills the owning process and force-releases the lock instead of resetting state and re-dispatching into a held lock forever. - Repeat stale/blocked alerts on a configurable cadence with an escalating ALERT #N counter; persistent skipped runs (lock unavailable) also alert instead of only logging warnings. - Gate SQL-dependent tasks (SQL actions + Reliability/SqlGatedTasks name patterns, default GenRTP,Extracteur) behind a new SqlReadinessChecker: lightweight connection probe with exponential backoff, deferring the task to the next tick while SQL Server is down. Login/database errors count as engine-up so a permissions issue cannot block tasks forever. - Add configurable post-reboot startup delay (default 90 s) and stagger simultaneous launches (default 5 s apart) with a pending-dispatch guard against double launch; dispatch is offloaded to the thread pool so lock waits no longer stall the scheduler tick. - Suppress Windows hard-error dialogs via SetErrorMode (inherited by children) and only pass PsExec -i in interactive sessions, so a child cannot block invisibly on a dialog under the service. - ExecuteSqlTask now reports failure when sqlcmd fails or times out. New optional settings section: Reliability (StartupDelaySeconds, TaskLaunchStaggerSeconds, SqlReadinessEnabled, SqlReadinessServer, SqlGatedTasks, StaleAlertRepeatMinutes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014A3QMSjA4Lz6kB5Xh6KbTQ
svtica
marked this pull request as ready for review
July 15, 2026 14:56
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.
Context
On 2026-07-10 ~16:00, all servers rebooted simultaneously for Windows Updates. LiteTask started before a SQL Server was up, launched all 6 tasks in the same second, and a process hung while holding its in-process lock. The stale detector alerted once, its cleanup could not release a lock whose process was still alive, and the process was silently skipped ~6,700 times over 5 days until a manual service restart on 2026-07-15.
This PR addresses the whole failure chain, following the six reliability goals from the incident review.
Changes
1. Dependency readiness gate (new
SqlReadinessChecker)Before dispatching a SQL-dependent task, the scheduler probes SQL Server with a lightweight 5 s connection test. While unavailable, the task is deferred to the next 30 s tick (its schedule is untouched) and probes retry with exponential backoff (15 s → 10 min cap), each attempt logged. A task is SQL-dependent when it has a SQL action or its name matches
Reliability/SqlGatedTasks(defaultGenRTP,Extracteur, case-insensitive substring match — this generalizes the site-sideExtracteur_VerifierSiPossibleExecutioncheck into a reusable gate). Safety valve: a login/database error means the engine answered, so it counts as ready — a permissions issue can never block tasks forever.2. Per-task execution timeout with forced kill (the key fix)
TaskAction.TimeoutMinuteswas persisted since day one but never enforced. Now:Executable/Batchchildren andsqlcmdrun throughRunProcessWithTimeout, which kills the entire process tree on expiry (the old no-timeoutRunProcesspath is removed).action timeout + 2 min) kills the tracked child processes, releases the lock, logs an error, and lets the next schedule fire — even if the execution path itself hangs (e.g. output streams never closing).3. Stale-lock recovery that actually kills the process
TaskRunnernow tracks child processes by PID per task (task name flows viaAsyncLocal, no signature changes) and exposesKillProcessesForTask.CleanupStaleTasksno longer resets the running state at 15 min — that reset is what caused the endless re-dispatch into a held lock. Instead it alerts, and pastsum of action timeouts + 10 minit kills the owning process(es), force-releases the semaphore, and resets the state.4. Escalating stale/blocked alerting
Stale alerts repeat every
Reliability/StaleAlertRepeatMinutes(default 60) with anALERT #Ncounter in the subject while the condition persists. A task that keeps being skipped (lock unavailable / already running) now also raises repeating alerts after 15 min of blockage, instead of thousands of silent log warnings.5. Post-reboot startup delay + staggered launch
The service waits
Reliability/StartupDelaySeconds(default 90) before the first dispatch, and tasks due in the same tick are staggeredReliability/TaskLaunchStaggerSecondsapart (default 5 s) via a pending-dispatch guard that also prevents double launches. Dispatch is offloaded to the thread pool, so the up-to-30 s lock wait no longer stalls the scheduler tick.6. Non-interactive child execution
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX)at service start (inherited by children) prevents Windows hard-error dialogs from blocking a child invisibly, and the PsExec-iflag is now only passed in interactive sessions. stdout/stderr were already redirected and logged; additionallyExecuteSqlTaskno longer reports success when sqlcmd fails or times out.Configuration
New optional
Reliabilitysection insettings.xml(defaults apply when absent):Behavior changes to review
TimeoutMinutes(default 60 min). Any task that legitimately runs longer needs itsTimeoutMinutesraised in the task definition.Integrated Securityunder the service account (same pattern asSqlTab); the login-error-counts-as-ready rule keeps this safe even if the account has no SQL login.