sidekiq-antidote 1.0.0.alpha.1

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: 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: []