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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.adoc +104 -0
- data/lib/sidekiq/antidote/class_qualifier.rb +102 -0
- data/lib/sidekiq/antidote/config.rb +66 -0
- data/lib/sidekiq/antidote/inhibitor.rb +55 -0
- data/lib/sidekiq/antidote/middlewares/client.rb +20 -0
- data/lib/sidekiq/antidote/middlewares/server.rb +20 -0
- data/lib/sidekiq/antidote/middlewares/shared.rb +27 -0
- data/lib/sidekiq/antidote/remedy.rb +61 -0
- data/lib/sidekiq/antidote/repository.rb +92 -0
- data/lib/sidekiq/antidote/version.rb +7 -0
- data/lib/sidekiq/antidote/web.rb +57 -0
- data/lib/sidekiq/antidote.rb +128 -0
- data/web/locales/en.yml +8 -0
- data/web/views/add.html.erb +31 -0
- data/web/views/index.html.erb +34 -0
- metadata +92 -0
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,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
|
data/web/locales/en.yml
ADDED
@@ -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: []
|