ht-memcache-lock 0.2.0 → 0.3.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.
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