sidekiq-unique-jobs 6.0.23 → 7.1.12

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 +922 -41
  3. data/README.md +821 -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 +314 -0
  8. data/lib/sidekiq_unique_jobs/connection.rb +6 -5
  9. data/lib/sidekiq_unique_jobs/constants.rb +45 -24
  10. data/lib/sidekiq_unique_jobs/core_ext.rb +80 -0
  11. data/lib/sidekiq_unique_jobs/deprecation.rb +65 -0
  12. data/lib/sidekiq_unique_jobs/digests.rb +70 -102
  13. data/lib/sidekiq_unique_jobs/exceptions.rb +88 -12
  14. data/lib/sidekiq_unique_jobs/job.rb +41 -12
  15. data/lib/sidekiq_unique_jobs/json.rb +47 -0
  16. data/lib/sidekiq_unique_jobs/key.rb +93 -0
  17. data/lib/sidekiq_unique_jobs/lock/base_lock.rb +111 -82
  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 +40 -15
  21. data/lib/sidekiq_unique_jobs/lock/until_executed.rb +25 -7
  22. data/lib/sidekiq_unique_jobs/lock/until_executing.rb +22 -2
  23. data/lib/sidekiq_unique_jobs/lock/until_expired.rb +26 -16
  24. data/lib/sidekiq_unique_jobs/lock/validator.rb +96 -0
  25. data/lib/sidekiq_unique_jobs/lock/while_executing.rb +23 -12
  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 +188 -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 +102 -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 -31
  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 +35 -32
  70. data/lib/sidekiq_unique_jobs/orphans/lua_reaper.rb +29 -0
  71. data/lib/sidekiq_unique_jobs/orphans/manager.rb +248 -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 +231 -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 +26 -0
  85. data/lib/sidekiq_unique_jobs/reflections.rb +79 -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 +61 -0
  91. data/lib/sidekiq_unique_jobs/sidekiq_unique_ext.rb +114 -65
  92. data/lib/sidekiq_unique_jobs/sidekiq_unique_jobs.rb +241 -35
  93. data/lib/sidekiq_unique_jobs/sidekiq_worker_methods.rb +18 -16
  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 +15 -15
  110. metadata +124 -184
  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,215 @@
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 `main` 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
+ - [v7.0.12](https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v7.0.12)
100
+ - [v6.0.25](https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v6.0.25)
101
+ - [v5.0.10](https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v5.0.10)
102
+ - [v4.0.18](https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v4.0.18)
60
103
 
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.
104
+ ## Usage
74
105
 
75
- ## Installation
106
+ ### Installation
76
107
 
77
108
  Add this line to your application's Gemfile:
78
109
 
79
- ```
110
+ ```ruby
80
111
  gem 'sidekiq-unique-jobs'
81
112
  ```
82
113
 
83
114
  And then execute:
84
115
 
85
- ```
116
+ ```bash
86
117
  bundle
87
118
  ```
88
119
 
89
- Or install it yourself as:
90
-
91
- ```
92
- gem install sidekiq-unique-jobs
93
- ```
94
-
95
- ## Support Me
120
+ ### Add the middleware
96
121
 
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.
122
+ 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
123
 
99
- ## General Information
124
+ *NOTE* if you want to use the reaper you also need to configure the server middleware.
100
125
 
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.
126
+ [A full example](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/myapp/config/initializers/sidekiq.rb#L12)
102
127
 
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.
128
+ ```ruby
129
+ Sidekiq.configure_server do |config|
130
+ config.redis = { url: ENV["REDIS_URL"], driver: :hiredis }
104
131
 
105
- ## Options
132
+ config.client_middleware do |chain|
133
+ chain.add SidekiqUniqueJobs::Middleware::Client
134
+ end
106
135
 
107
- ### Lock Expiration
136
+ config.server_middleware do |chain|
137
+ chain.add SidekiqUniqueJobs::Middleware::Server
138
+ end
108
139
 
109
- Lock expiration is used for two things. For the `UntilExpired` job releases the lock upon expiry. This is done from the client.
140
+ SidekiqUniqueJobs::Server.configure(config)
141
+ end
110
142
 
111
- Since v6.0.11 the other locks will expire after the server is done processing.
143
+ Sidekiq.configure_client do |config|
144
+ config.redis = { url: ENV["REDIS_URL"], driver: :hiredis }
112
145
 
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
146
+ config.client_middleware do |chain|
147
+ chain.add SidekiqUniqueJobs::Middleware::Client
148
+ end
149
+ end
116
150
  ```
117
151
 
118
- ### Lock Timeout
152
+ ### Your first worker
119
153
 
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.
154
+ 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
155
 
122
156
  ```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.
157
+ # frozen_string_literal: true
131
158
 
132
- ```ruby
133
- class Worker
159
+ class UntilExecutedWorker
134
160
  include Sidekiq::Worker
135
161
 
136
- sidekiq_options unique_across_queues: true, queue: 'default'
162
+ sidekiq_options queue: :until_executed,
163
+ lock: :until_executed
137
164
 
138
- def perform(args); end
165
+ def perform
166
+ logger.info("cowboy")
167
+ sleep(1) # hardcore processing
168
+ logger.info("beebop")
169
+ end
139
170
  end
140
171
  ```
141
172
 
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
173
+ You can read more about the worker configuration in [Worker Configuration](#worker-configuration) below.
145
174
 
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.
175
+ ## Requirements
147
176
 
148
- ```ruby
149
- class WorkerOne
150
- include Sidekiq::Worker
177
+ - Sidekiq `>= 5.0` (`>= 5.2` recommended)
178
+ - Ruby:
179
+ - MRI `>= 2.5` (`>= 2.6` recommended)
180
+ - JRuby `>= 9.0` (`>= 9.2` recommended)
181
+ - Truffleruby
182
+ - Redis Server `>= 3.2` (`>= 5.0` recommended)
183
+ - [ActiveJob officially not supported][48]
184
+ - [redis-namespace officially not supported][49]
151
185
 
152
- sidekiq_options unique_across_workers: true, queue: 'default'
186
+ See [Sidekiq requirements][24] for detailed requirements of Sidekiq itself (be sure to check the right sidekiq version).
153
187
 
154
- def perform(args); end
155
- end
188
+ ## Locks
156
189
 
157
- class WorkerTwo
158
- include Sidekiq::Worker
190
+ ### Until Executing
159
191
 
160
- sidekiq_options unique_across_workers: true, queue: 'default'
192
+ 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
193
 
162
- def perform(args); end
163
- end
194
+ #### Example worker
164
195
 
196
+ ```ruby
197
+ class UntilExecuting
198
+ include Sidekiq::Workers
165
199
 
166
- WorkerOne.perform_async(1)
167
- # => 'the jobs unique id'
200
+ sidekiq_options lock: :until_executing
168
201
 
169
- WorkerTwo.perform_async(1)
170
- # => nil because WorkerOne just stole the lock
202
+ def perform(id)
203
+ # Do work
204
+ end
205
+ end
171
206
  ```
172
207
 
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
208
  **NOTE** this is probably not so good for jobs that shouldn't be running simultaneously (aka slow jobs).
180
209
 
181
- ```ruby
182
- sidekiq_options lock: :until_executing
183
- ```
210
+ 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
211
 
185
212
  ### Until Executed
186
213
 
187
- Locks from when the client pushes the job to the queue. Will be unlocked when the server has successfully processed the job.
214
+ 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.
215
+
216
+ #### Example worker
188
217
 
189
218
  ```ruby
190
- sidekiq_options lock: :until_executed
219
+ class UntilExecuted
220
+ include Sidekiq::Workers
221
+
222
+ sidekiq_options lock: :until_executed
223
+
224
+ def perform(id)
225
+ # Do work
226
+ end
227
+ end
191
228
  ```
192
229
 
193
- ### Until Timeout
230
+ ### Until Expired
194
231
 
195
- Locks from when the client pushes the job to the queue. Will be unlocked when the specified timeout has been reached.
232
+ 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.
233
+
234
+ #### Example worker
196
235
 
197
236
  ```ruby
198
- sidekiq_options lock: :until_expired
237
+ class UntilExpired
238
+ include Sidekiq::Workers
239
+
240
+ sidekiq_options lock: :until_expired, lock_ttl: 1.day
241
+
242
+ def perform
243
+ # Do work
244
+ end
245
+ end
199
246
  ```
200
247
 
201
- ### Unique Until And While Executing
248
+ ### Until And While Executing
249
+
250
+ 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
251
 
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.
252
+ #### Example worker
204
253
 
205
254
  ```ruby
206
- sidekiq_options lock: :until_and_while_executing
255
+ class UntilAndWhileExecutingWorker
256
+ include Sidekiq::Workers
257
+
258
+ sidekiq_options lock: :until_and_while_executing,
259
+ lock_timeout: 2,
260
+ on_conflict: {
261
+ client: :log,
262
+ server: :raise
263
+ }
264
+ def perform(id)
265
+ # Do work
266
+ end
267
+ end
207
268
  ```
208
269
 
209
270
  ### While Executing
210
271
 
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.
272
+ These 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
273
 
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.
274
+ #### Example worker
214
275
 
215
276
  ```ruby
216
- sidekiq_options lock: :while_executing, lock_timeout: nil
277
+ class WhileExecutingWorker
278
+ include Sidekiq::Workers
279
+
280
+ sidekiq_options lock: :while_executing,
281
+ lock_timeout: 2,
282
+ on_conflict: {
283
+ server: :raise
284
+ }
285
+ def perform(id)
286
+ # Do work
287
+ end
288
+ end
217
289
  ```
218
290
 
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`.
291
+ **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.
292
+
293
+ **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.
294
+
295
+ 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
296
 
221
297
  In the console you should see something like:
222
298
 
223
- ```
299
+ ```bash
224
300
  0:32:24 worker.1 | 2017-04-23T08:32:24.955Z 84404 TID-ougq4thko WhileExecutingWorker JID-400ec51c9523f41cd4a35058 INFO: start
225
301
  10:32:24 worker.1 | 2017-04-23T08:32:24.956Z 84404 TID-ougq8csew WhileExecutingWorker JID-8d6d9168368eedaed7f75763 INFO: start
226
302
  10:32:24 worker.1 | 2017-04-23T08:32:24.957Z 84404 TID-ougq8crt8 WhileExecutingWorker JID-affcd079094c9b26e8b9ba60 INFO: start
@@ -235,183 +311,156 @@ In the console you should see something like:
235
311
  10:33:04 worker.1 | 2017-04-23T08:33:04.973Z 84404 TID-ougq8cs8s WhileExecutingWorker JID-9e197460c067b22eb1b5d07f INFO: done: 40.014 sec
236
312
  ```
237
313
 
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
314
+ ### Custom Locks
243
315
 
244
- ### Log
316
+ You may need to define some custom lock. You can define it in one project folder:
245
317
 
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.
318
+ ```ruby
319
+ # lib/locks/my_custom_lock.rb
320
+ module Locks
321
+ class MyCustomLock < SidekiqUniqueJobs::Lock::BaseLock
322
+ def execute
323
+ # Do something ...
324
+ end
325
+ end
326
+ end
327
+ ```
247
328
 
248
- `sidekiq_options lock: :until_executed, on_conflict: :log`
329
+ You can refer on all the locks defined in `lib/sidekiq_unique_jobs/lock/*.rb`.
249
330
 
250
- ### Raise
331
+ In order to make it available, you should call in your project startup:
251
332
 
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.
333
+ (For rails application config/initializers/sidekiq_unique_jobs.rb or other projects, wherever you prefer)
253
334
 
254
- `sidekiq_options lock: :while_executing, on_conflict: :raise, retry: 10`
335
+ ```ruby
336
+ SidekiqUniqueJobs.configure do |config|
337
+ config.add_lock :my_custom_lock, Locks::MyCustomLock
338
+ end
339
+ ```
255
340
 
256
- ### Reject
341
+ And then you can use it in the jobs definition:
257
342
 
258
- This strategy is intended to be used with `WhileExecuting` and will push the job to the dead queue on conflict.
343
+ `sidekiq_options lock: :my_custom_lock, on_conflict: :log`
259
344
 
260
- `sidekiq_options lock: :while_executing, on_conflict: :reject`
345
+ Please not that if you try to override a default lock, an `ArgumentError` will be raised.
261
346
 
262
- ### Replace
347
+ ## Conflict Strategy
263
348
 
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.
349
+ 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
350
 
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.
351
+ 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
352
 
271
- `sidekiq_options lock: :until_executed, on_conflict: :replace`
353
+ It is possible for locks to have different conflict strategy for the client and server. This is useful for `:until_and_while_executing`.
272
354
 
273
- ### Reschedule
355
+ ```ruby
356
+ sidekiq_options lock: :until_and_while_executing,
357
+ on_conflict: { client: :log, server: :reject }
358
+ ```
274
359
 
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.
360
+ ### log
276
361
 
277
- `sidekiq_options lock: :while_executing, on_conflict: :reschedule`
362
+ ```ruby
363
+ sidekiq_options on_conflict: :log
364
+ ```
278
365
 
279
- ## Usage
366
+ 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
367
 
281
- All that is required is that you specifically set the sidekiq option for _unique_ to a valid value like below:
368
+ ### raise
282
369
 
283
370
  ```ruby
284
- sidekiq_options lock: :while_executing
371
+ sidekiq_options on_conflict: :raise
285
372
  ```
286
373
 
287
- Requiring the gem in your gemfile should be sufficient to enable unique jobs.
374
+ 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
375
 
289
- ### Finer Control over Uniqueness
376
+ ### reject
290
377
 
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.
378
+ ```ruby
379
+ sidekiq_options on_conflict: :reject
380
+ ```
292
381
 
293
- The unique_args method need to return an array of values to use for uniqueness check.
382
+ This strategy is intended to be used with `WhileExecuting` and will push the job to the dead queue on conflict.
294
383
 
295
- The method or the proc can return a modified version of args without the transient arguments included, as shown below:
384
+ ### replace
296
385
 
297
386
  ```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
387
+ sidekiq_options on_conflict: :replace
388
+ ```
302
389
 
303
- def self.unique_args(args)
304
- [ args[0], args[2][:type] ]
305
- end
390
+ This strategy is intended to be used with client locks like `UntilExecuted`.
391
+ It will delete any existing job for these arguments from retry, schedule and
392
+ queue and retry the lock again.
306
393
 
307
- ...
394
+ This is slightly dangerous and should probably only be used for jobs that are
395
+ always scheduled in the future. Currently only attempting to retry one time.
308
396
 
309
- end
397
+ ### Reschedule
310
398
 
311
- class UniqueJobWithFilterProc
312
- include Sidekiq::Worker
313
- sidekiq_options lock: :until_executed,
314
- unique_args: ->(args) { [ args.first ] }
399
+ ```ruby
400
+ sidekiq_options on_conflict: :reschedule
401
+ ```
315
402
 
316
- ...
403
+ 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
404
 
318
- end
319
- ```
405
+ ### Custom Strategies
320
406
 
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.
407
+ You may need to define some custom strategy. You can define it in one project folder:
322
408
 
323
409
  ```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
410
+ # lib/strategies/my_custom_strategy.rb
411
+ module Strategies
412
+ class MyCustomStrategy < SidekiqUniqueJobs::OnConflict::Strategy
413
+ def call
414
+ # Do something ...
335
415
  end
336
416
  end
337
417
  end
338
418
  ```
339
419
 
340
- ### After Unlock Callback
420
+ You can refer to all the strategies defined in `lib/sidekiq_unique_jobs/on_conflict`.
341
421
 
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.
422
+ In order to make it available, you should call in your project startup:
343
423
 
344
- **Exception 1:** UntilExecuting unlocks and calls back before yielding.
345
- **Exception 2:** UntilExpired expires eventually, no after_unlock hook is called.
424
+ (For rails application config/initializers/sidekiq_unique_jobs.rb for other projects, wherever you prefer)
346
425
 
347
426
  ```ruby
348
- class UniqueJobWithFilterMethod
349
- include Sidekiq::Worker
350
- sidekiq_options lock: :while_executing,
351
-
352
- def after_unlock
353
- # block has yielded and lock is released
354
- end
355
- ...
356
- end.
427
+ SidekiqUniqueJobs.configure do |config|
428
+ config.add_strategy :my_custom_strategy, Strategies::MyCustomStrategy
429
+ end
357
430
  ```
358
431
 
359
- ### Logging
360
-
361
- 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
432
+ And then you can use it in the jobs definition:
362
433
 
363
434
  ```ruby
364
- class UniqueJobWithFilterMethod
365
- include Sidekiq::Worker
366
- sidekiq_options lock: :while_executing,
367
- log_duplicate_payload: true
368
-
369
- ...
370
-
371
- end
435
+ sidekiq_options lock: :while_executing, on_conflict: :my_custom_strategy
372
436
  ```
373
437
 
374
- ### Cleanup Dead Locks
438
+ Please not that if you try to override a default lock, an `ArgumentError` will be raised.
375
439
 
376
- For sidekiq versions before 5.1 a `sidekiq_retries_exhausted` block is required per worker class. This is deprecated in Sidekiq 6.0
440
+ ### 3 Cleanup Dead Locks
441
+
442
+ For sidekiq versions < 5.1 a `sidekiq_retries_exhausted` block is required per worker class. This is deprecated in Sidekiq 6.0
377
443
 
378
444
  ```ruby
379
445
  class MyWorker
380
446
  sidekiq_retries_exhausted do |msg, _ex|
381
- SidekiqUniqueJobs::Digests.delete_by_digest(msg['unique_digest']) if msg['unique_digest']
447
+ digest = msg['lock_digest']
448
+ SidekiqUniqueJobs::Digests.new.delete_by_digest(digest) if digest
382
449
  end
383
450
  end
384
451
  ```
385
452
 
386
- Starting in v5.1, Sidekiq can also fire a global callback when a job dies:
453
+ 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.
387
454
 
388
455
  ```ruby
389
- # this goes in your initializer
390
456
  Sidekiq.configure_server do |config|
391
457
  config.death_handlers << ->(job, _ex) do
392
- SidekiqUniqueJobs::Digests.delete_by_digest(job['unique_digest']) if job['unique_digest']
458
+ digest = job['lock_digest']
459
+ SidekiqUniqueJobs::Digests.new.delete_by_digest(digest) if digest
393
460
  end
394
461
  end
395
462
  ```
396
463
 
397
- ### Other Sidekiq gems
398
-
399
- #### sidekiq-global_id
400
-
401
- 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.
402
-
403
- ```ruby
404
- Sidekiq.client_middleware do |chain|
405
- chain.add Sidekiq::GlobalId::ClientMiddleware
406
- chain.add SidekiqUniqueJobs::Client::Middleware
407
- end
408
-
409
- Sidekiq.server_middleware do |chain|
410
- chain.add SidekiqUniqueJobs::Server::Middleware
411
- chain.add Sidekiq::GlobalId::ServerMiddleware
412
- end
413
- ```
414
-
415
464
  ## Debugging
416
465
 
417
466
  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.
@@ -421,7 +470,7 @@ There are several ways of removing keys that are stuck. The prefered way is by u
421
470
  To use the web extension you need to require it in your routes.
422
471
 
423
472
  ```ruby
424
- # app/config/routes.rb
473
+ #app/config/routes.rb
425
474
  require 'sidekiq_unique_jobs/web'
426
475
  mount Sidekiq::Web, at: '/sidekiq'
427
476
  ```
@@ -431,73 +480,562 @@ already does this.
431
480
 
432
481
  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`.
433
482
 
434
- #### Show Unique Digests
483
+ ### Reflections (metrics, logging, etc.)
435
484
 
436
- ![Unique Digests](assets/unique_digests_1.png)
485
+ To be able to gather some insights on what is going on inside this gem. I provide a reflection API that can be used.
437
486
 
438
- #### Show keys for digest
487
+ To setup reflections for logging or metrics, use the following API:
439
488
 
440
- ![Unique Digests](assets/unique_digests_2.png)
489
+ ```ruby
441
490
 
442
- ## Communication
491
+ def extract_log_from_job(message, job_hash)
492
+ worker = job_hash['class']
493
+ args = job_hash['args']
494
+ lock_args = job_hash['lock_args']
495
+ queue = job_hash['queue']
496
+ {
497
+ message: message,
498
+ worker: worker,
499
+ args: args,
500
+ lock_args: lock_args,
501
+ queue: queue
502
+ }
503
+ end
443
504
 
444
- 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.
505
+ SidekiqUniqueJobs.reflect do |on|
506
+ on.lock_failed do |job_hash|
507
+ message = extract_log_from_job('Lock Failed', job_hash)
508
+ Sidekiq.logger.warn(message)
509
+ end
510
+ end
511
+ ```
445
512
 
446
- ## Testing
513
+ #### after_unlock_callback_failed
447
514
 
448
- 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.
515
+ This is called when you have configured a custom callback for when a lock has been released.
449
516
 
450
- [Enterprise unique jobs][]
517
+ #### error
451
518
 
452
- ```ruby
453
- SidekiqUniqueJobs.configure do |config|
454
- config.enabled = !Rails.env.test?
455
- end
456
- ```
519
+ Not in use yet but will be used deep into the stack to provide a means to catch and report errors inside the gem.
457
520
 
458
- 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).
521
+ #### execution_failed
459
522
 
460
- ```ruby
461
- RSpec.describe Workers::CoolOne do
462
- before do
463
- SidekiqUniqueJobs.config.enabled = false
464
- end
523
+ 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.
465
524
 
466
- # ... your tests that don't test uniqueness
525
+ #### lock_failed
467
526
 
468
- context 'when Sidekiq::Testing.disabled?' do
469
- before do
470
- Sidekiq::Testing.disable!
471
- Sidekiq.redis(&:flushdb)
472
- end
527
+ 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.
473
528
 
474
- after do
475
- Sidekiq.redis(&:flushdb)
476
- end
529
+ The biggest reason for this reflection would be to gather metrics on which workers fail the most at the locking step for example.
477
530
 
478
- it 'prevents duplicate jobs from being scheduled' do
479
- SidekiqUniqueJobs.use_config(enabled: true) do
480
- expect(described_class.perform_in(3600, 1)).not_to eq(nil)
481
- expect(described_class.perform_async(1)).to eq(nil)
482
- end
483
- end
484
- end
485
- end
486
- ```
531
+ #### locked
487
532
 
488
- 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:
533
+ For when a lock has been successful. Again, mostly useful for metrics I suppose.
489
534
 
490
- - [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)
491
- - [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)
492
- - [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)
493
- - [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)
494
- - [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)
535
+ #### reschedule_failed
495
536
 
496
- ## Contributing
537
+ For when the reschedule strategy failed to reschedule the job.
497
538
 
498
- 1. Fork it
499
- 1. Create your feature branch (`git checkout -b my-new-feature`)
500
- 1. Commit your changes (`git commit -am 'Add some feature'`)
539
+ #### rescheduled
540
+
541
+ For when a job was successfully rescheduled
542
+
543
+ #### timeout
544
+
545
+ 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.
546
+
547
+ ### unlock_failed
548
+
549
+ This is not got, this is worth
550
+
551
+ ### unlocked
552
+
553
+ Also mostly useful for reporting purposes. The job was successfully unlocked.
554
+
555
+ ### unknown_sidekiq_worker
556
+
557
+ 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.
558
+
559
+ #### Show Locks
560
+
561
+ ![Locks](assets/unique_digests_1.png)
562
+
563
+ #### Show Lock
564
+
565
+ ![Lock](assets/unique_digests_2.png)
566
+
567
+ ## Testing
568
+
569
+ ### Validating Worker Configuration
570
+
571
+ 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.
572
+
573
+ Let's take a _bad_ worker:
574
+
575
+ ```ruby
576
+ #app/workers/bad_worker.rb
577
+ class BadWorker
578
+ sidekiq_options lock: :while_executing, on_conflict: :replace
579
+ end
580
+
581
+ #spec/workers/bad_worker_spec.rb
582
+
583
+ require "sidekiq_unique_jobs/testing"
584
+ #OR
585
+ require "sidekiq_unique_jobs/rspec/matchers"
586
+
587
+ RSpec.describe BadWorker do
588
+ specify { expect(described_class).to have_valid_sidekiq_options }
589
+ end
590
+ ```
591
+
592
+ This gives us a helpful error message for a wrongly configured worker:
593
+
594
+ ```bash
595
+ Expected BadWorker to have valid sidekiq options but found the following problems:
596
+ on_server_conflict: :replace is incompatible with the server process
597
+ ```
598
+
599
+ If you are not using RSpec (a lot of people prefer minitest or test unit) you can do something like:
600
+
601
+ ```ruby
602
+ assert SidekiqUniqueJobs.validate_worker!(BadWorker.get_sidekiq_options)
603
+ ```
604
+
605
+ ### Uniqueness
606
+
607
+ 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][]
608
+
609
+ ```ruby
610
+ SidekiqUniqueJobs.configure do |config|
611
+ config.enabled = !Rails.env.test?
612
+ end
613
+ ```
614
+
615
+ 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).
616
+
617
+ ```ruby
618
+ require "sidekiq_unique_jobs/testing"
619
+
620
+ RSpec.describe Workers::CoolOne do
621
+ before do
622
+ SidekiqUniqueJobs.config.enabled = false
623
+ end
624
+
625
+ # ... your tests that don't test uniqueness
626
+
627
+ context 'when Sidekiq::Testing.disabled?' do
628
+ before do
629
+ Sidekiq::Testing.disable!
630
+ Sidekiq.redis(&:flushdb)
631
+ end
632
+
633
+ after do
634
+ Sidekiq.redis(&:flushdb)
635
+ end
636
+
637
+ it 'prevents duplicate jobs from being scheduled' do
638
+ SidekiqUniqueJobs.use_config(enabled: true) do
639
+ expect(described_class.perform_in(3600, 1)).not_to eq(nil)
640
+ expect(described_class.perform_async(1)).to eq(nil)
641
+ end
642
+ end
643
+ end
644
+ end
645
+ ```
646
+
647
+ 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:
648
+
649
+ - [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)
650
+ - [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)
651
+ - [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)
652
+ - [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)
653
+ - [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)
654
+
655
+ ## Configuration
656
+
657
+ ### Other Sidekiq gems
658
+
659
+ #### apartment-sidekiq
660
+
661
+ 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.
662
+
663
+ ```ruby
664
+ Sidekiq.client_middleware do |chain|
665
+ chain.add Apartment::Sidekiq::Middleware::Client
666
+ chain.add SidekiqUniqueJobs::Middleware::Client
667
+ end
668
+
669
+ Sidekiq.server_middleware do |chain|
670
+ chain.add Apartment::Sidekiq::Middleware::Server
671
+ chain.add SidekiqUniqueJobs::Middleware::Server
672
+ end
673
+ ```
674
+
675
+ 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
676
+
677
+ #### sidekiq-global_id
678
+
679
+ 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.
680
+
681
+ For a working setup check the following [file](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/master/myapp/config/sidekiq.rb#L12).
682
+
683
+ ```ruby
684
+ Sidekiq.client_middleware do |chain|
685
+ chain.add Sidekiq::GlobalId::ClientMiddleware
686
+ chain.add SidekiqUniqueJobs::Middleware::Client
687
+ end
688
+
689
+ Sidekiq.server_middleware do |chain|
690
+ chain.add Sidekiq::GlobalId::ServerMiddleware
691
+ chain.add SidekiqUniqueJobs::Middleware::Server
692
+ end
693
+ ```
694
+
695
+ 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.
696
+
697
+ #### sidekiq-status
698
+
699
+ 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.
700
+
701
+ ```ruby
702
+ # Thanks to @ArturT for the correction
703
+
704
+ Sidekiq.configure_server do |config|
705
+ config.client_middleware do |chain|
706
+ chain.add SidekiqUniqueJobs::Middleware::Client
707
+ chain.add Sidekiq::Status::ClientMiddleware, expiration: 30.minutes
708
+ end
709
+
710
+ config.server_middleware do |chain|
711
+ chain.add Sidekiq::Status::ServerMiddleware, expiration: 30.minutes
712
+ chain.add SidekiqUniqueJobs::Middleware::Server
713
+ end
714
+
715
+ SidekiqUniqueJobs::Server.configure(config)
716
+ end
717
+
718
+
719
+ Sidekiq.configure_client do |config|
720
+ config.client_middleware do |chain|
721
+ chain.add SidekiqUniqueJobs::Middleware::Client
722
+ chain.add Sidekiq::Status::ClientMiddleware, expiration: 30.minutes
723
+ end
724
+ end
725
+ ```
726
+
727
+ 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.
728
+
729
+ ### Global Configuration
730
+
731
+ The gem supports a few different configuration options that might be of interest if you run into some weird issues.
732
+
733
+ Configure SidekiqUniqueJobs in an initializer or the sidekiq initializer on application startup.
734
+
735
+ ```ruby
736
+ SidekiqUniqueJobs.configure do |config|
737
+ config.logger = Sidekiq.logger # default, change at your own discretion
738
+ config.debug_lua = false # Turn on when debugging
739
+ config.lock_info = false # Turn on when debugging
740
+ config.lock_ttl = 600 # Expire locks after 10 minutes
741
+ config.lock_timeout = nil # turn off lock timeout
742
+ config.max_history = 0 # Turn on when debugging
743
+ config.reaper = :ruby # :ruby, :lua or :none/nil
744
+ config.reaper_count = 1000 # Stop reaping after this many keys
745
+ config.reaper_interval = 600 # Reap orphans every 10 minutes
746
+ config.reaper_timeout = 150 # Timeout reaper after 2.5 minutes
747
+ end
748
+ ```
749
+
750
+ #### debug_lua
751
+
752
+ ```ruby
753
+ SidekiqUniqueJobs.config.debug_lua #=> false
754
+ ```
755
+
756
+ 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.
757
+
758
+ #### lock_timeout
759
+
760
+ ```ruby
761
+ SidekiqUniqueJobs.config.lock_timeout #=> 0
762
+ ```
763
+
764
+ Set a global lock_timeout to use for all jobs that don't otherwise specify a lock_timeout.
765
+
766
+ 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.
767
+
768
+ #### lock_ttl
769
+
770
+ ```ruby
771
+ SidekiqUniqueJobs.config.lock_ttl #=> nil
772
+ ```
773
+
774
+ Set a global lock_ttl to use for all jobs that don't otherwise specify a lock_ttl.
775
+
776
+ Lock TTL decides how long to wait at most before considering a lock to be expired and making it possible to reuse that lock.
777
+
778
+ #### enabled
779
+
780
+ ```ruby
781
+ SidekiqUniqueJobs.config.enabled #=> true
782
+ ```
783
+
784
+ Globally turn the locking mechanism on or off.
785
+
786
+ #### logger
787
+
788
+ ```ruby
789
+ SidekiqUniqueJobs.config.logger #=> #<Sidekiq::Logger:0x00007fdc1f96d180>
790
+ ```
791
+
792
+ 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.
793
+
794
+ #### max_history
795
+
796
+ ```ruby
797
+ SidekiqUniqueJobs.config.max_history #=> 1_000
798
+ ```
799
+
800
+ 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.
801
+
802
+ 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.
803
+
804
+ #### reaper
805
+
806
+ ```ruby
807
+ SidekiqUniqueJobs.config.reaper #=> :ruby
808
+ ```
809
+
810
+ 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.
811
+
812
+ In my benchmarks deleting 1000 orphaned locks with lua performs around 65% faster than deleting 1000 keys in ruby.
813
+
814
+ On the other hand if I increase it to 10 000 orphaned locks per cleanup (`reaper_count: 10_0000`) then redis starts throwing:
815
+
816
+ > BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. (Redis::CommandError)
817
+
818
+ 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.
819
+
820
+ ```ruby
821
+ SidekiqUniqueJobs.config.reaper = :none
822
+ SidekiqUniqueJobs.config.reaper = nil
823
+ SidekiqUniqueJobs.config.reaper = false
824
+ ```
825
+
826
+ #### reaper_count
827
+
828
+ ```ruby
829
+ SidekiqUniqueJobs.config.reaper_count #=> 1_000
830
+ ```
831
+
832
+ 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.
833
+
834
+ #### reaper_interval
835
+
836
+ ```ruby
837
+ SidekiqUniqueJobs.config.reaper_interval #=> 600
838
+ ```
839
+
840
+ The number of seconds between reaping.
841
+
842
+ #### reaper_timeout
843
+
844
+ ```ruby
845
+ SidekiqUniqueJobs.config.reaper_timeout #=> 10
846
+ ```
847
+
848
+ 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.
849
+
850
+ #### lock_prefix
851
+
852
+ ```ruby
853
+ SidekiqUniqueJobs.config.lock_prefix #=> "uniquejobs"
854
+ ```
855
+
856
+ Use if you want a different key prefix for the keys in redis.
857
+
858
+ ### lock_info
859
+
860
+ ```ruby
861
+ SidekiqUniqueJobs.config.lock_info #=> false
862
+ ```
863
+
864
+ 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.
865
+
866
+ ### Worker Configuration
867
+
868
+ #### lock_info
869
+
870
+ 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.
871
+
872
+ ```ruby
873
+ sidekiq_options lock_info: false # this is the default, set to true to turn on
874
+ ```
875
+
876
+ #### lock_prefix
877
+
878
+ Use if you want a different key prefix for the keys in redis.
879
+
880
+ ```ruby
881
+ sidekiq_options lock_prefix: "uniquejobs" # this is the default value
882
+ ```
883
+
884
+ #### lock_ttl
885
+
886
+ Lock TTL decides how long to wait at most before considering a lock to be expired and making it possible to reuse that lock.
887
+
888
+ Starting from `v7` the expiration will take place when the job is pushed to the queue.
889
+
890
+ ```ruby
891
+ sidekiq_options lock_ttl: nil # default - don't expire keys
892
+ sidekiq_options lock_ttl: 20.days.to_i # expire this lock in 20 days
893
+ ```
894
+
895
+ #### lock_timeout
896
+
897
+ 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.
898
+
899
+ ```ruby
900
+ sidekiq_options lock_timeout: 0 # default - don't wait at all
901
+ sidekiq_options lock_timeout: 5 # wait 5 seconds
902
+ sidekiq_options lock_timeout: nil # lock indefinitely, this process won't continue until it gets a lock. VERY DANGEROUS!!
903
+ ```
904
+
905
+ #### unique_across_queues
906
+
907
+ 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.
908
+
909
+ This is mainly intended for `Worker.set(queue: :another).perform_async`.
910
+
911
+ ```ruby
912
+ class Worker
913
+ include Sidekiq::Worker
914
+
915
+ sidekiq_options unique_across_queues: true, queue: 'default'
916
+
917
+ def perform(args); end
918
+ end
919
+ ```
920
+
921
+ 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`).
922
+
923
+ #### unique_across_workers
924
+
925
+ 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.
926
+
927
+ ```ruby
928
+ class WorkerOne
929
+ include Sidekiq::Worker
930
+
931
+ sidekiq_options unique_across_workers: true, queue: 'default'
932
+
933
+ def perform(args); end
934
+ end
935
+
936
+ class WorkerTwo
937
+ include Sidekiq::Worker
938
+
939
+ sidekiq_options unique_across_workers: true, queue: 'default'
940
+
941
+ def perform(args); end
942
+ end
943
+
944
+
945
+ WorkerOne.perform_async(1)
946
+ # => 'the jobs unique id'
947
+
948
+ WorkerTwo.perform_async(1)
949
+ # => nil because WorkerOne just stole the lock
950
+ ```
951
+
952
+ ### Finer Control over Uniqueness
953
+
954
+ 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.
955
+
956
+ *NOTE:* The lock_args method need to return an array of values to use for uniqueness check.
957
+
958
+ *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 `[[...]]`.
959
+
960
+ The method or the proc can return a modified version of args without the transient arguments included, as shown below:
961
+
962
+ ```ruby
963
+ class UniqueJobWithFilterMethod
964
+ include Sidekiq::Worker
965
+ sidekiq_options lock: :until_and_while_executing,
966
+ lock_args_method: :lock_args # this is default and will be used if such a method is defined
967
+
968
+ def self.lock_args(args)
969
+ [ args[0], args[2][:type] ]
970
+ end
971
+
972
+ ...
973
+
974
+ end
975
+
976
+ class UniqueJobWithFilterProc
977
+ include Sidekiq::Worker
978
+ sidekiq_options lock: :until_executed,
979
+ lock_args_method: ->(args) { [ args.first ] }
980
+
981
+ ...
982
+
983
+ end
984
+ ```
985
+
986
+ 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.
987
+
988
+ ```ruby
989
+ class UniqueJobWithFilterMethod
990
+ include Sidekiq::Worker
991
+ sidekiq_options lock: :until_and_while_executing, lock_args_method: :lock_args
992
+
993
+ def self.lock_args(args)
994
+ if Sidekiq::ProcessSet.new.size > 1
995
+ # sidekiq runtime; uniqueness for the object (first arg)
996
+ args.first
997
+ else
998
+ # queuing from the app; uniqueness for all params
999
+ args
1000
+ end
1001
+ end
1002
+ end
1003
+ ```
1004
+
1005
+ ### After Unlock Callback
1006
+
1007
+ 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.
1008
+
1009
+ **Exception 1:** UntilExecuting unlocks and uses callback before yielding.
1010
+ **Exception 2:** UntilExpired expires eventually, no after_unlock hook is called.
1011
+
1012
+ **NOTE:** _It is also possible to write this code as a class method._
1013
+
1014
+ ```ruby
1015
+ class UniqueJobWithFilterMethod
1016
+ include Sidekiq::Worker
1017
+ sidekiq_options lock: :while_executing,
1018
+
1019
+ def self.after_unlock
1020
+ # block has yielded and lock is released
1021
+ end
1022
+
1023
+ def after_unlock
1024
+ # block has yielded and lock is released
1025
+ end
1026
+ ...
1027
+ end.
1028
+ ```
1029
+
1030
+ ## Communication
1031
+
1032
+ 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.
1033
+
1034
+ ## Contributing
1035
+
1036
+ 1. Fork it
1037
+ 1. Create your feature branch (`git checkout -b my-new-feature`)
1038
+ 1. Commit your changes (`git commit -am 'Add some feature'`)
501
1039
  1. Push to the branch (`git push origin my-new-feature`)
502
1040
  1. Create new Pull Request
503
1041
 
@@ -505,9 +1043,8 @@ I would strongly suggest you let this gem test uniqueness. If you care about how
505
1043
 
506
1044
  You can find a list of contributors over on [Contributors][]
507
1045
 
508
- [v5.0.10]: https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v5.0.10.
1046
+ [Enterprise unique jobs]: https://github.com/mperham/sidekiq/wiki/Ent-Unique-Jobs
1047
+ [Contributors]: https://github.com/mhenrixon/sidekiq-unique-jobs/graphs/contributors
509
1048
  [v4.0.18]: https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v4.0.18
1049
+ [v5.0.10]: https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v5.0.10.
510
1050
  [Sidekiq requirements]: https://github.com/mperham/sidekiq#requirements
511
- [Enterprise unique jobs]: https://www.dailydrip.com/topics/sidekiq/drips/sidekiq-enterprise-unique-jobs
512
- [Contributors]: https://github.com/mhenrixon/sidekiq-unique-jobs/graphs/contributors
513
- [Paypal link https://paypal.me/mhenrixon]: https://paypal.me/mhenrixon