Skip to content

Commit 381f078

Browse files
committed
retry insert active key on tx serialization failure
1 parent feae634 commit 381f078

File tree

2 files changed

+71
-14
lines changed

2 files changed

+71
-14
lines changed

enterprise/dbcrypt/dbcrypt.go

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ func New(ctx context.Context, db database.Store, ciphers ...Cipher) (database.St
4949
Store: db,
5050
}
5151
// nolint: gocritic // This is allowed.
52-
if err := dbc.ensureEncrypted(dbauthz.AsSystemRestricted(ctx)); err != nil {
52+
authCtx := dbauthz.AsSystemRestricted(ctx)
53+
if err := dbc.ensureEncryptedWithRetry(authCtx); err != nil {
5354
return nil, xerrors.Errorf("ensure encrypted database fields: %w", err)
5455
}
5556
return dbc, nil
@@ -334,6 +335,22 @@ func (db *dbCrypt) decryptField(field *string, digest sql.NullString) error {
334335
return nil
335336
}
336337

338+
func (db *dbCrypt) ensureEncryptedWithRetry(ctx context.Context) error {
339+
err := db.ensureEncrypted(ctx)
340+
if err == nil {
341+
return nil
342+
}
343+
// If we get a serialization error, then we need to retry.
344+
var pqerr *pq.Error
345+
if !xerrors.As(err, &pqerr) {
346+
return err
347+
}
348+
if pqerr.Code != "40001" { // serialization_failure
349+
return err
350+
}
351+
return db.ensureEncrypted(ctx)
352+
}
353+
337354
func (db *dbCrypt) ensureEncrypted(ctx context.Context) error {
338355
return db.InTx(func(s database.Store) error {
339356
// Attempt to read the encrypted test fields of the currently active keys.
@@ -364,21 +381,10 @@ func (db *dbCrypt) ensureEncrypted(ctx context.Context) error {
364381
}
365382

366383
// If we get here, then we have a new key that we need to insert.
367-
// If this conflicts with another transaction, we do not need to retry as
368-
// the other transaction will have inserted the key for us.
369-
if err := db.InsertDBCryptKey(ctx, database.InsertDBCryptKeyParams{
384+
return db.InsertDBCryptKey(ctx, database.InsertDBCryptKeyParams{
370385
Number: highestNumber + 1,
371386
ActiveKeyDigest: db.primaryCipherDigest,
372387
Test: testValue,
373-
}); err != nil {
374-
var pqErr *pq.Error
375-
if xerrors.As(err, &pqErr) && pqErr.Code == "23505" {
376-
// Unique constraint violation -> another transaction has inserted the key for us.
377-
return nil
378-
}
379-
return err
380-
}
381-
382-
return nil
388+
})
383389
}, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
384390
}

enterprise/dbcrypt/dbcrypt_internal_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ import (
88
"io"
99
"testing"
1010

11+
"github.com/golang/mock/gomock"
12+
"github.com/lib/pq"
1113
"github.com/stretchr/testify/require"
1214

1315
"github.com/coder/coder/v2/coderd/database"
1416
"github.com/coder/coder/v2/coderd/database/dbgen"
17+
"github.com/coder/coder/v2/coderd/database/dbmock"
1518
"github.com/coder/coder/v2/coderd/database/dbtestutil"
1619
)
1720

@@ -470,6 +473,46 @@ func TestNew(t *testing.T) {
470473
require.Error(t, err)
471474
require.ErrorContains(t, err, "has been revoked")
472475
})
476+
477+
t.Run("Retry", func(t *testing.T) {
478+
t.Parallel()
479+
// Given: a cipher is loaded
480+
cipher := initCipher(t)
481+
ctx, cancel := context.WithCancel(context.Background())
482+
testVal, err := cipher.Encrypt([]byte("coder"))
483+
key := database.DBCryptKey{
484+
Number: 1,
485+
ActiveKeyDigest: sql.NullString{String: cipher.HexDigest(), Valid: true},
486+
Test: b64encode(testVal),
487+
}
488+
require.NoError(t, err)
489+
t.Cleanup(cancel)
490+
491+
// And: a database that returns an error once when we try to serialize a key
492+
ctrl := gomock.NewController(t)
493+
mockDB := dbmock.NewMockStore(ctrl)
494+
495+
gomock.InOrder(
496+
// First try: we get a serialization error.
497+
expectTx(mockDB),
498+
mockDB.EXPECT().GetDBCryptKeys(gomock.Any()).Times(1).Return([]database.DBCryptKey{}, nil),
499+
mockDB.EXPECT().InsertDBCryptKey(gomock.Any(), gomock.Any()).Times(1).Return(&pq.Error{Code: "40001"}),
500+
// Second try: we get the key we wanted to insert initially.
501+
expectTx(mockDB),
502+
mockDB.EXPECT().GetDBCryptKeys(gomock.Any()).Times(1).Return([]database.DBCryptKey{key}, nil),
503+
)
504+
505+
_, err = New(ctx, mockDB, cipher)
506+
require.NoError(t, err)
507+
})
508+
}
509+
510+
func expectTx(mdb *dbmock.MockStore) *gomock.Call {
511+
return mdb.EXPECT().InTx(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
512+
func(f func(store database.Store) error, _ *sql.TxOptions) error {
513+
return f(mdb)
514+
},
515+
)
473516
}
474517

475518
func requireEncryptedEquals(t *testing.T, c Cipher, value, expected string) {
@@ -511,3 +554,11 @@ func fakeBase64RandomData(t *testing.T, n int) string {
511554
require.NoError(t, err)
512555
return base64.StdEncoding.EncodeToString(b)
513556
}
557+
558+
func withInTx(mTx *dbmock.MockStore) {
559+
mTx.EXPECT().InTx(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
560+
func(f func(store database.Store) error, _ *sql.TxOptions) error {
561+
return f(mTx)
562+
},
563+
)
564+
}

0 commit comments

Comments
 (0)