1/*
2Package locker provides a mechanism for creating finer-grained locking to help
3free up more global locks to handle other tasks.
4
5The implementation looks close to a sync.Mutex, however the user must provide a
6reference to use to refer to the underlying lock when locking and unlocking,
7and unlock may generate an error.
8
9If a lock with a given name does not exist when `Lock` is called, one is
10created.
11Lock references are automatically cleaned up on `Unlock` if nothing else is
12waiting for the lock.
13*/
14package locker
15
16import (
17	"errors"
18	"sync"
19	"sync/atomic"
20)
21
22// ErrNoSuchLock is returned when the requested lock does not exist
23var ErrNoSuchLock = errors.New("no such lock")
24
25// Locker provides a locking mechanism based on the passed in reference name
26type Locker struct {
27	mu    sync.Mutex
28	locks map[string]*lockCtr
29}
30
31// lockCtr is used by Locker to represent a lock with a given name.
32type lockCtr struct {
33	mu sync.Mutex
34	// waiters is the number of waiters waiting to acquire the lock
35	// this is int32 instead of uint32 so we can add `-1` in `dec()`
36	waiters int32
37}
38
39// inc increments the number of waiters waiting for the lock
40func (l *lockCtr) inc() {
41	atomic.AddInt32(&l.waiters, 1)
42}
43
44// dec decrements the number of waiters waiting on the lock
45func (l *lockCtr) dec() {
46	atomic.AddInt32(&l.waiters, -1)
47}
48
49// count gets the current number of waiters
50func (l *lockCtr) count() int32 {
51	return atomic.LoadInt32(&l.waiters)
52}
53
54// Lock locks the mutex
55func (l *lockCtr) Lock() {
56	l.mu.Lock()
57}
58
59// Unlock unlocks the mutex
60func (l *lockCtr) Unlock() {
61	l.mu.Unlock()
62}
63
64// New creates a new Locker
65func New() *Locker {
66	return &Locker{
67		locks: make(map[string]*lockCtr),
68	}
69}
70
71// Lock locks a mutex with the given name. If it doesn't exist, one is created
72func (l *Locker) Lock(name string) {
73	l.mu.Lock()
74	if l.locks == nil {
75		l.locks = make(map[string]*lockCtr)
76	}
77
78	nameLock, exists := l.locks[name]
79	if !exists {
80		nameLock = &lockCtr{}
81		l.locks[name] = nameLock
82	}
83
84	// increment the nameLock waiters while inside the main mutex
85	// this makes sure that the lock isn't deleted if `Lock` and `Unlock` are called concurrently
86	nameLock.inc()
87	l.mu.Unlock()
88
89	// Lock the nameLock outside the main mutex so we don't block other operations
90	// once locked then we can decrement the number of waiters for this lock
91	nameLock.Lock()
92	nameLock.dec()
93}
94
95// Unlock unlocks the mutex with the given name
96// If the given lock is not being waited on by any other callers, it is deleted
97func (l *Locker) Unlock(name string) error {
98	l.mu.Lock()
99	nameLock, exists := l.locks[name]
100	if !exists {
101		l.mu.Unlock()
102		return ErrNoSuchLock
103	}
104
105	if nameLock.count() == 0 {
106		delete(l.locks, name)
107	}
108	nameLock.Unlock()
109
110	l.mu.Unlock()
111	return nil
112}
113