-
Notifications
You must be signed in to change notification settings - Fork 0
/
transaction.go
112 lines (101 loc) · 3.96 KB
/
transaction.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package sqldb
import (
"database/sql"
"errors"
"fmt"
"sync/atomic"
)
var txCounter atomic.Uint64
// NextTransactionNo returns the next globally unique number
// for a new transaction in a threadsafe way.
//
// Use Connection.TransactionNo() to get the number
// from a transaction connection.
func NextTransactionNo() uint64 {
return txCounter.Add(1)
}
// Transaction executes txFunc within a database transaction that is passed in to txFunc as tx Connection.
// Transaction returns all errors from txFunc or transaction commit errors happening after txFunc.
// If parentConn is already a transaction, then it is passed through to txFunc unchanged as tx Connection
// and no parentConn.Begin, Commit, or Rollback calls will occour within this Transaction call.
// An error is returned, if the requested transaction options passed via opts
// are stricter than the options of the parent transaction.
// Errors and panics from txFunc will rollback the transaction if parentConn was not already a transaction.
// Recovered panics are re-paniced and rollback errors after a panic are logged with ErrLogger.
func Transaction(parentConn Connection, opts *sql.TxOptions, txFunc func(tx Connection) error) (err error) {
if parentOpts, parentIsTx := parentConn.TransactionOptions(); parentIsTx {
err = CheckTxOptionsCompatibility(parentOpts, opts, parentConn.Config().DefaultIsolationLevel)
if err != nil {
return err
}
return txFunc(parentConn)
}
return IsolatedTransaction(parentConn, opts, txFunc)
}
// IsolatedTransaction executes txFunc within a database transaction that is passed in to txFunc as tx Connection.
// IsolatedTransaction returns all errors from txFunc or transaction commit errors happening after txFunc.
// If parentConn is already a transaction, a brand new transaction will begin on the parent's connection.
// Errors and panics from txFunc will rollback the transaction.
// Recovered panics are re-paniced and rollback errors after a panic are logged with ErrLogger.
func IsolatedTransaction(parentConn Connection, opts *sql.TxOptions, txFunc func(tx Connection) error) (err error) {
txNo := NextTransactionNo()
tx, e := parentConn.Begin(opts, txNo)
if e != nil {
return fmt.Errorf("Transaction %d Begin error: %w", txNo, e)
}
defer func() {
if r := recover(); r != nil {
// txFunc paniced
e := tx.Rollback()
if e != nil && !errors.Is(e, sql.ErrTxDone) {
// Double error situation, log e so it doesn't get lost
ErrLogger.Printf("Transaction %d error (%s) from rollback after panic: %+v", txNo, e, r)
}
panic(r) // re-throw panic after Rollback
}
if err != nil {
// txFunc returned an error
e := tx.Rollback()
if e != nil && !errors.Is(e, sql.ErrTxDone) {
// Double error situation, wrap err with e so it doesn't get lost
err = fmt.Errorf("Transaction %d error (%s) from rollback after error: %w", txNo, e, err)
}
return
}
e := tx.Commit()
if e != nil {
// Set Commit error as function return value
err = fmt.Errorf("Transaction %d Commit error: %w", txNo, e)
}
}()
return txFunc(tx)
}
// CheckTxOptionsCompatibility returns an error
// if the parent transaction options are less strict than the child options.
func CheckTxOptionsCompatibility(parent, child *sql.TxOptions, defaultIsolation sql.IsolationLevel) error {
var (
parentReadOnly = false
parentIsolation = defaultIsolation
childReadOnly = false
childIsolation = defaultIsolation
)
if parent != nil {
parentReadOnly = parent.ReadOnly
if parent.Isolation != sql.LevelDefault {
parentIsolation = parent.Isolation
}
}
if child != nil {
childReadOnly = child.ReadOnly
if child.Isolation != sql.LevelDefault {
childIsolation = child.Isolation
}
}
if parentReadOnly && !childReadOnly {
return errors.New("parent transaction is read-only but child is not")
}
if parentIsolation < childIsolation {
return fmt.Errorf("parent transaction isolation level '%s' is less strict child level '%s'", parentIsolation, childIsolation)
}
return nil
}