taipei

What is taipei

Taipei is a library that integrates with Tower to enable writing servers that behave well under stress without tuning.

The examples here assume some familiarity with Tokio and Tower. Tokio is the most commonly used runtime for writing asynchronous Rust programs. It is often used in servers, allowing a single CPU thread to handle multiple requests and connections. Tower provides a Service trait that acts as an interface for composing reliability abstractions. For example, a Tower middleware layer might add retries. Taipei's functionality is implemented as Tower layers. That is, we take your service as a my_service: impl Service and wrap it with additional functionality to create my_reliable_service: impl Service. Whilst the examples use Tokio, they could be adapted to work with other runtimes, and the same principles can be applied in any language.

Here is a full example of a reliable server:

use axum::{error_handling::HandleErrorLayer, routing::get, Router};
use http::StatusCode;
use taipei::backpressure::{CpuBackpressureLayer, InstrumentedRuntime as _};
use taipei::queue::{QueueError, QueueLayer};
use taipei::tokio::InstrumentedTokioRuntime;
use tower::{make::Shared, ServiceBuilder};

fn main() -> anyhow::Result<()> {
    // instrument tokio's CPU usage
    let instrumented = InstrumentedTokioRuntime::new()?;
    let instr = instrumented.instrumentation();
    let handle = instrumented.runtime.handle().clone();

    let my_service = Router::new().route("/", get(|| async { "hello" }));

    // hold requests while CPU usage is above 50%
    let inner = ServiceBuilder::new()
        .layer(CpuBackpressureLayer::new(&instr))
        .service(my_service);
    let (service, worker) = QueueLayer::new().build(inner, handle.clone());

    // handle timeout in queue and other errors
    let service = Shared::new(
        ServiceBuilder::new()
            .layer(HandleErrorLayer::new(|e: QueueError| async move {
                (StatusCode::SERVICE_UNAVAILABLE, e.to_string())
            }))
            .service(service),
    );

    instrumented.runtime.block_on(async move {
        handle.spawn(worker.serve());
        let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
        axum::serve(listener, service).await?;
        Ok(())
    })
}

And a full visualization

taipei setup
// A manually picked limit (tune below)
let inner = ServiceBuilder::new()
    .layer(DynamicConcurrencyLimitLayer::new(limit))
    .service(my_service);
QueueLayer::with_custom_queue_timeout(queue_timeout).build(inner, handle)
await
run queue
QUEUE · 0 · unbounded
head wait 0 ms
queue timeout
limit 24
CPU · busy 0/8 (0%)
offered 0.0/s · queue timeout queue timeout 0
goodput 0.0/s · success 0 · response timeout 0
processing timeout 0
in-flight 0 · last latency — ms
0 ready
0 sleeping
TCP SYN · 0 arrived
0 pending
offered throughputgoodputreq/s, 5 s
in-flightqueue depth
tcp synqueuedreadyon cpuin iosuccessqueue timeoutproc timeout
simulation
client
server

We'll walk through why each component exists step-by-step and how we can do better than a manually tuned concurrency limit.