Actix blocks on the same browser instance but doesn't across browsers

Actix processes blocking request handlers blockingly if I send them from different tabs in the same browser, but seems doesn't block across two different browsers.
What I do is send get requests from Chrome to block_me from adjacent tabs versus one request from Chrome and one from Firefox.

Is this because a new session means a new worker is spawned? If so will it start blocking if I exceed available_parallelism? Thanks.

use actix_web::{web, App, HttpResponse, HttpServer, Responder};

async fn blocking_handler() -> impl Responder {
    println!("blocking request received");
    std::thread::sleep(std::time::Duration::from_secs(10));
    "Blocking handler finally returns"
}

async fn non_blocking_handler() -> impl Responder {
    println!("NON blocking request received");
    tokio::time::sleep(std::time::Duration::from_secs(10)).await;
    "NON-Blocking handler finally returns"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(||
        App::new()
            .route("/", web::get().to(HttpResponse::Ok))
            .route("/block_me", web::get().to(blocking_handler))
            .route("/non_block_me", web::get().to(non_blocking_handler))
    )
    .bind(("127.0.0.1", 9090))?
    .run()
    .await
}

No, actix starts with a set of worker threads which then handle incoming requests. The actix-web runtime is a pool of threads where each thread runs a local, single-threaded tokio instance. Unlike a multithreaded tokio runtime, this doesn't allow work-stealing where one worker steals tasks from another worker when idle. This means that a single connection will always be handled by the same (potentially blocked) worker. Looks to me like Chrome reuses the same connection over multiple tabs, where if you use two different browsers the same connection can't be reused and both connections (from Chrome and Firefox) are handled by different workers.

1 Like

Thanks. So it should start blocking when 10000 users with different connections hit the server at the same-ish time? Because maybe workers are OS threads, not green threads. Or blocking is exclusive to same connection?

Workers are OS threads. Blocking them with one connection will make them unresponsive to other connections.

std::thread::sleep in async fn just breaks the async runtime, and you can get all kinds of erratic behavior from it.

In this case, it probably prevents handling other requests on the same TCP connection, but not requests coming from other TCP connections. But that behavior is not guaranteed, it's just an accident of how futures happen to be ordered, or exposes implementation details that don't matter in correctly-working async setup.

If you need to block in an async function, use something like spawn_blocking, or at very least tokio's block_in_place (but block_in_place is a hack, and may not always help either).

1 Like