redsync

package module
v2.0.2+incompatible Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2018 License: BSD-3-Clause Imports: 6 Imported by: 1

README

Redsync

Build Status codecov

GoDoc license

Redsync provides a Redis-based distributed mutual exclusion lock implementation for Go as described in this post.

A reference library (by antirez) for Ruby is available at github.com/antirez/redlock-rb.

Installation

Install Redsync using the go get command:

$ go get github.com/rgalanakis/redsync

Dependencies are the Go distribution and Redigo. It also Redigomock for testing.

Examples

Taking a distributed lock:

host := "localhost:6379"
pool := &redis.Pool{Dial: redsync.TcpDialer(host)}
pools := []*redis.Pool{pool}
mutex := redsync.New(pools).NewMutex("redsync-example", redsync.NonBlocking())

// Use Mutex#Lock and Mutex#Unlock manually
if mutex.Lock() != nil {
    defer mutex.Unlock()
    expensiveOperation()
}

// Or use Mutex#WithLock to execute something conditionally.
mutex.WithLock(expensiveOperation)

Documentation

Contributing

Contributions are welcome.

License

Redsync is available under the BSD (3-Clause) License.

Disclaimer

This code implements an algorithm which is currently a proposal, it was not formally analyzed. Make sure to understand how it works before using it in production environments.

Documentation

Overview

Package redsync provides a Redis-based distributed mutual exclusion lock implementation as described in the post http://redis.io/topics/distlock.

See examples for suggestions on how to use the lock.

Testing with locks

This package uses a combination of testing against real redis servers using tempredis, and in-memory mocking using redigomock. Clients of redsync are expected to test against redigomock, rather than having to run real redis. There are helpers available for testing with locks in the redsync/rstest package. The Mutex examples include usages of rstest, in particular rstest.AddLockExpects. Please refer to them for examples of how to use mocks when testing redsync locks.

Example
package main

import (
	"github.com/gomodule/redigo/redis"
	"github.com/rgalanakis/redsync"
)

func expensiveOperation() {}

func main() {
	host := "localhost:6379"
	pool := &redis.Pool{Dial: redsync.TcpDialer(host)}
	mutex := redsync.New(pool).NewMutex("redsync-example", redsync.NonBlocking())

	// Use Mutex#Lock and Mutex#Unlock manually
	if mutex.Lock() != nil {
		defer mutex.Unlock()
		expensiveOperation()
	}

	// Or use Mutex#WithLock to execute something conditionally.
	mutex.WithLock(expensiveOperation)
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrFailed = errors.New("redsync: failed to acquire lock")

Functions

func Quorum

func Quorum(n int) int

Quorum returns the number of servers that must agree in order to acquire a lock (n/2 + 1).

Types

type Dialer

type Dialer func() (redis.Conn, error)

Dialer functions return an item with the redis.Conn interface, or an error. It fulfills the interface for the Dial argument to a redis.Pool.

func TcpDialer

func TcpDialer(addr string) Dialer

TcpDialer connects to an address string, like "localhost:6379".

func UnixDialer

func UnixDialer(addr string) Dialer

UnixDialer connects to an address string, like "/var/folders/6j/xyz/T/abc/redis.sock".

type Mutex

type Mutex struct {
	// contains filtered or unexported fields
}

Mutex is a distributed mutual exclusion lock. Note that a redsync.Mutex is not goroutine-safe. Each goroutine should create its own Mutex instance for locking. Note that Redsync instances are threadsafe, so they can be reused across goroutines.

func (*Mutex) Lock

func (m *Mutex) Lock() error

Lock acquires a lock on the mutex with the receiver's Name. If Lock returns nil, the lock is acquired. Callers should make sure Unlock is called, usually via defer m.Unlock(). If Lock returns ErrFailed, the lock could not be acquired because it was held by another mutex. Callers may wish to call Lock() again to retry. If Lock returns any other error, the lock may not be acquire-able do to an unexpected error, like if redis is not running.

Example
package main

import (
	"fmt"
	"github.com/rafaeljusto/redigomock"
	"github.com/rgalanakis/redsync"
	"github.com/rgalanakis/redsync/rstest"
)

func expensiveOperation() {}

func main() {
	conn := redigomock.NewConn()
	rstest.AddLockExpects(conn, "example-mutex-lock", "OK")

	pools := rstest.PoolsForConn(conn, 1)
	mutex := redsync.New(pools...).NewMutex("example-mutex-lock", redsync.NonBlocking())
	err := mutex.Lock()
	if err == redsync.ErrFailed {
		fmt.Println("Failed to acquire lock.")
	} else if err != nil {
		fmt.Println("Lock acquisition had unexpected error")
	} else {
		fmt.Println("Acquired lock")
		defer mutex.Unlock()
		expensiveOperation()
	}
}
Output:

Acquired lock

func (*Mutex) Name

func (m *Mutex) Name() string

Name returns the mutex name.

func (*Mutex) String

func (m *Mutex) String() string

String returns a string representation of the mutex.

func (*Mutex) Unlock

func (m *Mutex) Unlock() bool

Unlock unlocks m and returns the status of unlock.

func (*Mutex) Value

func (m *Mutex) Value() string

Value returns the mutex value.

func (*Mutex) WithLock

func (m *Mutex) WithLock(f func()) (bool, error)

WithLock invokes f if the lock was successfully invoked. See Lock for more info. The boolean return value is true if the lock was acquired and f was invoked, false if not. The error is only non-nil if an unexpected error occurred. In other words, if Lock() returns ErrFailed, WithLock returns an error of nil.

Example
package main

import (
	"fmt"
	"github.com/rafaeljusto/redigomock"
	"github.com/rgalanakis/redsync"
	"github.com/rgalanakis/redsync/rstest"
)

func main() {
	conn := redigomock.NewConn()
	rstest.AddLockExpects(conn, "example-mutex-with-lock", nil, "OK", "err")

	pools := rstest.PoolsForConn(conn, 1)
	rs := redsync.New(pools...)

	result := "no calls"

	mutex1 := rs.NewMutex("example-mutex-with-lock", redsync.NonBlocking())
	called1, err1 := mutex1.WithLock(func() {
		panic("Lock will not be acquired because Redis returns nil")
	})

	mutex2 := rs.NewMutex("example-mutex-with-lock", redsync.NonBlocking())
	called2, err2 := mutex2.WithLock(func() {
		result = "mutex2 called"
	})

	mutex3 := rs.NewMutex("example-mutex-with-lock", redsync.NonBlocking())
	called3, err3 := mutex3.WithLock(func() {
		panic("Lock will not be acquired because Redis returns error")
	})

	fmt.Printf("Mutex1: called: %v,\terror: %v\n", called1, err1)
	fmt.Printf("Mutex2: called: %v,\terror: %v\n", called2, err2)
	fmt.Printf("Mutex3: called: %v,\terror: %v\n", called3, err3)
	fmt.Printf("Result: %v\n", result)
}
Output:

Mutex1: called: false,	error: <nil>
Mutex2: called: true,	error: <nil>
Mutex3: called: false,	error: <nil>
Result: mutex2 called

type MutexOpts

type MutexOpts struct {
	// Expiry is the amount of time before the lock expires.
	// Useful to make sure the lock is expired even if the lock is never released,
	// like if a process dies while the lock is held.
	Expiry time.Duration
	// Tries is the number of times a lock acquisition is attempted.
	Tries int
	// Delay is the amount of time to wait between retries.
	Delay time.Duration
	// Factor is the clock drift Factor.
	Factor float64
}

MutexOpts are the options for mutex construction. In general, calls should use redsync.Blocking() or redsync.NonBlocking() and customize the result, but they can also create a MutexOpts themselves.

func Blocking

func Blocking() MutexOpts

Blocking returns the default MutexOpts for a blocking mutex. A blocking mutex will not return from Lock until the lock is acquired, or Delay has elapsed (500ms by default).

func NonBlocking

func NonBlocking() MutexOpts

NonBlocking returns the default MutexOpts for a non-blocking mutex. A non-blocking mutex gives up the first time if it cannot acquire a mutex, rather than retrying and spinning.

type Redsync

type Redsync struct {
	// contains filtered or unexported fields
}

Redsync is a factory for redsync.Mutex. It wraps a number of redis.Pool instances, each of which can have multiple connections. Use NewMutex to create a mutex.

func New

func New(pools ...*redis.Pool) *Redsync

New creates and returns a new Redsync instance from given Redis connection pools.

Example
package main

import (
	"fmt"
	"github.com/gomodule/redigo/redis"
	"github.com/rafaeljusto/redigomock"
	"github.com/rgalanakis/redsync"
	"github.com/rgalanakis/redsync/rstest"
)

func main() {
	var conn redis.Conn
	conn = redigomock.NewConn()
	pool := &redis.Pool{Dial: rstest.ConnDialer(conn)}
	mutex := redsync.New(pool).NewMutex("example-new", redsync.NonBlocking())
	fmt.Println(mutex)
}
Output:

redsync.Mutex{name: example-new, tries: 1, expiry: 8s, poolcnt: 1}

func (*Redsync) NewMutex

func (r *Redsync) NewMutex(name string, opts MutexOpts) *Mutex

NewMutex returns a new distributed mutex with given name and options.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL