Skip to content

Commit e2fc8ca

Browse files
committed
Fix review suggestions
1 parent d01cb05 commit e2fc8ca

7 files changed

Lines changed: 291 additions & 21 deletions

File tree

cuebot/src/main/java/com/imageworks/spcue/dao/postgres/NestedWhiteboardDaoJdbc.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ private static final NestedJob mapResultSetToJob(ResultSet rs) throws SQLExcepti
283283
+ "host.int_gpus, " + "host.int_gpus_idle, " + "host.int_gpu_mem, "
284284
+ "host.int_gpu_mem_idle, " + "host.int_mem, " + "host.int_mem_idle, "
285285
+ "host.str_lock_state, " + "host.str_tags, " + "host.b_comment, "
286-
+ "host.int_thread_mode, " + "host_stat.int_running_slots, " + "host_stat.str_os, "
286+
+ "host.int_thread_mode, " + "host_stat.int_running_slots, "
287+
+ "host.int_concurrent_slots_limit, " + "host_stat.str_os, "
287288
+ "host_stat.int_mem_total, " + "host_stat.int_mem_free, "
288289
+ "host_stat.int_swap_total, " + "host_stat.int_swap_free, "
289290
+ "host_stat.int_mcp_total, " + "host_stat.int_mcp_free, "

cuebot/src/main/resources/conf/ddl/postgres/migrations/V38__Add_host_frame_limit.sql renamed to cuebot/src/main/resources/conf/ddl/postgres/migrations/V38__Add_host_frame_slot_limit.sql

File renamed without changes.

docs/_docs/concepts/glossary.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -121,28 +121,17 @@ outline script to batch multiple job submissions into a single job, setting up
121121
dependencies and / or running in parallel. Outline scripts can submit almost any
122122
type of job to the cue, including Maya, Katana, or even shell commands.
123123

124-
## Proc
125-
126-
A proc is a unit of work on a render *host* that executes a *frame*. In
127-
core-based booking, a proc represents reserved cores on a host.
128-
129-
## Slot
130-
131-
A slot is a scheduling unit used by slot-based booking. Slots are defined per
132-
host and consumed per frame.
133-
134-
## Slot-based booking
135-
136-
A booking mode where hosts are scheduled by slots instead of cores, memory, or
137-
GPU resources. It is enabled by setting a concurrent slots limit on hosts and a
138-
`slots_required` value on layers.
139-
140124
## OpenCue REST Gateway
141125

142126
A production-ready HTTP service that translates REST API calls to gRPC
143127
communication with Cuebot. Enables web applications, scripts, and third-party
144128
tools to interact with OpenCue services through standard HTTP endpoints.
145129

130+
## Proc
131+
132+
A proc is a unit of work on a render *host* that executes a *frame*. In
133+
core-based booking, a proc represents reserved cores on a host.
134+
146135
## PyCue
147136

148137
The Python API library that provides programmatic access to OpenCue
@@ -184,6 +173,17 @@ service by default. Services can be used to setup different requirements for
184173
different software jobs. For instance, a Maya render may need 6GB of memory vs a
185174
Nuke render may only need 2GB.
186175

176+
## Slot
177+
178+
A slot is a scheduling unit used by slot-based booking. Slots are defined per
179+
host and consumed per frame.
180+
181+
## Slot-based booking
182+
183+
A booking mode where hosts are scheduled by slots instead of cores, memory, or
184+
GPU resources. It is enabled by setting a concurrent slots limit on hosts and a
185+
`slots_required` value on layers.
186+
187187
## Show
188188

189189
A show is a group of related work to be done. *Jobs* submitted to OpenCue exist

proto/src/host.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ service HostInterface {
9696
// Changes the host's [ThreadMode]
9797
rpc SetThreadMode(HostSetThreadModeRequest) returns (HostSetThreadModeResponse);
9898

99-
// Set the maximum concurrent procs limit for the host.
99+
// Set the maximum concurrent slots limit for the host.
100100
rpc SetConcurrentSlotsLimit(HostSetConcurrentSlotsLimitRequest) returns (HostSetConcurrentSlotsLimitResponse);
101101

102102
// Unlocks the host for booking if the proc is in the Locked state. You cannot unlock a NimbyLocked proc.

rust/crates/rqd/src/system/machine.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ impl MachineMonitor {
555555
.await
556556
{
557557
warn!(
558-
"Failed to release cores reserved by {}: {}",
558+
"Failed to release slots reserved by {}: {}",
559559
frame.request.resource_id(),
560560
err
561561
);

rust/crates/scheduler/src/dao/host_dao.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ UPDATE host_stat
167167
SET int_mem_free = int_mem_free - $1,
168168
int_gpu_mem_free = int_gpu_mem_free - $2,
169169
int_running_slots = int_running_slots + $3
170-
WHERE pk_host = $3
170+
WHERE pk_host = $4
171171
"#;
172172

173173
static UPDATE_SUBSCRIPTION: &str = r#"

rust/crates/scheduler/src/pipeline/dispatcher/actor.rs

Lines changed: 270 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,15 @@ impl RqdDispatcherService {
721721
)))?
722722
}
723723

724+
if let Some(concurrent_slots_limit) = host.concurrent_slots_limit {
725+
if concurrent_slots_limit < host.running_slots_count + frame.slots_required {
726+
Err(VirtualProcError::HostResourcesExtinguished(format!(
727+
"Not enough slots available: {} slots taken, requested {} slots",
728+
host.running_slots_count, frame.slots_required
729+
)))?
730+
}
731+
}
732+
724733
let memory_reserved = frame.min_memory;
725734
let gpus_reserved = frame.min_gpus;
726735
let gpu_memory_reserved = frame.min_gpu_memory;
@@ -733,6 +742,9 @@ impl RqdDispatcherService {
733742
ByteSize(host.idle_gpu_memory.as_u64() - gpu_memory_reserved.as_u64());
734743
// Field will be overwritten with database values as soon as the changes are committed
735744
host.last_updated = Utc::now();
745+
if host.concurrent_slots_limit.is_some() {
746+
host.running_slots_count += frame.slots_required;
747+
}
736748

737749
Ok((
738750
VirtualProc {
@@ -971,7 +983,7 @@ impl RqdDispatcherService {
971983
log_file: "deprecated".to_string(),
972984
#[allow(deprecated)]
973985
log_dir_file: "deprecated".to_string(),
974-
slots_required: 0,
986+
slots_required: proc.slots_required as i32,
975987
};
976988

977989
Ok(run_frame)
@@ -1588,4 +1600,261 @@ mod tests {
15881600
let result = RqdDispatcherService::prepare_rqd_run_frame(&virtual_proc);
15891601
assert!(result.is_err());
15901602
}
1603+
1604+
// ── Slot-based scheduling tests ──────────────────────────────────────────
1605+
1606+
#[tokio::test]
1607+
async fn test_consume_host_virtual_resources_slots_required_propagated_to_virtual_proc() {
1608+
// When frame.slots_required != 0, the VirtualProc should carry the same value
1609+
let mut frame = create_test_dispatch_frame();
1610+
frame.slots_required = 2;
1611+
1612+
let mut host = create_test_host();
1613+
host.concurrent_slots_limit = Some(4);
1614+
1615+
let result = RqdDispatcherService::consume_host_virtual_resources(
1616+
&frame,
1617+
&host,
1618+
ByteSize::gib(1),
1619+
)
1620+
.await;
1621+
1622+
assert!(result.is_ok());
1623+
let (virtual_proc, _updated_host) = result.unwrap();
1624+
assert_eq!(virtual_proc.slots_required, 2);
1625+
}
1626+
1627+
#[tokio::test]
1628+
async fn test_consume_host_virtual_resources_with_slots_deducts_cores_and_memory() {
1629+
// Slot-based frames still deduct core and memory resources from the host
1630+
let mut frame = create_test_dispatch_frame();
1631+
frame.slots_required = 2;
1632+
frame.min_cores = CoreSize(1);
1633+
frame.min_memory = ByteSize::gib(2);
1634+
frame.threadable = false; // predictable core reservation
1635+
1636+
let mut host = create_test_host();
1637+
host.concurrent_slots_limit = Some(4);
1638+
let initial_idle_memory = host.idle_memory;
1639+
let initial_idle_cores = host.idle_cores;
1640+
1641+
let result = RqdDispatcherService::consume_host_virtual_resources(
1642+
&frame,
1643+
&host,
1644+
ByteSize::gib(1),
1645+
)
1646+
.await;
1647+
1648+
assert!(result.is_ok());
1649+
let (virtual_proc, updated_host) = result.unwrap();
1650+
1651+
assert_eq!(
1652+
updated_host.idle_memory.as_u64(),
1653+
initial_idle_memory.as_u64() - frame.min_memory.as_u64()
1654+
);
1655+
assert!(updated_host.idle_cores < initial_idle_cores);
1656+
assert_eq!(virtual_proc.memory_reserved, frame.min_memory);
1657+
assert_eq!(virtual_proc.slots_required, 2);
1658+
}
1659+
1660+
#[tokio::test]
1661+
async fn test_consume_host_virtual_resources_with_slots_updates_running_slots_count() {
1662+
// When concurrent_slots_limit is set, running_slots_count should be incremented
1663+
let mut frame = create_test_dispatch_frame();
1664+
frame.slots_required = 2;
1665+
frame.threadable = false;
1666+
1667+
let mut host = create_test_host();
1668+
host.concurrent_slots_limit = Some(4);
1669+
host.running_slots_count = 0;
1670+
1671+
let result = RqdDispatcherService::consume_host_virtual_resources(
1672+
&frame,
1673+
&host,
1674+
ByteSize::gib(1),
1675+
)
1676+
.await;
1677+
1678+
assert!(result.is_ok());
1679+
let (_virtual_proc, updated_host) = result.unwrap();
1680+
assert_eq!(updated_host.running_slots_count, 2);
1681+
}
1682+
1683+
#[tokio::test]
1684+
async fn test_consume_host_virtual_resources_with_slots_fails_on_insufficient_cores() {
1685+
// Slot-based frames still fail when the host cannot satisfy core requirements
1686+
let mut frame = create_test_dispatch_frame();
1687+
frame.slots_required = 2;
1688+
frame.min_cores = CoreSize(100); // exceeds host capacity
1689+
1690+
let mut host = create_test_host();
1691+
host.concurrent_slots_limit = Some(4);
1692+
1693+
let result = RqdDispatcherService::consume_host_virtual_resources(
1694+
&frame,
1695+
&host,
1696+
ByteSize::gib(1),
1697+
)
1698+
.await;
1699+
1700+
assert!(result.is_err());
1701+
match result {
1702+
Err(VirtualProcError::HostResourcesExtinguished(msg)) => {
1703+
assert!(msg.contains("Not enough cores"));
1704+
}
1705+
_ => panic!("Expected HostResourcesExtinguished error for insufficient cores"),
1706+
}
1707+
}
1708+
1709+
#[tokio::test]
1710+
async fn test_consume_host_virtual_resources_with_slots_fails_on_insufficient_memory() {
1711+
// Slot-based frames still fail when the host cannot satisfy memory requirements
1712+
let mut frame = create_test_dispatch_frame();
1713+
frame.slots_required = 2;
1714+
frame.min_memory = ByteSize::gib(64); // exceeds host capacity
1715+
1716+
let mut host = create_test_host();
1717+
host.concurrent_slots_limit = Some(4);
1718+
1719+
let result = RqdDispatcherService::consume_host_virtual_resources(
1720+
&frame,
1721+
&host,
1722+
ByteSize::gib(1),
1723+
)
1724+
.await;
1725+
1726+
assert!(result.is_err());
1727+
match result {
1728+
Err(VirtualProcError::HostResourcesExtinguished(msg)) => {
1729+
assert!(msg.contains("Not enough memory"));
1730+
}
1731+
_ => panic!("Expected HostResourcesExtinguished error for insufficient memory"),
1732+
}
1733+
}
1734+
1735+
#[tokio::test]
1736+
async fn test_consume_host_virtual_resources_with_slots_fails_when_slots_exhausted() {
1737+
// When running_slots_count + slots_required exceeds concurrent_slots_limit, dispatch fails
1738+
let mut frame = create_test_dispatch_frame();
1739+
frame.slots_required = 3;
1740+
frame.threadable = false;
1741+
1742+
let mut host = create_test_host();
1743+
host.concurrent_slots_limit = Some(4);
1744+
host.running_slots_count = 2; // 2 + 3 > 4
1745+
1746+
let result = RqdDispatcherService::consume_host_virtual_resources(
1747+
&frame,
1748+
&host,
1749+
ByteSize::gib(1),
1750+
)
1751+
.await;
1752+
1753+
assert!(result.is_err());
1754+
match result {
1755+
Err(VirtualProcError::HostResourcesExtinguished(msg)) => {
1756+
assert!(msg.contains("Not enough slots"));
1757+
}
1758+
_ => panic!("Expected HostResourcesExtinguished error for insufficient slots"),
1759+
}
1760+
}
1761+
1762+
#[test]
1763+
fn test_prepare_rqd_run_frame_with_slots_required() {
1764+
// RunFrame message should include the slots_required value from the VirtualProc
1765+
let frame = create_test_dispatch_frame();
1766+
let virtual_proc = VirtualProc {
1767+
proc_id: Uuid::new_v4(),
1768+
host_id: Uuid::new_v4(),
1769+
show_id: Uuid::new_v4(),
1770+
layer_id: Uuid::new_v4(),
1771+
job_id: Uuid::new_v4(),
1772+
frame_id: Uuid::new_v4(),
1773+
alloc_id: Uuid::new_v4(),
1774+
cores_reserved: CoreSize(1).with_multiplier(),
1775+
memory_reserved: ByteSize::gib(2),
1776+
gpus_reserved: 0,
1777+
gpu_memory_reserved: ByteSize::gb(0),
1778+
os: "linux".to_string(),
1779+
is_local_dispatch: false,
1780+
frame,
1781+
host_name: "somehost".to_string(),
1782+
slots_required: 3,
1783+
};
1784+
1785+
let result = RqdDispatcherService::prepare_rqd_run_frame(&virtual_proc);
1786+
1787+
assert!(result.is_ok());
1788+
let run_frame = result.unwrap();
1789+
assert_eq!(run_frame.slots_required, 3);
1790+
}
1791+
1792+
#[tokio::test]
1793+
async fn test_consume_host_virtual_resources_sequential_slot_consumption() {
1794+
// Each successive slot-based dispatch reduces available slots and resources
1795+
let mut frame = create_test_dispatch_frame();
1796+
frame.slots_required = 1;
1797+
frame.min_cores = CoreSize(1);
1798+
frame.min_memory = ByteSize::gib(2);
1799+
frame.threadable = false;
1800+
1801+
let mut host = create_test_host();
1802+
host.concurrent_slots_limit = Some(4);
1803+
1804+
// First dispatch
1805+
let (vp1, host_after_first) = RqdDispatcherService::consume_host_virtual_resources(
1806+
&frame,
1807+
&host,
1808+
ByteSize::gib(1),
1809+
)
1810+
.await
1811+
.expect("first slot dispatch should succeed");
1812+
1813+
assert_eq!(vp1.slots_required, 1);
1814+
assert_eq!(host_after_first.running_slots_count, 1);
1815+
assert!(host_after_first.idle_memory < host.idle_memory);
1816+
1817+
// Second dispatch on the updated host
1818+
let (vp2, host_after_second) = RqdDispatcherService::consume_host_virtual_resources(
1819+
&frame,
1820+
&host_after_first,
1821+
ByteSize::gib(1),
1822+
)
1823+
.await
1824+
.expect("second slot dispatch should succeed");
1825+
1826+
assert_eq!(vp2.slots_required, 1);
1827+
assert_eq!(host_after_second.running_slots_count, 2);
1828+
assert!(host_after_second.idle_memory < host_after_first.idle_memory);
1829+
}
1830+
1831+
#[tokio::test]
1832+
async fn test_consume_host_virtual_resources_slots_zero_vs_nonzero() {
1833+
// Verify slots_required = 0 and != 0 produce VirtualProcs with the correct field
1834+
let frame_no_slots = create_test_dispatch_frame(); // slots_required = 0
1835+
1836+
let mut frame_with_slots = create_test_dispatch_frame();
1837+
frame_with_slots.slots_required = 5;
1838+
1839+
let host = create_test_host();
1840+
1841+
let (vp_no_slots, _) = RqdDispatcherService::consume_host_virtual_resources(
1842+
&frame_no_slots,
1843+
&host,
1844+
ByteSize::gib(1),
1845+
)
1846+
.await
1847+
.expect("dispatch without slots should succeed");
1848+
1849+
let (vp_with_slots, _) = RqdDispatcherService::consume_host_virtual_resources(
1850+
&frame_with_slots,
1851+
&host,
1852+
ByteSize::gib(1),
1853+
)
1854+
.await
1855+
.expect("dispatch with slots should succeed");
1856+
1857+
assert_eq!(vp_no_slots.slots_required, 0);
1858+
assert_eq!(vp_with_slots.slots_required, 5);
1859+
}
15911860
}

0 commit comments

Comments
 (0)