with_advisory_lock 0.0.1
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/.gitignore +18 -0
- data/.travis.yml +23 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +85 -0
- data/Rakefile +17 -0
- data/lib/with_advisory_lock.rb +5 -0
- data/lib/with_advisory_lock/base.rb +31 -0
- data/lib/with_advisory_lock/concern.rb +33 -0
- data/lib/with_advisory_lock/flock.rb +30 -0
- data/lib/with_advisory_lock/mysql.rb +22 -0
- data/lib/with_advisory_lock/postgresql.rb +26 -0
- data/lib/with_advisory_lock/version.rb +3 -0
- data/test/database.yml +17 -0
- data/test/minitest_helper.rb +29 -0
- data/test/test_models.rb +24 -0
- data/test/with_advisory_lock_test.rb +97 -0
- data/with_advisory_lock.gemspec +30 -0
- metadata +218 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
3
|
+
rvm:
|
4
|
+
- 1.8.7
|
5
|
+
- 1.9.3
|
6
|
+
|
7
|
+
env:
|
8
|
+
- DB=sqlite
|
9
|
+
- DB=mysql
|
10
|
+
- DB=pg
|
11
|
+
|
12
|
+
# 1.8.7 and sqlite fails: "SQL statements in progress: rollback transaction"
|
13
|
+
matrix:
|
14
|
+
exclude:
|
15
|
+
- rvm: 1.8.7
|
16
|
+
env: DB=sqlite
|
17
|
+
|
18
|
+
script: bundle exec rake
|
19
|
+
|
20
|
+
before_script:
|
21
|
+
- mysql -e 'create database with_advisory_lock_test'
|
22
|
+
- psql -c 'create database with_advisory_lock_test' -U postgres
|
23
|
+
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Matthew McEachen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# with_advisory_lock [](https://travis-ci.org/mceachen/with_advisory_lock)
|
2
|
+
|
3
|
+
Adds advisory locking to ActiveRecord 3.x.
|
4
|
+
[MySQL](http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock)
|
5
|
+
and [PostgreSQL](http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS)
|
6
|
+
are supported natively. SQLite resorts to file locking (which won't span hosts, of course!).
|
7
|
+
|
8
|
+
## What's an "Advisory Lock"?
|
9
|
+
|
10
|
+
An advisory lock is a [mutex](http://en.wikipedia.org/wiki/Mutual_exclusion) used to ensure no two
|
11
|
+
processes run some process at the same time. When the advisory lock is powered by your database
|
12
|
+
server, as long as it isn't SQLite, your mutex spans hosts.
|
13
|
+
|
14
|
+
Advisory locks ignore database transaction boundaries.
|
15
|
+
|
16
|
+
## Lock Types
|
17
|
+
|
18
|
+
First off, know that there are **lots** of different kinds of locks available to you. You want the
|
19
|
+
finest-grain lock that ensures correctness. If you choose a lock that is too coarse, you are
|
20
|
+
unnecessarily blocking other processes.
|
21
|
+
|
22
|
+
### Row-level locks
|
23
|
+
Whether [optimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html)
|
24
|
+
or [pessimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html),
|
25
|
+
row-level locks prevent concurrent modification to a given model.
|
26
|
+
|
27
|
+
**If you're building a
|
28
|
+
[CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete) application, this will be your
|
29
|
+
most commonly used lock.**
|
30
|
+
|
31
|
+
### Advisory locks
|
32
|
+
|
33
|
+
These are named mutexes that are inherently "application level"—it is up to the application
|
34
|
+
to acquire, run a critical code section, and release the advisory lock.
|
35
|
+
|
36
|
+
### Table-level locks
|
37
|
+
|
38
|
+
Provided through something like the [monogamy](https://github.com/mceachen/monogamy)
|
39
|
+
gem, these prevent concurrent access to **any instance of a model**. You probably don't want these,
|
40
|
+
and they can be a source of [deadlocks](http://en.wikipedia.org/wiki/Deadlock).
|
41
|
+
|
42
|
+
## Usage
|
43
|
+
|
44
|
+
Where ```User``` is an ActiveRecord model, and ```lock_name``` is some string:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
User.with_advisory_lock(lock_name) do
|
48
|
+
do_something_that_needs_locking
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
### What happens
|
53
|
+
|
54
|
+
1. The thread will wait indefinitely until the lock is acquired.
|
55
|
+
2. While inside the block, you will exclusively own the advisory lock.
|
56
|
+
3. The lock will be released after your block ends, even if an exception is raised in the block.
|
57
|
+
|
58
|
+
### Lock wait timeouts
|
59
|
+
|
60
|
+
The second parameter for ```with_advisory_lock``` is ```timeout_seconds```, and defaults to ```nil```,
|
61
|
+
which means wait indefinitely for the lock.
|
62
|
+
|
63
|
+
If a non-nil value is provided, the block may not be invoked.
|
64
|
+
|
65
|
+
The return value of ```with_advisory_lock``` will be the result of the yielded block,
|
66
|
+
if the lock was able to be acquired and the block yielded, or ```false```, if you provided
|
67
|
+
a timeout_seconds value and the lock was not able to be acquired in time.
|
68
|
+
|
69
|
+
## Installation
|
70
|
+
|
71
|
+
Add this line to your application's Gemfile:
|
72
|
+
|
73
|
+
``` ruby
|
74
|
+
gem 'with_advisory_lock'
|
75
|
+
```
|
76
|
+
|
77
|
+
And then execute:
|
78
|
+
|
79
|
+
$ bundle
|
80
|
+
|
81
|
+
## Changelog
|
82
|
+
|
83
|
+
### 0.0.1
|
84
|
+
|
85
|
+
* First whack
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'yard'
|
4
|
+
YARD::Rake::YardocTask.new do |t|
|
5
|
+
t.files = ['lib/**/*.rb', 'README.md']
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'rake/testtask'
|
9
|
+
|
10
|
+
Rake::TestTask.new do |t|
|
11
|
+
t.libs.push "lib"
|
12
|
+
t.libs.push "test"
|
13
|
+
t.pattern = 'test/**/*_test.rb'
|
14
|
+
t.verbose = true
|
15
|
+
end
|
16
|
+
|
17
|
+
task :default => :test
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module WithAdvisoryLock
|
2
|
+
class Base
|
3
|
+
attr_reader :connection, :lock_name, :timeout_seconds
|
4
|
+
|
5
|
+
def initialize(connection, lock_name, timeout_seconds)
|
6
|
+
@connection = connection
|
7
|
+
@lock_name = lock_name
|
8
|
+
@timeout_seconds = timeout_seconds
|
9
|
+
end
|
10
|
+
|
11
|
+
def quoted_lock_name
|
12
|
+
connection.quote(lock_name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def with_advisory_lock(&block)
|
16
|
+
give_up_at = Time.now + @timeout_seconds if @timeout_seconds
|
17
|
+
while @timeout_seconds.nil? || Time.now < give_up_at do
|
18
|
+
if try_lock
|
19
|
+
begin
|
20
|
+
return yield
|
21
|
+
ensure
|
22
|
+
release_lock
|
23
|
+
end
|
24
|
+
else
|
25
|
+
sleep(0.1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
false # failed to get lock in time.
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Tried desperately to monkeypatch the polymorphic connection object,
|
2
|
+
# but rails autoloading is too clever by half. Pull requests are welcome.
|
3
|
+
|
4
|
+
# Think of this module as a hipster, using "case" ironically.
|
5
|
+
|
6
|
+
require 'with_advisory_lock/base'
|
7
|
+
require 'with_advisory_lock/mysql'
|
8
|
+
require 'with_advisory_lock/postgresql'
|
9
|
+
require 'with_advisory_lock/flock'
|
10
|
+
require 'active_support/concern'
|
11
|
+
|
12
|
+
module WithAdvisoryLock
|
13
|
+
module Concern
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
def with_advisory_lock(lock_name, timeout_seconds=nil, &block)
|
17
|
+
self.class.with_advisory_lock(lock_name, timeout_seconds, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def with_advisory_lock(lock_name, timeout_seconds=nil, &block)
|
22
|
+
case (connection.adapter_name.downcase)
|
23
|
+
when "postgresql"
|
24
|
+
WithAdvisoryLock::PostgreSQL
|
25
|
+
when "mysql", "mysql2"
|
26
|
+
WithAdvisoryLock::MySQL
|
27
|
+
else
|
28
|
+
WithAdvisoryLock::Flock
|
29
|
+
end.new(connection, lock_name, timeout_seconds).with_advisory_lock(&block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module WithAdvisoryLock
|
4
|
+
class Flock < Base
|
5
|
+
|
6
|
+
def filename
|
7
|
+
@filename ||= begin
|
8
|
+
safe = @lock_name.gsub(/[^a-z0-9]/i, '')
|
9
|
+
fn = ".lock-#{safe}-#{@lock_name.to_s.hash}"
|
10
|
+
# Let the user specify a directory besides CWD.
|
11
|
+
ENV['FLOCK_DIR'] ? File.expand_path(fn, ENV['FLOCK_DIR']) : fn
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def file_io
|
16
|
+
@file_io ||= begin
|
17
|
+
FileUtils.touch(filename)
|
18
|
+
File.open(filename, 'r+')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def try_lock
|
23
|
+
0 == file_io.flock(File::LOCK_EX|File::LOCK_NB)
|
24
|
+
end
|
25
|
+
|
26
|
+
def release_lock
|
27
|
+
0 == file_io.flock(File::LOCK_UN)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module WithAdvisoryLock
|
2
|
+
class MySQL < Base
|
3
|
+
|
4
|
+
# See http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock
|
5
|
+
|
6
|
+
def try_lock
|
7
|
+
# Returns 1 if the lock was obtained successfully,
|
8
|
+
# 0 if the attempt timed out (for example, because another client has
|
9
|
+
# previously locked the name), or NULL if an error occurred
|
10
|
+
# (such as running out of memory or the thread was killed with mysqladmin kill).
|
11
|
+
1 == connection.select_value("SELECT GET_LOCK(#{quoted_lock_name}, 0)")
|
12
|
+
end
|
13
|
+
|
14
|
+
def release_lock
|
15
|
+
# Returns 1 if the lock was released,
|
16
|
+
# 0 if the lock was not established by this thread (
|
17
|
+
# in which case the lock is not released), and
|
18
|
+
# NULL if the named lock did not exist.
|
19
|
+
1 == connection.select_value("SELECT RELEASE_LOCK(#{quoted_lock_name})")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module WithAdvisoryLock
|
2
|
+
class PostgreSQL < Base
|
3
|
+
|
4
|
+
# See http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
|
5
|
+
|
6
|
+
def try_lock
|
7
|
+
# pg_try_advisory_lock will either obtain the lock immediately
|
8
|
+
# and return true, or return false if the lock cannot be acquired immediately
|
9
|
+
"t" == connection.select_value("SELECT pg_try_advisory_lock(#{numeric_lock})")
|
10
|
+
end
|
11
|
+
|
12
|
+
def release_lock
|
13
|
+
"t" == connection.select_value("SELECT pg_advisory_unlock(#{numeric_lock})")
|
14
|
+
end
|
15
|
+
|
16
|
+
def numeric_lock
|
17
|
+
@numeric_lock ||= begin
|
18
|
+
if lock_name.is_a? Numeric
|
19
|
+
lock_name.to_i
|
20
|
+
else
|
21
|
+
lock_name.to_s.hash
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/test/database.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
sqlite:
|
2
|
+
adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
|
3
|
+
database: test/sqlite3.db
|
4
|
+
timeout: 500
|
5
|
+
pool: 50
|
6
|
+
pg:
|
7
|
+
adapter: postgresql
|
8
|
+
username: postgres
|
9
|
+
database: with_advisory_lock_test
|
10
|
+
min_messages: ERROR
|
11
|
+
pool: 50
|
12
|
+
mysql:
|
13
|
+
adapter: mysql2
|
14
|
+
host: localhost
|
15
|
+
username: root
|
16
|
+
database: with_advisory_lock_test
|
17
|
+
pool: 50
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'active_record'
|
3
|
+
require 'with_advisory_lock'
|
4
|
+
require 'database_cleaner'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
db_config = File.expand_path("database.yml", File.dirname(__FILE__))
|
8
|
+
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(db_config)).result)
|
9
|
+
ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite")
|
10
|
+
ActiveRecord::Migration.verbose = false
|
11
|
+
|
12
|
+
require 'test_models'
|
13
|
+
require 'minitest/autorun'
|
14
|
+
require 'minitest/great_expectations'
|
15
|
+
|
16
|
+
Thread.abort_on_exception = true
|
17
|
+
|
18
|
+
DatabaseCleaner.strategy = :deletion
|
19
|
+
class MiniTest::Spec
|
20
|
+
before do
|
21
|
+
ENV['FLOCK_DIR'] = Dir.mktmpdir
|
22
|
+
DatabaseCleaner.start
|
23
|
+
end
|
24
|
+
after do
|
25
|
+
FileUtils.remove_entry_secure ENV['FLOCK_DIR']
|
26
|
+
DatabaseCleaner.clean
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
data/test/test_models.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 0) do
|
2
|
+
create_table "tags", :force => true do |t|
|
3
|
+
t.string "name"
|
4
|
+
end
|
5
|
+
create_table "tag_audits", :id => false, :force => true do |t|
|
6
|
+
t.string "tag_name"
|
7
|
+
end
|
8
|
+
create_table "labels", :id => false, :force => true do |t|
|
9
|
+
t.string "name"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Tag < ActiveRecord::Base
|
14
|
+
after_save do
|
15
|
+
TagAudit.create { |ea| ea.tag_name = name }
|
16
|
+
Label.create { |ea| ea.name = name }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class TagAudit < ActiveRecord::Base
|
21
|
+
end
|
22
|
+
|
23
|
+
class Label < ActiveRecord::Base
|
24
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe "with_advisory_lock" do
|
4
|
+
it "adds with_advisory_lock to ActiveRecord classes" do
|
5
|
+
assert Tag.respond_to?(:with_advisory_lock)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "adds with_advisory_lock to ActiveRecord instances" do
|
9
|
+
assert Tag.new.respond_to?(:with_advisory_lock)
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_or_create_at_even_second(run_at, with_advisory_lock)
|
13
|
+
sleep(run_at - Time.now.to_f)
|
14
|
+
ActiveRecord::Base.connection.reconnect!
|
15
|
+
name = run_at.to_s
|
16
|
+
task = lambda { Tag.find_by_name(name) || Tag.create!(:name => name) }
|
17
|
+
if with_advisory_lock
|
18
|
+
Tag.with_advisory_lock(name, nil, &task)
|
19
|
+
else
|
20
|
+
task.call
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def run_workers(with_advisory_lock)
|
25
|
+
start_time = Time.now.to_i + 2
|
26
|
+
threads = @workers.times.collect do
|
27
|
+
Thread.new do
|
28
|
+
@iterations.times do |ea|
|
29
|
+
find_or_create_at_even_second(start_time + (ea * 2), with_advisory_lock)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
threads.each { |ea| ea.join }
|
34
|
+
puts "Created #{Tag.all.size} (lock = #{with_advisory_lock})"
|
35
|
+
end
|
36
|
+
|
37
|
+
before :each do
|
38
|
+
@iterations = 5
|
39
|
+
@workers = 7
|
40
|
+
end
|
41
|
+
|
42
|
+
it "parallel threads create multiple duplicate rows" do
|
43
|
+
run_workers(with_advisory_lock = false)
|
44
|
+
if Tag.connection.adapter_name == "SQLite" && RUBY_VERSION == "1.9.3"
|
45
|
+
oper = :== # sqlite doesn't run in parallel.
|
46
|
+
else
|
47
|
+
oper = :> # Everything else should create duplicate rows.
|
48
|
+
end
|
49
|
+
Tag.all.size.must_be oper, @iterations # <- any duplicated rows will make me happy.
|
50
|
+
TagAudit.all.size.must_be oper, @iterations # <- any duplicated rows will make me happy.
|
51
|
+
Label.all.size.must_be oper, @iterations # <- any duplicated rows will make me happy.
|
52
|
+
end
|
53
|
+
|
54
|
+
it "parallel threads with_advisory_lock don't create multiple duplicate rows" do
|
55
|
+
run_workers(with_advisory_lock = true)
|
56
|
+
Tag.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
57
|
+
TagAudit.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
58
|
+
Label.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
59
|
+
end
|
60
|
+
|
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"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Make sure the lock is acquired:
|
74
|
+
sleep(0.1)
|
75
|
+
|
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
|
86
|
+
|
87
|
+
# Wait for them to finish:
|
88
|
+
t1.join
|
89
|
+
t2.join
|
90
|
+
|
91
|
+
t1_acquired_lock.must_be_true
|
92
|
+
t1_return_value.must_equal "boom"
|
93
|
+
|
94
|
+
t2_acquired_lock.must_be_false
|
95
|
+
t2_return_value.must_be_false
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'with_advisory_lock/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "with_advisory_lock"
|
8
|
+
gem.version = WithAdvisoryLock::VERSION
|
9
|
+
gem.authors = ["Matthew McEachen"]
|
10
|
+
gem.email = ["matthew+github@mceachen.org"]
|
11
|
+
gem.description = %q{Advisory locking for ActiveRecord}
|
12
|
+
gem.summary = gem.description
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^test/})
|
18
|
+
gem.require_paths = %w(lib)
|
19
|
+
|
20
|
+
gem.add_runtime_dependency 'activerecord', '>= 3.0.0'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rake'
|
23
|
+
gem.add_development_dependency 'yard'
|
24
|
+
gem.add_development_dependency 'minitest'
|
25
|
+
gem.add_development_dependency 'minitest-great_expectations'
|
26
|
+
gem.add_development_dependency 'mysql2'
|
27
|
+
gem.add_development_dependency 'pg'
|
28
|
+
gem.add_development_dependency 'sqlite3'
|
29
|
+
gem.add_development_dependency 'database_cleaner'
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: with_advisory_lock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matthew McEachen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: yard
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: minitest
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: minitest-great_expectations
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: mysql2
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: pg
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
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
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: sqlite3
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: database_cleaner
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
description: Advisory locking for ActiveRecord
|
159
|
+
email:
|
160
|
+
- matthew+github@mceachen.org
|
161
|
+
executables: []
|
162
|
+
extensions: []
|
163
|
+
extra_rdoc_files: []
|
164
|
+
files:
|
165
|
+
- .gitignore
|
166
|
+
- .travis.yml
|
167
|
+
- Gemfile
|
168
|
+
- LICENSE.txt
|
169
|
+
- README.md
|
170
|
+
- Rakefile
|
171
|
+
- lib/with_advisory_lock.rb
|
172
|
+
- lib/with_advisory_lock/base.rb
|
173
|
+
- lib/with_advisory_lock/concern.rb
|
174
|
+
- lib/with_advisory_lock/flock.rb
|
175
|
+
- lib/with_advisory_lock/mysql.rb
|
176
|
+
- lib/with_advisory_lock/postgresql.rb
|
177
|
+
- lib/with_advisory_lock/version.rb
|
178
|
+
- test/database.yml
|
179
|
+
- test/minitest_helper.rb
|
180
|
+
- test/test_models.rb
|
181
|
+
- test/with_advisory_lock_test.rb
|
182
|
+
- with_advisory_lock.gemspec
|
183
|
+
homepage: ''
|
184
|
+
licenses: []
|
185
|
+
post_install_message:
|
186
|
+
rdoc_options: []
|
187
|
+
require_paths:
|
188
|
+
- lib
|
189
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
190
|
+
none: false
|
191
|
+
requirements:
|
192
|
+
- - ! '>='
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
segments:
|
196
|
+
- 0
|
197
|
+
hash: 2709177541654258140
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
|
+
none: false
|
200
|
+
requirements:
|
201
|
+
- - ! '>='
|
202
|
+
- !ruby/object:Gem::Version
|
203
|
+
version: '0'
|
204
|
+
segments:
|
205
|
+
- 0
|
206
|
+
hash: 2709177541654258140
|
207
|
+
requirements: []
|
208
|
+
rubyforge_project:
|
209
|
+
rubygems_version: 1.8.23
|
210
|
+
signing_key:
|
211
|
+
specification_version: 3
|
212
|
+
summary: Advisory locking for ActiveRecord
|
213
|
+
test_files:
|
214
|
+
- test/database.yml
|
215
|
+
- test/minitest_helper.rb
|
216
|
+
- test/test_models.rb
|
217
|
+
- test/with_advisory_lock_test.rb
|
218
|
+
has_rdoc:
|