/
mutex.go
142 lines (133 loc) · 3.1 KB
/
mutex.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// Synchronization built on top of Redis.
// Depends on github.com/garyburd/redigo/redis
package redisync
import (
"fmt"
"github.com/garyburd/redigo/redis"
"os"
"sync"
"time"
)
/*
Locking algorithm:
if the key exists
if caller is owner of lock
update ttl
return true
else return false
if the key does not exist
set owner
set ttl
return true
*/
var luaLock = `
local call_owner = ARGV[1]
local ttl = ARGV[2]
if redis.call('EXISTS', KEYS[1]) == 1 then
local lock_owner = redis.call('GET', KEYS[1])
if lock_owner == call_owner then
redis.call('EXPIRE', KEYS[1], ttl)
return 1
end
return 0
else
redis.call('SET', KEYS[1], call_owner)
redis.call('EXPIRE', KEYS[1], ttl)
return 1
end
`
/*
Unlocking algorithm:
if the key exists
if owner of lock
delete key
return true
else return false
else return false
*/
var luaUnlock = `
local call_owner = ARGV[1]
if redis.call('EXISTS', KEYS[1]) == 1 then
local lock_owner = redis.call('GET', KEYS[1])
if lock_owner == call_owner then
redis.call('DEL', KEYS[1])
return 1
end
return 0
else
return 0
end
`
type Mutex struct {
// The key used in Redis.
Name string
// The amount of time before Redis will expire the lock.
Ttl time.Duration
// The time to sleep before retrying a lock attempt.
Backoff time.Duration
// A uuid representing the local instantiation of the mutex.
id string
// Local conrrency controll.
l sync.Mutex
// See lock.lua
lock *redis.Script
// See unlock.lua
unlock *redis.Script
}
// Each lock will have a name which corresponds to a key in the Redis server.
// The mutex will also be initialized with a uuid. The mutex uuid
// can be used to extend the TTL for the lock.
func NewMutex(name string, ttl time.Duration) *Mutex {
m := new(Mutex)
m.Name = name
m.Ttl = ttl
m.Backoff = time.Second
m.id = uuid()
m.lock = redis.NewScript(1, luaLock)
m.unlock = redis.NewScript(1, luaUnlock)
return m
}
// With similar behaviour to Go's sync pkg,
// this function will sleep until TryLock() returns true.
// The connection will be used once to execute the lock script.
func (m *Mutex) Lock(c redis.Conn) {
for {
if m.TryLock(c) {
return
}
time.Sleep(m.Backoff)
}
}
// Makes a single attempt to acquire the lock.
// Locking a mutex which has already been locked
// using the mutex uuid will result in the TTL of the mutex being extended.
// The connection will be used once to execute the lock script.
func (m *Mutex) TryLock(c redis.Conn) bool {
m.l.Lock()
defer m.l.Unlock()
reply, err := m.lock.Do(c, m.Name, m.id, m.Ttl.Seconds())
if err != nil {
return false
}
return reply.(int64) == 1
}
// If the local mutex uuid matches the uuid in Redis,
// the lock will be deleted.
// The connection will be used once to execute the unlock script.
func (m *Mutex) Unlock(c redis.Conn) (bool, error) {
m.l.Lock()
defer m.l.Unlock()
reply, err := m.unlock.Do(c, m.Name, m.id)
if err != nil {
return false, err
}
return reply.(int64) == 1, nil
}
func uuid() string {
f, _ := os.Open("/dev/urandom")
b := make([]byte, 16)
f.Read(b)
f.Close()
return fmt.Sprintf("%x-%x-%x-%x-%x",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}