sidekiq-pauzer 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
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: []