@@ -17,6 +17,7 @@ use chrono::{DateTime, Utc};
1717use miette:: { Context , IntoDiagnostic , Result } ;
1818use opencue_proto:: host:: ThreadMode ;
1919use sqlx:: { Pool , Postgres , Transaction } ;
20+ use thiserror:: Error ;
2021use tracing:: trace;
2122use uuid:: Uuid ;
2223
@@ -27,6 +28,45 @@ use crate::{
2728 pgpool:: connection_pool,
2829} ;
2930
31+ #[ derive( Debug , Error ) ]
32+ pub enum HostDaoError {
33+ #[ error( "Host resources exhausted (likely booked by another scheduler)" ) ]
34+ HostResourcesExhausted ,
35+
36+ #[ error( "Resource limit exceeded: {message}" ) ]
37+ ResourceLimitExceeded { message : String } ,
38+
39+ #[ error( "{context}: {source}" ) ]
40+ DbFailure {
41+ context : & ' static str ,
42+ source : sqlx:: Error ,
43+ } ,
44+ }
45+
46+ /// Known PostgreSQL trigger messages that indicate expected resource limit enforcement.
47+ const RESOURCE_LIMIT_MESSAGES : & [ & str ] = & [
48+ "job has exceeded max cores" ,
49+ "job has exceeded max GPU units" ,
50+ "subscription has exceeded burst size" ,
51+ "unable to allocate additional core units" ,
52+ "unable to allocate additional memory" ,
53+ "unable to allocate additional GPU units" ,
54+ "unable to allocate additional GPU memory" ,
55+ ] ;
56+
57+ fn check_resource_limit_error ( err : sqlx:: Error , context : & ' static str ) -> HostDaoError {
58+ let msg = err. to_string ( ) ;
59+ match RESOURCE_LIMIT_MESSAGES . iter ( ) . find ( |m| msg. contains ( * m) ) {
60+ Some ( m) => HostDaoError :: ResourceLimitExceeded {
61+ message : m. to_string ( ) ,
62+ } ,
63+ None => HostDaoError :: DbFailure {
64+ context,
65+ source : err,
66+ } ,
67+ }
68+ }
69+
3070/// Data Access Object for host operations in the job dispatch system.
3171///
3272/// Manages database operations related to render hosts, including:
@@ -114,16 +154,19 @@ impl From<HostModel> for Host {
114154// - memory and core fields on table host are only updated when booking procs (update_host_resources)
115155// - the table host_stat contains memory fields that are updated by cuebot on HostReportHandler
116156//
117- // In summary, use host_stat for most up to date memory stats
157+ // We use LEAST(h.int_mem_idle, hs.int_mem_free) to get the most conservative memory estimate.
158+ // h.int_mem_idle reflects bookings (decremented on dispatch), while hs.int_mem_free reflects
159+ // actual OS-reported free memory. Using the minimum avoids overestimating available memory
160+ // when another scheduler (e.g. Cuebot) has booked resources that haven't been consumed yet.
118161static QUERY_HOST_BY_SHOW_FACILITY_AND_TAG : & str = r#"
119162SELECT DISTINCT
120163 h.pk_host,
121164 h.str_name,
122165 hs.str_os,
123166 h.int_cores_idle,
124- hs.int_mem_free,
167+ LEAST(h.int_mem_idle, hs.int_mem_free) as int_mem_free,
125168 h.int_gpus_idle,
126- hs.int_gpu_mem_free,
169+ LEAST(h.int_gpu_mem_idle, hs.int_gpu_mem_free) as int_gpu_mem_free,
127170 h.int_cores,
128171 hs.int_mem_total,
129172 h.int_thread_mode,
@@ -149,6 +192,10 @@ SET int_cores_idle = int_cores_idle - $1,
149192 int_gpus_idle = int_gpus_idle - $3,
150193 int_gpu_mem_idle = int_gpu_mem_idle - $4
151194WHERE pk_host = $5
195+ AND int_cores_idle >= $1
196+ AND int_mem_idle >= $2
197+ AND int_gpus_idle >= $3
198+ AND int_gpu_mem_idle >= $4
152199RETURNING int_cores_idle, int_mem_idle, int_gpus_idle, int_gpu_mem_idle, NOW()
153200"# ;
154201
@@ -324,24 +371,22 @@ impl HostDao {
324371 transaction : & mut Transaction < ' _ , Postgres > ,
325372 host_id : & Uuid ,
326373 virtual_proc : & VirtualProc ,
327- dispatch_id : Uuid ,
328- ) -> Result < UpdatedHostResources > {
329- let ( cores_idle, mem_idle, gpus_idle, gpu_mem_idle, last_updated) : (
330- i64 ,
331- i64 ,
332- i64 ,
333- i64 ,
334- DateTime < Utc > ,
335- ) = sqlx:: query_as ( UPDATE_HOST_RESOURCES )
336- . bind ( virtual_proc. cores_reserved . value ( ) )
337- . bind ( ( virtual_proc. memory_reserved . as_u64 ( ) / KB ) as i64 )
338- . bind ( virtual_proc. gpus_reserved as i32 )
339- . bind ( virtual_proc. gpu_memory_reserved . as_u64 ( ) as i64 )
340- . bind ( host_id. to_string ( ) )
341- . fetch_one ( & mut * * transaction)
342- . await
343- . into_diagnostic ( )
344- . wrap_err ( format ! ( "({dispatch_id}) Failed to update host resources" ) ) ?;
374+ ) -> Result < UpdatedHostResources , HostDaoError > {
375+ let row: Option < ( i64 , i64 , i64 , i64 , DateTime < Utc > ) > =
376+ sqlx:: query_as ( UPDATE_HOST_RESOURCES )
377+ . bind ( virtual_proc. cores_reserved . value ( ) )
378+ . bind ( ( virtual_proc. memory_reserved . as_u64 ( ) / KB ) as i64 )
379+ . bind ( virtual_proc. gpus_reserved as i32 )
380+ . bind ( virtual_proc. gpu_memory_reserved . as_u64 ( ) as i64 )
381+ . bind ( host_id. to_string ( ) )
382+ . fetch_optional ( & mut * * transaction)
383+ . await
384+ . map_err ( |err| {
385+ check_resource_limit_error ( err, "Failed to update host resources" )
386+ } ) ?;
387+
388+ let ( cores_idle, mem_idle, gpus_idle, gpu_mem_idle, last_updated) =
389+ row. ok_or ( HostDaoError :: HostResourcesExhausted ) ?;
345390
346391 if CONFIG . host_cache . update_stat_on_book {
347392 sqlx:: query ( UPDATE_HOST_STAT )
@@ -350,8 +395,7 @@ impl HostDao {
350395 . bind ( host_id. to_string ( ) )
351396 . execute ( & mut * * transaction)
352397 . await
353- . into_diagnostic ( )
354- . wrap_err ( "Failed to update host stat" ) ?;
398+ . map_err ( |err| check_resource_limit_error ( err, "Failed to update host stat" ) ) ?;
355399 }
356400
357401 sqlx:: query ( UPDATE_SUBSCRIPTION )
@@ -361,35 +405,33 @@ impl HostDao {
361405 . bind ( virtual_proc. alloc_id . to_string ( ) )
362406 . execute ( & mut * * transaction)
363407 . await
364- . into_diagnostic ( )
365- . wrap_err ( "Failed to update subscription resources" ) ?;
408+ . map_err ( |err| {
409+ check_resource_limit_error ( err, "Failed to update subscription resources" )
410+ } ) ?;
366411
367412 sqlx:: query ( UPDATE_LAYER_RESOURCE )
368413 . bind ( virtual_proc. cores_reserved . value ( ) )
369414 . bind ( virtual_proc. gpus_reserved as i32 )
370415 . bind ( virtual_proc. layer_id . to_string ( ) )
371416 . execute ( & mut * * transaction)
372417 . await
373- . into_diagnostic ( )
374- . wrap_err ( "Failed to update layer resources" ) ?;
418+ . map_err ( |err| check_resource_limit_error ( err, "Failed to update layer resources" ) ) ?;
375419
376420 sqlx:: query ( UPDATE_JOB_RESOURCE )
377421 . bind ( virtual_proc. cores_reserved . value ( ) )
378422 . bind ( virtual_proc. gpus_reserved as i32 )
379423 . bind ( virtual_proc. job_id . to_string ( ) )
380424 . execute ( & mut * * transaction)
381425 . await
382- . into_diagnostic ( )
383- . wrap_err ( "Failed to update job resources" ) ?;
426+ . map_err ( |err| check_resource_limit_error ( err, "Failed to update job resources" ) ) ?;
384427
385428 sqlx:: query ( UPDATE_FOLDER_RESOURCE )
386429 . bind ( virtual_proc. cores_reserved . value ( ) )
387430 . bind ( virtual_proc. gpus_reserved as i32 )
388431 . bind ( virtual_proc. job_id . to_string ( ) )
389432 . execute ( & mut * * transaction)
390433 . await
391- . into_diagnostic ( )
392- . wrap_err ( "Failed to update folder resources" ) ?;
434+ . map_err ( |err| check_resource_limit_error ( err, "Failed to update folder resources" ) ) ?;
393435
394436 sqlx:: query ( UPDATE_POINT )
395437 . bind ( virtual_proc. cores_reserved . value ( ) )
@@ -398,8 +440,7 @@ impl HostDao {
398440 . bind ( virtual_proc. show_id . to_string ( ) )
399441 . execute ( & mut * * transaction)
400442 . await
401- . into_diagnostic ( )
402- . wrap_err ( "Failed to update point resources" ) ?;
443+ . map_err ( |err| check_resource_limit_error ( err, "Failed to update point resources" ) ) ?;
403444
404445 Ok ( UpdatedHostResources {
405446 cores_idle,
0 commit comments