activejob-uniqueness 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +38 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +102 -0
- data/Rakefile +8 -0
- data/activejob-uniqueness.gemspec +36 -0
- data/lib/active_job/uniqueness.rb +44 -0
- data/lib/active_job/uniqueness/configuration.rb +35 -0
- data/lib/active_job/uniqueness/errors.rb +35 -0
- data/lib/active_job/uniqueness/lock_key.rb +60 -0
- data/lib/active_job/uniqueness/lock_manager.rb +32 -0
- data/lib/active_job/uniqueness/log_subscriber.rb +101 -0
- data/lib/active_job/uniqueness/patch.rb +89 -0
- data/lib/active_job/uniqueness/strategies.rb +31 -0
- data/lib/active_job/uniqueness/strategies/base.rb +112 -0
- data/lib/active_job/uniqueness/strategies/until_and_while_executing.rb +43 -0
- data/lib/active_job/uniqueness/strategies/until_executed.rb +17 -0
- data/lib/active_job/uniqueness/strategies/until_executing.rb +17 -0
- data/lib/active_job/uniqueness/strategies/until_expired.rb +13 -0
- data/lib/active_job/uniqueness/strategies/while_executing.rb +26 -0
- data/lib/active_job/uniqueness/test_lock_manager.rb +16 -0
- data/lib/active_job/uniqueness/version.rb +7 -0
- data/lib/activejob/uniqueness.rb +3 -0
- data/lib/generators/active_job/uniqueness/install_generator.rb +16 -0
- data/lib/generators/active_job/uniqueness/templates/config/initializers/active_job_uniqueness.rb +40 -0
- metadata +165 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5fda42367a82ec4d28e8e9dd2aa4e4f1774b28133170aab20095a7cf1366ce6f
|
4
|
+
data.tar.gz: 4e29cd3caf6b85713a570fd95aa4272eec7df23f96a7fb44fb05574c83dc1d5d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6ca9eb292b99425c77f3f5d3a2082ba3e15d045c683123203667328bb0e24033a3139a5b465fffd1da6b80fea6f3e963544cef1bbde5107ecb666d92d82396e6
|
7
|
+
data.tar.gz: 8deceb5728a0459ac983a9e0ec7501abf9ba050e9536c626770aa568504ba05737bf1664907abdff312c83cfe3c97d4ea61df572eaee56cfb54dd88245afa51d
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: enable
|
3
|
+
|
4
|
+
Layout/LineLength:
|
5
|
+
Max: 120
|
6
|
+
Exclude:
|
7
|
+
- spec/**/*
|
8
|
+
|
9
|
+
Layout/MultilineMethodCallIndentation:
|
10
|
+
Exclude:
|
11
|
+
- spec/**/*
|
12
|
+
|
13
|
+
Lint/AmbiguousBlockAssociation:
|
14
|
+
Exclude:
|
15
|
+
- spec/**/*
|
16
|
+
|
17
|
+
Metrics/AbcSize:
|
18
|
+
Exclude:
|
19
|
+
- spec/**/*
|
20
|
+
|
21
|
+
Metrics/BlockLength:
|
22
|
+
Exclude:
|
23
|
+
- activejob-uniqueness.gemspec
|
24
|
+
- spec/**/*
|
25
|
+
|
26
|
+
Metrics/MethodLength:
|
27
|
+
Exclude:
|
28
|
+
- spec/**/*
|
29
|
+
|
30
|
+
Style/ClassAndModuleChildren:
|
31
|
+
Exclude:
|
32
|
+
- spec/**/*
|
33
|
+
|
34
|
+
Style/Documentation:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
Style/RescueModifier:
|
38
|
+
Enabled: false
|
data/.travis.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
dist: xenial
|
3
|
+
language: ruby
|
4
|
+
services:
|
5
|
+
- redis-server
|
6
|
+
cache: bundler
|
7
|
+
rvm:
|
8
|
+
- 2.5.8
|
9
|
+
- 2.6.6
|
10
|
+
- 2.7.1
|
11
|
+
env:
|
12
|
+
- ACTIVEJOB_VERSION="~> 4.2.11"
|
13
|
+
- ACTIVEJOB_VERSION="~> 5.2.4"
|
14
|
+
- ACTIVEJOB_VERSION="~> 6.0.3"
|
15
|
+
jobs:
|
16
|
+
exclude:
|
17
|
+
- rvm: 2.7.1
|
18
|
+
env: ACTIVEJOB_VERSION="~> 4.2.11"
|
19
|
+
|
20
|
+
before_install: gem install bundler -v 1.17.3
|
21
|
+
bundler_args: --jobs 3 --retry 3
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
## Original Release: 0.1.0
|
data/Gemfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
6
|
+
|
7
|
+
# Specify your gem's dependencies in activejob-uniqueness.gemspec
|
8
|
+
gemspec
|
9
|
+
|
10
|
+
gem 'activejob', ENV.fetch('ACTIVEJOB_VERSION', '~> 4.2.11')
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Rustam Sharshenov
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# Job uniqueness for ActiveJob
|
2
|
+
[![Build Status](https://travis-ci.com/veeqo/activejob-uniqueness.svg?branch=master)](https://travis-ci.com/veeqo/activejob-uniqueness) [![Gem Version](https://badge.fury.io/rb/activejob-uniqueness.svg)](https://badge.fury.io/rb/activejob-uniqueness)
|
3
|
+
|
4
|
+
The gem allows to protect job uniqueness with next strategies:
|
5
|
+
|
6
|
+
| Strategy | The job is locked | The job is unlocked |
|
7
|
+
|-|-|-|
|
8
|
+
| `until_executing` | when **pushed** to the queue | when **processing starts** |
|
9
|
+
| `until_executed` | when **pushed** to the queue | when the job is **processed successfully** |
|
10
|
+
| `until_expired` | when **pushed** to the queue | when the lock is **expired** |
|
11
|
+
| `until_and_while_executing` | when **pushed** to the queue | when **processing starts**<br>a runtime lock is acquired to **prevent simultaneous jobs** |
|
12
|
+
| `while_executing` | when **processing starts** | when the job is **processed**<br>with any result including an error |
|
13
|
+
|
14
|
+
Inspired by [SidekiqUniqueJobs](https://github.com/mhenrixon/sidekiq-unique-jobs), uses [Redlock](https://github.com/leandromoreira/redlock-rb) under the hood, sponsored by [Veeqo](https://www.veeqo.com/).
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add the "meta-tags" gem to your Gemfile.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'activejob-uniqueness'
|
22
|
+
```
|
23
|
+
|
24
|
+
And run `bundle install` command.
|
25
|
+
|
26
|
+
## Configuration
|
27
|
+
|
28
|
+
ActiveJob::Uniqueness is ready to work without any configuration. It will use `REDIS_URL` to connect to Redis instance.
|
29
|
+
To override the defaults, create an initializer `config/initializers/active_job_uniqueness.rb` using the following command:
|
30
|
+
|
31
|
+
```sh
|
32
|
+
rails generate active_job:uniqueness:install
|
33
|
+
```
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Define uniqueness strategy for your job via `unique` class method:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class MyJob < ActiveJob::Base
|
41
|
+
unique :until_executed
|
42
|
+
|
43
|
+
# Custom expiration:
|
44
|
+
# unique :until_executed, lock_ttl: 3.hours
|
45
|
+
|
46
|
+
# Do not raise error on non unique jobs enqueuing:
|
47
|
+
# unique :until_executed, on_conflict: :log
|
48
|
+
|
49
|
+
# Handle conflict by custom Proc:
|
50
|
+
# unique :until_executed, on_conflict: ->(job) { job.logger.info 'Oops' }
|
51
|
+
|
52
|
+
# The :until_and_while_executing strategy supports extra attributes for a runtime lock:
|
53
|
+
# unique :until_and_while_executing runtime_lock_ttl: 10.minutes, on_runtime_conflict: :log
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
ActiveJob::Uniqueness allows to manually unlock jobs:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
# Remove the lock for particular arguments:
|
61
|
+
MyJob.unlock!(foo: 'bar')
|
62
|
+
# or
|
63
|
+
ActiveJob::Uniqueness.unlock!(job_class_name: 'MyJob', arguments: [{foo: 'bar'}])
|
64
|
+
|
65
|
+
# Remove all locks of MyJob
|
66
|
+
MyJob.unlock!
|
67
|
+
# or
|
68
|
+
ActiveJob::Uniqueness.unlock!(job_class_name: 'MyJob')
|
69
|
+
|
70
|
+
# Remove all locks
|
71
|
+
ActiveJob::Uniqueness.unlock!
|
72
|
+
```
|
73
|
+
|
74
|
+
## Test mode
|
75
|
+
|
76
|
+
Most probably you don't want jobs to be locked in tests. Add this line to your test suite (`rails_helper.rb`):
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
ActiveJob::Uniqueness.test_mode!
|
80
|
+
```
|
81
|
+
|
82
|
+
## Logging
|
83
|
+
|
84
|
+
ActiveJob::Uniqueness instruments `ActiveSupport::Notifications` with next events:
|
85
|
+
* `lock.active_job_uniqueness`
|
86
|
+
* `runtime_lock.active_job_uniqueness`
|
87
|
+
* `unlock.active_job_uniqueness`
|
88
|
+
* `runtime_unlock.active_job_uniqueness`
|
89
|
+
* `conflict.active_job_uniqueness`
|
90
|
+
* `runtime_conflict.active_job_uniqueness`
|
91
|
+
|
92
|
+
And then writes to `ActiveJob::Base.logger`.
|
93
|
+
|
94
|
+
### ActiveJob prior to version `6.1` will always log `Enqueued MyJob (Job ID) ...` even if the callback chain was halted. [Details](https://github.com/rails/rails/pull/37830)
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
|
98
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/veeqo/activejob-uniqueness.
|
99
|
+
|
100
|
+
## License
|
101
|
+
|
102
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'active_job/uniqueness/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'activejob-uniqueness'
|
9
|
+
spec.version = ActiveJob::Uniqueness::VERSION
|
10
|
+
spec.authors = ['Rustam Sharshenov']
|
11
|
+
spec.email = ['rustam@sharshenov.com']
|
12
|
+
|
13
|
+
spec.summary = 'Ensure uniqueness of your ActiveJob jobs'
|
14
|
+
spec.description = 'Ensure uniqueness of your ActiveJob jobs'
|
15
|
+
spec.homepage = 'https://github.com/veeqo/activejob-uniqueness'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/veeqo/activejob-uniqueness/blob/master/CHANGELOG.md'
|
22
|
+
end
|
23
|
+
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|\.rubocop.yml)/}) }
|
26
|
+
end
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
|
29
|
+
spec.add_dependency 'activejob', '>= 4.2'
|
30
|
+
spec.add_dependency 'redlock', '>= 1.2', '< 2'
|
31
|
+
|
32
|
+
spec.add_development_dependency 'bundler'
|
33
|
+
spec.add_development_dependency 'pry-byebug'
|
34
|
+
spec.add_development_dependency 'rake'
|
35
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
36
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
require 'redlock'
|
6
|
+
require 'openssl'
|
7
|
+
require 'active_job/uniqueness/version'
|
8
|
+
require 'active_job/uniqueness/errors'
|
9
|
+
require 'active_job/uniqueness/log_subscriber'
|
10
|
+
require 'active_job/uniqueness/patch'
|
11
|
+
|
12
|
+
module ActiveJob
|
13
|
+
module Uniqueness
|
14
|
+
extend ActiveSupport::Autoload
|
15
|
+
|
16
|
+
autoload :Configuration
|
17
|
+
autoload :LockKey
|
18
|
+
autoload :Strategies
|
19
|
+
autoload :LockManager
|
20
|
+
autoload :TestLockManager
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def configure
|
24
|
+
yield config
|
25
|
+
end
|
26
|
+
|
27
|
+
def config
|
28
|
+
@config ||= ActiveJob::Uniqueness::Configuration.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def lock_manager
|
32
|
+
@lock_manager ||= ActiveJob::Uniqueness::LockManager.new(config.redlock_servers, config.redlock_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def unlock!(*args)
|
36
|
+
lock_manager.delete_locks(ActiveJob::Uniqueness::LockKey.new(*args).wildcard_key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_mode!
|
40
|
+
@lock_manager = ActiveJob::Uniqueness::TestLockManager.new
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
# Use /config/initializer/activejob_uniqueness.rb to configure ActiveJob::Uniqueness
|
6
|
+
#
|
7
|
+
# ActiveJob::Uniqueness.configure do |c|
|
8
|
+
# c.lock_ttl = 3.hours
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
class Configuration
|
12
|
+
include ActiveSupport::Configurable
|
13
|
+
|
14
|
+
config_accessor(:lock_ttl) { 1.day }
|
15
|
+
config_accessor(:lock_prefix) { 'activejob_uniqueness' }
|
16
|
+
config_accessor(:on_conflict) { :raise }
|
17
|
+
config_accessor(:digest_method) { OpenSSL::Digest::MD5 }
|
18
|
+
config_accessor(:redlock_servers) { [ENV.fetch('REDIS_URL', 'redis://localhost:6379')] }
|
19
|
+
config_accessor(:redlock_options) { {} }
|
20
|
+
config_accessor(:lock_strategies) { {} }
|
21
|
+
|
22
|
+
def on_conflict=(action)
|
23
|
+
validate_on_conflict_action!(action)
|
24
|
+
|
25
|
+
config.on_conflict = action
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_on_conflict_action!(action)
|
29
|
+
return if action.nil? || %i[log raise].include?(action) || action.respond_to?(:call)
|
30
|
+
|
31
|
+
raise ActiveJob::Uniqueness::InvalidOnConflictAction, "Unexpected '#{action}' action on conflict"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
class Error < ::RuntimeError; end
|
6
|
+
|
7
|
+
# Raised when unknown strategy is referenced in the job class
|
8
|
+
#
|
9
|
+
# class MyJob < ActiveJob::Base
|
10
|
+
# unique :invalid_strategy # exception raised
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
class StrategyNotFound < Error; end
|
14
|
+
|
15
|
+
# Raised on attempt to enqueue a not unique job with :raise on_conflict.
|
16
|
+
# Also raised when the runtime lock is taken by some other job.
|
17
|
+
#
|
18
|
+
# class MyJob < ActiveJob::Base
|
19
|
+
# unique :until_expired, on_conflict: :raise
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# MyJob.perform_later(1)
|
23
|
+
# MyJob.perform_later(1) # exception raised
|
24
|
+
#
|
25
|
+
class JobNotUnique < Error; end
|
26
|
+
|
27
|
+
# Raised when unsupported on_conflict action is used
|
28
|
+
#
|
29
|
+
# class MyJob < ActiveJob::Base
|
30
|
+
# unique :until_expired, on_conflict: :die # exception raised
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
class InvalidOnConflictAction < Error; end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_job/arguments'
|
4
|
+
|
5
|
+
module ActiveJob
|
6
|
+
module Uniqueness
|
7
|
+
class LockKey
|
8
|
+
FALLBACK_ARGUMENTS_STRING = 'no_arguments'
|
9
|
+
|
10
|
+
delegate :lock_prefix, :digest_method, to: :'ActiveJob::Uniqueness.config'
|
11
|
+
|
12
|
+
attr_reader :job_class_name, :arguments
|
13
|
+
|
14
|
+
def initialize(job_class_name: nil, arguments: nil)
|
15
|
+
if arguments.present? && job_class_name.blank?
|
16
|
+
raise ArgumentError, 'job_class_name is required if arguments given'
|
17
|
+
end
|
18
|
+
|
19
|
+
@job_class_name = job_class_name
|
20
|
+
@arguments = arguments || []
|
21
|
+
end
|
22
|
+
|
23
|
+
def lock_key
|
24
|
+
[
|
25
|
+
lock_prefix,
|
26
|
+
normalized_job_class_name,
|
27
|
+
arguments_key_part
|
28
|
+
].join(':')
|
29
|
+
end
|
30
|
+
|
31
|
+
def wildcard_key
|
32
|
+
[
|
33
|
+
lock_prefix,
|
34
|
+
normalized_job_class_name,
|
35
|
+
arguments.any? ? arguments_key_part + '*' : '*'
|
36
|
+
].compact.join(':')
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def arguments_key_part
|
42
|
+
arguments.any? ? arguments_digest : FALLBACK_ARGUMENTS_STRING
|
43
|
+
end
|
44
|
+
|
45
|
+
# ActiveJob::Arguments is used to reflect the way ActiveJob serializes arguments in order to
|
46
|
+
# serialize ActiveRecord models with GlobalID uuids instead of as_json which could give undesired artifacts
|
47
|
+
def serialized_arguments
|
48
|
+
ActiveSupport::JSON.encode(ActiveJob::Arguments.serialize(arguments))
|
49
|
+
end
|
50
|
+
|
51
|
+
def arguments_digest
|
52
|
+
digest_method.hexdigest(serialized_arguments)
|
53
|
+
end
|
54
|
+
|
55
|
+
def normalized_job_class_name
|
56
|
+
job_class_name&.underscore
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
# Redlock requires a value of the lock to release the resource by Redlock::Client#unlock method.
|
6
|
+
# LockManager introduces LockManager#delete_lock to unlock by resource key only.
|
7
|
+
# See https://github.com/leandromoreira/redlock-rb/issues/51 for more details.
|
8
|
+
class LockManager < ::Redlock::Client
|
9
|
+
# Unlocks a resource by resource only.
|
10
|
+
def delete_lock(resource)
|
11
|
+
@servers.each do |server|
|
12
|
+
server.instance_variable_get(:'@redis').with do |conn|
|
13
|
+
conn.del resource
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
# Unlocks multiple resources by key wildcard.
|
21
|
+
def delete_locks(wildcard)
|
22
|
+
@servers.each do |server|
|
23
|
+
server.instance_variable_get(:'@redis').with do |conn|
|
24
|
+
conn.scan_each(match: wildcard).each { |key| conn.del key }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/log_subscriber'
|
4
|
+
|
5
|
+
module ActiveJob
|
6
|
+
class LogSubscriber < ActiveSupport::LogSubscriber #:nodoc:
|
7
|
+
def lock(event)
|
8
|
+
job = event.payload[:job]
|
9
|
+
resource = event.payload[:resource]
|
10
|
+
|
11
|
+
debug do
|
12
|
+
"Locked #{lock_info(job, resource)}" + args_info(job)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def runtime_lock(event)
|
17
|
+
job = event.payload[:job]
|
18
|
+
resource = event.payload[:resource]
|
19
|
+
|
20
|
+
debug do
|
21
|
+
"Locked runtime #{lock_info(job, resource)}" + args_info(job)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def unlock(event)
|
26
|
+
job = event.payload[:job]
|
27
|
+
resource = event.payload[:resource]
|
28
|
+
|
29
|
+
debug do
|
30
|
+
"Unlocked #{lock_info(job, resource)}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def runtime_unlock(event)
|
35
|
+
job = event.payload[:job]
|
36
|
+
resource = event.payload[:resource]
|
37
|
+
|
38
|
+
debug do
|
39
|
+
"Unlocked runtime #{lock_info(job, resource)}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def conflict(event)
|
44
|
+
job = event.payload[:job]
|
45
|
+
resource = event.payload[:resource]
|
46
|
+
|
47
|
+
info do
|
48
|
+
"Not unique #{lock_info(job, resource)}" + args_info(job)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def runtime_conflict(event)
|
53
|
+
job = event.payload[:job]
|
54
|
+
resource = event.payload[:resource]
|
55
|
+
|
56
|
+
info do
|
57
|
+
"Not unique runtime #{lock_info(job, resource)}" + args_info(job)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def lock_info(job, resource)
|
64
|
+
"#{job.class.name} (Job ID: #{job.job_id}) (Lock key: #{resource})"
|
65
|
+
end
|
66
|
+
|
67
|
+
def args_info(job)
|
68
|
+
if job.arguments.any? && log_arguments?(job)
|
69
|
+
' with arguments: ' +
|
70
|
+
job.arguments.map { |arg| format(arg).inspect }.join(', ')
|
71
|
+
else
|
72
|
+
''
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def log_arguments?(job)
|
77
|
+
return true unless job.class.respond_to?(:log_arguments?)
|
78
|
+
|
79
|
+
job.class.log_arguments?
|
80
|
+
end
|
81
|
+
|
82
|
+
def format(arg)
|
83
|
+
case arg
|
84
|
+
when Hash
|
85
|
+
arg.transform_values { |value| format(value) }
|
86
|
+
when Array
|
87
|
+
arg.map { |value| format(value) }
|
88
|
+
when GlobalID::Identification
|
89
|
+
arg.to_global_id rescue arg
|
90
|
+
else
|
91
|
+
arg
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def logger
|
96
|
+
ActiveJob::Base.logger
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
ActiveJob::LogSubscriber.attach_to :active_job_uniqueness
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
# Provides ability to make ActiveJob job unique.
|
6
|
+
#
|
7
|
+
# For example:
|
8
|
+
#
|
9
|
+
# class FooJob < ActiveJob::Base
|
10
|
+
# queue_as :foo
|
11
|
+
#
|
12
|
+
# unique :until_executed, lock_ttl: 3.hours
|
13
|
+
#
|
14
|
+
# def perform(params)
|
15
|
+
# #...
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
module Patch
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
|
22
|
+
class_methods do
|
23
|
+
# Enables the uniqueness strategy for the job
|
24
|
+
# Params:
|
25
|
+
# +strategy+:: the uniqueness strategy.
|
26
|
+
# +options+:: uniqueness strategy options. For example: lock_ttl.
|
27
|
+
def unique(strategy, options = {})
|
28
|
+
validate_on_conflict_action!(options[:on_conflict])
|
29
|
+
validate_on_conflict_action!(options[:on_runtime_conflict])
|
30
|
+
|
31
|
+
self.lock_strategy_class = ActiveJob::Uniqueness::Strategies.lookup(strategy)
|
32
|
+
self.lock_options = options
|
33
|
+
end
|
34
|
+
|
35
|
+
# Unlocks all jobs of the job class if no arguments given
|
36
|
+
# Unlocks particular job if job arguments given
|
37
|
+
def unlock!(*arguments)
|
38
|
+
ActiveJob::Uniqueness.unlock!(job_class_name: name, arguments: arguments)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
delegate :validate_on_conflict_action!, to: :'ActiveJob::Uniqueness.config'
|
44
|
+
end
|
45
|
+
|
46
|
+
included do
|
47
|
+
class_attribute :lock_strategy_class, instance_writer: false
|
48
|
+
class_attribute :lock_options, instance_writer: false
|
49
|
+
|
50
|
+
before_enqueue { |job| job.lock_strategy.before_enqueue if job.lock_strategy_class }
|
51
|
+
before_perform { |job| job.lock_strategy.before_perform if job.lock_strategy_class }
|
52
|
+
after_perform { |job| job.lock_strategy.after_perform if job.lock_strategy_class }
|
53
|
+
around_enqueue { |job, block| job.lock_strategy_class ? job.lock_strategy.around_enqueue(block) : block.call }
|
54
|
+
around_perform { |job, block| job.lock_strategy_class ? job.lock_strategy.around_perform(block) : block.call }
|
55
|
+
end
|
56
|
+
|
57
|
+
def lock_strategy
|
58
|
+
@lock_strategy ||= lock_strategy_class.new(lock_options.merge(lock_key: lock_key, job: self))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Override in your job class if you want to customize arguments set for a digest.
|
62
|
+
def lock_key_arguments
|
63
|
+
arguments
|
64
|
+
end
|
65
|
+
|
66
|
+
# Override lock_key method in your job class if you want to build completely custom lock key.
|
67
|
+
delegate :lock_key, to: :lock_key_generator
|
68
|
+
|
69
|
+
def lock_key_generator
|
70
|
+
ActiveJob::Uniqueness::LockKey.new job_class_name: self.class.name,
|
71
|
+
arguments: lock_key_arguments
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if defined?(Rails)
|
76
|
+
class Railtie < Rails::Railtie
|
77
|
+
initializer 'active_job_uniqueness.patch_active_job' do
|
78
|
+
ActiveSupport.on_load(:active_job) do
|
79
|
+
ActiveJob::Base.include ActiveJob::Uniqueness::Patch
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
else
|
84
|
+
ActiveSupport.on_load(:active_job) do
|
85
|
+
ActiveJob::Base.include ActiveJob::Uniqueness::Patch
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
# See Configuration#lock_strategies if you want to define custom strategy
|
6
|
+
module Strategies
|
7
|
+
extend ActiveSupport::Autoload
|
8
|
+
|
9
|
+
autoload :Base
|
10
|
+
autoload :UntilExpired
|
11
|
+
autoload :UntilExecuted
|
12
|
+
autoload :UntilExecuting
|
13
|
+
autoload :UntilAndWhileExecuting
|
14
|
+
autoload :WhileExecuting
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def lookup(strategy)
|
18
|
+
matching_strategy(strategy.to_s.camelize) ||
|
19
|
+
ActiveJob::Uniqueness.config.lock_strategies[strategy] ||
|
20
|
+
raise(StrategyNotFound, "Strategy '#{strategy}' is not found. Is it declared in the configuration?")
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def matching_strategy(const)
|
26
|
+
const_get(const, false) if const_defined?(const, false)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
module Strategies
|
6
|
+
# Base strategy is not supposed to actually be used as uniqueness strategy.
|
7
|
+
class Base
|
8
|
+
# https://github.com/rails/rails/pull/17227
|
9
|
+
# https://groups.google.com/g/rubyonrails-core/c/mhD4T90g0G4
|
10
|
+
ACTIVEJOB_SUPPORTS_THROW_ABORT = ActiveJob.gem_version >= Gem::Version.new('5.0')
|
11
|
+
|
12
|
+
delegate :lock_manager, :config, to: :'ActiveJob::Uniqueness'
|
13
|
+
|
14
|
+
attr_reader :lock_key, :lock_ttl, :on_conflict, :job
|
15
|
+
|
16
|
+
def initialize(lock_key:, lock_ttl: nil, on_conflict: nil, job: nil)
|
17
|
+
@lock_key = lock_key
|
18
|
+
@lock_ttl = (lock_ttl || config.lock_ttl).to_i * 1000 # ms
|
19
|
+
@on_conflict = on_conflict || config.on_conflict
|
20
|
+
@job = job
|
21
|
+
end
|
22
|
+
|
23
|
+
def lock(resource:, ttl:, event: :lock)
|
24
|
+
lock_manager.lock(resource, ttl).tap do |result|
|
25
|
+
instrument(event, resource: resource, ttl: ttl) if result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def unlock(resource:, event: :unlock)
|
30
|
+
lock_manager.delete_lock(resource).tap do
|
31
|
+
instrument(event, resource: resource)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def before_enqueue
|
36
|
+
# Expected to be overriden in the descendant strategy
|
37
|
+
end
|
38
|
+
|
39
|
+
def before_perform
|
40
|
+
# Expected to be overriden in the descendant strategy
|
41
|
+
end
|
42
|
+
|
43
|
+
def around_enqueue(block)
|
44
|
+
# Expected to be overriden in the descendant strategy
|
45
|
+
block.call
|
46
|
+
end
|
47
|
+
|
48
|
+
def around_perform(block)
|
49
|
+
# Expected to be overriden in the descendant strategy
|
50
|
+
block.call
|
51
|
+
end
|
52
|
+
|
53
|
+
def after_perform; end
|
54
|
+
|
55
|
+
module LockingOnEnqueue
|
56
|
+
def before_enqueue
|
57
|
+
return if lock(resource: lock_key, ttl: lock_ttl)
|
58
|
+
|
59
|
+
handle_conflict(resource: lock_key, on_conflict: on_conflict)
|
60
|
+
abort_job
|
61
|
+
end
|
62
|
+
|
63
|
+
def around_enqueue(block)
|
64
|
+
return if @job_aborted # ActiveJob 4.2 workaround
|
65
|
+
|
66
|
+
enqueued = false
|
67
|
+
|
68
|
+
block.call
|
69
|
+
|
70
|
+
enqueued = true
|
71
|
+
ensure
|
72
|
+
unlock(resource: lock_key) unless @job_aborted || enqueued
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def handle_conflict(on_conflict:, resource:, event: :conflict)
|
79
|
+
case on_conflict
|
80
|
+
when :log then instrument(event, resource: resource)
|
81
|
+
when :raise then raise_not_unique_job_error(resource: resource, event: event)
|
82
|
+
else
|
83
|
+
on_conflict.call(job)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def abort_job
|
88
|
+
@job_aborted = true # ActiveJob 4.2 workaround
|
89
|
+
|
90
|
+
ACTIVEJOB_SUPPORTS_THROW_ABORT ? throw(:abort) : false
|
91
|
+
end
|
92
|
+
|
93
|
+
def instrument(action, payload = {})
|
94
|
+
ActiveSupport::Notifications.instrument "#{action}.active_job_uniqueness", payload.merge(job: job)
|
95
|
+
end
|
96
|
+
|
97
|
+
def raise_not_unique_job_error(resource:, event:)
|
98
|
+
message = [
|
99
|
+
job.class.name,
|
100
|
+
"(Job ID: #{job.job_id})",
|
101
|
+
"(Lock key: #{resource})",
|
102
|
+
job.arguments.inspect
|
103
|
+
]
|
104
|
+
|
105
|
+
message.unshift(event == :runtime_conflict ? 'Not unique runtime' : 'Not unique')
|
106
|
+
|
107
|
+
raise JobNotUnique, message.join(' ')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
module Strategies
|
6
|
+
# Locks the job when it is pushed to the queue.
|
7
|
+
# Unlocks the job before the job is started.
|
8
|
+
# Then creates runtime lock to prevent simultaneous jobs from being executed.
|
9
|
+
class UntilAndWhileExecuting < Base
|
10
|
+
include LockingOnEnqueue
|
11
|
+
|
12
|
+
attr_reader :runtime_lock_ttl, :on_runtime_conflict
|
13
|
+
|
14
|
+
def initialize(runtime_lock_ttl: nil, on_runtime_conflict: nil, **params)
|
15
|
+
super(params)
|
16
|
+
@runtime_lock_ttl = runtime_lock_ttl.present? ? runtime_lock_ttl.to_i * 1000 : lock_ttl
|
17
|
+
@on_runtime_conflict = on_runtime_conflict || on_conflict
|
18
|
+
end
|
19
|
+
|
20
|
+
def before_perform
|
21
|
+
unlock(resource: lock_key)
|
22
|
+
|
23
|
+
return if lock(resource: runtime_lock_key, ttl: runtime_lock_ttl, event: :runtime_lock)
|
24
|
+
|
25
|
+
handle_conflict(on_conflict: on_runtime_conflict, resource: runtime_lock_key, event: :runtime_conflict)
|
26
|
+
abort_job
|
27
|
+
end
|
28
|
+
|
29
|
+
def around_perform(block)
|
30
|
+
return if @job_aborted # ActiveJob 4.2 workaround
|
31
|
+
|
32
|
+
block.call
|
33
|
+
ensure
|
34
|
+
unlock(resource: runtime_lock_key, event: :runtime_unlock) unless @job_aborted
|
35
|
+
end
|
36
|
+
|
37
|
+
def runtime_lock_key
|
38
|
+
[lock_key, 'runtime'].join(':')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
module Strategies
|
6
|
+
# Locks the job when it is pushed to the queue.
|
7
|
+
# Unlocks the job when the job is finished.
|
8
|
+
class UntilExecuted < Base
|
9
|
+
include LockingOnEnqueue
|
10
|
+
|
11
|
+
def after_perform
|
12
|
+
unlock(resource: lock_key)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
module Strategies
|
6
|
+
# Locks the job when it is pushed to the queue.
|
7
|
+
# Unlocks the job before the job is started.
|
8
|
+
class UntilExecuting < Base
|
9
|
+
include LockingOnEnqueue
|
10
|
+
|
11
|
+
def before_perform
|
12
|
+
unlock(resource: lock_key)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
module Strategies
|
6
|
+
# Locks the job when it is pushed to the queue.
|
7
|
+
# Does not allow new jobs enqueued until lock is expired.
|
8
|
+
class UntilExpired < Base
|
9
|
+
include LockingOnEnqueue
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
module Strategies
|
6
|
+
# Locks the job when the job starts.
|
7
|
+
# Unlocks the job when the job is finished.
|
8
|
+
class WhileExecuting < Base
|
9
|
+
def before_perform
|
10
|
+
return if lock(resource: lock_key, ttl: lock_ttl, event: :runtime_lock)
|
11
|
+
|
12
|
+
handle_conflict(resource: lock_key, event: :runtime_conflict, on_conflict: on_conflict)
|
13
|
+
abort_job
|
14
|
+
end
|
15
|
+
|
16
|
+
def around_perform(block)
|
17
|
+
return if @job_aborted # ActiveJob 4.2 workaround
|
18
|
+
|
19
|
+
block.call
|
20
|
+
ensure
|
21
|
+
unlock(resource: lock_key, event: :runtime_unlock) unless @job_aborted
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
# Mocks ActiveJob::Uniqueness::LockManager methods.
|
6
|
+
# See ActiveJob::Uniqueness.test_mode!
|
7
|
+
class TestLockManager
|
8
|
+
def lock(*_args)
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
alias delete_lock lock
|
13
|
+
alias delete_locks lock
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Uniqueness
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
desc 'Copy ActiveJob::Uniqueness default files'
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
def copy_config
|
11
|
+
template 'config/initializers/active_job_uniqueness.rb'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/generators/active_job/uniqueness/templates/config/initializers/active_job_uniqueness.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActiveJob::Uniqueness.configure do |config|
|
4
|
+
# Global default expiration for lock keys. Each job can define its own ttl via :lock_ttl option.
|
5
|
+
# Stategy :until_and_while_executing also accept :on_runtime_ttl option.
|
6
|
+
#
|
7
|
+
# config.lock_ttl = 1.day
|
8
|
+
|
9
|
+
# Prefix for lock keys. Can not be set per job.
|
10
|
+
#
|
11
|
+
# config.lock_prefix = 'activejob_uniqueness'
|
12
|
+
|
13
|
+
# Default action on lock conflict. Can be set per job.
|
14
|
+
# Stategy :until_and_while_executing also accept :on_runtime_conflict option.
|
15
|
+
# Allowed values are
|
16
|
+
# :raise - raises ActiveJob::Uniqueness::JobNotUnique
|
17
|
+
# :log - instruments ActiveSupport::Notifications and logs event to the ActiveJob::Logger
|
18
|
+
# proc - custom Proc. For example, ->(job) { job.logger.info('Oops') }
|
19
|
+
#
|
20
|
+
# config.on_conflict = :raise
|
21
|
+
|
22
|
+
# Digest method for lock keys generating. Expected to have `hexdigest` class method.
|
23
|
+
#
|
24
|
+
# config.digest_method = OpenSSL::Digest::MD5
|
25
|
+
|
26
|
+
# Array of redis servers for Redlock quorum.
|
27
|
+
# Read more at https://github.com/leandromoreira/redlock-rb#redis-client-configuration
|
28
|
+
#
|
29
|
+
# config.redlock_servers = [ENV.fetch('REDIS_URL', 'redis://localhost:6379')]
|
30
|
+
|
31
|
+
# Custom options for Redlock.
|
32
|
+
# Read more at https://github.com/leandromoreira/redlock-rb#redlock-configuration
|
33
|
+
#
|
34
|
+
# config.redlock_options = {}
|
35
|
+
|
36
|
+
# Custom strategies.
|
37
|
+
# config.lock_strategies = { my_strategy: MyStrategy }
|
38
|
+
#
|
39
|
+
# config.lock_strategies = {}
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activejob-uniqueness
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rustam Sharshenov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activejob
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redlock
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.2'
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '2'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1.2'
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: bundler
|
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: pry-byebug
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rake
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rspec
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '3.0'
|
103
|
+
description: Ensure uniqueness of your ActiveJob jobs
|
104
|
+
email:
|
105
|
+
- rustam@sharshenov.com
|
106
|
+
executables: []
|
107
|
+
extensions: []
|
108
|
+
extra_rdoc_files: []
|
109
|
+
files:
|
110
|
+
- ".gitignore"
|
111
|
+
- ".rspec"
|
112
|
+
- ".rubocop.yml"
|
113
|
+
- ".travis.yml"
|
114
|
+
- CHANGELOG.md
|
115
|
+
- Gemfile
|
116
|
+
- LICENSE.txt
|
117
|
+
- README.md
|
118
|
+
- Rakefile
|
119
|
+
- activejob-uniqueness.gemspec
|
120
|
+
- lib/active_job/uniqueness.rb
|
121
|
+
- lib/active_job/uniqueness/configuration.rb
|
122
|
+
- lib/active_job/uniqueness/errors.rb
|
123
|
+
- lib/active_job/uniqueness/lock_key.rb
|
124
|
+
- lib/active_job/uniqueness/lock_manager.rb
|
125
|
+
- lib/active_job/uniqueness/log_subscriber.rb
|
126
|
+
- lib/active_job/uniqueness/patch.rb
|
127
|
+
- lib/active_job/uniqueness/strategies.rb
|
128
|
+
- lib/active_job/uniqueness/strategies/base.rb
|
129
|
+
- lib/active_job/uniqueness/strategies/until_and_while_executing.rb
|
130
|
+
- lib/active_job/uniqueness/strategies/until_executed.rb
|
131
|
+
- lib/active_job/uniqueness/strategies/until_executing.rb
|
132
|
+
- lib/active_job/uniqueness/strategies/until_expired.rb
|
133
|
+
- lib/active_job/uniqueness/strategies/while_executing.rb
|
134
|
+
- lib/active_job/uniqueness/test_lock_manager.rb
|
135
|
+
- lib/active_job/uniqueness/version.rb
|
136
|
+
- lib/activejob/uniqueness.rb
|
137
|
+
- lib/generators/active_job/uniqueness/install_generator.rb
|
138
|
+
- lib/generators/active_job/uniqueness/templates/config/initializers/active_job_uniqueness.rb
|
139
|
+
homepage: https://github.com/veeqo/activejob-uniqueness
|
140
|
+
licenses:
|
141
|
+
- MIT
|
142
|
+
metadata:
|
143
|
+
homepage_uri: https://github.com/veeqo/activejob-uniqueness
|
144
|
+
source_code_uri: https://github.com/veeqo/activejob-uniqueness
|
145
|
+
changelog_uri: https://github.com/veeqo/activejob-uniqueness/blob/master/CHANGELOG.md
|
146
|
+
post_install_message:
|
147
|
+
rdoc_options: []
|
148
|
+
require_paths:
|
149
|
+
- lib
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
requirements: []
|
161
|
+
rubygems_version: 3.0.6
|
162
|
+
signing_key:
|
163
|
+
specification_version: 4
|
164
|
+
summary: Ensure uniqueness of your ActiveJob jobs
|
165
|
+
test_files: []
|