sidekiq-antidote 1.0.0.alpha.1

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: 052bc50b4ce029ead4b2a4e6253625cf9a82a9c4071be61945e76336d9d13f18
4
+ data.tar.gz: a12ac47db5f3edee4fba2e600b1da4029db87657a8bde540a7b3f2fb2c07911b
5
+ SHA512:
6
+ metadata.gz: d9cf2a9e0db287b0b6c7d40a4fe99c7927dbb9aee9fdfd8efc06bdb81c91396f6b169271973746d2b543ac34673694c7894338d8c43d9f947f5f17659bafb4b8
7
+ data.tar.gz: 57d9306f0b40cbccd67a1cf9f5d7260ddb59098cf1078fca7aca4ae88df58b669e38e2a2dd6059cb4d1c48ecdbf35e2eb56ff55bcdf957f4955dca33f0dee102
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Alexey Zapparov
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.adoc ADDED
@@ -0,0 +1,104 @@
1
+ = Sidekiq::Antidote
2
+
3
+
4
+ == Installation
5
+
6
+ Add this line to your application's Gemfile:
7
+
8
+ $ bundle add sidekiq-antidote
9
+
10
+ Or install it yourself as:
11
+
12
+ $ gem install sidekiq-antidote
13
+
14
+
15
+ == Usage
16
+
17
+ [source, ruby]
18
+ ----
19
+ require "sidekiq"
20
+ require "sidekiq/antidote"
21
+
22
+ Sidekiq::Antidote.configure do |config|
23
+ # Set redis key prefix.
24
+ # Default: nil
25
+ config.key_prefix = "my-app:"
26
+
27
+ # Set inhibitors cache refresh rate in seconds.
28
+ # Default: 5.0
29
+ config.refresh_rate = 10.0
30
+ end
31
+ ----
32
+
33
+ When running in forked environment (e.g., Puma web server), you also need to
34
+ call `Sidekiq::Antidote.startup` on fork:
35
+
36
+ [source, ruby]
37
+ ----
38
+ # file: config/puma.rb
39
+ on_worker_boot { Sidekiq::Antidote.startup }
40
+ ----
41
+
42
+ === Web UI
43
+
44
+ Adding Antidote tab in Sidekiq web UI is as simple as:
45
+
46
+ [source, ruby]
47
+ ----
48
+ require "sidekiq/web"
49
+ require "sidekiq/antidote/web"
50
+ ----
51
+
52
+
53
+ == Supported Ruby Versions
54
+
55
+ This library aims to support and is tested against the following Ruby versions:
56
+
57
+ * Ruby 3.0.x
58
+ * Ruby 3.1.x
59
+ * Ruby 3.2.x
60
+
61
+ If something doesn't work on one of these versions, it's a bug.
62
+
63
+ This library may inadvertently work (or seem to work) on other Ruby versions,
64
+ however support will only be provided for the versions listed above.
65
+
66
+ If you would like this library to support another Ruby version or
67
+ implementation, you may volunteer to be a maintainer. Being a maintainer
68
+ entails making sure all tests run and pass on that implementation. When
69
+ something breaks on your implementation, you will be responsible for providing
70
+ patches in a timely fashion. If critical issues for a particular implementation
71
+ exist at the time of a major release, support for that Ruby version may be
72
+ dropped.
73
+
74
+
75
+ == Supported Sidekiq Versions
76
+
77
+ This library aims to support and work with following Sidekiq versions:
78
+
79
+ * Sidekiq 7.0.x
80
+ * Sidekiq 7.1.x
81
+ * Sidekiq 7.2.x
82
+
83
+
84
+ == Development
85
+
86
+ bundle install
87
+ bundle exec appraisal generate
88
+ bundle exec appraisal install
89
+ bundle exec rake
90
+
91
+
92
+ == Contributing
93
+
94
+ * Fork sidekiq-antidote
95
+ * Make your changes
96
+ * Ensure all tests pass (`bundle exec rake`)
97
+ * Send a merge request
98
+ * If we like them we'll merge them
99
+ * If we've accepted a patch, feel free to ask for commit access!
100
+
101
+
102
+ == Acknowledgement
103
+
104
+ * Inspired by https://github.com/square/sidekiq-killswitch[sidekiq-killswitch]
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "strscan"
5
+ require "stringio"
6
+
7
+ module Sidekiq
8
+ module Antidote
9
+ # Job display class pattern matcher. Some job classes may be represented by
10
+ # class and metehod name (e.g., `OnboardingMailer#welcome`) which is handled
11
+ # by this qualifier.
12
+ #
13
+ # ## Pattern Special Characters
14
+ #
15
+ # * `*` matches any number of alpha-numeric characters and underscores.
16
+ # Examples: `Foo`, `FooBar`, `method_name`
17
+ # * `**` matches any number of components.
18
+ # Examples: `Foo`, `Foo::Bar`, `Foo::Bar#method_name`
19
+ # * `{A,B,C}` matches literal `A`, `B`, or `C`.
20
+ class ClassQualifier
21
+ extend Forwardable
22
+
23
+ LITERAL = %r{(?:[a-z0-9_\#]|::)+}i
24
+ private_constant :LITERAL
25
+
26
+ WILDCARD = %r{\*+}
27
+ private_constant :WILDCARD
28
+
29
+ ALTERNATION = %r{\{[^*\{]+\}}
30
+ private_constant :ALTERNATION
31
+
32
+ # @return [String]
33
+ attr_reader :pattern
34
+ alias to_s pattern
35
+
36
+ # @return [Regexp]
37
+ attr_reader :regexp
38
+
39
+ # @!method match?(job_class)
40
+ # @param job_class [String]
41
+ # @return [Boolean]
42
+ def_delegator :regexp, :match?
43
+
44
+ # @param pattern [#to_s]
45
+ def initialize(pattern)
46
+ @pattern = pattern.to_s.strip.freeze
47
+ raise ArgumentError, "blank pattern" if @pattern.empty?
48
+
49
+ @regexp = build_regexp(@pattern)
50
+
51
+ freeze
52
+ end
53
+
54
+ # @param other [Object]
55
+ # @return [Boolean]
56
+ def eql?(other)
57
+ self.class == other.class && pattern == other.pattern
58
+ end
59
+ alias == eql?
60
+
61
+ private
62
+
63
+ def build_regexp(pattern)
64
+ scanner = StringScanner.new(pattern)
65
+ parts = StringIO.new
66
+
67
+ until scanner.eos?
68
+ next if consume_literal(scanner, parts)
69
+ next if consume_wildcard(scanner, parts)
70
+ next if consume_alternation(scanner, parts)
71
+
72
+ raise ArgumentError, "invalid token #{scanner.peek(1)} at #{scanner.pos}: #{scanner.string.inspect}"
73
+ end
74
+
75
+ %r{\A#{parts.string}\z}i
76
+ end
77
+
78
+ def consume_literal(scanner, parts)
79
+ scanner.scan(LITERAL)&.then { parts << _1 }
80
+ end
81
+
82
+ def consume_wildcard(scanner, parts)
83
+ scanner.scan(WILDCARD)&.then do |wildcard|
84
+ case wildcard.length
85
+ when 1 then parts << "[a-z0-9_]*"
86
+ when 2 then parts << "(?:(?:\\#|::)?[a-z0-9_]+)*"
87
+ else
88
+ scanner.unscan
89
+ raise ArgumentError, "ambiguous wildcard #{wildcard} at #{scanner.pos}: #{scanner.string.inspect}"
90
+ end
91
+ end
92
+ end
93
+
94
+ def consume_alternation(scanner, parts)
95
+ scanner.scan(ALTERNATION)&.then do |alternation|
96
+ variants = alternation[1...-1].split(",").map { Regexp.escape(_1) }
97
+ parts << "(?:#{variants.join('|')})"
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Antidote
5
+ class Config
6
+ REDIS_KEY = "sidekiq-antidote"
7
+ private_constant :REDIS_KEY
8
+
9
+ # Default refresh rate
10
+ REFRESH_RATE = 5.0
11
+
12
+ # @return [String?]
13
+ attr_reader :key_prefix
14
+
15
+ # @return [Float]
16
+ attr_reader :refresh_rate
17
+
18
+ # Fully qualified Redis key
19
+ #
20
+ # @example Without key prefix (default)
21
+ # config.redis_key # => "sidekiq-antidote"
22
+ #
23
+ # @example With key prefix
24
+ # config.key_prefix = "foobar:"
25
+ # config.redis_key # => "foobar:sidekiq-antidote"
26
+ #
27
+ # @see #key_prefix
28
+ # @return [String]
29
+ attr_reader :redis_key
30
+
31
+ def initialize
32
+ @key_prefix = nil
33
+ @redis_key = REDIS_KEY
34
+ @refresh_rate = REFRESH_RATE
35
+ end
36
+
37
+ # Redis key prefix.
38
+ #
39
+ # @example
40
+ # config.key_prefix = "foobar:"
41
+ # config.redis_key # => "foobar:sidekiq-antidote"
42
+ #
43
+ # @see #redis_key
44
+ # @param value [String, nil] String that should be prepended to redis key
45
+ # @return [void]
46
+ def key_prefix=(value)
47
+ raise ArgumentError, "expected String, or nil; got #{value.class}" unless value.nil? || value.is_a?(String)
48
+
49
+ @redis_key = [value, REDIS_KEY].compact.join.freeze
50
+ @key_prefix = value&.then(&:-@) # Don't freeze original String value if it was unfrozen
51
+ end
52
+
53
+ # Inhibitors cache refresh rate in seconds.
54
+ #
55
+ # @param value [Float] refresh interval in seconds
56
+ # @return [void]
57
+ def refresh_rate=(value)
58
+ unless value.is_a?(Float) && value.positive?
59
+ raise ArgumentError, "expected positive Float; got #{value.inspect}"
60
+ end
61
+
62
+ @refresh_rate = value
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ require_relative "./class_qualifier"
6
+
7
+ module Sidekiq
8
+ module Antidote
9
+ # Single poison inhibition rule: class qualifier + treatment action.
10
+ class Inhibitor
11
+ TREATMENTS = %w[skip kill].freeze
12
+
13
+ # @return [String]
14
+ attr_reader :id
15
+
16
+ # @return ["skip", "kill"]
17
+ attr_reader :treatment
18
+
19
+ # @return [ClassQualifier]
20
+ attr_reader :class_qualifier
21
+
22
+ # @param id [#to_s]
23
+ # @param treatment ["skip", "kill"]
24
+ # @param class_qualifier [#to_s]
25
+ def initialize(id:, treatment:, class_qualifier:)
26
+ @id = -id.to_s
27
+ @treatment = -treatment.to_s
28
+ @class_qualifier = ClassQualifier.new(class_qualifier.to_s)
29
+
30
+ raise ArgumentError, "invalid id: #{id.inspect}" if @id.empty?
31
+ raise ArgumentError, "invalid treatment: #{treatment.inspect}" unless TREATMENTS.include?(@treatment)
32
+
33
+ freeze
34
+ end
35
+
36
+ def lethal?
37
+ "kill" == treatment
38
+ end
39
+
40
+ def match?(job_record)
41
+ class_qualifier.match?(job_record.display_class)
42
+ end
43
+
44
+ def to_s
45
+ "#{treatment} #{class_qualifier}"
46
+ end
47
+
48
+ def eql?(other)
49
+ self.class == other.class \
50
+ && id == other.id && treatment == other.treatment && class_qualifier == other.class_qualifier
51
+ end
52
+ alias == eql?
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./shared"
4
+
5
+ module Sidekiq
6
+ module Antidote
7
+ module Middlewares
8
+ class Client
9
+ include Shared
10
+ include Sidekiq::ClientMiddleware
11
+
12
+ # @see https://github.com/sidekiq/sidekiq/wiki/Middleware
13
+ # @see https://github.com/sidekiq/sidekiq/wiki/Job-Format
14
+ def call(_, job_payload, _, _)
15
+ yield unless inhibit(job_payload)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./shared"
4
+
5
+ module Sidekiq
6
+ module Antidote
7
+ module Middlewares
8
+ class Server
9
+ include Shared
10
+ include Sidekiq::ServerMiddleware
11
+
12
+ # @see https://github.com/sidekiq/sidekiq/wiki/Middleware
13
+ # @see https://github.com/sidekiq/sidekiq/wiki/Job-Format
14
+ def call(_, job_payload, _)
15
+ yield unless inhibit(job_payload)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/api"
4
+
5
+ module Sidekiq
6
+ module Antidote
7
+ module Middlewares
8
+ module Shared
9
+ private
10
+
11
+ # @return [true] if message was inhibited
12
+ # @return [false] otherwise
13
+ def inhibit(message)
14
+ job_record = Sidekiq::JobRecord.new(message)
15
+ inhibitor = Antidote.remedy_for(job_record)
16
+ return false unless inhibitor
17
+
18
+ Antidote.log(:warn) { "I've got a poison! -- #{job_record.display_class}" }
19
+ Antidote.log(:warn) { "I've got a remedy! -- #{inhibitor}" }
20
+ DeadSet.new.kill(Sidekiq.dump_json(message)) if inhibitor.lethal?
21
+
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Sidekiq
6
+ module Antidote
7
+ # Eventually consistent list of inhibitors. Used by middlewares to avoid
8
+ # hitting Redis on every lookup.
9
+ class Remedy
10
+ include Enumerable
11
+
12
+ # @param refresh_rate [Float]
13
+ # @param repository [Repository]
14
+ def initialize(refresh_rate, repository:)
15
+ @inhibitors = [].freeze
16
+ @refresher = Concurrent::TimerTask.new(execution_interval: refresh_rate, run_now: true) do
17
+ @inhibitors = repository.to_a.freeze
18
+ end
19
+ end
20
+
21
+ # @overload each
22
+ # @return [Enumerator<Inhibitor>]
23
+ #
24
+ # @overload each(&block)
25
+ # For a block { |inhibitor| ... }
26
+ # @yieldparam inhibitor [Inhibitor]
27
+ # @return [self]
28
+ def each(&block)
29
+ return to_enum __method__ unless block
30
+
31
+ start_refresher unless refresher_running?
32
+ @inhibitors.each(&block)
33
+
34
+ self
35
+ end
36
+
37
+ # Starts inhibitors list async poller.
38
+ #
39
+ # @return [self]
40
+ def start_refresher
41
+ @refresher.execute
42
+ self
43
+ end
44
+
45
+ # Stops inhibitors list async poller.
46
+ #
47
+ # @return [self]
48
+ def stop_refresher
49
+ @refresher.shutdown
50
+ self
51
+ end
52
+
53
+ # Returns whenever inhibitors list async poller is running.
54
+ #
55
+ # @return [Boolean]
56
+ def refresher_running?
57
+ @refresher.running?
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./inhibitor"
4
+
5
+ module Sidekiq
6
+ module Antidote
7
+ # Inhibitors repository
8
+ class Repository
9
+ include Enumerable
10
+
11
+ # @param redis_key [#to_s]
12
+ def initialize(redis_key)
13
+ @redis_key = -redis_key.to_s
14
+
15
+ freeze
16
+ end
17
+
18
+ # @overload each
19
+ # @return [Enumerator<Inhibitor>]
20
+ #
21
+ # @overload each(&block)
22
+ # For a block { |inhibitor| ... }
23
+ # @yieldparam inhibitor [Inhibitor]
24
+ # @return [self]
25
+ def each
26
+ return to_enum __method__ unless block_given?
27
+
28
+ broken_ids = []
29
+
30
+ redis("HGETALL", @redis_key).each do |id, payload|
31
+ inhibitor = deserialize(id, payload)
32
+ next yield inhibitor if inhibitor
33
+
34
+ broken_ids << id
35
+ end
36
+
37
+ delete(*broken_ids)
38
+ self
39
+ end
40
+
41
+ # @param treatment (see Inhibitor#initialize)
42
+ # @param class_qualifier (see Inhibitor#initialize)
43
+ # @raise [RuntimeError] when can't generate new inhibitor ID
44
+ # @return [Inhibitor]
45
+ def add(treatment:, class_qualifier:)
46
+ 3.times do
47
+ inhibitor = Sidekiq::Antidote::Inhibitor.new(
48
+ id: SecureRandom.hex(8),
49
+ treatment: treatment,
50
+ class_qualifier: class_qualifier
51
+ )
52
+
53
+ return inhibitor if redis("HSETNX", @redis_key, *serialize(inhibitor)).to_i.positive?
54
+ end
55
+
56
+ raise "can't generate available ID"
57
+ end
58
+
59
+ # @param ids [Array<String>]
60
+ # @return [nil]
61
+ def delete(*ids)
62
+ redis("HDEL", @redis_key, *ids) unless ids.empty?
63
+ nil
64
+ end
65
+
66
+ private
67
+
68
+ def deserialize(id, payload)
69
+ treatment, class_qualifier = Sidekiq.load_json(payload)
70
+
71
+ Inhibitor.new(id: id, treatment: treatment, class_qualifier: class_qualifier)
72
+ rescue StandardError => e
73
+ Antidote.log(:error) { "failed deserializing inhibitor (#{payload.inspect}): #{e.message}" }
74
+ nil
75
+ end
76
+
77
+ def serialize(inhibitor)
78
+ [
79
+ inhibitor.id,
80
+ Sidekiq.dump_json([
81
+ inhibitor.treatment,
82
+ inhibitor.class_qualifier.pattern
83
+ ])
84
+ ]
85
+ end
86
+
87
+ def redis(...)
88
+ Sidekiq.redis { _1.call(...) }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Antidote
5
+ VERSION = "1.0.0.alpha.1"
6
+ end
7
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/web"
5
+
6
+ module Sidekiq
7
+ module Antidote
8
+ module Web
9
+ VIEWS = Pathname.new(__dir__).join("../../../web/views").expand_path
10
+
11
+ def self.registered(app) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
12
+ app.get("/antidote") do
13
+ @inhibitors = Antidote.inhibitors
14
+
15
+ erb(VIEWS.join("index.html.erb").read)
16
+ end
17
+
18
+ app.get("/antidote/add") do
19
+ @treatment = Sidekiq::Antidote::Inhibitor::TREATMENTS.first
20
+ @class_qualifier = ""
21
+
22
+ erb(VIEWS.join("add.html.erb").read)
23
+ end
24
+
25
+ app.post("/antidote/add") do
26
+ @treatment = params[:treatment]
27
+ @treatment = "skip" unless Sidekiq::Antidote::Inhibitor::TREATMENTS.include?(@treatment)
28
+ @class_qualifier = params[:class_qualifier]
29
+ @class_qualifier_error = nil
30
+
31
+ begin
32
+ Sidekiq::Antidote::ClassQualifier.new(@class_qualifier)
33
+ rescue StandardError => e
34
+ @class_qualifier_error = e.message
35
+ end
36
+
37
+ if @class_qualifier_error
38
+ erb(VIEWS.join("add.html.erb").read)
39
+ else
40
+ Antidote.add(treatment: @treatment, class_qualifier: @class_qualifier)
41
+ redirect "#{root_path}antidote"
42
+ end
43
+ end
44
+
45
+ app.post("/antidote/:id/delete") do
46
+ Antidote.delete(route_params[:id])
47
+
48
+ redirect "#{root_path}antidote"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ Sidekiq::Web.register(Sidekiq::Antidote::Web)
56
+ Sidekiq::Web.tabs["Antidote"] = "antidote"
57
+ Sidekiq::Web.locales << Pathname.new(__dir__).join("../../../web/locales").expand_path.to_s
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/api"
5
+
6
+ require_relative "./antidote/config"
7
+ require_relative "./antidote/inhibitor"
8
+ require_relative "./antidote/middlewares/client"
9
+ require_relative "./antidote/middlewares/server"
10
+ require_relative "./antidote/remedy"
11
+ require_relative "./antidote/repository"
12
+ require_relative "./antidote/version"
13
+
14
+ module Sidekiq
15
+ module Antidote
16
+ MUTEX = Mutex.new
17
+
18
+ @config = Config.new.freeze
19
+ @repository = Repository.new(@config.redis_key)
20
+ @remedy = Remedy.new(@config.refresh_rate, repository: @repository)
21
+
22
+ class << self
23
+ extend Forwardable
24
+
25
+ # @!method add(treatment:, class_qualifier:)
26
+ # @param (see Repository#add)
27
+ # @return (see Repository#add)
28
+ def_delegators :@repository, :add
29
+
30
+ # @!method delete(*ids)
31
+ # @param (see Repository#delete)
32
+ # @return (see Repository#delete)
33
+ def_delegators :@repository, :delete
34
+
35
+ # @!attribute [r] redis_key
36
+ # @return [String]
37
+ def_delegators :@config, :redis_key
38
+
39
+ # @return [Array<Inhibitor>] Live list of inhibitors
40
+ def inhibitors
41
+ @repository.to_a
42
+ end
43
+
44
+ # @api internal
45
+ # @param job_record [Sidekiq::JobRecord]
46
+ # @return [Inhibitor, nil]
47
+ def remedy_for(job_record)
48
+ @remedy.find { _1.match?(job_record) }
49
+ end
50
+
51
+ # Yields `config` for a block.
52
+ #
53
+ # @example
54
+ # Sidekiq::Antidote.configure do |config|
55
+ # config.refresh_rate = 42.0
56
+ # end
57
+ #
58
+ # @yieldparam config [Config]
59
+ def configure
60
+ MUTEX.synchronize do
61
+ config = @config.dup
62
+
63
+ yield config
64
+
65
+ @config = config.freeze
66
+ @repository = Repository.new(@config.redis_key)
67
+
68
+ self
69
+ ensure
70
+ reinit_remedy
71
+ end
72
+ end
73
+
74
+ # Starts inhibitors poller.
75
+ #
76
+ # @return [self]
77
+ def startup
78
+ MUTEX.synchronize { reinit_remedy.start_refresher }
79
+
80
+ self
81
+ end
82
+
83
+ # Shutdown inhibitors poller.
84
+ #
85
+ # @return [self]
86
+ def shutdown
87
+ MUTEX.synchronize { @remedy.stop_refresher }
88
+
89
+ self
90
+ end
91
+
92
+ # @api internal
93
+ #
94
+ # @return [nil]
95
+ def log(severity)
96
+ Sidekiq.logger.public_send(severity) { "sidekiq-antidote: #{yield}" }
97
+ nil
98
+ end
99
+
100
+ private
101
+
102
+ def reinit_remedy
103
+ @remedy.stop_refresher
104
+ @remedy = Remedy.new(@config.refresh_rate, repository: @repository)
105
+ end
106
+ end
107
+ end
108
+
109
+ # TODO: How to test both configure_{client,server}?
110
+ configure_client do |config|
111
+ config.client_middleware do |chain|
112
+ chain.add Sidekiq::Antidote::Middlewares::Client
113
+ end
114
+ end
115
+
116
+ configure_server do |config|
117
+ config.on(:startup) { Antidote.startup }
118
+ config.on(:shutdown) { Antidote.shutdown }
119
+
120
+ config.client_middleware do |chain|
121
+ chain.add Sidekiq::Antidote::Middlewares::Client
122
+ end
123
+
124
+ config.server_middleware do |chain|
125
+ chain.add Sidekiq::Antidote::Middlewares::Server
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,8 @@
1
+ ---
2
+ en:
3
+ antidote.treatment: Treatment
4
+ antidote.qualifier: Job Class Pattern
5
+ antidote.actions: Actions
6
+ antidote.add: Add Inhibitor
7
+ antidote.submit: Submit
8
+ antidote.no_inhibitors: No inhibitors found
@@ -0,0 +1,31 @@
1
+ <div class="header-container">
2
+ <h1><a href="<%=root_path %>antidote">Antidote</a> / <%= t("antidote.add") %></h1>
3
+ </div>
4
+
5
+ <% if @error %>
6
+ <div class="alert alert-danger"><%= @error %></div>
7
+ <% end %>
8
+
9
+ <form id="antidote-inhibitor" action="<%=root_path %>antidote/add" method="post">
10
+ <%= csrf_tag %>
11
+ <div class="form-row">
12
+ <div class="form-group col-md-2">
13
+ <label for="antidote-inhibitor-treatment"><%= t("antidote.treatment") %></label>
14
+ <select id="antidote-inhibitor-treatment" name="treatment" class="form-control">
15
+ <% Sidekiq::Antidote::Inhibitor::TREATMENTS.each do |treatment| %>
16
+ <option <%= "selected" if treatment == @treatment %>><%= treatment %></option>
17
+ <% end %>
18
+ </select>
19
+ </div>
20
+ <div class="form-group col-md-10">
21
+ <label for="antidote-inhibitor-class-qualifier"><%= t("antidote.qualifier") %></label>
22
+ <input id="antidote-inhibitor-class-qualifier" type="text" class="form-control <%= "is-invalid" if @class_qualifier_error %>" name="class_qualifier" value="<%= @class_qualifier %>">
23
+ <% if @class_qualifier_error %>
24
+ <div id="antidote-inhibitor-class-qualifier-error" class="invalid-feedback"><%= @class_qualifier_error %></div>
25
+ <% end %>
26
+ </div>
27
+ </div>
28
+ <div class="form-row text-right">
29
+ <button id="antidote-inhibitor-submit" type="submit" class="btn"><%= t("antidote.submit") %></button>
30
+ </div>
31
+ </form>
@@ -0,0 +1,34 @@
1
+ <div class="header-container">
2
+ <h1>Antidote</h1>
3
+ <div>
4
+ <a href="<%=root_path %>antidote/add" class="btn"><%= t("antidote.add") %></a>
5
+ </div>
6
+ </div>
7
+
8
+ <% if @inhibitors.empty? %>
9
+ <div class="alert alert-success"><%= t("antidote.no_inhibitors") %></div>
10
+ <% else %>
11
+ <div class="table_container">
12
+ <table class="antidote table table-hover table-bordered table-striped">
13
+ <thead>
14
+ <th><%= t("antidote.treatment") %></th>
15
+ <th><%= t("antidote.qualifier") %></th>
16
+ <th><%= t("antidote.actions") %></th>
17
+ </thead>
18
+ <tbody>
19
+ <% @inhibitors.each do |inhibitor| %>
20
+ <tr id="antidote-inhibitor-<%= CGI.escape(inhibitor.id) %>">
21
+ <td><%= inhibitor.treatment %></td>
22
+ <td><%= inhibitor.class_qualifier.pattern %></td>
23
+ <td class="delete-confirm">
24
+ <form action="<%=root_path %>antidote/<%= CGI.escape(inhibitor.id) %>/delete" method="post">
25
+ <%= csrf_tag %>
26
+ <input class="btn btn-danger" type="submit" name="delete" value="<%= t("Delete") %>" data-confirm="<%= t("AreYouSureDeleteQueue") %>" />
27
+ </form>
28
+ </td>
29
+ </tr>
30
+ <% end %>
31
+ </tbody>
32
+ </table>
33
+ </div>
34
+ <% end %>
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-antidote
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.alpha.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexey Zapparov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-12-02 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: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ description:
42
+ email:
43
+ - alexey@zapparov.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE.txt
49
+ - README.adoc
50
+ - lib/sidekiq/antidote.rb
51
+ - lib/sidekiq/antidote/class_qualifier.rb
52
+ - lib/sidekiq/antidote/config.rb
53
+ - lib/sidekiq/antidote/inhibitor.rb
54
+ - lib/sidekiq/antidote/middlewares/client.rb
55
+ - lib/sidekiq/antidote/middlewares/server.rb
56
+ - lib/sidekiq/antidote/middlewares/shared.rb
57
+ - lib/sidekiq/antidote/remedy.rb
58
+ - lib/sidekiq/antidote/repository.rb
59
+ - lib/sidekiq/antidote/version.rb
60
+ - lib/sidekiq/antidote/web.rb
61
+ - web/locales/en.yml
62
+ - web/views/add.html.erb
63
+ - web/views/index.html.erb
64
+ homepage: https://github.com/ixti/sidekiq-antidote
65
+ licenses:
66
+ - MIT
67
+ metadata:
68
+ homepage_uri: https://github.com/ixti/sidekiq-antidote
69
+ source_code_uri: https://github.com/ixti/sidekiq-antidote/tree/v1.0.0.alpha.1
70
+ bug_tracker_uri: https://github.com/ixti/sidekiq-antidote/issues
71
+ changelog_uri: https://github.com/ixti/sidekiq-antidote/blob/v1.0.0.alpha.1/CHANGES.md
72
+ rubygems_mfa_required: 'true'
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">"
85
+ - !ruby/object:Gem::Version
86
+ version: 1.3.1
87
+ requirements: []
88
+ rubygems_version: 3.4.10
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Sidekiq poison-pill instant remedy
92
+ test_files: []