mega_mutex 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Songkick.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,43 @@
1
+ # mega_mutex
2
+
3
+ ## Why
4
+
5
+ Sometimes I need to do this:
6
+
7
+ unless enough_things?
8
+ make_more_things
9
+ end
10
+
11
+ Sometimes though, if I'm running lots of processes in parallel, I get a race condition that means two of the processes both think there are not enough things. So we go and make some more, even though we don't need to.
12
+
13
+ ## How
14
+
15
+ Suppose you have a ThingMaker:
16
+
17
+ class ThingMaker
18
+ include MegaMutex
19
+
20
+ def ensure_just_enough_things
21
+ with_cross_process_mutex("ThingMaker Mutex ID") do
22
+ unless enough_things?
23
+ make_more_things
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ Now, thanks to the magic of MegaMutex, you can be sure that all processes trying to run this code will wait their turn, so each one will have the chance to make exactly the right number of things, without anyone else poking their nose in.
30
+
31
+ ## Configuration
32
+
33
+ MegaMutex uses [memcache-client](http://seattlerb.rubyforge.org/memcache-client/) to store the mutex, so your infrastructure must be set up to use memcache servers.
34
+
35
+ By default, MegaMutex will attempt to connect to a memcache on the local machine, but you can configure any number of servers like so:
36
+
37
+ MegaMutex.configure do |config|
38
+ config.memcache_servers = ['mc1', 'mc2']
39
+ end
40
+
41
+ ## Copyright
42
+
43
+ Copyright (c) 2009 Songkick.com. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "mega_mutex"
8
+ gem.summary = %Q{Cross-process mutex using MemCache}
9
+ gem.description = %Q{Cross-process mutex using MemCache}
10
+ gem.email = "developers@songkick.com"
11
+ gem.homepage = "http://github.com/songkick/mega_mutex"
12
+ gem.authors = ["Matt Johnson", "Matt Wynne"]
13
+ gem.add_dependency 'memcache-client', '>= 1.7.4'
14
+ gem.add_dependency 'logging', '>= 1.1.4'
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ if File.exist?('VERSION')
41
+ version = File.read('VERSION')
42
+ else
43
+ version = ""
44
+ end
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "mega_mutex #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,100 @@
1
+ require 'logging'
2
+ require 'memcache'
3
+
4
+ module MegaMutex
5
+ class TimeoutError < Exception; end
6
+
7
+ class CrossProcessMutex
8
+
9
+ class Configuration
10
+ attr_accessor :memcache_servers
11
+
12
+ def initialize
13
+ @memcache_servers = 'localhost'
14
+ end
15
+ end
16
+
17
+ class << self
18
+ def configure
19
+ yield configuration
20
+ end
21
+
22
+ def configuration
23
+ @configuration ||= Configuration.new
24
+ end
25
+ end
26
+
27
+ def initialize(key, timeout = nil)
28
+ @key = key
29
+ @timeout = timeout
30
+ end
31
+
32
+ def logger
33
+ Logging::Logger[self]
34
+ end
35
+
36
+ def run(&block)
37
+ @start_time = Time.now
38
+ log "Attempting to lock cross-process mutex..."
39
+ lock!
40
+ log "Locked. Running critical section..."
41
+ yield
42
+ log "Critical section complete. Unlocking..."
43
+ ensure
44
+ unlock!
45
+ log "Unlocking Mutex."
46
+ end
47
+
48
+ private
49
+
50
+ def timeout?
51
+ return false unless @timeout
52
+ Time.now > @start_time + @timeout
53
+ end
54
+
55
+ def log(message)
56
+ logger.debug do
57
+ "(key:#{@key}) (lock_id:#{my_lock_id}) #{message}"
58
+ end
59
+ end
60
+
61
+ def lock!
62
+ until timeout?
63
+ return if attempt_to_lock == my_lock_id
64
+ sleep 0.1
65
+ end
66
+ raise TimeoutError.new("Failed to obtain a lock within #{@timeout} seconds.")
67
+ end
68
+
69
+ def attempt_to_lock
70
+ if current_lock.nil?
71
+ set_current_lock my_lock_id
72
+ end
73
+ current_lock
74
+ end
75
+
76
+ def unlock!
77
+ cache.delete(@key) if locked_by_me?
78
+ end
79
+
80
+ def locked_by_me?
81
+ current_lock == my_lock_id
82
+ end
83
+
84
+ def current_lock
85
+ cache.get(@key)
86
+ end
87
+
88
+ def set_current_lock(new_lock)
89
+ cache.add(@key, my_lock_id)
90
+ end
91
+
92
+ def my_lock_id
93
+ @my_lock_id ||= "#{Process.pid.to_s}.#{self.object_id.to_s}.#{Time.now.to_i.to_s}"
94
+ end
95
+
96
+ def cache
97
+ @cache ||= MemCache.new self.class.configuration.memcache_servers, :namespace => 'mega_mutex'
98
+ end
99
+ end
100
+ end
data/lib/mega_mutex.rb ADDED
@@ -0,0 +1,61 @@
1
+ require 'rubygems'
2
+ $:.push File.expand_path(File.dirname(__FILE__))
3
+
4
+ require 'mega_mutex/cross_process_mutex'
5
+
6
+ # == Why
7
+ #
8
+ # Sometimes I need to do this:
9
+ #
10
+ # unless enough_things?
11
+ # make_more_things
12
+ # end
13
+ #
14
+ # Sometimes though, if I'm running lots of processes in parallel, I get a race condition that means two of the processes both think there are not enough things. So we go and make some more, even though we don't need to.
15
+ #
16
+ # == How
17
+ #
18
+ # Suppose you have a ThingMaker:
19
+ #
20
+ # class ThingMaker
21
+ # include MegaMutex
22
+ #
23
+ # def ensure_just_enough_things
24
+ # with_cross_process_mutex("ThingMaker Mutex ID") do
25
+ # unless enough_things?
26
+ # make_more_things
27
+ # end
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # Now, thanks to the magic of MegaMutex, you can be sure that all processes trying to run this code will wait their turn, so each one will have the chance to make exactly the right number of things, without anyone else poking their nose in.
33
+ #
34
+ # == Configuration
35
+ #
36
+ # MegaMutex uses http://seattlerb.rubyforge.org/memcache-client/ to store the mutex, so your infrastructure must be set up to use memcache servers.
37
+ #
38
+ # By default, MegaMutex will attempt to connect to a memcache on the local machine, but you can configure any number of servers like so:
39
+ #
40
+ # MegaMutex.configure do |config|
41
+ # config.memcache_servers = ['mc1', 'mc2']
42
+ # end
43
+ module MegaMutex
44
+
45
+ ##
46
+ # Wraps code that should only be run when the mutex has been obtained.
47
+ #
48
+ # The mutex_id uniquely identifies the section of code being run.
49
+ #
50
+ # You can optionally specify a :timeout to control how long to wait for the lock to be released
51
+ # before raising a MegaMutex::TimeoutError
52
+ #
53
+ # with_cross_process_mutex('my_mutex_id_1234', :timeout => 20) do
54
+ # do_something!
55
+ # end
56
+ def with_cross_process_mutex(mutex_id, options = {}, &block)
57
+ mutex = CrossProcessMutex.new(mutex_id, options[:timeout])
58
+ mutex.run(&block)
59
+ end
60
+
61
+ end
@@ -0,0 +1,55 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{mega_mutex}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Matt Johnson", "Matt Wynne"]
12
+ s.date = %q{2009-08-18}
13
+ s.description = %q{Cross-process mutex using MemCache}
14
+ s.email = %q{developers@songkick.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.markdown"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.markdown",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/mega_mutex.rb",
27
+ "lib/mega_mutex/cross_process_mutex.rb",
28
+ "mega_mutex.gemspec",
29
+ "spec/lib/mega_mutex_spec.rb"
30
+ ]
31
+ s.homepage = %q{http://github.com/songkick/mega_mutex}
32
+ s.rdoc_options = ["--charset=UTF-8"]
33
+ s.require_paths = ["lib"]
34
+ s.rubygems_version = %q{1.3.4}
35
+ s.summary = %q{Cross-process mutex using MemCache}
36
+ s.test_files = [
37
+ "spec/lib/mega_mutex_spec.rb"
38
+ ]
39
+
40
+ if s.respond_to? :specification_version then
41
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
42
+ s.specification_version = 3
43
+
44
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
45
+ s.add_runtime_dependency(%q<memcache-client>, [">= 1.7.4"])
46
+ s.add_runtime_dependency(%q<logging>, [">= 1.1.4"])
47
+ else
48
+ s.add_dependency(%q<memcache-client>, [">= 1.7.4"])
49
+ s.add_dependency(%q<logging>, [">= 1.1.4"])
50
+ end
51
+ else
52
+ s.add_dependency(%q<memcache-client>, [">= 1.7.4"])
53
+ s.add_dependency(%q<logging>, [">= 1.1.4"])
54
+ end
55
+ end
@@ -0,0 +1,129 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../lib/mega_mutex')
2
+
3
+ # Logging::Logger[:root].add_appenders(Logging::Appenders.stdout)
4
+
5
+ module MegaMutex
6
+ describe MegaMutex do
7
+ def logger
8
+ Logging::Logger['Specs']
9
+ end
10
+
11
+ before(:all) do
12
+ @old_abort_on_exception_value = Thread.abort_on_exception
13
+ Thread.abort_on_exception = true
14
+ end
15
+ after(:all) do
16
+ Thread.abort_on_exception = @old_abort_on_exception_value
17
+ end
18
+
19
+ describe "two blocks, one fast, one slow" do
20
+ before(:each) do
21
+ @errors = []
22
+ @mutually_exclusive_block = lambda do
23
+ @errors << "Someone else is running this code!" if @running
24
+ @running = true
25
+ sleep 0.5
26
+ @running = nil
27
+ end
28
+ end
29
+
30
+ describe "with no lock" do
31
+ it "trying to run the block twice should raise an error" do
32
+ threads = []
33
+ threads << Thread.new(&@mutually_exclusive_block)
34
+ threads << Thread.new(&@mutually_exclusive_block)
35
+ threads.each{ |t| t.join }
36
+ @errors.should_not be_empty
37
+ end
38
+ end
39
+
40
+ describe "with the same lock key" do
41
+ before(:each) do
42
+ MemCache.new('localhost').delete(mutex_id)
43
+ end
44
+
45
+ def mutex_id
46
+ 'tests-mutex-key'
47
+ end
48
+
49
+ include MegaMutex
50
+
51
+ [2, 20].each do |n|
52
+ describe "when #{n} blocks try to run at the same instant in the same process" do
53
+ it "should run each in turn" do
54
+ threads = []
55
+ n.times do
56
+ threads << Thread.new{ with_cross_process_mutex(mutex_id, &@mutually_exclusive_block) }
57
+ end
58
+ threads.each{ |t| t.join }
59
+ @errors.should be_empty
60
+ end
61
+ end
62
+ end
63
+
64
+ describe "when the first block raises an exception" do
65
+ before(:each) do
66
+ with_cross_process_mutex(mutex_id) do
67
+ raise "Something went wrong in my code"
68
+ end rescue nil
69
+ end
70
+
71
+ it "the second block should find that the lock is clear and it can run" do
72
+ @success = nil
73
+ with_cross_process_mutex(mutex_id) do
74
+ @success = true
75
+ end
76
+ @success.should be_true
77
+ end
78
+ end
79
+
80
+ describe "when two blocks try to run at the same instant in different processes" do
81
+ before(:each) do
82
+ @lock_file = File.expand_path(File.dirname(__FILE__) + '/tmp_lock')
83
+ @errors_file = File.expand_path(File.dirname(__FILE__) + '/tmp_errors')
84
+ @mutually_exclusive_block = lambda {
85
+ File.open(@errors_file, 'w').puts "Someone else is running this code!" if File.exists?(@lock_file)
86
+ FileUtils.touch @lock_file
87
+ sleep 1
88
+ File.delete @lock_file
89
+ }
90
+ end
91
+
92
+ after(:each) do
93
+ File.delete @lock_file if File.exists?(@lock_file)
94
+ File.delete @errors_file if File.exists?(@errors_file)
95
+ end
96
+
97
+ it "should run each in turn" do
98
+ pids = []
99
+ pids << fork { with_cross_process_mutex(mutex_id, &@mutually_exclusive_block); Kernel.exit! }
100
+ pids << fork { with_cross_process_mutex(mutex_id, &@mutually_exclusive_block); Kernel.exit! }
101
+ pids.each{ |p| Process.wait(p) }
102
+ if File.exists?(@errors_file)
103
+ raise "Expected no errors but found #{File.read(@errors_file)}"
104
+ end
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+
111
+ describe "with a timeout" do
112
+ include MegaMutex
113
+ it "should raise an error if the code blocks for longer than the timeout" do
114
+ @success = false
115
+ threads = []
116
+ threads << Thread.new{ with_cross_process_mutex('foo'){ sleep 2 } }
117
+ threads << Thread.new do
118
+ begin
119
+ with_cross_process_mutex('foo', :timeout => 1 ){ puts 'nobody will ever hear me scream' }
120
+ rescue MegaMutex::TimeoutError
121
+ @success = true
122
+ end
123
+ end
124
+ threads.each{ |t| t.join }
125
+ @success.should be_true
126
+ end
127
+ end
128
+ end
129
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mega_mutex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Johnson
8
+ - Matt Wynne
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-08-18 00:00:00 +01:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: memcache-client
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 1.7.4
25
+ version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: logging
28
+ type: :runtime
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 1.1.4
35
+ version:
36
+ description: Cross-process mutex using MemCache
37
+ email: developers@songkick.com
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files:
43
+ - LICENSE
44
+ - README.markdown
45
+ files:
46
+ - .document
47
+ - .gitignore
48
+ - LICENSE
49
+ - README.markdown
50
+ - Rakefile
51
+ - VERSION
52
+ - lib/mega_mutex.rb
53
+ - lib/mega_mutex/cross_process_mutex.rb
54
+ - mega_mutex.gemspec
55
+ - spec/lib/mega_mutex_spec.rb
56
+ has_rdoc: true
57
+ homepage: http://github.com/songkick/mega_mutex
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options:
62
+ - --charset=UTF-8
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.4
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Cross-process mutex using MemCache
84
+ test_files:
85
+ - spec/lib/mega_mutex_spec.rb