sidekiq-rescue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f87fcb8d7e00cd287d289735c0655237d16520c298e9c96b12bbd19f745848d
4
+ data.tar.gz: 2be3ba005f0379b6f124974b0e464c0ac197f174952d2bb50c4be9ec4f6f979b
5
+ SHA512:
6
+ metadata.gz: 0757e9ea80e869a0eb819e4147d2c65b1bb8e6763af382ae2cf5a8910172d93fafeeb96e1d9cc51af18619e26f7dae0e13b43b2b59cdc07d41ea2330b644bbce
7
+ data.tar.gz: 3ab9dcffcbe32a7d26b89791b0c76b1b59d46132afd779ace5d02554f95e2bd418b33ecffb05c89ced58554a25e287d6c5a6a8db13988227c65ab47bbedbd2dd
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-01-20
4
+
5
+ - Initial release
6
+ - Add DSL to configure retries and delay
7
+ - Add middleware to rescue jobs
8
+ - Add specs
9
+ - Add documentation
10
+ - Add CI
11
+
12
+ [Unreleased]: https://github.com/moofkit/sidekiq-rescue/compare/v0.1.0...HEAD
13
+ [0.1.0]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Dmitrii Ivliev
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.md ADDED
@@ -0,0 +1,118 @@
1
+ # Sidekiq::Rescue
2
+
3
+ [![Build Status](https://github.com/moofkit/sidekiq-rescue/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/moofkit/sidekiq-rescue/actions/workflows/main.yml)
4
+
5
+ [Sidekiq](https://github.com/sidekiq/sidekiq) plugin to rescue jobs from expected errors and retry them later.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem "sidekiq-rescue"
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install sidekiq-rescue
22
+
23
+ ## Usage
24
+
25
+ 1. Add the middleware to your Sidekiq configuration:
26
+
27
+ ```ruby
28
+ Sidekiq.configure_server do |config|
29
+ config.server_middleware do |chain|
30
+ chain.add Sidekiq::Rescue::Middleware
31
+ end
32
+ end
33
+ ```
34
+
35
+ 2. Add DSL to your job:
36
+
37
+ ```ruby
38
+ class MyJob
39
+ include Sidekiq::Job
40
+ include Sidekiq::Rescue::DSL
41
+
42
+ sidekiq_rescue ExpectedError
43
+
44
+ def perform(*)
45
+ # ...
46
+ end
47
+ end
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ You can configure the number of retries and the delay (in seconds) between retries:
53
+
54
+ ```ruby
55
+ class MyJob
56
+ include Sidekiq::Job
57
+ include Sidekiq::Rescue::DSL
58
+
59
+ sidekiq_rescue ExpectedError, delay: 60, limit: 5
60
+
61
+ def perform(*)
62
+ # ...
63
+ end
64
+ end
65
+ ```
66
+
67
+ The `delay` is not the exact time between retries, but a minimum delay. The actual delay calculates based on retries counter and `delay` value. The formula is `delay + retries * rand(10)` seconds. Randomization is used to avoid retry storms.
68
+
69
+ The default values are:
70
+ - `delay`: 60 seconds
71
+ - `limit`: 5 retries
72
+
73
+ Delay and limit can be configured globally:
74
+
75
+ ```ruby
76
+ Sidekiq::Rescue.configure do |config|
77
+ config.delay = 65
78
+ config.limit = 10
79
+ end
80
+ ```
81
+
82
+ ## Use cases
83
+
84
+ Sidekiq::Rescue is useful when you want to retry jobs that failed due to expected errors and not spam your exception tracker with these errors. For example, you may want to retry a job that failed due to a network error or a temporary outage of a third party service, rather than a bug in your code.
85
+
86
+ ## Motivation
87
+
88
+ Sidekiq provides a retry mechanism for jobs that failed due to unexpected errors. However, it does not provide a way to retry jobs that failed due to expected errors. This gem aims to fill this gap.
89
+ In addition, it provides a way to configure the number of retries and the delay between retries independently from the Sidekiq standard retry mechanism.
90
+
91
+ ## Supported Ruby versions
92
+
93
+ This gem supports Ruby 2.7+
94
+
95
+ If something doesn't work on one of these versions, it's a bug
96
+
97
+ ## Supported Sidekiq versions
98
+
99
+ This gem supports Sidekiq 6.5+. It may work with older versions, but it's not tested.
100
+
101
+ If you need support for older versions, please open an issue
102
+
103
+ ## Development
104
+
105
+ To install dependencies and run tests:
106
+ ```bash
107
+ make init
108
+ make test
109
+ make lint
110
+ ```
111
+
112
+ ## Contributing
113
+
114
+ Bug reports and pull requests are welcome on GitHub at https://github.com/moofkit/sidekiq-rescue.
115
+
116
+ ## License
117
+
118
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Rescue
5
+ # Config class is used to store the configuration of Sidekiq::Rescue
6
+ # and to allow to configure it.
7
+ class Config
8
+ DEFAULTS = {
9
+ delay: 60,
10
+ limit: 10
11
+ }.freeze
12
+
13
+ def initialize
14
+ @delay = DEFAULTS[:delay]
15
+ @limit = DEFAULTS[:limit]
16
+ @logger = Sidekiq.logger
17
+ end
18
+
19
+ # Delay in seconds before retrying the job.
20
+ # @return [Integer, Float]
21
+ attr_reader :delay
22
+
23
+ # @param delay [Integer, Float] The delay in seconds before retrying the job.
24
+ # @return [void]
25
+ # @raise [ArgumentError] if delay is not an Integer or Float
26
+ def delay=(delay)
27
+ raise ArgumentError, "delay must be an Integer or Float" unless delay.is_a?(Integer) || delay.is_a?(Float)
28
+
29
+ @delay = delay
30
+ end
31
+
32
+ # The maximum number of retries.
33
+ # @return [Integer]
34
+ attr_reader :limit
35
+
36
+ # @param limit [Integer] The maximum number of retries.
37
+ # @return [void]
38
+ # @raise [ArgumentError] if limit is not an Integer
39
+ def limit=(limit)
40
+ raise ArgumentError, "limit must be an Integer" unless limit.is_a?(Integer)
41
+
42
+ @limit = limit
43
+ end
44
+
45
+ # The logger instance.
46
+ # @return [Logger]
47
+ # @note The default logger is Sidekiq's logger.
48
+ attr_reader :logger
49
+
50
+ # @param logger [Logger] The logger instance.
51
+ # @return [void]
52
+ # @raise [ArgumentError] if logger is not a Logger
53
+ def logger=(logger)
54
+ raise ArgumentError, "logger must be a Logger" if !logger.nil? && !logger.respond_to?(:info)
55
+
56
+ @logger = logger
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Rescue
5
+ # This module is included into the job class to provide the DSL for
6
+ # configuring rescue options.
7
+ module DSL
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ base.sidekiq_class_attribute(:sidekiq_rescue_options)
11
+ end
12
+
13
+ # Module containing the DSL methods
14
+ module ClassMethods
15
+ # Configure rescue options for the job.
16
+ # @param error [StandardError] The error class to rescue.
17
+ # @param error [Array<StandardError>] The error classes to rescue.
18
+ # @param delay [Integer] The delay in seconds before retrying the job.
19
+ # @param limit [Integer] The maximum number of retries.
20
+ # @return [void]
21
+ # @raise [ArgumentError] if error is not a StandardError
22
+ # @raise [ArgumentError] if error is not an array of StandardError
23
+ # @raise [ArgumentError] if delay is not an Integer or Float
24
+ # @raise [ArgumentError] if limit is not an Integer
25
+ # @example
26
+ # sidekiq_rescue NetworkError, delay: 60, limit: 10
27
+ def sidekiq_rescue(error, delay: nil, limit: nil)
28
+ validate_error_argument(error)
29
+ validate_delay_argument(delay)
30
+ validate_limit_argument(limit)
31
+
32
+ self.sidekiq_rescue_options = {
33
+ error: error,
34
+ delay: delay || Sidekiq::Rescue.config.delay,
35
+ limit: limit || Sidekiq::Rescue.config.limit
36
+ }
37
+ end
38
+
39
+ private
40
+
41
+ def validate_error_argument(error)
42
+ error_arg_valid = if error.is_a?(Array)
43
+ error.all? { |e| e < StandardError }
44
+ else
45
+ error < StandardError
46
+ end
47
+ return if error_arg_valid
48
+
49
+ raise ArgumentError,
50
+ "error must be an ancestor of StandardError or an array of ancestors of StandardError"
51
+ end
52
+
53
+ def validate_delay_argument(delay)
54
+ return unless delay && !delay.is_a?(Integer) && !delay.is_a?(Float)
55
+
56
+ raise ArgumentError,
57
+ "delay must be integer or float"
58
+ end
59
+
60
+ def validate_limit_argument(limit)
61
+ raise ArgumentError, "limit must be integer" if limit && !limit.is_a?(Integer)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Rescue
5
+ # Server middleware for sidekiq-rescue
6
+ # It is responsible for catching the errors and rescheduling the job
7
+ # according to the options provided
8
+ # @api private
9
+ class ServerMiddleware
10
+ include Sidekiq::ServerMiddleware
11
+
12
+ def call(job_instance, job_payload, _queue, &block)
13
+ job_class = job_instance.class
14
+ options = job_class.sidekiq_rescue_options if job_class.respond_to?(:sidekiq_rescue_options)
15
+ if options
16
+ sidekiq_rescue(job_payload, **options, &block)
17
+ else
18
+ yield
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def sidekiq_rescue(job_payload, delay:, limit:, error:, **)
25
+ yield
26
+ rescue *error => e
27
+ rescue_counter = job_payload["sidekiq_rescue_counter"].to_i
28
+ rescue_counter += 1
29
+ raise e if rescue_counter > limit
30
+
31
+ # NOTE: we use the retry counter to increase the jitter
32
+ # so that the jobs don't retry at the same time
33
+ # inspired by sidekiq https://github.com/sidekiq/sidekiq/blob/73c150d0430a8394cadb5cd49218895b113613a0/lib/sidekiq/job_retry.rb#L188
34
+ jitter = rand(10) * rescue_counter
35
+ reschedule_at = Time.now.to_f + delay + jitter
36
+
37
+ Sidekiq::Rescue.logger.info("[sidekiq_rescue] Job failed #{rescue_counter} times with error:" \
38
+ "#{e.message}; rescheduling at #{reschedule_at}")
39
+ Sidekiq::Client.push(job_payload.merge("at" => reschedule_at, "sidekiq_rescue_counter" => rescue_counter))
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Rescue
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ # Sidekiq::Rescue is a Sidekiq plugin which allows you to easily handle
5
+ # exceptions thrown by your jobs.
6
+ #
7
+ # To use Sidekiq::Rescue, you need to include Sidekiq::Rescue::DSL module
8
+ # in your job class and use the sidekiq_rescue class method to define
9
+ # exception handlers.
10
+ #
11
+ # class MyJob
12
+ # include Sidekiq::Job
13
+ # include Sidekiq::Rescue::DSL
14
+ #
15
+ # sidekiq_rescue NetworkError, delay: 60, limit: 10
16
+ #
17
+ # def perform
18
+ # # ...
19
+ # end
20
+ # end
21
+ #
22
+ # Also it needs to be registered in Sidekiq server middleware chain:
23
+ # Sidekiq.configure_server do |config|
24
+ # config.server_middleware do |chain|
25
+ # chain.add Sidekiq::Rescue::ServerMiddleware
26
+ # ...
27
+ module Rescue
28
+ @mutex = Mutex.new
29
+ @config = Config.new.freeze
30
+
31
+ class << self
32
+ extend Forwardable
33
+ # Returns the logger instance
34
+ #
35
+ # @return [Logger] The logger instance.
36
+ def_delegators :config, :logger
37
+
38
+ # @return [Sidekiq::Rescue::Config] The configuration object.
39
+ attr_reader :config
40
+
41
+ # Configures Sidekiq::Rescue
42
+ # @return [void]
43
+ # @yieldparam config [Sidekiq::Rescue::Config] The configuration object.
44
+ # @example
45
+ # Sidekiq::Rescue.configure do |config|
46
+ # config.delay = 10
47
+ # config.limit = 5
48
+ # config.logger = Logger.new($stdout)
49
+ # end
50
+ def configure
51
+ @mutex.synchronize do
52
+ config = @config.dup
53
+ yield(config)
54
+ @config = config.freeze
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq_rescue"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require "forwardable"
5
+ require_relative "sidekiq/rescue/config"
6
+ require_relative "sidekiq/rescue"
7
+ require_relative "sidekiq/rescue/version"
8
+ require_relative "sidekiq/rescue/dsl"
9
+ require_relative "sidekiq/rescue/server_middleware"
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-rescue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dmitrii Ivliev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-01-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sidekiq
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.5'
27
+ description:
28
+ email:
29
+ - mail@ivda.dev
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE.txt
36
+ - README.md
37
+ - Rakefile
38
+ - lib/sidekiq-rescue.rb
39
+ - lib/sidekiq/rescue.rb
40
+ - lib/sidekiq/rescue/config.rb
41
+ - lib/sidekiq/rescue/dsl.rb
42
+ - lib/sidekiq/rescue/server_middleware.rb
43
+ - lib/sidekiq/rescue/version.rb
44
+ - lib/sidekiq_rescue.rb
45
+ homepage: https://github.com/moofkit/sidekiq-rescue
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ homepage_uri: https://github.com/moofkit/sidekiq-rescue
50
+ source_code_uri: https://github.com/moofkit/sidekiq-rescue
51
+ changelog_uri: https://github.com/moofkit/sidekiq-rescue/blob/master/CHANGELOG.md
52
+ rubygems_mfa_required: 'true'
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.7.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.4.10
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: Rescue Sidekiq jobs on expected error and reschedule them
72
+ test_files: []