Skip to content

Commit d66689d

Browse files
polysconemholt
andauthored
Add TryLock for use with optional tasks like ARI updates to reduce lock contention (#357)
* Implement `TryLock` for use with optional tasks like ARI updates to reduce lock contention * Update maintain.go --------- Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
1 parent aba1313 commit d66689d

File tree

3 files changed

+83
-11
lines changed

3 files changed

+83
-11
lines changed

filestorage.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,10 @@ func (s *FileStorage) Filename(key string) string {
168168
return filepath.Join(s.Path, filepath.FromSlash(key))
169169
}
170170

171-
// Lock obtains a lock named by the given name. It blocks
172-
// until the lock can be obtained or an error is returned.
173-
func (s *FileStorage) Lock(ctx context.Context, name string) error {
171+
// obtainLock will attempt to obtain a lock for the given name up to the
172+
// number of attempts given.
173+
// if attempts is negative then it will try forever.
174+
func (s *FileStorage) obtainLock(ctx context.Context, name string, attempts int) (bool, error) {
174175
filename := s.lockFilename(name)
175176

176177
// sometimes the lockfiles read as empty (size 0) - this is either a stale lock or it
@@ -179,14 +180,26 @@ func (s *FileStorage) Lock(ctx context.Context, name string) error {
179180
var emptyCount int
180181

181182
for {
183+
// if attempts is negative then we should allow the loop
184+
// to retry until the context is done, otherwise we decrement
185+
// the remaining attempts if there are any here to ensure we
186+
// don't miss it due to continue statements throughout the loop
187+
switch {
188+
case attempts == 0:
189+
return false, nil
190+
191+
case attempts > 0:
192+
attempts--
193+
}
194+
182195
err := createLockfile(filename)
183196
if err == nil {
184197
// got the lock, yay
185-
return nil
198+
return true, nil
186199
}
187200
if !os.IsExist(err) {
188201
// unexpected error
189-
return fmt.Errorf("creating lock file: %v", err)
202+
return false, fmt.Errorf("creating lock file: %v", err)
190203
}
191204

192205
// lock file already exists
@@ -204,7 +217,7 @@ func (s *FileStorage) Lock(ctx context.Context, name string) error {
204217
select {
205218
case <-time.After(250 * time.Millisecond):
206219
case <-ctx.Done():
207-
return ctx.Err()
220+
return false, ctx.Err()
208221
}
209222
continue
210223
} else {
@@ -215,7 +228,7 @@ func (s *FileStorage) Lock(ctx context.Context, name string) error {
215228
defaultLogger.Sugar().Infof("[%s] %s: Empty lockfile (%v) - likely previous process crashed or storage medium failure; treating as stale", s, filename, err2)
216229
}
217230
} else if err2 != nil {
218-
return fmt.Errorf("decoding lockfile contents: %w", err2)
231+
return false, fmt.Errorf("decoding lockfile contents: %w", err2)
219232
}
220233
}
221234

@@ -226,7 +239,7 @@ func (s *FileStorage) Lock(ctx context.Context, name string) error {
226239

227240
case err != nil:
228241
// unexpected error
229-
return fmt.Errorf("accessing lock file: %v", err)
242+
return false, fmt.Errorf("accessing lock file: %v", err)
230243

231244
case fileLockIsStale(meta):
232245
// lock file is stale - delete it and try again to obtain lock
@@ -237,7 +250,7 @@ func (s *FileStorage) Lock(ctx context.Context, name string) error {
237250
defaultLogger.Sugar().Infof("[%s] Lock for '%s' is stale (created: %s, last update: %s); removing then retrying: %s", s, name, meta.Created, meta.Updated, filename)
238251
if err = os.Remove(filename); err != nil { // hopefully we can replace the lock file quickly!
239252
if !errors.Is(err, fs.ErrNotExist) {
240-
return fmt.Errorf("unable to delete stale lockfile; deadlocked: %w", err)
253+
return false, fmt.Errorf("unable to delete stale lockfile; deadlocked: %w", err)
241254
}
242255
}
243256
continue
@@ -249,12 +262,30 @@ func (s *FileStorage) Lock(ctx context.Context, name string) error {
249262
select {
250263
case <-time.After(fileLockPollInterval):
251264
case <-ctx.Done():
252-
return ctx.Err()
265+
return false, ctx.Err()
253266
}
254267
}
255268
}
256269
}
257270

271+
// Lock obtains a lock named by the given name. It blocks
272+
// until the lock can be obtained or an error is returned.
273+
func (s *FileStorage) Lock(ctx context.Context, name string) error {
274+
ok, err := s.obtainLock(ctx, name, -1)
275+
if !ok && err == nil {
276+
return errors.New("unable to obtain lock")
277+
}
278+
279+
return err
280+
}
281+
282+
// TryLock attempts to obtain a lock named by the given name.
283+
// If the lock was obtained it will return true, otherwise it will
284+
// return false along with any errors that may have occurred.
285+
func (s *FileStorage) TryLock(ctx context.Context, name string) (bool, error) {
286+
return s.obtainLock(ctx, name, 2)
287+
}
288+
258289
// Unlock releases the lock for name.
259290
func (s *FileStorage) Unlock(_ context.Context, name string) error {
260291
return os.Remove(s.lockFilename(name))

maintain.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,16 @@ func (cfg *Config) updateARI(ctx context.Context, cert Certificate, logger *zap.
468468

469469
// synchronize ARI fetching; see #297
470470
lockName := "ari_" + cert.ari.UniqueIdentifier
471-
if err := acquireLock(ctx, cfg.Storage, lockName); err != nil {
471+
if _, ok := cfg.Storage.(TryLocker); ok {
472+
ok, err := tryAcquireLock(ctx, cfg.Storage, lockName)
473+
if err != nil {
474+
return cert, false, fmt.Errorf("unable to obtain ARI lock: %v", err)
475+
}
476+
if !ok {
477+
logger.Debug("attempted to obtain ARI lock but it was already taken")
478+
return cert, false, nil
479+
}
480+
} else if err := acquireLock(ctx, cfg.Storage, lockName); err != nil {
472481
return cert, false, fmt.Errorf("unable to obtain ARI lock: %v", err)
473482
}
474483
defer func() {

storage.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package certmagic
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"path"
2021
"regexp"
2122
"strings"
@@ -149,6 +150,22 @@ type Locker interface {
149150
Unlock(ctx context.Context, name string) error
150151
}
151152

153+
type TryLocker interface {
154+
// TryLock attempts to acquire the lock for name, and returns a
155+
// boolean that reports whether the lock was successfully aquired
156+
// or not along with any errors that may have occurred.
157+
//
158+
// Implementations should honor context cancellation.
159+
TryLock(ctx context.Context, name string) (bool, error)
160+
161+
// Unlock releases named lock. This method must ONLY be called
162+
// after a successful call to TryLock, and only after the critical
163+
// section is finished, even if it errored or timed out. Unlock
164+
// cleans up any resources allocated during TryLock. Unlock should
165+
// only return an error if the lock was unable to be released.
166+
Unlock(ctx context.Context, name string) error
167+
}
168+
152169
// LockLeaseRenewer is an optional interface that can be implemented by a Storage
153170
// implementation to support renewing the lease on a lock. This is useful for
154171
// long-running operations that need to be synchronized across a cluster.
@@ -298,6 +315,21 @@ func acquireLock(ctx context.Context, storage Storage, lockKey string) error {
298315
return err
299316
}
300317

318+
func tryAcquireLock(ctx context.Context, storage Storage, lockKey string) (bool, error) {
319+
locker, ok := storage.(TryLocker)
320+
if !ok {
321+
return false, fmt.Errorf("%T does not implement TryLocker", storage)
322+
}
323+
324+
ok, err := locker.TryLock(ctx, lockKey)
325+
if ok && err == nil {
326+
locksMu.Lock()
327+
locks[lockKey] = storage
328+
locksMu.Unlock()
329+
}
330+
return ok, err
331+
}
332+
301333
func releaseLock(ctx context.Context, storage Storage, lockKey string) error {
302334
err := storage.Unlock(context.WithoutCancel(ctx), lockKey)
303335
if err == nil {

0 commit comments

Comments
 (0)