sidekiq-locking 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 920668894edb18696853798031f13f8192595b6f7c82f1e2902a6f7219bef21e
4
+ data.tar.gz: f7173b85a3f6b4d9524c312244f2dc90eb91869203a3b0049029e5d919d37262
5
+ SHA512:
6
+ metadata.gz: a89b55c2dac277700ec9e59b9fd90faba733117ebad38a63ee1fd00f3030c449756bb226c11483196dcf1d557fb5c26eabc7cbf56036e893dd4a25987fc2693c
7
+ data.tar.gz: 86be7fb1fe072579f1682eec4b11d9d17d27e106755de55a2b583f752b3b516b810282997c73405c47315e47707c328248e8f669407c3a28fdfb43cbe8310534
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BigBinary Technologies Pvt. Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # sidekiq-locking
2
+
3
+ Best-effort, **TTL-bounded enqueue deduplication** for Sidekiq jobs — drop
4
+ duplicate enqueues of the same job while a short-lived lock is held, **without a
5
+ database or a runtime mutex**.
6
+
7
+ If application code enqueues the same work repeatedly — a webhook storm, a
8
+ fan-out that re-triggers the same sync, a retry loop upstream — only the first
9
+ copy is kept in Redis while the lock is active. A duplicate is skipped when it
10
+ matches the same lock context: `(class, queue, args)` by default.
11
+
12
+ This is intentionally **best-effort deduplication, not a correctness primitive.**
13
+ It is not a distributed lock and not a runtime mutex: two copies can still run
14
+ concurrently if the TTL expires before the first finishes. Jobs must stay
15
+ idempotent, and business-critical uniqueness should be protected with database
16
+ constraints, row locks, or advisory locks.
17
+
18
+ > **Naming.** The gem is `sidekiq-locking` and the namespace is `Sidekiq::Locking`,
19
+ > with a `lock_for` DSL. The word "lock" somewhat overclaims — this is
20
+ > best-effort, TTL-bounded enqueue *deduplication*, not a true mutex. The name
21
+ > and DSL are kept stable for now and may be revisited.
22
+
23
+ ## Installation
24
+
25
+ ```ruby
26
+ gem "sidekiq-locking"
27
+ ```
28
+
29
+ ```sh
30
+ bundle install
31
+ ```
32
+
33
+ Requires Ruby >= 3.1 and Sidekiq >= 7.0. The gem depends only on `sidekiq` — no
34
+ Rails or ActiveSupport required.
35
+
36
+ ## Quick start
37
+
38
+ Install the middleware once, in the host application's Sidekiq initializer:
39
+
40
+ ```ruby
41
+ # config/initializers/sidekiq.rb
42
+ require "sidekiq-locking"
43
+
44
+ Sidekiq::Locking.install! unless Rails.env.test?
45
+ ```
46
+
47
+ `install!` registers the client middleware (acquires the lock and skips
48
+ duplicate pushes), the server middleware (releases the lock around `perform`),
49
+ and a death handler (releases the lock when a job is discarded). Disabling it in
50
+ test keeps tests deterministic and avoids locks leaking between examples.
51
+
52
+ Then opt a job in with a lock window:
53
+
54
+ ```ruby
55
+ class SyncAddressJob
56
+ include Sidekiq::Job
57
+
58
+ sidekiq_options lock_for: 5.minutes
59
+
60
+ def perform(user_id)
61
+ # sync user address
62
+ end
63
+ end
64
+ ```
65
+
66
+ A second `SyncAddressJob` with the same queue and args cannot be enqueued while
67
+ the first lock is active. The lock is released when the job succeeds, or when
68
+ `lock_for` expires — whichever happens first. Keep `lock_for` short; a window
69
+ longer than a few minutes usually means the job wants stronger application-level
70
+ concurrency control.
71
+
72
+ ## Lock context
73
+
74
+ By default jobs are deduplicated by `[class, queue, args]`, so the same job
75
+ class with the same args can still be enqueued on different queues:
76
+
77
+ ```ruby
78
+ SyncAddressJob.set(queue: "default").perform_async(1)
79
+ SyncAddressJob.set(queue: "low").perform_async(1) # different lock — queue differs
80
+ ```
81
+
82
+ ### Custom lock args
83
+
84
+ Use a `lock_args(job)` class method when only part of the args should
85
+ participate in the lock key. It must return an `Array`; if it is absent, the
86
+ full args array is used.
87
+
88
+ ```ruby
89
+ class RefreshAccountCacheJob
90
+ include Sidekiq::Job
91
+
92
+ sidekiq_options lock_for: 5.minutes
93
+
94
+ # Only account_id participates in the lock; the reason arg is ignored.
95
+ def self.lock_args(job)
96
+ [job["args"].first]
97
+ end
98
+
99
+ def perform(account_id, reason = nil)
100
+ # refresh account cache
101
+ end
102
+ end
103
+
104
+ # These two share one lock:
105
+ RefreshAccountCacheJob.perform_async(42, "user_updated")
106
+ RefreshAccountCacheJob.perform_async(42, "manual_refresh")
107
+ ```
108
+
109
+ ### ActiveJob
110
+
111
+ `Sidekiq::Locking` unwraps Sidekiq's ActiveJob wrapper and keys on the wrapped job
112
+ class plus the real ActiveJob arguments. A wrapped job that defines
113
+ `lock_args(job)` receives the unwrapped args.
114
+
115
+ ## Scheduled jobs
116
+
117
+ Scheduled jobs include the delay in the lock TTL. With `lock_for: 10.minutes`:
118
+
119
+ ```ruby
120
+ SyncAddressJob.set(wait: 1.hour).perform_async(1)
121
+ ```
122
+
123
+ the lock lasts ~70 minutes total (the one-hour delay plus the ten-minute
124
+ window), so a duplicate cannot be enqueued for that full period unless the
125
+ original runs and releases the lock earlier.
126
+
127
+ ## Retries and the unlock policy
128
+
129
+ `release_lock` controls when the lock is released.
130
+
131
+ - **`:after_perform` (default)** — release only after `perform` returns
132
+ successfully. A failed job keeps its lock while it waits to retry, so a
133
+ transient failure does not let a duplicate slip in. The lock clears on
134
+ success, on death (via the death handler), or on TTL expiry.
135
+
136
+ ```ruby
137
+ sidekiq_options lock_for: 5.minutes, release_lock: :after_perform
138
+ ```
139
+
140
+ - **`:before_perform`** — release immediately before the job starts running.
141
+ Use this only when it is acceptable for another copy to be enqueued while the
142
+ current job is still executing.
143
+
144
+ ```ruby
145
+ sidekiq_options lock_for: 5.minutes, release_lock: :before_perform
146
+ ```
147
+
148
+ If the TTL expires while a job is still retrying, a duplicate can be enqueued.
149
+ Design jobs with that best-effort behavior in mind.
150
+
151
+ ## Per-enqueue overrides
152
+
153
+ Use Sidekiq's `set` API to disable or override the lock for one enqueue:
154
+
155
+ ```ruby
156
+ SyncAddressJob.set(lock_for: false).perform_async(1) # no lock this time
157
+ SyncAddressJob.set(lock_for: 30.seconds).perform_async(1) # shorter window
158
+ SyncAddressJob.set(lock_for: 10.minutes).perform_async(1) # longer window
159
+ ```
160
+
161
+ ## Return value on a duplicate enqueue
162
+
163
+ A duplicate enqueue does not return a JID because the job was never pushed.
164
+ Through Sidekiq's public `perform_async` API this is observed as `nil`:
165
+
166
+ ```ruby
167
+ SyncAddressJob.perform_async(1) # => "abc123..."
168
+ SyncAddressJob.perform_async(1) # => nil (duplicate, lock held)
169
+ ```
170
+
171
+ Do not rely on `perform_async` always returning a JID for lockable jobs. Each
172
+ skipped duplicate is logged at info level —
173
+ `Skipping enqueue for <Class>, lock held by JID <jid>`.
174
+
175
+ ## Redis key prefix
176
+
177
+ Each active lock is one Redis string at `sidekiq:locking:<digest>`, whose value is
178
+ the owning job's JID (releases are JID-guarded so a late finisher cannot delete
179
+ a newer lock). The prefix is configurable:
180
+
181
+ ```ruby
182
+ Sidekiq::Locking.redis_key_prefix = "my-app:sidekiq:locking:"
183
+ ```
184
+
185
+ ## How it works
186
+
187
+ ```text
188
+ perform_async
189
+ → client middleware builds a lock key from (class, queue, args)
190
+ → Redis: SET sidekiq:locking:<digest> <jid> GET NX PX <ttl_ms>
191
+
192
+ Acquired (key was new):
193
+ → stamp job["lock_token"] = digest, push the job
194
+ → server middleware releases the lock on success (per release_lock)
195
+ → death handler releases the lock if the job is discarded
196
+
197
+ Already held:
198
+ → do not push; perform_async returns nil; log the skip
199
+ ```
200
+
201
+ If a *downstream* client middleware aborts the push by returning `false` (for
202
+ example a blackhole route from
203
+ [sidekiq-routing](https://github.com/neetozone/sidekiq-routing)), the lock
204
+ middleware releases the lock it just acquired so the next identical enqueue is
205
+ not blocked until the TTL. When composing with other client middleware, the
206
+ lock middleware should run **before** any middleware that may abort the push.
207
+
208
+ ## Caveats
209
+
210
+ - Locks are best-effort and TTL-bounded; jobs must remain idempotent.
211
+ - This is enqueue deduplication, not execution-time mutual exclusion — if the
212
+ TTL expires while a job is still running or retrying, another copy can be
213
+ enqueued.
214
+ - Duplicate jobs are dropped, not rescheduled or replaced.
215
+ - There is no Web UI, lock browser, or stale-lock reaper — the TTL is the safety
216
+ mechanism.
217
+ - `release_lock: :before_perform` allows duplicates while the job is running.
218
+ - Manually deleting jobs from Sidekiq queues may leave the lock in Redis until
219
+ the TTL expires.
220
+ - The lock adds one Redis round-trip before each push; if Redis is unavailable,
221
+ enqueue can fail with a Redis/network error.
222
+
223
+ ## License
224
+
225
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Locking
5
+ # Claims a lock with `SET key <jid> NX PX <ttl>` plus the GET option — the
6
+ # check-and-set lock recipe from the Redis SET docs
7
+ # (https://redis.io/commands/set/). The acquiring job's JID is stored as the
8
+ # value so the release can later be guarded by ownership.
9
+ module Acquirer
10
+ Held = Struct.new(:jid) do
11
+ def held?
12
+ true
13
+ end
14
+ end
15
+
16
+ class << self
17
+ def try_acquire(key, jid, ttl_ms, redis_pool = nil)
18
+ setter = ->(conn) { conn.set(key.redis_key, jid, "get", "nx", "px", ttl_ms) }
19
+ holder = redis_pool ? redis_pool.with(&setter) : Sidekiq.redis(&setter)
20
+
21
+ holder ? Held.new(holder) : :acquired
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha2"
4
+ require "json"
5
+
6
+ module Sidekiq
7
+ module Locking
8
+ # Value object for a lock key. Builds the uniqueness tuple (job class, queue,
9
+ # args) from the Sidekiq payload and hashes it into the Redis key shared by
10
+ # the acquirer and releaser. The hash encoding (SHA-256 hex over a JSON dump
11
+ # of the tuple) is an internal implementation detail — it only has to be
12
+ # stable and collision-resistant for a given tuple.
13
+ class Key
14
+ ACTIVE_JOB_WRAPPERS = [
15
+ "Sidekiq::ActiveJob::Wrapper",
16
+ "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
17
+ ].freeze
18
+
19
+ attr_reader :job, :klass, :queue, :args, :context, :digest
20
+
21
+ def initialize(job)
22
+ @job = job
23
+ @klass = job["wrapped"] || job["class"]
24
+ @queue = job["queue"]
25
+ @args = resolved_args
26
+ @context = [klass, queue, args]
27
+ @digest = Digest::SHA256.hexdigest(JSON.generate(context))
28
+ end
29
+
30
+ def redis_key
31
+ "#{Locking.redis_key_prefix}#{digest}"
32
+ end
33
+
34
+ private
35
+
36
+ def resolved_args
37
+ job_class = constantized_job_class
38
+ return default_args unless job_class&.respond_to?(:lock_args)
39
+
40
+ scratch = job.merge("class" => klass, "args" => copy_args(default_args))
41
+ result = job_class.lock_args(scratch)
42
+ unless result.is_a?(Array)
43
+ raise TypeError, "#{job_class.name}.lock_args must return an Array (got #{result.class})"
44
+ end
45
+
46
+ result
47
+ end
48
+
49
+ def default_args
50
+ if ACTIVE_JOB_WRAPPERS.include?(job["class"])
51
+ job.dig("args", 0, "arguments") || []
52
+ else
53
+ job["args"] || []
54
+ end
55
+ end
56
+
57
+ def constantized_job_class
58
+ Object.const_get(klass)
59
+ rescue NameError
60
+ nil
61
+ end
62
+
63
+ def copy_args(value)
64
+ value.respond_to?(:dup) ? value.dup : value
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Locking
5
+ module Middleware
6
+ # Acquires the lock before push. Skips the push (returns false) if a lock
7
+ # is already held for the same (class, queue, lock_args) tuple.
8
+ #
9
+ # Scheduled jobs (`perform_in`) include their delay in the TTL: a job
10
+ # scheduled an hour out with `lock_for: 10.minutes` holds the lock for 70
11
+ # minutes total.
12
+ class Client
13
+ def call(_worker_class, job, _queue, redis_pool = nil)
14
+ return yield unless lockable?(job)
15
+
16
+ expiry_ms = expiry_for(job)
17
+ if expiry_ms <= 0
18
+ Sidekiq.logger.info("Skipping lock for #{job["class"]}/#{job["jid"]}: lock window ends in the past")
19
+ return yield
20
+ end
21
+
22
+ key = Key.new(job)
23
+ result = Acquirer.try_acquire(key, job["jid"], expiry_ms, redis_pool)
24
+
25
+ if result == :acquired
26
+ job[TOKEN_KEY] = key.digest
27
+ pushed = yield
28
+ # If a downstream middleware aborted the push (e.g. Sidekiq::Routing's
29
+ # blackhole returns false) the job never reaches Redis but our lock
30
+ # is held. Release it so the next identical enqueue isn't blocked
31
+ # until the TTL expires.
32
+ Releaser.release(key.digest, job["jid"]) if pushed == false
33
+ pushed
34
+ else
35
+ klass = job["wrapped"] || job["class"]
36
+ Sidekiq.logger.info { "Skipping enqueue for #{klass}, lock held by JID #{result.jid}" }
37
+ false
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def lockable?(job)
44
+ job[LOCK_FOR_KEY] && !job.key?(TOKEN_KEY) && job[LOCK_FOR_KEY].to_i >= 0
45
+ end
46
+
47
+ # How long the lock should live, in milliseconds. The base window is
48
+ # `lock_for` seconds; a scheduled job ("at" in the future) stays locked
49
+ # until it actually runs, so the remaining delay is added on top.
50
+ # Immediate jobs carry no delay.
51
+ def expiry_for(job)
52
+ window_seconds = job[LOCK_FOR_KEY].to_i
53
+ run_at = job["at"]
54
+ delay_seconds = run_at ? run_at - Time.now.to_f : 0.0
55
+
56
+ ((window_seconds + delay_seconds) * 1000).to_i
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Locking
5
+ module Middleware
6
+ # Releases the lock around perform.
7
+ #
8
+ # release_lock: :after_perform (default) — release after successful
9
+ # perform. Errored jobs keep the lock through retries until success or
10
+ # death.
11
+ # release_lock: :before_perform — release just before perform begins.
12
+ class Server
13
+ def call(_worker_class, job, _queue)
14
+ return yield unless locked?(job)
15
+
16
+ if release_before_perform?(job)
17
+ release(job)
18
+ yield
19
+ else
20
+ yield
21
+ release(job)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def locked?(job)
28
+ job[LOCK_FOR_KEY] && job[LOCK_FOR_KEY].to_i >= 0 && job[TOKEN_KEY]
29
+ end
30
+
31
+ def release_before_perform?(job)
32
+ job.fetch(RELEASE_LOCK_KEY, RELEASE_AFTER_PERFORM).to_s == RELEASE_BEFORE_PERFORM
33
+ end
34
+
35
+ def release(job)
36
+ Releaser.release(job[TOKEN_KEY], job["jid"])
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Locking
5
+ # Deletes a lock only when the Redis value still equals the releasing job's
6
+ # JID, so a late finisher cannot delete a newer lock acquired after the
7
+ # original TTL expired. This compare-and-delete is the standard safe-release
8
+ # pattern from the Redis SET docs (https://redis.io/commands/set/).
9
+ module Releaser
10
+ UNLOCK_SCRIPT = <<~LUA
11
+ if redis.call('get', KEYS[1]) == ARGV[1] then
12
+ redis.call('del', KEYS[1])
13
+ end
14
+ LUA
15
+
16
+ class << self
17
+ def release(digest, jid)
18
+ return unless digest && jid
19
+
20
+ Sidekiq.redis do |conn|
21
+ conn.call("EVAL", UNLOCK_SCRIPT, 1, "#{Locking.redis_key_prefix}#{digest}", jid)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Locking
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ # Sidekiq::Locking — best-effort, TTL-bounded deduplication of Sidekiq enqueues.
6
+ #
7
+ # While a lock is held for a job, a second enqueue of the same job is dropped.
8
+ # Treat it as a pruning convenience, never a guarantee — the lock expires on a
9
+ # TTL and the check is inherently racy, so keep jobs idempotent and enforce real
10
+ # uniqueness in your datastore.
11
+ #
12
+ # The Redis building blocks used here — a token-valued `SET ... NX PX` to claim
13
+ # the lock and a compare-and-delete to release it — are the standard recipes from
14
+ # the Redis SET documentation (https://redis.io/commands/set/).
15
+ #
16
+ # Opt a job in with a lock window, and optionally narrow the dedup scope:
17
+ #
18
+ # # config/initializers/sidekiq.rb
19
+ # Sidekiq::Locking.install! unless Rails.env.test?
20
+ #
21
+ # class MyJob
22
+ # include Sidekiq::Job
23
+ # sidekiq_options lock_for: 5.minutes
24
+ #
25
+ # def self.lock_args(job)
26
+ # [job["args"].first]
27
+ # end
28
+ # end
29
+ module Sidekiq
30
+ module Locking
31
+ LOCK_FOR_KEY = "lock_for"
32
+ RELEASE_LOCK_KEY = "release_lock"
33
+ TOKEN_KEY = "lock_token"
34
+
35
+ RELEASE_AFTER_PERFORM = "after_perform"
36
+ RELEASE_BEFORE_PERFORM = "before_perform"
37
+
38
+ DEFAULT_REDIS_KEY_PREFIX = "sidekiq:locking:"
39
+
40
+ class << self
41
+ attr_writer :redis_key_prefix
42
+
43
+ def redis_key_prefix
44
+ @redis_key_prefix || DEFAULT_REDIS_KEY_PREFIX
45
+ end
46
+
47
+ # Registers the client + server middleware on both client and server
48
+ # configurations, plus a death handler so dead jobs release their lock.
49
+ def install!
50
+ Sidekiq.configure_client do |config|
51
+ config.client_middleware do |chain|
52
+ chain.add Middleware::Client
53
+ end
54
+ # ensure `perform_inline` releases locks too
55
+ config.server_middleware do |chain|
56
+ chain.add Middleware::Server
57
+ end
58
+ end
59
+
60
+ Sidekiq.configure_server do |config|
61
+ config.client_middleware do |chain|
62
+ chain.add Middleware::Client
63
+ end
64
+ config.server_middleware do |chain|
65
+ chain.add Middleware::Server
66
+ end
67
+ config.death_handlers << ->(job, _ex) do
68
+ Releaser.release(job[TOKEN_KEY], job["jid"]) if job[TOKEN_KEY]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ require "sidekiq/locking/key"
77
+ require "sidekiq/locking/acquirer"
78
+ require "sidekiq/locking/releaser"
79
+ require "sidekiq/locking/middleware/client"
80
+ require "sidekiq/locking/middleware/server"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/locking/version"
4
+
5
+ # Sidekiq::Locking: TTL-bounded, best-effort enqueue deduplication for Sidekiq jobs.
6
+ #
7
+ # Defines Sidekiq::Locking and its public API (install!, redis_key_prefix). The
8
+ # module file requires its own collaborators (Key, Acquirer, Releaser, and the
9
+ # client/server middleware), so requiring it here loads the whole subsystem.
10
+ #
11
+ # The per-job DSL (`lock_for`, `release_lock`, `lock_args`) is unchanged. "Lock"
12
+ # somewhat overclaims: this is best-effort dedup, not a runtime mutex (see README).
13
+ require "sidekiq/locking"
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-locking
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vishnu M
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sidekiq
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ description: |
28
+ sidekiq-locking keeps only the first copy of a job in Redis while a
29
+ short-lived lock is active. A duplicate enqueue for the same lock context
30
+ (class, queue, args by default) is skipped until the original job succeeds
31
+ or the lock_for TTL expires — whichever comes first. It is deliberately
32
+ best-effort enqueue coalescing, not a correctness primitive or a runtime
33
+ mutex: jobs must still be idempotent and protect true uniqueness with
34
+ database constraints/locks where required. Opt a job in with
35
+ `sidekiq_options lock_for: 5.minutes`; customize the dedup scope with a
36
+ `lock_args` class method. No Rails or ActiveSupport required.
37
+ email:
38
+ - vishnu.m@bigbinary.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - LICENSE
44
+ - README.md
45
+ - lib/sidekiq-locking.rb
46
+ - lib/sidekiq/locking.rb
47
+ - lib/sidekiq/locking/acquirer.rb
48
+ - lib/sidekiq/locking/key.rb
49
+ - lib/sidekiq/locking/middleware/client.rb
50
+ - lib/sidekiq/locking/middleware/server.rb
51
+ - lib/sidekiq/locking/releaser.rb
52
+ - lib/sidekiq/locking/version.rb
53
+ homepage: https://github.com/neetozone/sidekiq-locking
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ source_code_uri: https://github.com/neetozone/sidekiq-locking
58
+ rubygems_mfa_required: 'true'
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.1'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.5.22
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: 'Best-effort, TTL-bounded enqueue deduplication for Sidekiq jobs: drop duplicate
78
+ enqueues of the same job while a lock is held.'
79
+ test_files: []