Skip to content

Commit 752a1f6

Browse files
Merge branch 'master' into dev-days/2301-show-dialog
2 parents ac224c5 + 57ab286 commit 752a1f6

18 files changed

Lines changed: 946 additions & 533 deletions

File tree

cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,17 @@ else if (report.getHost().getNimbyLocked()) {
529529
if (newFrameState.equals(FrameState.WAITING)
530530
|| newFrameState.equals(FrameState.SUCCEEDED)) {
531531

532+
/*
533+
* Scheduler-managed shows: the standalone Rust scheduler owns dispatch. Don't
534+
* reuse/rebook the proc here (that races the scheduler and strands pk_frame=NULL
535+
* procs with reserved cores). Release it so its cores return to idle and let the
536+
* scheduler own rebooking. Local dispatches are always Cuebot-managed.
537+
*/
538+
if (!proc.isLocalDispatch && showDao.isSchedulerManaged(proc.getShowId())) {
539+
dispatchSupport.unbookProc(proc, "scheduler-managed show, releasing proc");
540+
return;
541+
}
542+
532543
/*
533544
* Check for stranded cores on the host.
534545
*/

cuebot/src/test/java/com/imageworks/spcue/test/dispatcher/FrameCompleteHandlerTests.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.imageworks.spcue.VirtualProc;
3838
import com.imageworks.spcue.dao.FrameDao;
3939
import com.imageworks.spcue.dao.LayerDao;
40+
import com.imageworks.spcue.dao.ShowDao;
4041
import com.imageworks.spcue.dispatcher.Dispatcher;
4142
import com.imageworks.spcue.dispatcher.DispatchSupport;
4243
import com.imageworks.spcue.dispatcher.FrameCompleteHandler;
@@ -56,10 +57,17 @@
5657
import static org.junit.Assert.assertEquals;
5758
import static org.junit.Assert.assertFalse;
5859
import static org.junit.Assert.assertTrue;
60+
import static org.mockito.AdditionalAnswers.delegatesTo;
5961
import static org.mockito.ArgumentMatchers.any;
62+
import static org.mockito.ArgumentMatchers.eq;
6063
import static org.mockito.Mockito.doAnswer;
64+
import static org.mockito.Mockito.doReturn;
6165
import static org.mockito.Mockito.doThrow;
66+
import static org.mockito.Mockito.mock;
67+
import static org.mockito.Mockito.never;
6268
import static org.mockito.Mockito.spy;
69+
import static org.mockito.Mockito.times;
70+
import static org.mockito.Mockito.verify;
6371

6472
import org.springframework.transaction.CannotCreateTransactionException;
6573

@@ -534,4 +542,104 @@ public void testDependRetryExhausted() {
534542
// All three calls fail — depend remains unsatisfied, frame stays in DEPEND
535543
executeDependWithRetry(3, 1, FrameState.DEPEND);
536544
}
545+
546+
/**
547+
* Drives a frame completion through the proc-reuse branch (the job still has a WAITING frame,
548+
* so it stays dispatchable) and asserts whether the scheduler-managed release path fired. When
549+
* the show is scheduler-managed, the standalone scheduler owns dispatch, so Cuebot must release
550+
* (unbook) the proc here instead of rebooking it on the same proc — otherwise the proc lingers
551+
* with pk_frame=NULL holding reserved cores. The release is identified by the unique reason
552+
* string passed to unbookProc, which makes this independent of the nondeterministic downstream
553+
* dispatch.
554+
*
555+
* <p>
556+
* isSchedulerManaged is stubbed on a spy rather than written to the DB on purpose: the real
557+
* flag lives in an in-memory cache on ShowDaoJdbc that is NOT rolled back with the transaction,
558+
* so a real write would leak scheduler-managed behavior into other tests sharing the Spring
559+
* context.
560+
*/
561+
private void executeProcReuseGate(boolean schedulerManaged, FrameState terminalState) {
562+
JobDetail job = jobManager.findJobDetail("pipe-default-testuser_test_depend");
563+
jobManager.setJobPaused(job, false);
564+
565+
DispatchHost host = getHost(HOSTNAME);
566+
List<VirtualProc> procs = dispatcher.dispatchHost(host);
567+
assertEquals(1, procs.size());
568+
VirtualProc proc = procs.get(0);
569+
570+
RunningFrameInfo info = RunningFrameInfo.newBuilder().setJobId(proc.getJobId())
571+
.setLayerId(proc.getLayerId()).setFrameId(proc.getFrameId())
572+
.setResourceId(proc.getProcId()).build();
573+
// The report must carry a healthy host (UP, plenty of free memory), otherwise the handler
574+
// unbooks the proc early (e.g. the < 512MB low-memory check) and returns before reaching
575+
// the
576+
// WAITING/SUCCEEDED proc-reuse branch that the scheduler-managed gate lives in.
577+
RenderHost reportHost =
578+
RenderHost.newBuilder().setName(HOSTNAME).setFreeMem((int) CueUtil.GB8)
579+
.setNimbyEnabled(false).setState(HardwareState.UP).build();
580+
FrameCompleteReport report = FrameCompleteReport.newBuilder().setFrame(info)
581+
.setHost(reportHost).setExitStatus(0).build();
582+
583+
DispatchJob dispatchJob = jobManager.getDispatchJob(proc.getJobId());
584+
DispatchFrame dispatchFrame = jobManager.getDispatchFrame(report.getFrame().getFrameId());
585+
FrameDetail frameDetail = jobManager.getFrameDetail(report.getFrame().getFrameId());
586+
dispatchSupport.stopFrame(dispatchFrame, terminalState, report.getExitStatus(),
587+
report.getFrame().getMaxRss());
588+
589+
// The DAO beans are Spring JDK dynamic proxies (final), so they can't be spied. Wrap them
590+
// in interface mocks that delegate to the real bean for everything except the one method we
591+
// stub, while still recording invocations for verification.
592+
ShowDao originalShowDao = frameCompleteHandler.getShowDao();
593+
DispatchSupport originalDispatchSupport = frameCompleteHandler.getDispatchSupport();
594+
ShowDao mockShowDao = mock(ShowDao.class, delegatesTo(originalShowDao));
595+
DispatchSupport mockDispatchSupport =
596+
mock(DispatchSupport.class, delegatesTo(originalDispatchSupport));
597+
doReturn(schedulerManaged).when(mockShowDao).isSchedulerManaged(proc.getShowId());
598+
599+
frameCompleteHandler.setShowDao(mockShowDao);
600+
frameCompleteHandler.setDispatchSupport(mockDispatchSupport);
601+
try {
602+
frameCompleteHandler.handlePostFrameCompleteOperations(proc, report, dispatchJob,
603+
dispatchFrame, terminalState, frameDetail);
604+
} finally {
605+
frameCompleteHandler.setShowDao(originalShowDao);
606+
frameCompleteHandler.setDispatchSupport(originalDispatchSupport);
607+
}
608+
609+
verify(mockDispatchSupport, schedulerManaged ? times(1) : never())
610+
.unbookProc(any(VirtualProc.class), eq("scheduler-managed show, releasing proc"));
611+
}
612+
613+
@Test
614+
@Transactional
615+
@Rollback(true)
616+
public void testSchedulerManagedShowReleasesProc() {
617+
// Scheduler-managed, SUCCEEDED: the proc must be unbooked (released), not reused.
618+
executeProcReuseGate(true, FrameState.SUCCEEDED);
619+
}
620+
621+
@Test
622+
@Transactional
623+
@Rollback(true)
624+
public void testNonSchedulerManagedShowReusesProc() {
625+
// Control, SUCCEEDED: the scheduler-managed release branch must not fire.
626+
executeProcReuseGate(false, FrameState.SUCCEEDED);
627+
}
628+
629+
@Test
630+
@Transactional
631+
@Rollback(true)
632+
public void testSchedulerManagedShowReleasesProcWaiting() {
633+
// Scheduler-managed, WAITING: the other side of the WAITING/SUCCEEDED gate must also
634+
// release the proc.
635+
executeProcReuseGate(true, FrameState.WAITING);
636+
}
637+
638+
@Test
639+
@Transactional
640+
@Rollback(true)
641+
public void testNonSchedulerManagedShowReusesProcWaiting() {
642+
// Control, WAITING: the scheduler-managed release branch must not fire.
643+
executeProcReuseGate(false, FrameState.WAITING);
644+
}
537645
}

docs/_docs/developer-guide/scheduler.md

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -764,37 +764,44 @@ scheduler_host_cache_hits_total
764764

765765
### Cluster Configuration
766766

767-
**Allocation Tags**:
767+
**Show selection** is driven solely by the `show.b_scheduler_managed` boolean DB column
768+
(toggled with `cueadmin -scheduler-managed <show> on|off`). The scheduler automatically
769+
loads *all* clusters — allocation, manual-tag, hostname-tag, and hardware host-tag — for
770+
every show where `b_scheduler_managed = true`. There is no per-show, per-allocation, or
771+
per-tag selection in the config; a show is wholly scheduler-managed or wholly
772+
Cuebot-managed.
773+
774+
**Facility**:
768775
```yaml
769776
scheduler:
770-
alloc_tags:
771-
- show: myshow
772-
tag: general
777+
facility: spi
773778
```
774779

775-
Loads one cluster per entry: `(facility_id, show_id, "general")`
780+
Optionally scopes the instance to one facility's scheduler-managed clusters.
776781

777-
**Manual Tags**:
782+
**Ignore Tags**:
778783
```yaml
779784
scheduler:
780-
manual_tags:
781-
- urgent
782-
- desktop
783-
- workstation
785+
ignore_tags:
786+
- deprecated_tag
784787
```
785788

786-
Chunks into groups (default: 100 tags per cluster):
787-
- Cluster 1: `(facility_id, [urgent, desktop, workstation])`
788-
- If more than 100 tags, splits into multiple clusters
789+
Filters out specified tags from all loaded clusters before processing.
789790

790-
**Ignore Tags**:
791+
**Cluster Reload Interval**:
791792
```yaml
792-
scheduler:
793-
ignore_tags:
794-
- deprecated_tag
793+
queue:
794+
cluster_reload_interval: 120s
795795
```
796796

797-
Filters out specified tags from all cluster types before processing.
797+
Interval between full reloads of the cluster set from the DB. The scheduler periodically
798+
rebuilds the set of clusters from all currently-managed shows and swaps the live set only
799+
when it actually changed, so flipping `b_scheduler_managed` (and host-tag / subscription
800+
changes) is picked up without a restart. Default: 120s.
801+
802+
The manual-tag and hostname-tag chunk sizes (`queue.manual_tags_chunk_size`,
803+
`queue.hostname_tags_chunk_size`) control how DB-loaded host-tags are grouped into
804+
clusters (see [Cluster System](#1-cluster-system)).
798805

799806
### Performance Tuning
800807

@@ -904,24 +911,28 @@ The matching Cuebot side requires `accounting.redis.enabled=true` and the same
904911

905912
### Current Architecture (v1.0)
906913

907-
The scheduler supports distributed operation via manual cluster assignment:
914+
The scheduler supports distributed operation by scoping each instance to a facility with
915+
`--facility`. Show ownership is selected by the per-show `b_scheduler_managed` flag, and
916+
each instance automatically loads all clusters (allocation, manual, hostname, and
917+
hardware host-tags) for every scheduler-managed show in its facility:
908918

909919
**Instance 1**:
910920
```bash
911-
cue-scheduler --alloc_tags=show1:general,show1:priority
921+
cue-scheduler --facility spi
912922
```
913923

914924
**Instance 2**:
915925
```bash
916-
cue-scheduler --alloc_tags=show2:general,show2:priority
926+
cue-scheduler --facility lax
917927
```
918928

919-
**Instance 3**:
920-
```bash
921-
cue-scheduler --manual_tags=urgent,desktop
922-
```
929+
There is no hand-listing of tags or clusters. To move work onto (or off) the scheduler,
930+
toggle the show with `cueadmin -scheduler-managed <show> on|off`; the change is picked up
931+
on the next cluster reload (`queue.cluster_reload_interval`, default 120s) with no
932+
restart.
923933

924-
**Critical**: Ensure no cluster overlap between instances to prevent race conditions.
934+
**Critical**: When running multiple instances, scope them to non-overlapping facilities
935+
to prevent two instances from owning the same clusters.
925936

926937
### Coordination Mechanisms
927938

0 commit comments

Comments
 (0)