activejob-debounce 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +12 -0
- data/activejob-debounce.gemspec +44 -0
- data/lib/activejob/debounce/concern.rb +102 -0
- data/lib/activejob/debounce/configuration.rb +26 -0
- data/lib/activejob/debounce/version.rb +7 -0
- data/lib/activejob/debounce.rb +31 -0
- data/lib/activejob-debounce.rb +3 -0
- metadata +131 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e1f75d298bfa0f6d3465458cfbae9b6c1e0aa478931d7239bb3165313513cef2
|
|
4
|
+
data.tar.gz: 2f5878e7ceb1d785a24084491fab650cebea2b9a477a8dac492cd1ca46ce5091
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a37dad2992e3def5f3876ce2126a21f711b93ca7746d027b5099c12f070fb48c1529252f600dfcc9c638c8af2f785fc617e91f75d37bd5be146f9791c45df21b
|
|
7
|
+
data.tar.gz: f981f5b6a520b3811642c45b0dd6062ed11e69e4400fb3f9448cc82a63e5cd0c1342303f6eff72b940ca20e305f3afe03585399dc2850e9e5f796414e3d5eeeb
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2025-01-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release
|
|
12
|
+
- `Sidekiq::Debouncer::Concern` module for adding debounce behavior to ActiveJob classes
|
|
13
|
+
- `debounce_for` class method to configure debounce duration
|
|
14
|
+
- `perform_debounce` class method to queue debounced jobs
|
|
15
|
+
- Global configuration for default_delay, buffer, ttl, and redis_connection
|
|
16
|
+
- Support for ActiveRecord objects via GlobalID
|
|
17
|
+
- Automatic Redis key cleanup after job execution
|
|
18
|
+
- Comprehensive test suite
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# ActiveJob::Debounce
|
|
2
|
+
|
|
3
|
+
Leading-edge debounce for ActiveJob. One job per debounce window, atomic Redis gating, crash recovery.
|
|
4
|
+
|
|
5
|
+
Works with **any ActiveJob backend**: Sidekiq, GoodJob, Solid Queue, Resque, etc.
|
|
6
|
+
|
|
7
|
+
**The problem:** Webhooks, callbacks, and real-time triggers fire multiple times for the same entity within seconds. Without debouncing, you get duplicate jobs flooding your queue — wasted workers, inflated stats, and race conditions.
|
|
8
|
+
|
|
9
|
+
**This gem** gates at dispatch time using Redis GETSET — only 1 job enters the queue per debounce window. Subsequent calls are true no-ops (nothing queued). Clean queue stats, full crash recovery.
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Time: 0s 5s 10s 30s 35s
|
|
15
|
+
| | | | |
|
|
16
|
+
v v v v v
|
|
17
|
+
call call call [executes] call
|
|
18
|
+
|______|_______| |_____|
|
|
19
|
+
| |
|
|
20
|
+
These 3 calls become This call
|
|
21
|
+
ONE execution starts new window
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
1. First `perform_debounce` → sets Redis key, queues job with delay
|
|
25
|
+
2. Subsequent calls within the window → Redis key exists, skip (nothing queued)
|
|
26
|
+
3. Job executes → `after_perform` cleans up Redis key
|
|
27
|
+
4. Next call → starts a new debounce window
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem 'activejob-debounce'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bundle install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- Ruby >= 2.7
|
|
42
|
+
- Rails >= 6.0 (ActiveJob, ActiveSupport)
|
|
43
|
+
- Redis >= 4.0
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### Basic usage
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
class SyncContactJob < ApplicationJob
|
|
51
|
+
include ActiveJob::Debounce::Concern
|
|
52
|
+
|
|
53
|
+
debounce_for 30.seconds
|
|
54
|
+
|
|
55
|
+
def perform(contact_id)
|
|
56
|
+
Contact.find(contact_id).sync_to_crm
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# In your model:
|
|
61
|
+
class Contact < ApplicationRecord
|
|
62
|
+
after_save :sync_to_crm
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def sync_to_crm
|
|
67
|
+
# Even if called 100 times in 30 seconds, only ONE job executes
|
|
68
|
+
SyncContactJob.perform_debounce(id)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Multiple arguments
|
|
74
|
+
|
|
75
|
+
The debouncer creates unique keys based on ALL arguments:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
class UpdateTicketJob < ApplicationJob
|
|
79
|
+
include ActiveJob::Debounce::Concern
|
|
80
|
+
|
|
81
|
+
debounce_for 1.minute
|
|
82
|
+
|
|
83
|
+
def perform(ticket_id, update_type)
|
|
84
|
+
# ...
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# These are DIFFERENT debounce windows:
|
|
89
|
+
UpdateTicketJob.perform_debounce(123, "status") # Window 1
|
|
90
|
+
UpdateTicketJob.perform_debounce(123, "priority") # Window 2
|
|
91
|
+
UpdateTicketJob.perform_debounce(456, "status") # Window 3
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### ActiveRecord objects
|
|
95
|
+
|
|
96
|
+
Pass ActiveRecord objects directly — serialized via GlobalID:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
class SyncTrainingJob < ApplicationJob
|
|
100
|
+
include ActiveJob::Debounce::Concern
|
|
101
|
+
|
|
102
|
+
debounce_for 2.minutes
|
|
103
|
+
|
|
104
|
+
def perform(training)
|
|
105
|
+
training.sync_to_external_service
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
training = Training.find(123)
|
|
110
|
+
SyncTrainingJob.perform_debounce(training)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
# config/initializers/activejob_debounce.rb
|
|
117
|
+
ActiveJob::Debounce.configure do |config|
|
|
118
|
+
config.default_delay = 60 # Default debounce window (seconds)
|
|
119
|
+
config.buffer = 1 # Buffer to prevent race conditions (seconds)
|
|
120
|
+
config.ttl = 60 # Redis key TTL safety net (seconds)
|
|
121
|
+
config.redis_connection = Redis.new(url: ENV['REDIS_URL']) # Optional
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Crash recovery
|
|
126
|
+
|
|
127
|
+
If a job crashes without cleanup (worker killed, OOM, etc.), the Redis key holds an expired timestamp. The next `perform_debounce` call detects this and re-queues:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
T=0s Job queued, Redis key set to T+30
|
|
131
|
+
T=30s Worker crashes — Redis key still holds T+30
|
|
132
|
+
T=45s New call → GETSET returns T+30 → T+30 <= now → crash detected → re-queue
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## How it works internally
|
|
136
|
+
|
|
137
|
+
Uses Redis `GETSET` for atomic dispatch-time gating:
|
|
138
|
+
|
|
139
|
+
1. `GETSET key new_timestamp` — atomically reads old value, writes new
|
|
140
|
+
2. If old value is `nil` (no job pending) or expired (crashed) → queue the job
|
|
141
|
+
3. If old value is in the future → job already pending, skip
|
|
142
|
+
4. `after_perform` deletes the key → opens the window for next cycle
|
|
143
|
+
|
|
144
|
+
This is a **leading-edge** debounce: first event triggers execution after the delay. Subsequent events during the window are dropped.
|
|
145
|
+
|
|
146
|
+
## Redis key format
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
activejob_debounce:{JobClass}:{args}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
SyncContactJob.debounce_key([123])
|
|
154
|
+
# => "activejob_debounce:SyncContactJob:123"
|
|
155
|
+
|
|
156
|
+
UpdateTicketJob.debounce_key([user, "full"])
|
|
157
|
+
# => "activejob_debounce:UpdateTicketJob:gid://app/User/456:full"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Testing
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
RSpec.describe SyncContactJob do
|
|
164
|
+
let(:mock_redis) { instance_double("Redis") }
|
|
165
|
+
|
|
166
|
+
before do
|
|
167
|
+
ActiveJob::Debounce.configure { |c| c.redis_connection = mock_redis }
|
|
168
|
+
allow(mock_redis).to receive(:getset, :expire, :del)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "debounces multiple calls into one job" do
|
|
172
|
+
allow(mock_redis).to receive(:getset).and_return(nil, (Time.now.to_i + 100).to_s)
|
|
173
|
+
|
|
174
|
+
10.times { SyncContactJob.perform_debounce(123) }
|
|
175
|
+
|
|
176
|
+
expect(enqueued_jobs.size).to eq(1)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/activejob/debounce/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "activejob-debounce"
|
|
7
|
+
spec.version = ActiveJob::Debounce::VERSION
|
|
8
|
+
spec.authors = ["Mohamed Bouzentm"]
|
|
9
|
+
spec.email = ["bouzentoutamohamed@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Leading-edge debounce for ActiveJob. One job per debounce window, atomic Redis gating, crash recovery."
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
A zero-dependency debouncing solution for ActiveJob. Uses Redis GETSET for
|
|
14
|
+
atomic dispatch-time gating — only 1 job enters the queue per debounce window.
|
|
15
|
+
Subsequent calls are true no-ops (nothing queued). Includes crash recovery
|
|
16
|
+
via expired timestamp detection. Works with any ActiveJob backend: Sidekiq,
|
|
17
|
+
GoodJob, Solid Queue, Resque, etc.
|
|
18
|
+
DESC
|
|
19
|
+
spec.homepage = "https://github.com/m2cci-bouzentm/activejob-debounce"
|
|
20
|
+
spec.license = "MIT"
|
|
21
|
+
spec.required_ruby_version = ">= 2.7.0"
|
|
22
|
+
|
|
23
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
24
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
25
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
26
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
27
|
+
|
|
28
|
+
spec.files = Dir.chdir(__dir__) do
|
|
29
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
30
|
+
(File.expand_path(f) == __FILE__) ||
|
|
31
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
spec.bindir = "exe"
|
|
35
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
37
|
+
|
|
38
|
+
spec.add_dependency "activesupport", ">= 6.0"
|
|
39
|
+
spec.add_dependency "redis", ">= 4.0"
|
|
40
|
+
|
|
41
|
+
spec.add_development_dependency "activejob", ">= 6.0"
|
|
42
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
43
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
44
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
require "active_support/core_ext/array/wrap"
|
|
5
|
+
require "active_support/core_ext/object/blank"
|
|
6
|
+
|
|
7
|
+
module ActiveJob
|
|
8
|
+
module Debounce
|
|
9
|
+
# Include this concern in your ActiveJob classes to add debouncing behavior.
|
|
10
|
+
#
|
|
11
|
+
# When multiple calls to `perform_debounce` are made with the same arguments
|
|
12
|
+
# within the debounce window, only ONE job will be queued and executed.
|
|
13
|
+
#
|
|
14
|
+
# Works with any ActiveJob backend: Sidekiq, GoodJob, Solid Queue, Resque, etc.
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# class SyncUserJob < ApplicationJob
|
|
18
|
+
# include ActiveJob::Debounce::Concern
|
|
19
|
+
#
|
|
20
|
+
# debounce_for 30.seconds
|
|
21
|
+
#
|
|
22
|
+
# def perform(user_id)
|
|
23
|
+
# User.find(user_id).sync_to_crm
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # Call it multiple times - only one job executes
|
|
28
|
+
# 10.times { SyncUserJob.perform_debounce(user.id) }
|
|
29
|
+
#
|
|
30
|
+
module Concern
|
|
31
|
+
extend ActiveSupport::Concern
|
|
32
|
+
|
|
33
|
+
included do
|
|
34
|
+
class_attribute :debounce_settings, default: {}
|
|
35
|
+
|
|
36
|
+
after_perform do |job|
|
|
37
|
+
if self.class.debounce_settings.present?
|
|
38
|
+
ActiveJob::Debounce.redis.del(self.class.debounce_key(job.arguments))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class_methods do
|
|
44
|
+
# Configure the debounce duration for this job class.
|
|
45
|
+
#
|
|
46
|
+
# @param duration [Integer, ActiveSupport::Duration] The debounce window
|
|
47
|
+
def debounce_for(duration)
|
|
48
|
+
self.debounce_settings = { duration: duration.to_i }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
alias_method :debounce_for_seconds, :debounce_for
|
|
52
|
+
|
|
53
|
+
# Queue a debounced job. If called multiple times with the same arguments
|
|
54
|
+
# within the debounce window, only one job will be queued.
|
|
55
|
+
#
|
|
56
|
+
# @param params [Array] The arguments to pass to the job's perform method
|
|
57
|
+
def perform_debounce(*params)
|
|
58
|
+
delay = debounce_settings[:duration] || config.default_delay
|
|
59
|
+
buffer = config.buffer
|
|
60
|
+
ttl = config.ttl
|
|
61
|
+
redis_key = debounce_key(params)
|
|
62
|
+
|
|
63
|
+
# Atomic: set new timestamp, get old value
|
|
64
|
+
scheduled_at = current_timestamp + delay
|
|
65
|
+
old_timestamp = ActiveJob::Debounce.redis.getset(redis_key, scheduled_at)
|
|
66
|
+
ActiveJob::Debounce.redis.expire(redis_key, delay + buffer + ttl)
|
|
67
|
+
|
|
68
|
+
no_job_pending = old_timestamp.nil?
|
|
69
|
+
timestamp_expired = old_timestamp.to_i <= current_timestamp
|
|
70
|
+
should_queue = no_job_pending || timestamp_expired
|
|
71
|
+
|
|
72
|
+
return unless should_queue
|
|
73
|
+
|
|
74
|
+
set(wait: delay + buffer).perform_later(*params)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Generate a unique Redis key for this job + arguments combination.
|
|
78
|
+
#
|
|
79
|
+
# @param params [Array] The job arguments
|
|
80
|
+
# @return [String] The Redis key
|
|
81
|
+
def debounce_key(params)
|
|
82
|
+
params_list = Array.wrap(params).map do |param|
|
|
83
|
+
param.respond_to?(:to_global_id) ? param.to_global_id.to_s : param.to_s
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
key_string = params_list.join(":")
|
|
87
|
+
"activejob_debounce:#{name}:#{key_string}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def config
|
|
93
|
+
ActiveJob::Debounce.configuration
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def current_timestamp
|
|
97
|
+
Time.now.to_i
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
module Debounce
|
|
5
|
+
class Configuration
|
|
6
|
+
# Custom Redis connection. If not set, falls back to Redis.current
|
|
7
|
+
attr_accessor :redis_connection
|
|
8
|
+
|
|
9
|
+
# Default debounce delay in seconds (can be overridden per-job)
|
|
10
|
+
attr_accessor :default_delay
|
|
11
|
+
|
|
12
|
+
# Buffer time added to delay to prevent race conditions
|
|
13
|
+
attr_accessor :buffer
|
|
14
|
+
|
|
15
|
+
# TTL for Redis keys (cleanup safety net)
|
|
16
|
+
attr_accessor :ttl
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@redis_connection = nil
|
|
20
|
+
@default_delay = 60
|
|
21
|
+
@buffer = 1
|
|
22
|
+
@ttl = 60
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "debounce/version"
|
|
4
|
+
require_relative "debounce/configuration"
|
|
5
|
+
require_relative "debounce/concern"
|
|
6
|
+
|
|
7
|
+
module ActiveJob
|
|
8
|
+
module Debounce
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
attr_writer :configuration
|
|
13
|
+
|
|
14
|
+
def configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield(configuration)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset_configuration!
|
|
23
|
+
@configuration = Configuration.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def redis
|
|
27
|
+
configuration.redis_connection || Redis.current
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activejob-debounce
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mohamed Bouzentm
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-07-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: redis
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '4.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '4.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: activejob
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '6.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '6.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
description: |
|
|
83
|
+
A zero-dependency debouncing solution for ActiveJob. Uses Redis GETSET for
|
|
84
|
+
atomic dispatch-time gating — only 1 job enters the queue per debounce window.
|
|
85
|
+
Subsequent calls are true no-ops (nothing queued). Includes crash recovery
|
|
86
|
+
via expired timestamp detection. Works with any ActiveJob backend: Sidekiq,
|
|
87
|
+
GoodJob, Solid Queue, Resque, etc.
|
|
88
|
+
email:
|
|
89
|
+
- bouzentoutamohamed@gmail.com
|
|
90
|
+
executables: []
|
|
91
|
+
extensions: []
|
|
92
|
+
extra_rdoc_files: []
|
|
93
|
+
files:
|
|
94
|
+
- ".rspec"
|
|
95
|
+
- CHANGELOG.md
|
|
96
|
+
- LICENSE.txt
|
|
97
|
+
- README.md
|
|
98
|
+
- Rakefile
|
|
99
|
+
- activejob-debounce.gemspec
|
|
100
|
+
- lib/activejob-debounce.rb
|
|
101
|
+
- lib/activejob/debounce.rb
|
|
102
|
+
- lib/activejob/debounce/concern.rb
|
|
103
|
+
- lib/activejob/debounce/configuration.rb
|
|
104
|
+
- lib/activejob/debounce/version.rb
|
|
105
|
+
homepage: https://github.com/m2cci-bouzentm/activejob-debounce
|
|
106
|
+
licenses:
|
|
107
|
+
- MIT
|
|
108
|
+
metadata:
|
|
109
|
+
homepage_uri: https://github.com/m2cci-bouzentm/activejob-debounce
|
|
110
|
+
source_code_uri: https://github.com/m2cci-bouzentm/activejob-debounce
|
|
111
|
+
changelog_uri: https://github.com/m2cci-bouzentm/activejob-debounce/blob/main/CHANGELOG.md
|
|
112
|
+
rubygems_mfa_required: 'true'
|
|
113
|
+
rdoc_options: []
|
|
114
|
+
require_paths:
|
|
115
|
+
- lib
|
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: 2.7.0
|
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
requirements: []
|
|
127
|
+
rubygems_version: 3.6.2
|
|
128
|
+
specification_version: 4
|
|
129
|
+
summary: Leading-edge debounce for ActiveJob. One job per debounce window, atomic
|
|
130
|
+
Redis gating, crash recovery.
|
|
131
|
+
test_files: []
|