activejob-unique 0.5.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/CHANGELOG.md +128 -0
- data/LICENSE.txt +21 -0
- data/README.md +266 -0
- data/lib/active_job/uniqueness/active_job_patch.rb +82 -0
- data/lib/active_job/uniqueness/configuration.rb +60 -0
- data/lib/active_job/uniqueness/errors.rb +35 -0
- data/lib/active_job/uniqueness/lock_key.rb +68 -0
- data/lib/active_job/uniqueness/lock_manager.rb +48 -0
- data/lib/active_job/uniqueness/log_subscriber.rb +102 -0
- data/lib/active_job/uniqueness/sidekiq_patch.rb +103 -0
- data/lib/active_job/uniqueness/strategies/base.rb +129 -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/strategies.rb +31 -0
- data/lib/active_job/uniqueness/test_lock_manager.rb +16 -0
- data/lib/active_job/uniqueness/version.rb +7 -0
- data/lib/active_job/uniqueness.rb +47 -0
- data/lib/activejob/uniqueness.rb +3 -0
- data/lib/activejob-unique.rb +4 -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 +47 -0
- metadata +180 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 061e508b94c278bf8ccd34aa73f19461625922aec84783d6337968342ef7a8db
|
|
4
|
+
data.tar.gz: 135d045c623b181d2bb86bd2234b72f25c84f7c4549829e4d40d0908fd4dd838
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 14b1956520105caeef38dcb7e5c4b5f51dbc197d575f3f839d6d9d5a7ca2502cda85ebeacf56b7dff206589566bc1680fa20707d7437a74c95509706e4ce253b
|
|
7
|
+
data.tar.gz: a821c7491fd5f0768863d0ed475ea72f81ee318ce19ac5999765c6bde3f156bda1e78baeda5390e0fb0dc73d14f9453b20bc10365f9a5c0352f4452a2dcf9b1e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
All notable changes to this project will be documented in this file.
|
|
3
|
+
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased](https://github.com/nordinvestments/activejob-uniqueness/compare/v0.5.0...HEAD)
|
|
7
|
+
|
|
8
|
+
## [0.5.0](https://github.com/nordinvestments/activejob-uniqueness/compare/v0.4.0...v0.5.0) - 2026-01-12
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Fork published as `activejob-unique` gem by [@nordinvestments](https://github.com/nordinvestments)
|
|
12
|
+
- Updated gem metadata to point to fork repository
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
*Versions below are from the original [veeqo/activejob-uniqueness](https://github.com/veeqo/activejob-uniqueness) repository.*
|
|
17
|
+
|
|
18
|
+
## [0.4.0](https://github.com/veeqo/activejob-uniqueness/compare/v0.3.2...v0.4.0) - 2024-12-07
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- [#86](https://github.com/veeqo/activejob-uniqueness/pull/86) Add Rails 8.0 rc1 support by[@sharshenov](https://github.com/sharshenov)
|
|
23
|
+
- [#78](https://github.com/veeqo/activejob-uniqueness/pull/78) Add on_redis_connection_error config to adjust to new redlock behaviour by[@nduitz](https://github.com/nduitz)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- [#82](https://github.com/veeqo/activejob-uniqueness/pull/82) Optimize bulk unlocking [@sharshenov](https://github.com/sharshenov)
|
|
27
|
+
|
|
28
|
+
## [0.3.2](https://github.com/veeqo/activejob-uniqueness/compare/v0.3.1...v0.3.2) - 2024-08-16
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- [#80](https://github.com/veeqo/activejob-uniqueness/pull/80) Add rails 7.2 support by [@viralpraxis](https://github.com/viralpraxis)
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- [#74](https://github.com/veeqo/activejob-uniqueness/pull/74) Fix log subscriber by [@shahidkhaliq](https://github.com/shahidkhaliq)
|
|
35
|
+
|
|
36
|
+
## [0.3.1](https://github.com/veeqo/activejob-uniqueness/compare/v0.3.0...v0.3.1) - 2023-10-30
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- [#67](https://github.com/veeqo/activejob-uniqueness/pull/67) Random redis errors on delete_lock by [@laurafeier](https://github.com/laurafeier)
|
|
41
|
+
|
|
42
|
+
## [0.3.0](https://github.com/veeqo/activejob-uniqueness/compare/v0.2.5...v0.3.0) - 2023-10-20
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- [#66](https://github.com/veeqo/activejob-uniqueness/pull/66) Activejob 7.1 support by [@laurafeier](https://github.com/laurafeier)
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- [#57](https://github.com/veeqo/activejob-uniqueness/pull/57) Upgrade to Redlock 2 & use redis-client by [@bmulholland](https://github.com/bmulholland)
|
|
49
|
+
|
|
50
|
+
### Removed
|
|
51
|
+
- Support fo Redlock v1 is removed. Switching to `RedisClient` is [a breaking change of Redlock v2](https://github.com/leandromoreira/redlock-rb/blob/main/CHANGELOG.md#200---2023-02-09).
|
|
52
|
+
|
|
53
|
+
## [0.2.5](https://github.com/veeqo/activejob-uniqueness/compare/v0.2.4...v0.2.5) - 2023-02-01
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- [#45](https://github.com/veeqo/activejob-uniqueness/pull/45) Add Dependabot for GitHub Actions by [@petergoldstein](https://github.com/petergoldstein)
|
|
57
|
+
- [#51](https://github.com/veeqo/activejob-uniqueness/pull/51) Add support for Sidekiq 7 by [@dwightwatson](https://github.com/dwightwatson)
|
|
58
|
+
- [#52](https://github.com/veeqo/activejob-uniqueness/pull/52) Add Ruby 3.2.0 to the CI matrix by [@petergoldstein](https://github.com/petergoldstein)
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
- [#46](https://github.com/veeqo/activejob-uniqueness/pull/46) Fix a method name typo in CHANGELOG by [@y-yagi](https://github.com/y-yagi)
|
|
62
|
+
|
|
63
|
+
## [0.2.4](https://github.com/veeqo/activejob-uniqueness/compare/v0.2.3...v0.2.4) - 2022-06-22
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
- [#43](https://github.com/veeqo/activejob-uniqueness/pull/43) Run rubocop on Github Actions
|
|
67
|
+
- [#44](https://github.com/veeqo/activejob-uniqueness/pull/44) Add ActiveJob::Uniqueness.reset_manager! method to reset lock manager by [@akostadinov](https://github.com/akostadinov)
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
- [#42](https://github.com/veeqo/activejob-uniqueness/pull/42) Actualize rubies and gems for tests
|
|
71
|
+
|
|
72
|
+
## [0.2.3](https://github.com/veeqo/activejob-uniqueness/compare/v0.2.2...v0.2.3) - 2022-02-28
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
- [#36](https://github.com/veeqo/activejob-uniqueness/pull/36) Support ActiveJob/Rails 7.0
|
|
76
|
+
- [#37](https://github.com/veeqo/activejob-uniqueness/pull/37) Add Ruby 3.1 to CI by [@petergoldstein](https://github.com/petergoldstein)
|
|
77
|
+
|
|
78
|
+
## [0.2.2](https://github.com/veeqo/activejob-uniqueness/compare/v0.2.1...v0.2.2) - 2021-10-22
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
- [#32](https://github.com/veeqo/activejob-uniqueness/pull/32) Add ability to set a custom runtime lock key for `:until_and_while_executing` strategy
|
|
82
|
+
|
|
83
|
+
## [0.2.1](https://github.com/veeqo/activejob-uniqueness/compare/v0.2.0...v0.2.1) - 2021-08-24
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
- [#30](https://github.com/veeqo/activejob-uniqueness/pull/30) Add Sidekiq::JobRecord support (reported by [@dwightwatson](https://github.com/dwightwatson))
|
|
87
|
+
|
|
88
|
+
## [0.2.0](https://github.com/veeqo/activejob-uniqueness/compare/v0.1.4...v0.2.0) - 2021-05-09
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
- [#22](https://github.com/veeqo/activejob-uniqueness/pull/22) Test with ruby 3.0.1
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
- [#20](https://github.com/veeqo/activejob-uniqueness/pull/20) **Breaking** Sidekiq patch is not applied automatically anymore
|
|
95
|
+
- [#21](https://github.com/veeqo/activejob-uniqueness/pull/21) Migrate from Travis to Github Actions
|
|
96
|
+
- [#24](https://github.com/veeqo/activejob-uniqueness/pull/24) The default value for `retry_count` of redlock is now 0
|
|
97
|
+
- Require ruby 2.5+
|
|
98
|
+
|
|
99
|
+
## [0.1.4](https://github.com/veeqo/activejob-uniqueness/compare/v0.1.3...v0.1.4) - 2020-09-22
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
- [#11](https://github.com/veeqo/activejob-uniqueness/pull/11) Fix deprecation warnings for ruby 2.7 by [@DanAndreasson](https://github.com/DanAndreasson)
|
|
103
|
+
- [#13](https://github.com/veeqo/activejob-uniqueness/pull/13) Fix deprecation warnings for ruby 2.7
|
|
104
|
+
|
|
105
|
+
## [0.1.3](https://github.com/veeqo/activejob-uniqueness/compare/v0.1.2...v0.1.3) - 2020-08-17
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- [#7](https://github.com/veeqo/activejob-uniqueness/pull/7) Fix deprecation warnings for ruby 2.7 by [@tonobo](https://github.com/tonobo)
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
- [#8](https://github.com/veeqo/activejob-uniqueness/pull/8) Use appraisal gem to control gem versions of tests matrix
|
|
112
|
+
- [#9](https://github.com/veeqo/activejob-uniqueness/pull/9) Refactor of Sidekiq API patch. Fixes [#6](https://github.com/veeqo/activejob-uniqueness/issues/6) Rails boot error for version 0.1.2
|
|
113
|
+
- [#10](https://github.com/veeqo/activejob-uniqueness/pull/10) Refactor changelog to comply with Keep a Changelog
|
|
114
|
+
|
|
115
|
+
## [0.1.2](https://github.com/veeqo/activejob-uniqueness/compare/v0.1.1...v0.1.2) - 2020-07-30
|
|
116
|
+
|
|
117
|
+
### Added
|
|
118
|
+
- [#5](https://github.com/veeqo/activejob-uniqueness/pull/5) Release lock for Sidekiq adapter by [@vbyno](https://github.com/vbyno)
|
|
119
|
+
|
|
120
|
+
## [0.1.1](https://github.com/veeqo/activejob-uniqueness/compare/v0.1.0...v0.1.1) - 2020-07-23
|
|
121
|
+
|
|
122
|
+
### Fixed
|
|
123
|
+
- [#4](https://github.com/veeqo/activejob-uniqueness/pull/4) Fix `NoMethodError` on `Rails.application.eager_load!` in Rails initializer
|
|
124
|
+
|
|
125
|
+
## [0.1.0](https://github.com/veeqo/activejob-uniqueness/releases/tag/v0.1.0) - 2020-07-05
|
|
126
|
+
|
|
127
|
+
### Added
|
|
128
|
+
- Job uniqueness for ActiveJob
|
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,266 @@
|
|
|
1
|
+
# Job uniqueness for ActiveJob
|
|
2
|
+
|
|
3
|
+
> **Maintained fork of [veeqo/activejob-uniqueness](https://github.com/veeqo/activejob-uniqueness)**
|
|
4
|
+
>
|
|
5
|
+
> This gem is published as `activejob-unique` on RubyGems. The API is fully compatible with the original gem — just update your Gemfile.
|
|
6
|
+
|
|
7
|
+
[](https://github.com/nordinvestments/activejob-uniqueness/actions/workflows/main.yml) [](https://badge.fury.io/rb/activejob-unique)
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
| Dependency | Version |
|
|
12
|
+
|------------|---------|
|
|
13
|
+
| Ruby | >= 3.1 |
|
|
14
|
+
| Rails (ActiveJob) | >= 7.1, < 7.3 |
|
|
15
|
+
| Redis | >= 2.8 |
|
|
16
|
+
|
|
17
|
+
> **Note:** This fork targets modern Ruby and Rails versions. For older Ruby (2.5-3.0) or Rails (4.2-7.0) support, use the [original gem](https://github.com/veeqo/activejob-uniqueness).
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
The gem allows to protect job uniqueness with next strategies:
|
|
22
|
+
|
|
23
|
+
| Strategy | The job is locked | The job is unlocked |
|
|
24
|
+
|-|-|-|
|
|
25
|
+
| `until_executing` | when **pushed** to the queue | when **processing starts** |
|
|
26
|
+
| `until_executed` | when **pushed** to the queue | when the job is **processed successfully** |
|
|
27
|
+
| `until_expired` | when **pushed** to the queue | when the lock is **expired** |
|
|
28
|
+
| `until_and_while_executing` | when **pushed** to the queue | when **processing starts**<br>a runtime lock is acquired to **prevent simultaneous jobs**<br>*has extra options: `runtime_lock_ttl`, `on_runtime_conflict`* |
|
|
29
|
+
| `while_executing` | when **processing starts** | when the job is **processed**<br>with any result including an error |
|
|
30
|
+
|
|
31
|
+
Inspired by [SidekiqUniqueJobs](https://github.com/mhenrixon/sidekiq-unique-jobs), uses [Redlock](https://github.com/leandromoreira/redlock-rb) under the hood.
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<a href="https://www.veeqo.com/" title="Sponsored by Veeqo">
|
|
35
|
+
<img src="https://static.veeqo.com/assets/sponsored_by_veeqo.png" width="360" />
|
|
36
|
+
</a>
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Add the `activejob-unique` gem to your Gemfile.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
gem 'activejob-unique'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If you want jobs unlocking for Sidekiq Web UI, require the patch explicitly. [**Queues cleanup becomes slower!**](#sidekiq-api-support)
|
|
48
|
+
```ruby
|
|
49
|
+
gem 'activejob-unique', require: 'active_job/uniqueness/sidekiq_patch'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
And run `bundle install` command.
|
|
53
|
+
|
|
54
|
+
### Migrating from `activejob-uniqueness`
|
|
55
|
+
|
|
56
|
+
If you're switching from the original gem, simply replace in your Gemfile:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# Before
|
|
60
|
+
gem 'activejob-uniqueness'
|
|
61
|
+
|
|
62
|
+
# After
|
|
63
|
+
gem 'activejob-unique'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
No code changes required — the `ActiveJob::Uniqueness` module namespace is unchanged.
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
ActiveJob::Uniqueness is ready to work without any configuration. It will use `REDIS_URL` to connect to Redis instance.
|
|
71
|
+
To override the defaults, create an initializer `config/initializers/active_job_uniqueness.rb` using the following command:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
rails generate active_job:uniqueness:install
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This gem relies on `redlock` for it's Redis connection, that means **it will not inherit global configuration of `Sidekiq`**. To configure the connection, you can use `config.redlock_servers`, for example to disable SSL verification for Redis/Key-Value cloud providers:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
ActiveJob::Uniqueness.configure do |config|
|
|
81
|
+
config.redlock_servers = [
|
|
82
|
+
RedisClient.new(
|
|
83
|
+
url: ENV['REDIS_URL'],
|
|
84
|
+
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
|
85
|
+
)
|
|
86
|
+
]
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Usage
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
### Make the job to be unique
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
class MyJob < ActiveJob::Base
|
|
97
|
+
# new jobs with the same args will raise error until existing one is executed
|
|
98
|
+
unique :until_executed
|
|
99
|
+
|
|
100
|
+
def perform(args)
|
|
101
|
+
# work
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Tune uniqueness settings per job
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class MyJob < ActiveJob::Base
|
|
110
|
+
# new jobs with the same args will be logged within 3 hours or until existing one is being executing
|
|
111
|
+
unique :until_executing, lock_ttl: 3.hours, on_conflict: :log
|
|
112
|
+
|
|
113
|
+
def perform(args)
|
|
114
|
+
# work
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
You can set defaults globally with [the configuration](#configuration)
|
|
120
|
+
|
|
121
|
+
### Control lock conflicts
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
class MyJob < ActiveJob::Base
|
|
125
|
+
# Proc gets the job instance including its arguments
|
|
126
|
+
unique :until_executing, on_conflict: ->(job) { job.logger.info "Oops: #{job.arguments}" }
|
|
127
|
+
|
|
128
|
+
def perform(args)
|
|
129
|
+
# work
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Control redis connection errors
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
class MyJob < ActiveJob::Base
|
|
138
|
+
# Proc gets the job instance including its arguments, and as keyword arguments the resource(lock key) `resource` and the original error `error`
|
|
139
|
+
unique :until_executing, on_redis_connection_error: ->(job, resource: _, error: _) { job.logger.info "Oops: #{job.arguments}" }
|
|
140
|
+
|
|
141
|
+
def perform(args)
|
|
142
|
+
# work
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Control lock key arguments
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
class MyJob < ActiveJob::Base
|
|
151
|
+
unique :until_executed
|
|
152
|
+
|
|
153
|
+
def perform(foo, bar, baz)
|
|
154
|
+
# work
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def lock_key_arguments
|
|
158
|
+
arguments.first(2) # baz is ignored
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Control the lock key
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
class MyJob < ActiveJob::Base
|
|
167
|
+
unique :until_executed
|
|
168
|
+
|
|
169
|
+
def perform(foo, bar, baz)
|
|
170
|
+
# work
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def lock_key
|
|
174
|
+
'qux' # completely custom lock key
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def runtime_lock_key
|
|
178
|
+
'quux' # completely custom runtime lock key for :until_and_while_executing
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Unlock jobs manually
|
|
184
|
+
|
|
185
|
+
The selected strategy automatically unlocks jobs, but in some cases (e.g. the queue is purged) it is handy to unlock jobs manually.
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# Remove the lock for particular arguments:
|
|
189
|
+
MyJob.unlock!(foo: 'bar')
|
|
190
|
+
# or
|
|
191
|
+
ActiveJob::Uniqueness.unlock!(job_class_name: 'MyJob', arguments: [{foo: 'bar'}])
|
|
192
|
+
|
|
193
|
+
# Remove all locks of MyJob
|
|
194
|
+
MyJob.unlock!
|
|
195
|
+
# or
|
|
196
|
+
ActiveJob::Uniqueness.unlock!(job_class_name: 'MyJob')
|
|
197
|
+
|
|
198
|
+
# Remove all locks
|
|
199
|
+
ActiveJob::Uniqueness.unlock!
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Test mode
|
|
203
|
+
|
|
204
|
+
Most probably you don't want jobs to be locked in tests. Add this line to your test suite (`rails_helper.rb`):
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
ActiveJob::Uniqueness.test_mode!
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Logging
|
|
211
|
+
|
|
212
|
+
ActiveJob::Uniqueness instruments `ActiveSupport::Notifications` with next events:
|
|
213
|
+
* `lock.active_job_uniqueness`
|
|
214
|
+
* `runtime_lock.active_job_uniqueness`
|
|
215
|
+
* `unlock.active_job_uniqueness`
|
|
216
|
+
* `runtime_unlock.active_job_uniqueness`
|
|
217
|
+
* `conflict.active_job_uniqueness`
|
|
218
|
+
* `runtime_conflict.active_job_uniqueness`
|
|
219
|
+
|
|
220
|
+
And then writes to `ActiveJob::Base.logger`.
|
|
221
|
+
|
|
222
|
+
**ActiveJob prior to version `6.1` will always log `Enqueued MyJob (Job ID) ...` even if the callback chain is halted. [Details](https://github.com/rails/rails/pull/37830)**
|
|
223
|
+
|
|
224
|
+
## Testing
|
|
225
|
+
|
|
226
|
+
Run redis server (in separate console):
|
|
227
|
+
```
|
|
228
|
+
docker run --rm -p 6379:6379 redis
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Run tests with:
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
bundle
|
|
235
|
+
rake
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Sidekiq API support
|
|
239
|
+
|
|
240
|
+
ActiveJob::Uniqueness supports Sidekiq API to unset job locks on queues cleanup (e.g. via Sidekiq Web UI). Starting Sidekiq 5.1 job death also triggers locks cleanup.
|
|
241
|
+
Take into account that **[big queues cleanup becomes much slower](https://github.com/nordinvestments/activejob-uniqueness/issues/16)** because each job is being unlocked individually. In order to activate Sidekiq API patch require it explicitly in your Gemfile:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
gem 'activejob-unique', require: 'active_job/uniqueness/sidekiq_patch'
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Why this fork?
|
|
248
|
+
|
|
249
|
+
The [original gem](https://github.com/veeqo/activejob-uniqueness) by Veeqo is excellent, but maintenance has slowed. This fork aims to:
|
|
250
|
+
|
|
251
|
+
- Keep up with new Rails and Ruby versions
|
|
252
|
+
- Merge useful PRs from the community
|
|
253
|
+
- Provide timely security updates
|
|
254
|
+
- Maintain backwards compatibility
|
|
255
|
+
|
|
256
|
+
We're grateful to the original authors and contributors. This fork preserves the MIT license and full attribution.
|
|
257
|
+
|
|
258
|
+
## Contributing
|
|
259
|
+
|
|
260
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/nordinvestments/activejob-uniqueness.
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
265
|
+
|
|
266
|
+
Originally created by [Veeqo](https://www.veeqo.com). Maintained by [Nord Investments](https://github.com/nordinvestments).
|
|
@@ -0,0 +1,82 @@
|
|
|
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 ActiveJobPatch
|
|
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
|
+
validate_on_redis_connection_error!(options[:on_redis_connection_error])
|
|
31
|
+
|
|
32
|
+
self.lock_strategy_class = ActiveJob::Uniqueness::Strategies.lookup(strategy)
|
|
33
|
+
self.lock_options = options
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Unlocks all jobs of the job class if no arguments given
|
|
37
|
+
# Unlocks particular job if job arguments given
|
|
38
|
+
def unlock!(*arguments)
|
|
39
|
+
ActiveJob::Uniqueness.unlock!(job_class_name: name, arguments: arguments)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
delegate :validate_on_conflict_action!,
|
|
45
|
+
:validate_on_redis_connection_error!,
|
|
46
|
+
to: :'ActiveJob::Uniqueness.config'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
included do
|
|
50
|
+
class_attribute :lock_strategy_class, instance_writer: false
|
|
51
|
+
class_attribute :lock_options, instance_writer: false
|
|
52
|
+
|
|
53
|
+
before_enqueue { |job| job.lock_strategy.before_enqueue if job.lock_strategy_class }
|
|
54
|
+
before_perform { |job| job.lock_strategy.before_perform if job.lock_strategy_class }
|
|
55
|
+
after_perform { |job| job.lock_strategy.after_perform if job.lock_strategy_class }
|
|
56
|
+
around_enqueue { |job, block| job.lock_strategy_class ? job.lock_strategy.around_enqueue(block) : block.call }
|
|
57
|
+
around_perform { |job, block| job.lock_strategy_class ? job.lock_strategy.around_perform(block) : block.call }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def lock_strategy
|
|
61
|
+
@lock_strategy ||= lock_strategy_class.new(job: self)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Override in your job class if you want to customize arguments set for a digest.
|
|
65
|
+
def lock_key_arguments
|
|
66
|
+
arguments
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Override lock_key method in your job class if you want to build completely custom lock key.
|
|
70
|
+
delegate :lock_key, :runtime_lock_key, to: :lock_key_generator
|
|
71
|
+
|
|
72
|
+
def lock_key_generator
|
|
73
|
+
@lock_key_generator ||= ActiveJob::Uniqueness::LockKey.new job_class_name: self.class.name,
|
|
74
|
+
arguments: lock_key_arguments
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
ActiveSupport.on_load(:active_job) do
|
|
79
|
+
ActiveJob::Base.include ActiveJob::Uniqueness::ActiveJobPatch
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
class_attribute :lock_ttl, default: 86_400 # 1.day
|
|
13
|
+
class_attribute :lock_prefix, default: 'activejob_uniqueness'
|
|
14
|
+
class_attribute :_on_conflict, default: :raise
|
|
15
|
+
class_attribute :_on_redis_connection_error, default: :raise
|
|
16
|
+
class_attribute :redlock_servers, default: [ENV.fetch('REDIS_URL', 'redis://localhost:6379')]
|
|
17
|
+
class_attribute :redlock_options, default: { retry_count: 0 }
|
|
18
|
+
class_attribute :lock_strategies, default: {}
|
|
19
|
+
class_attribute :digest_method
|
|
20
|
+
|
|
21
|
+
def on_conflict
|
|
22
|
+
_on_conflict
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_conflict=(action)
|
|
26
|
+
validate_on_conflict_action!(action)
|
|
27
|
+
|
|
28
|
+
self._on_conflict = action
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def on_redis_connection_error
|
|
32
|
+
_on_redis_connection_error
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def on_redis_connection_error=(action)
|
|
36
|
+
validate_on_redis_connection_error!(action)
|
|
37
|
+
|
|
38
|
+
self._on_redis_connection_error = action
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_on_conflict_action!(action)
|
|
42
|
+
return if action.nil? || %i[log raise].include?(action) || action.respond_to?(:call)
|
|
43
|
+
|
|
44
|
+
raise ActiveJob::Uniqueness::InvalidOnConflictAction, "Unexpected '#{action}' action on conflict"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_on_redis_connection_error!(action)
|
|
48
|
+
return if action.nil? || action == :raise || action.respond_to?(:call)
|
|
49
|
+
|
|
50
|
+
raise ActiveJob::Uniqueness::InvalidOnConflictAction, "Unexpected '#{action}' action on_redis_connection_error"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Set default digest_method after class is defined
|
|
57
|
+
ActiveJob::Uniqueness::Configuration.digest_method = begin
|
|
58
|
+
require 'openssl'
|
|
59
|
+
OpenSSL::Digest::MD5
|
|
60
|
+
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,68 @@
|
|
|
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
|
+
# used only by :until_and_while_executing strategy
|
|
32
|
+
def runtime_lock_key
|
|
33
|
+
[
|
|
34
|
+
lock_key,
|
|
35
|
+
'runtime'
|
|
36
|
+
].join(':')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def wildcard_key
|
|
40
|
+
[
|
|
41
|
+
lock_prefix,
|
|
42
|
+
normalized_job_class_name,
|
|
43
|
+
arguments.any? ? "#{arguments_key_part}*" : '*'
|
|
44
|
+
].compact.join(':')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def arguments_key_part
|
|
50
|
+
arguments.any? ? arguments_digest : FALLBACK_ARGUMENTS_STRING
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ActiveJob::Arguments is used to reflect the way ActiveJob serializes arguments in order to
|
|
54
|
+
# serialize ActiveRecord models with GlobalID uuids instead of as_json which could give undesired artifacts
|
|
55
|
+
def serialized_arguments
|
|
56
|
+
ActiveSupport::JSON.encode(ActiveJob::Arguments.serialize(arguments))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def arguments_digest
|
|
60
|
+
digest_method.hexdigest(serialized_arguments)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def normalized_job_class_name
|
|
64
|
+
job_class_name&.underscore
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|