with_advisory_lock 1.0.0 → 2.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.
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