-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: fix buggy behaviour when cloning request extensions
Our naive update for axum@0.7 / hyper@1.0 led to buggy behaviour whereby the `Tx` extractor would always fail with `OverlappingExtractors` if there were any outstanding clones of the request extensions (see the new test). Since request extensions now must all implement `Clone`, it's possible that some middleware might wish to keep a clone of all request extensions (e.g. for request inspection/tracing/debugging), rendering `Tx` unusable with those middleware. To fix it, we simplify the synchronisation by implementing `Clone` for `Slot` and creating new `Extension<DB>` and `LazyTransaction<DB>` types to replace `TxSlot<DB>` and `Lazy<DB>`. `Slot<T>` is a wrapper around an `Arc<Mutex<Option<T>>>`, and as such it can trivially implement `Clone` (there was some "pit of success" considerations with the previous API intended to enforce proper usage, but that is unnecesarily limiting given the underlying `Mutex`). The `Extension<DB>` holds a `Slot` containing a `LazyTransaction<DB>`. `Extension<DB>` is trivially clonable since `Slot` itself is. The `LazyTransaction<DB>` then implements a simple "lazily acquired transaction" protocol, making use of normal rust ownership and borrowing rules to manage the transaction (i.e. it has no internal synchronisation). This makes the overall synchronisation picture much simpler: the middleware future and all clones of the request extension hold a reference to the same `Slot`. The `Tx` extractor obtains its copy of the request extension and attempts to `lease` the inner `LazyTransaction`, failing with `OverlappingExtractors` if the lease is already taken (this is the only public invocation of `lease`, and so overlapping extractors can be the only* cause of an absent transaction). If the lease is successful, the extractor can acquire a transaction (if there's not one already) and package it up for request handlers to then interact with. * Technically the transaction can be "stolen" from the `Tx` extractor by committing explicitly, but this considered to create an endless "overlap" in the current semantics. The main caveat of this approach seems to be that the `Tx` extractor no longer has type-level knowledge that it can access a `Transaction` - the `Transaction` can only be accessed by matching on the `LazyTransaction` state. This doesn't affect the API, but it could make bumping into a panic more likely, or there may be performance implications (though these would likely be dwarfed by the I/O involved in interacting with a database).
- Loading branch information
Showing
6 changed files
with
176 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
use sqlx::Transaction; | ||
|
||
use crate::{ | ||
slot::{Lease, Slot}, | ||
Error, Marker, State, | ||
}; | ||
|
||
/// The request extension. | ||
pub(crate) struct Extension<DB: Marker> { | ||
slot: Slot<LazyTransaction<DB>>, | ||
} | ||
|
||
impl<DB: Marker> Extension<DB> { | ||
pub(crate) fn new(state: State<DB>) -> Self { | ||
let slot = Slot::new(LazyTransaction::new(state)); | ||
Self { slot } | ||
} | ||
|
||
pub(crate) async fn acquire(&self) -> Result<Lease<LazyTransaction<DB>>, Error> { | ||
let mut tx = self.slot.lease().ok_or(Error::OverlappingExtractors)?; | ||
tx.acquire().await?; | ||
|
||
Ok(tx) | ||
} | ||
|
||
pub(crate) async fn resolve(&self) -> Result<(), sqlx::Error> { | ||
if let Some(tx) = self.slot.lease() { | ||
tx.steal().resolve().await?; | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl<DB: Marker> Clone for Extension<DB> { | ||
fn clone(&self) -> Self { | ||
Self { | ||
slot: self.slot.clone(), | ||
} | ||
} | ||
} | ||
|
||
/// The lazy transaction. | ||
pub(crate) struct LazyTransaction<DB: Marker>(LazyTransactionState<DB>); | ||
|
||
enum LazyTransactionState<DB: Marker> { | ||
Unacquired { | ||
state: State<DB>, | ||
}, | ||
Acquired { | ||
tx: Transaction<'static, DB::Driver>, | ||
}, | ||
} | ||
|
||
impl<DB: Marker> LazyTransaction<DB> { | ||
fn new(state: State<DB>) -> Self { | ||
Self(LazyTransactionState::Unacquired { state }) | ||
} | ||
|
||
pub(crate) fn as_ref(&self) -> &Transaction<'static, DB::Driver> { | ||
match &self.0 { | ||
LazyTransactionState::Unacquired { .. } => { | ||
panic!("BUG: exposed unacquired LazyTransaction") | ||
} | ||
LazyTransactionState::Acquired { tx } => tx, | ||
} | ||
} | ||
|
||
pub(crate) fn as_mut(&mut self) -> &mut Transaction<'static, DB::Driver> { | ||
match &mut self.0 { | ||
LazyTransactionState::Unacquired { .. } => { | ||
panic!("BUG: exposed unacquired LazyTransaction") | ||
} | ||
LazyTransactionState::Acquired { tx } => tx, | ||
} | ||
} | ||
|
||
async fn acquire(&mut self) -> Result<(), sqlx::Error> { | ||
match &self.0 { | ||
LazyTransactionState::Unacquired { state } => { | ||
let tx = state.transaction().await?; | ||
self.0 = LazyTransactionState::Acquired { tx }; | ||
Ok(()) | ||
} | ||
LazyTransactionState::Acquired { .. } => Ok(()), | ||
} | ||
} | ||
|
||
pub(crate) async fn resolve(self) -> Result<(), sqlx::Error> { | ||
match self.0 { | ||
LazyTransactionState::Unacquired { .. } => Ok(()), | ||
LazyTransactionState::Acquired { tx } => tx.commit().await, | ||
} | ||
} | ||
|
||
pub(crate) async fn commit(self) -> Result<(), sqlx::Error> { | ||
match self.0 { | ||
LazyTransactionState::Unacquired { .. } => { | ||
panic!("BUG: tried to commit unacquired transaction") | ||
} | ||
LazyTransactionState::Acquired { tx } => tx.commit().await, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -85,6 +85,7 @@ | |
|
||
mod config; | ||
mod error; | ||
mod extension; | ||
mod layer; | ||
mod marker; | ||
mod slot; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters