Skip to content

Commit

Permalink
New feature - lock/release
Browse files Browse the repository at this point in the history
  • Loading branch information
mrz1836 committed Jan 8, 2020
1 parent cd15c9e commit 72cafda
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ You can view the generated [documentation here](https://godoc.org/github.com/mrz
- Better Pool Management & Creation
- Register Scripts
- Helper Methods (Get, Set, HashGet, etc)
- Basic Lock/Release (from [bgentry lock.go](https://gist.github.com/bgentry/6105288))

## Examples & Tests
All unit tests and [examples](examples/examples.go) run via [Travis CI](https://travis-ci.org/mrz1836/go-cache) and uses [Go version 1.13.x](https://golang.org/doc/go1.13). View the [deployment configuration file](.travis.yml).
Expand Down
77 changes: 77 additions & 0 deletions redis_lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cache

import (
"errors"

"github.com/gomodule/redigo/redis"
)

// ErrLockMismatch is the error if the key is locked by someone else
var ErrLockMismatch = errors.New("key is locked with a different secret")

// lockScript is the locking script
const lockScript = `
local v = redis.call("GET", KEYS[1])
if v == false or v == ARGV[1]
then
return redis.call("SET", KEYS[1], ARGV[1], "EX", ARGV[2]) and 1
else
return 0
end
`

// unlockScript is the unlocking script
const unlockScript = `
local v = redis.call("GET",KEYS[1])
if v == false then
return 1
elseif v == ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
`

// WriteLock attempts to grab a redis lock.
func WriteLock(name, secret string, ttl int64) (locked bool, err error) {

// Create a new connection and defer closing
conn := GetConnection()
defer func() {
_ = conn.Close()
}()

script := redis.NewScript(1, lockScript)
var resp int
resp, err = redis.Int(script.Do(conn, name, secret, ttl))
if err != nil {
return
} else if resp != 0 {
locked = true
return
}
err = ErrLockMismatch
return
}

// ReleaseLock releases the redis lock
func ReleaseLock(name, secret string) (released bool, err error) {

// Create a new connection and defer closing
conn := GetConnection()
defer func() {
_ = conn.Close()
}()

script := redis.NewScript(1, unlockScript)
var resp int
resp, err = redis.Int(script.Do(conn, name, secret))
if err != nil {
return
} else if resp != 0 {
released = true
return
}
err = ErrLockMismatch
return
}
102 changes: 102 additions & 0 deletions redis_lock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package cache

import (
"testing"
"time"
)

// TestWriteLock will run basic tests for lock/release
func TestWriteLock(t *testing.T) {

// Create a local connection
if err := startTest(); err != nil {
t.Fatal(err.Error())
}

// Disconnect at end
defer endTest()

// attempt to lock
locked, err := WriteLock("my-key", "the-secret", int64(10))
if err != nil {
t.Fatalf("error acquiring lock: %q", err.Error())
}
if !locked {
t.Fatal("expected WriteLock to return true")
}

// attempt to re-lock (should succeed)
locked, err = WriteLock("my-key", "the-secret", int64(5))
if !locked || err != nil {
t.Fatalf("expected re-lock attempt to succeed, got locked %t error %q", locked, err)
}

// attempt to re-lock with different secret (should return error)
locked, err = WriteLock("my-key", "the-different-secret", int64(5))
if locked || err != ErrLockMismatch {
t.Fatalf("expected re-lock attempt to fail, got locked %t error %q", locked, err)
}

// attempt to release lock w/ bad secret
var unlocked bool
unlocked, err = ReleaseLock("my-key", "the-wrong-secret")
if unlocked || err != ErrLockMismatch {
t.Fatalf("expected release lock w/ bad secret to fail, got unlocked %t error %q", unlocked, err)
}

// attempt to release lock w/ correct secret
unlocked, err = ReleaseLock("my-key", "the-secret")
if !unlocked || err != nil {
t.Fatalf("expected release lock to succeed, got unlocked %t error %q", unlocked, err)
}

// attempt to release lock again (should return true, nil)
unlocked, err = ReleaseLock("myKey", "the-secret")
if !unlocked || err != nil {
t.Fatalf("expected repeat release lock to succeed, got unlocked %t error %q", unlocked, err)
}
}

// TestReleaseLock will run basic tests for lock/release
func TestReleaseLock(t *testing.T) {

// Create a local connection
if err := startTest(); err != nil {
t.Fatal(err.Error())
}

// Disconnect at end
defer endTest()

// attempt to lock
locked, err := WriteLock("my-key", "the-secret", int64(5))
if err != nil {
t.Fatalf("error acquiring lock: %q", err.Error())
}
if !locked {
t.Fatal("expected WriteLock to return true")
}

time.Sleep(50 * time.Millisecond)

// test if lock is there
locked, err = WriteLock("my-key", "the-different-secret", int64(5))
if locked || err != ErrLockMismatch {
t.Fatalf("expected lock attempt to fail, got locked %t error %q", locked, err)
}

time.Sleep(50 * time.Millisecond)

// test if lock is there
locked, err = WriteLock("my-key", "the-different-secret", int64(5))
if locked || err != ErrLockMismatch {
t.Fatalf("expected lock attempt to fail, got locked %t error %q", locked, err)
}

// attempt to release lock w/ correct secret
var unlocked bool
unlocked, err = ReleaseLock("my-key", "the-secret")
if !unlocked || err != nil {
t.Fatalf("expected release lock to succeed, got unlocked %t error %q", unlocked, err)
}
}

0 comments on commit 72cafda

Please sign in to comment.