Diesel Async 0.5

I'm happy to announce the release of diesel-async 0.5.0. This release includes two new features. It introduces a SyncConnectionWrapper struct that provides an AsyncConnection implementation for sync diesel connections. In addition it restores compatibility with diesel 2.2 including the new Instrumentation feature there. Be sure to checkout the full Change log for all details. To support future development efforts, please consider sponsoring me on GitHub. I would like to extend a sincere thank you to all our generous sponsors who have contributed to making this release possible. A special thanks goes to Mohamed Belaouad and Wattsense for contributing the SyncConnectionWrapper implementation.

Sync Connection Wrapper

Diesel-async 0.5 introduces a SyncConnectionWrapper type that transforms an existing sync diesel::Connection into an async diesel_async::AsyncConnection by using tokio::spawn_blocking. We plan to relax the dependency on tokio in future versions by making the blocking function configurable. The connection wrapper type itself is written in a generic way to support any existing diesel Connection as long as it implements the required helper traits. At the time of writing this is only satisfied by diesel::SqliteConnection as this fills a gap in diesel-async by providing a way to use the SQLite backend there. Again we want to expand the set of supported connection types here in the future.

This feature is gated by the sync-connection-wrapper feature.

Example usage:

use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use diesel_async::sync_connection_wrapper::SyncConnectionWrapper;
use diesel_async::{AsyncConnection, RunQueryDsl};

let db_url = std::env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set");

let mut sync_wrapper =  SyncConnectionWrapper::<SqliteConnection>::establish(&db_url).await?;

// use the normal diesel DSL via diesel-async
diesel::delete(users::table)
    .execute(&mut sync_wrapper)
    .await?;

diesel::insert_into(users::table)
    .values((users::id.eq(3), users::name.eq("toto")))
    .execute(&mut sync_wrapper)
    .await?;

let data: Vec<User> = users::table
    .select(User::as_select())
    .load(&mut sync_wrapper)
    .await?;

The implementation of SyncConnectionWrapper obviously introduces some performance overhead, so it's worth to talk a bit about performance here. The diesel/metrics project on GitHub tracks the performance of various Rust database crates over time. Since today it also includes a diesel_async::SyncConnectionWrapper<SqliteConnection> variant. This allows us to easily compare the performance to:

  • Diesel itself, to estimate the overhead introduces by this wrapper
  • Other async database crates, to understand how well our approach fares in the general ecosystem.
Results of the "trivial query" benchmark from diesel for various input sizes for the SQLite backend

Results of the "trivial query" benchmark from diesel for different input sizes for the SQLite backend.

This picture shows us the result from the (at the time of writing) latest benchmark run on GitHub actions. As this is inherently a noisy environment it only allows us to spot large differences in performance, so please rely on the results of your own benchmark runs if you need depend on finer differences. The code for these benchmarks is part of the diesel-repo For the sake of this text, these results are sufficient as we are only interested in larger differences, so this results give us a sufficient overview.

The image shows 5 subplots, which corresponds to the benchmark results for numbers of results returned by the same query, which range from 1 to 10.000 entries. Each plot contains results for the following crates:

As expected, all async variants are slower for all return set sizes as SQLite only provides an interface that is inherently synchronous. Any asynchronous implementation needs to perform additional work so it's expected that they are slower.

In comparison to a plain diesel::SqliteConnection the SyncConnectionWrapper<SqliteConnection> variant introduces at least 2x, but up to 10x overhead. In comparison to other async variants, like those provided by sqlx and SeaORM, the SyncConnectionWrapper<SqliteConnection> variant performs with a similar performance level for small response sizes and is up to 4x faster for larger response sizes. This result demonstrate that the new connection wrapper a competitive alternative in comparison to the existing async solutions. Checkout the metrics repository for more benchmarks.

This raises the question, in which cases you should use this connection implementation? I personally believe it is only useful for cases where you want to support several database backends, including SQLite and you already use diesel-async for supporting the other backends. For cases where you only want to support SQLite you should consider if you need an async wrapper at all. SQLite queries can easily stay below the 10 to 100 microseconds deadline that is cite by famous What is blocking blog post from Alice Ryhl. For more complex queries it's always possible to use something like spawn_blocking directly on case by case basis instead of introducing a larger overhead for all your queries.

Instrumentation support

Diesel-async 0.5 updates its diesel dependency to restore compatibility with the latest diesel 2.2 release. As part of this update diesel-async now supports the instrumentation of connections via the same Instrumentation trait that is used by diesel. This hopefully enables the creation of a set of bridge crates, to provide connection instrumentation via crates like tracing or log and that can be used by the whole diesel ecosystem.

let mut conn = AsyncPgConnection::establish(&database_url).await.unwrap();

// note: the explicit type in the closure signature is required due to a rustc bug
connection.set_instrumentation(|event: InstrumentationEvent<'_>| {
    // This just prints every possible event
    // A real world implementation wants to filter events here
    // and possibly use a crate like `tracing` or `log` to forward 
    // these events to the general logging infrastructure of the application
    println!("{event:?}")
});