Skip to content

libsql: remote hrana transactions#653

Merged
LucioFranco merged 12 commits into
tursodatabase:mainfrom
Horusiath:libsql-transactions
Nov 29, 2023
Merged

libsql: remote hrana transactions#653
LucioFranco merged 12 commits into
tursodatabase:mainfrom
Horusiath:libsql-transactions

Conversation

@Horusiath

Copy link
Copy Markdown
Contributor

This PR introduces transactions in libsql client using http+json. It introduces a Hrana concept of HttpStream, which is opened for sequential execution of subsequent requests.

@Horusiath Horusiath marked this pull request as ready for review November 20, 2023 21:50

@MarinPostma MarinPostma left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reading the code for transactions I'm starting to wonder if it's a good idea to have connection's execute take &self, and be Sync and Send. If you're sharing a connection between threads, you're likely doing something wrong.

On the surface it looks fine from the remote perspective, because a new sqlite connection is created by the server for each execute request, but locally, the connection is backed by the same sqlite connection, so there is an inadequation in the semantics I think. I don't think connection should be shared, and if you really need to, I think the connection itself should be wrapped into a Mutex to synchronize accesses.

The transaction code made that even more evident to me, since we need a mutex around the HTTP stream to serialize requests, when it's clearly a code smell to have sharable access to a running transaction.

Comment on lines +30 to +32
stream.execute(Stmt::new(begin_stmt, false)).await?;
Ok(HttpTransaction { stream })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe the tx open should be batched with the first statement to save a roundtrip?

@Horusiath Horusiath Nov 21, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

On one side yes. On the other - this may screw happen before relationships as opening the transaction won't actually start a new transaction ie.:

  1. Have a database in state S1.
  2. Open transaction T1 (current db state: S1).
  3. Open transaction T2.
  4. Within T2, modify database state S1→S2.
  5. Read something in T1: since this is first time when actual request is being made - T1 will point to S2, even though T1 was "opened" before T2.

Comment thread libsql/src/hrana/transaction.rs Outdated
) -> Result<BatchResult> {
let mut batch = Batch::new();
for stmt in stmts.into_iter() {
batch.step(None, stmt);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe that there should be a conditional execution on the previous step for batches, and a conditional rollback at the end if the last step wasn't a success

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is how it's realised in libsql-client-rs. I'm also using libsql-client-ts for reference, and have problems with that implementation: namely every batch starts with BEGIN ... statement, yet END/COMMIT/ROLLBACK are not attached to batch.

Comment thread libsql/src/hrana/transaction.rs
Comment thread libsql/src/hrana/stream.rs Outdated
Comment thread libsql/src/hrana/stream.rs Outdated
Comment thread libsql/src/hrana/stream.rs Outdated
Comment on lines +42 to +63
#[derive(Serialize, Debug)]
pub struct SequenceStreamReq {
pub sql: Option<String>,
pub sql_id: Option<i32>,
}

#[derive(Serialize, Debug)]
pub struct DescribeStreamReq {
pub sql: Option<String>,
pub sql_id: Option<i32>,
}

#[derive(Serialize, Debug)]
pub struct StoreSqlStreamReq {
pub sql: String,
pub sql_id: i32,
}

#[derive(Serialize, Debug)]
pub struct CloseSqlStreamReq {
pub sql_id: i32,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we really need to factorize the proto types with libsql-server

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not important now but agreed, it would be nice to centralize these structs and we should prob do the same for protobuf (both hrana and gRPC).

Comment thread libsql/src/hrana/hyper.rs Outdated

@LucioFranco LucioFranco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall, really nice work! I left a few comments and some questions.

Comment on lines +11 to +12
where
T: for<'a> HttpSend<'a>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unless the struct refers to an associated type on the trait then we shouldn't have this bound here, it makes defining structs that use this harder.

@Horusiath Horusiath Nov 28, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I didn't defined it because I wanted it to be fancy. I defined it this way, because it was the only way I've found to make borrow checker cooperate. The use case also seems to fit the criteria of this language feature. If there's a better way I'm open for suggestions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What I mean is that if your struct foo doesn't use the trait inside of the struct def you don't need to bound T by the trait. You can just leave it as T. This will then flow throughout the rest of the code since you no longer need to bound T: Trait for each struct that embeds the struct that contains T.

Trait bounds should only be set at the impl of where the function is used. The only time you want to bound the struct generic is if you are using an associated type with in the struct.

Comment on lines +42 to +63
#[derive(Serialize, Debug)]
pub struct SequenceStreamReq {
pub sql: Option<String>,
pub sql_id: Option<i32>,
}

#[derive(Serialize, Debug)]
pub struct DescribeStreamReq {
pub sql: Option<String>,
pub sql_id: Option<i32>,
}

#[derive(Serialize, Debug)]
pub struct StoreSqlStreamReq {
pub sql: String,
pub sql_id: i32,
}

#[derive(Serialize, Debug)]
pub struct CloseSqlStreamReq {
pub sql_id: i32,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not important now but agreed, it would be nice to centralize these structs and we should prob do the same for protobuf (both hrana and gRPC).

pub(super) fn stmts_to_batch(protocol_v3: bool, stmts: impl IntoIterator<Item = Stmt>) -> Batch {
let mut batch = Batch::new();
let mut step = -1;
for stmt in stmts.into_iter() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

could use https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.enumerate maybe here to do the counter for you so you don't need to do that logic manually.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since step always refer to a previous step, I find it easier to read/write than using enumerate.

Comment thread libsql/src/hrana/hyper.rs Outdated
Comment thread libsql/src/hrana/hyper.rs Outdated
Comment thread libsql/src/hrana/stream.rs
Comment on lines +370 to +383
Sql(String),
SqlId(SqlId),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What about String and Remote? Instead of these two names that seem quite similar?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I named them this way, since they map directly onto Hrana protocol fields.

Comment thread libsql/src/hrana/transaction.rs
TransactionBehavior::Deferred => "BEGIN DEFERRED",
TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE",
TransactionBehavior::ReadOnly => "BEGIN READONLY",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We probably need to map this to DEFERRED since I don't think the server supports this syntax. We should make sure we test this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was suspecting that since replication client supports this (this code is copied from there), it should be supported in hrana as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No clue, we should test that it actually works I have a feeling it might not but I could also be wrong.

Comment thread libsql/src/wasm/mod.rs
T: for<'a> HttpSend<'a>,
{
pub async fn query(&self, sql: &str, params: impl IntoParams) -> crate::Result<Rows> {
tracing::trace!("querying `{}`", sql);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: could prefix the logs here like so to make it easier to debug in the future.

Suggested change
tracing::trace!("querying `{}`", sql);
tracing::trace!("txn querying `{}`", sql);

@Horusiath

Copy link
Copy Markdown
Contributor Author

If you don't mind guys, unless you have some strong opinions and must-fix remarks about existing code, we could resolve remaining questions in subsequent PRs. I don't think it's good to have this PR rot. I've created a #723 for topics opened but not covered here.

@LucioFranco LucioFranco changed the title Libsql transactions libsql: remote hrana transactions Nov 29, 2023

@LucioFranco LucioFranco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am approving, I added a few more items in the follow up issue.

@LucioFranco LucioFranco added this pull request to the merge queue Nov 29, 2023
Merged via the queue into tursodatabase:main with commit 6b5606a Nov 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants