fix(streaming): sane DynamicBatchingConfig default; worker lifecycle regression tests
CI / Format Check (push) Failing after 7s
CI / Build (ubuntu-latest) (push) Failing after 7s
CI / Clippy Check (push) Failing after 8s
Documentation / Build API Documentation (push) Failing after 7s
Documentation / Build User Guide (push) Successful in 8s
CI / Build (macos-latest) (push) Failing after 8s
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
Performance Benchmarks / Run Benchmarks (push) Successful in 26s
CI / Build CPU-Only (Explicit) (push) Failing after 1m17s
CI / CI Success (push) Failing after 0s

The new lifecycle tests caught that the batch-optimizer worker crashed at
spawn: DynamicBatchingConfig derived Default (all zeros), and tokio's
interval() panics on a zero period. Default is now a usable config
(batch 32 in [1,128], step 4, 1ms latency target, 100-sample window,
1s optimization interval, AIMD adaptation).

New regression tests in AdaptiveProcessor, EdgeComputingManager, and
MonitoringSystem assert that all workers are still alive shortly after
start() (catches workers dying at startup) and that stop() completes via
the graceful control-channel path, not the 5s abort backstop (catches
shutdown hangs).

cargo test -p rtx-streaming: 58 lib + 8 integration + 6 aux, all green.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
osobh
2026-07-10 17:16:04 -07:00
co-authored by Claude Fable 5
parent c83e0fb22d
commit ad6405663f
4 changed files with 108 additions and 1 deletions
@@ -197,7 +197,7 @@ impl DynamicBatchController {
// ============================================================================ // ============================================================================
/// Dynamic batching configuration /// Dynamic batching configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DynamicBatchingConfig { pub struct DynamicBatchingConfig {
/// Initial batch size /// Initial batch size
pub initial_batch_size: usize, pub initial_batch_size: usize,
@@ -224,6 +224,24 @@ pub struct DynamicBatchingConfig {
pub optimization_interval_ms: u64, pub optimization_interval_ms: u64,
} }
impl Default for DynamicBatchingConfig {
fn default() -> Self {
// The derived all-zero Default was unusable: batch sizes of 0 and a
// 0ms optimization interval (tokio's `interval` panics on a zero
// period, killing the batch-optimizer worker at spawn).
Self {
initial_batch_size: 32,
min_batch_size: 1,
max_batch_size: 128,
batch_size_step: 4,
target_latency_micros: 1000,
adaptation_algorithm: BatchAdaptationAlgorithm::default(),
stats_window_size: 100,
optimization_interval_ms: 1000,
}
}
}
/// Batch adaptation algorithms /// Batch adaptation algorithms
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BatchAdaptationAlgorithm { pub enum BatchAdaptationAlgorithm {
@@ -440,6 +440,37 @@ mod tests {
assert!(processor.is_ok()); assert!(processor.is_ok());
} }
/// Regression test for two lifecycle bugs: (1) the resource monitor used
/// to exit on ANY control message — including the Start sent by start() —
/// so it died moments after starting; (2) stop() used to await interval
/// loops that never exit, hanging forever. Workers must stay alive after
/// start() and exit gracefully on Stop (not via the 5s abort backstop).
#[tokio::test]
async fn test_worker_lifecycle_start_stop() {
let processor = AdaptiveProcessor::new(AdaptiveProcessingConfig::default())
.await
.unwrap();
processor.start().await.expect("start failed");
tokio::time::sleep(Duration::from_millis(300)).await;
{
let handles = processor.worker_handles.lock().await;
assert_eq!(handles.len(), 3, "expected 3 workers running");
assert!(
handles.iter().all(|h| !h.is_finished()),
"a worker exited right after start"
);
}
let stop_started = Instant::now();
processor.stop().await.expect("stop failed");
assert!(
stop_started.elapsed() < Duration::from_secs(4),
"stop() fell back to the abort path instead of graceful exit"
);
assert!(processor.worker_handles.lock().await.is_empty());
}
#[tokio::test] #[tokio::test]
async fn test_adaptive_batch_processing() { async fn test_adaptive_batch_processing() {
let config = AdaptiveProcessingConfig::default(); let config = AdaptiveProcessingConfig::default();
@@ -2173,6 +2173,35 @@ mod tests {
assert!(manager.is_ok()); assert!(manager.is_ok());
} }
/// Regression test: workers must stay alive after start() and exit
/// gracefully on stop() (via the control channel, not the 5s abort
/// backstop).
#[tokio::test]
async fn test_worker_lifecycle_start_stop() {
let manager = EdgeComputingManager::new(EdgeComputingConfig::default())
.await
.unwrap();
manager.start().await.expect("start failed");
tokio::time::sleep(Duration::from_millis(200)).await;
{
let handles = manager.worker_handles.lock().await;
assert_eq!(handles.len(), 3, "expected 3 workers running");
assert!(
handles.iter().all(|h| !h.is_finished()),
"a worker exited right after start"
);
}
let stop_started = std::time::Instant::now();
manager.stop().await.expect("stop failed");
assert!(
stop_started.elapsed() < Duration::from_secs(4),
"stop() fell back to the abort path instead of graceful exit"
);
assert!(manager.worker_handles.lock().await.is_empty());
}
#[tokio::test] #[tokio::test]
async fn test_lightweight_inference() { async fn test_lightweight_inference() {
let config = EdgeComputingConfig::default(); let config = EdgeComputingConfig::default();
@@ -1922,6 +1922,35 @@ mod tests {
assert!(monitoring.is_ok()); assert!(monitoring.is_ok());
} }
/// Regression test: workers must stay alive after start() and exit
/// gracefully on stop() (via the control channel, not the 5s abort
/// backstop).
#[tokio::test]
async fn test_worker_lifecycle_start_stop() {
let monitoring = MonitoringSystem::new(MonitoringConfig::default())
.await
.unwrap();
monitoring.start().await.expect("start failed");
tokio::time::sleep(Duration::from_millis(200)).await;
{
let handles = monitoring.worker_handles.lock().await;
assert_eq!(handles.len(), 4, "expected 4 workers running");
assert!(
handles.iter().all(|h| !h.is_finished()),
"a worker exited right after start"
);
}
let stop_started = std::time::Instant::now();
monitoring.stop().await.expect("stop failed");
assert!(
stop_started.elapsed() < Duration::from_secs(4),
"stop() fell back to the abort path instead of graceful exit"
);
assert!(monitoring.worker_handles.lock().await.is_empty());
}
#[tokio::test] #[tokio::test]
async fn test_stream_metrics_recording() { async fn test_stream_metrics_recording() {
let config = MonitoringConfig::default(); let config = MonitoringConfig::default();