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