Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

driver-adapters: remove dispose method for JS transaction interface #4489

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ mod prisma_7010;
mod prisma_7072;
mod prisma_7434;
mod prisma_8265;
mod prisma_engines_4286;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use query_engine_tests::*;

#[test_suite(schema(generic), only(JS, Sqlite))]
Copy link
Member Author

@aqrln aqrln Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS-only because after adding the test turned out that it's actually broken with native SQLite connector, this scenario has never been tested before (except indirectly via smoke tests for driver adapters which uncovered the issue back then):

[2023-11-22T14:21:52Z] called `Result::unwrap()` on an `Err` value: QueryCoreError(ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError(SqliteError { extended_code: 1, message: Some("cannot start a transaction within a transaction") }), transient: false }))

I'll open an issue with TypeScript reproduction, and we can fix it separately.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix might be to move the logic added in this PR somewhere to the level above, let me know if you'd prefer it to be done in this PR rather than separately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that should maybe be blanket Drop for Transaction from quaint? If that's easy enough to do, then I'd prefer to do it here and fix the issue for all driver, otherwise follow-up should be fine too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that should maybe be blanket Drop for Transaction from quaint?

Unfortunately this violates orphan rules, and if it didn't, the fact that we also need some state and not just the Drop impl could make this a little awkward. I think it would be better to wrap the quaint transaction into some struct that will take care of this. Let's do that as a follow-up so we can land this PR already.

aqrln marked this conversation as resolved.
Show resolved Hide resolved
mod sqlite {
#[connector_test]
async fn close_tx_on_error(runner: Runner) -> TestResult<()> {
// Try to open a transaction with unsupported isolation error in SQLite.
let result = runner.start_tx(2000, 5000, Some("ReadUncommitted".to_owned())).await;
assert!(result.is_err());

// Without the changes from https://github.com/prisma/prisma-engines/pull/4286 or
// https://github.com/prisma/prisma-engines/pull/4489 this second `start_tx` call will
// either hang infinitely with libSQL driver adapter, or fail with a "cannot start a
// transaction within a transaction" error.
// A more future proof way to check this would be to make both transactions EXCLUSIVE or
// IMMEDIATE if we had control over SQLite transaction type here, as that would not rely on
// both transactions using the same connection if we were to pool multiple SQLite
// connections in the future.
let tx = runner.start_tx(2000, 5000, None).await?;
runner.rollback_tx(tx).await?.unwrap();
aqrln marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}
}
4 changes: 4 additions & 0 deletions query-engine/driver-adapters/src/async_js_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ where
.map_err(into_quaint_error)?;
js_result.into()
}

pub(crate) fn as_raw(&self) -> &ThreadsafeFunction<ArgType, ErrorStrategy::Fatal> {
&self.threadsafe_fn
}
}

impl<ArgType, ReturnType> FromNapiValue for AsyncJsFunction<ArgType, ReturnType>
Expand Down
49 changes: 42 additions & 7 deletions query-engine/driver-adapters/src/proxy.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use std::borrow::Cow;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};

use crate::async_js_function::AsyncJsFunction;
use crate::conversion::JSArg;
use crate::transaction::JsTransaction;
use metrics::increment_gauge;
use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction};
use napi::{JsObject, JsString};
use napi_derive::napi;
use quaint::connector::ResultSet as QuaintResultSet;
Expand Down Expand Up @@ -52,9 +52,8 @@ pub(crate) struct TransactionProxy {
/// rollback transaction
rollback: AsyncJsFunction<(), ()>,

/// dispose transaction, cleanup logic executed at the end of the transaction lifecycle
/// on drop.
dispose: ThreadsafeFunction<(), ErrorStrategy::Fatal>,
/// whether the transaction has already been committed or rolled back
closed: AtomicBool,
}

/// This result set is more convenient to be manipulated from both Rust and NodeJS.
Expand Down Expand Up @@ -581,34 +580,70 @@ impl TransactionProxy {
pub fn new(js_transaction: &JsObject) -> napi::Result<Self> {
let commit = js_transaction.get_named_property("commit")?;
let rollback = js_transaction.get_named_property("rollback")?;
let dispose = js_transaction.get_named_property("dispose")?;
let options = js_transaction.get_named_property("options")?;

Ok(Self {
commit,
rollback,
dispose,
options,
closed: AtomicBool::new(false),
})
}

pub fn options(&self) -> &TransactionOptions {
&self.options
}

/// Commits the transaction via the driver adapter.
///
/// ## Cancellation safety
///
/// The future is cancellation-safe as long as the underlying Node-API call
/// is cancellation-safe and no new await points are introduced between storing true in
/// [`TransactionProxy::closed`] and calling the underlying JS function.
///
/// - If `commit` is called but never polled or awaited, it's a no-op, the transaction won't be
/// committed and [`TransactionProxy::closed`] will not be changed.
///
/// - If it is polled at least once, `true` will be stored in [`TransactionProxy::closed`] and
/// the underlying FFI call will be delivered to JavaScript side in lockstep, so the destructor
/// will not attempt rolling the transaction back even if the `commit` future was dropped while
/// waiting on the JavaScript call to complete and deliver response.
pub async fn commit(&self) -> quaint::Result<()> {
self.closed.store(true, Ordering::Release);
self.commit.call(()).await
}

/// Rolls back the transaction via the driver adapter.
///
/// ## Cancellation safety
///
/// The future is cancellation-safe as long as the underlying Node-API call
/// is cancellation-safe and no new await points are introduced between storing true in
/// [`TransactionProxy::closed`] and calling the underlying JS function.
///
/// - If `rollback` is called but never polled or awaited, it's a no-op, the transaction won't be
/// rolled back yet and [`TransactionProxy::closed`] will not be changed.
///
/// - If it is polled at least once, `true` will be stored in [`TransactionProxy::closed`] and
/// the underlying FFI call will be delivered to JavaScript side in lockstep, so the destructor
/// will not attempt rolling back again even if the `rollback` future was dropped while waiting
/// on the JavaScript call to complete and deliver response.
pub async fn rollback(&self) -> quaint::Result<()> {
self.closed.store(true, Ordering::Release);
self.rollback.call(()).await
}
}

impl Drop for TransactionProxy {
fn drop(&mut self) {
if self.closed.swap(true, Ordering::Acquire) {
aqrln marked this conversation as resolved.
Show resolved Hide resolved
return;
}

_ = self
.dispose
.rollback
.as_raw()
.call((), napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking);
aqrln marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand Down
Loading