juspay/hyperswitch · error
Spawn block failure
Error message
Spawn block failure
What it means
Panic message from .expect() on the JoinHandle of a tokio::task::spawn_blocking call in test_authenticated_connection_set_get (crates/redis_interface/src/test.rs:2358). The spawned blocking closure runs set/get/del round-trips; .await.unwrap-style expect fails either if the task panicked inside (e.g. an inner expect on a Redis op) or if the JoinError is a cancel/panic. The inner Redis failures are surfaced as this generic 'Spawn block failure'.
Solutions
- Fix the root cause inside the closure: run the test with a reachable Redis so set/get/del succeed instead of panicking.
- Replace inner .expect()s in the closure with a Result-returning closure so failures surface as assertion messages, not JoinError panics.
- Log the JoinError kind before asserting to distinguish panic vs cancel: expect(...) with a formatted message including err.is_panic().
- Ensure the tokio runtime outlives the task (no missing .await, no early return) so the blocking task is not cancelled.
Example fix
// before
let is_success = tokio::task::spawn_blocking(move || {
...set_result.is_ok() && ...
})
.await
.expect("Spawn block failure");
// after
let is_success = tokio::task::spawn_blocking(move || {
...set_result.is_ok() && ...
})
.await
.unwrap_or_else(|e| panic!("Spawn block failure (panic={})", e.is_panic())); Defensive patterns
Strategy: try-catch
Try / catch
let is_success = tokio::task::spawn_blocking(move || { ... })
.await
.unwrap_or_else(|e| panic!("Spawn block failure (is_panic={}, cancelled={})", e.is_panic(), e.is_cancelled())); Prevention
- Never expect()/unwrap() inside spawn_blocking closures; return Result or bool
- Format JoinError details in the outer expect to expose the inner panic message
- Ensure the tokio runtime outlives spawned blocking tasks
- Confirm Redis availability first so inner operations don't panic
When it happens
Trigger: spawn_blocking(...).await.expect("Spawn block failure") returns Err: the closure panicked (unwrap/expect inside on set/get/del results, connection pool gone), or the runtime shut down the task (blocking pool saturated, runtime dropped).
Common situations: Redis not available so inner expects panic, propagating as JoinError::is_panic; test runtime torn down before the blocking task completes; too many blocking tasks starving the pool in parallel test runs.
Related errors
- failed to connect to authenticated redis
- failed to publish on authenticated connection
- failed to subscribe on authenticated connection
- todo!()
- todo!()
AI-assisted analysis of juspay/hyperswitch@afd9284352 (2026-09-18).
Data as JSON: /api/errors/a7070386d9f94580.
Report an issue: GitHub.
Appendix: source
Thrown at crates/redis_interface/src/test.rs:2358
let Some(pool) = get_auth_pool_or_skip().await else {
tracing::warn!("SKIP: auth test skipped — set TEST_REDIS_AUTH_PASSWORD to enable");
return true;
};
let key: RedisKey = format!("test_auth_set_get_{}", unique_test_id()).into();
let value = "authenticated_value".to_string();
let set_result = pool.set_key_with_expiry(&key, value.clone(), 60).await;
let get_result: Result<String, _> = pool.get_key(&key).await;
let del_result = pool.delete_key(&key).await;
set_result.is_ok()
&& matches!(get_result, Ok(retrieved) if retrieved == value)
&& del_result.is_ok()
})
})
.await
.expect("Spawn block failure");
assert!(is_success);
}
#[tokio::test]
async fn test_authenticated_connection_pubsub() {
let is_success = tokio::task::spawn_blocking(move || {
futures::executor::block_on(async {
let Some(pool) = get_auth_pool_or_skip().await else {
tracing::warn!("SKIP: auth test skipped — set TEST_REDIS_AUTH_PASSWORD to enable");
return true;
};
let channel = format!("test_auth_pubsub_{}", unique_test_id());
let test_message = "authenticated_message";
pool.redis_conn
.subscriberView on GitHub (pinned to afd9284352)