sql: organize connection pool as a stack rather than a queue

When connection pool is organized as a stack,
it always returns most recently used connection.
Because each connection has its own page cache,
using the connection with fresh cache improves performance.

I commented out `oauth2::tests::test_oauth_from_mx`
because it requires network connection,
turned off the Wi-Fi and ran the tests.

Before the change, with a queue:
```
$ hyperfine "cargo test"
Benchmark 1: cargo test
  Time (mean ± σ):     56.424 s ±  4.515 s    [User: 183.181 s, System: 128.156 s]
  Range (min … max):   52.123 s … 68.193 s    10 runs
```

With a stack:
```
$ hyperfine "cargo test"
Benchmark 1: cargo test
  Time (mean ± σ):     29.887 s ±  1.377 s    [User: 101.226 s, System: 45.573 s]
  Range (min … max):   26.591 s … 31.010 s    10 runs
```

On version 1.107.1:
```
$ hyperfine "cargo test"
Benchmark 1: cargo test
  Time (mean ± σ):     43.658 s ±  1.079 s    [User: 202.582 s, System: 50.723 s]
  Range (min … max):   41.531 s … 45.170 s    10 runs
```
This commit is contained in:
link2xt
2023-02-20 14:02:19 +00:00
parent 840497d356
commit 2eeacb0f8a
4 changed files with 23 additions and 14 deletions

View File

@@ -4,6 +4,8 @@
### Changes
- use transaction in `Contact::add_or_lookup()` #4059
- Organize the connection pool as a stack rather than a queue to ensure that
connection page cache is reused more often. #4065
### Fixes

2
Cargo.lock generated
View File

@@ -848,7 +848,6 @@ dependencies = [
"bitflags",
"chrono",
"criterion",
"crossbeam-queue",
"deltachat_derive",
"email",
"encoded-words",
@@ -870,6 +869,7 @@ dependencies = [
"num-traits",
"num_cpus",
"once_cell",
"parking_lot",
"percent-encoding",
"pgp",
"pretty_env_logger",

View File

@@ -38,7 +38,6 @@ backtrace = "0.3"
base64 = "0.21"
bitflags = "1.3"
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
crossbeam-queue = "0.3"
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
@@ -58,6 +57,7 @@ num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.17.0"
percent-encoding = "2.2"
parking_lot = "0.12"
pgp = { version = "0.9", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
qrcodegen = "1.7.0"

View File

@@ -1,10 +1,18 @@
//! Connection pool.
//! # SQLite connection pool.
//!
//! The connection pool holds a number of SQLite connections and allows to allocate them.
//! When allocated connection is dropped, underlying connection is returned back to the pool.
//!
//! The pool is organized as a stack. It always allocates the most recently used connection.
//! Each SQLite connection has its own page cache, so allocating recently used connections
//! improves the performance compared to, for example, organizing the pool as a queue
//! and returning the least recently used connection each time.
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Weak};
use anyhow::{Context, Result};
use crossbeam_queue::ArrayQueue;
use parking_lot::Mutex;
use rusqlite::Connection;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
@@ -12,7 +20,7 @@ use tokio::sync::{OwnedSemaphorePermit, Semaphore};
#[derive(Debug)]
struct InnerPool {
/// Available connections.
connections: ArrayQueue<Connection>,
connections: Mutex<Vec<Connection>>,
/// Counts the number of available connections.
semaphore: Arc<Semaphore>,
@@ -23,7 +31,9 @@ impl InnerPool {
///
/// The connection could be new or returned back.
fn put(&self, connection: Connection) {
self.connections.force_push(connection);
let mut connections = self.connections.lock();
connections.push(connection);
drop(connections);
}
}
@@ -74,22 +84,19 @@ pub struct Pool {
impl Pool {
/// Creates a new connection pool.
pub fn new(connections: Vec<Connection>) -> Self {
let semaphore = Arc::new(Semaphore::new(connections.len()));
let inner = Arc::new(InnerPool {
connections: ArrayQueue::new(connections.len()),
semaphore: Arc::new(Semaphore::new(connections.len())),
connections: Mutex::new(connections),
semaphore,
});
for connection in connections {
inner.connections.force_push(connection);
}
Pool { inner }
}
/// Retrieves a connection from the pool.
pub async fn get(&self) -> Result<PooledConnection> {
let permit = self.inner.semaphore.clone().acquire_owned().await?;
let conn = self
.inner
.connections
let mut connections = self.inner.connections.lock();
let conn = connections
.pop()
.context("got a permit when there are no connections in the pool")?;
let conn = PooledConnection {