Skip to content

[cuebot] Add extra measures to prevent double booking - #2506

Merged
DiegoTavares merged 5 commits into
AcademySoftwareFoundation:masterfrom
DiegoTavares:double_booking_04
Aug 12, 2026
Merged

DiegoTavares merged 5 commits into
AcademySoftwareFoundation:masterfrom
DiegoTavares:double_booking_04

Conversation

@DiegoTavares

@DiegoTavares DiegoTavares commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator

Patch another condition that may lead to double-booking on retried frames.

retryFrame released the frame (WAITING, proc link cleared) before killing RQD, so it was instantly booked onto a second host. FrameCompleteHandler then applied the killed run's report without checking the reporting proc still owned the frame, stopping the new run and freeing the frame again — every kill produced a report and every report freed the frame, sustaining a kill→report→free→rebook loop across hosts and silently discarding successful renders.

  • FrameCompleteHandler: ownership fence — a report may only stop the frame while its proc still holds it; superseded reports divert to the stale path. The stale path no longer unbooks a proc re-dispatched the same frame, and unapplied reports (notably exit 0) are logged and counted.
  • JobManagerSupport.retryFrame: kill and confirm dead via isFrameRunning polling (dispatcher.retry_frame_kill_budget_ms, shared across a bulk retry) before releasing; fail closed (frame stays RUNNING) when death cannot be confirmed. Gated by dispatcher.kill_running_frame_before_release_enabled.
  • New counters: frame_complete_superseded, frame_complete_dropped, frame_retry_deferred, frame_zombie_render.

LLM usage disclosure

Claude Opus was used for investigating the root cause and creating unit tests.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented outdated frame-completion reports from stopping or releasing newer frame runs.
    • Improved handling of dropped, redirected, missing, and orphaned frame reports.
    • Retries now confirm renders have stopped before releasing frames, reducing duplicate or conflicting work.
    • Retries are deferred safely when termination cannot be confirmed within the configured time limit.
  • Monitoring

    • Added metrics for superseded and dropped completions, deferred retries, and zombie renders, improving operational visibility.

Patch another condition that may lead to double-booking on retried frames.

retryFrame released the frame (WAITING, proc link cleared) before killing
RQD, so it was instantly booked onto a second host. FrameCompleteHandler
then applied the killed run's report without checking the reporting proc
still owned the frame, stopping the new run and freeing the frame again —
every kill produced a report and every report freed the frame, sustaining
a kill→report→free→rebook loop across hosts and silently discarding
successful renders.

- FrameCompleteHandler: ownership fence — a report may only stop the frame
  while its proc still holds it; superseded reports divert to the stale
  path. The stale path no longer unbooks a proc re-dispatched the same
  frame, and unapplied reports (notably exit 0) are logged and counted.
- JobManagerSupport.retryFrame: kill and confirm dead via isFrameRunning
  polling (dispatcher.retry_frame_kill_budget_ms, shared across a bulk
  retry) before releasing; fail closed (frame stays RUNNING) when death
  cannot be confirmed. Gated by
  dispatcher.kill_running_frame_before_release_enabled.
- New counters: frame_complete_superseded, frame_complete_dropped,
  frame_retry_deferred, frame_zombie_render.
@DiegoTavares
DiegoTavares marked this pull request as ready for review August 11, 2026 23:29
@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds metrics for stale completions, deferred retries, and zombie renders. Completion handling validates processor ownership. Running-frame retries can confirm render termination before release by using shared configurable deadlines.

Changes

Completion and retry safety

Layer / File(s) Summary
Frame completion ownership fencing
cuebot/src/main/java/com/imageworks/spcue/PrometheusMetricsCollector.java, cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java, cuebot/src/main/java/com/imageworks/spcue/dispatcher/HostReportHandler.java, cuebot/src/test/java/com/imageworks/spcue/test/dispatcher/FrameCompleteHandlerOwnershipTests.java
Completion handlers verify processor ownership before stopping or unbooking frames. Stale, orphaned, dropped, and zombie reports increment Prometheus metrics.
Kill-before-release retry flow
cuebot/src/main/java/com/imageworks/spcue/service/JobManagerSupport.java, cuebot/src/main/resources/opencue.properties, cuebot/src/test/java/com/imageworks/spcue/test/service/JobManagerSupportRetryFrameTests.java
Running-frame retries kill renders and poll RQD until termination or a shared deadline. Failed confirmation defers retries and preserves RUNNING. Disabled kill-before-release retains the legacy order.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RQD
  participant FrameCompleteHandler
  participant FrameDetail
  participant PrometheusMetricsCollector
  RQD->>FrameCompleteHandler: submit frame completion report
  FrameCompleteHandler->>FrameDetail: verify current processor ownership
  alt report is superseded or stale
    FrameCompleteHandler->>PrometheusMetricsCollector: increment dropped completion metric
    FrameCompleteHandler->>FrameDetail: preserve newer frame ownership
  else report is current
    FrameCompleteHandler->>FrameDetail: stop completed frame
  end
Loading
sequenceDiagram
  participant JobManagerSupport
  participant RQD
  participant FrameDetail
  participant PrometheusMetricsCollector
  JobManagerSupport->>RQD: kill running render
  JobManagerSupport->>RQD: poll render status
  alt render exits
    JobManagerSupport->>FrameDetail: release frame
  else deadline or confirmation failure
    JobManagerSupport->>PrometheusMetricsCollector: increment deferred retry metric
    JobManagerSupport->>FrameDetail: leave frame RUNNING
  end
Loading

Possibly related PRs

Suggested reviewers: lithorus, ramonfigueiredo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: adding safeguards to prevent double booking during retried frame execution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
cuebot/src/main/java/com/imageworks/spcue/service/JobManagerSupport.java (1)

441-451: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The shared budget bounds sleeping, not RQD RPC time.

retryFrames shares one deadline across all frames, and killAndConfirmDead guarantees at least one isFrameRunning poll per frame even after the deadline passes. Each poll is a gRPC call to RQD with its own deadline (grpc.rqd_task_deadline=10 seconds). A bulk retry of N running frames therefore performs N kill calls plus at least N confirmation calls on the calling thread, and the wall-clock cost scales with N regardless of dispatcher.retry_frame_kill_budget_ms.

Consider skipping the confirmation poll once the shared deadline is spent and deferring those frames directly, or bounding the number of frames confirmed per request.

♻️ Proposed change: skip the poll when the shared budget is already spent
-        // do/while so the render gets at least one confirmation poll even when the shared budget
-        // was spent on earlier frames.
-        do {
+        if (System.currentTimeMillis() >= deadlineMs) {
+            // The shared budget is spent. Defer rather than spend another RQD round trip per
+            // remaining frame; the kill above is still best-effort delivered.
+            return false;
+        }
+        do {

Also applies to: 582-601

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cuebot/src/main/java/com/imageworks/spcue/service/JobManagerSupport.java`
around lines 441 - 451, Update retryFrame and its killAndConfirmDead path to
check the shared killDeadlineMs before issuing any RQD confirmation poll. When
the deadline has expired after the kill request, skip isFrameRunning
confirmation and defer the frame directly; retain confirmation while budget
remains and preserve existing retryFrames behavior for frames processed within
the budget.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java`:
- Around line 304-317: Update handleFrameCompleteReport to re-read the proc
through hostManager.getVirtualProc for every ownership decision, regardless of
frameDetail.state. After the fresh read, drop the stale report whenever
currentProc.frameId represents any newer assignment, including a different
frame, before redirecting or unbooking; retain the missing-proc early return.

In `@cuebot/src/main/java/com/imageworks/spcue/service/JobManagerSupport.java`:
- Around line 76-77: Add annotation processing for JobManagerSupport by enabling
context:annotation-config in applicationContext-service.xml, or explicitly wire
the Environment dependency through XML. Ensure the `@Autowired` env field in
JobManagerSupport is populated so configuration properties are applied.

---

Nitpick comments:
In `@cuebot/src/main/java/com/imageworks/spcue/service/JobManagerSupport.java`:
- Around line 441-451: Update retryFrame and its killAndConfirmDead path to
check the shared killDeadlineMs before issuing any RQD confirmation poll. When
the deadline has expired after the kill request, skip isFrameRunning
confirmation and defer the frame directly; retain confirmation while budget
remains and preserve existing retryFrames behavior for frames processed within
the budget.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 78a85e4c-ec12-4e67-a79a-5afa53ec283b

📥 Commits

Reviewing files that changed from the base of the PR and between 1f04fce and 3087348.

📒 Files selected for processing (7)
  • cuebot/src/main/java/com/imageworks/spcue/PrometheusMetricsCollector.java
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/HostReportHandler.java
  • cuebot/src/main/java/com/imageworks/spcue/service/JobManagerSupport.java
  • cuebot/src/main/resources/opencue.properties
  • cuebot/src/test/java/com/imageworks/spcue/test/dispatcher/FrameCompleteHandlerOwnershipTests.java
  • cuebot/src/test/java/com/imageworks/spcue/test/service/JobManagerSupportRetryFrameTests.java

Comment thread cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java Outdated
Both release paths end in deleteVirtualProc(pk_proc), which has no fence on the proc's current frame
assignment, but the fresh proc re-read only ran when the reported frame was RUNNING — and even then
only dropped the report when the fresh read showed the same frame. A proc reassigned to a different
frame between the entry snapshot and the release (e.g. duplicate report handled while the dispatcher
rebooks the proc) would fall through to redirect/unbook and delete the row under the live run,
orphaning it. The fix at FrameCompleteHandler.java:305 makes the re-read unconditional before any
redirect/unbook and drops the report whenever the fresh read shows any frame assigned (newer run of
the same frame or a different frame); the missing-proc early return is retained, and only a proc
confirmed idle is released. Updated the method javadoc, adjusted staleReportReleasesIdleProc to
model the cleared assignment on the fresh read, and added a regression test
staleReportDoesNotReleaseProcReassignedToAnotherFrame.
Signed-off-by: Diego Tavares <dtavares@imageworks.com>

@ramonfigueiredo ramonfigueiredo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with some suggested changes

Comment thread cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@cuebot/src/test/java/com/imageworks/spcue/test/service/JobManagerSupportRetryFrameTests.java`:
- Around line 108-114: Update the test fixture setup in
JobManagerSupportRetryFrameTests to inject a PrometheusMetricsCollector mock
into the service under test. In the deferred-retry tests, including
exhausted-budget and unreachable-host scenarios, verify that
incrementFrameRetryDeferred() is called exactly once when kill confirmation
fails.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c0e0cf6-dd58-408a-9c0a-22c0e0c438b7

📥 Commits

Reviewing files that changed from the base of the PR and between 9673585 and 0920848.

📒 Files selected for processing (4)
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java
  • cuebot/src/main/java/com/imageworks/spcue/service/JobManagerSupport.java
  • cuebot/src/main/resources/opencue.properties
  • cuebot/src/test/java/com/imageworks/spcue/test/service/JobManagerSupportRetryFrameTests.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java
  • cuebot/src/main/resources/opencue.properties
  • cuebot/src/main/java/com/imageworks/spcue/service/JobManagerSupport.java

Comment on lines +108 to +114
// Kill-before-release enabled by default, with a full confirmation budget. Tests whose
// render is confirmed gone on the first poll never sleep; tests that need the budget to be
// spent set it explicitly.
when(env.getProperty(eq(KILL_BEFORE_RELEASE_PROPERTY), eq(Boolean.class), eq(true)))
.thenReturn(true);
when(env.getProperty(eq(RETRY_KILL_BUDGET_PROPERTY), eq(Long.class), eq(10000L)))
.thenReturn(10000L);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test the frame_retry_deferred metric.

The fixture does not inject PrometheusMetricsCollector. The deferred-retry tests therefore cannot detect a missing incrementFrameRetryDeferred() call or missing collector wiring.

Inject a collector mock. Assert one increment when kill confirmation fails, including the exhausted-budget and unreachable-host paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@cuebot/src/test/java/com/imageworks/spcue/test/service/JobManagerSupportRetryFrameTests.java`
around lines 108 - 114, Update the test fixture setup in
JobManagerSupportRetryFrameTests to inject a PrometheusMetricsCollector mock
into the service under test. In the deferred-retry tests, including
exhausted-budget and unreachable-host scenarios, verify that
incrementFrameRetryDeferred() is called exactly once when kill confirmation
fails.

@DiegoTavares
DiegoTavares merged commit d9f5ba2 into AcademySoftwareFoundation:master Aug 12, 2026
24 of 25 checks passed
DiegoTavares added a commit that referenced this pull request Aug 12, 2026
Follow-up to #2506: a restarted RQD answers kill-before-release with
NOT_FOUND, which was wrapped as a failure, so lostProc deferred provably
dead frames indefinitely and re-logged the "Deferring release of lost
proc" WARN every 30s.

- RqdClientGrpc.killFrame now treats NOT_FOUND as confirmed-stopped
(matching isFrameRunning); only unknown outcomes throw
RqdClientException.
- Bound the deferral: after dispatcher.lost_proc_max_defer_ms (default
20 min) without a ping proving the frame alive, release the proc and
fail the frame closed to DEAD (manual retry) instead of deferring again.
Negative disables the bound.
- Boot-time fence: a host whose reported boot time is later than the
proc's dispatch (plus a clock-skew margin) cannot be running the frame,
so release it to WAITING even when the kill cannot be delivered.
- Orphaned-proc reaper: order candidates oldest-ping first
(deterministic batch turnover) and make the batch size configurable
(maintenance.orphaned_proc_batch_size, default 100) so unconfirmable
procs cannot starve releasable orphans.

New unit tests cover the fail-closed bound, the boot fence, and
killFrame status classification against a real localhost gRPC server;
DAO tests cover the new queries.

## LLM usage disclosure
Claude Opus was used for investigating the logs and implementing this
fix.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved handling of lost processes by detecting stale pings and host
reboots.
- Automatically marks frames as **DEAD** when lost-process recovery
cannot be confirmed within the configured time limit.
- Treats already-missing frames as successfully stopped during kill
operations.
  - Orphaned-process cleanup now processes oldest entries first.

- **Configuration**
- Added settings for lost-process deferral limits and orphaned-process
cleanup batch size.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
DiegoTavares added a commit that referenced this pull request Aug 26, 2026
Close the remaining free-then-rebook path that survived #2506/#2508: the
generic launch rollback released the booking first (proc deleted, frame
back to WAITING) and fired a best-effort kill whose failure was
swallowed. When a launchFrame RPC fails on the Cuebot side after the
request was delivered (deadline expired waiting for the response,
connection dropped mid-call), the render is alive on the host, the kill
fails on the same broken network, and the freed frame is instantly
re-booked by another Cuebot -- each failed launch becomes a
double-booking under RPC instability.

- RqdClientGrpc.launchFrame classifies failures: transport-level codes
(DEADLINE_EXCEEDED, UNAVAILABLE, CANCELLED, INTERNAL, DATA_LOSS) throw
the new RqdLaunchUnknownOutcomeException; application-level responses
(RQD rejects with ABORTED/FAILED_PRECONDITION/INVALID_ARGUMENT) prove
the frame never started and keep the plain RqdClientException.
- Both copies of the release-first rollback route unknown-outcome launch
failures through DispatchSupport.resolveUnknownLaunchOutcome instead:
CoreUnitDispatcher's DispatchFrameTemplate (the production dispatch
path, used by dispatchHost and dispatchProcToJob) and
AbstractDispatcher.dispatchProc/dispatchHost (the LocalDispatcher and
RedirectManager paths). The legacy rollback is preserved for proven
rejections.
- resolveUnknownLaunchOutcome confirms before releasing anything: a
frame confirmed running keeps its booking and finishes through its own
frame complete report; a frame confirmed not running (two consecutive
isFrameRunning polls within dispatcher.launch_confirm_budget_ms) is
released for re-dispatch; anything unconfirmable keeps the booking (fail
closed) and is reclaimed by the orphaned-proc reaper's own
kill-and-confirm path. A non-positive budget restores the legacy
behavior.
- New counter cue_frame_launch_outcome_unknown_total{resolution} plus
DispatchSupport.unknownLaunchOutcomes.

Unit tests cover the status classification against a real localhost gRPC
server, the resolver's four resolutions, and the rollback routing in
both dispatcher rollback sites.


## LLM usage disclosure

Claude Opus was used for implementing the fix and unit tests.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Prevented frames from being double-booked when launch results are
uncertain.
* Verified frame status before releasing bookings after uncertain launch
failures.
  * Preserved rollback behavior for confirmed launch failures.
* Prevented rollback from clearing frames belonging to newer dispatches.

* **New Features**
* Added configurable launch-outcome confirmation with a 20-second
default window and 7-second polling interval.
  * Added Prometheus metrics for launches with unknown outcomes.
  * Improved monitoring and handling of uncertain launch failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
DiegoTavares added a commit to akheffache/OpenCue that referenced this pull request Aug 28, 2026
Brings in AcademySoftwareFoundation#2473, AcademySoftwareFoundation#2497, AcademySoftwareFoundation#2501, AcademySoftwareFoundation#2502, AcademySoftwareFoundation#2504, AcademySoftwareFoundation#2506, AcademySoftwareFoundation#2508, AcademySoftwareFoundation#2510, AcademySoftwareFoundation#2511 and
the cueweb dependency bumps.

Conflict resolutions:

- applicationContext-service.xml, FrameDao: keep both sides.
- DispatchQuery: keep the branch's layer.pk_layer fix plus master's
  ts_start_after predicate.
- RqdClientGrpc: master's split catch and unknown-outcome throw, keeping the
  branch's warn log of the underlying launch failure on both paths.
- FrameCompleteHandler: master refactored the class into small methods, so the
  scheduler hooks were re-applied on top of the new structure rather than
  merged line by line. The drain path (resolveForDrain) now also runs master's
  orphan-finalize and run-ownership fences, and handleStaleCompletion delegates
  to handleStaleReport so scheduler-owned shows get the same duplicate check
  and re-read guard as legacy ones; its Scheduler call site passes the report.
  The bookingOff guards became isBookingOff() calls inside the extracted
  booking methods, and the per-frame OOM bump moved into
  retryFrameWithRaisedMemory.

FrameCompleteHandlerOwnershipTests builds the handler from a bare Environment
mock; the constructor now unboxes numeric properties, so the mock returns the
supplied defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114CS5RrhDjTJse5faC4Pcb
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.

2 participants