@@ -10,22 +10,24 @@ import (
10
10
"go.viam.com/utils"
11
11
)
12
12
13
- // SingleOperationManager ensures only 1 operation is happening a time
13
+ // SingleOperationManager ensures only 1 operation is happening at a time.
14
14
// An operation can be nested, so if there is already an operation in progress,
15
15
// it can have sub-operations without an issue.
16
16
type SingleOperationManager struct {
17
17
mu sync.Mutex
18
18
currentOp * anOp
19
19
}
20
20
21
- // CancelRunning cancel's a current operation unless it's mine.
21
+ // CancelRunning cancels a current operation unless it's mine.
22
22
func (sm * SingleOperationManager ) CancelRunning (ctx context.Context ) {
23
23
if ctx .Value (somCtxKeySingleOp ) != nil {
24
24
return
25
25
}
26
26
sm .mu .Lock ()
27
27
defer sm .mu .Unlock ()
28
- sm .cancelInLock (ctx )
28
+ if sm .currentOp != nil {
29
+ sm .currentOp .cancelAndWaitFunc ()
30
+ }
29
31
}
30
32
31
33
// OpRunning returns if there is a current operation.
@@ -41,32 +43,49 @@ const somCtxKeySingleOp = somCtxKey(iota)
41
43
42
44
// New creates a new operation, cancels previous, returns a new context and function to call when done.
43
45
func (sm * SingleOperationManager ) New (ctx context.Context ) (context.Context , func ()) {
44
- // handle nested ops
46
+ // Handle nested ops. Note an operation set on a context by one `SingleOperationManager` can be
47
+ // observed on a different instance of a `SingleOperationManager`.
45
48
if ctx .Value (somCtxKeySingleOp ) != nil {
46
49
return ctx , func () {}
47
50
}
48
51
49
52
sm .mu .Lock ()
50
53
51
- // first cancel any old operation
52
- sm .cancelInLock (ctx )
54
+ // Cancel any existing operation. This blocks until the operation is completed.
55
+ if sm .currentOp != nil {
56
+ sm .currentOp .cancelAndWaitFunc ()
57
+ }
53
58
54
- theOp := & anOp {}
59
+ theOp := & anOp {
60
+ closedCond : sync .NewCond (& sm .mu ),
61
+ }
55
62
56
63
ctx = context .WithValue (ctx , somCtxKeySingleOp , theOp )
57
64
58
- theOp .ctx , theOp .cancelFunc = context .WithCancel (ctx )
65
+ var newUserCtx context.Context
66
+ newUserCtx , theOp .interruptFunc = context .WithCancel (ctx )
67
+ theOp .cancelAndWaitFunc = func () {
68
+ // Precondition: Caller must be holding `sm.mu`.
69
+ //
70
+ // If there are two threads competing to win a race, it's not sufficient to return once the
71
+ // condition variable is signaled. We must re-check that a new operation didn't beat us to
72
+ // getting the next operation slot.
73
+ //
74
+ // Ironically, "winning the race" in this scenario just means the "loser" is going to
75
+ // immediately interrupt the winner. A future optimization could avoid this unnecessary
76
+ // starting/stopping.
77
+ for sm .currentOp != nil {
78
+ sm .currentOp .interruptFunc ()
79
+ sm .currentOp .closedCond .Wait ()
80
+ }
81
+ }
59
82
sm .currentOp = theOp
60
83
sm .mu .Unlock ()
61
84
62
- return theOp .ctx , func () {
63
- if ! theOp .closed {
64
- theOp .closed = true
65
- }
85
+ return newUserCtx , func () {
66
86
sm .mu .Lock ()
67
- if theOp == sm .currentOp {
68
- sm .currentOp = nil
69
- }
87
+ theOp .closedCond .Broadcast ()
88
+ sm .currentOp = nil
70
89
sm .mu .Unlock ()
71
90
}
72
91
}
@@ -93,17 +112,11 @@ func (sm *SingleOperationManager) WaitTillNotPowered(ctx context.Context, pollTi
93
112
) (err error ) {
94
113
// Defers a function that will stop and clean up if the context errors
95
114
defer func (ctx context.Context ) {
96
- var errStop error
97
115
if errors .Is (ctx .Err (), context .Canceled ) {
98
- sm .mu .Lock ()
99
- oldOp := sm .currentOp == ctx .Value (somCtxKeySingleOp )
100
- sm .mu .Unlock ()
101
-
102
- if oldOp || sm .currentOp == nil {
103
- errStop = stop (ctx , map [string ]interface {}{})
104
- }
116
+ err = multierr .Combine (ctx .Err (), stop (ctx , map [string ]interface {}{}))
117
+ } else {
118
+ err = ctx .Err ()
105
119
}
106
- err = multierr .Combine (ctx .Err (), errStop )
107
120
}(ctx )
108
121
return sm .WaitForSuccess (
109
122
ctx ,
@@ -139,21 +152,12 @@ func (sm *SingleOperationManager) WaitForSuccess(
139
152
}
140
153
}
141
154
142
- func (sm * SingleOperationManager ) cancelInLock (ctx context.Context ) {
143
- myOp := ctx .Value (somCtxKeySingleOp )
144
- op := sm .currentOp
145
-
146
- if op == nil || myOp == op {
147
- return
148
- }
149
-
150
- op .cancelFunc ()
151
-
152
- sm .currentOp = nil
153
- }
154
-
155
155
type anOp struct {
156
- ctx context.Context
157
- cancelFunc context.CancelFunc
158
- closed bool
156
+ // cancelAndWaitFunc waits until the `SingleOperationManager.currentOp` is empty. This will
157
+ // interrupt any existing operations as necessary.
158
+ cancelAndWaitFunc func ()
159
+ // Cancels the context of what's currently running an operation.
160
+ interruptFunc context.CancelFunc
161
+ // Used with `SingleOperationManager.mu`.
162
+ closedCond * sync.Cond
159
163
}
0 commit comments