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
2529type 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
6168func (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
110123func (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
147166func (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
195229func (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
236299func (lock * EtcdRWLock ) Close () error {
0 commit comments