seamusabshere-redlock 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a3db3c911a17f8b605fa6d1792916d28cb46dfcd
4
+ data.tar.gz: a7d53070405f730965e3966ebd3a0e4cf70297a4
5
+ SHA512:
6
+ metadata.gz: 6cee08cdc8d900bbdfbf23187995d3a777d4b55f43d7b1e718463ba0a5021ac64c1991edf6dd603f51c2eeef3f7c0605a6179ff882802b0b381568df8603b0b9
7
+ data.tar.gz: 7b555aec308f81d40f6d34bbe706ea1c1a3353dbf6dd607e36ae68e6f87b6d28168cbcf39729b7f6819556ba17ea70af08887a6381a3364144e83202db7013b1
@@ -0,0 +1,2 @@
1
+ *.gem
2
+ coverage/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ services:
3
+ - redis-server
4
+ rvm:
5
+ - "2.0.0"
6
+ script: bundle exec rspec spec
@@ -0,0 +1,11 @@
1
+ # This is the official list of people who have contributed code to
2
+ # redlock.
3
+ # You can update this list using the following command:
4
+ #
5
+ # % git shortlog -se | awk '{$1=""; print $0}' | sed -e 's/^ //'
6
+ #
7
+ # Please keep this file sorted, and group users with multiple emails.
8
+
9
+ Leandro Moreira <leandro.ribeiro.moreira@gmail.com>
10
+ Malte Rohde
11
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in redlock_rb.gemspec
4
+ gemspec
@@ -0,0 +1,57 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ redlock (0.1.1)
5
+ redis (~> 3, >= 3.0.5)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ coveralls (0.7.11)
11
+ multi_json (~> 1.10)
12
+ rest-client (>= 1.6.8, < 2)
13
+ simplecov (~> 0.9.1)
14
+ term-ansicolor (~> 1.3)
15
+ thor (~> 0.19.1)
16
+ diff-lcs (1.2.5)
17
+ docile (1.1.5)
18
+ mime-types (2.4.3)
19
+ multi_json (1.11.0)
20
+ netrc (0.10.3)
21
+ rake (10.4.2)
22
+ redis (3.2.1)
23
+ rest-client (1.7.3)
24
+ mime-types (>= 1.16, < 3.0)
25
+ netrc (~> 0.7)
26
+ rspec (3.2.0)
27
+ rspec-core (~> 3.2.0)
28
+ rspec-expectations (~> 3.2.0)
29
+ rspec-mocks (~> 3.2.0)
30
+ rspec-core (3.2.2)
31
+ rspec-support (~> 3.2.0)
32
+ rspec-expectations (3.2.0)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.2.0)
35
+ rspec-mocks (3.2.1)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.2.0)
38
+ rspec-support (3.2.2)
39
+ simplecov (0.9.2)
40
+ docile (~> 1.1.0)
41
+ multi_json (~> 1.0)
42
+ simplecov-html (~> 0.9.0)
43
+ simplecov-html (0.9.0)
44
+ term-ansicolor (1.3.0)
45
+ tins (~> 1.0)
46
+ thor (0.19.1)
47
+ tins (1.3.5)
48
+
49
+ PLATFORMS
50
+ ruby
51
+
52
+ DEPENDENCIES
53
+ bundler (~> 1.7)
54
+ coveralls
55
+ rake (~> 10.0)
56
+ redlock!
57
+ rspec (~> 3.1)
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2014-2015, Salvatore Sanfilippo <antirez at gmail dot com>
2
+ Copyright (c) 2014-2015, Leandro Moreira <leandro dot ribeiro dot moreira at gmail dot com>
3
+ Copyright (c) 2015, Malte Rohde <malte dot rohde at flavoursys dot com>
4
+
5
+ All rights reserved.
6
+
7
+ Redistribution and use in source and binary forms, with or without
8
+ modification, are permitted provided that the following conditions are met:
9
+
10
+ * Redistributions of source code must retain the above copyright notice,
11
+ this list of conditions and the following disclaimer.
12
+
13
+ * Redistributions in binary form must reproduce the above copyright notice,
14
+ this list of conditions and the following disclaimer in the documentation
15
+ and/or other materials provided with the distribution.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
21
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,100 @@
1
+ [![Stories in Ready](https://badge.waffle.io/leandromoreira/redlock-rb.png?label=ready&title=Ready)](https://waffle.io/leandromoreira/redlock-rb)
2
+ [![Build Status](https://travis-ci.org/leandromoreira/redlock-rb.svg?branch=master)](https://travis-ci.org/leandromoreira/redlock-rb)
3
+ [![Coverage Status](https://coveralls.io/repos/leandromoreira/redlock-rb/badge.svg?branch=master)](https://coveralls.io/r/leandromoreira/redlock-rb?branch=master)
4
+ [![Code Climate](https://codeclimate.com/github/leandromoreira/redlock-rb/badges/gpa.svg)](https://codeclimate.com/github/leandromoreira/redlock-rb)
5
+ [![Dependency Status](https://gemnasium.com/leandromoreira/redlock-rb.svg)](https://gemnasium.com/leandromoreira/redlock-rb)
6
+ [![Gem Version](https://badge.fury.io/rb/redlock.svg)](http://badge.fury.io/rb/redlock)
7
+ [![security](https://hakiri.io/github/leandromoreira/redlock-rb/master.svg)](https://hakiri.io/github/leandromoreira/redlock-rb/master)
8
+ [![Inline docs](http://inch-ci.org/github/leandromoreira/redlock-rb.svg?branch=master)](http://inch-ci.org/github/leandromoreira/redlock-rb)
9
+ [![Join the chat at https://gitter.im/leandromoreira/redlock-rb](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/leandromoreira/redlock-rb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
10
+
11
+ [![Codeship](https://codeship.com/projects/901ff180-c1ad-0132-1a88-3eb2295b72b3/status?branch=master)](https://codeship.com/projects/901ff180-c1ad-0132-1a88-3eb2295b72b3/status?branch=master)
12
+
13
+
14
+ # Redlock - A ruby distributed lock using redis.
15
+
16
+ # TEMPORARY GEM TO RELEASE `extend` FUNCTIONALITY https://github.com/leandromoreira/redlock-rb/pull/20
17
+
18
+ > Distributed locks are a very useful primitive in many environments where different processes require to operate with shared resources in a mutually exclusive way.
19
+ >
20
+ > There are a number of libraries and blog posts describing how to implement a DLM (Distributed Lock Manager) with Redis, but every library uses a different approach, and many use a simple approach with lower guarantees compared to what can be achieved with slightly more complex designs.
21
+
22
+ This is an implementation of a proposed [distributed lock algorithm with Redis](http://redis.io/topics/distlock). It started as a fork from [antirez implementation.](https://github.com/antirez/redlock-rb)
23
+
24
+ ## Installation
25
+
26
+ Add this line to your application's Gemfile:
27
+
28
+ ```ruby
29
+ gem 'redlock'
30
+ ```
31
+
32
+ And then execute:
33
+
34
+ $ bundle
35
+
36
+ Or install it yourself as:
37
+
38
+ $ gem install redlock
39
+
40
+ ## Documentation
41
+
42
+ [RubyDoc](http://www.rubydoc.info/gems/redlock/frames)
43
+
44
+ ## Usage example
45
+
46
+ ```ruby
47
+ # Locking
48
+ lock_manager = Redlock::Client.new([ "redis://127.0.0.1:7777", "redis://127.0.0.1:7778", "redis://127.0.0.1:7779" ])
49
+ first_try_lock_info = lock_manager.lock("resource_key", 2000)
50
+ second_try_lock_info = lock_manager.lock("resource_key", 2000)
51
+
52
+ # it prints lock info {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
53
+ p first_try_lock_info
54
+ # it prints false
55
+ p second_try_lock_info
56
+
57
+ # Unlocking
58
+ lock_manager.unlock(first_try_lock_info)
59
+ second_try_lock_info = lock_manager.lock("resource_key", 2000)
60
+
61
+ # now it prints lock info
62
+ p second_try_lock_info
63
+ ```
64
+
65
+ Redlock works seamlessly with [redis sentinel](http://redis.io/topics/sentinel), which is supported in redis 3.2+. It also allows clients to set any other arbitrary options on the Redis connection, e.g. password, driver, and more.
66
+
67
+ ```ruby
68
+ servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
69
+ redlock = Redlock::Client.new(servers)
70
+ ```
71
+
72
+ There's also a block version that automatically unlocks the lock:
73
+
74
+ ```ruby
75
+ lock_manager.lock("resource_key", 2000) do |locked|
76
+ if locked
77
+ # critical code
78
+ else
79
+ # error handling
80
+ end
81
+ end
82
+ ```
83
+
84
+ ## Run tests
85
+
86
+ Make sure you have at least 1 redis instances up.
87
+
88
+ $ rspec
89
+
90
+ ## Disclaimer
91
+
92
+ 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 your production environments. You can see discussion about this approach at [reddit](http://www.reddit.com/r/programming/comments/2nt0nq/distributed_lock_using_redis_implemented_in_ruby/).
93
+
94
+ ## Contributing
95
+
96
+ 1. [Fork it](https://github.com/leandromoreira/redlock-rb/fork)
97
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
98
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
99
+ 4. Push to the branch (`git push origin my-new-feature`)
100
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,5 @@
1
+ require 'redlock/version'
2
+
3
+ module Redlock
4
+ autoload :Client, 'redlock/client'
5
+ end
@@ -0,0 +1,143 @@
1
+ require 'redis'
2
+ require 'securerandom'
3
+
4
+ module Redlock
5
+ class Client
6
+ DEFAULT_REDIS_URLS = ['redis://localhost:6379']
7
+ DEFAULT_REDIS_TIMEOUT = 0.1
8
+ DEFAULT_RETRY_COUNT = 3
9
+ DEFAULT_RETRY_DELAY = 200
10
+ CLOCK_DRIFT_FACTOR = 0.01
11
+
12
+ # Create a distributed lock manager implementing redlock algorithm.
13
+ # Params:
14
+ # +servers+:: The array of redis connection URLs or Redis connection instances. Or a mix of both.
15
+ # +options+:: You can override the default value for `retry_count` and `retry_delay`.
16
+ # * `retry_count` being how many times it'll try to lock a resource (default: 3)
17
+ # * `retry_delay` being how many ms to sleep before try to lock again (default: 200)
18
+ # * `redis_timeout` being how the Redis timeout will be set in seconds (default: 0.1)
19
+ def initialize(servers = DEFAULT_REDIS_URLS, options = {})
20
+ redis_timeout = options[:redis_timeout] || DEFAULT_REDIS_TIMEOUT
21
+ @servers = servers.map do |server|
22
+ if server.is_a?(String)
23
+ RedisInstance.new(url: server, timeout: redis_timeout)
24
+ else
25
+ RedisInstance.new(server)
26
+ end
27
+ end
28
+ @quorum = servers.length / 2 + 1
29
+ @retry_count = options[:retry_count] || DEFAULT_RETRY_COUNT
30
+ @retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
31
+ end
32
+
33
+ # Locks a resource for a given time.
34
+ # Params:
35
+ # +resource+:: the resource (or key) string to be locked.
36
+ # +ttl+:: The time-to-live in ms for the lock.
37
+ # +extend+: A lock ("lock_info") to extend.
38
+ # +block+:: an optional block that automatically unlocks the lock.
39
+ def lock(resource, ttl, extend: nil, &block)
40
+ lock_info = try_lock_instances(resource, ttl, extend)
41
+
42
+ if block_given?
43
+ begin
44
+ yield lock_info
45
+ !!lock_info
46
+ ensure
47
+ unlock(lock_info) if lock_info
48
+ end
49
+ else
50
+ lock_info
51
+ end
52
+ end
53
+
54
+ # Unlocks a resource.
55
+ # Params:
56
+ # +lock_info+:: the lock that has been acquired when you locked the resource.
57
+ def unlock(lock_info)
58
+ @servers.each { |s| s.unlock(lock_info[:resource], lock_info[:value]) }
59
+ end
60
+
61
+ private
62
+
63
+ class RedisInstance
64
+ UNLOCK_SCRIPT = <<-eos
65
+ if redis.call("get",KEYS[1]) == ARGV[1] then
66
+ return redis.call("del",KEYS[1])
67
+ else
68
+ return 0
69
+ end
70
+ eos
71
+ # thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
72
+ # also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
73
+ # and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
74
+ LOCK_SCRIPT = <<-eos
75
+ if redis.call("exists", KEYS[1]) == 0 or redis.call("get", KEYS[1]) == ARGV[1] then
76
+ return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
77
+ end
78
+ eos
79
+
80
+ def initialize(connection)
81
+ if connection.respond_to?(:client)
82
+ @redis = connection
83
+ else
84
+ @redis = Redis.new(connection)
85
+ end
86
+
87
+ @unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
88
+ @lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
89
+ end
90
+
91
+ def lock(resource, val, ttl)
92
+ @redis.evalsha(@lock_script_sha, keys: [resource], argv: [val, ttl])
93
+ end
94
+
95
+ def unlock(resource, val)
96
+ @redis.evalsha(@unlock_script_sha, keys: [resource], argv: [val])
97
+ rescue
98
+ # Nothing to do, unlocking is just a best-effort attempt.
99
+ end
100
+ end
101
+
102
+ def try_lock_instances(resource, ttl, extend)
103
+ @retry_count.times do
104
+ lock_info = lock_instances(resource, ttl, extend)
105
+ return lock_info if lock_info
106
+
107
+ # Wait a random delay before retrying
108
+ sleep(rand(@retry_delay).to_f / 1000)
109
+ end
110
+
111
+ false
112
+ end
113
+
114
+ def lock_instances(resource, ttl, extend)
115
+ value = extend ? extend.fetch(:value) : SecureRandom.uuid
116
+
117
+ locked, time_elapsed = timed do
118
+ @servers.select { |s| s.lock(resource, value, ttl) }.size
119
+ end
120
+
121
+ validity = ttl - time_elapsed - drift(ttl)
122
+
123
+ if locked >= @quorum && validity >= 0
124
+ { validity: validity, resource: resource, value: value }
125
+ else
126
+ @servers.each { |s| s.unlock(resource, value) }
127
+ false
128
+ end
129
+ end
130
+
131
+ def drift(ttl)
132
+ # Add 2 milliseconds to the drift to account for Redis expires
133
+ # precision, which is 1 millisecond, plus 1 millisecond min drift
134
+ # for small TTLs.
135
+ drift = (ttl * CLOCK_DRIFT_FACTOR).to_i + 2
136
+ end
137
+
138
+ def timed
139
+ start_time = (Time.now.to_f * 1000).to_i
140
+ [yield, (Time.now.to_f * 1000).to_i - start_time]
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,27 @@
1
+ module Redlock
2
+ class Client
3
+ attr_writer :testing_mode
4
+
5
+ alias_method :try_lock_instances_without_testing, :try_lock_instances
6
+
7
+ def try_lock_instances(resource, ttl, extend)
8
+ if @testing_mode == :bypass
9
+ {
10
+ validity: ttl,
11
+ resource: resource,
12
+ value: SecureRandom.uuid
13
+ }
14
+ elsif @testing_mode == :fail
15
+ false
16
+ else
17
+ try_lock_instances_without_testing resource, ttl, extend
18
+ end
19
+ end
20
+
21
+ alias_method :unlock_without_testing, :unlock
22
+
23
+ def unlock(lock_info)
24
+ unlock_without_testing lock_info unless @testing_mode == :bypass
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Redlock
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'redlock/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "seamusabshere-redlock"
8
+ spec.version = Redlock::VERSION
9
+ spec.authors = ["Leandro Moreira"]
10
+ spec.email = ["leandro.ribeiro.moreira@gmail.com"]
11
+ spec.summary = %q{(temporary gem) Distributed lock using Redis written in Ruby.}
12
+ spec.description = %q{(temporary gem) Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.}
13
+ spec.homepage = "https://github.com/leandromoreira/redlock-rb"
14
+ spec.license = 'BSD-2-Clause'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'redis', '~> 3', '>= 3.0.5'
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "coveralls"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.1"
27
+ end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+ RSpec.describe Redlock::Client do
5
+ # It is recommended to have at least 3 servers in production
6
+ let(:lock_manager) { Redlock::Client.new }
7
+ let(:resource_key) { SecureRandom.hex(3) }
8
+ let(:ttl) { 1000 }
9
+
10
+ describe 'initialize' do
11
+ it 'accepts both redis URLs and Redis objects' do
12
+ servers = [ 'redis://localhost:6379', Redis.new(url: 'redis://127.0.0.1:6379') ]
13
+ redlock = Redlock::Client.new(servers)
14
+
15
+ redlock_servers = redlock.instance_variable_get(:@servers).map do |s|
16
+ s.instance_variable_get(:@redis).client.host
17
+ end
18
+
19
+ expect(redlock_servers).to match_array(%w{ localhost 127.0.0.1 })
20
+ end
21
+ end
22
+
23
+ describe 'lock' do
24
+ context 'when lock is available' do
25
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
26
+
27
+ it 'locks' do
28
+ @lock_info = lock_manager.lock(resource_key, ttl)
29
+
30
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
31
+ end
32
+
33
+ it 'returns lock information' do
34
+ @lock_info = lock_manager.lock(resource_key, ttl)
35
+
36
+ expect(@lock_info).to be_lock_info_for(resource_key)
37
+ end
38
+
39
+ it 'can extend its own lock' do
40
+ my_lock_info = lock_manager.lock(resource_key, ttl)
41
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: my_lock_info)
42
+ expect(@lock_info).to be_lock_info_for(resource_key)
43
+ expect(@lock_info[:value]).to eq(my_lock_info[:value])
44
+ end
45
+
46
+ it "sets the given value when trying to extend a non-existent lock" do
47
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'})
48
+ expect(@lock_info).to be_lock_info_for(resource_key)
49
+ expect(@lock_info[:value]).to eq('hello world') # really we should test what's in redis
50
+ end
51
+
52
+ it "doesn't extend lock by default" do
53
+ @lock_info = lock_manager.lock(resource_key, ttl)
54
+ second_attempt = lock_manager.lock(resource_key, ttl)
55
+ expect(second_attempt).to eq(false)
56
+ end
57
+ end
58
+
59
+ context 'when lock is not available' do
60
+ before { @another_lock_info = lock_manager.lock(resource_key, ttl) }
61
+ after { lock_manager.unlock(@another_lock_info) }
62
+
63
+ it 'returns false' do
64
+ lock_info = lock_manager.lock(resource_key, ttl)
65
+
66
+ expect(lock_info).to eql(false)
67
+ end
68
+
69
+ it "can't extend somebody else's lock" do
70
+ yet_another_lock_info = @another_lock_info.merge value: 'gibberish'
71
+ lock_info = lock_manager.lock(resource_key, ttl, extend: yet_another_lock_info)
72
+ expect(lock_info).to eql(false)
73
+ end
74
+ end
75
+
76
+ describe 'block syntax' do
77
+ context 'when lock is available' do
78
+ it 'locks' do
79
+ lock_manager.lock(resource_key, ttl) do |_|
80
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
81
+ end
82
+ end
83
+
84
+ it 'passes lock information as block argument' do
85
+ lock_manager.lock(resource_key, ttl) do |lock_info|
86
+ expect(lock_info).to be_lock_info_for(resource_key)
87
+ end
88
+ end
89
+
90
+ it 'returns true' do
91
+ rv = lock_manager.lock(resource_key, ttl) {}
92
+ expect(rv).to eql(true)
93
+ end
94
+
95
+ it 'automatically unlocks' do
96
+ lock_manager.lock(resource_key, ttl) {}
97
+ expect(resource_key).to be_lockable(lock_manager, ttl)
98
+ end
99
+
100
+ it 'automatically unlocks when block raises exception' do
101
+ lock_manager.lock(resource_key, ttl) { fail } rescue nil
102
+ expect(resource_key).to be_lockable(lock_manager, ttl)
103
+ end
104
+ end
105
+
106
+ context 'when lock is not available' do
107
+ before { @another_lock_info = lock_manager.lock(resource_key, ttl) }
108
+ after { lock_manager.unlock(@another_lock_info) }
109
+
110
+ it 'passes false as block argument' do
111
+ lock_manager.lock(resource_key, ttl) do |lock_info|
112
+ expect(lock_info).to eql(false)
113
+ end
114
+ end
115
+
116
+ it 'returns false' do
117
+ rv = lock_manager.lock(resource_key, ttl) {}
118
+ expect(rv).to eql(false)
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ describe 'unlock' do
125
+ before { @lock_info = lock_manager.lock(resource_key, ttl) }
126
+
127
+ it 'unlocks' do
128
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
129
+
130
+ lock_manager.unlock(@lock_info)
131
+
132
+ expect(resource_key).to be_lockable(lock_manager, ttl)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ require 'coveralls'
3
+ Coveralls.wear!
4
+ require 'redlock'
5
+
6
+ LOCK_INFO_KEYS = %i{validity resource value}
7
+
8
+ RSpec::Matchers.define :be_lock_info_for do |resource|
9
+ def correct_type?(actual)
10
+ actual.is_a?(Hash)
11
+ end
12
+
13
+ def correct_layout?(actual)
14
+ ((LOCK_INFO_KEYS | actual.keys) - (LOCK_INFO_KEYS & actual.keys)).empty?
15
+ end
16
+
17
+ def correct_resource?(actual, resource)
18
+ actual[:resource] == resource
19
+ end
20
+
21
+ match do |actual|
22
+ correct_type?(actual) && correct_layout?(actual) && correct_resource?(actual, resource)
23
+ end
24
+
25
+ failure_message do |actual|
26
+ "expected that #{actual} would be lock information for #{expected}"
27
+ end
28
+ end
29
+
30
+ RSpec::Matchers.define :be_lockable do |lock_manager, ttl|
31
+ match do |resource_key|
32
+ begin
33
+ lock_info = lock_manager.lock(resource_key, ttl)
34
+ lock_info != false
35
+ ensure
36
+ lock_manager.unlock(lock_info) if lock_info
37
+ end
38
+ end
39
+
40
+ failure_message do |resource_key|
41
+ "expected that #{resource_key} would be lockable"
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+ require 'redlock/testing'
5
+
6
+ RSpec.describe Redlock::Client do
7
+ let(:lock_manager) { Redlock::Client.new }
8
+ let(:resource_key) { SecureRandom.hex(3) }
9
+ let(:ttl) { 1000 }
10
+
11
+ describe '(testing mode)' do
12
+ describe 'try_lock_instances' do
13
+ context 'when testing with bypass mode' do
14
+ before { lock_manager.testing_mode = :bypass }
15
+
16
+ it 'bypasses the redis servers' do
17
+ expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
18
+ lock_manager.lock(resource_key, ttl) do |lock_info|
19
+ expect(lock_info).to be_lock_info_for(resource_key)
20
+ end
21
+ end
22
+ end
23
+
24
+ context 'when testing with fail mode' do
25
+ before { lock_manager.testing_mode = :fail }
26
+
27
+ it 'fails' do
28
+ expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
29
+ lock_manager.lock(resource_key, ttl) do |lock_info|
30
+ expect(lock_info).to eql(false)
31
+ end
32
+ end
33
+ end
34
+
35
+ context 'when testing is disabled' do
36
+ before { lock_manager.testing_mode = nil }
37
+
38
+ it 'works as usual' do
39
+ expect(lock_manager).to receive(:try_lock_instances_without_testing)
40
+ lock_manager.lock(resource_key, ttl) { |lock_info| }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: seamusabshere-redlock
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Leandro Moreira
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 3.0.5
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '3'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.5
33
+ - !ruby/object:Gem::Dependency
34
+ name: bundler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.7'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.7'
47
+ - !ruby/object:Gem::Dependency
48
+ name: coveralls
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '10.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '10.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rspec
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.1'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.1'
89
+ description: "(temporary gem) Distributed lock using Redis written in Ruby. Highly
90
+ inspired by https://github.com/antirez/redlock-rb."
91
+ email:
92
+ - leandro.ribeiro.moreira@gmail.com
93
+ executables: []
94
+ extensions: []
95
+ extra_rdoc_files: []
96
+ files:
97
+ - ".gitignore"
98
+ - ".rspec"
99
+ - ".travis.yml"
100
+ - CONTRIBUTORS
101
+ - Gemfile
102
+ - Gemfile.lock
103
+ - LICENSE
104
+ - README.md
105
+ - Rakefile
106
+ - lib/redlock.rb
107
+ - lib/redlock/client.rb
108
+ - lib/redlock/testing.rb
109
+ - lib/redlock/version.rb
110
+ - redlock.gemspec
111
+ - spec/client_spec.rb
112
+ - spec/spec_helper.rb
113
+ - spec/testing_spec.rb
114
+ homepage: https://github.com/leandromoreira/redlock-rb
115
+ licenses:
116
+ - BSD-2-Clause
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 2.2.2
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: "(temporary gem) Distributed lock using Redis written in Ruby."
138
+ test_files:
139
+ - spec/client_spec.rb
140
+ - spec/spec_helper.rb
141
+ - spec/testing_spec.rb
142
+ has_rdoc: