rubocop-sidekiq_plus 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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +322 -0
- data/config/default.yml +396 -0
- data/lib/rubocop/cop/sidekiq/active_record_argument.rb +77 -0
- data/lib/rubocop/cop/sidekiq/async_in_test.rb +42 -0
- data/lib/rubocop/cop/sidekiq/base.rb +14 -0
- data/lib/rubocop/cop/sidekiq/consistent_job_suffix.rb +44 -0
- data/lib/rubocop/cop/sidekiq/constant_job_class_name.rb +53 -0
- data/lib/rubocop/cop/sidekiq/database_connection_leak.rb +33 -0
- data/lib/rubocop/cop/sidekiq/date_time_argument.rb +80 -0
- data/lib/rubocop/cop/sidekiq/deprecated_default_worker_options.rb +44 -0
- data/lib/rubocop/cop/sidekiq/deprecated_delay_extension.rb +29 -0
- data/lib/rubocop/cop/sidekiq/deprecated_worker_module.rb +40 -0
- data/lib/rubocop/cop/sidekiq/enqueue_inefficiency.rb +75 -0
- data/lib/rubocop/cop/sidekiq/excessive_retry.rb +52 -0
- data/lib/rubocop/cop/sidekiq/find_each_in_job.rb +58 -0
- data/lib/rubocop/cop/sidekiq/huge_job_arguments.rb +73 -0
- data/lib/rubocop/cop/sidekiq/job_dependency.rb +30 -0
- data/lib/rubocop/cop/sidekiq/job_file_location.rb +52 -0
- data/lib/rubocop/cop/sidekiq/job_file_naming.rb +38 -0
- data/lib/rubocop/cop/sidekiq/job_include.rb +67 -0
- data/lib/rubocop/cop/sidekiq/missing_logging.rb +49 -0
- data/lib/rubocop/cop/sidekiq/missing_timeout.rb +65 -0
- data/lib/rubocop/cop/sidekiq/mixed_retry_strategies.rb +55 -0
- data/lib/rubocop/cop/sidekiq/mixin/argument_traversal.rb +28 -0
- data/lib/rubocop/cop/sidekiq/mixin/class_name_helper.rb +23 -0
- data/lib/rubocop/cop/sidekiq/mixin/processed_source_path.rb +19 -0
- data/lib/rubocop/cop/sidekiq/no_rescue_all.rb +70 -0
- data/lib/rubocop/cop/sidekiq/perform_inline_usage.rb +53 -0
- data/lib/rubocop/cop/sidekiq/perform_method_parameters.rb +53 -0
- data/lib/rubocop/cop/sidekiq/pii_in_arguments.rb +90 -0
- data/lib/rubocop/cop/sidekiq/puts_or_print_usage.rb +37 -0
- data/lib/rubocop/cop/sidekiq/queue_specified.rb +74 -0
- data/lib/rubocop/cop/sidekiq/redis_in_job.rb +32 -0
- data/lib/rubocop/cop/sidekiq/retry_specified.rb +81 -0
- data/lib/rubocop/cop/sidekiq/retry_zero.rb +48 -0
- data/lib/rubocop/cop/sidekiq/self_scheduling_job.rb +54 -0
- data/lib/rubocop/cop/sidekiq/sensitive_data_in_arguments.rb +92 -0
- data/lib/rubocop/cop/sidekiq/sidekiq_over_active_job.rb +30 -0
- data/lib/rubocop/cop/sidekiq/silent_rescue.rb +63 -0
- data/lib/rubocop/cop/sidekiq/sleep_in_jobs.rb +46 -0
- data/lib/rubocop/cop/sidekiq/symbol_argument.rb +69 -0
- data/lib/rubocop/cop/sidekiq/thread_in_job.rb +53 -0
- data/lib/rubocop/cop/sidekiq/transaction_leak.rb +72 -0
- data/lib/rubocop/cop/sidekiq/unknown_sidekiq_option.rb +52 -0
- data/lib/rubocop/cop/sidekiq_cops.rb +71 -0
- data/lib/rubocop/cop/sidekiq_ent/base.rb +43 -0
- data/lib/rubocop/cop/sidekiq_ent/encryption_with_many_arguments.rb +71 -0
- data/lib/rubocop/cop/sidekiq_ent/encryption_without_secret_bag.rb +83 -0
- data/lib/rubocop/cop/sidekiq_ent/leader_election_without_block.rb +75 -0
- data/lib/rubocop/cop/sidekiq_ent/limiter_not_reused.rb +79 -0
- data/lib/rubocop/cop/sidekiq_ent/limiter_without_lock_timeout.rb +46 -0
- data/lib/rubocop/cop/sidekiq_ent/limiter_without_wait_timeout.rb +49 -0
- data/lib/rubocop/cop/sidekiq_ent/periodic_job_invalid_cron.rb +108 -0
- data/lib/rubocop/cop/sidekiq_ent/periodic_job_with_arguments.rb +94 -0
- data/lib/rubocop/cop/sidekiq_ent/unique_job_too_short_ttl.rb +80 -0
- data/lib/rubocop/cop/sidekiq_ent/unique_job_without_ttl.rb +52 -0
- data/lib/rubocop/cop/sidekiq_ent/unique_until_mismatch.rb +59 -0
- data/lib/rubocop/cop/sidekiq_pro/base.rb +39 -0
- data/lib/rubocop/cop/sidekiq_pro/batch_callback_method.rb +66 -0
- data/lib/rubocop/cop/sidekiq_pro/batch_retry_in_callback.rb +54 -0
- data/lib/rubocop/cop/sidekiq_pro/batch_status_polling.rb +68 -0
- data/lib/rubocop/cop/sidekiq_pro/batch_without_callback.rb +92 -0
- data/lib/rubocop/cop/sidekiq_pro/empty_batch.rb +108 -0
- data/lib/rubocop/cop/sidekiq_pro/expiring_job_without_ttl.rb +101 -0
- data/lib/rubocop/cop/sidekiq_pro/large_argument_in_batch.rb +93 -0
- data/lib/rubocop/cop/sidekiq_pro/nested_batch_without_parent.rb +55 -0
- data/lib/rubocop/cop/sidekiq_pro/reliability_not_enabled.rb +67 -0
- data/lib/rubocop/sidekiq/config_formatter.rb +60 -0
- data/lib/rubocop/sidekiq/description_extractor.rb +70 -0
- data/lib/rubocop/sidekiq/language.rb +79 -0
- data/lib/rubocop/sidekiq/plugin.rb +30 -0
- data/lib/rubocop/sidekiq/version.rb +7 -0
- data/lib/rubocop/sidekiq.rb +10 -0
- data/lib/rubocop-sidekiq.rb +5 -0
- data/lib/rubocop-sidekiq_plus.rb +9 -0
- metadata +150 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqEnt
|
|
6
|
+
# Checks that unique jobs specify the unique_for option.
|
|
7
|
+
#
|
|
8
|
+
# Sidekiq Enterprise unique jobs require a TTL (unique_for) to be specified.
|
|
9
|
+
# Without it, uniqueness locks may persist indefinitely if jobs fail.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad - unique_until without unique_for
|
|
13
|
+
# class MyJob
|
|
14
|
+
# include Sidekiq::Job
|
|
15
|
+
# sidekiq_options unique_until: :start
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # good - both unique_for and unique_until specified
|
|
19
|
+
# class MyJob
|
|
20
|
+
# include Sidekiq::Job
|
|
21
|
+
# sidekiq_options unique_for: 1.hour, unique_until: :start
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
class UniqueJobWithoutTTL < Base
|
|
25
|
+
MSG = 'Specify `unique_for` option when using unique jobs.'
|
|
26
|
+
|
|
27
|
+
# @!method sidekiq_options_with_unique?(node)
|
|
28
|
+
def_node_matcher :sidekiq_options_with_unique?, <<~PATTERN
|
|
29
|
+
(send nil? :sidekiq_options (hash $...))
|
|
30
|
+
PATTERN
|
|
31
|
+
|
|
32
|
+
def on_send(node)
|
|
33
|
+
sidekiq_options_with_unique?(node) do |pairs|
|
|
34
|
+
has_unique_until = pairs.any? { |pair| option_key?(pair, :unique_until) }
|
|
35
|
+
has_unique_for = pairs.any? { |pair| option_key?(pair, :unique_for) }
|
|
36
|
+
|
|
37
|
+
add_offense(node) if has_unique_until && !has_unique_for
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
alias on_csend on_send
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def option_key?(pair, key)
|
|
45
|
+
return false unless pair.pair_type?
|
|
46
|
+
|
|
47
|
+
pair.key.sym_type? && pair.key.value == key
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqEnt
|
|
6
|
+
# Checks that unique_until option has an appropriate value.
|
|
7
|
+
#
|
|
8
|
+
# Using unique_until: :start may cause unexpected behavior as the lock
|
|
9
|
+
# is released when the job starts, allowing concurrent execution
|
|
10
|
+
# of identical jobs.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad - lock released on start
|
|
14
|
+
# class MyJob
|
|
15
|
+
# include Sidekiq::Job
|
|
16
|
+
# sidekiq_options unique_for: 600, unique_until: :start
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # good - default behavior (success)
|
|
20
|
+
# class MyJob
|
|
21
|
+
# include Sidekiq::Job
|
|
22
|
+
# sidekiq_options unique_for: 600
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good - explicit success
|
|
26
|
+
# class MyJob
|
|
27
|
+
# include Sidekiq::Job
|
|
28
|
+
# sidekiq_options unique_for: 600, unique_until: :success
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
class UniqueUntilMismatch < Base
|
|
32
|
+
MSG = 'Avoid `unique_until: :%<value>s`. ' \
|
|
33
|
+
'Prefer `unique_until: :success` (default) to prevent concurrent execution.'
|
|
34
|
+
|
|
35
|
+
ALLOWED_VALUES = %i[success].freeze
|
|
36
|
+
|
|
37
|
+
# @!method unique_until_value(node)
|
|
38
|
+
def_node_matcher :unique_until_value, <<~PATTERN
|
|
39
|
+
(send nil? :sidekiq_options (hash <(pair (sym :unique_until) (sym $_)) ...>))
|
|
40
|
+
PATTERN
|
|
41
|
+
|
|
42
|
+
def on_send(node)
|
|
43
|
+
unique_until_value(node) do |value|
|
|
44
|
+
return if allowed_values.include?(value)
|
|
45
|
+
|
|
46
|
+
add_offense(node, message: format(MSG, value: value))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
alias on_csend on_send
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def allowed_values
|
|
54
|
+
Array(cop_config.fetch('AllowedValues', ALLOWED_VALUES)).map(&:to_sym)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqPro
|
|
6
|
+
# @abstract
|
|
7
|
+
# Base class for Sidekiq Pro cops.
|
|
8
|
+
# Provides common functionality for detecting Pro-specific patterns.
|
|
9
|
+
class Base < ::RuboCop::Cop::Base
|
|
10
|
+
abstract! if respond_to?(:abstract!)
|
|
11
|
+
include RuboCop::Sidekiq::Language
|
|
12
|
+
|
|
13
|
+
# Detect Sidekiq::Batch.new
|
|
14
|
+
# @!method batch_new?(node)
|
|
15
|
+
def_node_matcher :batch_new?, <<~PATTERN
|
|
16
|
+
(send (const (const {nil? cbase} :Sidekiq) :Batch) :new ...)
|
|
17
|
+
PATTERN
|
|
18
|
+
|
|
19
|
+
# Detect batch.jobs block
|
|
20
|
+
# @!method batch_jobs_block?(node)
|
|
21
|
+
def_node_matcher :batch_jobs_block?, <<~PATTERN
|
|
22
|
+
(block (send $_ :jobs) _ $_)
|
|
23
|
+
PATTERN
|
|
24
|
+
|
|
25
|
+
# Detect batch.on callback registration
|
|
26
|
+
# @!method batch_on_callback?(node)
|
|
27
|
+
def_node_matcher :batch_on_callback?, <<~PATTERN
|
|
28
|
+
(send $_ :on (sym $_) $...)
|
|
29
|
+
PATTERN
|
|
30
|
+
|
|
31
|
+
# Detect batch.description= assignment
|
|
32
|
+
# @!method batch_description_set?(node)
|
|
33
|
+
def_node_matcher :batch_description_set?, <<~PATTERN
|
|
34
|
+
(send $_ :description= $_)
|
|
35
|
+
PATTERN
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqPro
|
|
6
|
+
# Checks that batch callback methods are named correctly.
|
|
7
|
+
#
|
|
8
|
+
# Sidekiq Pro batch callbacks require specific method names:
|
|
9
|
+
# - `:complete` callback requires `on_complete` method
|
|
10
|
+
# - `:success` callback requires `on_success` method
|
|
11
|
+
# - `:death` callback requires `on_death` method
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad - callback method name is incorrect
|
|
15
|
+
# class MyCallback
|
|
16
|
+
# def complete(status, options)
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
# batch.on(:complete, MyCallback)
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# class MyCallback
|
|
23
|
+
# def on_complete(status, options)
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
# batch.on(:complete, MyCallback)
|
|
27
|
+
#
|
|
28
|
+
# # good - method specified as string
|
|
29
|
+
# batch.on(:complete, 'MyCallback#handle_complete')
|
|
30
|
+
#
|
|
31
|
+
class BatchCallbackMethod < Base
|
|
32
|
+
CALLBACK_METHODS = {
|
|
33
|
+
complete: :on_complete,
|
|
34
|
+
success: :on_success,
|
|
35
|
+
death: :on_death
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
MSG = 'Batch callback method should be named `%<expected>s`, not `%<actual>s`.'
|
|
39
|
+
|
|
40
|
+
def on_def(node)
|
|
41
|
+
return unless potential_callback_method?(node)
|
|
42
|
+
|
|
43
|
+
method_name = node.method_name
|
|
44
|
+
expected_name = expected_method_name_for(method_name)
|
|
45
|
+
return unless expected_name
|
|
46
|
+
|
|
47
|
+
add_offense(node.loc.name, message: format(MSG, expected: expected_name, actual: method_name))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def potential_callback_method?(node)
|
|
53
|
+
CALLBACK_METHODS.key?(node.method_name) && callback_like_signature?(node)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def callback_like_signature?(node)
|
|
57
|
+
node.arguments.size == 2
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def expected_method_name_for(method_name)
|
|
61
|
+
CALLBACK_METHODS[method_name]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqPro
|
|
6
|
+
# Checks that jobs enqueued in batch callbacks have retry enabled.
|
|
7
|
+
#
|
|
8
|
+
# When a batch succeeds but the callback job fails without retry,
|
|
9
|
+
# the overall workflow may be incomplete without any automatic recovery.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad - callback enqueues a job without retry
|
|
13
|
+
# class MyCallback
|
|
14
|
+
# def on_success(status, options)
|
|
15
|
+
# FinalizeJob.perform_async(options['order_id'])
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# class FinalizeJob
|
|
20
|
+
# include Sidekiq::Job
|
|
21
|
+
# sidekiq_options retry: false
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # good - callback enqueues a job with retry enabled
|
|
25
|
+
# class FinalizeJob
|
|
26
|
+
# include Sidekiq::Job
|
|
27
|
+
# sidekiq_options retry: 5
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
class BatchRetryInCallback < Base
|
|
31
|
+
MSG = 'Jobs enqueued in batch callbacks should have retry enabled.'
|
|
32
|
+
|
|
33
|
+
# @!method callback_method?(node)
|
|
34
|
+
def_node_matcher :callback_method?, <<~PATTERN
|
|
35
|
+
(def {:on_complete :on_success :on_death} (args _ _) ...)
|
|
36
|
+
PATTERN
|
|
37
|
+
|
|
38
|
+
# @!method perform_async_call?(node)
|
|
39
|
+
def_node_matcher :perform_async_call?, <<~PATTERN
|
|
40
|
+
(send (const ...) {:perform_async :perform_in :perform_at} ...)
|
|
41
|
+
PATTERN
|
|
42
|
+
|
|
43
|
+
def on_def(node)
|
|
44
|
+
return unless callback_method?(node)
|
|
45
|
+
|
|
46
|
+
node.each_descendant(:send) do |send_node|
|
|
47
|
+
add_offense(send_node) if perform_async_call?(send_node)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
alias on_defs on_def
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqPro
|
|
6
|
+
# Checks for batch status polling anti-patterns.
|
|
7
|
+
#
|
|
8
|
+
# Polling batch status in a loop wastes resources and can cause
|
|
9
|
+
# issues. Use batch callbacks instead.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad - polling for status
|
|
13
|
+
# loop do
|
|
14
|
+
# status = Sidekiq::Batch::Status.new(bid)
|
|
15
|
+
# break if status.complete?
|
|
16
|
+
# sleep 5
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # good - use callbacks
|
|
20
|
+
# batch = Sidekiq::Batch.new
|
|
21
|
+
# batch.on(:complete, MyCallback)
|
|
22
|
+
# batch.jobs do
|
|
23
|
+
# SomeJob.perform_async
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class BatchStatusPolling < Base
|
|
27
|
+
MSG = 'Avoid polling batch status. Use batch callbacks instead.'
|
|
28
|
+
|
|
29
|
+
# @!method batch_status_new?(node)
|
|
30
|
+
def_node_matcher :batch_status_new?, <<~PATTERN
|
|
31
|
+
(send (const (const (const {nil? cbase} :Sidekiq) :Batch) :Status) :new ...)
|
|
32
|
+
PATTERN
|
|
33
|
+
|
|
34
|
+
# @!method status_complete_check?(node)
|
|
35
|
+
def_node_matcher :status_complete_check?, <<~PATTERN
|
|
36
|
+
(send _ {:complete? :pending :failures :total} ...)
|
|
37
|
+
PATTERN
|
|
38
|
+
|
|
39
|
+
def on_send(node)
|
|
40
|
+
return unless batch_status_new?(node)
|
|
41
|
+
return unless inside_loop?(node)
|
|
42
|
+
|
|
43
|
+
add_offense(node)
|
|
44
|
+
end
|
|
45
|
+
alias on_csend on_send
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def inside_loop?(node)
|
|
50
|
+
node.each_ancestor.any? do |ancestor|
|
|
51
|
+
loop_node?(ancestor)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def loop_node?(node)
|
|
56
|
+
return true if node.type?(:while, :until)
|
|
57
|
+
return true if node.block_type? && loop_method?(node.send_node)
|
|
58
|
+
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def loop_method?(send_node)
|
|
63
|
+
%i[loop each times].include?(send_node.method_name)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqPro
|
|
6
|
+
# Checks that batches have callbacks or descriptions for tracking.
|
|
7
|
+
#
|
|
8
|
+
# Batches without callbacks or descriptions are difficult to track
|
|
9
|
+
# and monitor. Consider adding at least one callback or a description.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad - no callback or description
|
|
13
|
+
# batch = Sidekiq::Batch.new
|
|
14
|
+
# batch.jobs do
|
|
15
|
+
# SomeJob.perform_async
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # good - has callback
|
|
19
|
+
# batch = Sidekiq::Batch.new
|
|
20
|
+
# batch.on(:complete, MyCallback)
|
|
21
|
+
# batch.jobs do
|
|
22
|
+
# SomeJob.perform_async
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good - has description
|
|
26
|
+
# batch = Sidekiq::Batch.new
|
|
27
|
+
# batch.description = "Import users"
|
|
28
|
+
# batch.jobs do
|
|
29
|
+
# SomeJob.perform_async
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
class BatchWithoutCallback < Base
|
|
33
|
+
MSG = 'Batch should have a callback or description for tracking.'
|
|
34
|
+
|
|
35
|
+
CALLBACK_METHODS = %i[on description=].freeze
|
|
36
|
+
private_constant :CALLBACK_METHODS
|
|
37
|
+
|
|
38
|
+
def on_block(node)
|
|
39
|
+
return unless batch_jobs_block?(node)
|
|
40
|
+
|
|
41
|
+
batch_receiver = node.receiver
|
|
42
|
+
return unless batch_receiver
|
|
43
|
+
|
|
44
|
+
batch_var_name = extract_variable_name(batch_receiver)
|
|
45
|
+
return unless batch_var_name
|
|
46
|
+
|
|
47
|
+
add_offense(node.send_node) unless callback_or_description?(node, batch_var_name)
|
|
48
|
+
end
|
|
49
|
+
alias on_numblock on_block
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def extract_variable_name(node)
|
|
54
|
+
case node.type
|
|
55
|
+
when :lvar
|
|
56
|
+
node.children.first
|
|
57
|
+
when :send
|
|
58
|
+
node.method_name if node.receiver.nil?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def callback_or_description?(jobs_block, batch_var_name)
|
|
63
|
+
parent_scope = find_parent_scope(jobs_block)
|
|
64
|
+
return false unless parent_scope
|
|
65
|
+
|
|
66
|
+
parent_scope.each_descendant(:send).any? do |send_node|
|
|
67
|
+
receiver_matches?(send_node, batch_var_name) &&
|
|
68
|
+
CALLBACK_METHODS.include?(send_node.method_name)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def receiver_matches?(send_node, batch_var_name)
|
|
73
|
+
return false unless send_node.receiver
|
|
74
|
+
|
|
75
|
+
case send_node.receiver.type
|
|
76
|
+
when :lvar
|
|
77
|
+
send_node.receiver.children.first == batch_var_name
|
|
78
|
+
when :send
|
|
79
|
+
send_node.receiver.method?(batch_var_name)
|
|
80
|
+
else
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def find_parent_scope(node)
|
|
86
|
+
node.each_ancestor(:any_def, :block, :begin).first ||
|
|
87
|
+
node.each_ancestor.find(&:begin_type?)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqPro
|
|
6
|
+
# Checks for batch.jobs blocks that might be empty.
|
|
7
|
+
#
|
|
8
|
+
# Empty batches cause errors in Sidekiq Pro versions before 7.1.
|
|
9
|
+
# Even in newer versions, creating empty batches is often unintentional.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad - block might add no jobs
|
|
13
|
+
# batch = Sidekiq::Batch.new
|
|
14
|
+
# batch.jobs do
|
|
15
|
+
# items.each do |item|
|
|
16
|
+
# ProcessJob.perform_async(item.id) if item.active?
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # good - ensure at least one job is added or check before creating batch
|
|
21
|
+
# active_items = items.select(&:active?)
|
|
22
|
+
# if active_items.any?
|
|
23
|
+
# batch = Sidekiq::Batch.new
|
|
24
|
+
# batch.jobs do
|
|
25
|
+
# active_items.each do |item|
|
|
26
|
+
# ProcessJob.perform_async(item.id)
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
class EmptyBatch < Base
|
|
32
|
+
MSG = 'Batch jobs block may be empty. Ensure jobs are added or guard against empty batches.'
|
|
33
|
+
|
|
34
|
+
CONDITIONAL_METHODS = %i[if unless case].freeze
|
|
35
|
+
|
|
36
|
+
def on_block(node)
|
|
37
|
+
batch_jobs_block?(node) do |_receiver, body|
|
|
38
|
+
add_offense(node) if potentially_empty_block?(body)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
alias on_numblock on_block
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def potentially_empty_block?(body)
|
|
47
|
+
return true if body.nil?
|
|
48
|
+
return false if contains_unconditional_perform?(body)
|
|
49
|
+
|
|
50
|
+
all_perform_calls_conditional?(body)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def contains_unconditional_perform?(node)
|
|
54
|
+
return false unless node
|
|
55
|
+
|
|
56
|
+
case node.type
|
|
57
|
+
when :send
|
|
58
|
+
check_send_node_for_perform(node)
|
|
59
|
+
when :begin
|
|
60
|
+
node.children.any? { |child| contains_unconditional_perform?(child) }
|
|
61
|
+
else
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def check_send_node_for_perform(node)
|
|
67
|
+
return true if perform_call?(node)
|
|
68
|
+
|
|
69
|
+
node.children.any? { |child| child.is_a?(::Parser::AST::Node) && contains_unconditional_perform?(child) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def all_perform_calls_conditional?(node)
|
|
73
|
+
return false unless node
|
|
74
|
+
|
|
75
|
+
perform_calls = find_perform_calls(node)
|
|
76
|
+
return false if perform_calls.empty?
|
|
77
|
+
|
|
78
|
+
perform_calls.all? { |call| inside_conditional?(call) || inside_iterator_with_condition?(call) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def find_perform_calls(node, calls = [])
|
|
82
|
+
return calls unless node.is_a?(::Parser::AST::Node)
|
|
83
|
+
|
|
84
|
+
calls << node if node.send_type? && perform_call?(node)
|
|
85
|
+
node.children.each { |child| find_perform_calls(child, calls) }
|
|
86
|
+
calls
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def inside_conditional?(node)
|
|
90
|
+
node.each_ancestor.any? do |ancestor|
|
|
91
|
+
ancestor.type?(:if, :case, :case_match)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def inside_iterator_with_condition?(node)
|
|
96
|
+
node.each_ancestor(:block, :numblock).any? do |block|
|
|
97
|
+
iterator_with_potential_empty_collection?(block)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def iterator_with_potential_empty_collection?(block)
|
|
102
|
+
send_node = block.send_node
|
|
103
|
+
%i[each map select filter find_each find_in_batches].include?(send_node.method_name)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqPro
|
|
6
|
+
# Checks that expiring jobs have appropriate TTL values.
|
|
7
|
+
#
|
|
8
|
+
# A TTL that is too short may cause jobs to expire before processing,
|
|
9
|
+
# while a TTL that is too long defeats the purpose of expiring jobs.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad - TTL too short
|
|
13
|
+
# class MyJob
|
|
14
|
+
# include Sidekiq::Job
|
|
15
|
+
# sidekiq_options expires_in: 1.minute
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # bad - TTL too long
|
|
19
|
+
# class MyJob
|
|
20
|
+
# include Sidekiq::Job
|
|
21
|
+
# sidekiq_options expires_in: 30.days
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # good - appropriate TTL
|
|
25
|
+
# class MyJob
|
|
26
|
+
# include Sidekiq::Job
|
|
27
|
+
# sidekiq_options expires_in: 1.hour
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
class ExpiringJobWithoutTTL < Base
|
|
31
|
+
MSG_TOO_SHORT = 'Expiring job TTL is too short (minimum: %<minimum>s seconds). ' \
|
|
32
|
+
'Jobs may expire before processing.'
|
|
33
|
+
MSG_TOO_LONG = 'Expiring job TTL is too long (maximum: %<maximum>s seconds). ' \
|
|
34
|
+
'Consider a shorter TTL for expiring jobs.'
|
|
35
|
+
|
|
36
|
+
MINIMUM_TTL = 300
|
|
37
|
+
MAXIMUM_TTL = 604_800
|
|
38
|
+
|
|
39
|
+
# @!method expires_in_value(node)
|
|
40
|
+
def_node_matcher :expires_in_value, <<~PATTERN
|
|
41
|
+
(send nil? :sidekiq_options (hash <(pair (sym :expires_in) $_) ...>))
|
|
42
|
+
PATTERN
|
|
43
|
+
|
|
44
|
+
def on_send(node)
|
|
45
|
+
expires_in_value(node) do |value_node|
|
|
46
|
+
ttl = extract_seconds(value_node)
|
|
47
|
+
return unless ttl
|
|
48
|
+
|
|
49
|
+
check_ttl_range(value_node, ttl)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
alias on_csend on_send
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def check_ttl_range(node, ttl)
|
|
57
|
+
if ttl < minimum_ttl
|
|
58
|
+
add_offense(node, message: format(MSG_TOO_SHORT, minimum: minimum_ttl))
|
|
59
|
+
elsif ttl > maximum_ttl
|
|
60
|
+
add_offense(node, message: format(MSG_TOO_LONG, maximum: maximum_ttl))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def minimum_ttl
|
|
65
|
+
cop_config.fetch('MinimumTTL', MINIMUM_TTL)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def maximum_ttl
|
|
69
|
+
cop_config.fetch('MaximumTTL', MAXIMUM_TTL)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def extract_seconds(node)
|
|
73
|
+
case node.type
|
|
74
|
+
when :int
|
|
75
|
+
node.value
|
|
76
|
+
when :float
|
|
77
|
+
node.value.to_i
|
|
78
|
+
when :send
|
|
79
|
+
extract_duration_seconds(node)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def extract_duration_seconds(node)
|
|
84
|
+
return unless node.receiver&.type?(:int, :float)
|
|
85
|
+
|
|
86
|
+
value = node.receiver.value.to_f
|
|
87
|
+
duration_multiplier(node.method_name, value)&.to_i
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def duration_multiplier(method_name, value)
|
|
91
|
+
case method_name
|
|
92
|
+
when :seconds, :second then value
|
|
93
|
+
when :minutes, :minute then value * 60
|
|
94
|
+
when :hours, :hour then value * 3600
|
|
95
|
+
when :days, :day then value * 86_400
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|