I shipped a transaction bug, so I built a linter

leonh.fr

45 points

leonhfr

4 days ago


6 comments

branko_d 4 hours ago

The wording "outside of transaction" irks me. Everything in a relational database is done within a transaction, the only question is whether it's the transaction you think it is, or some other.

I believe this is largely an API design problem. Many client APIs (especially ORMs) will start a transaction implicitly for you if you haven't explicitly specified your own, leading to problems like in the article.

Having implicit transactions is just wrong design, IMO. A better-designed API should make transactions very explicit and very visible in the code: if you want to execute a query, you must start a transaction yourself and then query on that transaction supplied as an actual parameter. Implicit transactions should be difficult-to-impossible. We - the programmers - should think about transactions just as we think about querying and manipulating data. Hiding from transactions in the name of "ergonomy" brings more harm than good.

  • black_knight 4 hours ago

    I love how this is done in Software Transactional Memory (STM) in Haskell. There, the transaction code happens in its own type (monad), and there is an explicit conversion function called `atomically :: STM a -> IO a`, which carries out the transaction.

    This means that the transaction becomes its own block, clearly separated, but which can reference pure values in the surrounding context.

        do
           …
           some IO stuff 
           …
           res <- atomically $ do
              …
              transaction code
              which can reference
              results from the IO above
              …
           …
           More IO code using res, which is the result of the transaction
           …
rowyourboat 5 hours ago

This shows, once more, that humans are bad with modes. You have two copies of the repo, one in a transaction and one not in a transaction.

The problem is that the thing you use to build the transaction can also be used to directly manipulate the DB. A better API design would be to separate those two things.

mnahkies 6 hours ago

Aside from data consistency issues mentioned, you can also quickly get yourself into connection pool exhaustion issues, where concurrent requests have already obtained a transaction but are asking for another accidentally, then all stall holding the first open until timeouts occur.

cben 3 hours ago

Great walkthrough teaching how to DIY a Go AST linter, thanks

dgunay 6 hours ago

I wrote exactly this linter a while back after making the same mistake. Very annoying. Unlike you I did try to get it into golangci-lint but the process wore me down. In the age of LLMs maybe it'd be worth another try.