Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7b9edc4
[scheduler/cuebot] Replace Redis-backed accounting with in-memory sto…
DiegoTavares Jun 26, 2026
ca91041
Version up and minor refactor
DiegoTavares Jun 29, 2026
f4b63f2
Remove unecessary migration and version change
DiegoTavares Jun 29, 2026
bfdd419
Merge branch 'master' into rip_redis
DiegoTavares Jul 9, 2026
66744df
[scheduler] Correct accounting docs/comments + add recompute cycle me…
DiegoTavares Jul 15, 2026
955a359
Apply review suggestions and improve docs
DiegoTavares Jul 15, 2026
615bfdd
Address review comments
DiegoTavares Jul 15, 2026
d1c685e
Changing migrations is forbidden
DiegoTavares Jul 15, 2026
3c18717
Spotless apply
DiegoTavares Jul 15, 2026
78b1389
Optimize CICD
DiegoTavares Jul 16, 2026
05995aa
[scheduler/cuebot] Add slot-based scheduling DB columns (V46-V49)
DiegoTavares Jul 3, 2026
7045efc
[scheduler] Per-host slot cap for slot-based scheduling
DiegoTavares Jul 3, 2026
03425e4
[rqd/scheduler] Run slot-based frames unpinned
DiegoTavares Jul 3, 2026
fbcebed
[scheduler] Slot accounting axis in the in-memory Store
DiegoTavares Jul 3, 2026
4c6f626
[cuebot] Publish slot deltas to the scheduler accounting store
DiegoTavares Jul 3, 2026
6c4fb93
[proto/cuebot/pycue/cuegui/pyoutline] Slot-based scheduling managemen…
DiegoTavares Jul 3, 2026
67d1f1c
[scheduler/docs] Slot gate tests + slot-based-booking concept doc
DiegoTavares Jul 3, 2026
66c5503
Update docs/_docs/concepts/slot-based-booking.md
DiegoTavares Jul 14, 2026
5188f45
Fix review findings
DiegoTavares Jul 14, 2026
65861d3
Merge branch 'master' into slot_rework
DiegoTavares Jul 24, 2026
995199d
[cuebot] Implement slot based dispatching on Cuebot
DiegoTavares Aug 11, 2026
dfc679a
Merge remote-tracking branch 'origin/rip_redis' into slot_rework
DiegoTavares Aug 11, 2026
218abaf
Merge remote-tracking branch 'public/master' into slot_rework
DiegoTavares Aug 11, 2026
c05c733
[cuebot] Consolidate slot-booking migrations into a single V47
DiegoTavares Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION.in
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.29
1.30
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public class DispatchFrame extends FrameEntity implements FrameInterface {
public int maxGpus;
public long minGpuMemory;

// Concurrency slots each frame of this layer requires (slot-based scheduling).
// 0 = not slot-based. Only populated by the slot dispatch queries; the generic
// dispatch queries never return slot-based frames.
public int slotsRequired;

// A comma separated list of services
public String services;

Expand Down
21 changes: 21 additions & 0 deletions cuebot/src/main/java/com/imageworks/spcue/DispatchHost.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ public class DispatchHost extends Entity
// Basically an 0 = auto, 1 = all.
public int threadMode;

/**
* Max concurrent frames (slots) this host may run. -1 marks a regular (cores/memory) host; >= 0
* marks a slot-based host that only runs slot-based layers, capped at this many concurrent
* slots.
*/
public int concurrentSlotsLimit = -1;

/**
* Number of slots not currently reserved on this host (concurrentSlotsLimit minus
* SUM(proc.int_slots_reserved)). Only meaningful for slot-based hosts; -1 otherwise.
*/
public int idleSlots = -1;

public long memory;
public long idleMemory;
public long gpuMemory;
Expand All @@ -69,6 +82,14 @@ public String getHostId() {
return id;
}

/**
* True when this host is slot-based: it only runs slot-based layers, capped by
* concurrentSlotsLimit, and ignores cores/memory for booking decisions.
*/
public boolean isSlotHost() {
return concurrentSlotsLimit >= 0;
}

public String getAllocationId() {
return allocationId;
}
Expand Down
2 changes: 2 additions & 0 deletions cuebot/src/main/java/com/imageworks/spcue/LayerDetail.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public class LayerDetail extends LayerEntity implements LayerInterface {
public int timeout_llu;
public int dispatchOrder;
public int totalFrameCount;
// Concurrency slots each frame requires (slot-based scheduling). 0 = not slot-based.
public int slotsRequired;

public Set<String> tags = new LinkedHashSet<String>();
public Set<String> services = new LinkedHashSet<String>();
Expand Down
42 changes: 42 additions & 0 deletions cuebot/src/main/java/com/imageworks/spcue/VirtualProc.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public class VirtualProc extends FrameEntity implements ProcInterface {
public int gpusReserved;
public long gpuMemoryReserved;
public long gpuMemoryUsed;

// Concurrency slots reserved by this proc (slot-based scheduling). 0 for regular procs.
public int slotsReserved;
public long gpuMemoryMax;

public boolean unbooked;
Expand Down Expand Up @@ -207,6 +210,45 @@ && containsSelfishService(frame.services.split(","), selfishServices)) {
return proc;
}

/**
* Build a proc for a slot-based booking. Slot procs reserve 0 cores, 0 memory and 0 gpus - the
* only resource they consume is the frame's slot requirement, which counts against the host's
* concurrent slots limit and the subscription/folder/job max_slots caps.
*
* @param host a slot-based host (isSlotHost() == true)
* @param frame a slot-based frame (slotsRequired > 0)
* @return
*/
public static final VirtualProc buildSlotProc(DispatchHost host, DispatchFrame frame) {
if (frame.slotsRequired <= 0) {
throw new EntityException(
"Cannot build a slot proc for a frame that requires no slots.");
}

VirtualProc proc = new VirtualProc();
proc.allocationId = host.getAllocationId();
proc.hostId = host.getHostId();
proc.frameId = null;
proc.layerId = frame.getLayerId();
proc.jobId = frame.getJobId();
proc.showId = frame.getShowId();
proc.facilityId = frame.getFacilityId();
proc.os = frame.os;

proc.hostName = host.getName();
proc.unbooked = false;
proc.isLocalDispatch = false;
proc.canHandleNegativeCoresRequest = false;

proc.coresReserved = 0;
proc.memoryReserved = 0;
proc.gpusReserved = 0;
proc.gpuMemoryReserved = 0;
proc.slotsReserved = frame.slotsRequired;

return proc;
}

private static final boolean containsSelfishService(String[] frameServices,
String[] selfishServices) {
for (String frameService : frameServices) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,40 @@ public interface DispatcherDao {
*/
List<DispatchFrame> findNextDispatchFrames(LayerInterface layer, DispatchHost host, int limit);

/**
* Return a list of jobs with pending slot-based work that could book on the specified
* slot-based host. Jobs over their job/folder/subscription max_slots caps are excluded.
*
* @param host a slot-based host (concurrentSlotsLimit >= 0)
* @param numJobs
* @return a set of unique job ids ordered by priority
*/
Set<String> findSlotDispatchJobs(DispatchHost host, int numJobs);

/**
* Return the next slot-based frames in the given job bookable on the specified slot-based host.
* Only frames of slot-based layers whose slot requirement fits the host's idle slots and whose
* job/folder/subscription max_slots caps allow another booking are returned.
*
* @param job
* @param host a slot-based host (concurrentSlotsLimit >= 0)
* @param limit
* @return
*/
List<DispatchFrame> findNextSlotDispatchFrames(JobInterface job, DispatchHost host, int limit);

/**
* Return how many more slots the given job may book on the given host's allocation: the
* smallest remaining allowance across the job, folder and subscription max_slots caps.
* Integer.MAX_VALUE when all three caps are unlimited (-1); 0 when the job's show has no
* subscription to the host's allocation.
*
* @param job
* @param host
* @return
*/
int getSlotCapacityRemaining(JobInterface job, DispatchHost host);

/**
* Return Scheduling Mode selected
*
Expand Down
9 changes: 9 additions & 0 deletions cuebot/src/main/java/com/imageworks/spcue/dao/GroupDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ public interface GroupDao {
*/
public void updateMaxCores(GroupInterface group, int value);

/**
* Sets the max concurrent slots for slot-based layers in the group's folder. -1 unlimited, 0
* reject-all, N caps at N.
*
* @param group
* @param value
*/
public void updateMaxSlots(GroupInterface group, int value);

/**
* Set the minimum number of cores for this group
*
Expand Down
9 changes: 9 additions & 0 deletions cuebot/src/main/java/com/imageworks/spcue/dao/HostDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,15 @@ public interface HostDao {
*/
void updateThreadMode(HostInterface host, ThreadMode mode);

/**
* Sets the host's concurrent slots limit. -1 disables slot mode (regular host); >= 0 makes the
* host slot-based, running only slot layers up to this many concurrent slots.
*
* @param host HostInterface
* @param limit int
*/
void updateConcurrentSlotsLimit(HostInterface host, int limit);

/**
* Update the specified host's hardware information.
*
Expand Down
16 changes: 16 additions & 0 deletions cuebot/src/main/java/com/imageworks/spcue/dao/JobDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ public interface JobDao {
*/
public void updateMaxCores(GroupInterface g, int cores);

/**
* Updates all jobs in the specified group to the max slots value.
*
* @param g
* @param slots
*/
public void updateMaxSlots(GroupInterface g, int slots);

/**
* Updates all jobs in the specifid group to the min cores value.
*
Expand Down Expand Up @@ -306,6 +314,14 @@ public interface JobDao {
*/
void updateMaxCores(JobInterface j, int v);

/**
* Sets the job's max concurrent slots for slot-based layers. -1 unlimited, 0 reject-all.
*
* @param j
* @param v
*/
void updateMaxSlots(JobInterface j, int v);

/**
* sets the jobs new min gpu value
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,13 @@ public interface SubscriptionDao {
* @param size int
*/
void updateSubscriptionBurst(SubscriptionInterface sub, int size);

/**
* update the subscription max concurrent slots for slot-based layers (-1 unlimited, 0
* reject-all, N cap)
*
* @param sub SubscriptionInterface
* @param maxSlots int
*/
void updateSubscriptionMaxSlots(SubscriptionInterface sub, int maxSlots);
}
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,13 @@ public List<DispatchFrame> findNextDispatchFrames(JobInterface job, VirtualProc
long lastTime = System.currentTimeMillis();
List<DispatchFrame> frames;
if (proc.isLocalDispatch) {
frames = getJdbcTemplate().query(FIND_LOCAL_DISPATCH_FRAME_BY_JOB_AND_PROC,
frames = getJdbcTemplate().query(
SlotDispatchQuery.FIND_LOCAL_DISPATCH_FRAME_BY_JOB_AND_PROC_EXCLUDE_SLOT,
FrameDaoJdbc.DISPATCH_FRAME_MAPPER, proc.memoryReserved, proc.gpuMemoryReserved,
job.getJobId(), limit);
} else {
frames = getJdbcTemplate().query(FIND_DISPATCH_FRAME_BY_JOB_AND_PROC,
frames = getJdbcTemplate().query(
SlotDispatchQuery.FIND_DISPATCH_FRAME_BY_JOB_AND_PROC_EXCLUDE_SLOT,
FrameDaoJdbc.DISPATCH_FRAME_MAPPER, proc.coresReserved, proc.memoryReserved,
proc.gpusReserved, (proc.gpuMemoryReserved > 0) ? 1 : 0, proc.gpuMemoryReserved,
job.getJobId(), proc.hostName, job.getJobId(), limit);
Expand All @@ -386,12 +388,14 @@ public List<DispatchFrame> findNextDispatchFrames(JobInterface job, DispatchHost
List<DispatchFrame> frames;

if (host.isLocalDispatch) {
frames = getJdbcTemplate().query(FIND_LOCAL_DISPATCH_FRAME_BY_JOB_AND_HOST,
frames = getJdbcTemplate().query(
SlotDispatchQuery.FIND_LOCAL_DISPATCH_FRAME_BY_JOB_AND_HOST_EXCLUDE_SLOT,
FrameDaoJdbc.DISPATCH_FRAME_MAPPER, host.idleMemory, host.idleGpuMemory,
job.getJobId(), limit);

} else {
frames = getJdbcTemplate().query(FIND_DISPATCH_FRAME_BY_JOB_AND_HOST,
frames = getJdbcTemplate().query(
SlotDispatchQuery.FIND_DISPATCH_FRAME_BY_JOB_AND_HOST_EXCLUDE_SLOT,
FrameDaoJdbc.DISPATCH_FRAME_MAPPER, host.idleCores, host.idleMemory,
threadMode(host.threadMode), host.idleGpus, (host.idleGpuMemory > 0) ? 1 : 0,
host.idleGpuMemory, job.getJobId(), host.getName(), job.getJobId(), limit);
Expand All @@ -409,11 +413,13 @@ public List<DispatchFrame> findNextDispatchFrames(LayerInterface layer, VirtualP
List<DispatchFrame> frames;

if (proc.isLocalDispatch) {
frames = getJdbcTemplate().query(FIND_LOCAL_DISPATCH_FRAME_BY_LAYER_AND_PROC,
frames = getJdbcTemplate().query(
SlotDispatchQuery.FIND_LOCAL_DISPATCH_FRAME_BY_LAYER_AND_PROC_EXCLUDE_SLOT,
FrameDaoJdbc.DISPATCH_FRAME_MAPPER, proc.memoryReserved, proc.gpuMemoryReserved,
layer.getLayerId(), limit);
} else {
frames = getJdbcTemplate().query(FIND_DISPATCH_FRAME_BY_LAYER_AND_PROC,
frames = getJdbcTemplate().query(
SlotDispatchQuery.FIND_DISPATCH_FRAME_BY_LAYER_AND_PROC_EXCLUDE_SLOT,
FrameDaoJdbc.DISPATCH_FRAME_MAPPER, proc.coresReserved, proc.memoryReserved,
proc.gpusReserved, proc.gpuMemoryReserved, layer.getLayerId(),
layer.getLayerId(), proc.hostName, limit);
Expand All @@ -432,12 +438,14 @@ public List<DispatchFrame> findNextDispatchFrames(LayerInterface layer, Dispatch
List<DispatchFrame> frames;

if (host.isLocalDispatch) {
frames = getJdbcTemplate().query(FIND_LOCAL_DISPATCH_FRAME_BY_LAYER_AND_HOST,
frames = getJdbcTemplate().query(
SlotDispatchQuery.FIND_LOCAL_DISPATCH_FRAME_BY_LAYER_AND_HOST_EXCLUDE_SLOT,
FrameDaoJdbc.DISPATCH_FRAME_MAPPER, host.idleMemory, host.idleGpuMemory,
layer.getLayerId(), limit);

} else {
frames = getJdbcTemplate().query(FIND_DISPATCH_FRAME_BY_LAYER_AND_HOST,
frames = getJdbcTemplate().query(
SlotDispatchQuery.FIND_DISPATCH_FRAME_BY_LAYER_AND_HOST_EXCLUDE_SLOT,
FrameDaoJdbc.DISPATCH_FRAME_MAPPER, host.idleCores, host.idleMemory,
threadMode(host.threadMode), host.idleGpus, host.idleGpuMemory,
layer.getLayerId(), layer.getLayerId(), host.getName(), limit);
Expand All @@ -449,6 +457,77 @@ public List<DispatchFrame> findNextDispatchFrames(LayerInterface layer, Dispatch
return frames;
}

/**
* Maps rows of the slot dispatch frame queries; delegates to the generic mapper and adds the
* slot requirement column.
*/
public static final RowMapper<DispatchFrame> SLOT_DISPATCH_FRAME_MAPPER =
new RowMapper<DispatchFrame>() {
public DispatchFrame mapRow(ResultSet rs, int rowNum) throws SQLException {
DispatchFrame frame = FrameDaoJdbc.DISPATCH_FRAME_MAPPER.mapRow(rs, rowNum);
frame.slotsRequired = rs.getInt("int_slots_required");
return frame;
}
};

@Override
public Set<String> findSlotDispatchJobs(DispatchHost host, int numJobs) {
long lastTime = System.currentTimeMillis();
LinkedHashSet<String> result = new LinkedHashSet<String>();

result.addAll(getJdbcTemplate().query(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
String query = handleInClause("str_os", SlotDispatchQuery.FIND_SLOT_DISPATCH_JOBS,
host.getOs().length);
PreparedStatement findJobsStmt = conn.prepareStatement(query);
int index = 1;
findJobsStmt.setString(index++, host.getFacilityId());
for (String item : host.getOs()) {
findJobsStmt.setString(index++, item);
}
findJobsStmt.setInt(index++, host.idleSlots);
findJobsStmt.setString(index++, host.getName());
findJobsStmt.setString(index++, host.getAllocationId());
findJobsStmt.setInt(index++, numJobs);
return findJobsStmt;
}
}, PKJOB_MAPPER));

prometheusMetrics.setBookingDurationMetric("findSlotDispatchJobs query",
System.currentTimeMillis() - lastTime);
return result;
}

@Override
public List<DispatchFrame> findNextSlotDispatchFrames(JobInterface job, DispatchHost host,
int limit) {
long lastTime = System.currentTimeMillis();
List<DispatchFrame> frames =
getJdbcTemplate().query(SlotDispatchQuery.FIND_SLOT_DISPATCH_FRAMES_BY_JOB_AND_HOST,
SLOT_DISPATCH_FRAME_MAPPER, host.idleSlots, job.getJobId(), host.getName(),
job.getJobId(), host.getAllocationId(), limit);
prometheusMetrics.setBookingDurationMetric("findNextSlotDispatchFrames query",
System.currentTimeMillis() - lastTime);
return frames;
}

@Override
public int getSlotCapacityRemaining(JobInterface job, DispatchHost host) {
try {
Integer capacity =
getJdbcTemplate().queryForObject(SlotDispatchQuery.GET_SLOT_CAPACITY_REMAINING,
Integer.class, host.getAllocationId(), job.getJobId());
if (capacity == null) {
return Integer.MAX_VALUE;
}
return Math.max(capacity, 0);
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
// No subscription between the job's show and the host's allocation.
return 0;
}
}

@Override
public DispatchFrame findNextDispatchFrame(JobInterface job, VirtualProc proc) {
return findNextDispatchFrames(job, proc, 1).get(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,22 @@ public void updateMaxCores(GroupInterface group, int value) {
}
}

@Override
public void updateMaxSlots(GroupInterface group, int value) {
// Slots are whole counts: -1 = unlimited, 0 = reject-all, N = cap. Normalize any
// negative to the -1 sentinel.
if (value < 0) {
value = CueUtil.FEATURE_DISABLED;
}

getJdbcTemplate().update("UPDATE folder_resource SET int_max_slots=? WHERE pk_folder=?",
value, group.getId());

if (accountingNotifier.isEnabled() && showDao.isSchedulerManaged(group.getShowId())) {
accountingNotifier.notifyFolderMaxSlots(group.getId(), value);
}
}

@Override
public void updateMinCores(GroupInterface group, int value) {
if (value < 0) {
Expand Down
Loading
Loading