mirror of
https://github.com/rancher/steve.git
synced 2025-09-10 11:49:15 +00:00
sql: use a closure to wrap transactions (#469)
This introduces the a `WithTransaction` function, which is then used for all transactional work in Steve. Because `WithTransaction` takes care of all `Begin`s, `Commit`s and `Rollback`s, it eliminates the problem where forgotten open transactions can block all other operations (with long stalling and `SQLITE_BUSY` errors). This also: - merges together the disparate `DBClient` interfaces in one only `db.Client` interface with one unexported non-test implementation. I found this much easier to follow - refactors the transaction package in order to make it as minimal as possible, and as close to the wrapped `sql.Tx` and `sql.Stmt` functions as possible, in order to reduce cognitive load when working with this part of the codebase - simplifies tests accordingly - adds a couple of known files to `.gitignore` Credits to @tomleb for suggesting the approach: https://github.com/rancher/lasso/pull/121#pullrequestreview-2515872507
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/steve/pkg/sqlcache/db"
|
||||
"github.com/rancher/steve/pkg/sqlcache/partition"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
|
||||
//go:generate mockgen --build_flags=--mod=mod -package informer -destination ./informer_mocks_test.go github.com/rancher/steve/pkg/sqlcache/informer ByOptionsLister
|
||||
//go:generate mockgen --build_flags=--mod=mod -package informer -destination ./dynamic_mocks_test.go k8s.io/client-go/dynamic ResourceInterface
|
||||
//go:generate mockgen --build_flags=--mod=mod -package informer -destination ./store_mocks_test.go github.com/rancher/steve/pkg/sqlcache/store DBClient
|
||||
|
||||
func TestNewInformer(t *testing.T) {
|
||||
type testCase struct {
|
||||
@@ -31,7 +31,7 @@ func TestNewInformer(t *testing.T) {
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "NewInformer() with no errors returned, should return no error", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dbClient := NewMockClient(gomock.NewController(t))
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
|
||||
@@ -40,29 +40,44 @@ func TestNewInformer(t *testing.T) {
|
||||
|
||||
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
||||
// is tested in depth in its own package.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
dbClient.EXPECT().Prepare(gomock.Any()).Return(&sql.Stmt{}).AnyTimes()
|
||||
|
||||
// NewIndexer() logic (within NewListOptionIndexer(). This test is only concerned with whether it returns err or not as NewIndexer
|
||||
// is tested in depth in its own indexer_test.go
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
// NewListOptionIndexer() logic. This test is only concerned with whether it returns err or not as NewIndexer
|
||||
// is tested in depth in its own indexer_test.go
|
||||
dbClient.EXPECT().BeginTx(context.Background(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
informer, err := NewInformer(dynamicClient, fields, nil, gvk, dbClient, false, true)
|
||||
assert.Nil(t, err)
|
||||
@@ -70,7 +85,7 @@ func TestNewInformer(t *testing.T) {
|
||||
assert.NotNil(t, informer.SharedIndexInformer)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewInformer() with errors returned from NewStore(), should return an error", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dbClient := NewMockClient(gomock.NewController(t))
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
|
||||
@@ -79,15 +94,20 @@ func TestNewInformer(t *testing.T) {
|
||||
|
||||
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
||||
// is tested in depth in its own package.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(fmt.Errorf("error"))
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
_, err := NewInformer(dynamicClient, fields, nil, gvk, dbClient, false, true)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewInformer() with errors returned from NewIndexer(), should return an error", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dbClient := NewMockClient(gomock.NewController(t))
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
|
||||
@@ -96,23 +116,33 @@ func TestNewInformer(t *testing.T) {
|
||||
|
||||
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
||||
// is tested in depth in its own package.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
dbClient.EXPECT().Prepare(gomock.Any()).Return(&sql.Stmt{}).AnyTimes()
|
||||
|
||||
// NewIndexer() logic (within NewListOptionIndexer(). This test is only concerned with whether it returns err or not as NewIndexer
|
||||
// is tested in depth in its own indexer_test.go
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(fmt.Errorf("error"))
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
_, err := NewInformer(dynamicClient, fields, nil, gvk, dbClient, false, true)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewInformer() with errors returned from NewListOptionIndexer(), should return an error", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dbClient := NewMockClient(gomock.NewController(t))
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
|
||||
@@ -121,35 +151,50 @@ func TestNewInformer(t *testing.T) {
|
||||
|
||||
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
||||
// is tested in depth in its own package.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
dbClient.EXPECT().Prepare(gomock.Any()).Return(&sql.Stmt{}).AnyTimes()
|
||||
|
||||
// NewIndexer() logic (within NewListOptionIndexer(). This test is only concerned with whether it returns err or not as NewIndexer
|
||||
// is tested in depth in its own indexer_test.go
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
// NewListOptionIndexer() logic. This test is only concerned with whether it returns err or not as NewIndexer
|
||||
// is tested in depth in its own indexer_test.go
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(fmt.Errorf("error"))
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
_, err := NewInformer(dynamicClient, fields, nil, gvk, dbClient, false, true)
|
||||
assert.NotNil(t, err)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewInformer() with transform func", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dbClient := NewMockClient(gomock.NewController(t))
|
||||
txClient := NewMockTXClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
mockInformer := mockInformer{}
|
||||
@@ -166,29 +211,44 @@ func TestNewInformer(t *testing.T) {
|
||||
|
||||
// NewStore() from store package logic. This package is only concerned with whether it returns err or not as NewStore
|
||||
// is tested in depth in its own package.
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
dbClient.EXPECT().Prepare(gomock.Any()).Return(&sql.Stmt{}).AnyTimes()
|
||||
|
||||
// NewIndexer() logic (within NewListOptionIndexer(). This test is only concerned with whether it returns err or not as NewIndexer
|
||||
// is tested in depth in its own indexer_test.go
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
// NewListOptionIndexer() logic. This test is only concerned with whether it returns err or not as NewIndexer
|
||||
// is tested in depth in its own indexer_test.go
|
||||
dbClient.EXPECT().BeginTx(gomock.Any(), true).Return(txClient, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil)
|
||||
txClient.EXPECT().Commit().Return(nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
||||
dbClient.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
||||
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
||||
err := f(txClient)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
transformFunc := func(input interface{}) (interface{}, error) {
|
||||
return "someoutput", nil
|
||||
@@ -210,7 +270,7 @@ func TestNewInformer(t *testing.T) {
|
||||
newInformer = cache.NewSharedIndexInformer
|
||||
}})
|
||||
tests = append(tests, testCase{description: "NewInformer() unable to set transform func", test: func(t *testing.T) {
|
||||
dbClient := NewMockDBClient(gomock.NewController(t))
|
||||
dbClient := NewMockClient(gomock.NewController(t))
|
||||
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||||
mockInformer := mockInformer{
|
||||
setTranformErr: fmt.Errorf("some error"),
|
||||
|
Reference in New Issue
Block a user