active_record_mutex 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +0 -0
- data/README +46 -0
- data/Rakefile +86 -0
- data/VERSION +1 -0
- data/lib/active_record/mutex.rb +148 -0
- data/lib/active_record/mutex/version.rb +10 -0
- data/lib/active_record_mutex.rb +1 -0
- data/test/mutex_test.rb +101 -0
- metadata +77 -0
data/CHANGES
ADDED
File without changes
|
data/README
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
== Description
|
2
|
+
|
3
|
+
This gem provides a Mutex that is based on ctiveRecord's database connection.
|
4
|
+
(At the moment this only works for Mysql.) It can be used to synchronise
|
5
|
+
different ruby processes via the connected database.
|
6
|
+
|
7
|
+
== Download
|
8
|
+
|
9
|
+
The latest version of the <b>active_record_mutex</b> source archive can be
|
10
|
+
found at
|
11
|
+
|
12
|
+
* http://www.ping.de/~flori
|
13
|
+
|
14
|
+
== Installation
|
15
|
+
|
16
|
+
You can use rubygems to fetch the gem and install it for you:
|
17
|
+
|
18
|
+
# gem install active_record_mutex
|
19
|
+
|
20
|
+
You can also put this line into your Rails environment.rb file
|
21
|
+
|
22
|
+
config.gem 'active_record_mutex'
|
23
|
+
|
24
|
+
and install the gem via
|
25
|
+
|
26
|
+
$ rake gems:install
|
27
|
+
|
28
|
+
== Usage
|
29
|
+
|
30
|
+
If you want to synchronize method calls to your model's methods you can easily
|
31
|
+
do this by passing a mutex instance to ActiveRecord's synchronize class method.
|
32
|
+
This mutex instance will be named Foo like the ActiveRecord was named:
|
33
|
+
|
34
|
+
class Foo < ActiveRecord::Base
|
35
|
+
def foo
|
36
|
+
end
|
37
|
+
|
38
|
+
synchronize :foo, :with => :mutex
|
39
|
+
end
|
40
|
+
|
41
|
+
If you want more control over the mutex and/or give it a special name you can
|
42
|
+
create Mutex instance like this:
|
43
|
+
|
44
|
+
my_mutex = ActiveRecord::Mutex::Mutex.new(:name => 'my_mutex')
|
45
|
+
|
46
|
+
Now you can send all messages directly to the Mutex instance.
|
data/Rakefile
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
begin
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
require 'rake/clean'
|
6
|
+
require 'rbconfig'
|
7
|
+
include Config
|
8
|
+
|
9
|
+
PKG_NAME = 'active_record_mutex'
|
10
|
+
PKG_VERSION = File.read('VERSION').chomp
|
11
|
+
PKG_FILES = FileList['**/*'].exclude(/^(doc|CVS|pkg|coverage)/)
|
12
|
+
PKG_SUMMARY = 'Implementation of a Mutex for Active Record'
|
13
|
+
PKG_RDOC_OPTIONS = [ '--main', 'README', '--title', PKG_SUMMARY ]
|
14
|
+
CLEAN.include 'coverage', 'doc'
|
15
|
+
|
16
|
+
desc "Testing library"
|
17
|
+
task :test do
|
18
|
+
ruby %{-Ilib test/mutex_test.rb}
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "Testing library (with coverage)"
|
22
|
+
task :coverage do
|
23
|
+
sh %{rcov -Ilib test/mutex_test.rb}
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "Installing library"
|
27
|
+
task :install do
|
28
|
+
ruby 'install.rb'
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "Creating documentation"
|
32
|
+
task :doc do
|
33
|
+
sh 'rdoc', *(PKG_RDOC_OPTIONS + Dir['lib/**/*.rb'] + [ 'README' ])
|
34
|
+
end
|
35
|
+
|
36
|
+
if defined? Gem
|
37
|
+
spec = Gem::Specification.new do |s|
|
38
|
+
s.name = PKG_NAME
|
39
|
+
s.version = PKG_VERSION
|
40
|
+
s.summary = PKG_SUMMARY
|
41
|
+
s.description = "Mutex that can be used to synchronise ruby processes via an ActiveRecord"\
|
42
|
+
" datababase connection. (Only Mysql is supported at the moment.)"
|
43
|
+
|
44
|
+
s.files = PKG_FILES.to_a.sort
|
45
|
+
|
46
|
+
s.require_path = 'lib'
|
47
|
+
|
48
|
+
s.has_rdoc = true
|
49
|
+
s.rdoc_options = PKG_RDOC_OPTIONS
|
50
|
+
s.extra_rdoc_files << 'README'
|
51
|
+
#s.test_files << 'test/mutex_test.rb'
|
52
|
+
|
53
|
+
s.author = "Florian Frank"
|
54
|
+
s.email = "flori@ping.de"
|
55
|
+
s.homepage = "http://flori.github.com/#{PKG_NAME}"
|
56
|
+
s.rubyforge_project = "#{PKG_NAME}"
|
57
|
+
end
|
58
|
+
|
59
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
60
|
+
pkg.need_tar = true
|
61
|
+
pkg.package_files += PKG_FILES
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
desc m = "Writing version information for #{PKG_VERSION}"
|
66
|
+
task :version do
|
67
|
+
puts m
|
68
|
+
File.open(File.join('lib', 'active_record', 'mutex', 'version.rb'), 'w') do |v|
|
69
|
+
v.puts <<EOT
|
70
|
+
module ActiveRecord
|
71
|
+
module Mutex
|
72
|
+
# ActiveRecord::Mutex version
|
73
|
+
VERSION = '#{PKG_VERSION}'
|
74
|
+
VERSION_ARRAY = VERSION.split(/\\./).map { |x| x.to_i } # :nodoc:
|
75
|
+
VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
|
76
|
+
VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
|
77
|
+
VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
|
78
|
+
end
|
79
|
+
end
|
80
|
+
EOT
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
task :default => [ :version, :test ]
|
85
|
+
|
86
|
+
task :release => [ :clean, :version, :package ]
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/mutex/version'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
# This module is mixed into ActiveRecord::Base to provide the mutex methods
|
6
|
+
# that return a mutex for a particular ActiveRecord::Base subclass/instance.
|
7
|
+
module Mutex
|
8
|
+
# This is the base exception of all mutex exceptions.
|
9
|
+
class MutexError < ActiveRecordError; end
|
10
|
+
|
11
|
+
# This exception is raised if a mutex of the given name isn't locked at the
|
12
|
+
# moment and unlock was called.
|
13
|
+
class MutexUnlockFailed < MutexError; end
|
14
|
+
|
15
|
+
# This exception is raised if a mutex of the given name is locked at the
|
16
|
+
# moment and lock was called again.
|
17
|
+
class MutexLocked < MutexError; end
|
18
|
+
|
19
|
+
def self.included(modul)
|
20
|
+
modul.instance_eval do
|
21
|
+
extend ClassMethods
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
# Returns a mutex instance for this ActiveRecord subclass.
|
27
|
+
def mutex
|
28
|
+
@mutex ||= Mutex.new(:name => name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a mutex instance for this ActiveRecord instance.
|
33
|
+
def mutex
|
34
|
+
@mutex ||= Mutex.new(:name => self.class.name)
|
35
|
+
end
|
36
|
+
|
37
|
+
class Mutex
|
38
|
+
# Creates a mutex with the name given with the option :name.
|
39
|
+
def initialize(opts = {})
|
40
|
+
@name = opts[:name] or raise ArgumentError, "Mutex requires a :name argument"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the name of this mutex as given as a constructor argument.
|
44
|
+
attr_reader :name
|
45
|
+
|
46
|
+
# Locks the mutex if it isn't already locked via another database
|
47
|
+
# connection and yields to the given block. After executing the block's
|
48
|
+
# content the mutex is unlocked (only if it was locked by this
|
49
|
+
# synchronize method before).
|
50
|
+
#
|
51
|
+
# If the mutex was already locked by another database connection the
|
52
|
+
# method blocks until it could aquire the lock and only then the block's
|
53
|
+
# content is executed. If the mutex was already locked by the current database
|
54
|
+
# connection then the block's content is run and the the mutex isn't
|
55
|
+
# unlocked afterwards.
|
56
|
+
#
|
57
|
+
# If a value in seconds is passed to the :timeout option the blocking
|
58
|
+
# ends after that many seconds and the method returns immediately if the
|
59
|
+
# lock couldn't be aquired during that time.
|
60
|
+
def synchronize(opts = {})
|
61
|
+
locked_before = aquired_lock?
|
62
|
+
lock opts
|
63
|
+
yield
|
64
|
+
rescue ActiveRecord::Mutex::MutexLocked
|
65
|
+
return nil
|
66
|
+
ensure
|
67
|
+
locked_before or unlock
|
68
|
+
end
|
69
|
+
|
70
|
+
# Locks the mutex and returns true if successful. If the mutex is
|
71
|
+
# already locked and the timeout in seconds is given as the :timeout
|
72
|
+
# option, this method raises a MutexLocked exception after that many
|
73
|
+
# seconds. If the :timeout option wasn't given, this method blocks until
|
74
|
+
# the lock could be aquired.
|
75
|
+
def lock(opts = {})
|
76
|
+
if opts[:timeout]
|
77
|
+
lock_with_timeout opts
|
78
|
+
else
|
79
|
+
begin
|
80
|
+
lock_with_timeout :timeout => 1
|
81
|
+
rescue MutexLocked
|
82
|
+
retry
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Unlocks the mutex and returns true if successful. Otherwise this method
|
88
|
+
# raises a MutexLocked exception.
|
89
|
+
def unlock(*)
|
90
|
+
case query("SELECT RELEASE_LOCK(#{ActiveRecord::Base.quote_value(name)})")
|
91
|
+
when 1 then true
|
92
|
+
when 0, nil then raise MutexUnlockFailed, "unlocking of mutex '#{name}' failed"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns true if this mutex is unlocked at the moment.
|
97
|
+
def unlocked?
|
98
|
+
query("SELECT IS_FREE_LOCK(#{ActiveRecord::Base.quote_value(name)})") == 1
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns true if this mutex is locked at the moment.
|
102
|
+
def locked?
|
103
|
+
not unlocked?
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns true if this mutex is locked by this database connection.
|
107
|
+
def aquired_lock?
|
108
|
+
query("SELECT CONNECTION_ID() = IS_USED_LOCK(#{ActiveRecord::Base.quote_value(name)})") == 1
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns true if this mutex is not locked by this database connection.
|
112
|
+
def not_aquired_lock?
|
113
|
+
not aquired_lock?
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns a string representation of this Mutex instance.
|
117
|
+
def to_s
|
118
|
+
"#<#{self.class} #{name}>"
|
119
|
+
end
|
120
|
+
|
121
|
+
alias inspect to_s
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def lock_with_timeout(opts = {})
|
126
|
+
timeout = opts[:timeout] || 1
|
127
|
+
case query("SELECT GET_LOCK(#{ActiveRecord::Base.quote_value(name)}, #{timeout})")
|
128
|
+
when 1 then true
|
129
|
+
when 0 then raise MutexLocked, "mutex '#{name}' is already locked"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def query(sql)
|
134
|
+
if result = ActiveRecord::Base.connection.execute(sql)
|
135
|
+
result = result.fetch_row.first.to_i
|
136
|
+
$DEBUG and warn %{query("#{sql}") = #{result}}
|
137
|
+
end
|
138
|
+
result
|
139
|
+
rescue ActiveRecord::StatementInvalid
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
ActiveRecord::Base.class_eval do
|
147
|
+
include ActiveRecord::Mutex
|
148
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Mutex
|
3
|
+
# ActiveRecord::Mutex version
|
4
|
+
VERSION = '0.0.0'
|
5
|
+
VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
|
6
|
+
VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
|
7
|
+
VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
|
8
|
+
VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_record/mutex'
|
data/test/mutex_test.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
require 'active_record'
|
5
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
6
|
+
require 'active_record/mutex'
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(
|
9
|
+
:adapter => "mysql",
|
10
|
+
:database => ENV['DATABASE'] || "test",
|
11
|
+
:username => ENV['USER'],
|
12
|
+
:password => ENV['PASSWORD']
|
13
|
+
)
|
14
|
+
|
15
|
+
class MutexTest < Test::Unit::TestCase
|
16
|
+
class Foo < ActiveRecord::Base; end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
ActiveRecord::Schema.define(:version => 1) do
|
20
|
+
create_table(:foos, :force => true) { |t| t.string :bar }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def teardown
|
25
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
26
|
+
ActiveRecord::Base.connection.drop_table(table)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_exported_methods
|
31
|
+
mutex = Foo.mutex
|
32
|
+
assert_kind_of ActiveRecord::Mutex::Mutex, mutex
|
33
|
+
assert_equal mutex.name, Foo.name
|
34
|
+
mutex = Foo.new.mutex
|
35
|
+
assert_kind_of ActiveRecord::Mutex::Mutex, mutex
|
36
|
+
assert_equal mutex.name, Foo.name
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_create
|
40
|
+
mutex = ActiveRecord::Mutex::Mutex.new(:name => 'Create')
|
41
|
+
assert_equal 'Create', mutex.name
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_lock
|
45
|
+
mutex = ActiveRecord::Mutex::Mutex.new(:name => 'Lock')
|
46
|
+
assert mutex.unlocked?
|
47
|
+
assert mutex.lock
|
48
|
+
assert mutex.locked?
|
49
|
+
assert mutex.aquired_lock?
|
50
|
+
assert mutex.lock
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_unlock
|
54
|
+
mutex = ActiveRecord::Mutex::Mutex.new(:name => 'Unlock')
|
55
|
+
assert_raises(ActiveRecord::Mutex::MutexUnlockFailed) { mutex.unlock }
|
56
|
+
assert mutex.lock
|
57
|
+
assert mutex.locked?
|
58
|
+
assert mutex.aquired_lock?
|
59
|
+
assert mutex.unlock
|
60
|
+
assert mutex.unlocked?
|
61
|
+
assert_raises(ActiveRecord::Mutex::MutexUnlockFailed) { mutex.unlock }
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_synchronize
|
65
|
+
mutex = ActiveRecord::Mutex::Mutex.new(:name => 'Sync1')
|
66
|
+
assert mutex.unlocked?
|
67
|
+
mutex.synchronize do
|
68
|
+
assert mutex.locked?
|
69
|
+
end
|
70
|
+
assert mutex.unlocked?
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_synchronize_exception
|
74
|
+
mutex = ActiveRecord::Mutex::Mutex.new(:name => 'Sync2')
|
75
|
+
exception = Class.new StandardError
|
76
|
+
begin
|
77
|
+
assert mutex.unlocked?
|
78
|
+
mutex.synchronize do
|
79
|
+
assert mutex.locked?
|
80
|
+
raise exception
|
81
|
+
end
|
82
|
+
rescue exception
|
83
|
+
assert mutex.unlocked?
|
84
|
+
assert mutex.lock
|
85
|
+
assert mutex.locked?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_synchronize_nested
|
90
|
+
mutex = ActiveRecord::Mutex::Mutex.new(:name => 'Sync3')
|
91
|
+
assert mutex.unlocked?
|
92
|
+
mutex.synchronize do
|
93
|
+
assert mutex.locked?
|
94
|
+
mutex.synchronize do
|
95
|
+
assert mutex.locked?
|
96
|
+
end
|
97
|
+
assert mutex.locked?
|
98
|
+
end
|
99
|
+
assert mutex.unlocked?
|
100
|
+
end
|
101
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_record_mutex
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 0.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Florian Frank
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-04-01 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Mutex that can be used to synchronise ruby processes via an ActiveRecord datababase connection. (Only Mysql is supported at the moment.)
|
23
|
+
email: flori@ping.de
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README
|
30
|
+
files:
|
31
|
+
- CHANGES
|
32
|
+
- README
|
33
|
+
- Rakefile
|
34
|
+
- VERSION
|
35
|
+
- lib/active_record/mutex.rb
|
36
|
+
- lib/active_record/mutex/version.rb
|
37
|
+
- lib/active_record_mutex.rb
|
38
|
+
- test/mutex_test.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://flori.github.com/active_record_mutex
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options:
|
45
|
+
- --main
|
46
|
+
- README
|
47
|
+
- --title
|
48
|
+
- Implementation of a Mutex for Active Record
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
hash: 3
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project: active_record_mutex
|
72
|
+
rubygems_version: 1.6.2
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Implementation of a Mutex for Active Record
|
76
|
+
test_files: []
|
77
|
+
|