Skip to content

Conversation

flyingsilverfin
Copy link
Member

@flyingsilverfin flyingsilverfin commented Sep 26, 2025

Usage and product changes

We notice that calling transaction.close() does not wait until the server has freed up resource. This makes quick sequences, such as tests where transactions open and are followed by database deletes, unreliable. Further investigation that workarounds using the existing on_close callbacks in Python and Java caused segfaults. We fix both:

  1. Transaction.close() in Python and Java now blocks for 1 round trip. We have the option to convert this fully into a promise return value the user can optionally resolve. We should pick the most relevant default!
  2. We fix segfaults that occur when the Rust driver calls into Python/Java once the user attaches .on_close callbacks to transactions.

Implementation

  • Fix and enhance on_close callbacks:

    • on attaching a callback, we don't return until the callback is actually registered (used to submit into an async channel, but not necessarily be recorded)
      • this is also sped up by having the lowest-level registration listener listen in an async context instead of a polling context
    • we fix calling segfault that occurred on invoking the callback from Rust, mostly by enabling threading from the SWIG .i layer!
  • Make close() a blocking operation in Java and Python, which awaits a signal from the server that the transaction is actually closed and the resources are freed up.

  • We add on_close callback integration tests for Python, Java, and Rust

* under the License.
*/

%module(threads=1) typedb_driver
Copy link
Member Author

Choose a reason for hiding this comment

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

Key point! Since we're calling transaction close callbacks from another thread, we need this to avoid segfaults (not sure why - perhaps SWIG manages the GIL correctly for Python in this case!)

Copy link
Member

Choose a reason for hiding this comment

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

:O

Copy link
Member

Choose a reason for hiding this comment

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

How did it work in 2.x? Or did it?..

virtual ~TransactionCallbackDirector() {}
virtual void callback(Error*) = 0;
virtual ~TransactionCallbackDirector() {
std::cout << "Callback::~Callback()" << std:: endl;
Copy link
Member Author

Choose a reason for hiding this comment

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

helpful in case things aren't working as expected

)

py_test(
name = "test_driver",
Copy link
Member Author

@flyingsilverfin flyingsilverfin Sep 30, 2025

Choose a reason for hiding this comment

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

good place to leave future integration tests

if (nativeObject.isOwned()) {
try {
transaction_force_close(nativeObject);
transaction_force_close(nativeObject).get();
Copy link
Member Author

Choose a reason for hiding this comment

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

Java: force resolve close(). We could also let the user resolve it themselves?

)

typedb_java_test(
name = "test-driver",
Copy link
Member Author

Choose a reason for hiding this comment

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

new place to put java specific integration tests

impl Drop for TransactionTransmitter {
fn drop(&mut self) {
self.force_close();
// TODO: in the async context, this now returns a promise... do we care?
Copy link
Member Author

Choose a reason for hiding this comment

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

Big open question @dmitrii-ubskii !

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok: looks like the typical recommended thing to do is make it fire-and-forget. So basically users can call .force_close() if they want to await a promise first, then they can drop it if they want to be super sure in Rust.

Copy link
Member Author

Choose a reason for hiding this comment

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

Java/Python only use force_close() so they're blocking in GC anyway


pub(in crate::connection) fn force_close(&self) {
#[cfg(not(feature = "sync"))]
pub(in crate::connection) fn force_close(&self) -> impl Promise<'static, Result<()>> {
Copy link
Member Author

Choose a reason for hiding this comment

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

Please validate these two functions!

Comment on lines 166 to 168
let (sender, mut sink) = unbounded_async();
self.on_close_register_sink.send((Box::new(callback), sender)).ok();
sink.blocking_recv().expect("Did not receive on_close registration success signal");
Copy link
Member Author

Choose a reason for hiding this comment

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

we now don't return to the user unti the on_close is actually registered

Comment on lines +289 to +291
move || Self::sync_dispatch_loop(queue_source, request_sink, collector, shutdown_signal)
});
tokio::spawn(Self::listen_loop(response_source, collector, shutdown_sink));
tokio::spawn(Self::async_listen_loop(response_source, collector, on_close_callback_source, shutdown_sink));
Copy link
Member Author

Choose a reason for hiding this comment

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

Since we just have to get the on_close_callback into the Collector in some way, we can choose whether we lsiten to the channel in the async listen loop or the sync dispatch loop. To minimize waits, we're going to let Tokio manage it in the Async loop.

Comment on lines 81 to 82
pub(crate) fn force_close(&self) -> impl Promise<'static, Result<()>> {
self.transaction_transmitter.force_close()
Copy link
Member Author

Choose a reason for hiding this comment

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

Note: in commit and rollback, we do something this:

let promise = self.single(TransactionRequest::Commit);
        promisify! { require_transaction_response!(resolve!(promise), Rollback)

Do we want to do anything like this here?

}
}));

drop(transaction); // TODO: drop isn't blocking... so we need to spin?
Copy link
Member Author

Choose a reason for hiding this comment

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

Drop question comes in here!

@flyingsilverfin flyingsilverfin changed the title Improve Rust Transaction on_close_callback Fix transaction on_close and Java and Python block on close() Sep 30, 2025
@flyingsilverfin flyingsilverfin marked this pull request as ready for review September 30, 2025 18:40
WIP

Finally fixed on_close callback - threading issue

Convert close to promise and resolve it in Python and Java

Add Java onclose callback test

Cleanup test

Add mutex
@flyingsilverfin flyingsilverfin force-pushed the improve-on-close-callback branch from 8fb5f1e to e907782 Compare September 30, 2025 19:30
"@typedb_bazel_distribution//platform:is_linux_x86_64": "@typedb_artifact_linux-x86_64//file",
"@typedb_bazel_distribution//platform:is_mac_arm64": "@typedb_artifact_mac-arm64//file",
"@typedb_bazel_distribution//platform:is_mac_x86_64": "@typedb_artifact_mac-x86_64//file",
# "@typedb_bazel_distribution//platform:is_windows_x86_64": "@typedb_artifact_windows-x86_64//file",
Copy link
Member

Choose a reason for hiding this comment

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

Can be uncommented already

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants