package database import ( "database/sql" "github.com/lib/pq" "golang.org/x/xerrors" ) const maxRetries = 5 // ReadModifyUpdate is a helper function to run a db transaction that reads some // object(s), modifies some of the data, and writes the modified object(s) back // to the database. It is run in a transaction at RepeatableRead isolation so // that if another database client also modifies the data we are writing and // commits, then the transaction is rolled back and restarted. // // This is needed because we typically read all object columns, modify some // subset, and then write all columns. Consider an object with columns A, B and // initial values A=1, B=1. Two database clients work simultaneously, with one // client attempting to set A=2, and another attempting to set B=2. They both // initially read A=1, B=1, and then one writes A=2, B=1, and the other writes // A=1, B=2. With default PostgreSQL isolation of ReadCommitted, both of these // transactions would succeed and we end up with either A=2, B=1 or A=1, B=2. // One or other client gets their transaction wiped out even though the data // they wanted to change didn't conflict. // // If we run at RepeatableRead isolation, then one or other transaction will // fail. Let's say the transaction that sets A=2 succeeds. Then the first B=2 // transaction fails, but here we retry. The second attempt we read A=2, B=1, // then write A=2, B=2 as desired, and this succeeds. func ReadModifyUpdate(db Store, f func(tx Store) error, ) error { var err error for retries := 0; retries < maxRetries; retries++ { err = db.InTx(f, &sql.TxOptions{ Isolation: sql.LevelRepeatableRead, }) var pqe *pq.Error if xerrors.As(err, &pqe) { if pqe.Code == "40001" { // serialization error, retry continue } } return err } return xerrors.Errorf("too many errors; last error: %w", err) }