active_record_mutex 0.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.
- 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
|
+
|