generated from bool64/go-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
lock.go
134 lines (105 loc) · 3.13 KB
/
lock.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Package resource provides a helper to synchronize access to a shared resources from concurrent scenarios.
package resource
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)
}
// ErrMissingScenarioLock is a sentinel error.
const ErrMissingScenarioLock = sentinelError("missing scenario lock key in context")
// Lock keeps exclusive access to the scenario steps.
type Lock struct {
mu sync.Mutex
locks map[string]chan struct{}
onRelease func(lockName string) error
ctxKey *struct{ _ int }
}
// NewLock creates a new Lock.
func NewLock(onRelease func(name string) error) *Lock {
return &Lock{
locks: make(map[string]chan struct{}),
onRelease: onRelease,
ctxKey: new(struct{ _ int }),
}
}
// Acquire 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 *Lock) Acquire(ctx context.Context, name string) (bool, error) {
currentLock, ok := ctx.Value(s.ctxKey).(chan struct{})
if !ok {
return false, ErrMissingScenarioLock
}
s.mu.Lock()
lock := s.locks[name]
if lock == nil {
s.locks[name] = currentLock
}
s.mu.Unlock()
// Wait for the alien lock to be released.
if lock != nil && lock != currentLock {
<-lock
return s.Acquire(ctx, name)
}
if lock == nil {
return true, nil
}
return false, nil
}
// Register adds hooks to scenario context.
func (s *Lock) 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 name, lock := range s.locks {
if lock == currentLock {
delete(s.locks, name)
}
if s.onRelease != nil {
if err := s.onRelease(name); err != nil {
errs = append(errs, err.Error())
}
}
}
// Godog v0.12.5 has an issue of calling after scenario multiple times when there are undefined steps.
// This is a workaround.
closeIfNot := func() {
defer func() {
_ = recover() //nolint: errcheck // Only close of the closed channel can panic here.
}()
close(currentLock)
}
closeIfNot()
if len(errs) > 0 {
return ctx, errors.New(strings.Join(errs, ", ")) //nolint:goerr113
}
return ctx, nil
})
}
// IsLocked is true if resource is currently locked for another scenario.
func (s *Lock) IsLocked(ctx context.Context, name string) bool {
s.mu.Lock()
defer s.mu.Unlock()
lock := s.locks[name]
return lock != nil && lock != ctx.Value(s.ctxKey).(chan struct{})
}