From e823cdc714b59b41eb7aec044157b1623abbc0a3 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Mon, 10 Jan 2022 23:33:49 +0100 Subject: [PATCH] Use godogx/resource lock (#2) --- go.mod | 1 + go.sum | 2 + manager.go | 9 +-- manager_concurrency_test.go | 13 +--- sync.go | 116 ------------------------------------ 5 files changed, 10 insertions(+), 131 deletions(-) delete mode 100644 sync.go diff --git a/go.mod b/go.mod index 54edaca..514048c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/bool64/shared v0.1.4 github.com/bool64/sqluct v0.1.9 github.com/cucumber/godog v0.12.3 + github.com/godogx/resource v0.1.0 github.com/jmoiron/sqlx v1.3.4 github.com/stretchr/testify v1.7.0 github.com/swaggest/form/v5 v5.0.1 diff --git a/go.sum b/go.sum index 6752e28..319a444 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godogx/resource v0.1.0 h1:sKb8UtuYERQW9BKfvlXCMTye3EDXS7dTkd3KZWrDf0s= +github.com/godogx/resource v0.1.0/go.mod h1:yzBWW0+dz7/BA0VPpLXexgA4MfJYLMxsV5iK9nwObBk= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= diff --git a/manager.go b/manager.go index b728523..f5bec11 100644 --- a/manager.go +++ b/manager.go @@ -130,6 +130,7 @@ import ( "github.com/bool64/shared" "github.com/bool64/sqluct" "github.com/cucumber/godog" + "github.com/godogx/resource" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/swaggest/form/v5" @@ -140,7 +141,7 @@ const Default = "default" // RegisterSteps adds database manager context to test suite. func (m *Manager) RegisterSteps(s *godog.ScenarioContext) { - m.sync.register(s) + m.lock.Register(s) m.registerPrerequisites(s) m.registerAssertions(s) } @@ -228,14 +229,14 @@ func NewManager() *Manager { return &Manager{ TableMapper: NewTableMapper(), Instances: make(map[string]Instance), - sync: newSynchronized(nil), + lock: resource.NewLock(nil), Vars: &shared.Vars{}, } } // Manager owns database connections. type Manager struct { - sync *synchronized + lock *resource.Lock TableMapper *TableMapper Instances map[string]Instance @@ -291,7 +292,7 @@ func (m *Manager) instance(ctx context.Context, tableName, dbName string) (Insta } // Locking per table. - _, err := m.sync.acquireLock(ctx, dbName+"::"+tableName) + _, err := m.lock.Acquire(ctx, dbName+"::"+tableName) if err != nil { return Instance{}, nil, ctx, err } diff --git a/manager_concurrency_test.go b/manager_concurrency_test.go index 90037a5..cc0cde7 100644 --- a/manager_concurrency_test.go +++ b/manager_concurrency_test.go @@ -16,15 +16,6 @@ import ( "github.com/stretchr/testify/assert" ) -func (s *synchronized) isLocked(ctx context.Context, service string) bool { - s.mu.Lock() - defer s.mu.Unlock() - - lock := s.locks[service] - - return lock != nil && lock != ctx.Value(s.ctxKey).(chan struct{}) -} - func TestNewManager_concurrent(t *testing.T) { dbm := NewManager() @@ -61,7 +52,7 @@ func TestNewManager_concurrent(t *testing.T) { ScenarioInitializer: func(s *godog.ScenarioContext) { dbm.RegisterSteps(s) s.Step(`^I should not be blocked for "([^"]*)"$`, func(ctx context.Context, key string) error { - if dbm.sync.isLocked(ctx, key) { + if dbm.lock.IsLocked(ctx, key) { return fmt.Errorf("%s is locked", key) } @@ -122,7 +113,7 @@ func TestNewManager_concurrent_blocked(t *testing.T) { ScenarioInitializer: func(s *godog.ScenarioContext) { dbm.RegisterSteps(s) s.Step(`^I should not be blocked for "([^"]*)"$`, func(ctx context.Context, key string) error { - if dbm.sync.isLocked(ctx, key) { + if dbm.lock.IsLocked(ctx, key) { return fmt.Errorf("%s is locked", key) } diff --git a/sync.go b/sync.go deleted file mode 100644 index f8516de..0000000 --- a/sync.go +++ /dev/null @@ -1,116 +0,0 @@ -package dbsteps - -import ( - "context" - "errors" - "strings" - "sync" - - "github.com/cucumber/godog" -) - -type sentinelError string - -// Error returns the error message. -func (e sentinelError) Error() string { - return string(e) -} - -const errMissingScenarioLock = sentinelError("missing scenario lock key in context") - -// synchronized keeps exclusive access to the scenario steps. -type synchronized struct { - mu sync.Mutex - locks map[string]chan struct{} - onRelease func(lockName string) error - ctxKey *struct{ _ int } -} - -func newSynchronized(onRelease func(lockName string) error) *synchronized { - return &synchronized{ - locks: make(map[string]chan struct{}), - onRelease: onRelease, - ctxKey: new(struct{ _ int }), - } -} - -// acquireLock acquires resource lock for the given key and returns true. -// -// If the lock is already held by another context, it waits for the lock to be released. -// It returns false is the lock is already held by this context. -// This function fails if the context is missing current lock. -func (s *synchronized) acquireLock(ctx context.Context, lockName string) (bool, error) { - currentLock, ok := ctx.Value(s.ctxKey).(chan struct{}) - if !ok { - return false, errMissingScenarioLock - } - - s.mu.Lock() - lock := s.locks[lockName] - - if lock == nil { - if s.locks == nil { - s.locks = make(map[string]chan struct{}) - } - - s.locks[lockName] = currentLock - } - - s.mu.Unlock() - - // Wait for the alien lock to be released. - if lock != nil && lock != currentLock { - <-lock - - return s.acquireLock(ctx, lockName) - } - - if lock == nil { - return true, nil - } - - return false, nil -} - -// register adds hooks to scenario context. -func (s *synchronized) register(sc *godog.ScenarioContext) { - sc.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) { - lock := make(chan struct{}) - - // Adding unique pointer to context to avoid collisions. - return context.WithValue(ctx, s.ctxKey, lock), nil - }) - - sc.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) { - s.mu.Lock() - defer s.mu.Unlock() - - // Releasing locks owned by scenario. - currentLock, ok := ctx.Value(s.ctxKey).(chan struct{}) - if !ok { - return ctx, errMissingScenarioLock - } - - var errs []string - - for key, lock := range s.locks { - if lock == currentLock { - delete(s.locks, key) - } - - if s.onRelease != nil { - if err := s.onRelease(key); err != nil { - errs = append(errs, err.Error()) - } - } - } - - close(currentLock) - - if len(errs) > 0 { - return ctx, errors.New(strings.Join(errs, ", ")) // nolint:goerr113 - } - - return ctx, nil - }) -}