with_advisory_lock 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MWIyOWFmZWRlMTkzZWM0ZTFlY2UzZjVkYmQ3NTc5M2M5OTYxNzI0OQ==
5
- data.tar.gz: !binary |-
6
- OTdhNjkxNjZmODc5MjE2ODc2OWZmYzkzZDYxOTViNWY2ZWRlMDk2Nw==
2
+ SHA1:
3
+ metadata.gz: 9a06363d16fb4769a2ee0486a8f0295dfe6e0c21
4
+ data.tar.gz: 3ce9cf5d558777ab689388fc086ce1fc2b18e1aa
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- NWFmZDViM2VmYmJmNWIwNTYwZGMzY2ZmODQ1MTcxNmZhZmFkZDUyNmQyNGE1
10
- NWRlNGY3YTA0NmU3MjBiOGJiNjVkOGYxNWI3NjFmMjRlYmNhODM2NDg3N2U5
11
- ZWM2NzNjYTczOWNiN2E2MzI4OWJiYWVlZWY1ZWEyNjM3MDhhNzY=
12
- data.tar.gz: !binary |-
13
- NmQyOWI0NmY4ZTJmNTFjY2YzNzlhNzM1ZDlmNWM0NWE5NjRiMWE2NDI3ZmU3
14
- NTBmYzIwODZhZDUzMjQxYjM2NThkZDgxNGM1M2ZiODE4MjJhNmJiYTcwMTEw
15
- MTEzMmMyYzQ0Yjk2YjRmMWZjMzAzZjAwNzlmZTJlYjQ1ODg2MWQ=
6
+ metadata.gz: 1e162976296fc77f93eb8a46bf2681c4a7694727317a384b38bdfc552cd7dba6738a84e035879df673256624a6bdf3c0a74e16a677ce06adae80b2ee9daf59b8
7
+ data.tar.gz: 5454c4de4150ceaa24c797ae726913ed0b239fdfcbf19471985d760d76c41a2b78da3ea1df4c733afb1ba75301e02ae0550252876e3b3e4b145e4984b4c5e3b4
data/.gitignore CHANGED
@@ -4,7 +4,7 @@
4
4
  .bundle
5
5
  .config
6
6
  .yardoc
7
- Gemfile.lock
7
+ *.lock
8
8
  InstalledFiles
9
9
  _yardoc
10
10
  coverage
data/.travis.yml CHANGED
@@ -1,16 +1,16 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - 2.0.0
5
- # - 1.8.7
4
+ - jruby-19mode
5
+ - 2.1.2
6
6
  - 1.9.3
7
+ # TODO - rbx-2
7
8
 
8
9
  gemfile:
9
- - ci/Gemfile.rails-4.1.x
10
- - ci/Gemfile.rails-4.0.x
11
- - ci/Gemfile.rails-3.2.x
12
- # - ci/Gemfile.rails-3.1.x
13
- # - ci/Gemfile.rails-3.0.x
10
+ - gemfiles/activerecord_3.2.gemfile
11
+ - gemfiles/activerecord_4.0.gemfile
12
+ - gemfiles/activerecord_4.1.gemfile
13
+ - gemfiles/activerecord_edge.gemfile
14
14
 
15
15
  env:
16
16
  - DB=sqlite
@@ -23,41 +23,10 @@ before_script:
23
23
  - mysql -e 'create database with_advisory_lock_test'
24
24
  - psql -c 'create database with_advisory_lock_test' -U postgres
25
25
 
26
+ addons:
27
+ postgresql: "9.3"
28
+
26
29
  matrix:
27
- exclude:
28
- - rvm: 1.8.7
29
- gemfile: ci/Gemfile.rails-4.0.x
30
- env: DB=sqlite
31
- - rvm: 1.8.7
32
- gemfile: ci/Gemfile.rails-4.0.x
33
- env: DB=mysql
34
- - rvm: 1.8.7
35
- gemfile: ci/Gemfile.rails-4.0.x
36
- env: DB=postgresql
37
- - rvm: 2.0.0
38
- gemfile: ci/Gemfile.rails-3.0.x
39
- env: DB=sqlite
40
- - rvm: 2.0.0
41
- gemfile: ci/Gemfile.rails-3.0.x
42
- env: DB=mysql
43
- - rvm: 2.0.0
44
- gemfile: ci/Gemfile.rails-3.0.x
45
- env: DB=postgresql
46
- - rvm: 2.0.0
47
- gemfile: ci/Gemfile.rails-3.1.x
48
- env: DB=sqlite
49
- - rvm: 2.0.0
50
- gemfile: ci/Gemfile.rails-3.1.x
51
- env: DB=mysql
52
- - rvm: 2.0.0
53
- gemfile: ci/Gemfile.rails-3.1.x
54
- env: DB=postgresql
55
- - rvm: 2.0.0
56
- gemfile: ci/Gemfile.rails-3.2.x
57
- env: DB=sqlite
58
- - rvm: 2.0.0
59
- gemfile: ci/Gemfile.rails-3.2.x
60
- env: DB=mysql
61
- - rvm: 2.0.0
62
- gemfile: ci/Gemfile.rails-3.2.x
63
- env: DB=postgresql
30
+ allow_failures:
31
+ - gemfile: gemfiles/activerecord_edge.gemfile
32
+ - rvm: rbx-2
data/Appraisals ADDED
@@ -0,0 +1,16 @@
1
+ appraise "activerecord-3.2" do
2
+ gem 'activerecord', '~> 3.2.0'
3
+ end
4
+
5
+ appraise "activerecord-4.0" do
6
+ gem "activerecord", "~> 4.0.0"
7
+ end
8
+
9
+ appraise "activerecord-4.1" do
10
+ gem "activerecord", "~> 4.1.0"
11
+ end
12
+
13
+ appraise "activerecord-edge" do
14
+ gem "activerecord", github: "rails/rails"
15
+ gem 'arel', github: 'rails/arel'
16
+ end
data/Gemfile CHANGED
@@ -1,3 +1,15 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ platforms :ruby do
6
+ gem 'mysql2'
7
+ gem 'pg'
8
+ gem 'sqlite3'
9
+ end
10
+
11
+ platforms :jruby do
12
+ gem 'activerecord-jdbcmysql-adapter'
13
+ gem 'activerecord-jdbcpostgresql-adapter'
14
+ gem 'activerecord-jdbcsqlite3-adapter'
15
+ end
data/README.md CHANGED
@@ -1,15 +1,14 @@
1
1
  # with_advisory_lock
2
2
 
3
- Adds advisory locking (mutexes) to ActiveRecord 3.0, 3.1, 3.2, 4.0 and 4.1 when used with
3
+ Adds advisory locking (mutexes) to ActiveRecord 3.2, 4.0 and 4.1 when used with
4
4
  [MySQL](http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock)
5
- or [PostgreSQL](http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS).
5
+ or [PostgreSQL](http://www.postgresql.org/docs/9.3/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS).
6
6
  SQLite resorts to file locking.
7
7
 
8
8
  [![Build Status](https://api.travis-ci.org/mceachen/with_advisory_lock.png?branch=master)](https://travis-ci.org/mceachen/with_advisory_lock)
9
9
  [![Gem Version](https://badge.fury.io/rb/with_advisory_lock.png)](http://rubygems.org/gems/with_advisory_lock)
10
10
  [![Code Climate](https://codeclimate.com/github/mceachen/with_advisory_lock.png)](https://codeclimate.com/github/mceachen/with_advisory_lock)
11
11
  [![Dependency Status](https://gemnasium.com/mceachen/with_advisory_lock.png)](https://gemnasium.com/mceachen/with_advisory_lock)
12
- [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/mceachen/with_advisory_lock/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
13
12
 
14
13
  ## What's an "Advisory Lock"?
15
14
 
@@ -38,8 +37,14 @@ end
38
37
  The second parameter for ```with_advisory_lock``` is ```timeout_seconds```, and defaults to ```nil```,
39
38
  which means wait indefinitely for the lock.
40
39
 
41
- If a non-nil value is provided, the block may not be invoked.
40
+ A value of zero will try the lock only once. If the lock is acquired, the block
41
+ will be yielded to. If the lock is currently being held, the block will not be called.
42
42
 
43
+ Note that if a non-nil value is provided for `timeout_seconds`, the block will not be invoked if
44
+ the lock cannot be acquired within that timeframe.
45
+
46
+ ### Return values
47
+
43
48
  The return value of ```with_advisory_lock``` will be the result of the yielded block,
44
49
  if the lock was able to be acquired and the block yielded, or ```false```, if you provided
45
50
  a timeout_seconds value and the lock was not able to be acquired in time.
@@ -129,6 +134,19 @@ end
129
134
 
130
135
  ## Changelog
131
136
 
137
+ ### 2.0.0
138
+
139
+ * Lock timeouts of 0 now attempt the lock once, as per suggested by
140
+ [Jon Leighton](https://github.com/jonleighton) and implemented by
141
+ [Abdelkader Boudih](https://github.com/seuros). Thanks to both of you!
142
+ * [Pull request 11](https://github.com/mceachen/with_advisory_lock/pull/11)
143
+ fixed a downstream issue with jruby support! Thanks, [Aaron Todd](https://github.com/ozzyaaron)!
144
+ * Added Travis tests for jruby
145
+ * Dropped support for Rails 3.0, 3.1, and Ruby 1.8.7, as they are no longer
146
+ receiving security patches. See http://rubyonrails.org/security/ for more information.
147
+ This required the major version bump.
148
+ * Refactored `advisory_lock_exists?` to use existing functionality
149
+ * Fixed sqlite's implementation so parallel tests could be run against it
132
150
 
133
151
  ### 1.0.0
134
152
 
data/Rakefile CHANGED
@@ -8,8 +8,8 @@ end
8
8
  require 'rake/testtask'
9
9
 
10
10
  Rake::TestTask.new do |t|
11
- t.libs.push "lib"
12
- t.libs.push "test"
11
+ t.libs.push 'lib'
12
+ t.libs.push 'test'
13
13
  t.pattern = 'test/**/*_test.rb'
14
14
  t.verbose = true
15
15
  end
@@ -0,0 +1,19 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 3.2.0"
6
+
7
+ platforms :ruby do
8
+ gem "mysql2"
9
+ gem "pg"
10
+ gem "sqlite3"
11
+ end
12
+
13
+ platforms :jruby do
14
+ gem "activerecord-jdbcmysql-adapter"
15
+ gem "activerecord-jdbcpostgresql-adapter"
16
+ gem "activerecord-jdbcsqlite3-adapter"
17
+ end
18
+
19
+ gemspec :path => "../"
@@ -0,0 +1,19 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.0.0"
6
+
7
+ platforms :ruby do
8
+ gem "mysql2"
9
+ gem "pg"
10
+ gem "sqlite3"
11
+ end
12
+
13
+ platforms :jruby do
14
+ gem "activerecord-jdbcmysql-adapter"
15
+ gem "activerecord-jdbcpostgresql-adapter"
16
+ gem "activerecord-jdbcsqlite3-adapter"
17
+ end
18
+
19
+ gemspec :path => "../"
@@ -0,0 +1,19 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.1.0"
6
+
7
+ platforms :ruby do
8
+ gem "mysql2"
9
+ gem "pg"
10
+ gem "sqlite3"
11
+ end
12
+
13
+ platforms :jruby do
14
+ gem "activerecord-jdbcmysql-adapter"
15
+ gem "activerecord-jdbcpostgresql-adapter"
16
+ gem "activerecord-jdbcsqlite3-adapter"
17
+ end
18
+
19
+ gemspec :path => "../"
@@ -0,0 +1,20 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", :github => "rails/rails"
6
+ gem "arel", :github => "rails/arel"
7
+
8
+ platforms :ruby do
9
+ gem "mysql2"
10
+ gem "pg"
11
+ gem "sqlite3"
12
+ end
13
+
14
+ platforms :jruby do
15
+ gem "activerecord-jdbcmysql-adapter"
16
+ gem "activerecord-jdbcpostgresql-adapter"
17
+ gem "activerecord-jdbcsqlite3-adapter"
18
+ end
19
+
20
+ gemspec :path => "../"
@@ -1,4 +1,17 @@
1
+ require 'with_advisory_lock/version'
2
+
3
+ module WithAdvisoryLock
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Concern
7
+ autoload :Base
8
+ autoload :DatabaseAdapterSupport
9
+ autoload :Flock
10
+ autoload :MySQL, 'with_advisory_lock/mysql'
11
+ autoload :NestedAdvisoryLockError
12
+ autoload :PostgreSQL, 'with_advisory_lock/postgresql'
13
+ end
14
+
1
15
  ActiveSupport.on_load :active_record do
2
- require 'with_advisory_lock/concern'
3
16
  ActiveRecord::Base.send :include, WithAdvisoryLock::Concern
4
17
  end
@@ -7,35 +7,21 @@ module WithAdvisoryLock
7
7
  def initialize(connection, lock_name, timeout_seconds)
8
8
  @connection = connection
9
9
  @lock_name = lock_name
10
- lock_name_prefix = ENV['WITH_ADVISORY_LOCK_PREFIX']
11
- if lock_name_prefix
12
- @lock_name = if lock_name.is_a? Numeric
13
- "#{lock_name_prefix.to_i}#{lock_name}".to_i
14
- else
15
- "#{lock_name_prefix}#{lock_name}"
16
- end
17
- end
18
10
  @timeout_seconds = timeout_seconds
19
11
  end
20
12
 
21
- def quoted_lock_name
22
- connection.quote(lock_name)
13
+ def lock_str
14
+ @lock_str ||= "#{ENV['WITH_ADVISORY_LOCK_PREFIX'].to_s}#{lock_name.to_s}"
23
15
  end
24
16
 
25
17
  def self.lock_stack
26
18
  Thread.current[:with_advisory_lock_stack] ||= []
27
19
  end
28
20
 
29
- def lock_stack
30
- self.class.lock_stack
31
- end
21
+ delegate :lock_stack, to: 'self.class'
32
22
 
33
23
  def already_locked?
34
- lock_stack.include? @lock_name
35
- end
36
-
37
- def advisory_lock_exists?(name)
38
- raise NoMethodError, "method must be implemented in implementation subclasses"
24
+ lock_stack.include? lock_str
39
25
  end
40
26
 
41
27
  def with_advisory_lock_if_needed
@@ -56,12 +42,18 @@ module WithAdvisoryLock
56
42
  end
57
43
  end
58
44
 
45
+ def advisory_lock_exists?
46
+ acquired_lock = try_lock
47
+ ensure
48
+ release_lock if acquired_lock
49
+ end
50
+
59
51
  def yield_with_lock
60
52
  give_up_at = Time.now + @timeout_seconds if @timeout_seconds
61
- while @timeout_seconds.nil? || Time.now < give_up_at do
53
+ begin
62
54
  if try_lock
63
55
  begin
64
- lock_stack.push(lock_name)
56
+ lock_stack.push(lock_str)
65
57
  return yield
66
58
  ensure
67
59
  lock_stack.pop
@@ -72,8 +64,13 @@ module WithAdvisoryLock
72
64
  # Randomizing sleep time may help reduce contention.
73
65
  sleep(rand * 0.15 + 0.05)
74
66
  end
75
- end
67
+ end while @timeout_seconds.nil? || Time.now < give_up_at
76
68
  false # failed to get lock in time.
77
69
  end
70
+
71
+ # The timestamp prevents AR from caching the result improperly, and is ignored.
72
+ def query_cache_buster
73
+ "AS t#{(Time.now.to_f * 1000).to_i}"
74
+ end
78
75
  end
79
76
  end
@@ -2,23 +2,12 @@
2
2
  # but rails autoloading is too clever by half. Pull requests are welcome.
3
3
 
4
4
  require 'active_support/concern'
5
- require 'with_advisory_lock/base'
6
- require 'with_advisory_lock/database_adapter_support'
7
- require 'with_advisory_lock/flock'
8
- require 'with_advisory_lock/mysql'
9
- require 'with_advisory_lock/postgresql'
10
5
 
11
6
  module WithAdvisoryLock
12
7
  module Concern
13
8
  extend ActiveSupport::Concern
14
9
 
15
- def with_advisory_lock(lock_name, timeout_seconds=nil, &block)
16
- self.class.with_advisory_lock(lock_name, timeout_seconds, &block)
17
- end
18
-
19
- def advisory_lock_exists?(lock_name)
20
- self.class.advisory_lock_exists?(lock_name)
21
- end
10
+ delegate :with_advisory_lock, :advisory_lock_exists?, to: 'self.class'
22
11
 
23
12
  module ClassMethods
24
13
  def with_advisory_lock(lock_name, timeout_seconds=nil, &block)
@@ -27,23 +16,23 @@ module WithAdvisoryLock
27
16
  end
28
17
 
29
18
  def advisory_lock_exists?(lock_name)
30
- impl = impl_class.new(connection, lock_name, nil)
31
- impl.advisory_lock_exists?(lock_name)
19
+ impl = impl_class.new(connection, lock_name, 0)
20
+ impl.already_locked? || !impl.yield_with_lock { true }
32
21
  end
33
22
 
34
23
  def current_advisory_lock
35
24
  WithAdvisoryLock::Base.lock_stack.first
36
25
  end
37
26
 
38
- private
27
+ private
39
28
 
40
29
  def impl_class
41
- das = WithAdvisoryLock::DatabaseAdapterSupport.new(connection)
42
- impl_class = if das.postgresql?
30
+ case WithAdvisoryLock::DatabaseAdapterSupport.new(connection).adapter
31
+ when :postgresql
43
32
  WithAdvisoryLock::PostgreSQL
44
- elsif das.mysql?
33
+ when :mysql
45
34
  WithAdvisoryLock::MySQL
46
- else
35
+ else #sqlite
47
36
  WithAdvisoryLock::Flock
48
37
  end
49
38
  end
@@ -15,5 +15,11 @@ module WithAdvisoryLock
15
15
  def sqlite?
16
16
  :sqlite3 == @sym_name
17
17
  end
18
+
19
+ def adapter
20
+ return :mysql if mysql?
21
+ return :postgresql if postgresql?
22
+ :sqlite3
23
+ end
18
24
  end
19
25
  end
@@ -5,8 +5,8 @@ module WithAdvisoryLock
5
5
 
6
6
  def filename
7
7
  @filename ||= begin
8
- safe = @lock_name.to_s.gsub(/[^a-z0-9]/i, '')
9
- fn = ".lock-#{safe}-#{stable_hashcode(@lock_name)}"
8
+ safe = lock_str.to_s.gsub(/[^a-z0-9]/i, '')
9
+ fn = ".lock-#{safe}-#{stable_hashcode(lock_str)}"
10
10
  # Let the user specify a directory besides CWD.
11
11
  ENV['FLOCK_DIR'] ? File.expand_path(fn, ENV['FLOCK_DIR']) : fn
12
12
  end
@@ -1,9 +1,6 @@
1
- require 'with_advisory_lock/nested_advisory_lock_error'
2
1
  module WithAdvisoryLock
3
2
  class MySQL < Base
4
-
5
3
  # See http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock
6
-
7
4
  def try_lock
8
5
  unless lock_stack.empty?
9
6
  raise NestedAdvisoryLockError.new(
@@ -14,30 +11,27 @@ module WithAdvisoryLock
14
11
  # 0 if the attempt timed out (for example, because another client has
15
12
  # previously locked the name), or NULL if an error occurred
16
13
  # (such as running out of memory or the thread was killed with mysqladmin kill).
17
- # The timestamp prevents AR from caching the result improperly, and is ignored.
18
- sql = "SELECT GET_LOCK(#{quoted_lock_name}, 0), #{Time.now.to_f}"
19
- 1 == connection.select_value(sql).to_i
14
+ sql = "SELECT GET_LOCK(#{quoted_lock_str}, 0) #{query_cache_buster}"
15
+ connection.select_value(sql).to_i > 0
20
16
  end
21
17
 
22
18
  def release_lock
23
19
  # Returns > 0 if the lock was released,
24
- # 0 if the lock was not established by this thread (
25
- # in which case the lock is not released), and
20
+ # 0 if the lock was not established by this thread
21
+ # (in which case the lock is not released), and
26
22
  # NULL if the named lock did not exist.
27
- # The timestamp prevents AR from caching the result improperly, and is ignored.
28
- sql = "SELECT RELEASE_LOCK(#{quoted_lock_name}), #{Time.now.to_f}"
29
- 1 == connection.select_value(sql).to_i
23
+ sql = "SELECT RELEASE_LOCK(#{quoted_lock_str}) #{query_cache_buster}"
24
+ connection.select_value(sql).to_i > 0
30
25
  end
31
26
 
27
+ # MySQL doesn't support nested locks:
32
28
  def already_locked?
33
- lock_stack.last == @lock_name
29
+ lock_stack.last == lock_str
34
30
  end
35
31
 
36
- def advisory_lock_exists?(name)
37
- quoted_name = connection.quote(name)
38
- # See http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_is-used-lock
39
- sql = "SELECT IS_USED_LOCK(#{quoted_name})"
40
- connection.select_value(sql).present?
32
+ # MySQL wants a string as the lock key.
33
+ def quoted_lock_str
34
+ connection.quote(lock_str)
41
35
  end
42
36
  end
43
37
  end
@@ -1,29 +1,27 @@
1
-
2
1
  module WithAdvisoryLock
3
2
  class PostgreSQL < Base
4
-
5
3
  # See http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
6
-
7
4
  def try_lock
8
- # pg_try_advisory_lock will either obtain the lock immediately
9
- # and return true, or return false if the lock cannot be acquired immediately
10
- sql = "SELECT pg_try_advisory_lock(#{numeric_lock}), #{Time.now.to_f}"
11
- "t" == connection.select_value(sql).to_s
5
+ # pg_try_advisory_lock will either obtain the lock immediately and return true
6
+ # or return false if the lock cannot be acquired immediately
7
+ sql = "SELECT pg_try_advisory_lock(#{lock_keys.join(',')}) #{query_cache_buster}"
8
+ 't' == connection.select_value(sql).to_s
12
9
  end
13
10
 
14
11
  def release_lock
15
- sql = "SELECT pg_advisory_unlock(#{numeric_lock}), #{Time.now.to_f}"
16
- "t" == connection.select_value(sql).to_s
12
+ sql = "SELECT pg_advisory_unlock(#{lock_keys.join(',')}) #{query_cache_buster}"
13
+ 't' == connection.select_value(sql).to_s
17
14
  end
18
15
 
19
- def numeric_lock(name=lock_name)
20
- stable_hashcode(name)
16
+ # PostgreSQL wants 2 32bit integers as the lock key.
17
+ def lock_keys
18
+ @lock_keys ||= begin
19
+ [stable_hashcode(lock_name), ENV['WITH_ADVISORY_LOCK_PREFIX']].map do |ea|
20
+ # pg advisory args must be 31 bit ints
21
+ ea.to_i & 0x7fffffff
22
+ end
23
+ end
21
24
  end
22
-
23
- def advisory_lock_exists?(name)
24
- sql = "SELECT 't'::text FROM pg_locks WHERE objid = #{numeric_lock(name)} AND locktype = 'advisory'"
25
- "t" == connection.select_value(sql).to_s
26
- end
27
-
28
25
  end
29
26
  end
27
+
@@ -1,3 +1,3 @@
1
1
  module WithAdvisoryLock
2
- VERSION = Gem::Version.new('1.0.0')
2
+ VERSION = Gem::Version.new('2.0.0')
3
3
  end
data/test/lock_test.rb CHANGED
@@ -1,9 +1,7 @@
1
1
  require 'minitest_helper'
2
2
 
3
3
  describe 'class methods' do
4
-
5
4
  let(:lock_name) { "test lock #{rand(1024)}" }
6
- let(:expected_lock_name) { "#{ENV['WITH_ADVISORY_LOCK_PREFIX']}#{lock_name}" }
7
5
 
8
6
  describe '.current_advisory_lock' do
9
7
  it "returns nil outside an advisory lock request" do
@@ -12,21 +10,30 @@ describe 'class methods' do
12
10
 
13
11
  it 'returns the name of the last lock acquired' do
14
12
  Tag.with_advisory_lock(lock_name) do
15
- Tag.current_advisory_lock.must_equal expected_lock_name
13
+ Tag.current_advisory_lock.must_match /#{lock_name}/
16
14
  end
17
15
  end
18
16
  end
19
17
 
20
18
  describe '.advisory_lock_exists?' do
21
19
  it "returns false for an unacquired lock" do
22
- Tag.advisory_lock_exists?(expected_lock_name).must_equal false
20
+ Tag.advisory_lock_exists?(lock_name).must_be_false
23
21
  end
24
22
 
25
23
  it 'returns the name of the last lock acquired' do
26
24
  Tag.with_advisory_lock(lock_name) do
27
- Tag.advisory_lock_exists?(expected_lock_name).must_equal true
25
+ Tag.advisory_lock_exists?(lock_name).must_be_true
28
26
  end
29
27
  end
30
28
  end
31
29
 
32
- end if test_lock_exists?
30
+ describe "0 timeout" do
31
+ it 'attempts the lock exactly once with no timeout' do
32
+ block_was_yielded = false
33
+ Tag.with_advisory_lock(lock_name, 0) do
34
+ block_was_yielded = true
35
+ end
36
+ block_was_yielded.must_be_true
37
+ end
38
+ end
39
+ end
@@ -2,28 +2,31 @@ require 'erb'
2
2
  require 'active_record'
3
3
  require 'with_advisory_lock'
4
4
  require 'tmpdir'
5
+ require 'securerandom'
5
6
 
6
7
  db_config = File.expand_path("database.yml", File.dirname(__FILE__))
7
8
  ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(db_config)).result)
8
9
 
9
10
  def env_db
10
- ENV["DB"] || "mysql"
11
+ (ENV["DB"] || :mysql).to_sym
11
12
  end
12
13
 
14
+ ENV["WITH_ADVISORY_LOCK_PREFIX"] ||= SecureRandom.base64
15
+
13
16
  ActiveRecord::Base.establish_connection(env_db)
14
17
  ActiveRecord::Migration.verbose = false
15
18
 
16
19
  require 'test_models'
20
+ begin
21
+ require 'minitest'
22
+ rescue LoadError => rails_four_zero_is_lame
23
+ end
17
24
  require 'minitest/autorun'
18
25
  require 'minitest/great_expectations'
19
26
  require 'mocha/setup'
20
27
 
21
28
  Thread.abort_on_exception = true
22
29
 
23
- def test_lock_exists?
24
- %w{mysql postgres}.include? env_db
25
- end
26
-
27
30
  class MiniTest::Spec
28
31
  before do
29
32
  ENV['FLOCK_DIR'] = Dir.mktmpdir
data/test/nesting_test.rb CHANGED
@@ -26,7 +26,7 @@ describe "lock nesting" do
26
26
  end
27
27
 
28
28
  it "raises errors with MySQL when acquiring nested lock" do
29
- skip unless env_db == 'mysql'
29
+ skip unless env_db == :mysql
30
30
  exc = proc {
31
31
  Tag.with_advisory_lock("first") do
32
32
  Tag.with_advisory_lock("second") do
@@ -37,7 +37,7 @@ describe "lock nesting" do
37
37
  end
38
38
 
39
39
  it "supports nested advisory locks with !MySQL" do
40
- skip if env_db == 'mysql'
40
+ skip if env_db == :mysql
41
41
  impl = WithAdvisoryLock::Base.new(nil, nil, nil)
42
42
  impl.lock_stack.must_be_empty
43
43
  Tag.with_advisory_lock("first") do
@@ -1,97 +1,115 @@
1
1
  require 'minitest_helper'
2
2
 
3
- parallelism_is_broken = begin
4
- # Rails < 3.2 has known bugs with parallelism
5
- (ActiveRecord::VERSION::MAJOR <= 3 && ActiveRecord::VERSION::MINOR < 2) ||
6
- # SQLite doesn't support parallel writes
7
- ENV["DB"] =~ /sqlite/
8
- end
9
-
10
3
  describe "parallelism" do
11
- def find_or_create_at(run_at, with_advisory_lock)
12
- ActiveRecord::Base.connection.reconnect!
13
- sleep(run_at - Time.now.to_f)
14
- name = run_at.to_s
15
- task = lambda do
4
+ class FindOrCreateWorker
5
+ def initialize(target, run_at, name, use_advisory_lock)
6
+ @thread = Thread.new do
7
+ ActiveRecord::Base.connection_pool.with_connection do
8
+ sleep((run_at - Time.now).to_f)
9
+ if use_advisory_lock
10
+ Tag.with_advisory_lock(name) { work(name) }
11
+ else
12
+ work(name)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def work(name)
16
19
  Tag.transaction do
17
- Tag.find_by_name(name) || Tag.create(:name => name)
20
+ Tag.where(name: name).first_or_create
18
21
  end
19
22
  end
20
- if with_advisory_lock
21
- Tag.with_advisory_lock(name, nil, &task)
22
- else
23
- task.call
23
+
24
+ def join
25
+ @thread.join
24
26
  end
25
- ActiveRecord::Base.connection.close if ActiveRecord::Base.connection.respond_to?(:close)
26
27
  end
27
28
 
28
- def run_workers(with_advisory_lock)
29
- skip if env_db == "sqlite"
30
- @iterations.times do
31
- time = (Time.now.to_i + 4).to_f
32
- threads = @workers.times.collect do
33
- Thread.new do
34
- find_or_create_at(time, with_advisory_lock)
35
- end
29
+ def run_workers
30
+ all_workers = []
31
+ @names = @iterations.times.map { |iter| "iteration ##{iter}" }
32
+ @names.each do |name|
33
+ wake_time = 1.second.from_now
34
+ workers = @workers.times.map do
35
+ FindOrCreateWorker.new(@target, wake_time, name, @use_advisory_lock)
36
36
  end
37
- threads.each { |ea| ea.join }
37
+ workers.each(&:join)
38
+ all_workers += workers
38
39
  end
39
- puts "Created #{Tag.all.size} (lock = #{with_advisory_lock})"
40
+ # Ensure we're still connected:
41
+ ActiveRecord::Base.connection_pool.connection
42
+ all_workers
40
43
  end
41
44
 
42
45
  before :each do
43
- @iterations = 5
44
- @workers = 5
46
+ ActiveRecord::Base.connection.reconnect!
47
+ @workers = 10
45
48
  end
46
49
 
47
- it "parallel threads create multiple duplicate rows" do
48
- run_workers(with_advisory_lock = false)
50
+ it "creates multiple duplicate rows without advisory locks" do
51
+ @use_advisory_lock = false
52
+ @iterations = 1
53
+ run_workers
49
54
  Tag.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
50
55
  TagAudit.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
51
56
  Label.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
52
- end
57
+ end unless env_db == :sqlite
53
58
 
54
- it "parallel threads with_advisory_lock don't create multiple duplicate rows" do
55
- run_workers(with_advisory_lock = true)
59
+ it "doesn't create multiple duplicate rows with advisory locks" do
60
+ @use_advisory_lock = true
61
+ @iterations = 10
62
+ run_workers
56
63
  Tag.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
57
64
  TagAudit.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
58
65
  Label.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
59
66
  end
67
+ end
60
68
 
61
- it "returns false if the lock wasn't acquirable" do
62
- t1_acquired_lock = false
63
- t1_return_value = nil
64
- t1 = Thread.new do
65
- ActiveRecord::Base.connection.reconnect!
66
- t1_return_value = Label.with_advisory_lock("testing 1,2,3") do
67
- t1_acquired_lock = true
68
- sleep(0.3)
69
- "boom"
69
+ describe "separate thread tests" do
70
+ let(:lock_name) { "testing 1,2,3" }
71
+
72
+ before do
73
+ @t1_acquired_lock = false
74
+ @t1_return_value = nil
75
+
76
+ @t1 = Thread.new do
77
+ ActiveRecord::Base.connection_pool.with_connection do
78
+ @t1_return_value = Label.with_advisory_lock(lock_name) do
79
+ t1_acquired_lock = true
80
+ sleep(0.4)
81
+ 't1 finished'
82
+ end
70
83
  end
71
84
  end
72
85
 
73
- # Make sure the lock is acquired:
86
+ # Wait for the thread to acquire the lock:
74
87
  sleep(0.1)
88
+ ActiveRecord::Base.connection.reconnect!
89
+ end
75
90
 
76
- # Now try to acquire the lock impatiently:
77
- t2_acquired_lock = false
78
- t2_return_value = nil
79
- t2 = Thread.new do
80
- ActiveRecord::Base.connection.reconnect!
81
- t2_return_value = Label.with_advisory_lock("testing 1,2,3", 0.1) do
82
- t2_acquired_lock = true
83
- "not expected"
84
- end
85
- end
91
+ after do
92
+ @t1.join
93
+ end
94
+
95
+ it "#with_advisory_lock with a 0 timeout returns false immediately" do
96
+ response = Label.with_advisory_lock(lock_name, 0) {}
97
+ response.must_be_false
98
+ end
86
99
 
87
- # Wait for them to finish:
88
- t1.join
89
- t2.join
100
+ it "#advisory_lock_exists? returns true when another thread has the lock" do
101
+ Tag.advisory_lock_exists?(lock_name).must_be_true
102
+ end
90
103
 
91
- t1_acquired_lock.must_be_true
92
- t1_return_value.must_equal "boom"
104
+ it "can re-establish the lock after the other thread releases it" do
105
+ @t1.join
106
+ @t1_return_value.must_equal 't1 finished'
93
107
 
94
- t2_acquired_lock.must_be_false
95
- t2_return_value.must_be_false
108
+ # We should now be able to acquire the lock immediately:
109
+ reacquired = false
110
+ Label.with_advisory_lock(lock_name, 0) do
111
+ reacquired = true
112
+ end.must_be_true
113
+ reacquired.must_be_true
96
114
  end
97
- end unless parallelism_is_broken
115
+ end
data/test/test_models.rb CHANGED
@@ -12,8 +12,8 @@ end
12
12
 
13
13
  class Tag < ActiveRecord::Base
14
14
  after_save do
15
- TagAudit.create { |ea| ea.tag_name = name }
16
- Label.create { |ea| ea.name = name }
15
+ TagAudit.create(tag_name: name)
16
+ Label.create(name: name)
17
17
  end
18
18
  end
19
19
 
data/tests.sh CHANGED
@@ -1,10 +1,11 @@
1
- #!/bin/sh -e
2
- export BUNDLE_GEMFILE DB
1
+ #!/bin/bash -e
2
+ export DB
3
3
 
4
- for BUNDLE_GEMFILE in ci/Gemfile.rails-4.1.x ci/Gemfile.rails-3.2.x ; do
5
- for DB in sqlite mysql postgresql
6
- do
7
- echo $DB $BUNDLE_GEMFILE `ruby -v`
8
- bundle exec rake
4
+ for RUBY in 2.1.2 jruby-1.7.12 ; do
5
+ rbenv local $RUBY
6
+ for DB in mysql postgresql sqlite ; do
7
+ echo "$DB | $(ruby -v)"
8
+ appraisal bundle update
9
+ appraisal rake test
9
10
  done
10
11
  done
@@ -18,14 +18,12 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^test/})
19
19
  gem.require_paths = %w(lib)
20
20
 
21
- gem.add_runtime_dependency 'activerecord', '>= 3.0.0'
21
+ gem.add_runtime_dependency 'activerecord', '>= 3.2'
22
22
 
23
23
  gem.add_development_dependency 'rake'
24
24
  gem.add_development_dependency 'yard'
25
25
  gem.add_development_dependency 'minitest'
26
26
  gem.add_development_dependency 'minitest-great_expectations'
27
27
  gem.add_development_dependency 'mocha'
28
- gem.add_development_dependency 'mysql2'
29
- gem.add_development_dependency 'pg'
30
- gem.add_development_dependency 'sqlite3'
28
+ gem.add_development_dependency 'appraisal'
31
29
  end
metadata CHANGED
@@ -1,139 +1,111 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: with_advisory_lock
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew McEachen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-16 00:00:00.000000000 Z
11
+ date: 2014-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ! '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 3.0.0
19
+ version: '3.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ! '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 3.0.0
26
+ version: '3.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: yard
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ! '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ! '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ! '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ! '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: minitest-great_expectations
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ! '>='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ! '>='
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: mocha
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ! '>='
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ! '>='
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: mysql2
98
+ name: appraisal
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ! '>='
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
103
  version: '0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ! '>='
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: pg
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ! '>='
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ! '>='
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: sqlite3
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ! '>='
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ! '>='
108
+ - - ">="
137
109
  - !ruby/object:Gem::Version
138
110
  version: '0'
139
111
  description: Advisory locking for ActiveRecord
@@ -143,17 +115,17 @@ executables: []
143
115
  extensions: []
144
116
  extra_rdoc_files: []
145
117
  files:
146
- - .gitignore
147
- - .travis.yml
118
+ - ".gitignore"
119
+ - ".travis.yml"
120
+ - Appraisals
148
121
  - Gemfile
149
122
  - LICENSE.txt
150
123
  - README.md
151
124
  - Rakefile
152
- - ci/Gemfile.rails-3.0.x
153
- - ci/Gemfile.rails-3.1.x
154
- - ci/Gemfile.rails-3.2.x
155
- - ci/Gemfile.rails-4.0.x
156
- - ci/Gemfile.rails-4.1.x
125
+ - gemfiles/activerecord_3.2.gemfile
126
+ - gemfiles/activerecord_4.0.gemfile
127
+ - gemfiles/activerecord_4.1.gemfile
128
+ - gemfiles/activerecord_edge.gemfile
157
129
  - lib/with_advisory_lock.rb
158
130
  - lib/with_advisory_lock/base.rb
159
131
  - lib/with_advisory_lock/concern.rb
@@ -169,7 +141,6 @@ files:
169
141
  - test/minitest_helper.rb
170
142
  - test/nesting_test.rb
171
143
  - test/parallelism_test.rb
172
- - test/simple_parallel_test.rb
173
144
  - test/test_models.rb
174
145
  - tests.sh
175
146
  - with_advisory_lock.gemspec
@@ -183,17 +154,17 @@ require_paths:
183
154
  - lib
184
155
  required_ruby_version: !ruby/object:Gem::Requirement
185
156
  requirements:
186
- - - ! '>='
157
+ - - ">="
187
158
  - !ruby/object:Gem::Version
188
159
  version: '0'
189
160
  required_rubygems_version: !ruby/object:Gem::Requirement
190
161
  requirements:
191
- - - ! '>='
162
+ - - ">="
192
163
  - !ruby/object:Gem::Version
193
164
  version: '0'
194
165
  requirements: []
195
166
  rubyforge_project:
196
- rubygems_version: 2.2.0
167
+ rubygems_version: 2.3.0
197
168
  signing_key:
198
169
  specification_version: 4
199
170
  summary: Advisory locking for ActiveRecord
@@ -204,6 +175,5 @@ test_files:
204
175
  - test/minitest_helper.rb
205
176
  - test/nesting_test.rb
206
177
  - test/parallelism_test.rb
207
- - test/simple_parallel_test.rb
208
178
  - test/test_models.rb
209
179
  has_rdoc:
@@ -1,5 +0,0 @@
1
- source 'https://rubygems.org'
2
- gemspec :path => '..'
3
-
4
- gem 'activerecord', '~> 3.0.0'
5
- gem 'mysql2', '< 0.3.0' # See https://github.com/brianmario/mysql2/issues/155
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
- gemspec :path => '..'
3
-
4
- gem 'activerecord', '~> 3.1.0'
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
- gemspec :path => '..'
3
-
4
- gem 'activerecord', '~> 3.2.0'
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
- gemspec :path => '..'
3
-
4
- gem 'activerecord', '~> 4.0.0'
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
- gemspec :path => '..'
3
-
4
- gem 'activerecord', '~> 4.1.0.beta1'
@@ -1,37 +0,0 @@
1
- require 'minitest_helper'
2
-
3
- describe "prevents threads from accessing a resource concurrently" do
4
- def assert_correct_parallel_behavior(lock_name)
5
- times = ActiveSupport::OrderedHash.new
6
- ActiveRecord::Base.connection_pool.disconnect!
7
- t1 = Thread.new do
8
- ActiveRecord::Base.connection.reconnect!
9
- ActiveRecord::Base.with_advisory_lock(lock_name) do
10
- times[:t1_acquire] = Time.now
11
- sleep 0.5
12
- end
13
- times[:t1_release] = Time.now
14
- end
15
- sleep 0.1
16
- t2 = Thread.new do
17
- ActiveRecord::Base.connection.reconnect!
18
- ActiveRecord::Base.with_advisory_lock(lock_name) do
19
- times[:t2_acquire] = Time.now
20
- sleep 1
21
- end
22
- times[:t2_release] = Time.now
23
- end
24
- t1.join
25
- t2.join
26
- times.keys.must_equal [:t1_acquire, :t1_release, :t2_acquire, :t2_release]
27
- times[:t2_acquire].must_be :>, times[:t1_release]
28
- end
29
-
30
- it "with a string lock name" do
31
- assert_correct_parallel_behavior("example lock name")
32
- end
33
-
34
- it "with a numeric lock name" do
35
- assert_correct_parallel_behavior(1234)
36
- end
37
- end