@@ -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