Skip to content

[Cuebot] Double booking 5 - #2513

Draft
DiegoTavares wants to merge 8 commits into
AcademySoftwareFoundation:masterfrom
DiegoTavares:double_booking_5
Draft

[Cuebot] Double booking 5#2513
DiegoTavares wants to merge 8 commits into
AcademySoftwareFoundation:masterfrom
DiegoTavares:double_booking_5

Conversation

@DiegoTavares

Copy link
Copy Markdown
Collaborator

Handle lingering double booking conditions:

DiegoTavares and others added 8 commits August 27, 2026 21:40
getKillClearance wrote the attempt counter back to killRequestCounterCache
before checking the retry limit. Since put() restarts the cache's
expireAfterWrite clock and a still-running frame is re-detected on every
host report (~10s), every denied attempt refreshed the entry, so it could
never expire: after one exhausted burst of 3 failed kills, the host+frame
pair was never killed again for as long as the zombie kept reporting.

A transient kill failure (RQD busy behind its monitor-loop mutex, a
dropped RPC, a deadline) therefore left a double-booked render running to
completion. The double-booking log crawl shows this class directly: 259
overlapping runs in the last two weeks where the superseded render
survived 20+ minutes (p90 2.7h) after the frame was rebooked, most
finishing SUCCESS hours later.

Only write the counter on granted attempts. Once the limit is hit the
entry is left untouched, expiring FRAME_KILL_CACHE_EXPIRE_AFTER_WRITE
minutes after the last granted kill, after which a fresh kill budget is
available -- converting "give up forever" into "back off for 3 minutes".

Unit tests pin the cooldown with a fake ticker; the refresh-forever
regression is covered explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2kQu2ZFrvhTLAdjTau2hQ
…t delivered

lostProc treated a successful killFrame RPC as proof the render was
stopped and released the booking immediately. RQD acknowledges a kill as
soon as the SIGTERM is delivered to the process group -- the render stays
alive (still writing output) until it honors the signal, which takes
~20s for a well-behaved render flushing its log, and indefinitely for one
that traps SIGTERM (rust RQD ships with force_kill_after_timeout=false,
so there is no SIGKILL escalation). Releasing on the ACK re-books the
frame within seconds while the superseded render is still running: the
double-booking log crawl attributes ~3,200 overlapping runs in the last
two weeks to this window (booking freed mid-run, superseded log closing
~20s after the replacement started).

- RqdClient.killFrame now returns whether the frame is CONFIRMED not
  running: true only on RQD's NOT_FOUND answer (nothing to kill), false
  when the kill was delivered to a live render.
- lostProc releases immediately only on confirmed-stopped. A delivered
  kill defers the release instead: the proc and RUNNING frame are left
  intact so the run's own FrameCompleteReport finalizes it through the
  normal ownership-fenced path. The existing lost_proc_max_defer_ms bound
  still applies (frame parked DEAD, never WAITING, if the report never
  arrives).
- A delivered kill is positive proof of a live RQD and render, so it also
  disables the host-DOWN / host-not-Up / rebooted-since-dispatch release
  escape hatches for that proc: a host marked DOWN that still answers its
  kill RPC is partitioned or report-starved, not dead.

Unit tests cover the ACK-vs-NOT_FOUND return in RqdClientGrpc against a
real localhost gRPC server, and the new defer/confirm/bound resolutions
in lostProc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2kQu2ZFrvhTLAdjTau2hQ
lostProc released a proc the moment its host was marked DOWN
(EXIT_STATUS_DOWN_HOST or !isHostUp): the kill's failure was ignored and
the RUNNING frame went straight back to WAITING. But DOWN only means "no
processed host report for 300s" -- a network partition or a Cuebot-side
report-ingest stall produces exactly the same mark while every render on
the host keeps running. clearDownProcs then rebooked all of them at once,
which the double-booking log crawl shows as the multi-minute-gap
double-render class (~3.5k rows post-Aug-11; partition-length overlaps,
first render killed only after the partition healed).

A DOWN host now goes through the same fail-closed deferral as any
unconfirmed kill, and the lost_proc_max_defer_ms bound decides by
evidence when it expires:

- Host down/not-Up AND silent for the whole bound: a live-but-partitioned
  host would have resumed reporting, so it is treated as genuinely dead
  and the frame returns to WAITING (auto-retry preserved, delayed by the
  bound).
- Host still Up (or a delivered kill whose complete report never came):
  frame parked DEAD (manual retry), as before.

Kill retries against an unreachable host stay cheap after the first
attempt: the cached gRPC channel is in TRANSIENT_FAILURE and fails fast
until its backoff reconnects.

clearDownProcs now skips its Sentry report for deferred procs, and only
counts actual releases.

The trade-off is that frames on a genuinely dead host re-queue after
lost_proc_max_defer_ms (20 min default) instead of ~5 minutes; partitions
shorter than the bound no longer double-render at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2kQu2ZFrvhTLAdjTau2hQ
updateFrameHostDown (UPDATE_FRAME_REASON) reset a frame to WAITING with
no state fence -- only a no-proc-attached guard. Its caller checks the
frame is DEAD first, but between that read and the update the frame can
be finalized by another actor (eaten, completed after a late report), and
the unfenced write would resurrect it for a fresh re-run of an already
finished frame.

Add str_state='DEAD' to the WHERE clause so the resurrection can only
ever flip the state it was designed for. DAO test covers both the
resurrect and refuse directions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2kQu2ZFrvhTLAdjTau2hQ
Four defects let a "successfully killed" frame keep rendering after
Cuebot released or re-booked it, producing double-renders that the Aug
2026 log crawl shows surviving from 20 minutes to hours:

- No SIGKILL escalation by default: force_kill_after_timeout was false,
  so the kill monitor logged "gave up" after 20 minutes and a
  SIGTERM-trapping render survived its kill indefinitely. Escalation is
  now on by default, with a 5 minute timeout and 1 minute checks.
- The monitor countdown only decremented while limit >= interval, so a
  timeout that is not a multiple of the interval never reached zero and
  escalation never fired. The decrement is now unconditional.
- killpg ESRCH ("no such process") was reported as a failed kill even
  though the target being gone is exactly the state the kill was meant
  to reach. Cuebot then counted it against its kill-retry budget and
  eventually stopped killing the zombie. ESRCH now counts as success in
  kill_session/force_kill_session/force_kill on both platforms.
- Killing a frame in a terminal state (Finished/FailedBeforeStart)
  returned FAILED_PRECONDITION, which Cuebot treats as an unconfirmed
  kill and defers on. It now returns NOT_FOUND ("nothing running to
  kill"), the strongest not-running proof Cuebot already understands.

get_pid_to_kill now returns a typed KillOutcome instead of a stringly
Result, and kill_all_running_frames skips unkillable frames instead of
aborting the sweep on the first one (a single not-yet-started frame
previously left every other frame alive and propagated an error into the
NIMBY lock path).

Unit tests cover all four get_pid_to_kill dispositions and the ESRCH
mapping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2kQu2ZFrvhTLAdjTau2hQ
A kill for a frame in Created state (inserted into the running-frames
cache, process not spawned yet -- a window that stretches to tens of
seconds when log setup blocks on NFS) was refused with
FAILED_PRECONDITION and forgotten: no kill_reason was recorded, so the
frame proceeded to launch and render as if nothing happened. Cuebot
kills landing in that window (verification kills, kill-before-release,
orphan reclaims) were silently dropped, leaving a render alive on a
booking Cuebot believed it had killed.

The kill is now recorded on the Created state as a pending kill and the
RPC is acknowledged (KillOutcome::Scheduled): start() carries the reason
into the Running state, and run_inner terminates the process group
immediately after spawn, before the render does any real work. The
frame then finishes through its normal report path with exit_signal 15.

kill_all_running_frames counts scheduled kills as handled instead of
skipping the frame.

Tests cover the pending-kill recording, the carry-over into Running, and
an end-to-end run where a launch-window kill terminates the spawned
process immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2kQu2ZFrvhTLAdjTau2hQ
When the cached /proc scan failed to see a frame's pid for 15 seconds,
monitor_running_frames declared the frame finished (exit_code=1,
exit_signal=19), removed it from the cache and queued a
FrameCompleteReport -- without ever touching the process. A stalled or
incomplete scan (the monitor loop blocks on NFS stats and the same mutex
serves launches and kills) produces exactly that picture while the
render is alive: Cuebot receives the completion, re-books the frame, and
the untouched render keeps running as an RQD-originated double-render
that even the verification kill cannot see (the frame is no longer in
the cache, so isFrameRunning answers NOT_FOUND).

The dangling expiry now probes the pid directly with kill(pid, 0) --
independent of the cached scan. A live process is force-killed and kept
in the cache; the completion is only reported once a later sweep
confirms the pid is actually gone.

Tests cover the pid probe for both a live process and a reaped child.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2kQu2ZFrvhTLAdjTau2hQ
The monitor loop propagated errors from collect_and_send_host_report and
handle_nimby_state_change with `?`, terminating the loop on the first
failure: one report send that exhausts its gRPC retry ladder (Cuebot
restart windows, network blips), or one NIMBY kill_all error, and the
host silently stops reporting forever while its frames keep rendering.
Cuebot then marks the host DOWN after 5 minutes and re-books every
running frame -- a whole-host double-render factory, invisible until
someone notices the host went quiet (main() only logs a JoinError).

Both failures are now logged and retried on the next 5s tick instead of
killing the loop. The NIMBY state transition keeps its previous state on
failure so the transition is re-attempted, and kill_all_running_frames
no longer aborts on individual frames since the previous commits.

The existing integration suite covers the loop's happy path; the change
is error containment only, with no behavior change on success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2kQu2ZFrvhTLAdjTau2hQ
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DiegoTavares DiegoTavares changed the title Double booking 5 [Cuebot] Double booking 5 Aug 28, 2026
@DiegoTavares

Copy link
Copy Markdown
Collaborator Author

Most fixes on this PR have a strong tradeoff that needs to be evaluated. I'm keeping it on standby for now. Planning to get back to it next week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant