sidekiq-unique-jobs 6.0.25 → 7.1.29

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