Skip to content

Commit 9a7362a

Browse files
committed
impr(locks): handle context cancelation with recovery mecanisms
1 parent 05425c0 commit 9a7362a

1 file changed

Lines changed: 113 additions & 50 deletions

File tree

pkg/lock/etcd.go

Lines changed: 113 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/ctfer-io/chall-manager/global"
1010
"go.etcd.io/etcd/client/v3/concurrency"
11+
"go.uber.org/multierr"
1112
"go.uber.org/zap"
1213
)
1314

@@ -20,6 +21,9 @@ import (
2021
// This implementation goes further than a simple mutex, as it implements the
2122
// readers-writer lock for a writer-preference.
2223
//
24+
// It assumes the network is reliable.
25+
// Moreover, it is unfair as it does not use a queue to order requests as a FIFO.
26+
//
2327
// Based upon 'Concurrent Control with "Readers" and "Writers"' by Courtois et al. (1971)
2428
// DOI: 10.1145/362759.362813
2529
type EtcdRWLock struct {
@@ -31,6 +35,9 @@ type EtcdRWLock struct {
3135
m1, m2, m3, r, w *concurrency.Mutex
3236
// m1 -> /chall-manager/<key>/m1
3337
// m2 -> /chall-manager/<key>/m2
38+
// m3 "prevents too many readers from waiting on mutex r, so writers have a good
39+
// chance to signal r when they come", from user "Attala" on Stackoverflow.
40+
// Ref: https://stackoverflow.com/questions/9974384/second-algorithm-solution-to-readers-writer
3441
// m3 -> /chall-manager/<key>/m3
3542
// r -> /chall-manager/<key>/r
3643
// w -> /chall-manager/<key>/w
@@ -60,26 +67,27 @@ func (lock *EtcdRWLock) Key() string {
6067

6168
func (lock *EtcdRWLock) RLock(ctx context.Context) error {
6269
etcdCli := global.GetEtcdManager()
70+
ctxNc := context.WithoutCancel(ctx)
6371

6472
if err := lock.m3.Lock(ctx); err != nil {
65-
return err
73+
return err // could be context.Canceled
6674
}
67-
defer unlock(ctx, lock.m3)
75+
defer unlock(ctxNc, lock.m3)
6876

6977
if err := lock.r.Lock(ctx); err != nil {
70-
return err
78+
return err // could be context.Canceled
7179
}
72-
defer unlock(ctx, lock.r)
80+
defer unlock(ctxNc, lock.r)
7381

7482
if err := lock.m1.Lock(ctx); err != nil {
75-
return err
83+
return err // could be context.Canceled
7684
}
77-
defer unlock(ctx, lock.m1)
85+
defer unlock(ctxNc, lock.m1)
7886

7987
k := fmt.Sprintf("/chall-manager/%s/readCounter", lock.key)
8088
res, err := etcdCli.Get(ctx, k)
8189
if err != nil {
82-
return err
90+
return err // could be context.Canceled
8391
}
8492
var readCounter int
8593
switch len(res.Kvs) {
@@ -95,30 +103,36 @@ func (lock *EtcdRWLock) RLock(ctx context.Context) error {
95103
return errors.New("invalid etcd filter for " + k)
96104
}
97105
readCounter++
98-
_, perr := etcdCli.Put(ctx, k, strconv.Itoa(readCounter))
99-
// Don't return perr for now, let's avoid race conditions and starvations
106+
_, err = etcdCli.Put(ctx, k, strconv.Itoa(readCounter))
107+
if err != nil {
108+
// Commited no value to etcd so it's fine.
109+
// Defered functions will reach the equilibrium state
110+
return err
111+
}
100112

101113
if readCounter == 1 {
102-
if err := lock.w.Lock(ctx); err != nil {
114+
// Now that we wrote the readcounter, we can't skip the lock else deadlock
115+
if err := lock.w.Lock(ctxNc); err != nil {
103116
return err
104117
}
105118
}
106119

107-
return perr
120+
return nil
108121
}
109122

110123
func (lock *EtcdRWLock) RUnlock(ctx context.Context) error {
111124
etcdCli := global.GetEtcdManager()
125+
ctxNc := context.WithoutCancel(ctx)
112126

113127
if err := lock.m1.Lock(ctx); err != nil {
114-
return err
128+
return err // could be context.Canceled
115129
}
116-
defer unlock(ctx, lock.m1)
130+
defer unlock(ctxNc, lock.m1)
117131

118132
k := fmt.Sprintf("/chall-manager/%s/readCounter", lock.key)
119133
res, err := etcdCli.Get(ctx, k)
120134
if err != nil {
121-
return err
135+
return err // could be context.Canceled
122136
}
123137
var readCounter int
124138
switch len(res.Kvs) {
@@ -132,39 +146,38 @@ func (lock *EtcdRWLock) RUnlock(ctx context.Context) error {
132146
return errors.New("invalid etcd filter for " + k)
133147
}
134148
readCounter--
135-
_, perr := etcdCli.Put(ctx, k, strconv.Itoa(readCounter))
136-
// Don't return perr for now, let's avoid race conditions and starvations
149+
_, err = etcdCli.Put(ctx, k, strconv.Itoa(readCounter))
150+
if err != nil {
151+
// Commited no value to etcd so it's fine.
152+
// Defered functions will reach the equilibrium state
153+
return err
154+
}
137155

138156
if readCounter == 0 {
139-
if err := lock.w.Unlock(ctx); err != nil {
157+
// Now that we wrote the readcounter, we can't skip the unlock else deadlock
158+
if err := lock.w.Unlock(ctxNc); err != nil {
140159
return err
141160
}
142161
}
143162

144-
return perr
163+
return nil
145164
}
146165

147166
func (lock *EtcdRWLock) RWLock(ctx context.Context) error {
148167
etcdCli := global.GetEtcdManager()
149-
150-
defer func(ctx context.Context, mx *concurrency.Mutex) {
151-
if err := mx.Lock(ctx); err != nil {
152-
global.Log().Error(ctx, "failed to lock etcd mutex",
153-
zap.Error(err),
154-
zap.String("key", mx.Key()),
155-
)
156-
}
157-
}(ctx, lock.w)
168+
ctxNc := context.WithoutCancel(ctx)
158169

159170
if err := lock.m2.Lock(ctx); err != nil {
160-
return err
171+
return err // could be context.Canceled
161172
}
162-
defer unlock(ctx, lock.m2)
163173

164174
k := fmt.Sprintf("/chall-manager/%s/writeCounter", lock.key)
165175
res, err := etcdCli.Get(ctx, k)
166176
if err != nil {
167-
return err
177+
if err == context.Canceled {
178+
return lock.m2.Unlock(ctxNc) // stop there, request simply don't need to go further
179+
}
180+
return multierr.Combine(err, lock.m2.Unlock(ctxNc))
168181
}
169182
var writeCounter int
170183
switch len(res.Kvs) {
@@ -174,63 +187,113 @@ func (lock *EtcdRWLock) RWLock(ctx context.Context) error {
174187
str := string(res.Kvs[0].Value)
175188
writeCounter, err = strconv.Atoi(str)
176189
if err != nil {
177-
return errors.New("invalid format for " + k + ", got " + str)
190+
return multierr.Combine(
191+
errors.New("invalid format for "+k+", got "+str),
192+
lock.m2.Unlock(ctxNc),
193+
)
178194
}
179195
default:
180-
return errors.New("invalid etcd filter for " + k)
196+
return multierr.Combine(
197+
errors.New("invalid etcd filter for "+k),
198+
lock.m2.Unlock(ctxNc),
199+
)
181200
}
182201
writeCounter++
183202
_, perr := etcdCli.Put(ctx, k, strconv.Itoa(writeCounter))
184-
// Don't return perr for now, let's avoid race conditions and starvations
203+
if perr != nil {
204+
// Commited no value to etcd so it's fine.
205+
// Defered functions will reach the equilibrium state
206+
return multierr.Combine(
207+
err,
208+
lock.m2.Unlock(ctxNc),
209+
)
210+
}
185211

186212
if writeCounter == 1 {
187-
if err := lock.r.Lock(ctx); err != nil {
188-
return err
213+
// Now that we wrote the writecounter, we can't skip the lock else deadlock
214+
if err := lock.r.Lock(ctxNc); err != nil {
215+
return multierr.Combine(
216+
err,
217+
lock.m2.Unlock(ctxNc),
218+
lock.w.Lock(ctxNc), // don't forget we need to lock W to avoid deadlock and keep the equilibrium state
219+
)
189220
}
190221
}
191222

192-
return perr
223+
return multierr.Combine(
224+
lock.m2.Unlock(ctxNc),
225+
lock.w.Lock(ctxNc),
226+
)
193227
}
194228

195229
func (lock *EtcdRWLock) RWUnlock(ctx context.Context) error {
196230
etcdCli := global.GetEtcdManager()
197-
198-
if err := lock.w.Unlock(ctx); err != nil {
199-
return err
200-
}
231+
ctxNc := context.WithoutCancel(ctx)
232+
233+
// We cannot start by V(w) as in Courtois et al. paper, as if something goes wrong
234+
// we might be tempted to recover using P(w).
235+
//
236+
// Nonetheless, we have no guarantee that re-locking w will end shortly, thus
237+
// might starve indefinitely without possibility to complete request handling...
238+
// Then, we consider this operation unrecoverable hence perform it at last.
239+
//
240+
// This does not invalidate the Courtois et al. paper, simply reconsider unrelated
241+
// (in the meaning of involved locks and values) steps that are less efficient in
242+
// time to profit recoverability.
201243

202244
if err := lock.m2.Lock(ctx); err != nil {
203-
return err
245+
return err // could be context.Canceled
204246
}
205-
defer unlock(ctx, lock.m2)
206247

207248
k := fmt.Sprintf("/chall-manager/%s/writeCounter", lock.key)
208249
res, err := etcdCli.Get(ctx, k)
209250
if err != nil {
210-
return err
251+
return multierr.Combine(
252+
err, // Could be context.Canceled
253+
lock.m2.Unlock(ctxNc),
254+
)
211255
}
212256
var writeCounter int
213257
switch len(res.Kvs) {
214258
case 1:
215259
str := string(res.Kvs[0].Value)
216260
writeCounter, err = strconv.Atoi(str)
217261
if err != nil {
218-
return errors.New("invalid format for " + k + ", got " + str)
262+
return multierr.Combine(
263+
errors.New("invalid format for "+k+", got "+str),
264+
lock.m2.Unlock(ctxNc),
265+
)
219266
}
220267
default:
221-
return errors.New("invalid etcd filter for " + k)
268+
return multierr.Combine(
269+
errors.New("invalid etcd filter for "+k),
270+
lock.m2.Unlock(ctxNc),
271+
)
222272
}
223273
writeCounter--
224-
_, perr := etcdCli.Put(ctx, k, strconv.Itoa(writeCounter))
225-
// Don't return perr for now, let's avoid race conditions and starvations
274+
_, err = etcdCli.Put(ctx, k, strconv.Itoa(writeCounter))
275+
if err != nil {
276+
// Commited no value to etcd so it's fine.
277+
return multierr.Combine(
278+
err,
279+
lock.m2.Unlock(ctxNc),
280+
)
281+
}
226282

227283
if writeCounter == 0 {
228-
if err := lock.r.Unlock(ctx); err != nil {
229-
return err
284+
// Now that we wrote the writecounter, we can't skip the unlock else deadlock
285+
if err := lock.r.Unlock(ctxNc); err != nil {
286+
return multierr.Combine(
287+
err,
288+
lock.m2.Unlock(ctxNc),
289+
)
230290
}
231291
}
232292

233-
return perr
293+
// Don't forget the unrecoverable V(w) we discussed at the very beginning, we
294+
// here need to do it.
295+
// As we reached the critical section we MUST commit this change.
296+
return lock.w.Unlock(ctxNc)
234297
}
235298

236299
func (lock *EtcdRWLock) Close() error {

0 commit comments

Comments
 (0)