with_transactional_lock 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.com/Betterment/with_transactional_lock.svg?token=6b6DErRMUHX47kEoBZ3t&branch=master)](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: []
|