@@ -145,26 +145,7 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error {
145
145
146
146
logger .Debug (ctx ,"starting reconciliation" )
147
147
148
- // This tx holds a global lock, which prevents any other coderd replica from starting a reconciliation and
149
- // possibly getting an inconsistent view of the state.
150
- //
151
- // The lock MUST be held until ALL modifications have been effected.
152
- //
153
- // It is run with RepeatableRead isolation, so it's effectively snapshotting the data at the start of the tx.
154
- //
155
- // This is a read-only tx, so returning an error (i.e. causing a rollback) has no impact.
156
- err := c .store .InTx (func (db database.Store )error {
157
- start := c .clock .Now ()
158
-
159
- // TODO: use TryAcquireLock here and bail out early.
160
- err := db .AcquireLock (ctx ,database .LockIDReconcileTemplatePrebuilds )
161
- if err != nil {
162
- logger .Warn (ctx ,"failed to acquire top-level reconciliation lock; likely running on another coderd replica" ,slog .Error (err ))
163
- return nil
164
- }
165
-
166
- logger .Debug (ctx ,"acquired top-level reconciliation lock" ,slog .F ("acquire_wait_secs" ,fmt .Sprintf ("%.4f" ,c .clock .Since (start ).Seconds ())))
167
-
148
+ err := c .WithReconciliationLock (ctx ,logger ,func (ctx context.Context ,db database.Store )error {
168
149
state ,err := c .SnapshotState (ctx ,db )
169
150
if err != nil {
170
151
return xerrors .Errorf ("determine current state: %w" ,err )
@@ -209,10 +190,6 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error {
209
190
}
210
191
211
192
return eg .Wait ()
212
- },& database.TxOptions {
213
- Isolation :sql .LevelRepeatableRead ,
214
- ReadOnly :true ,
215
- TxIdentifier :"template_prebuilds" ,
216
193
})
217
194
if err != nil {
218
195
logger .Error (ctx ,"failed to reconcile" ,slog .Error (err ))
@@ -221,6 +198,35 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error {
221
198
return err
222
199
}
223
200
201
+ func (c * StoreReconciler )WithReconciliationLock (ctx context.Context ,logger slog.Logger ,fn func (ctx context.Context ,db database.Store )error )error {
202
+ // This tx holds a global lock, which prevents any other coderd replica from starting a reconciliation and
203
+ // possibly getting an inconsistent view of the state.
204
+ //
205
+ // The lock MUST be held until ALL modifications have been effected.
206
+ //
207
+ // It is run with RepeatableRead isolation, so it's effectively snapshotting the data at the start of the tx.
208
+ //
209
+ // This is a read-only tx, so returning an error (i.e. causing a rollback) has no impact.
210
+ return c .store .InTx (func (db database.Store )error {
211
+ start := c .clock .Now ()
212
+
213
+ // TODO: use TryAcquireLock here and bail out early.
214
+ err := db .AcquireLock (ctx ,database .LockIDReconcileTemplatePrebuilds )
215
+ if err != nil {
216
+ logger .Warn (ctx ,"failed to acquire top-level reconciliation lock; likely running on another coderd replica" ,slog .Error (err ))
217
+ return nil
218
+ }
219
+
220
+ logger .Debug (ctx ,"acquired top-level reconciliation lock" ,slog .F ("acquire_wait_secs" ,fmt .Sprintf ("%.4f" ,c .clock .Since (start ).Seconds ())))
221
+
222
+ return fn (ctx ,db )
223
+ },& database.TxOptions {
224
+ Isolation :sql .LevelRepeatableRead ,
225
+ ReadOnly :true ,
226
+ TxIdentifier :"template_prebuilds" ,
227
+ })
228
+ }
229
+
224
230
// SnapshotState determines the current state of prebuilds & the presets which define them.
225
231
// An application-level lock is used
226
232
func (c * StoreReconciler )SnapshotState (ctx context.Context ,store database.Store ) (* prebuilds.ReconciliationState ,error ) {