sidekiq-debouncer 3.0.0 → 3.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c98e0c985f240350757ce46298f39cb5586d7e01966f78bd3a886c8b4a78b7e0
4
- data.tar.gz: e1f7ceda05cd4cc580fc5d318da59ccaf9a8a3f737020524c76a147add5fa5e3
3
+ metadata.gz: e4a9a96453d838cc36e81e1fe2d68ac8269a86b17d6db437f84aec0b599267cf
4
+ data.tar.gz: bbd138d4b850c86ecdac14e2d9857444c92ec9811a3612e19627aee3145873e1
5
5
  SHA512:
6
- metadata.gz: 3175ce8da01db8a593290273a4a7c392133792de93c04a9091662ca96ab24caeea8a60e17e0ab7a7fd16e8b8273d184b62a421deb58374430e26f720f0708927
7
- data.tar.gz: 2e42dedd11a4b6c05e9c4af5446803a1fab05daa7fe762b83ef3846b0f5c8cb253bf6f03e1bbbdc8e21311245087526c89098955c088d040c0c09606bba526da
6
+ metadata.gz: 7d7457e8273f79dac578f78231d5ce73380259076bae636a362124b54c26acd2d4ebc487598dfad7dcb529a023e246beabcf7d24383159f0abde667caec822bc
7
+ data.tar.gz: 9d730f2c1f36671a550a80a222a1f363f79624b2f0f74ceefc70d7928c06a40ef556a7cb3e360088ee482755d99c610323a9699f3db5b4dfee5de284bd1d9600
data/CHANGELOG.md CHANGED
@@ -1,9 +1,22 @@
1
+ ## [3.2.0] - 2026-02-15
2
+ - Support extra attributes ([#37](https://github.com/paladinsoftware/sidekiq-debouncer/pull/37))
3
+ - Fix loading order issues ([#36](https://github.com/paladinsoftware/sidekiq-debouncer/pull/36))
4
+ - Bump Ruby and Sidekiq versions ([#35](https://github.com/paladinsoftware/sidekiq-debouncer/pull/35))
5
+
6
+ ## [3.1.0] - 2025-03-28
7
+ - Drop support for Ruby < 3.2.0
8
+ - Support for Sidekiq 8 ([#31](https://github.com/paladinsoftware/sidekiq-debouncer/pull/31), [#32](https://github.com/paladinsoftware/sidekiq-debouncer/pull/32))
9
+ - DragonflyDB support ([#29](https://github.com/paladinsoftware/sidekiq-debouncer/pull/29))
10
+
1
11
  ## [3.0.0] - 2024-10-22
2
12
  - Complete rewrite of the library ([#25](https://github.com/paladinsoftware/sidekiq-debouncer/pull/25))
3
13
  - Read only Web UI ([#26](https://github.com/paladinsoftware/sidekiq-debouncer/pull/26))
4
14
  - Drop support for sidekiq 6.x and ruby 2.7 ([#28](https://github.com/paladinsoftware/sidekiq-debouncer/pull/28))
5
15
  - Respect sidekiq_options overridden by .set ([#27](https://github.com/paladinsoftware/sidekiq-debouncer/pull/27))
6
16
 
17
+ **Upgrade notes:**
18
+ Since the job format changed, V3 won't debounce jobs enqueued with V2, although they'll still get executed
19
+
7
20
  ## [2.0.2] - 2023-03-13
8
21
  - support Sidekiq::Testing
9
22
 
@@ -11,7 +24,7 @@
11
24
  - don't remove debounce key in redis to avoid invalid debouncing
12
25
 
13
26
  ## [2.0.0] - 2023-02-28
14
- Complete rewrite of the library:
27
+ Complete rewrite of the library:
15
28
  - Instead of iterating through whole schedule set, sidekiq-debouncer will now cache debounce key in redis with a reference to the job.
16
29
  Thanks to that there is a huge performance boost compared to V1. With 1k jobs in schedule set it's over 100x faster.
17
30
  The difference is even bigger with larger amount of jobs.
data/README.md CHANGED
@@ -99,6 +99,12 @@ Sidekiq.configure_client do |config|
99
99
  end
100
100
  ```
101
101
 
102
+ ## Middleware context
103
+
104
+ When multiple jobs are debounced together, the full job hash from the **last job** is preserved. This means any context added by other middlewares (like `acts_as_tenant`, Sidekiq's `CurrentAttributes`, or custom middleware data) will be taken from the most recent job.
105
+
106
+ For example, if you debounce 3 jobs with different tenant contexts, the final executed job will use the tenant from the 3rd (last) job.
107
+
102
108
  ## Web UI
103
109
  Add `require 'sidekiq/debouncer/web'` after `require 'sidekiq/web'`.
104
110
 
@@ -18,11 +18,7 @@ module Sidekiq
18
18
  while !@done && (job, score = zpopbyscore_withscore(conn, [Sidekiq::Debouncer::SET], [Time.now.to_f.to_s]))
19
19
  job_args = zpopbyscore_multi(conn, [job], [score])
20
20
 
21
- final_args = job_args.map { |elem| Sidekiq.load_json(elem.split("-", 2)[1]) }
22
- job_class = job.split("/")[2]
23
- klass = Object.const_get(job_class)
24
-
25
- @client.push({"args" => final_args, "class" => klass, "debounce_key" => job})
21
+ @client.push(JobBuilder.build(job_args, job))
26
22
 
27
23
  logger.debug { "enqueued #{Sidekiq::Debouncer::SET}: #{job}" }
28
24
  end
@@ -3,13 +3,11 @@
3
3
  module Sidekiq
4
4
  module Debouncer
5
5
  class Job
6
- include Sidekiq::JobUtil
7
-
8
6
  attr_reader :key, :score
9
7
 
10
8
  def initialize(key, score)
11
9
  @key = key
12
- @score = score
10
+ @score = Float(score)
13
11
  end
14
12
 
15
13
  def at
@@ -17,12 +15,11 @@ module Sidekiq
17
15
  end
18
16
 
19
17
  def args
20
- @_args ||= Sidekiq.redis { |conn| conn.call("ZRANGE", key, "-inf", "+inf", "BYSCORE") }
21
- .map { |elem| Sidekiq.load_json(elem.split("-", 2)[1]) }
18
+ item["args"]
22
19
  end
23
20
 
24
21
  def queue
25
- normalized["queue"]
22
+ item["queue"]
26
23
  end
27
24
 
28
25
  def klass
@@ -31,10 +28,11 @@ module Sidekiq
31
28
 
32
29
  alias_method :display_class, :klass
33
30
 
34
- private
35
-
36
- def normalized
37
- @_normalized ||= normalize_item({"args" => args, "class" => Object.const_get(klass), "debounce_key" => key})
31
+ def item
32
+ @_item ||= begin
33
+ job_args = Sidekiq.redis { |conn| conn.call("ZRANGE", key, "-inf", "+inf", "BYSCORE") }
34
+ JobBuilder.build(job_args, key)
35
+ end
38
36
  end
39
37
  end
40
38
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Debouncer
5
+ module JobBuilder
6
+ extend Sidekiq::JobUtil
7
+
8
+ def self.build(job_args, debounce_key)
9
+ base_job = nil
10
+ final_args = job_args.map do |elem|
11
+ if elem.start_with?("{")
12
+ base_job = Sidekiq.load_json(elem)
13
+ base_job["args"]
14
+ else
15
+ Sidekiq.load_json(elem.split("-", 2)[1])
16
+ end
17
+ end
18
+
19
+ if base_job
20
+ base_job.merge("args" => final_args, "debounce_key" => debounce_key)
21
+ else
22
+ # Old format fallback - normalize to get queue from class
23
+ job_class = debounce_key.split("/")[2]
24
+ normalize_item("args" => final_args, "class" => Object.const_get(job_class), "debounce_key" => debounce_key)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "securerandom"
4
-
5
3
  module Sidekiq
6
4
  module Debouncer
7
5
  module Middleware
@@ -42,10 +40,10 @@ module Sidekiq
42
40
 
43
41
  return job.merge("args" => [job["args"]], "debounce_key" => key) if testing?
44
42
 
45
- args_stringified = "#{SecureRandom.hex(12)}-#{Sidekiq.dump_json(job["args"])}"
43
+ job_stringified = Sidekiq.dump_json(job)
46
44
 
47
45
  redis do |connection|
48
- redis_debounce(connection, [Sidekiq::Debouncer::SET, key], [args_stringified, time, @debounce_key_ttl])
46
+ redis_debounce(connection, [Sidekiq::Debouncer::SET, key], [job_stringified, time, @debounce_key_ttl])
49
47
  end
50
48
 
51
49
  # prevent normal sidekiq flow
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Debouncer
5
- VERSION = "3.0.0"
5
+ VERSION = "3.2.0"
6
6
  end
7
7
  end
@@ -1,48 +1,50 @@
1
- <header class='row'>
2
- <div class='col-sm-5 pull-left'>
3
- <h3><%= t('Debounces') %></h3>
4
- </div>
5
- <div class='col-sm-7 pull-right'>
6
- <%= erb :_paging, locals: { url: "#{root_path}debounces" } %>
7
- </div>
8
- </header>
9
-
10
- <% if @debounces.size > 0 %>
11
- <form action="<%= root_path %>debounces" method="post">
12
- <%= csrf_tag %>
13
- <div class="table_container">
14
- <table class="table table-striped table-bordered table-hover">
15
- <thead>
16
- <tr>
17
- <th class="table-checkbox checkbox-column">
18
- <label>
19
- <input type="checkbox" class="check_all" />
20
- </label>
21
- </th>
22
- <th><%= t('When') %></th>
23
- <th><%= t('DebounceKey') %></th>
24
- </tr>
25
- </thead>
26
- <% @debounces.each do |entry| %>
27
- <tr>
28
- <td class="table-checkbox">
29
- <label>
30
- <input type='checkbox' name='key[]' value='<%= entry.key %>' class='shift_clickable' />
31
- </label>
32
- </td>
33
- <td>
34
- <a href="<%= root_path %>debounces/<%= ::Base64.urlsafe_encode64(entry.key) %>"><%= relative_time(entry.at) %></a>
35
- </td>
36
- <td>
37
- <a href="<%= root_path %>debounces/<%= ::Base64.urlsafe_encode64(entry.key) %>"><%= entry.key %></a>
38
- </td>
39
- </tr>
40
- <% end %>
41
- </table>
1
+ <section>
2
+ <header class='row'>
3
+ <div class='col-sm-5 pull-left'>
4
+ <h3><%= t('Debounces') %></h3>
5
+ </div>
6
+ <div class='col-sm-7 pull-right'>
7
+ <%= erb :_paging, locals: { url: "#{root_path}debounces" } %>
42
8
  </div>
43
- <!-- <input class="btn btn-danger pull-right flip" type="submit" name="delete" value="<%#= t('Delete') %>" />-->
44
- <!-- <input class="btn btn-danger pull-right flip" type="submit" name="add_to_queue" value="<%#= t('AddToQueue') %>" />-->
45
- </form>
46
- <% else %>
47
- <div class="alert alert-success"><%= t('NoDebouncesFound') %></div>
48
- <% end %>
9
+ </header>
10
+
11
+ <% if @debounces.size > 0 %>
12
+ <form action="<%= root_path %>debounces" method="post">
13
+ <%= csrf_tag %>
14
+ <div class="table_container">
15
+ <table class="table table-striped table-bordered table-hover">
16
+ <thead>
17
+ <tr>
18
+ <th class="table-checkbox checkbox-column">
19
+ <label>
20
+ <input type="checkbox" class="check_all" />
21
+ </label>
22
+ </th>
23
+ <th><%= t('When') %></th>
24
+ <th><%= t('DebounceKey') %></th>
25
+ </tr>
26
+ </thead>
27
+ <% @debounces.each do |entry| %>
28
+ <tr>
29
+ <td class="table-checkbox">
30
+ <label>
31
+ <input type='checkbox' name='key[]' value='<%= entry.key %>' class='shift_clickable' />
32
+ </label>
33
+ </td>
34
+ <td>
35
+ <a href="<%= root_path %>debounces/<%= ::Base64.urlsafe_encode64(entry.key) %>"><%= relative_time(entry.at) %></a>
36
+ </td>
37
+ <td>
38
+ <a href="<%= root_path %>debounces/<%= ::Base64.urlsafe_encode64(entry.key) %>"><%= entry.key %></a>
39
+ </td>
40
+ </tr>
41
+ <% end %>
42
+ </table>
43
+ </div>
44
+ <!-- <input class="btn btn-danger pull-right flip" type="submit" name="delete" value="<%#= t('Delete') %>" />-->
45
+ <!-- <input class="btn btn-danger pull-right flip" type="submit" name="add_to_queue" value="<%#= t('AddToQueue') %>" />-->
46
+ </form>
47
+ <% else %>
48
+ <div class="alert alert-success"><%= t('NoDebouncesFound') %></div>
49
+ <% end %>
50
+ </section>
@@ -1,38 +1,48 @@
1
- <div class="header-container">
2
- <h3><%= t('Job') %></h3>
3
- </div>
1
+ <section>
2
+ <header class="header-container">
3
+ <h3><%= t('Job') %></h3>
4
+ </header>
4
5
 
5
- <div class="table_container">
6
- <table class="table table-bordered table-striped table-hover">
7
- <tbody>
8
- <tr>
9
- <th><%= t('Queue') %></th>
10
- <td><%= @job.queue %></td>
11
- </tr>
12
- <tr>
13
- <th><%= t('When') %></th>
14
- <td><%= relative_time(@job.at) %></td>
15
- </tr>
16
- <tr>
17
- <th><%= t('DebounceKey') %></th>
18
- <td>
19
- <%= @job.key %>
20
- </td>
21
- </tr>
22
- <tr>
23
- <th><%= t('Job') %></th>
24
- <td>
25
- <%= @job.display_class %>
26
- </td>
27
- </tr>
28
- <tr>
29
- <th><%= t('Arguments') %></th>
30
- <td>
31
- <code class="code-wrap">
32
- <div class="args-extended"><%= display_args(@job.args, nil) %></div>
33
- </code>
34
- </td>
35
- </tr>
36
- </tbody>
37
- </table>
38
- </div>
6
+ <div class="table_container">
7
+ <table class="table table-bordered table-striped table-hover">
8
+ <tbody>
9
+ <tr>
10
+ <th><%= t('Queue') %></th>
11
+ <td><%= @job.queue %></td>
12
+ </tr>
13
+ <tr>
14
+ <th><%= t('When') %></th>
15
+ <td><%= relative_time(@job.at) %></td>
16
+ </tr>
17
+ <tr>
18
+ <th><%= t('DebounceKey') %></th>
19
+ <td>
20
+ <%= @job.key %>
21
+ </td>
22
+ </tr>
23
+ <tr>
24
+ <th><%= t('Job') %></th>
25
+ <td>
26
+ <%= @job.display_class %>
27
+ </td>
28
+ </tr>
29
+ <tr>
30
+ <th><%= t('Arguments') %></th>
31
+ <td>
32
+ <code class="code-wrap">
33
+ <div class="args-extended"><%= display_args(@job.args, nil) %></div>
34
+ </code>
35
+ </td>
36
+ </tr>
37
+ <% unless retry_extra_items(@job).empty? %>
38
+ <tr>
39
+ <th><%= t('Extras') %></th>
40
+ <td>
41
+ <code><%= retry_extra_items(@job).inspect %></code>
42
+ </td>
43
+ </tr>
44
+ <% end %>
45
+ </tbody>
46
+ </table>
47
+ </div>
48
+ </section>
@@ -3,6 +3,24 @@
3
3
  if defined?(Sidekiq::Web)
4
4
  require "sidekiq/debouncer/web_extension"
5
5
 
6
- Sidekiq::Web.register Sidekiq::Debouncer::WebExtension
7
- Sidekiq::Web.tabs["Debounces"] = "debounces"
6
+ if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("8.0.0")
7
+ Sidekiq::Web.configure do |config|
8
+ config.register(
9
+ Sidekiq::Debouncer::WebExtension,
10
+ name: "debounces",
11
+ tab: "Debounces",
12
+ index: "debounces"
13
+ )
14
+ end
15
+ elsif Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("7.3.0")
16
+ Sidekiq::Web.register(
17
+ Sidekiq::Debouncer::WebExtension,
18
+ name: "debounces",
19
+ tab: "Debounces",
20
+ index: "debounces"
21
+ )
22
+ else
23
+ Sidekiq::Web.register Sidekiq::Debouncer::WebExtension
24
+ Sidekiq::Web.tabs["Debounces"] = "debounces"
25
+ end
8
26
  end
@@ -1,18 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "base64"
4
+ require "sidekiq/api"
5
+ require "sidekiq/debouncer/set"
4
6
 
5
7
  module Sidekiq
6
8
  module Debouncer
7
9
  module WebExtension
10
+ module Helpers
11
+ def get_route_param(key)
12
+ if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("8.0.0")
13
+ route_params(key)
14
+ else
15
+ route_params[key]
16
+ end
17
+ end
18
+
19
+ def get_url_param(key)
20
+ if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("8.0.0")
21
+ url_params(key)
22
+ else
23
+ params[key]
24
+ end
25
+ end
26
+ end
27
+
8
28
  def self.registered(app)
9
- app.settings.locales << File.join(File.expand_path("..", __FILE__), "locales")
29
+ locales = if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("8.0.0")
30
+ Sidekiq::Web.configure.locales
31
+ else
32
+ app.settings.locales
33
+ end
34
+
35
+ locales << File.join(File.expand_path("..", __FILE__), "locales")
36
+
37
+ app.helpers(Helpers)
10
38
 
11
39
  app.get "/debounces" do
12
40
  view_path = File.join(File.expand_path("..", __FILE__), "views")
13
41
 
14
- @count = (params["count"] || 25).to_i
15
- (@current_page, @total_size, @debounces) = page("debouncer", params["page"], @count)
42
+ @count = (get_url_param("count") || 25).to_i
43
+ (@current_page, @total_size, @debounces) = page("debouncer", get_url_param("page"), @count)
16
44
  @debounces = @debounces.map { |key, score| Sidekiq::Debouncer::Job.new(key, score) }
17
45
 
18
46
  render(:erb, File.read(File.join(view_path, "index.html.erb")))
@@ -21,7 +49,7 @@ module Sidekiq
21
49
  app.get "/debounces/:key" do
22
50
  view_path = File.join(File.expand_path("..", __FILE__), "views")
23
51
 
24
- @job = Sidekiq::Debouncer::Set.new.fetch_by_key(Base64.urlsafe_decode64(route_params[:key]))
52
+ @job = Sidekiq::Debouncer::Set.new.fetch_by_key(Base64.urlsafe_decode64(get_route_param(:key)))
25
53
 
26
54
  render(:erb, File.read(File.join(view_path, "show.html.erb")))
27
55
  end
@@ -3,10 +3,10 @@
3
3
  require "sidekiq/debouncer/version"
4
4
  require "sidekiq/debouncer/errors"
5
5
  require "sidekiq/debouncer/lua_commands"
6
+ require "sidekiq/debouncer/job_builder"
6
7
  require "sidekiq/debouncer/middleware/client"
7
8
  require "sidekiq/debouncer/middleware/server"
8
9
  require "sidekiq/debouncer/job"
9
- require "sidekiq/debouncer/set"
10
10
 
11
11
  module Sidekiq
12
12
  module Debouncer
@@ -27,7 +27,7 @@ Gem::Specification.new do |gem|
27
27
  "sidekiq-debouncer.gemspec"
28
28
  ]
29
29
 
30
- gem.add_dependency "sidekiq", ">= 7.0", "< 8.0"
30
+ gem.add_dependency "sidekiq", ">= 7.0", "< 9.0"
31
31
 
32
32
  gem.add_development_dependency "rspec", "~> 3.12.0"
33
33
  gem.add_development_dependency "timecop", "~> 0.9.6"
@@ -35,4 +35,5 @@ Gem::Specification.new do |gem|
35
35
  gem.add_development_dependency "parallel", "~> 1.22.1"
36
36
  gem.add_development_dependency "standard", "~> 1.24.3"
37
37
  gem.add_development_dependency "rack-test", "~> 2.1.0"
38
+ gem.add_development_dependency "rack-session", "~> 2.1.0"
38
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-debouncer
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Zuchmański
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-10-22 00:00:00.000000000 Z
12
+ date: 2026-02-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
@@ -20,7 +20,7 @@ dependencies:
20
20
  version: '7.0'
21
21
  - - "<"
22
22
  - !ruby/object:Gem::Version
23
- version: '8.0'
23
+ version: '9.0'
24
24
  type: :runtime
25
25
  prerelease: false
26
26
  version_requirements: !ruby/object:Gem::Requirement
@@ -30,7 +30,7 @@ dependencies:
30
30
  version: '7.0'
31
31
  - - "<"
32
32
  - !ruby/object:Gem::Version
33
- version: '8.0'
33
+ version: '9.0'
34
34
  - !ruby/object:Gem::Dependency
35
35
  name: rspec
36
36
  requirement: !ruby/object:Gem::Requirement
@@ -115,6 +115,20 @@ dependencies:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
117
  version: 2.1.0
118
+ - !ruby/object:Gem::Dependency
119
+ name: rack-session
120
+ requirement: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 2.1.0
125
+ type: :development
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 2.1.0
118
132
  description: |
119
133
  Sidekiq extension that adds the ability to debounce job execution.
120
134
  Worker will postpone its execution after `wait time` have elapsed since the last time it was invoked.
@@ -134,6 +148,7 @@ files:
134
148
  - lib/sidekiq/debouncer/enq.rb
135
149
  - lib/sidekiq/debouncer/errors.rb
136
150
  - lib/sidekiq/debouncer/job.rb
151
+ - lib/sidekiq/debouncer/job_builder.rb
137
152
  - lib/sidekiq/debouncer/launcher.rb
138
153
  - lib/sidekiq/debouncer/locales/en.yml
139
154
  - lib/sidekiq/debouncer/lua/debounce.lua
@@ -169,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
184
  - !ruby/object:Gem::Version
170
185
  version: '0'
171
186
  requirements: []
172
- rubygems_version: 3.3.26
187
+ rubygems_version: 3.5.22
173
188
  signing_key:
174
189
  specification_version: 4
175
190
  summary: Sidekiq extension that adds the ability to debounce job execution