libsql: remote hrana transactions#653
Conversation
52f7e4b to
4a7a854
Compare
MarinPostma
left a comment
There was a problem hiding this comment.
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.
| stream.execute(Stmt::new(begin_stmt, false)).await?; | ||
| Ok(HttpTransaction { stream }) |
There was a problem hiding this comment.
Maybe the tx open should be batched with the first statement to save a roundtrip?
There was a problem hiding this comment.
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.:
- Have a database in state S1.
- Open transaction T1 (current db state: S1).
- Open transaction T2.
- Within T2, modify database state S1→S2.
- 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.
| ) -> Result<BatchResult> { | ||
| let mut batch = Batch::new(); | ||
| for stmt in stmts.into_iter() { | ||
| batch.step(None, stmt); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| #[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, | ||
| } |
There was a problem hiding this comment.
we really need to factorize the proto types with libsql-server
There was a problem hiding this comment.
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).
LucioFranco
left a comment
There was a problem hiding this comment.
Overall, really nice work! I left a few comments and some questions.
| where | ||
| T: for<'a> HttpSend<'a>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| #[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, | ||
| } |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Since step always refer to a previous step, I find it easier to read/write than using enumerate.
| Sql(String), | ||
| SqlId(SqlId), |
There was a problem hiding this comment.
What about String and Remote? Instead of these two names that seem quite similar?
There was a problem hiding this comment.
I named them this way, since they map directly onto Hrana protocol fields.
| TransactionBehavior::Deferred => "BEGIN DEFERRED", | ||
| TransactionBehavior::Immediate => "BEGIN IMMEDIATE", | ||
| TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE", | ||
| TransactionBehavior::ReadOnly => "BEGIN READONLY", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I was suspecting that since replication client supports this (this code is copied from there), it should be supported in hrana as well.
There was a problem hiding this comment.
No clue, we should test that it actually works I have a feeling it might not but I could also be wrong.
| T: for<'a> HttpSend<'a>, | ||
| { | ||
| pub async fn query(&self, sql: &str, params: impl IntoParams) -> crate::Result<Rows> { | ||
| tracing::trace!("querying `{}`", sql); |
There was a problem hiding this comment.
nit: could prefix the logs here like so to make it easier to debug in the future.
| tracing::trace!("querying `{}`", sql); | |
| tracing::trace!("txn querying `{}`", sql); |
7dcfad7 to
0a0e012
Compare
|
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
left a comment
There was a problem hiding this comment.
I am approving, I added a few more items in the follow up issue.
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.