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

  1. Fix the root cause inside the closure: run the test with a reachable Redis so set/get/del succeed instead of panicking.
  2. Replace inner .expect()s in the closure with a Result-returning closure so failures surface as assertion messages, not JoinError panics.
  3. Log the JoinError kind before asserting to distinguish panic vs cancel: expect(...) with a formatted message including err.is_panic().
  4. 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

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


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
                .subscriber

View on GitHub (pinned to afd9284352)