sidekiq-unique-jobs 6.0.25 → 7.1.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq-unique-jobs might be problematic. Click here for more details.

Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +840 -41
  3. data/README.md +814 -284
  4. data/lib/sidekiq_unique_jobs/batch_delete.rb +123 -0
  5. data/lib/sidekiq_unique_jobs/changelog.rb +78 -0
  6. data/lib/sidekiq_unique_jobs/cli.rb +34 -31
  7. data/lib/sidekiq_unique_jobs/config.rb +275 -0
  8. data/lib/sidekiq_unique_jobs/connection.rb +6 -5
  9. data/lib/sidekiq_unique_jobs/constants.rb +45 -25
  10. data/lib/sidekiq_unique_jobs/core_ext.rb +80 -0
  11. data/lib/sidekiq_unique_jobs/deprecation.rb +35 -0
  12. data/lib/sidekiq_unique_jobs/digests.rb +71 -100
  13. data/lib/sidekiq_unique_jobs/exceptions.rb +87 -12
  14. data/lib/sidekiq_unique_jobs/job.rb +41 -12
  15. data/lib/sidekiq_unique_jobs/json.rb +40 -0
  16. data/lib/sidekiq_unique_jobs/key.rb +93 -0
  17. data/lib/sidekiq_unique_jobs/lock/base_lock.rb +100 -79
  18. data/lib/sidekiq_unique_jobs/lock/client_validator.rb +28 -0
  19. data/lib/sidekiq_unique_jobs/lock/server_validator.rb +27 -0
  20. data/lib/sidekiq_unique_jobs/lock/until_and_while_executing.rb +34 -15
  21. data/lib/sidekiq_unique_jobs/lock/until_executed.rb +19 -7
  22. data/lib/sidekiq_unique_jobs/lock/until_executing.rb +16 -2
  23. data/lib/sidekiq_unique_jobs/lock/until_expired.rb +20 -16
  24. data/lib/sidekiq_unique_jobs/lock/validator.rb +96 -0
  25. data/lib/sidekiq_unique_jobs/lock/while_executing.rb +19 -10
  26. data/lib/sidekiq_unique_jobs/lock/while_executing_reject.rb +3 -3
  27. data/lib/sidekiq_unique_jobs/lock.rb +325 -0
  28. data/lib/sidekiq_unique_jobs/lock_args.rb +123 -0
  29. data/lib/sidekiq_unique_jobs/lock_config.rb +126 -0
  30. data/lib/sidekiq_unique_jobs/lock_digest.rb +79 -0
  31. data/lib/sidekiq_unique_jobs/lock_info.rb +68 -0
  32. data/lib/sidekiq_unique_jobs/lock_timeout.rb +62 -0
  33. data/lib/sidekiq_unique_jobs/lock_ttl.rb +77 -0
  34. data/lib/sidekiq_unique_jobs/locksmith.rb +275 -102
  35. data/lib/sidekiq_unique_jobs/logging/middleware_context.rb +44 -0
  36. data/lib/sidekiq_unique_jobs/logging.rb +179 -33
  37. data/lib/sidekiq_unique_jobs/lua/delete.lua +51 -0
  38. data/lib/sidekiq_unique_jobs/lua/delete_by_digest.lua +42 -0
  39. data/lib/sidekiq_unique_jobs/lua/delete_job_by_digest.lua +38 -0
  40. data/lib/sidekiq_unique_jobs/lua/find_digest_in_queues.lua +26 -0
  41. data/lib/sidekiq_unique_jobs/lua/lock.lua +93 -0
  42. data/lib/sidekiq_unique_jobs/lua/locked.lua +35 -0
  43. data/lib/sidekiq_unique_jobs/lua/queue.lua +87 -0
  44. data/lib/sidekiq_unique_jobs/lua/reap_orphans.lua +94 -0
  45. data/lib/sidekiq_unique_jobs/lua/shared/_common.lua +40 -0
  46. data/lib/sidekiq_unique_jobs/lua/shared/_current_time.lua +8 -0
  47. data/lib/sidekiq_unique_jobs/lua/shared/_delete_from_queue.lua +22 -0
  48. data/lib/sidekiq_unique_jobs/lua/shared/_delete_from_sorted_set.lua +18 -0
  49. data/lib/sidekiq_unique_jobs/lua/shared/_find_digest_in_process_set.lua +53 -0
  50. data/lib/sidekiq_unique_jobs/lua/shared/_find_digest_in_queues.lua +43 -0
  51. data/lib/sidekiq_unique_jobs/lua/shared/_find_digest_in_sorted_set.lua +24 -0
  52. data/lib/sidekiq_unique_jobs/lua/shared/_hgetall.lua +13 -0
  53. data/lib/sidekiq_unique_jobs/lua/shared/_upgrades.lua +3 -0
  54. data/lib/sidekiq_unique_jobs/lua/unlock.lua +95 -0
  55. data/lib/sidekiq_unique_jobs/lua/update_version.lua +40 -0
  56. data/lib/sidekiq_unique_jobs/lua/upgrade.lua +68 -0
  57. data/lib/sidekiq_unique_jobs/middleware/client.rb +40 -0
  58. data/lib/sidekiq_unique_jobs/middleware/server.rb +29 -0
  59. data/lib/sidekiq_unique_jobs/middleware.rb +29 -43
  60. data/lib/sidekiq_unique_jobs/normalizer.rb +4 -4
  61. data/lib/sidekiq_unique_jobs/on_conflict/log.rb +9 -5
  62. data/lib/sidekiq_unique_jobs/on_conflict/null_strategy.rb +1 -1
  63. data/lib/sidekiq_unique_jobs/on_conflict/raise.rb +1 -1
  64. data/lib/sidekiq_unique_jobs/on_conflict/reject.rb +61 -15
  65. data/lib/sidekiq_unique_jobs/on_conflict/replace.rb +54 -14
  66. data/lib/sidekiq_unique_jobs/on_conflict/reschedule.rb +16 -5
  67. data/lib/sidekiq_unique_jobs/on_conflict/strategy.rb +25 -6
  68. data/lib/sidekiq_unique_jobs/on_conflict.rb +23 -10
  69. data/lib/sidekiq_unique_jobs/options_with_fallback.rb +34 -29
  70. data/lib/sidekiq_unique_jobs/orphans/lua_reaper.rb +29 -0
  71. data/lib/sidekiq_unique_jobs/orphans/manager.rb +213 -0
  72. data/lib/sidekiq_unique_jobs/orphans/null_reaper.rb +24 -0
  73. data/lib/sidekiq_unique_jobs/orphans/observer.rb +42 -0
  74. data/lib/sidekiq_unique_jobs/orphans/reaper.rb +114 -0
  75. data/lib/sidekiq_unique_jobs/orphans/reaper_resurrector.rb +170 -0
  76. data/lib/sidekiq_unique_jobs/orphans/ruby_reaper.rb +213 -0
  77. data/lib/sidekiq_unique_jobs/redis/entity.rb +112 -0
  78. data/lib/sidekiq_unique_jobs/redis/hash.rb +56 -0
  79. data/lib/sidekiq_unique_jobs/redis/list.rb +32 -0
  80. data/lib/sidekiq_unique_jobs/redis/set.rb +32 -0
  81. data/lib/sidekiq_unique_jobs/redis/sorted_set.rb +86 -0
  82. data/lib/sidekiq_unique_jobs/redis/string.rb +49 -0
  83. data/lib/sidekiq_unique_jobs/redis.rb +11 -0
  84. data/lib/sidekiq_unique_jobs/reflectable.rb +17 -0
  85. data/lib/sidekiq_unique_jobs/reflections.rb +68 -0
  86. data/lib/sidekiq_unique_jobs/rspec/matchers/have_valid_sidekiq_options.rb +51 -0
  87. data/lib/sidekiq_unique_jobs/rspec/matchers.rb +26 -0
  88. data/lib/sidekiq_unique_jobs/script/caller.rb +127 -0
  89. data/lib/sidekiq_unique_jobs/script.rb +15 -0
  90. data/lib/sidekiq_unique_jobs/server.rb +49 -0
  91. data/lib/sidekiq_unique_jobs/sidekiq_unique_ext.rb +92 -65
  92. data/lib/sidekiq_unique_jobs/sidekiq_unique_jobs.rb +241 -35
  93. data/lib/sidekiq_unique_jobs/sidekiq_worker_methods.rb +11 -15
  94. data/lib/sidekiq_unique_jobs/testing.rb +62 -21
  95. data/lib/sidekiq_unique_jobs/timer_task.rb +78 -0
  96. data/lib/sidekiq_unique_jobs/timing.rb +58 -0
  97. data/lib/sidekiq_unique_jobs/unlockable.rb +20 -4
  98. data/lib/sidekiq_unique_jobs/update_version.rb +25 -0
  99. data/lib/sidekiq_unique_jobs/upgrade_locks.rb +155 -0
  100. data/lib/sidekiq_unique_jobs/version.rb +3 -1
  101. data/lib/sidekiq_unique_jobs/version_check.rb +23 -4
  102. data/lib/sidekiq_unique_jobs/web/helpers.rb +128 -13
  103. data/lib/sidekiq_unique_jobs/web/views/_paging.erb +4 -4
  104. data/lib/sidekiq_unique_jobs/web/views/changelogs.erb +54 -0
  105. data/lib/sidekiq_unique_jobs/web/views/lock.erb +108 -0
  106. data/lib/sidekiq_unique_jobs/web/views/locks.erb +54 -0
  107. data/lib/sidekiq_unique_jobs/web.rb +57 -27
  108. data/lib/sidekiq_unique_jobs.rb +52 -7
  109. data/lib/tasks/changelog.rake +5 -5
  110. metadata +121 -177
  111. data/lib/sidekiq_unique_jobs/client/middleware.rb +0 -56
  112. data/lib/sidekiq_unique_jobs/scripts.rb +0 -118
  113. data/lib/sidekiq_unique_jobs/server/middleware.rb +0 -46
  114. data/lib/sidekiq_unique_jobs/timeout/calculator.rb +0 -63
  115. data/lib/sidekiq_unique_jobs/timeout.rb +0 -8
  116. data/lib/sidekiq_unique_jobs/unique_args.rb +0 -150
  117. data/lib/sidekiq_unique_jobs/util.rb +0 -103
  118. data/lib/sidekiq_unique_jobs/web/views/unique_digest.erb +0 -28
  119. data/lib/sidekiq_unique_jobs/web/views/unique_digests.erb +0 -46
  120. data/redis/acquire_lock.lua +0 -21
  121. data/redis/convert_legacy_lock.lua +0 -13
  122. data/redis/delete.lua +0 -14
  123. data/redis/delete_by_digest.lua +0 -23
  124. data/redis/delete_job_by_digest.lua +0 -60
  125. data/redis/lock.lua +0 -62
  126. data/redis/release_stale_locks.lua +0 -90
  127. data/redis/unlock.lua +0 -35
data/README.md CHANGED
@@ -1,45 +1,86 @@
1
- # SidekiqUniqueJobs [![Join the chat at https://gitter.im/mhenrixon/sidekiq-unique-jobs](https://badges.gitter.im/mhenrixon/sidekiq-unique-jobs.svg)](https://gitter.im/mhenrixon/sidekiq-unique-jobs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.com/mhenrixon/sidekiq-unique-jobs.svg?branch=v6.x)](https://travis-ci.com/mhenrixon/sidekiq-unique-jobs) [![Code Climate](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs.png)](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs) [![Test Coverage](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs/badges/coverage.svg)](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs/coverage)
1
+ # SidekiqUniqueJobs
2
+
3
+ [![Join the chat at https://gitter.im/mhenrixon/sidekiq-unique-jobs](https://badges.gitter.im/mhenrixon/sidekiq-unique-jobs.svg)](https://gitter.im/mhenrixon/sidekiq-unique-jobs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![Build Status](https://github.com/mhenrixon/sidekiq-unique-jobs/actions/workflows/rspec.yml/badge.svg?branch=master) [![Code Climate](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs.svg)](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs) [![Test Coverage](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs/badges/coverage.svg)](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs/coverage)
4
+
5
+ ## Support Me
6
+
7
+ Want to show me some ❤️ for the hard work I do on this gem? You can use the following PayPal link: [https://paypal.me/mhenrixon1](https://paypal.me/mhenrixon1). Any amount is welcome and let me tell you it feels good to be appreciated. Even a dollar makes me super excited about all of this.
2
8
 
3
9
  <!-- MarkdownTOC -->
4
10
 
5
11
  - [Introduction](#introduction)
6
- - [Documentation](#documentation)
12
+ - [Usage](#usage)
13
+ - [Installation](#installation)
14
+ - [Add the middleware](#add-the-middleware)
15
+ - [Your first worker](#your-first-worker)
7
16
  - [Requirements](#requirements)
8
- - [ActiveJob](#activejob)
9
- - [redis-namespace](#redis-namespace)
10
- - [Installation](#installation)
11
- - [Support Me](#support-me)
12
- - [General Information](#general-information)
13
- - [Options](#options)
14
- - [Lock Expiration](#lock-expiration)
15
- - [Lock Timeout](#lock-timeout)
16
- - [Unique Across Queues](#unique-across-queues)
17
- - [Unique Across Workers](#unique-across-workers)
18
17
  - [Locks](#locks)
19
18
  - [Until Executing](#until-executing)
19
+ - [Example worker](#example-worker)
20
20
  - [Until Executed](#until-executed)
21
- - [Until Timeout](#until-timeout)
22
- - [Unique Until And While Executing](#unique-until-and-while-executing)
21
+ - [Example worker](#example-worker-1)
22
+ - [Until Expired](#until-expired)
23
+ - [Example worker](#example-worker-2)
24
+ - [Until And While Executing](#until-and-while-executing)
25
+ - [Example worker](#example-worker-3)
23
26
  - [While Executing](#while-executing)
27
+ - [Example worker](#example-worker-4)
28
+ - [Custom Locks](#custom-locks)
24
29
  - [Conflict Strategy](#conflict-strategy)
25
- - [Log](#log)
26
- - [Raise](#raise)
27
- - [Reject](#reject)
28
- - [Replace](#replace)
30
+ - [log](#log)
31
+ - [raise](#raise)
32
+ - [reject](#reject)
33
+ - [replace](#replace)
29
34
  - [Reschedule](#reschedule)
30
- - [Usage](#usage)
31
- - [Finer Control over Uniqueness](#finer-control-over-uniqueness)
32
- - [After Unlock Callback](#after-unlock-callback)
33
- - [Logging](#logging)
34
- - [Cleanup Dead Locks](#cleanup-dead-locks)
35
- - [Other Sidekiq gems](#other-sidekiq-gems)
36
- - [sidekiq-global_id](#sidekiq-global_id)
35
+ - [Custom Strategies](#custom-strategies)
36
+ - [3 Cleanup Dead Locks](#3-cleanup-dead-locks)
37
37
  - [Debugging](#debugging)
38
38
  - [Sidekiq Web](#sidekiq-web)
39
- - [Show Unique Digests](#show-unique-digests)
40
- - [Show keys for digest](#show-keys-for-digest)
41
- - [Communication](#communication)
39
+ - [Reflections \(metrics, logging, etc.\)](#reflections-metrics-logging-etc)
40
+ - [after_unlock_callback_failed](#after_unlock_callback_failed)
41
+ - [error](#error)
42
+ - [execution_failed](#execution_failed)
43
+ - [lock_failed](#lock_failed)
44
+ - [locked](#locked)
45
+ - [reschedule_failed](#reschedule_failed)
46
+ - [rescheduled](#rescheduled)
47
+ - [timeout](#timeout)
48
+ - [unlock_failed](#unlock_failed)
49
+ - [unlocked](#unlocked)
50
+ - [unknown_sidekiq_worker](#unknown_sidekiq_worker)
51
+ - [Show Locks](#show-locks)
52
+ - [Show Lock](#show-lock)
42
53
  - [Testing](#testing)
54
+ - [Validating Worker Configuration](#validating-worker-configuration)
55
+ - [Uniqueness](#uniqueness)
56
+ - [Configuration](#configuration)
57
+ - [Other Sidekiq gems](#other-sidekiq-gems)
58
+ - [apartment-sidekiq](#apartment-sidekiq)
59
+ - [sidekiq-global_id](#sidekiq-global_id)
60
+ - [sidekiq-status](#sidekiq-status)
61
+ - [Global Configuration](#global-configuration)
62
+ - [debug_lua](#debug_lua)
63
+ - [lock_timeout](#lock_timeout)
64
+ - [lock_ttl](#lock_ttl)
65
+ - [enabled](#enabled)
66
+ - [logger](#logger)
67
+ - [max_history](#max_history)
68
+ - [reaper](#reaper)
69
+ - [reaper_count](#reaper_count)
70
+ - [reaper_interval](#reaper_interval)
71
+ - [reaper_timeout](#reaper_timeout)
72
+ - [lock_prefix](#lock_prefix)
73
+ - [lock_info](#lock_info)
74
+ - [Worker Configuration](#worker-configuration)
75
+ - [lock_info](#lock_info-1)
76
+ - [lock_prefix](#lock_prefix-1)
77
+ - [lock_ttl](#lock_ttl-1)
78
+ - [lock_timeout](#lock_timeout-1)
79
+ - [unique_across_queues](#unique_across_queues)
80
+ - [unique_across_workers](#unique_across_workers)
81
+ - [Finer Control over Uniqueness](#finer-control-over-uniqueness)
82
+ - [After Unlock Callback](#after-unlock-callback)
83
+ - [Communication](#communication)
43
84
  - [Contributing](#contributing)
44
85
  - [Contributors](#contributors)
45
86
 
@@ -47,180 +88,214 @@
47
88
 
48
89
  ## Introduction
49
90
 
50
- The goal of this gem is to ensure your Sidekiq jobs are unique. We do this by creating unique keys in Redis based on how you configure uniqueness.
91
+ This gem adds unique constraints to sidekiq jobs. The uniqueness is achieved by creating a set of keys in redis based off of `queue`, `class`, `args` (in the sidekiq job hash).
51
92
 
52
- ## Documentation
93
+ By default, only one lock for a given hash can be acquired. What happens when a lock can't be acquired is governed by a chosen [Conflict Strategy](#conflict-strategy) strategy. Unless a conflict strategy is chosen
53
94
 
54
- This is the documentation for the master branch. You can find the documentation for each release by navigating to its tag: [v5.0.10][]
95
+ This is the documentation for the master branch. You can find the documentation for each release by navigating to its tag.
55
96
 
56
- Below are links to the latest major versions (4 & 5):
97
+ Here are links to some of the old versions
57
98
 
58
- - [v5.0.10][]
59
- - [v4.0.18][]
99
+ - [v6.0.25](https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v6.0.25)
100
+ - [v5.0.10](https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v5.0.10)
101
+ - [v4.0.18](https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v4.0.18)
60
102
 
61
- ## Requirements
62
-
63
- See [Sidekiq requirements][] for what is required. Starting from 5.0.0 only sidekiq >= 4 and MRI >= 2.2. ActiveJob is not supported
64
-
65
- ### ActiveJob
66
-
67
- Version 6 requires Redis >= 3 and pure Sidekiq, no ActiveJob supported anymore. See [About ActiveJob](https://github.com/mhenrixon/sidekiq-unique-jobs/wiki/About-ActiveJob) for why. It simply is too complex and generates more issues than I can handle given how little timer I have to spend on this project.
68
-
69
- ### redis-namespace
70
-
71
- Will not be officially supported anymore. Since Mike [won't support redis-namespace](https://github.com/mperham/sidekiq/issues/3366#issuecomment-284270120) neither will I.
72
-
73
- [Read this](http://www.mikeperham.com/2017/04/10/migrating-from-redis-namespace/) for how to migrate away from namespacing.
103
+ ## Usage
74
104
 
75
- ## Installation
105
+ ### Installation
76
106
 
77
107
  Add this line to your application's Gemfile:
78
108
 
79
- ```
109
+ ```ruby
80
110
  gem 'sidekiq-unique-jobs'
81
111
  ```
82
112
 
83
113
  And then execute:
84
114
 
85
- ```
115
+ ```bash
86
116
  bundle
87
117
  ```
88
118
 
89
- Or install it yourself as:
90
-
91
- ```
92
- gem install sidekiq-unique-jobs
93
- ```
94
-
95
- ## Support Me
119
+ ### Add the middleware
96
120
 
97
- Want to show me some ❤️ for the hard work I do on this gem? You can use the following [PayPal link][]. Any amount is welcome and let me tell you it feels good to be appreciated. Even a dollar makes me super excited about all of this.
121
+ Before v7, the middleware was configured automatically. Since some people reported issues with other gems (see [Other Sidekiq Gems](#other-sidekiq-gems)) it was decided to give full control over to the user.
98
122
 
99
- ## General Information
123
+ *NOTE* if you want to use the reaper you also need to configure the server middleware.
100
124
 
101
- See [Interaction w/ Sidekiq](https://github.com/mhenrixon/sidekiq-unique-jobs/wiki/How-this-gem-interacts-with-Sidekiq) on how the gem interacts with Sidekiq.
125
+ [A full example](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/myapp/config/initializers/sidekiq.rb#L12)
102
126
 
103
- See [Locking & Unlocking](https://github.com/mhenrixon/sidekiq-unique-jobs/wiki/Locking-&-Unlocking) for an overview of the differences on when the various lock types are locked and unlocked.
127
+ ```ruby
128
+ Sidekiq.configure_server do |config|
129
+ config.redis = { url: ENV["REDIS_URL"], driver: :hiredis }
104
130
 
105
- ## Options
131
+ config.client_middleware do |chain|
132
+ chain.add SidekiqUniqueJobs::Middleware::Client
133
+ end
106
134
 
107
- ### Lock Expiration
135
+ config.server_middleware do |chain|
136
+ chain.add SidekiqUniqueJobs::Middleware::Server
137
+ end
108
138
 
109
- Lock expiration is used for two things. For the `UntilExpired` job releases the lock upon expiry. This is done from the client.
139
+ SidekiqUniqueJobs::Server.configure(config)
140
+ end
110
141
 
111
- Since v6.0.11 the other locks will expire after the server is done processing.
142
+ Sidekiq.configure_client do |config|
143
+ config.redis = { url: ENV["REDIS_URL"], driver: :hiredis }
112
144
 
113
- ```ruby
114
- sidekiq_options lock_expiration: nil # default - don't expire keys
115
- sidekiq_options lock_expiration: 20.days.to_i # expire this lock in 20 days
145
+ config.client_middleware do |chain|
146
+ chain.add SidekiqUniqueJobs::Middleware::Client
147
+ end
148
+ end
116
149
  ```
117
150
 
118
- ### Lock Timeout
151
+ ### Your first worker
119
152
 
120
- This is the timeout (how long to wait) when creating the lock. By default we don't use a timeout so we won't wait for the lock to be created. If you want it is possible to set this like below.
153
+ The most likely to be used worker is `:until_executed`. This type of lock creates a lock from when `UntilExecutedWorker.perform_async` is called until right after `UntilExecutedWorker.new.perform` has been called.
121
154
 
122
155
  ```ruby
123
- sidekiq_options lock_timeout: 0 # default - don't wait at all
124
- sidekiq_options lock_timeout: 5 # wait 5 seconds
125
- sidekiq_options lock_timeout: nil # lock indefinitely, this process won't continue until it gets a lock. VERY DANGEROUS!!
126
- ```
127
-
128
- ### Unique Across Queues
129
-
130
- This configuration option is slightly misleading. It doesn't disregard the queue on other jobs. Just on itself, this means that a worker that might schedule jobs into multiple queues will be able to have uniqueness enforced on all queues it is pushed to.
156
+ # frozen_string_literal: true
131
157
 
132
- ```ruby
133
- class Worker
158
+ class UntilExecutedWorker
134
159
  include Sidekiq::Worker
135
160
 
136
- sidekiq_options unique_across_queues: true, queue: 'default'
161
+ sidekiq_options queue: :until_executed,
162
+ lock: :until_executed
137
163
 
138
- def perform(args); end
164
+ def perform
165
+ logger.info("cowboy")
166
+ sleep(1) # hardcore processing
167
+ logger.info("beebop")
168
+ end
139
169
  end
140
170
  ```
141
171
 
142
- Now if you push override the queue with `Worker.set(queue: 'another').perform_async(1)` it will still be considered unique when compared to `Worker.perform_async(1)` (that was actually pushed to the queue `default`).
143
-
144
- ### Unique Across Workers
172
+ You can read more about the worker configuration in [Worker Configuration](#worker-configuration) below.
145
173
 
146
- This configuration option is slightly misleading. It doesn't disregard the worker class on other jobs. Just on itself, this means that a worker that the worker class won't be used for generating the unique digest. The only way this option really makes sense is when you want to have uniqueness between two different worker classes.
174
+ ## Requirements
147
175
 
148
- ```ruby
149
- class WorkerOne
150
- include Sidekiq::Worker
176
+ - Sidekiq `>= 5.0` (`>= 5.2` recommended)
177
+ - Ruby:
178
+ - MRI `>= 2.5` (`>= 2.6` recommended)
179
+ - JRuby `>= 9.0` (`>= 9.2` recommended)
180
+ - Truffleruby
181
+ - Redis Server `>= 3.2` (`>= 5.0` recommended)
182
+ - [ActiveJob officially not supported][48]
183
+ - [redis-namespace officially not supported][49]
151
184
 
152
- sidekiq_options unique_across_workers: true, queue: 'default'
185
+ See [Sidekiq requirements][24] for detailed requirements of Sidekiq itself (be sure to check the right sidekiq version).
153
186
 
154
- def perform(args); end
155
- end
187
+ ## Locks
156
188
 
157
- class WorkerTwo
158
- include Sidekiq::Worker
189
+ ### Until Executing
159
190
 
160
- sidekiq_options unique_across_workers: true, queue: 'default'
191
+ A lock is created when `UntilExecuting.perform_async` is called. Then it is either unlocked when `lock_ttl` is hit or before Sidekiq calls the `perform` method on your worker.
161
192
 
162
- def perform(args); end
163
- end
193
+ #### Example worker
164
194
 
195
+ ```ruby
196
+ class UntilExecuting
197
+ include Sidekiq::Workers
165
198
 
166
- WorkerOne.perform_async(1)
167
- # => 'the jobs unique id'
199
+ sidekiq_options lock: :until_executing
168
200
 
169
- WorkerTwo.perform_async(1)
170
- # => nil because WorkerOne just stole the lock
201
+ def perform(id)
202
+ # Do work
203
+ end
204
+ end
171
205
  ```
172
206
 
173
- ## Locks
174
-
175
- ### Until Executing
176
-
177
- Locks from when the client pushes the job to the queue. Will be unlocked before the server starts processing the job.
178
-
179
207
  **NOTE** this is probably not so good for jobs that shouldn't be running simultaneously (aka slow jobs).
180
208
 
181
- ```ruby
182
- sidekiq_options lock: :until_executing
183
- ```
209
+ The reason this type of lock exists is to fix the following problem: [sidekiq/issues/3471](https://github.com/mperham/sidekiq/issues/3471#issuecomment-300866335)
184
210
 
185
211
  ### Until Executed
186
212
 
187
- Locks from when the client pushes the job to the queue. Will be unlocked when the server has successfully processed the job.
213
+ A lock is created when `UntilExecuted.perform_async` is called. Then it is either unlocked when `lock_ttl` is hit or when Sidekiq has called the `perform` method on your worker.
214
+
215
+ #### Example worker
188
216
 
189
217
  ```ruby
190
- sidekiq_options lock: :until_executed
218
+ class UntilExecuted
219
+ include Sidekiq::Workers
220
+
221
+ sidekiq_options lock: :until_executed
222
+
223
+ def perform(id)
224
+ # Do work
225
+ end
226
+ end
191
227
  ```
192
228
 
193
- ### Until Timeout
229
+ ### Until Expired
194
230
 
195
- Locks from when the client pushes the job to the queue. Will be unlocked when the specified timeout has been reached.
231
+ This lock behaves identically to the [Until Executed](#until-executed) except for one thing. This job won't be unlocked until the expiration is hit. For jobs that need to run only once per day, this would be the perfect lock. This way, we can't create more jobs until one day after this job was first pushed.
232
+
233
+ #### Example worker
196
234
 
197
235
  ```ruby
198
- sidekiq_options lock: :until_expired
236
+ class UntilExpired
237
+ include Sidekiq::Workers
238
+
239
+ sidekiq_options lock: :until_expired, lock_ttl: 1.day
240
+
241
+ def perform
242
+ # Do work
243
+ end
244
+ end
199
245
  ```
200
246
 
201
- ### Unique Until And While Executing
247
+ ### Until And While Executing
248
+
249
+ This lock is a combination of two locks (`:until_executing` and `:while_executing`). Please see the configuration for [Until Executing](#until-executing) and [While Executing](#while-executing)
202
250
 
203
- Locks when the client pushes the job to the queue. The queue will be unlocked when the server starts processing the job. The server then goes on to creating a runtime lock for the job to prevent simultaneous jobs from being executed. As soon as the server starts processing a job, the client can push the same job to the queue.
251
+ #### Example worker
204
252
 
205
253
  ```ruby
206
- sidekiq_options lock: :until_and_while_executing
254
+ class UntilAndWhileExecutingWorker
255
+ include Sidekiq::Workers
256
+
257
+ sidekiq_options lock: :until_and_while_executing,
258
+ lock_timeout: 2,
259
+ on_conflict: {
260
+ client: :log,
261
+ server: :raise
262
+ }
263
+ def perform(id)
264
+ # Do work
265
+ end
266
+ end
207
267
  ```
208
268
 
209
269
  ### While Executing
210
270
 
211
- With this lock type it is possible to put any number of these jobs on the queue, but as the server pops the job from the queue it will create a lock and then wait until other locks are done processing. It _looks_ like multiple jobs are running at the same time but in fact the second job will only be waiting for the first job to finish.
271
+ Tese locks are put on a queue without any type of locking mechanism, the locking doesn't happen until Sidekiq pops the job from the queue and starts processing it.
212
272
 
213
- **NOTE** Unless this job is configured with a `lock_timeout: nil` or `lock_timeout: > 0` then all jobs that are attempted to be executed will just be dropped without waiting.
273
+ #### Example worker
214
274
 
215
275
  ```ruby
216
- sidekiq_options lock: :while_executing, lock_timeout: nil
276
+ class WhileExecutingWorker
277
+ include Sidekiq::Workers
278
+
279
+ sidekiq_options lock: :while_executing,
280
+ lock_timeout: 2,
281
+ on_conflict: {
282
+ server: :raise
283
+ }
284
+ def perform(id)
285
+ # Do work
286
+ end
287
+ end
217
288
  ```
218
289
 
219
- There is an example of this to try it out in the `rails_example` application. Run `foreman start` in the root of the directory and open the url: `localhost:5000/work/duplicate_while_executing`.
290
+ **NOTE** Unless a conflict strategy of `:raise` is specified, if lock fails, the job will be dropped without notice. When told to raise, the job will be put back and retried. It would also be possible to use `:reschedule` with this lock.
291
+
292
+ **NOTE** Unless this job is configured with a `lock_timeout: nil` or `lock_timeout: > 0` then all jobs that are attempted to be executed will just be dropped without waiting.
293
+
294
+ There is an example of this to try it out in the `myapp` application. Run `foreman start` in the root of the directory and open the url: `localhost:5000/work/duplicate_while_executing`.
220
295
 
221
296
  In the console you should see something like:
222
297
 
223
- ```
298
+ ```bash
224
299
  0:32:24 worker.1 | 2017-04-23T08:32:24.955Z 84404 TID-ougq4thko WhileExecutingWorker JID-400ec51c9523f41cd4a35058 INFO: start
225
300
  10:32:24 worker.1 | 2017-04-23T08:32:24.956Z 84404 TID-ougq8csew WhileExecutingWorker JID-8d6d9168368eedaed7f75763 INFO: start
226
301
  10:32:24 worker.1 | 2017-04-23T08:32:24.957Z 84404 TID-ougq8crt8 WhileExecutingWorker JID-affcd079094c9b26e8b9ba60 INFO: start
@@ -235,189 +310,156 @@ In the console you should see something like:
235
310
  10:33:04 worker.1 | 2017-04-23T08:33:04.973Z 84404 TID-ougq8cs8s WhileExecutingWorker JID-9e197460c067b22eb1b5d07f INFO: done: 40.014 sec
236
311
  ```
237
312
 
238
- ## Conflict Strategy
239
-
240
- Decides how we handle conflict. We can either reject the job to the dead queue or reschedule it. Both are useful for jobs that absolutely need to run and have been configured to use the lock `WhileExecuting` that is used only by the sidekiq server process.
241
-
242
- The last one is log which can be be used with the lock `UntilExecuted` and `UntilExpired`. Now we write a log entry saying the job could not be pushed because it is a duplicate of another job with the same arguments
313
+ ### Custom Locks
243
314
 
244
- ### Log
315
+ You may need to define some custom lock. You can define it in one project folder:
245
316
 
246
- This strategy is intended to be used with `UntilExecuted` and `UntilExpired`. It will log a line about that this is job is a duplicate of another.
317
+ ```ruby
318
+ # lib/locks/my_custom_lock.rb
319
+ module Locks
320
+ class MyCustomLock < SidekiqUniqueJobs::Lock::BaseLock
321
+ def execute
322
+ # Do something ...
323
+ end
324
+ end
325
+ end
326
+ ```
247
327
 
248
- `sidekiq_options lock: :until_executed, on_conflict: :log`
328
+ You can refer on all the locks defined in `lib/sidekiq_unique_jobs/lock/*.rb`.
249
329
 
250
- ### Raise
330
+ In order to make it available, you should call in your project startup:
251
331
 
252
- This strategy is intended to be used with `WhileExecuting`. Basically it will allow us to let the server process crash with a specific error message and be retried without messing up the Sidekiq stats.
332
+ (For rails application config/initializers/sidekiq_unique_jobs.rb or other projects, wherever you prefer)
253
333
 
254
- `sidekiq_options lock: :while_executing, on_conflict: :raise, retry: 10`
334
+ ```ruby
335
+ SidekiqUniqueJobs.configure do |config|
336
+ config.add_lock :my_custom_lock, Locks::MyCustomLock
337
+ end
338
+ ```
255
339
 
256
- ### Reject
340
+ And then you can use it in the jobs definition:
257
341
 
258
- This strategy is intended to be used with `WhileExecuting` and will push the job to the dead queue on conflict.
342
+ `sidekiq_options lock: :my_custom_lock, on_conflict: :log`
259
343
 
260
- `sidekiq_options lock: :while_executing, on_conflict: :reject`
344
+ Please not that if you try to override a default lock, an `ArgumentError` will be raised.
261
345
 
262
- ### Replace
346
+ ## Conflict Strategy
263
347
 
264
- This strategy is intended to be used with client locks like `UntilExecuted`.
265
- It will delete any existing job for these arguments from retry, schedule and
266
- queue and retry the lock again.
348
+ Decides how we handle conflict. We can either reject the job to the dead queue or reschedule it. Both are useful for jobs that absolutely need to run and have been configured to use the lock `WhileExecuting` that is used only by the sidekiq server process.
267
349
 
268
- This is slightly dangerous and should probably only be used for jobs that are
269
- always scheduled in the future. Currently only attempting to retry one time.
350
+ The last one is log which can be be used with the lock `UntilExecuted` and `UntilExpired`. Now we write a log entry saying the job could not be pushed because it is a duplicate of another job with the same arguments.
270
351
 
271
- `sidekiq_options lock: :until_executed, on_conflict: :replace`
352
+ It is possible for locks to have different conflict strategy for the client and server. This is useful for `:until_and_while_executing`.
272
353
 
273
- ### Reschedule
354
+ ```ruby
355
+ sidekiq_options lock: :until_and_while_executing,
356
+ on_conflict: { client: :log, server: :reject }
357
+ ```
274
358
 
275
- This strategy is intended to be used with `WhileExecuting` and will delay the job to be tried again in 5 seconds. This will mess up the sidekiq stats but will prevent exceptions from being logged and confuse your sysadmins.
359
+ ### log
276
360
 
277
- `sidekiq_options lock: :while_executing, on_conflict: :reschedule`
361
+ ```ruby
362
+ sidekiq_options on_conflict: :log
363
+ ```
278
364
 
279
- ## Usage
365
+ This strategy is intended to be used with `UntilExecuted` and `UntilExpired`. It will log a line about that this is job is a duplicate of another.
280
366
 
281
- All that is required is that you specifically set the sidekiq option for _unique_ to a valid value like below:
367
+ ### raise
282
368
 
283
369
  ```ruby
284
- sidekiq_options lock: :while_executing
370
+ sidekiq_options on_conflict: :raise
285
371
  ```
286
372
 
287
- Requiring the gem in your gemfile should be sufficient to enable unique jobs.
373
+ This strategy is intended to be used with `WhileExecuting`. Basically it will allow us to let the server process crash with a specific error message and be retried without messing up the Sidekiq stats.
288
374
 
289
- ### Finer Control over Uniqueness
375
+ ### reject
290
376
 
291
- Sometimes it is desired to have a finer control over which arguments are used in determining uniqueness of the job, and others may be _transient_. For this use-case, you need to define either a `unique_args` method, or a ruby proc.
377
+ ```ruby
378
+ sidekiq_options on_conflict: :reject
379
+ ```
292
380
 
293
- The unique_args method need to return an array of values to use for uniqueness check.
381
+ This strategy is intended to be used with `WhileExecuting` and will push the job to the dead queue on conflict.
294
382
 
295
- The method or the proc can return a modified version of args without the transient arguments included, as shown below:
383
+ ### replace
296
384
 
297
385
  ```ruby
298
- class UniqueJobWithFilterMethod
299
- include Sidekiq::Worker
300
- sidekiq_options lock: :until_and_while_executing,
301
- unique_args: :unique_args # this is default and will be used if such a method is defined
386
+ sidekiq_options on_conflict: :replace
387
+ ```
302
388
 
303
- def self.unique_args(args)
304
- [ args[0], args[2][:type] ]
305
- end
389
+ This strategy is intended to be used with client locks like `UntilExecuted`.
390
+ It will delete any existing job for these arguments from retry, schedule and
391
+ queue and retry the lock again.
306
392
 
307
- ...
393
+ This is slightly dangerous and should probably only be used for jobs that are
394
+ always scheduled in the future. Currently only attempting to retry one time.
308
395
 
309
- end
396
+ ### Reschedule
310
397
 
311
- class UniqueJobWithFilterProc
312
- include Sidekiq::Worker
313
- sidekiq_options lock: :until_executed,
314
- unique_args: ->(args) { [ args.first ] }
398
+ ```ruby
399
+ sidekiq_options on_conflict: :reschedule
400
+ ```
315
401
 
316
- ...
402
+ This strategy is intended to be used with `WhileExecuting` and will delay the job to be tried again in 5 seconds. This will mess up the sidekiq stats but will prevent exceptions from being logged and confuse your sysadmins.
317
403
 
318
- end
319
- ```
404
+ ### Custom Strategies
320
405
 
321
- It is also quite possible to ensure different types of unique args based on context. I can't vouch for the below example but see [#203](https://github.com/mhenrixon/sidekiq-unique-jobs/issues/203) for the discussion.
406
+ You may need to define some custom strategy. You can define it in one project folder:
322
407
 
323
408
  ```ruby
324
- class UniqueJobWithFilterMethod
325
- include Sidekiq::Worker
326
- sidekiq_options lock: :until_and_while_executing, unique_args: :unique_args
327
-
328
- def self.unique_args(args)
329
- if Sidekiq::ProcessSet.new.size > 1
330
- # sidekiq runtime; uniqueness for the object (first arg)
331
- args.first
332
- else
333
- # queuing from the app; uniqueness for all params
334
- args
409
+ # lib/strategies/my_custom_strategy.rb
410
+ module Strategies
411
+ class MyCustomStrategy < SidekiqUniqueJobs::OnConflict::Strategy
412
+ def call
413
+ # Do something ...
335
414
  end
336
415
  end
337
416
  end
338
417
  ```
339
418
 
340
- ### After Unlock Callback
341
-
342
- If you need to perform any additional work after the lock has been released you can provide an `#after_unlock` instance method. The method will be called when the lock has been unlocked. Most times this means after yield but there are two exceptions to that.
419
+ You can refer to all the strategies defined in `lib/sidekiq_unique_jobs/on_conflict`.
343
420
 
344
- **Exception 1:** UntilExecuting unlocks and calls back before yielding.
345
- **Exception 2:** UntilExpired expires eventually, no after_unlock hook is called.
421
+ In order to make it available, you should call in your project startup:
346
422
 
347
- **NOTE:** _It is also possible to write this code as a class method._
423
+ (For rails application config/initializers/sidekiq_unique_jobs.rb for other projects, wherever you prefer)
348
424
 
349
425
  ```ruby
350
- class UniqueJobWithFilterMethod
351
- include Sidekiq::Worker
352
- sidekiq_options lock: :while_executing,
353
-
354
- def self.after_unlock
355
- # block has yielded and lock is released
356
- end
357
-
358
- def after_unlock
359
- # block has yielded and lock is released
360
- end
361
- ...
362
- end.
426
+ SidekiqUniqueJobs.configure do |config|
427
+ config.add_strategy :my_custom_strategy, Strategies::MyCustomStrategy
428
+ end
363
429
  ```
364
430
 
365
- ### Logging
366
-
367
- To see logging in sidekiq when duplicate payload has been filtered out you can enable on a per worker basis using the sidekiq options. The default value is false
431
+ And then you can use it in the jobs definition:
368
432
 
369
433
  ```ruby
370
- class UniqueJobWithFilterMethod
371
- include Sidekiq::Worker
372
- sidekiq_options lock: :while_executing,
373
- log_duplicate_payload: true
374
-
375
- ...
376
-
377
- end
434
+ sidekiq_options lock: :while_executing, on_conflict: :my_custom_strategy
378
435
  ```
379
436
 
380
- ### Cleanup Dead Locks
437
+ Please not that if you try to override a default lock, an `ArgumentError` will be raised.
381
438
 
382
- For sidekiq versions before 5.1 a `sidekiq_retries_exhausted` block is required per worker class. This is deprecated in Sidekiq 6.0
439
+ ### 3 Cleanup Dead Locks
440
+
441
+ For sidekiq versions < 5.1 a `sidekiq_retries_exhausted` block is required per worker class. This is deprecated in Sidekiq 6.0
383
442
 
384
443
  ```ruby
385
444
  class MyWorker
386
445
  sidekiq_retries_exhausted do |msg, _ex|
387
- SidekiqUniqueJobs::Digests.delete_by_digest(msg['unique_digest']) if msg['unique_digest']
446
+ digest = msg['lock_digest']
447
+ SidekiqUniqueJobs::Digests.new.delete_by_digest(digest) if digest
388
448
  end
389
449
  end
390
450
  ```
391
451
 
392
- Starting in v5.1, Sidekiq can also fire a global callback when a job dies:
452
+ Starting in v5.1, Sidekiq can also fire a global callback when a job dies: In version 7, this is handled automatically for you. You don't need to add a death handler, if you configure v7 like in [Add the middleware](#add-the-middleware) you don't have to worry about the below.
393
453
 
394
454
  ```ruby
395
- # this goes in your initializer
396
455
  Sidekiq.configure_server do |config|
397
456
  config.death_handlers << ->(job, _ex) do
398
- SidekiqUniqueJobs::Digests.delete_by_digest(job['unique_digest']) if job['unique_digest']
457
+ digest = job['lock_digest']
458
+ SidekiqUniqueJobs::Digests.new.delete_by_digest(digest) if digest
399
459
  end
400
460
  end
401
461
  ```
402
462
 
403
- ### Other Sidekiq gems
404
-
405
- #### sidekiq-global_id
406
-
407
- It was reported in [#235](https://github.com/mhenrixon/sidekiq-unique-jobs/issues/235) that the order of the Sidekiq middleware needs to be as follows.
408
-
409
- ```ruby
410
- Sidekiq.client_middleware do |chain|
411
- chain.add Sidekiq::GlobalId::ClientMiddleware
412
- chain.add SidekiqUniqueJobs::Client::Middleware
413
- end
414
-
415
- Sidekiq.server_middleware do |chain|
416
- chain.add SidekiqUniqueJobs::Server::Middleware
417
- chain.add Sidekiq::GlobalId::ServerMiddleware
418
- end
419
- ```
420
-
421
463
  ## Debugging
422
464
 
423
465
  There are several ways of removing keys that are stuck. The prefered way is by using the unique extension to `Sidekiq::Web`. The old console and command line versions still work but might be deprecated in the future. It is better to search for the digest itself and delete the keys matching that digest.
@@ -427,7 +469,7 @@ There are several ways of removing keys that are stuck. The prefered way is by u
427
469
  To use the web extension you need to require it in your routes.
428
470
 
429
471
  ```ruby
430
- # app/config/routes.rb
472
+ #app/config/routes.rb
431
473
  require 'sidekiq_unique_jobs/web'
432
474
  mount Sidekiq::Web, at: '/sidekiq'
433
475
  ```
@@ -437,67 +479,556 @@ already does this.
437
479
 
438
480
  To filter/search for keys we can use the wildcard `*`. If we have a unique digest `'uniquejobs:9e9b5ce5d423d3ea470977004b50ff84` we can search for it by enter `*ff84` and it should return all digests that end with `ff84`.
439
481
 
440
- #### Show Unique Digests
482
+ ### Reflections (metrics, logging, etc.)
441
483
 
442
- ![Unique Digests](assets/unique_digests_1.png)
484
+ To be able to gather some insights on what is going on inside this gem. I provide a reflection API that can be used.
443
485
 
444
- #### Show keys for digest
486
+ To setup reflections for logging or metrics, use the following API:
445
487
 
446
- ![Unique Digests](assets/unique_digests_2.png)
488
+ ```ruby
447
489
 
448
- ## Communication
490
+ def extract_log_from_job(message, job_hash)
491
+ worker = job_hash['class']
492
+ args = job_hash['args']
493
+ lock_args = job_hash['lock_args']
494
+ queue = job_hash['queue']
495
+ {
496
+ message: message,
497
+ worker: worker,
498
+ args: args,
499
+ lock_args: lock_args,
500
+ queue: queue
501
+ }
502
+ end
449
503
 
450
- There is a [![Join the chat at https://gitter.im/mhenrixon/sidekiq-unique-jobs](https://badges.gitter.im/mhenrixon/sidekiq-unique-jobs.svg)](https://gitter.im/mhenrixon/sidekiq-unique-jobs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) for praise or scorn. This would be a good place to have lengthy discuss or brilliant suggestions or simply just nudge me if I forget about anything.
504
+ SidekiqUniqueJobs.reflect do |on|
505
+ on.lock_failed do |job_hash|
506
+ message = extract_log_from_job('Lock Failed', job_hash)
507
+ Sidekiq.logger.warn(message)
508
+ end
509
+ end
510
+ ```
451
511
 
452
- ## Testing
512
+ #### after_unlock_callback_failed
453
513
 
454
- This has been probably the most confusing part of this gem. People get really confused with how unreliable the unique jobs have been. I there for decided to do what Mike is doing for sidekiq enterprise. Read the section about unique jobs.
514
+ This is called when you have configured a custom callback for when a lock has been released.
455
515
 
456
- [Enterprise unique jobs][]
516
+ #### error
457
517
 
458
- ```ruby
459
- SidekiqUniqueJobs.configure do |config|
460
- config.enabled = !Rails.env.test?
461
- end
462
- ```
518
+ Not in use yet but will be used deep into the stack to provide a means to catch and report errors inside the gem.
463
519
 
464
- If you truly wanted to test the sidekiq client push you could do something like below. Note that it will only work for the jobs that lock when the client pushes the job to redis (UntilExecuted, UntilAndWhileExecuting and UntilExpired).
520
+ #### execution_failed
465
521
 
466
- ```ruby
467
- RSpec.describe Workers::CoolOne do
468
- before do
469
- SidekiqUniqueJobs.config.enabled = false
470
- end
522
+ When the sidekiq processor picks the job of the queue for certain jobs but your job raised an error to the middleware. This will be the reflection. It is probably nothing to worry about. When your worker raises an error, we need to handle some edge cases for until and while executing.
471
523
 
472
- # ... your tests that don't test uniqueness
524
+ #### lock_failed
473
525
 
474
- context 'when Sidekiq::Testing.disabled?' do
475
- before do
476
- Sidekiq::Testing.disable!
477
- Sidekiq.redis(&:flushdb)
478
- end
526
+ If we can't achieve a lock, this will be the reflection. It most likely is nothing to worry about. We just couldn't retrieve a lock in a timely fashion.
479
527
 
480
- after do
481
- Sidekiq.redis(&:flushdb)
482
- end
528
+ The biggest reason for this reflection would be to gather metrics on which workers fail the most at the locking step for example.
483
529
 
484
- it 'prevents duplicate jobs from being scheduled' do
485
- SidekiqUniqueJobs.use_config(enabled: true) do
486
- expect(described_class.perform_in(3600, 1)).not_to eq(nil)
487
- expect(described_class.perform_async(1)).to eq(nil)
488
- end
489
- end
490
- end
530
+ #### locked
531
+
532
+ For when a lock has been successful. Again, mostly useful for metrics I suppose.
533
+
534
+ #### reschedule_failed
535
+
536
+ For when the reschedule strategy failed to reschedule the job.
537
+
538
+ #### rescheduled
539
+
540
+ For when a job was successfully rescheduled
541
+
542
+ #### timeout
543
+
544
+ This is also mostly useful for reporting/metrics purposes. What this reflection does is signal that the job was configured to wait (`lock_timeout` was configured), but we couldn't retrieve a lock even though we waited for some time.
545
+
546
+ ### unlock_failed
547
+
548
+ This is not got, this is worth
549
+
550
+ ### unlocked
551
+
552
+ Also mostly useful for reporting purposes. The job was successfully unlocked.
553
+
554
+ ### unknown_sidekiq_worker
555
+
556
+ The reason this happens is that the server couldn't find a valid sidekiq worker class. Most likely, that worker isn't intended to be processed by this sidekiq server instance.
557
+
558
+ #### Show Locks
559
+
560
+ ![Locks](assets/unique_digests_1.png)
561
+
562
+ #### Show Lock
563
+
564
+ ![Lock](assets/unique_digests_2.png)
565
+
566
+ ## Testing
567
+
568
+ ### Validating Worker Configuration
569
+
570
+ Since v7 it is possible to perform some simple validation against your workers sidekiq_options. What it does is scan for some issues that are known to cause problems in production.
571
+
572
+ Let's take a _bad_ worker:
573
+
574
+ ```ruby
575
+ #app/workers/bad_worker.rb
576
+ class BadWorker
577
+ sidekiq_options lock: :while_executing, on_conflict: :replace
578
+ end
579
+
580
+ #spec/workers/bad_worker_spec.rb
581
+
582
+ require "sidekiq_unique_jobs/testing"
583
+ #OR
584
+ require "sidekiq_unique_jobs/rspec/matchers"
585
+
586
+ RSpec.describe BadWorker do
587
+ specify { expect(described_class).to have_valid_sidekiq_options }
588
+ end
589
+ ```
590
+
591
+ This gives us a helpful error message for a wrongly configured worker:
592
+
593
+ ```bash
594
+ Expected BadWorker to have valid sidekiq options but found the following problems:
595
+ on_server_conflict: :replace is incompatible with the server process
596
+ ```
597
+
598
+ If you are not using RSpec (a lot of people prefer minitest or test unit) you can do something like:
599
+
600
+ ```ruby
601
+ assert SidekiqUniqueJobs.validate_worker!(BadWorker.get_sidekiq_options)
602
+ ```
603
+
604
+ ### Uniqueness
605
+
606
+ This has been probably the most confusing part of this gem. People get really confused with how unreliable the unique jobs have been. I there for decided to do what Mike is doing for sidekiq enterprise. Read the section about unique jobs: [Enterprise unique jobs][]
607
+
608
+ ```ruby
609
+ SidekiqUniqueJobs.configure do |config|
610
+ config.enabled = !Rails.env.test?
611
+ end
612
+ ```
613
+
614
+ If you truly wanted to test the sidekiq client push you could do something like below. Note that it will only work for the jobs that lock when the client pushes the job to redis (UntilExecuted, UntilAndWhileExecuting and UntilExpired).
615
+
616
+ ```ruby
617
+ require "sidekiq_unique_jobs/testing"
618
+
619
+ RSpec.describe Workers::CoolOne do
620
+ before do
621
+ SidekiqUniqueJobs.config.enabled = false
622
+ end
623
+
624
+ # ... your tests that don't test uniqueness
625
+
626
+ context 'when Sidekiq::Testing.disabled?' do
627
+ before do
628
+ Sidekiq::Testing.disable!
629
+ Sidekiq.redis(&:flushdb)
630
+ end
631
+
632
+ after do
633
+ Sidekiq.redis(&:flushdb)
634
+ end
635
+
636
+ it 'prevents duplicate jobs from being scheduled' do
637
+ SidekiqUniqueJobs.use_config(enabled: true) do
638
+ expect(described_class.perform_in(3600, 1)).not_to eq(nil)
639
+ expect(described_class.perform_async(1)).to eq(nil)
640
+ end
641
+ end
642
+ end
643
+ end
644
+ ```
645
+
646
+ It is recommended to leave the uniqueness testing to the gem maintainers. If you care about how the gem is integration tested have a look at the following specs:
647
+
648
+ - [spec/sidekiq_unique_jobs/lock/until_and_while_executing_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/sidekiq_unique_jobs/lock/until_and_while_executing_spec.rb)
649
+ - [spec/sidekiq_unique_jobs/lock/until_executed_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/sidekiq_unique_jobs/lock/until_executed_spec.rb)
650
+ - [spec/sidekiq_unique_jobs/lock/until_expired_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/sidekiq_unique_jobs/lock/until_expired_spec.rb)
651
+ - [spec/sidekiq_unique_jobs/lock/while_executing_reject_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/sidekiq_unique_jobs/lock/while_executing_reject_spec.rb)
652
+ - [spec/sidekiq_unique_jobs/lock/while_executing_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/sidekiq_unique_jobs/lock/while_executing_spec.rb)
653
+
654
+ ## Configuration
655
+
656
+ ### Other Sidekiq gems
657
+
658
+ #### apartment-sidekiq
659
+
660
+ It was reported in [#536](https://github.com/mhenrixon/sidekiq-unique-jobs/issues/536) that the order of the Sidekiq middleware needs to be as follows.
661
+
662
+ ```ruby
663
+ Sidekiq.client_middleware do |chain|
664
+ chain.add Apartment::Sidekiq::Middleware::Client
665
+ chain.add SidekiqUniqueJobs::Middleware::Client
666
+ end
667
+
668
+ Sidekiq.server_middleware do |chain|
669
+ chain.add Apartment::Sidekiq::Middleware::Server
670
+ chain.add SidekiqUniqueJobs::Middleware::Server
671
+ end
672
+ ```
673
+
674
+ The reason being that this gem needs to be configured AFTER the apartment gem or the apartment will not be able to be considered for uniqueness
675
+
676
+ #### sidekiq-global_id
677
+
678
+ It was reported in [#235](https://github.com/mhenrixon/sidekiq-unique-jobs/issues/235) that the order of the Sidekiq middleware needs to be as follows.
679
+
680
+ For a working setup check the following [file](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/myapp/config/sidekiq.rb#L12).
681
+
682
+ ```ruby
683
+ Sidekiq.client_middleware do |chain|
684
+ chain.add Sidekiq::GlobalId::ClientMiddleware
685
+ chain.add SidekiqUniqueJobs::Middleware::Client
686
+ end
687
+
688
+ Sidekiq.server_middleware do |chain|
689
+ chain.add Sidekiq::GlobalId::ServerMiddleware
690
+ chain.add SidekiqUniqueJobs::Middleware::Server
691
+ end
692
+ ```
693
+
694
+ The reason for this is that the global id needs to be set before the unique jobs middleware runs. Otherwise that won't be available for uniqueness.
695
+
696
+ #### sidekiq-status
697
+
698
+ It was reported in [#564](https://github.com/mhenrixon/sidekiq-unique-jobs/issues/564) that the order of the middleware needs to be as follows.
699
+
700
+ ```ruby
701
+ # Thanks to @ArturT for the correction
702
+
703
+ Sidekiq.configure_server do |config|
704
+ config.client_middleware do |chain|
705
+ chain.add SidekiqUniqueJobs::Middleware::Client
706
+ chain.add Sidekiq::Status::ClientMiddleware, expiration: 30.minutes
707
+ end
708
+
709
+ config.server_middleware do |chain|
710
+ chain.add Sidekiq::Status::ServerMiddleware, expiration: 30.minutes
711
+ chain.add SidekiqUniqueJobs::Middleware::Server
712
+ end
713
+
714
+ SidekiqUniqueJobs::Server.configure(config)
715
+ end
716
+
717
+
718
+ Sidekiq.configure_client do |config|
719
+ config.client_middleware do |chain|
720
+ chain.add SidekiqUniqueJobs::Middleware::Client
721
+ chain.add Sidekiq::Status::ClientMiddleware, expiration: 30.minutes
722
+ end
723
+ end
724
+ ```
725
+
726
+ The reason for this is that if a job is duplicated it shouldn't end up with the status middleware at all. Status is just a monitor so to prevent clashes, leftovers and ensure cleanup. The status middleware should run after uniqueness on client and before on server. This will lead to less surprises.
727
+
728
+ ### Global Configuration
729
+
730
+ The gem supports a few different configuration options that might be of interest if you run into some weird issues.
731
+
732
+ Configure SidekiqUniqueJobs in an initializer or the sidekiq initializer on application startup.
733
+
734
+ ```ruby
735
+ SidekiqUniqueJobs.configure do |config|
736
+ config.logger = Sidekiq.logger # default, change at your own discretion
737
+ config.debug_lua = false # Turn on when debugging
738
+ config.lock_info = false # Turn on when debugging
739
+ config.lock_ttl = 600 # Expire locks after 10 minutes
740
+ config.lock_timeout = nil # turn off lock timeout
741
+ config.max_history = 0 # Turn on when debugging
742
+ config.reaper = :ruby # :ruby, :lua or :none/nil
743
+ config.reaper_count = 1000 # Stop reaping after this many keys
744
+ config.reaper_interval = 600 # Reap orphans every 10 minutes
745
+ config.reaper_timeout = 150 # Timeout reaper after 2.5 minutes
746
+ end
747
+ ```
748
+
749
+ #### debug_lua
750
+
751
+ ```ruby
752
+ SidekiqUniqueJobs.config.debug_lua #=> false
753
+ ```
754
+
755
+ Turning on debug_lua will allow the lua scripts to output debug information about what the lua scripts do. It will log all redis commands that are executed and also some helpful messages about what is going on inside the lua script.
756
+
757
+ #### lock_timeout
758
+
759
+ ```ruby
760
+ SidekiqUniqueJobs.config.lock_timeout #=> 0
761
+ ```
762
+
763
+ Set a global lock_timeout to use for all jobs that don't otherwise specify a lock_timeout.
764
+
765
+ Lock timeout decides how long to wait for acquiring the lock. A value of nil means to wait indefinitely for a lock resource to become available.
766
+
767
+ #### lock_ttl
768
+
769
+ ```ruby
770
+ SidekiqUniqueJobs.config.lock_ttl #=> nil
771
+ ```
772
+
773
+ Set a global lock_ttl to use for all jobs that don't otherwise specify a lock_ttl.
774
+
775
+ Lock TTL decides how long to wait at most before considering a lock to be expired and making it possible to reuse that lock.
776
+
777
+ #### enabled
778
+
779
+ ```ruby
780
+ SidekiqUniqueJobs.config.enabled #=> true
781
+ ```
782
+
783
+ Globally turn the locking mechanism on or off.
784
+
785
+ #### logger
786
+
787
+ ```ruby
788
+ SidekiqUniqueJobs.config.logger #=> #<Sidekiq::Logger:0x00007fdc1f96d180>
789
+ ```
790
+
791
+ By default this gem piggybacks on the Sidekiq logger. It is not recommended to change this as the gem uses some features in the Sidekiq logger and you might run into problems. If you need a different logger and you do run into problems then get in touch and we'll see what we can do about it.
792
+
793
+ #### max_history
794
+
795
+ ```ruby
796
+ SidekiqUniqueJobs.config.max_history #=> 1_000
797
+ ```
798
+
799
+ The max_history setting can be used to tweak the number of changelogs generated. It can also be completely turned off if performance suffers or if you are just not interested in using the changelog.
800
+
801
+ This is a log that can be accessed by a lock to see what happened for that lock. Any items after the configured `max_history` will be automatically deleted as new items are added.
802
+
803
+ #### reaper
804
+
805
+ ```ruby
806
+ SidekiqUniqueJobs.config.reaper #=> :ruby
807
+ ```
808
+
809
+ If using the orphans cleanup process it is critical to be aware of the following. The `:ruby` job is much slower but the `:lua` job locks redis while executing. While doing intense processing it is best to avoid locking redis with a lua script. There for the batch size (controlled by the `reaper_count` setting) needs to be reduced.
810
+
811
+ In my benchmarks deleting 1000 orphaned locks with lua performs around 65% faster than deleting 1000 keys in ruby.
812
+
813
+ On the other hand if I increase it to 10 000 orphaned locks per cleanup (`reaper_count: 10_0000`) then redis starts throwing:
814
+
815
+ > BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. (Redis::CommandError)
816
+
817
+ If you want to disable the reaper set it to `:none`, `nil` or `false`. Actually, any value that isn't `:ruby` or `:lua` will disable the reaping.
818
+
819
+ ```ruby
820
+ SidekiqUniqueJobs.config.reaper = :none
821
+ SidekiqUniqueJobs.config.reaper = nil
822
+ SidekiqUniqueJobs.config.reaper = false
823
+ ```
824
+
825
+ #### reaper_count
826
+
827
+ ```ruby
828
+ SidekiqUniqueJobs.config.reaper_count #=> 1_000
829
+ ```
830
+
831
+ The reaper_count setting configures how many orphans at a time will be cleaned up by the orphan cleanup job. This might have to be tweaked depending on which orphan job is running.
832
+
833
+ #### reaper_interval
834
+
835
+ ```ruby
836
+ SidekiqUniqueJobs.config.reaper_interval #=> 600
837
+ ```
838
+
839
+ The number of seconds between reaping.
840
+
841
+ #### reaper_timeout
842
+
843
+ ```ruby
844
+ SidekiqUniqueJobs.config.reaper_timeout #=> 10
845
+ ```
846
+
847
+ The number of seconds to wait for the reaper to finish before raising a TimeoutError. This is done to ensure that the next time we reap isn't getting stuck due to the previous process already running.
848
+
849
+ #### lock_prefix
850
+
851
+ ```ruby
852
+ SidekiqUniqueJobs.config.lock_prefix #=> "uniquejobs"
853
+ ```
854
+
855
+ Use if you want a different key prefix for the keys in redis.
856
+
857
+ ### lock_info
858
+
859
+ ```ruby
860
+ SidekiqUniqueJobs.config.lock_info #=> false
861
+ ```
862
+
863
+ Using lock info will create an additional key for the lock with a json object containing information about the lock. This will be presented in the web interface and might help track down why some jobs are getting stuck.
864
+
865
+ ### Worker Configuration
866
+
867
+ #### lock_info
868
+
869
+ Lock info gathers information about a specific lock. It collects things like which `lock_args` where used to compute the `lock_digest` that is used for maintaining uniqueness.
870
+
871
+ ```ruby
872
+ sidekiq_options lock_info: false # this is the default, set to true to turn on
873
+ ```
874
+
875
+ #### lock_prefix
876
+
877
+ Use if you want a different key prefix for the keys in redis.
878
+
879
+ ```ruby
880
+ sidekiq_options lock_prefix: "uniquejobs" # this is the default value
881
+ ```
882
+
883
+ #### lock_ttl
884
+
885
+ Lock TTL decides how long to wait at most before considering a lock to be expired and making it possible to reuse that lock.
886
+
887
+ Starting from `v7` the expiration will take place when the job is pushed to the queue.
888
+
889
+ ```ruby
890
+ sidekiq_options lock_ttl: nil # default - don't expire keys
891
+ sidekiq_options lock_ttl: 20.days.to_i # expire this lock in 20 days
892
+ ```
893
+
894
+ #### lock_timeout
895
+
896
+ This is the timeout (how long to wait) when creating the lock. By default we don't use a timeout so we won't wait for the lock to be created. If you want it is possible to set this like below.
897
+
898
+ ```ruby
899
+ sidekiq_options lock_timeout: 0 # default - don't wait at all
900
+ sidekiq_options lock_timeout: 5 # wait 5 seconds
901
+ sidekiq_options lock_timeout: nil # lock indefinitely, this process won't continue until it gets a lock. VERY DANGEROUS!!
902
+ ```
903
+
904
+ #### unique_across_queues
905
+
906
+ This configuration option is slightly misleading. It doesn't disregard the queue on other jobs. Just on itself, this means that a worker that might schedule jobs into multiple queues will be able to have uniqueness enforced on all queues it is pushed to.
907
+
908
+ This is mainly intended for `Worker.set(queue: :another).perform_async`.
909
+
910
+ ```ruby
911
+ class Worker
912
+ include Sidekiq::Worker
913
+
914
+ sidekiq_options unique_across_queues: true, queue: 'default'
915
+
916
+ def perform(args); end
491
917
  end
492
918
  ```
493
919
 
494
- I would strongly suggest you let this gem test uniqueness. If you care about how the gem is integration tested have a look at the following specs:
920
+ Now if you push override the queue with `Worker.set(queue: 'another').perform_async(1)` it will still be considered unique when compared to `Worker.perform_async(1)` (that was actually pushed to the queue `default`).
921
+
922
+ #### unique_across_workers
923
+
924
+ This configuration option is slightly misleading. It doesn't disregard the worker class on other jobs. Just on itself, this means that the worker class won't be used for generating the unique digest. The only way this option really makes sense is when you want to have uniqueness between two different worker classes.
925
+
926
+ ```ruby
927
+ class WorkerOne
928
+ include Sidekiq::Worker
929
+
930
+ sidekiq_options unique_across_workers: true, queue: 'default'
495
931
 
496
- - [spec/integration/sidekiq_unique_jobs/lock/until_and_while_executing_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/integration/sidekiq_unique_jobs/lock/until_and_while_executing_spec.rb)
497
- - [spec/integration/sidekiq_unique_jobs/lock/until_executed_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/integration/sidekiq_unique_jobs/lock/until_executed_spec.rb)
498
- - [spec/integration/sidekiq_unique_jobs/lock/until_expired_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/integration/sidekiq_unique_jobs/lock/until_expired_spec.rb)
499
- - [spec/integration/sidekiq_unique_jobs/lock/while_executing_reject_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/integration/sidekiq_unique_jobs/lock/while_executing_reject_spec.rb)
500
- - [spec/integration/sidekiq_unique_jobs/lock/while_executing_spec.rb](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/spec/integration/sidekiq_unique_jobs/lock/while_executing_spec.rb)
932
+ def perform(args); end
933
+ end
934
+
935
+ class WorkerTwo
936
+ include Sidekiq::Worker
937
+
938
+ sidekiq_options unique_across_workers: true, queue: 'default'
939
+
940
+ def perform(args); end
941
+ end
942
+
943
+
944
+ WorkerOne.perform_async(1)
945
+ # => 'the jobs unique id'
946
+
947
+ WorkerTwo.perform_async(1)
948
+ # => nil because WorkerOne just stole the lock
949
+ ```
950
+
951
+ ### Finer Control over Uniqueness
952
+
953
+ Sometimes it is desired to have a finer control over which arguments are used in determining uniqueness of the job, and others may be _transient_. For this use-case, you need to define either a `lock_args` method, or a ruby proc.
954
+
955
+ *NOTE:* The lock_args method need to return an array of values to use for uniqueness check.
956
+
957
+ *NOTE:* The arguments passed to the proc or the method is always an array. If your method takes a single array as argument the value of args will be `[[...]]`.
958
+
959
+ The method or the proc can return a modified version of args without the transient arguments included, as shown below:
960
+
961
+ ```ruby
962
+ class UniqueJobWithFilterMethod
963
+ include Sidekiq::Worker
964
+ sidekiq_options lock: :until_and_while_executing,
965
+ lock_args_method: :lock_args # this is default and will be used if such a method is defined
966
+
967
+ def self.lock_args(args)
968
+ [ args[0], args[2][:type] ]
969
+ end
970
+
971
+ ...
972
+
973
+ end
974
+
975
+ class UniqueJobWithFilterProc
976
+ include Sidekiq::Worker
977
+ sidekiq_options lock: :until_executed,
978
+ lock_args_method: ->(args) { [ args.first ] }
979
+
980
+ ...
981
+
982
+ end
983
+ ```
984
+
985
+ It is possible to ensure different types of unique args based on context. I can't vouch for the below example but see [#203](https://github.com/mhenrixon/sidekiq-unique-jobs/issues/203) for the discussion.
986
+
987
+ ```ruby
988
+ class UniqueJobWithFilterMethod
989
+ include Sidekiq::Worker
990
+ sidekiq_options lock: :until_and_while_executing, lock_args_method: :lock_args
991
+
992
+ def self.lock_args(args)
993
+ if Sidekiq::ProcessSet.new.size > 1
994
+ # sidekiq runtime; uniqueness for the object (first arg)
995
+ args.first
996
+ else
997
+ # queuing from the app; uniqueness for all params
998
+ args
999
+ end
1000
+ end
1001
+ end
1002
+ ```
1003
+
1004
+ ### After Unlock Callback
1005
+
1006
+ If you need to perform any additional work after the lock has been released you can provide an `#after_unlock` instance method. The method will be called when the lock has been unlocked. Most times this means after yield but there are two exceptions to that.
1007
+
1008
+ **Exception 1:** UntilExecuting unlocks and uses callback before yielding.
1009
+ **Exception 2:** UntilExpired expires eventually, no after_unlock hook is called.
1010
+
1011
+ **NOTE:** _It is also possible to write this code as a class method._
1012
+
1013
+ ```ruby
1014
+ class UniqueJobWithFilterMethod
1015
+ include Sidekiq::Worker
1016
+ sidekiq_options lock: :while_executing,
1017
+
1018
+ def self.after_unlock
1019
+ # block has yielded and lock is released
1020
+ end
1021
+
1022
+ def after_unlock
1023
+ # block has yielded and lock is released
1024
+ end
1025
+ ...
1026
+ end.
1027
+ ```
1028
+
1029
+ ## Communication
1030
+
1031
+ There is a [![Join the chat at https://gitter.im/mhenrixon/sidekiq-unique-jobs](https://badges.gitter.im/mhenrixon/sidekiq-unique-jobs.svg)](https://gitter.im/mhenrixon/sidekiq-unique-jobs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) for praise or scorn. This would be a good place to have lengthy discuss or brilliant suggestions or simply just nudge me if I forget about anything.
501
1032
 
502
1033
  ## Contributing
503
1034
 
@@ -511,9 +1042,8 @@ I would strongly suggest you let this gem test uniqueness. If you care about how
511
1042
 
512
1043
  You can find a list of contributors over on [Contributors][]
513
1044
 
514
- [v5.0.10]: https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v5.0.10.
515
- [v4.0.18]: https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v4.0.18
516
- [Sidekiq requirements]: https://github.com/mperham/sidekiq#requirements
517
1045
  [Enterprise unique jobs]: https://www.dailydrip.com/topics/sidekiq/drips/sidekiq-enterprise-unique-jobs
518
1046
  [Contributors]: https://github.com/mhenrixon/sidekiq-unique-jobs/graphs/contributors
519
- [Paypal link https://paypal.me/mhenrixon]: https://paypal.me/mhenrixon
1047
+ [v4.0.18]: https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v4.0.18
1048
+ [v5.0.10]: https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v5.0.10.
1049
+ [Sidekiq requirements]: https://github.com/mperham/sidekiq#requirements