1 unstable release

0.1.0 Oct 17, 2023

#2675 in Database interfaces

MIT license

22KB
360 lines

async-bb8-diesel

This crate provides an interface for asynchronously accessing a bb8 connection pool atop Diesel.

This is accomplished by implementing an async version of Diesel's "RunQueryDsl" trait, aptly named "AsyncRunQueryDsl", which operates on an async-compatible connection. When called from an async context, these operations transfer the query to a blocking tokio thread, where it may be executed.

Pre-requisites

  • The yet-to-be-released Diesel 2.0. Although not strictly a requirement, the core functionality of this crates relies on an ability to transfer queries from an async task to a blocking thread. This frequently requires the "Send" trait, which is only partially implemented for statements in Diesel 1.x.
  • A willingness to tolerate some instability. This crate is effectively a stop-gap until more native asynchronous support exists within Diesel.

Comparisons with existing crates

This crate was heavily inspired by both tokio-diesel and bb8-diesel, but serves a slightly different purpose.

What do those crates do?

Both of those crates rely heavily on the tokio::block_in_place function to actually execute synchronous Diesel queries.

Their flow is effectively:

  • A query is issued (in the case of tokio-diesel, it's async. In the case of bb8-diesel, it's synchronous - but you're using bb8, so presumably calling from an asynchronous task).
  • The query and connection to the DB are moved into the block_in_place call.
  • Diesel's native synchronous API is used within block_in_place.

These crates have some advantages by taking this approach:

  • The tokio executor knows not to schedule additional async tasks for the duration of the block_in_place call.
  • The callback used within block_in_place doesn't need to be Send - it executes synchronously within the otherwise asynchronous task.

However, they also have some downsides:

  • The call to block_in_place effectively pauses an async thread for the duration of the call. This requires a multi-threaded runtime, and reduces efficacy of one of these threads for the duration of the call.
  • The call to block_in_place starves all other asynchronous code running in the same task.

This starvation results in some subtle inhibition of other futures, such as in the following example, where a timeout would be ignored if a long-running database operation was issued from the same task as a timeout.

tokio::select! {
  // Calls "tokio::block_in_place", doing a synchronous Diesel operation
  // on the calling thread...
  _ = perform_database_operation() => {},
  // ... meaning this asynchronous timeout cannot complete!
  _ = sleep_until(timeout) = {},
}

What does this crate do?

This crate attempts to avoid calls to block_in_place - which would block the calling thread - and prefers to use tokio::spawn_blocking function. This function moves the requested operation to an entirely distinct thread where blocking is acceptable, but does not prevent the current task from executing other asynchronous work.

This isn't entirely free - as this work now needs to be transferred to a new thread, it imposes a "Send + 'static" constraint on the queries which are constructed.

Which one is right for me?

  • If you care about preserving typically expected semantics for asynchronous operations, we recommend this crate.
  • If you don't - maybe you have an asynchronous workload, but you know when you can block those threads - you can use either of the tokio-diesel or bb8-diesel crates, depending on whether or not you want access to the asynchronous thread pool.

Dependencies

~5–12MB
~127K SLoC