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  }