Course contentsShow
← Rust
Lesson 1275 of 1,91011. Asynchronous ProgrammingFree lesson

Async Connection Pooling

Construct an efficient connection pool that yields connections to tasks without blocking.

In this lesson, you will learn how to build an efficient, non-blocking connection pool that enables multiple asynchronous tasks to share a limited set of network resources.

The Problem with Naive Pooling

In a high-concurrency system, you cannot create a new database connection for every incoming request. Opening a TCP connection is expensive and latency-heavy. However, if you use a standard Mutex<Vec<Connection>> to manage a pool, a task requesting a connection might block the entire thread if the pool is empty, violating the core principle of non-blocking async design.

The Non-Blocking Approach

To yield connections without blocking, we combine a Channel (for storage) and a Semaphore (for capacity management). Think of the Semaphore as a "bouncer" at the door of the connection pool: it tracks how many slots are available without locking the resources themselves.

When a task needs a connection:

  1. It calls acquire() on the Semaphore. This is an await point. If no connections are available, the task suspends and yields control to the executor, allowing other work to continue.
  2. Once the semaphore grants access, the task pops a connection from an async channel.
  3. Once finished, the task pushes the connection back into the channel and releases the semaphore permit.
use tokio::sync::{Semaphore, mpsc};

pub struct ConnectionPool<T> {
    connections: mpsc::Receiver<T>,
    sender: mpsc::Sender<T>,
    permit: Semaphore,
}

impl<T> ConnectionPool<T> {
    pub async fn get(&self) -> T {
        // Wait for a slot to open up without blocking the executor
        let _permit = self.permit.acquire().await.unwrap();
        self.connections.recv().await.unwrap()
    }
}

Why This is Efficient

By using a Semaphore combined with an mpsc channel, we transform a resource-contention problem into a scheduling problem. The executor is never stalled; it simply manages the task's wake-up state once a connection becomes available. This architecture ensures that your application throughput remains high, even when database latency spikes, because the tasks waiting for connections are effectively "parked" by the runtime rather than "blocking" a thread.

Key Takeaway: Use a Semaphore to regulate access to a channel-based resource pool to ensure tasks suspend asynchronously rather than blocking system threads.