@@ -28,7 +28,7 @@ type Store interface {
28
28
wrapper
29
29
30
30
Ping (ctx context.Context ) (time.Duration , error )
31
- InTx (func (Store ) error , * sql. TxOptions ) error
31
+ InTx (func (Store ) error , * TxOptions ) error
32
32
}
33
33
34
34
type wrapper interface {
@@ -57,6 +57,43 @@ func New(sdb *sql.DB) Store {
57
57
}
58
58
}
59
59
60
+ // TxOptions is used to pass some execution metadata to the callers.
61
+ // Ideally we could throw this into a context, but no context is used for
62
+ // transactions. So instead, the return context is attached to the options
63
+ // passed in.
64
+ // This metadata should not be returned in the method signature, because it
65
+ // is only used for metric tracking. It should never be used by business logic.
66
+ type TxOptions struct {
67
+ // Isolation is the transaction isolation level.
68
+ // If zero, the driver or database's default level is used.
69
+ Isolation sql.IsolationLevel
70
+ ReadOnly bool
71
+
72
+ // -- Coder specific metadata --
73
+ // TxIdentifier is a unique identifier for the transaction to be used
74
+ // in metrics. Can be any string.
75
+ TxIdentifier string
76
+
77
+ // Set by InTx
78
+ executionCount int
79
+ }
80
+
81
+ // IncrementExecutionCount is a helper function for external packages
82
+ // to increment the unexported count.
83
+ // Mainly for `dbmem`.
84
+ func IncrementExecutionCount (opts * TxOptions ) {
85
+ opts .executionCount ++
86
+ }
87
+
88
+ func (o TxOptions ) ExecutionCount () int {
89
+ return o .executionCount
90
+ }
91
+
92
+ func (o * TxOptions ) WithID (id string ) * TxOptions {
93
+ o .TxIdentifier = id
94
+ return o
95
+ }
96
+
60
97
// queries encompasses both are sqlc generated
61
98
// queries and our custom queries.
62
99
type querier interface {
@@ -80,25 +117,39 @@ func (q *sqlQuerier) Ping(ctx context.Context) (time.Duration, error) {
80
117
return time .Since (start ), err
81
118
}
82
119
83
- func (q * sqlQuerier ) InTx (function func (Store ) error , txOpts * sql.TxOptions ) error {
120
+ func DefaultTXOptions () * TxOptions {
121
+ return & TxOptions {
122
+ Isolation : sql .LevelDefault ,
123
+ ReadOnly : false ,
124
+ }
125
+ }
126
+
127
+ func (q * sqlQuerier ) InTx (function func (Store ) error , txOpts * TxOptions ) error {
84
128
_ , inTx := q .db .(* sqlx.Tx )
85
- isolation := sql .LevelDefault
86
- if txOpts != nil {
87
- isolation = txOpts .Isolation
129
+
130
+ if txOpts == nil {
131
+ // create a default txOpts if left to nil
132
+ txOpts = DefaultTXOptions ()
133
+ }
134
+
135
+ sqlOpts := & sql.TxOptions {
136
+ Isolation : txOpts .Isolation ,
137
+ ReadOnly : txOpts .ReadOnly ,
88
138
}
89
139
90
140
// If we are not already in a transaction, and we are running in serializable
91
141
// mode, we need to run the transaction in a retry loop. The caller should be
92
142
// prepared to allow retries if using serializable mode.
93
143
// If we are in a transaction already, the parent InTx call will handle the retry.
94
144
// We do not want to duplicate those retries.
95
- if ! inTx && isolation == sql .LevelSerializable {
145
+ if ! inTx && sqlOpts . Isolation == sql .LevelSerializable {
96
146
// This is an arbitrarily chosen number.
97
147
const retryAmount = 3
98
148
var err error
99
149
attempts := 0
100
150
for attempts = 0 ; attempts < retryAmount ; attempts ++ {
101
- err = q .runTx (function , txOpts )
151
+ txOpts .executionCount ++
152
+ err = q .runTx (function , sqlOpts )
102
153
if err == nil {
103
154
// Transaction succeeded.
104
155
return nil
@@ -111,7 +162,9 @@ func (q *sqlQuerier) InTx(function func(Store) error, txOpts *sql.TxOptions) err
111
162
// Transaction kept failing in serializable mode.
112
163
return xerrors .Errorf ("transaction failed after %d attempts: %w" , attempts , err )
113
164
}
114
- return q .runTx (function , txOpts )
165
+
166
+ txOpts .executionCount ++
167
+ return q .runTx (function , sqlOpts )
115
168
}
116
169
117
170
// InTx performs database operations inside a transaction.
0 commit comments