ht-memcache-lock 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format nested
3
+ --backtrace
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ht-memcache-lock (0.2.0)
4
+ ht-memcache-lock (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
@@ -15,10 +15,11 @@ GEM
15
15
  coderay (~> 1.0.5)
16
16
  method_source (~> 0.8)
17
17
  slop (~> 3.3.1)
18
+ pry-nav (0.2.2)
19
+ pry (~> 0.9.10)
18
20
  rake (10.0.0)
19
21
  rdoc (3.12)
20
22
  json (~> 1.4)
21
- rr (1.0.4)
22
23
  rspec (2.12.0)
23
24
  rspec-core (~> 2.12.0)
24
25
  rspec-expectations (~> 2.12.0)
@@ -36,7 +37,7 @@ DEPENDENCIES
36
37
  ht-memcache-lock!
37
38
  memcache-client
38
39
  pry
40
+ pry-nav
39
41
  rake
40
42
  rdoc
41
- rr
42
43
  rspec
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
2
  require 'rspec/core/rake_task'
3
- require 'rake/rdoctask'
3
+ require 'rdoc/task'
4
4
 
5
5
 
6
6
  desc 'Run spec tests'
@@ -1,5 +1,5 @@
1
1
  module Memcache
2
2
  module Lock
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
data/lib/memcache-lock.rb CHANGED
@@ -1,20 +1,24 @@
1
- require "memcache-lock/version"
1
+ require 'memcache-lock/version'
2
+ require 'securerandom'
2
3
 
3
4
  class MemcacheLock
4
5
  class Error < RuntimeError; end
5
6
 
6
- DEFAULT_RETRY = 5
7
- DEFAULT_EXPIRY = 30
7
+ DEFAULT_OPTIONS = {
8
+ :initial_wait => 10e-3, # seconds -- first soft fail will wait for 10ms
9
+ :expiry => 60, # seconds
10
+ :retries => 11, # these defaults will retry for a total 41sec max
11
+ }
8
12
 
9
13
  def initialize(cache)
10
14
  @cache = cache
11
15
  end
12
16
 
13
- def synchronize(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
14
- if recursive_lock?(key)
17
+ def synchronize(key, options={})
18
+ if acquired?(key)
15
19
  yield
16
20
  else
17
- acquire_lock(key, lock_expiry, retries)
21
+ acquire_lock(key, options)
18
22
  begin
19
23
  yield
20
24
  ensure
@@ -23,14 +27,13 @@ class MemcacheLock
23
27
  end
24
28
  end
25
29
 
26
- def acquire_lock(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
27
- retries.times do |count|
28
- begin
29
- response = @cache.add("lock/#{key}", Process.pid, lock_expiry)
30
- return if response == "STORED\r\n"
31
- raise Error if count == retries - 1
32
- end
33
- exponential_sleep(count) unless count == retries - 1
30
+ def acquire_lock(key, options={})
31
+ options = DEFAULT_OPTIONS.merge(options)
32
+ 1.upto(options[:retries]) do |attempt|
33
+ response = @cache.add("lock/#{key}", uid, options[:expiry])
34
+ return if response == "STORED\r\n"
35
+ break if attempt == options[:retries]
36
+ Kernel.sleep(2 ** (attempt + rand - 1) * options[:initial_wait])
34
37
  end
35
38
  raise Error, "Couldn't acquire memcache lock for: #{key}"
36
39
  end
@@ -39,13 +42,20 @@ class MemcacheLock
39
42
  @cache.delete("lock/#{key}")
40
43
  end
41
44
 
42
- def exponential_sleep(count)
43
- @runtime += Benchmark::measure { sleep((2**count) / 2.0) }
45
+ private
46
+
47
+ def acquired?(key)
48
+ @cache.get("lock/#{key}") == uid
44
49
  end
45
50
 
46
51
  private
47
- def recursive_lock?(key)
48
- @cache.get("lock/#{key}") == Process.pid
52
+
53
+ # Globally unique ID for the current thread (or close enough)
54
+ def uid
55
+ "#{Socket.gethostname}-#{Process.pid}-#{thread_id}"
49
56
  end
50
- end
51
57
 
58
+ def thread_id
59
+ Thread.current[:thread_uid] ||= SecureRandom.hex(4)
60
+ end
61
+ end
@@ -15,8 +15,8 @@ Gem::Specification.new do |gem|
15
15
  # gem.add_runtime_dependancy
16
16
  gem.add_development_dependency 'rake'
17
17
  gem.add_development_dependency 'pry'
18
+ gem.add_development_dependency 'pry-nav'
18
19
  gem.add_development_dependency 'rspec'
19
- gem.add_development_dependency 'rr'
20
20
  gem.add_development_dependency 'rdoc'
21
21
  gem.add_development_dependency 'memcache-client'
22
22
 
@@ -1,10 +1,15 @@
1
- require File.join(File.dirname(__FILE__), 'spec_helper')
1
+ require 'spec_helper'
2
2
 
3
3
  describe MemcacheLock do
4
+ before do
5
+ Kernel.stub(:sleep)
6
+ @lock = MemcacheLock.new($memcache)
7
+ end
8
+
4
9
  describe '#synchronize' do
5
10
  it "yields the block" do
6
11
  block_was_called = false
7
- $lock.synchronize('lock_key') do
12
+ @lock.synchronize('lock_key') do
8
13
  block_was_called = true
9
14
  end
10
15
  block_was_called.should == true
@@ -12,27 +17,43 @@ describe MemcacheLock do
12
17
 
13
18
  it "acquires the specified lock before the block is run" do
14
19
  $memcache.get("lock/lock_key").should == nil
15
- $lock.synchronize('lock_key') do
20
+ @lock.synchronize('lock_key') do
16
21
  $memcache.get("lock/lock_key").should_not == nil
17
22
  end
18
23
  end
19
24
 
20
25
  it "releases the lock after the block is run" do
21
26
  $memcache.get("lock/lock_key").should == nil
22
- $lock.synchronize('lock_key') {}
27
+ @lock.synchronize('lock_key') {}
23
28
  $memcache.get("lock/lock_key").should == nil
24
29
 
25
30
  end
26
31
 
27
32
  it "releases the lock even if the block raises" do
28
33
  $memcache.get("lock/lock_key").should == nil
29
- $lock.synchronize('lock_key') { raise } rescue nil
34
+ @lock.synchronize('lock_key') { raise } rescue nil
30
35
  $memcache.get("lock/lock_key").should == nil
31
36
  end
32
37
 
33
38
  specify "does not block on recursive lock acquisition" do
34
- $lock.synchronize('lock_key') do
35
- lambda { $lock.synchronize('lock_key') {} }.should_not raise_error
39
+ @lock.synchronize('lock_key') do
40
+ lambda { @lock.synchronize('lock_key') {} }.should_not raise_error
41
+ end
42
+ end
43
+
44
+ it "permits recursive calls from the same thread" do
45
+ @lock.acquire_lock('lock_key')
46
+ lambda {
47
+ @lock.synchronize('lock_key') { nil }
48
+ }.should_not raise_error
49
+ end
50
+
51
+ it "prevents calls from different threads" do
52
+ @lock.acquire_lock('lock_key')
53
+ as_another_thread do
54
+ lambda {
55
+ @lock.synchronize('lock_key') { nil }
56
+ }.should raise_error(MemcacheLock::Error)
36
57
  end
37
58
  end
38
59
  end
@@ -40,46 +61,68 @@ describe MemcacheLock do
40
61
  describe '#acquire_lock' do
41
62
  specify "creates a lock at a given cache key" do
42
63
  $memcache.get("lock/lock_key").should == nil
43
- $lock.acquire_lock("lock_key")
64
+ @lock.acquire_lock("lock_key")
44
65
  $memcache.get("lock/lock_key").should_not == nil
45
66
  end
46
67
 
47
68
  specify "retries specified number of times" do
48
- $lock.acquire_lock('lock_key')
69
+ @lock.acquire_lock('lock_key')
49
70
  as_another_process do
50
- mock($memcache).add("lock/lock_key", Process.pid, timeout = 10) { "NOT_STORED\r\n" }.times(3)
51
- stub($lock).exponential_sleep
52
- lambda { $lock.acquire_lock('lock_key', timeout, 3) }.should raise_error
71
+ $memcache.should_receive(:add).exactly(3).times.and_return("NOT_STORED\r\n")
72
+ lambda { @lock.acquire_lock('lock_key', :expiry => 10, :retries => 3) }.should raise_error(MemcacheLock::Error)
53
73
  end
54
74
  end
55
75
 
56
76
  specify "correctly sets timeout on memcache entries" do
57
- mock($memcache).add('lock/lock_key', Process.pid, timeout = 10) { "STORED\r\n" }
58
- $lock.acquire_lock('lock_key', timeout)
77
+ $memcache.should_receive(:add).with('lock/lock_key', anything, 42).and_return("STORED\r\n")
78
+ @lock.acquire_lock('lock_key', :expiry => 42)
59
79
  end
60
80
 
61
81
  specify "prevents two processes from acquiring the same lock at the same time" do
62
- $lock.acquire_lock('lock_key')
82
+ @lock.acquire_lock('lock_key')
63
83
  as_another_process do
64
- lambda { $lock.acquire_lock('lock_key') }.should raise_error
84
+ lambda { @lock.acquire_lock('lock_key') }.should raise_error(MemcacheLock::Error)
65
85
  end
66
86
  end
67
87
 
68
- def as_another_process
69
- current_pid = Process.pid
70
- stub(Process).pid { current_pid + 1 }
71
- yield
88
+ specify "prevents two threads from acquiring the same lock at the same time" do
89
+ @lock.acquire_lock('lock_key')
90
+ as_another_thread do
91
+ lambda { @lock.acquire_lock('lock_key') }.should raise_error(MemcacheLock::Error)
92
+ end
72
93
  end
73
94
 
95
+ specify "prevents a given thread from acquiring the same lock twice" do
96
+ @lock.acquire_lock('lock_key')
97
+ lambda { @lock.acquire_lock('lock_key') }.should raise_error(MemcacheLock::Error)
98
+ end
74
99
  end
75
100
 
76
101
  describe '#release_lock' do
77
102
  specify "deletes the lock for a given cache key" do
78
103
  $memcache.get("lock/lock_key").should == nil
79
- $lock.acquire_lock("lock_key")
104
+ @lock.acquire_lock("lock_key")
80
105
  $memcache.get("lock/lock_key").should_not == nil
81
- $lock.release_lock("lock_key")
106
+ @lock.release_lock("lock_key")
82
107
  $memcache.get("lock/lock_key").should == nil
83
108
  end
84
109
  end
110
+
111
+
112
+ # helpers
113
+
114
+ def as_another_process
115
+ current_pid = Process.pid
116
+ Process.stub :pid => (current_pid + 1)
117
+ yield
118
+ Process.unstub :pid
119
+ end
120
+
121
+ def as_another_thread
122
+ old_tid = Thread.current[:thread_uid]
123
+ Thread.current[:thread_uid] = nil
124
+ yield
125
+ Thread.current[:thread_uid] = old_tid
126
+ end
127
+
85
128
  end
data/spec/spec_helper.rb CHANGED
@@ -8,12 +8,10 @@ require "rspec/core"
8
8
  require 'rspec/core/rake_task'
9
9
 
10
10
  RSpec.configure do |config|
11
- config.mock_with :rr
12
11
  config.before :suite do
13
12
  config = YAML.load(IO.read((File.expand_path(File.dirname(__FILE__) + "/memcache.yml"))))['test']
14
13
  $memcache = MemCache.new(config)
15
14
  $memcache.servers = config['servers']
16
- $lock = MemcacheLock.new($memcache)
17
15
  end
18
16
 
19
17
  config.before :each do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ht-memcache-lock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-11-13 00:00:00.000000000 Z
13
+ date: 2012-11-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rake
@@ -45,7 +45,7 @@ dependencies:
45
45
  - !ruby/object:Gem::Version
46
46
  version: '0'
47
47
  - !ruby/object:Gem::Dependency
48
- name: rspec
48
+ name: pry-nav
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
@@ -61,7 +61,7 @@ dependencies:
61
61
  - !ruby/object:Gem::Version
62
62
  version: '0'
63
63
  - !ruby/object:Gem::Dependency
64
- name: rr
64
+ name: rspec
65
65
  requirement: !ruby/object:Gem::Requirement
66
66
  none: false
67
67
  requirements:
@@ -117,6 +117,7 @@ extra_rdoc_files: []
117
117
  files:
118
118
  - .document
119
119
  - .gitignore
120
+ - .rspec
120
121
  - Gemfile
121
122
  - Gemfile.lock
122
123
  - LICENSE
@@ -143,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
143
144
  version: '0'
144
145
  segments:
145
146
  - 0
146
- hash: 2722382117642160453
147
+ hash: 3262276607432502556
147
148
  required_rubygems_version: !ruby/object:Gem::Requirement
148
149
  none: false
149
150
  requirements:
@@ -152,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
153
  version: '0'
153
154
  segments:
154
155
  - 0
155
- hash: 2722382117642160453
156
+ hash: 3262276607432502556
156
157
  requirements: []
157
158
  rubyforge_project:
158
159
  rubygems_version: 1.8.23