Skip to content

pool.get() can acquire more connections than are necessary #221

Closed
@tneely

Description

@tneely

Issue

It's possible to trigger more approvals than are necessary, in turn grabbing more connections than we need. This happens when we drop a connection. The drop produces a notify, which doesn't get used until the pool is empty. The first Pool::get() call on an empty pool will spawn an connect task, immediately complete notify.notified().await, then spawn a second connect task. Both will connect and we'll end up with 1 more connection than we need.

This gets worse if one of those connections is broken when it gets returned to the pool. We end up calling notify.notify_waiters();, which means all connections in the PoolInner::get() loop could spawn an extra connect task. This should just be a single notify.notify_one(); call.

self.inner.notify.notify_waiters();

Affected Versions

I have only tested on 0.8.5

Fix

Not sure on this one. I think removing the notify.notify_one() call from the drop path would fix this specific issue, but it would hurt performance and could cause other issues (e.g. if the pool is at max capacity and we have no inflight connect tasks).

We should definitely change notify.notify_waiters() -> notify.notify_one() in the broken connection case if possible.

Reproduction

#[tokio::test]
async fn test_notify() {
    let pool = Pool::builder()
        .min_idle(0)
        .max_size(100)
        .queue_strategy(QueueStrategy::Lifo)
        .build(OkManager::<FakeConnection>::new())
        .await
        .unwrap();

    // The first get should
    // 1) see nothing in the pool,
    // 2) spawn a single connect task,
    // 3) get notified of the new connection and grab it from the pool
    let conn_0 = pool.get().await.expect("should connect");
    // Dropping the connection queues up a notify
    drop(conn_0);
    // The second get should
    // 1) see the first connection in the pool and grab it
    // NB: This does not consume the notify
    let _conn_1: PooledConnection<OkManager<FakeConnection>> = pool.get().await.expect("should connect");
    // The third get will
    // 1) see nothing in the pool,
    // 2) spawn a single connect task,
    // 3) get notified from the dropped connection
    // 4) see nothing in the pool,
    // 5) spawn a single connect task (oh no!),
    // 6) get notified of the new connection and grab it from the pool
    let _conn_2: PooledConnection<OkManager<FakeConnection>> = pool.get().await.expect("should connect");

    assert_eq!(pool.state().connections, 2);
}
thread 'test_notify' panicked at bb8/tests/test.rs:1160:5:
assertion `left == right` failed
  left: 3
 right: 2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions