sidekiq-pauzer 1.0.0.alpha

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: b38aee70ca050a4c5c0f8c41d81bae5b755bb4a73fd1b5ddb7b365fb61ba3f40
4
+ data.tar.gz: e233b4b60fc065a12c29f809ad196f763f05a54d7bec640c338dce6ec2c62984
5
+ SHA512:
6
+ metadata.gz: 8e106b6c3e162eafddf56da121c6d3af3319928a145c1991f9c09ceeb54751eb68a15f716731e5bf2feb15fd785a7062ce19db5523a79b8a3be2e1889af39c94
7
+ data.tar.gz: 1db224ec249b5729e903a0ad8fc9f58235e8a39c5bc39f00a2e11a426c3a0ec20fb216faa5bf144ef20bbfa93f4dc94d8599546ab46052724142a89420e29a07
data/README.adoc ADDED
@@ -0,0 +1,67 @@
1
+ = Sidekiq::Pauzer
2
+
3
+
4
+ == Installation
5
+
6
+ Add this line to your application's Gemfile:
7
+
8
+ $ bundle add sidekiq-pauzer
9
+
10
+ Or install it yourself as:
11
+
12
+ $ gem install sidekiq-pauzer
13
+
14
+
15
+ == Usage
16
+
17
+ [source, ruby]
18
+ ----
19
+ require "sidekiq"
20
+ require "sidekiq/pauzer"
21
+
22
+ Sidekiq::Pauzer.configure do |config|
23
+ # Set redis key prefix.
24
+ # Default: nil
25
+ config.key_prefix = "my-app:"
26
+
27
+ # Set paused queues local cache refresh rate in seconds.
28
+ # Default: 10
29
+ config.refresh_rate = 15
30
+ end
31
+
32
+ Sidekiq.configure_server do |config|
33
+ config[:fetch_class] = Sidekiq::Pauzer::BasicFetch
34
+ end
35
+ ----
36
+
37
+
38
+ === Adding Pause/Resume Button to the Queues Tab
39
+
40
+ If you're not overriding `Sidekiq::Web.views` path, then you can override
41
+ default queues tab with:
42
+
43
+ [source, ruby]
44
+ ----
45
+ require "sidekiq/web"
46
+ require "sidekiq/pauzer/web"
47
+ ----
48
+
49
+ NOTE: If you are using custom Sidekiq views path, then you will need to call
50
+ (after requiring `sidekiq/pauzer/web`): `Sidekiq::Pauzer.unpatch_views!`.
51
+
52
+
53
+ == Development
54
+
55
+ scripts/update-gemfiles
56
+ scripts/run-rspec
57
+ bundle exec rubocop
58
+
59
+
60
+ == Contributing
61
+
62
+ * Fork sidekiq-pauzer
63
+ * Make your changes
64
+ * Ensure all tests pass (`bundle exec rake`)
65
+ * Send a merge request
66
+ * If we like them we'll merge them
67
+ * If we've accepted a patch, feel free to ask for commit access!
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Pauzer
5
+ module Adapters
6
+ # redis-rb adapter
7
+ module Redis
8
+ class << self
9
+ def adapts?(redis)
10
+ return true if defined?(::Redis) && redis.is_a?(::Redis)
11
+ return true if defined?(::Redis::Namespace) && redis.is_a?(::Redis::Namespace)
12
+
13
+ false
14
+ end
15
+
16
+ def pause!(redis, key, queue)
17
+ redis.sadd(key, queue)
18
+ end
19
+
20
+ def unpause!(redis, key, queue)
21
+ redis.srem(key, queue)
22
+ end
23
+
24
+ def paused_queues(redis, key)
25
+ # Cursor is not atomic, so there may be duplicates because of
26
+ # concurrent update operations
27
+ # See: https://redis.io/commands/scan/#scan-guarantees
28
+ redis.sscan_each(key).to_a.uniq.each(&:freeze)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Pauzer
5
+ module Adapters
6
+ # redis-client adapter
7
+ module RedisClient
8
+ SIDEKIQ_SEVEN = Gem::Version.new("7.0.0").freeze
9
+ SIDEKIQ_VERSION = Gem::Version.new(Sidekiq::VERSION).freeze
10
+
11
+ class << self
12
+ def adapts?(redis)
13
+ return true if SIDEKIQ_SEVEN <= SIDEKIQ_VERSION
14
+ return true if defined?(::RedisClient) && redis.is_a?(::RedisClient)
15
+ return true if defined?(::RedisClient::Decorator::Client) && redis.is_a?(::RedisClient::Decorator::Client)
16
+
17
+ false
18
+ end
19
+
20
+ def pause!(redis, key, queue)
21
+ redis.call("SADD", key, queue)
22
+ end
23
+
24
+ def unpause!(redis, key, queue)
25
+ redis.call("SREM", key, queue)
26
+ end
27
+
28
+ def paused_queues(redis, key)
29
+ # Cursor is not atomic, so there may be duplicates because of
30
+ # concurrent update operations
31
+ # See: https://redis.io/commands/scan/#scan-guarantees
32
+ redis.sscan(key).to_a.uniq.each(&:freeze)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./adapters/redis"
4
+ require_relative "./adapters/redis_client"
5
+
6
+ module Sidekiq
7
+ module Pauzer
8
+ # @api internal
9
+ module Adapters
10
+ def self.[](redis)
11
+ return Adapters::RedisClient if Adapters::RedisClient.adapts?(redis)
12
+ return Adapters::Redis if Adapters::Redis.adapts?(redis)
13
+
14
+ raise TypeError, "Unsupported redis client: #{redis.class}"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/fetch"
5
+
6
+ module Sidekiq
7
+ module Pauzer
8
+ # Default Sidekiq's BasicFetch infused with Pauzer
9
+ class BasicFetch < Sidekiq::BasicFetch
10
+ private
11
+
12
+ if Gem::Version.new("7.0.0") <= Gem::Version.new(Sidekiq::VERSION)
13
+ def queues_cmd
14
+ if @strictly_ordered_queues
15
+ @queues - Pauzer.paused_queues
16
+ else
17
+ permute = (@queues - Pauzer.paused_queues)
18
+ permute.shuffle!
19
+ permute.uniq!
20
+ permute
21
+ end
22
+ end
23
+ else
24
+ def queues_cmd
25
+ if @strictly_ordered_queues
26
+ *queues, timeout = @queues
27
+
28
+ (queues - Pauzer.paused_queues) << timeout
29
+ else
30
+ permute = (@queues - Pauzer.paused_queues)
31
+ permute.shuffle!
32
+ permute.uniq!
33
+ permute << { timeout: Sidekiq::BasicFetch::TIMEOUT }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Pauzer
5
+ class Config
6
+ REDIS_KEY = "sidekiq-pauzer"
7
+ private_constant :REDIS_KEY
8
+
9
+ # Default refresh rate
10
+ REFRESH_RATE = 10
11
+
12
+ # @return [String?]
13
+ attr_reader :key_prefix
14
+
15
+ # @return [Integer, Float]
16
+ attr_reader :refresh_rate
17
+
18
+ # Fully qualified Redis key
19
+ #
20
+ # @example Without key prefix (default)
21
+ # config.redis_key # => "sidekiq-pauzer"
22
+ #
23
+ # @example With key prefix
24
+ # config.key_prefix = "foobar:"
25
+ # config.redis_key # => "foobar:sidekiq-pauzer"
26
+ #
27
+ # @return [String]
28
+ attr_reader :redis_key
29
+
30
+ def initialize
31
+ @key_prefix = nil
32
+ @redis_key = REDIS_KEY
33
+ @refresh_rate = REFRESH_RATE
34
+ end
35
+
36
+ # Set redis key prefix.
37
+ #
38
+ # @see redis_key
39
+ # @param value [String?] String that should be prepended to redis key
40
+ # @return [void]
41
+ def key_prefix=(value)
42
+ raise ArgumentError, "expected String, or nil; got #{value.class}" unless value.is_a?(String) || value.nil?
43
+
44
+ @redis_key = [value, REDIS_KEY].compact.join.freeze
45
+ @key_prefix = value&.then(&:-@) # Don't freeze original String value if it was unfrozen
46
+ end
47
+
48
+ # Set paused queues local cache refresh rate in seconds.
49
+ #
50
+ # @param value [Float, Integer] refresh interval in seconds
51
+ # @return [void]
52
+ def refresh_rate=(value)
53
+ unless value.is_a?(Integer) || value.is_a?(Float)
54
+ raise ArgumentError, "expected Integer, or Float; got #{value.class}"
55
+ end
56
+
57
+ raise ArgumentError, "expected positive value; got #{value.inspect}" unless value.positive?
58
+
59
+ @refresh_rate = value
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ require_relative "./adapters"
6
+
7
+ module Sidekiq
8
+ module Pauzer
9
+ # @api internal
10
+ class Queues
11
+ include Enumerable
12
+
13
+ class Refresher < Concurrent::TimerTask; end
14
+
15
+ QUEUE_PREFIX = "queue:"
16
+
17
+ # @param config [Config]
18
+ def initialize(config)
19
+ @mutex = Mutex.new
20
+ @queues = []
21
+ @redis_key = config.redis_key
22
+ @refresher = initialize_refresher(config.refresh_rate)
23
+ end
24
+
25
+ def each(&block)
26
+ return to_enum __method__ unless block
27
+
28
+ @mutex.synchronize { @queues.dup }.each(&block)
29
+
30
+ self
31
+ end
32
+
33
+ def pause!(queue)
34
+ queue = normalize_queue_name(queue)
35
+
36
+ Sidekiq.redis { |conn| Adapters[conn].pause!(conn, @redis_key, queue) }
37
+
38
+ refresh
39
+ end
40
+
41
+ def unpause!(queue)
42
+ queue = normalize_queue_name(queue)
43
+
44
+ Sidekiq.redis { |conn| Adapters[conn].unpause!(conn, @redis_key, queue) }
45
+
46
+ refresh
47
+ end
48
+
49
+ def paused?(queue)
50
+ include?(normalize_queue_name(queue))
51
+ end
52
+
53
+ def start_refresher
54
+ @refresher.execute
55
+ nil
56
+ end
57
+
58
+ def stop_refresher
59
+ @refresher.shutdown
60
+ nil
61
+ end
62
+
63
+ def refresher_running?
64
+ @refresher.running?
65
+ end
66
+
67
+ private
68
+
69
+ def initialize_refresher(refresh_rate)
70
+ Refresher.new(execution_interval: refresh_rate, run_now: true) do
71
+ refresh
72
+ end
73
+ end
74
+
75
+ def refresh
76
+ @mutex.synchronize do
77
+ paused_queues = Sidekiq.redis do |conn|
78
+ Adapters[conn].paused_queues(conn, @redis_key)
79
+ end
80
+
81
+ @queues.replace(paused_queues)
82
+ end
83
+
84
+ self
85
+ end
86
+
87
+ def normalize_queue_name(queue)
88
+ queue.dup.delete_prefix(QUEUE_PREFIX)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Pauzer
5
+ VERSION = "1.0.0.alpha"
6
+ end
7
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "sidekiq"
5
+ require "sidekiq/web"
6
+
7
+ require_relative "../pauzer"
8
+
9
+ module Sidekiq
10
+ module Pauzer
11
+ def self.unpatch_views!
12
+ WebAction.remove_method(:_erb_queues)
13
+ end
14
+ end
15
+
16
+ class WebApplication
17
+ @routes[Sidekiq::WebRouter::POST].delete_if do |web_route|
18
+ web_route.pattern == "/queues/:name"
19
+ end
20
+
21
+ post "/queues/:name" do
22
+ queue = Sidekiq::Queue.new(route_params[:name])
23
+
24
+ if params["pause"]
25
+ queue.pause!
26
+ elsif params["unpause"]
27
+ queue.unpause!
28
+ else
29
+ queue.clear
30
+ end
31
+
32
+ redirect "#{root_path}queues"
33
+ end
34
+ end
35
+
36
+ class WebAction
37
+ PAUZER_QUEUES_TEMPLATE =
38
+ ERB.new(File.read(File.expand_path("../../../web/views/queues.erb", __dir__))).src
39
+
40
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
41
+ def _erb_queues
42
+ #{PAUZER_QUEUES_TEMPLATE}
43
+ end
44
+ RUBY
45
+ end
46
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "sidekiq"
5
+ require "sidekiq/api"
6
+
7
+ require_relative "./pauzer/basic_fetch"
8
+ require_relative "./pauzer/config"
9
+ require_relative "./pauzer/queues"
10
+ require_relative "./pauzer/version"
11
+
12
+ begin
13
+ require "sidekiq-ent/version"
14
+ raise "sidekiq-pauzer is incompatible with Sidekiq Enterprise"
15
+ rescue LoadError
16
+ # All good - no compatibility issues
17
+ end
18
+
19
+ begin
20
+ require "sidekiq/pro/version"
21
+
22
+ raise "sidekiq-pauzer is incompatible with Sidekiq Pro"
23
+ rescue LoadError
24
+ # All good - no compatibility issues
25
+ end
26
+
27
+ module Sidekiq
28
+ module Pauzer
29
+ MUTEX = Mutex.new
30
+
31
+ @config = Config.new
32
+ @queues = Queues.new(@config)
33
+
34
+ class << self
35
+ extend Forwardable
36
+
37
+ def_delegators :@queues, :pause!, :unpause!, :paused?
38
+
39
+ def paused_queues
40
+ @queues.map { |queue| "#{Queues::QUEUE_PREFIX}#{queue}" }
41
+ end
42
+
43
+ def configure
44
+ MUTEX.synchronize do
45
+ yield @config
46
+ ensure
47
+ start_refresher = @queues.refresher_running?
48
+ @queues.stop_refresher
49
+ @queues = Queues.new(@config)
50
+ @queues.start_refresher if start_refresher
51
+ end
52
+ end
53
+
54
+ def startup
55
+ MUTEX.synchronize { @queues.start_refresher }
56
+ end
57
+
58
+ def shutdown
59
+ MUTEX.synchronize { @queues.stop_refresher }
60
+ end
61
+ end
62
+ end
63
+
64
+ class Queue
65
+ remove_method :paused?
66
+
67
+ def paused?
68
+ Pauzer.paused?(name)
69
+ end
70
+
71
+ def pause!
72
+ Pauzer.pause!(name)
73
+ end
74
+
75
+ def unpause!
76
+ Pauzer.unpause!(name)
77
+ end
78
+ end
79
+
80
+ configure_server do |config|
81
+ config.on(:startup) { Pauzer.startup }
82
+ config.on(:shutdown) { Pauzer.shutdown }
83
+ end
84
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./sidekiq/pauzer"
@@ -0,0 +1,38 @@
1
+ <div class="header-container">
2
+ <h1><%= t('Queues') %></h1>
3
+ </div>
4
+
5
+ <div class="table_container">
6
+ <table class="queues table table-hover table-bordered table-striped">
7
+ <thead>
8
+ <th><%= t('Queue') %></th>
9
+ <th><%= t('Size') %></th>
10
+ <th><%= t('Latency') %></th>
11
+ <th><%= t('Actions') %></th>
12
+ </thead>
13
+ <% @queues.each do |queue| %>
14
+ <tr>
15
+ <td>
16
+ <a href="<%= root_path %>queues/<%= CGI.escape(queue.name) %>"><%= h queue.name %></a>
17
+ <% if queue.paused? %>
18
+ <span class="label label-danger"><%= t('Paused') %></span>
19
+ <% end %>
20
+ </td>
21
+ <td><%= number_with_delimiter(queue.size) %> </td>
22
+ <td><% queue_latency = queue.latency %><%= number_with_delimiter(queue_latency.round(2)) %><%= (queue_latency < 60) ? '' : " (#{relative_time(Time.at(Time.now.to_f - queue_latency))})" %> </td>
23
+ <td class="delete-confirm">
24
+ <form action="<%=root_path %>queues/<%= CGI.escape(queue.name) %>" method="post">
25
+ <%= csrf_tag %>
26
+ <input class="btn btn-danger" type="submit" name="delete" title="This will delete all jobs within the queue, it will reappear if you push more jobs to it in the future." value="<%= t('Delete') %>" data-confirm="<%= t('AreYouSureDeleteQueue', :queue => h(queue.name)) %>" />
27
+
28
+ <% if queue.paused? %>
29
+ <input class="btn btn-danger" type="submit" name="unpause" value="<%= t('Unpause') %>" />
30
+ <% else %>
31
+ <input class="btn btn-danger" type="submit" name="pause" value="<%= t('Pause') %>" />
32
+ <% end %>
33
+ </form>
34
+ </td>
35
+ </tr>
36
+ <% end %>
37
+ </table>
38
+ </div>
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-pauzer
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.alpha
5
+ platform: ruby
6
+ authors:
7
+ - Alexey Zapparov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-05-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: sidekiq
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.5'
41
+ description:
42
+ email:
43
+ - alexey@zapparov.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - README.adoc
49
+ - lib/sidekiq-pauzer.rb
50
+ - lib/sidekiq/pauzer.rb
51
+ - lib/sidekiq/pauzer/adapters.rb
52
+ - lib/sidekiq/pauzer/adapters/redis.rb
53
+ - lib/sidekiq/pauzer/adapters/redis_client.rb
54
+ - lib/sidekiq/pauzer/basic_fetch.rb
55
+ - lib/sidekiq/pauzer/config.rb
56
+ - lib/sidekiq/pauzer/queues.rb
57
+ - lib/sidekiq/pauzer/version.rb
58
+ - lib/sidekiq/pauzer/web.rb
59
+ - web/views/queues.erb
60
+ homepage: https://gitlab.com/ixti/sidekiq-pauzer
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ homepage_uri: https://gitlab.com/ixti/sidekiq-pauzer
65
+ source_code_uri: https://gitlab.com/ixti/sidekiq-pauzer/tree/v1.0.0.alpha
66
+ bug_tracker_uri: https://gitlab.com/ixti/sidekiq-pauzer/issues
67
+ changelog_uri: https://gitlab.com/ixti/sidekiq-pauzer/blob/v1.0.0.alpha/CHANGES.md
68
+ rubygems_mfa_required: 'true'
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '2.7'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.3.1
83
+ requirements: []
84
+ rubygems_version: 3.4.10
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Enhance Sidekiq with queue pausing
88
+ test_files: []