Live data from Hacker News

I shipped a transaction bug, so I built a linter

leonh.fr

1–10 of 14 posts

Re: I shipped a transaction bug, so I built a linter

#2
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.

Re: I shipped a transaction bug, so I built a linter

#4
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.

Re: I shipped a transaction bug, so I built a linter

#5
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.

Re: I shipped a transaction bug, so I built a linter

#6
post #5

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 impl…

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 

Re: I shipped a transaction bug, so I built a linter

#8
I would question the framework design: the method is called "UpdateUser", so it should be executed in a transaction, so it should be a parameter of the service, and the transaction logic handled by the framework.

  func (s \*Service) UpdateUser(ctx context.Context, tx models.Repo, userID string) error {
        user, err := tx.GetUser(ctx, userID)
        if err != nil {
            return err
        }
        user.Name = "Updated"
        return tx.SaveUser(ctx, user)
  }

Re: I shipped a transaction bug, so I built a linter

#9

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.

Interestingly, modal editing comes very natural to us. It enters muscle memory quite well.

Re: I shipped a transaction bug, so I built a linter

#10
post #8

I would question the framework design: the method is called "UpdateUser", so it should be executed in a transaction, so it should be a parameter of the service, and the transaction logic handled by the framework. func (s \*Service) UpdateUser(ctx context.Context, tx models.Repo, userID string) error { user, err := tx.GetUser(ctx, userID) if err != nil { return err } user.Name = "Updated" return tx.SaveUser(ctx, user)…

In that instance, you are right, but there are often cases where you need to do multiple queries / updates spanning multiple tables in a single transaction, then you do need a generic transaction wrapper.
Post reply on HN