github.com/mattermost/mattermost-plugin-api@v0.1.4/cluster/mutex.go (about) 1 package cluster 2 3 import ( 4 "context" 5 "sync" 6 "time" 7 8 "github.com/mattermost/mattermost-server/v6/model" 9 "github.com/pkg/errors" 10 ) 11 12 const ( 13 // mutexPrefix is used to namespace key values created for a mutex from other key values 14 // created by a plugin. 15 mutexPrefix = "mutex_" 16 ) 17 18 const ( 19 // ttl is the interval after which a locked mutex will expire unless refreshed 20 ttl = time.Second * 15 21 22 // refreshInterval is the interval on which the mutex will be refreshed when locked 23 refreshInterval = ttl / 2 24 ) 25 26 // MutexPluginAPI is the plugin API interface required to manage mutexes. 27 type MutexPluginAPI interface { 28 KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) 29 LogError(msg string, keyValuePairs ...interface{}) 30 } 31 32 // Mutex is similar to sync.Mutex, except usable by multiple plugin instances across a cluster. 33 // 34 // Internally, a mutex relies on an atomic key-value set operation as exposed by the Mattermost 35 // plugin API. 36 // 37 // Mutexes with different names are unrelated. Mutexes with the same name from different plugins 38 // are unrelated. Pick a unique name for each mutex your plugin requires. 39 // 40 // A Mutex must not be copied after first use. 41 type Mutex struct { 42 pluginAPI MutexPluginAPI 43 key string 44 45 // lock guards the variables used to manage the refresh task, and is not itself related to 46 // the cluster-wide lock. 47 lock sync.Mutex 48 stopRefresh chan bool 49 refreshDone chan bool 50 } 51 52 // NewMutex creates a mutex with the given key name. 53 // 54 // Panics if key is empty. 55 func NewMutex(pluginAPI MutexPluginAPI, key string) (*Mutex, error) { 56 key, err := makeLockKey(key) 57 if err != nil { 58 return nil, err 59 } 60 61 return &Mutex{ 62 pluginAPI: pluginAPI, 63 key: key, 64 }, nil 65 } 66 67 // makeLockKey returns the prefixed key used to namespace mutex keys. 68 func makeLockKey(key string) (string, error) { 69 if key == "" { 70 return "", errors.New("must specify valid mutex key") 71 } 72 73 return mutexPrefix + key, nil 74 } 75 76 // lock makes a single attempt to atomically lock the mutex, returning true only if successful. 77 func (m *Mutex) tryLock() (bool, error) { 78 ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{ 79 Atomic: true, 80 OldValue: nil, // No existing key value. 81 ExpireInSeconds: int64(ttl / time.Second), 82 }) 83 if err != nil { 84 return false, errors.Wrap(err, "failed to set mutex kv") 85 } 86 87 return ok, nil 88 } 89 90 // refreshLock rewrites the lock key value with a new expiry, returning true only if successful. 91 func (m *Mutex) refreshLock() error { 92 ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{ 93 Atomic: true, 94 OldValue: []byte{1}, 95 ExpireInSeconds: int64(ttl / time.Second), 96 }) 97 if err != nil { 98 return errors.Wrap(err, "failed to refresh mutex kv") 99 } else if !ok { 100 return errors.New("unexpectedly failed to refresh mutex kv") 101 } 102 103 return nil 104 } 105 106 // Lock locks m. If the mutex is already locked by any plugin instance, including the current one, 107 // the calling goroutine blocks until the mutex can be locked. 108 func (m *Mutex) Lock() { 109 _ = m.LockWithContext(context.Background()) 110 } 111 112 // LockWithContext locks m unless the context is canceled. If the mutex is already locked by any plugin 113 // instance, including the current one, the calling goroutine blocks until the mutex can be locked, 114 // or the context is canceled. 115 // 116 // The mutex is locked only if a nil error is returned. 117 func (m *Mutex) LockWithContext(ctx context.Context) error { 118 var waitInterval time.Duration 119 120 for { 121 select { 122 case <-ctx.Done(): 123 return ctx.Err() 124 case <-time.After(waitInterval): 125 } 126 127 locked, err := m.tryLock() 128 if err != nil { 129 m.pluginAPI.LogError("failed to lock mutex", "err", err, "lock_key", m.key) 130 waitInterval = nextWaitInterval(waitInterval, err) 131 continue 132 } else if !locked { 133 waitInterval = nextWaitInterval(waitInterval, err) 134 continue 135 } 136 137 stop := make(chan bool) 138 done := make(chan bool) 139 go func() { 140 defer close(done) 141 t := time.NewTicker(refreshInterval) 142 for { 143 select { 144 case <-t.C: 145 err := m.refreshLock() 146 if err != nil { 147 m.pluginAPI.LogError("failed to refresh mutex", "err", err, "lock_key", m.key) 148 return 149 } 150 case <-stop: 151 return 152 } 153 } 154 }() 155 156 m.lock.Lock() 157 m.stopRefresh = stop 158 m.refreshDone = done 159 m.lock.Unlock() 160 161 return nil 162 } 163 } 164 165 // Unlock unlocks m. It is a run-time error if m is not locked on entry to Unlock. 166 // 167 // Just like sync.Mutex, a locked Lock is not associated with a particular goroutine or plugin 168 // instance. It is allowed for one goroutine or plugin instance to lock a Lock and then arrange 169 // for another goroutine or plugin instance to unlock it. In practice, ownership of the lock should 170 // remain within a single plugin instance. 171 func (m *Mutex) Unlock() { 172 m.lock.Lock() 173 if m.stopRefresh == nil { 174 m.lock.Unlock() 175 panic("mutex has not been acquired") 176 } 177 178 close(m.stopRefresh) 179 m.stopRefresh = nil 180 <-m.refreshDone 181 m.lock.Unlock() 182 183 // If an error occurs deleting, the mutex kv will still expire, allowing later retry. 184 _, _ = m.pluginAPI.KVSetWithOptions(m.key, nil, model.PluginKVSetOptions{}) 185 }