@@ -1223,6 +1223,32 @@ impl SchedulerImpl {
12231223 self . cluster . schedule_repartition ( p) . await
12241224 }
12251225
1226+ // Stateless hash placement of (table_id, location). Empty counts make every worker tie,
1227+ // so pick_import_worker degenerates to the plain hash pick.
1228+ async fn schedule_import_jobs_hashed (
1229+ & self ,
1230+ table_id : u64 ,
1231+ locations : & [ & String ] ,
1232+ ) -> Result < ( ) , CubeError > {
1233+ let empty = HashMap :: new ( ) ;
1234+ for & l in locations {
1235+ let node = pick_import_worker ( self . config . as_ref ( ) , table_id, l, & empty) ;
1236+ let job = self
1237+ . meta_store
1238+ . add_job ( Job :: new (
1239+ RowKey :: Table ( TableId :: Tables , table_id) ,
1240+ JobType :: TableImportCSV ( l. clone ( ) ) ,
1241+ node. clone ( ) ,
1242+ ) )
1243+ . await ?;
1244+ if job. is_some ( ) {
1245+ // TODO queue failover
1246+ self . cluster . notify_job_runner ( node) . await ?;
1247+ }
1248+ }
1249+ Ok ( ( ) )
1250+ }
1251+
12261252 async fn schedule_table_import (
12271253 & self ,
12281254 table_id : u64 ,
@@ -1233,25 +1259,26 @@ impl SchedulerImpl {
12331259 return Ok ( ( ) ) ;
12341260 }
12351261
1262+ // Stream imports are long-lived and run on a separate pool; they need stable,
1263+ // deterministic placement, so they always use plain hash placement and are never
1264+ // affected by load-aware placement, which targets bursty CSV file imports only.
1265+ let ( csv_locations, stream_locations) : ( Vec < & String > , Vec < & String > ) = locations
1266+ . iter ( )
1267+ . copied ( )
1268+ . partition ( |& l| !Table :: is_stream_location ( l) ) ;
1269+
1270+ self . schedule_import_jobs_hashed ( table_id, & stream_locations)
1271+ . await ?;
1272+
1273+ // Stream-only tables (the common reconcile path) have nothing left to place; skip the
1274+ // load-aware lock and the in_flight_import_jobs_by_node scan entirely.
1275+ if csv_locations. is_empty ( ) {
1276+ return Ok ( ( ) ) ;
1277+ }
1278+
12361279 if !self . config . load_aware_import_placement_enabled ( ) {
1237- // Default: stateless hash placement of (table_id, location). Empty counts make
1238- // every worker tie, so pick_import_worker degenerates to the plain hash pick.
1239- let empty = HashMap :: new ( ) ;
1240- for & l in locations {
1241- let node = pick_import_worker ( self . config . as_ref ( ) , table_id, l, & empty) ;
1242- let job = self
1243- . meta_store
1244- . add_job ( Job :: new (
1245- RowKey :: Table ( TableId :: Tables , table_id) ,
1246- JobType :: TableImportCSV ( l. clone ( ) ) ,
1247- node. clone ( ) ,
1248- ) )
1249- . await ?;
1250- if job. is_some ( ) {
1251- // TODO queue failover
1252- self . cluster . notify_job_runner ( node) . await ?;
1253- }
1254- }
1280+ self . schedule_import_jobs_hashed ( table_id, & csv_locations)
1281+ . await ?;
12551282 return Ok ( ( ) ) ;
12561283 }
12571284
@@ -1270,7 +1297,7 @@ impl SchedulerImpl {
12701297 ) ;
12711298 }
12721299 let mut counts = self . meta_store . in_flight_import_jobs_by_node ( ) . await ?;
1273- for & l in locations {
1300+ for & l in & csv_locations {
12741301 let node = pick_import_worker ( self . config . as_ref ( ) , table_id, l, & counts) ;
12751302 let job = self
12761303 . meta_store
@@ -1687,6 +1714,54 @@ mod tests {
16871714 let _ = fs:: remove_dir_all ( config. remote_dir ( ) ) ;
16881715 }
16891716
1717+ #[ tokio:: test]
1718+ async fn schedule_table_import_stream_ignores_load_aware ( ) {
1719+ let config =
1720+ Config :: test ( "schedule_table_import_stream_load_aware" ) . update_config ( |mut c| {
1721+ c. select_workers = ( 0 ..4 ) . map ( |i| format ! ( "worker{}" , i) ) . collect ( ) ;
1722+ c. load_aware_import_placement_enabled = true ;
1723+ c
1724+ } ) ;
1725+ let ( meta_store, scheduler) = test_scheduler (
1726+ "schedule_table_import_stream_load_aware" ,
1727+ config. config_obj ( ) ,
1728+ ) ;
1729+ meta_store
1730+ . create_schema ( "s" . to_string ( ) , false )
1731+ . await
1732+ . unwrap ( ) ;
1733+
1734+ // Heavily pre-seed worker0 with CSV imports. Stream locations must ignore this load and
1735+ // follow the stateless hash, since load-aware placement targets CSV imports only.
1736+ let seed = create_import_table ( & meta_store, "seed" ) . await ;
1737+ for i in 0 ..5 {
1738+ add_import_job ( & meta_store, seed, & format ! ( "seed-{}" , i) , "worker0" ) . await ;
1739+ }
1740+
1741+ let table = create_import_table ( & meta_store, "bar" ) . await ;
1742+ let locs: Vec < String > = ( 0 ..6 ) . map ( |i| format ! ( "stream:topic-{}" , i) ) . collect ( ) ;
1743+ let loc_refs: Vec < & String > = locs. iter ( ) . collect ( ) ;
1744+ scheduler
1745+ . schedule_table_import ( table, & loc_refs)
1746+ . await
1747+ . unwrap ( ) ;
1748+
1749+ let placement = import_placement ( & meta_store, table) . await ;
1750+ let empty = HashMap :: new ( ) ;
1751+ for l in & locs {
1752+ let expected = pick_import_worker ( config. config_obj ( ) . as_ref ( ) , table, l, & empty) ;
1753+ assert_eq ! (
1754+ placement. get( l) ,
1755+ Some ( & expected) ,
1756+ "stream placement must follow the stateless hash even with load-aware enabled"
1757+ ) ;
1758+ }
1759+
1760+ RocksMetaStore :: cleanup_test_metastore ( "schedule_table_import_stream_load_aware" ) ;
1761+ let _ = fs:: remove_dir_all ( config. local_dir ( ) ) ;
1762+ let _ = fs:: remove_dir_all ( config. remote_dir ( ) ) ;
1763+ }
1764+
16901765 #[ tokio:: test]
16911766 async fn schedule_table_import_hash_placement_by_default ( ) {
16921767 let config = Config :: test ( "schedule_table_import_hash" ) . update_config ( |mut c| {
0 commit comments