with_transactional_lock 1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +125 -0
- data/Rakefile +41 -0
- data/lib/generators/with_transactional_lock/install/install_generator.rb +34 -0
- data/lib/generators/with_transactional_lock/install/templates/README +8 -0
- data/lib/generators/with_transactional_lock/install/templates/db/migrate/create_transactional_advisory_locks.rb +8 -0
- data/lib/tasks/with_transactional_lock_tasks.rake +4 -0
- data/lib/with_transactional_lock.rb +11 -0
- data/lib/with_transactional_lock/engine.rb +4 -0
- data/lib/with_transactional_lock/mixin.rb +72 -0
- data/lib/with_transactional_lock/my_sql_helper.rb +13 -0
- data/lib/with_transactional_lock/version.rb +3 -0
- metadata +174 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 39e6b2f7021e9656666cecc940c89da9b7e8152b
|
4
|
+
data.tar.gz: aa13d311218556caa23e1ea7f8212f6cfc920d55
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 478fd8a135e213bc74a9b1bd3d446a8091761685b19b1936bc129bafe9491dd9d8bc6d0b99ad412bedc314b3aaebfdba042c5a7aaa7c27e3b2d2201dde556aca
|
7
|
+
data.tar.gz: adc45942dd45574a4b6a1a1f9b14df677b3c9f350dccdd2bf82d3a4ee388e526572c6c8f19bbc50e0f0c1027ecece4407eaf008f8bebc5cbcefec6a17739869a
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016-2018 Betterment
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# with_transactional_lock
|
2
|
+
|
3
|
+
[](https://travis-ci.com/Betterment/with_transactional_lock)
|
4
|
+
|
5
|
+
A simple extension to ActiveRecord for performing advisory locking on
|
6
|
+
MySQL and PostgreSQL.
|
7
|
+
|
8
|
+
An advisory lock is a database-level mutex that can be used to prevent
|
9
|
+
concurrent access to a shared resource or to prevent two workers from
|
10
|
+
performing the same process concurrently.
|
11
|
+
|
12
|
+
This gem is different from other advisory locking gems because it
|
13
|
+
uses advisory transaction locks instead of advisory session locks.
|
14
|
+
|
15
|
+
## Why transactional?
|
16
|
+
|
17
|
+
At Betterment correctness is paramount. As such, we chose to use
|
18
|
+
transaction-level locks because we find them to be more trustworthy for
|
19
|
+
our production use.
|
20
|
+
|
21
|
+
Some types of advisory locks can be held for the lifetime of a database
|
22
|
+
session. In a typical database connection pooling configuration, like
|
23
|
+
that of ActiveRecord, a connection and its associated session are not
|
24
|
+
reset when the connection is returned to the pool. In that case, if you
|
25
|
+
do not properly release a session-level lock, it will leak and become
|
26
|
+
effectively unreleasable. In our Betterment production systems, that
|
27
|
+
type of risk is unacceptable. Fortunately, because ActiveRecord
|
28
|
+
doesn't leak open transactions back into the connection pool, we can
|
29
|
+
use transactional locks as our safety device rather than becoming
|
30
|
+
familiar with the nuances necessary to be confident that we are
|
31
|
+
preventing leaks.
|
32
|
+
|
33
|
+
Additionally, application developers tend to think about discrete units
|
34
|
+
of database work in terms of transactions. By leveraging the transaction
|
35
|
+
boundary, we ensure the advisory lock is released at the earliest
|
36
|
+
possible moment that it can be and no sooner.
|
37
|
+
|
38
|
+
## Lock acquisition efficiency & fairness
|
39
|
+
|
40
|
+
Some libraries providing advisory locking use try-lock semantics. This
|
41
|
+
library uses a blocking strategy for lock acquisition. It will wait
|
42
|
+
until the lock can be acquired instead of immediately returning false
|
43
|
+
and forcing the application layer to manage retry behavior.
|
44
|
+
Additionally, by waiting in line (in the database) for locks that cannot
|
45
|
+
be immediately acquired, you get fairness in the acquisition sequence.
|
46
|
+
|
47
|
+
Notably, this library performs lock-waiting in the database rather than
|
48
|
+
using `Timeout.timeout`. That means that the waiting is bounded by your
|
49
|
+
database timeout rather than a library-specific option. We see this as a
|
50
|
+
strength of the library. In practice, we have found that if there is a
|
51
|
+
chance of contention that you can't afford to wait for, you should
|
52
|
+
perform the operation that requires the lock asynchronously and/or
|
53
|
+
reduce the time spent in your critical section so that you can afford to
|
54
|
+
wait.
|
55
|
+
|
56
|
+
In contrast, when using a try-based strategy your ability to acquire a
|
57
|
+
lock can get worse at higher levels of concurrency. You may spend more
|
58
|
+
time spinning in application code issuing requests for a lock instead of
|
59
|
+
waiting on I/O (possibly allowing another thread to use the CPU).
|
60
|
+
|
61
|
+
## Installation
|
62
|
+
|
63
|
+
Add this line to your application's Gemfile:
|
64
|
+
|
65
|
+
``` ruby
|
66
|
+
gem 'with_transactional_lock'
|
67
|
+
```
|
68
|
+
|
69
|
+
And then bundle install:
|
70
|
+
|
71
|
+
```
|
72
|
+
$ bundle install
|
73
|
+
```
|
74
|
+
|
75
|
+
And then if you're using MySQL, you will need to run the installer:
|
76
|
+
|
77
|
+
```
|
78
|
+
$ rails g with_transactional_lock:install
|
79
|
+
```
|
80
|
+
|
81
|
+
This will create a migration that will add an
|
82
|
+
`transactional_advisory_locks` table to your database.
|
83
|
+
|
84
|
+
## Usage
|
85
|
+
|
86
|
+
Because transactional locks are meaningless outside of the context of a
|
87
|
+
transaction, we provide an interface that wraps your work in a
|
88
|
+
transaction and acquires the lock.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
ActiveRecord::Base.with_transactional_lock('name_of_a_resource') do
|
92
|
+
# do something critical in here
|
93
|
+
# this block is already inside a transaction
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
This call will attempt to acquire an exclusive lock using the provided
|
98
|
+
lock name. It will wait indefinitely for that lock -- or at least as
|
99
|
+
long as your database connection timeout is willing to allow. Once the
|
100
|
+
lock is acquired you will have exclusive ownership of the advisory lock
|
101
|
+
with the name that you provided. Your block is free to execute its
|
102
|
+
critical work. Upon completion of your transaction, the lock will be
|
103
|
+
released.
|
104
|
+
|
105
|
+
## Supported databases
|
106
|
+
|
107
|
+
### PostgreSQL
|
108
|
+
|
109
|
+
PostgreSQL has first-class support for transactional advisory locks via
|
110
|
+
`pg_advisory_xact_lock`. This is an exclusive lock that is held for the
|
111
|
+
duration of a given transaction and automatically released upon
|
112
|
+
transaction commit.
|
113
|
+
|
114
|
+
### MySQL
|
115
|
+
|
116
|
+
MySQL does not have built-in support for transactional advisory locks.
|
117
|
+
So, MySQL gets a special treatment. We emulate the behavior of PostgreSQL
|
118
|
+
using a special `transactional_advisory_locks` table with a unique index
|
119
|
+
on the `lock_id` column. This allows us to provide the same transactional
|
120
|
+
and mutual exclusivity guarantees as PostgreSQL. The trade-off is that
|
121
|
+
you need to add another table to your database.
|
122
|
+
|
123
|
+
## License
|
124
|
+
|
125
|
+
Any contributions made to this project are covered under the MIT License, found [here](LICENSE)
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'WithTransactionalLock'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
load 'rails/tasks/statistics.rake'
|
20
|
+
|
21
|
+
Bundler::GemHelper.install_tasks
|
22
|
+
|
23
|
+
if Rails.env.development? || Rails.env.test?
|
24
|
+
if defined? Dummy
|
25
|
+
require 'rspec/core'
|
26
|
+
require 'rspec/core/rake_task'
|
27
|
+
require 'rubocop/rake_task'
|
28
|
+
|
29
|
+
RuboCop::RakeTask.new
|
30
|
+
RSpec::Core::RakeTask.new(:spec)
|
31
|
+
|
32
|
+
task(:default).clear
|
33
|
+
if ENV['APPRAISAL_INITIALIZED'] || ENV['TRAVIS']
|
34
|
+
task default: %i(rubocop spec)
|
35
|
+
else
|
36
|
+
require 'appraisal'
|
37
|
+
Appraisal::Task.new
|
38
|
+
task default: :appraisal
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module WithTransactionalLock
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
def show_readme
|
11
|
+
readme 'README'
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_with_transactional_lock_migration
|
15
|
+
if mysql?
|
16
|
+
migration_template(
|
17
|
+
'db/migrate/create_transactional_advisory_locks.rb',
|
18
|
+
'db/migrate/create_transactional_advisory_locks.rb'
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.next_migration_number(dir)
|
24
|
+
ActiveRecord::Generators::Base.next_migration_number(dir)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def mysql?
|
30
|
+
ActiveRecord::Base.connection.adapter_name.downcase =~ /mysql/
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
*******************************************************************************
|
2
|
+
|
3
|
+
If you're using MySql, then run `rake db:migrate` to setup the `transactional_advisory_locks`
|
4
|
+
table.
|
5
|
+
|
6
|
+
If you're using Postgresql, then you're all set!
|
7
|
+
|
8
|
+
*******************************************************************************
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class CreateTransactionalAdvisoryLocks < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :transactional_advisory_locks, id: false do |t|
|
4
|
+
t.integer :lock_id, null: false, limit: 8
|
5
|
+
end
|
6
|
+
add_index(:transactional_advisory_locks, :lock_id, unique: true, name: 'index_transactional_advisory_locks_on_lock_id')
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require "with_transactional_lock/engine"
|
2
|
+
|
3
|
+
module WithTransactionalLock
|
4
|
+
extend ActiveSupport::Autoload
|
5
|
+
autoload :Mixin
|
6
|
+
autoload :MySqlHelper
|
7
|
+
end
|
8
|
+
|
9
|
+
ActiveSupport.on_load :active_record do
|
10
|
+
ActiveRecord::Base.send :include, WithTransactionalLock::Mixin
|
11
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module WithTransactionalLock
|
4
|
+
module Mixin
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
delegate :with_transactional_lock, to: :class
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def with_transactional_lock(lock_name, &block)
|
10
|
+
_advisory_lock_class.new(connection, lock_name).yield_with_lock(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def _advisory_lock_class
|
16
|
+
@_advisory_lock_class ||= AdvisoryLockClassLocator.locate(connection)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module AdvisoryLockClassLocator
|
21
|
+
def self.locate(connection)
|
22
|
+
adapter = connection.adapter_name.downcase.to_sym
|
23
|
+
case adapter
|
24
|
+
when :mysql, :mysql2
|
25
|
+
MySqlAdvisoryLock
|
26
|
+
when :postgresql
|
27
|
+
PostgresAdvisoryLock
|
28
|
+
else
|
29
|
+
raise "adapter not supported: #{adapter}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class AdvisoryLockBase
|
35
|
+
attr_reader :connection, :lock_name
|
36
|
+
|
37
|
+
def initialize(connection, lock_name)
|
38
|
+
@connection = connection
|
39
|
+
@lock_name = lock_name
|
40
|
+
end
|
41
|
+
|
42
|
+
def yield_with_lock
|
43
|
+
connection.transaction do
|
44
|
+
acquire_lock
|
45
|
+
yield
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def db_lock_name
|
52
|
+
@db_lock_name ||= Digest::SHA256.digest(lock_name)[0, 8].unpack('q').first
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class MySqlAdvisoryLock < AdvisoryLockBase
|
57
|
+
private
|
58
|
+
|
59
|
+
def acquire_lock
|
60
|
+
connection.execute("insert into transactional_advisory_locks values (#{connection.quote(db_lock_name)}) on duplicate key update lock_id = lock_id")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class PostgresAdvisoryLock < AdvisoryLockBase
|
65
|
+
private
|
66
|
+
|
67
|
+
def acquire_lock
|
68
|
+
connection.execute("select pg_advisory_xact_lock(#{connection.quote(db_lock_name)})")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module WithTransactionalLock
|
2
|
+
class MySqlHelper
|
3
|
+
def self.cleanup(klass = ActiveRecord::Base)
|
4
|
+
klass.connection_pool.with_connection do |conn|
|
5
|
+
target_count = conn.select_value('select count(1) from transactional_advisory_locks')
|
6
|
+
count = 0
|
7
|
+
until count >= target_count
|
8
|
+
count += conn.delete('delete from transactional_advisory_locks limit 1000')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: with_transactional_lock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sam Moore
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-05-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.2'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: appraisal
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 2.2.0
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 2.2.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: database_cleaner
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
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
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: mime-types
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "<"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "<"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec-rails
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.1'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.1'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rspec-retry
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rubocop-betterment
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: travis
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
description: Advisory locking support for MySQL and Postgresql done right.
|
132
|
+
email:
|
133
|
+
- sam@betterment.com
|
134
|
+
executables: []
|
135
|
+
extensions: []
|
136
|
+
extra_rdoc_files: []
|
137
|
+
files:
|
138
|
+
- LICENSE
|
139
|
+
- README.md
|
140
|
+
- Rakefile
|
141
|
+
- lib/generators/with_transactional_lock/install/install_generator.rb
|
142
|
+
- lib/generators/with_transactional_lock/install/templates/README
|
143
|
+
- lib/generators/with_transactional_lock/install/templates/db/migrate/create_transactional_advisory_locks.rb
|
144
|
+
- lib/tasks/with_transactional_lock_tasks.rake
|
145
|
+
- lib/with_transactional_lock.rb
|
146
|
+
- lib/with_transactional_lock/engine.rb
|
147
|
+
- lib/with_transactional_lock/mixin.rb
|
148
|
+
- lib/with_transactional_lock/my_sql_helper.rb
|
149
|
+
- lib/with_transactional_lock/version.rb
|
150
|
+
homepage: https://github.com/Betterment/with_transactional_lock
|
151
|
+
licenses:
|
152
|
+
- MIT
|
153
|
+
metadata: {}
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
requirements: []
|
169
|
+
rubyforge_project:
|
170
|
+
rubygems_version: 2.5.1
|
171
|
+
signing_key:
|
172
|
+
specification_version: 4
|
173
|
+
summary: Transactional advisory locks for ActiveRecord
|
174
|
+
test_files: []
|