Just got bitten by a caveat in next.jdbc's transaction behavior. https://cljdoc.org/d/com.github.seancorfield/next.jdbc/1.3.939/doc/getting-started/transactions?q=nested#nesting-transactions as
> Note: active-tx? only knows about next.jdbc transactions -- it cannot track any transactions that you create yourself using the underlying JDBC Connection. In addition, this is a per-thread "global" setting and not related to just a single connection, so you can't use this setting if you are working with multiple databases in the same dynamic thread context (`binding`).
The exact pattern that bit me was
(jdbc/with-transaction [tx data-source-from-db-1]
(jdbc/with-transaction [tx2 data-source-from-db-2]
,,,))
However, I'd be surprised even with
(jdbc/with-transaction [tx data-source]
(jdbc/with-transaction [tx2 data-source]
,,,))
given that those are transactions on two different SQL connections.
Is there an agreed upon way out of next.jdbc.transaction/*active-tx* ? Is there a recommendation in place? E.g. my own (extend-protocol p/Transactable java.sql.Connection) or perhaps using next.jdbc.transaction/transact* even though it's private or own tx implementation?Thank you ๐ We have a workaround for this in production code where I'd rather avoid pointing to a snapshot version of a dependency. Besides, it's a minor issue in any case, so we'll most likely wait with upgrading until the next stable release.
Not sure when I'll cut a new release since that's the only change since the last release. Might be a month.
Thank you!
Okay, there is a new snapshot that should address this (or you can use git deps).
By default, next.jdbc allows nested TX so both of the above will work, I believe? What exactly did you get bitten by?
*nested-tx* is set to :allow by default. Or are you relying on active-tx? which is saying, yes, you have a nested TX here when you don't really?
(inside you ,,, code you would have active-tx? returning true anyway so I'm not sure what you're saying here)
IIRC, the problem here is that if the body of inner transaction throws, the outer transaction get rolled back, even though theyโre running on two different datasources.
(for clarity, I work with @miro.bezjak ๐
But you might want to wait for @miro.bezjak to explain this properly
But here is what he wrote on our internal slack:
Briefly, this snippet doesnโt work as I would expect
(jdbc/with-transaction [tx-in-a-different-db (:ardoq-ds (lib/ctx :pp))]
(jdbc/execute! tx-in-a-different-db ["LOCK TABLE job;"])
(jdbc/with-transaction [completely-unrelated-tx-to-above (:org-ds (lib/ctx :pp))]
(jdbc/execute! completely-unrelated-tx-to-above ["delete from component__history"])
(throw (Exception. "An exception that should rollback the delete!"))))
๐ those are 2 completely unrelated / isolated / non-nested transactions to me. However, due to the https://github.com/seancorfield/next-jdbc/blob/1bd4bdedce465761bd2575aa7bf10f6cc71f5067/src/next/jdbc/transaction.clj#L119-L128, they are considered nested. It seems that nested for next.jdbc is whenever with-transaction is used in other with-transaction block. Regardless of arguments to with-transaction.
The end result of the snippet above is that delete from component__history is not rolled back! (edited)Since *nested-tx* should be :allow, the locking branch should be taken here. Are you binding *nested-tx* to something else?
Look what I found:
;; Reuse the outer transaction (i.e. inner tx are ignored and will succeed / fail in line with the
;; whole, outermost transaction) instead of allowing a nested tx to complete successfully even if
;; the outer one fails. Thus, it is always safe to wrap code in jdbc/with-transaction and being
;; able to roll everything back if there is any error.
(alter-var-root #'jdbc.tx/*nested-tx* (constantly :ignore))(also, Iโm just the messenger here, in particular a messenger whoโs really scared and ignorant about the finer details of transactions)
Just a note: active-tx? is just a wrapper on top of a dynamic var *active-tx* which is set inside the with-transaction macro. Thus, it doesn't correlate 100% with what happens under the wire. You may open a transaction, then pass the tx value into another thread, and the binding context will be lost.
The most reliable way to check if there is a transaction is to ask the connection. Say, in Postgres, there is a trick like that:
SELECT now() = statement_timestamp();
outside a transaction, it will be true, but inside, it's falseThe real issue is that transactions don't really "exist" -- they are inherently just state inside the Connection object. next.jdbc currently does its best to kind of pretend that you can execute code "inside" a TX and by default it assumes you know what you are doing. If you get it wrong, next.jdbc can't help you -- but it does provide one "knob" you can tweak, if necessary, to get you out of some holes.
Sorry for resurrecting an old thread, but I think a bug might have been introduced in the https://github.com/seancorfield/next-jdbc/commit/b0a640a10150713313af4f9dd9782b4b6f0dbf78 that fixed transactions on distinct connections, and I figured it might be worth discussing it in the context of the original change.
The problem is that *active-tx* always contains a connection unwrapped by raw-connection , but the new version of active-tx? just looks for con in *active-tx* without unwrapping it. One issue is that it's inconsistent with (contains? *active-tx* raw) checks in the implementation of Transactable , and from the library user's perspective it forces usage like for example (jdbc/active-tx? (#'jdbc-tx/raw-connection conn#)) because if the connection is wrapped, one cannot use the same arg for active-tx? and with-transaction.
I think active-tx? should unwrap the connection, but I'm not 100% sure how the implementation should look like, especially around possibly moving or making raw-connection a public function.
@seancorfield If you agree with the analysis, please advise if I should follow up with an issue or PR on GitHub, or if you'd rather look into it yourself ๐
@mslupny You could be right. Can you create an issue on GitHub please and I'll take a look?
Apologies for not providing enough context. In my haste to meet a friend for beer, I forgot to make sure that the question makes sense. ๐
Let me try again.
As Erik said above, we change the default of *nested-tx*.
(alter-var-root #'jdbc.tx/*nested-tx* (constantly :ignore))
This is a saner default for us. We know what we're doing. ๐
I also read next.jdbc's transaction documentation. However, that was some time ago. Before https://github.com/seancorfield/next-jdbc/commit/06f9bae note was added.
For that reason, my assumption was that with-transaction is going to work the same way it does elsewhere. I haven't done groovy and spring in a while, but I do remember it working the way I'd expect them to work. Assumption aside, let's see what I mean by "bitten".
I have expected the following snippet to open two separate transactions.
(jdbc/with-transaction [tx1 data-source-1]
(jdbc/execute! tx1 ,,,)
(jdbc/with-transaction [tx2 data-source-2]
(jdbc/execute! tx2 ,,,)
(throw (Exception. "An exception happened, so rollback tx2"))))
(in the real world, those two with-transaction calls are separated by many function calls)
Now that I've seen *active-tx* I know that it's not. It's equivalent to.
(jdbc/with-transaction [tx1 data-source-1]
(jdbc/execute! tx1 ,,,)
(jdbc/execute! data-source-2 ,,,)
(throw (Exception. "An exception happened, so it'll rollback tx1")))
The reason it was surprising to me was that, in the snippet above, those are two different data sources; two different SQL connections; should be two different transactions, that happen to be executed on a single thread. Yes, the two with-transaction are nested, but I'm passing different arguments.
It's as if, I had two nested with-open calls with different arguments, and they don't work as one would expect. ๐
In other words, we know that
(jdbc/with-transaction [tx data-source]
(jdbc/execute! tx ,,,))
is not equivalent to
(jdbc/with-transaction [tx data-source]
(jdbc/execute! data-source ,,,))
The argument makes all the difference!
I also wouldn't expect
(jdbc/with-transaction [tx1 data-source-1]
(jdbc/execute! tx1 ,,,)
(jdbc/with-transaction [tx2 data-source-2]
(jdbc/execute! tx2 ,,,)))
to be equivalent to
(jdbc/with-transaction [tx1 data-source-1]
(jdbc/execute! tx1 ,,,)
(jdbc/execute! data-source-2 ,,,))
The argument makes all the difference. ๐
That's what I mean by bitten.
Anyway, we'll probably provide our own implementation to (extend-protocol p/Transactable java.sql.Connection) in the way that's less surprising to us. I have to see how. If .getAutoCommit is false, that might be a good indicator. Or *active-tx* being connection->boolean hash-map might do the trick. Would it be possible to make next.jdbc.transaction/transact* public as both approaches rely on it?
Do you have a better recommendation for ๐ ?Auto-commit isn't a useful indicator, unfortunately (see the closed tickets that led to some of this behavior -- pretty sure auto-commit was mentioned in one or more of them).
*nested-tx* was exposed mainly as a migration aid for folks coming from c.j.j ๐
Feel free to create an issue on GH that references this Slack thread. Making the TX active/nesting checks specific to Connection objects sounds like a promising approach but it will need some analysis.
Issue created: https://github.com/seancorfield/next-jdbc/issues/282 I'll see tomorrow if I can come up with the solution that will work for us. That could be the basis for the PR.
Thank you!