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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +322 -0
  4. data/config/default.yml +396 -0
  5. data/lib/rubocop/cop/sidekiq/active_record_argument.rb +77 -0
  6. data/lib/rubocop/cop/sidekiq/async_in_test.rb +42 -0
  7. data/lib/rubocop/cop/sidekiq/base.rb +14 -0
  8. data/lib/rubocop/cop/sidekiq/consistent_job_suffix.rb +44 -0
  9. data/lib/rubocop/cop/sidekiq/constant_job_class_name.rb +53 -0
  10. data/lib/rubocop/cop/sidekiq/database_connection_leak.rb +33 -0
  11. data/lib/rubocop/cop/sidekiq/date_time_argument.rb +80 -0
  12. data/lib/rubocop/cop/sidekiq/deprecated_default_worker_options.rb +44 -0
  13. data/lib/rubocop/cop/sidekiq/deprecated_delay_extension.rb +29 -0
  14. data/lib/rubocop/cop/sidekiq/deprecated_worker_module.rb +40 -0
  15. data/lib/rubocop/cop/sidekiq/enqueue_inefficiency.rb +75 -0
  16. data/lib/rubocop/cop/sidekiq/excessive_retry.rb +52 -0
  17. data/lib/rubocop/cop/sidekiq/find_each_in_job.rb +58 -0
  18. data/lib/rubocop/cop/sidekiq/huge_job_arguments.rb +73 -0
  19. data/lib/rubocop/cop/sidekiq/job_dependency.rb +30 -0
  20. data/lib/rubocop/cop/sidekiq/job_file_location.rb +52 -0
  21. data/lib/rubocop/cop/sidekiq/job_file_naming.rb +38 -0
  22. data/lib/rubocop/cop/sidekiq/job_include.rb +67 -0
  23. data/lib/rubocop/cop/sidekiq/missing_logging.rb +49 -0
  24. data/lib/rubocop/cop/sidekiq/missing_timeout.rb +65 -0
  25. data/lib/rubocop/cop/sidekiq/mixed_retry_strategies.rb +55 -0
  26. data/lib/rubocop/cop/sidekiq/mixin/argument_traversal.rb +28 -0
  27. data/lib/rubocop/cop/sidekiq/mixin/class_name_helper.rb +23 -0
  28. data/lib/rubocop/cop/sidekiq/mixin/processed_source_path.rb +19 -0
  29. data/lib/rubocop/cop/sidekiq/no_rescue_all.rb +70 -0
  30. data/lib/rubocop/cop/sidekiq/perform_inline_usage.rb +53 -0
  31. data/lib/rubocop/cop/sidekiq/perform_method_parameters.rb +53 -0
  32. data/lib/rubocop/cop/sidekiq/pii_in_arguments.rb +90 -0
  33. data/lib/rubocop/cop/sidekiq/puts_or_print_usage.rb +37 -0
  34. data/lib/rubocop/cop/sidekiq/queue_specified.rb +74 -0
  35. data/lib/rubocop/cop/sidekiq/redis_in_job.rb +32 -0
  36. data/lib/rubocop/cop/sidekiq/retry_specified.rb +81 -0
  37. data/lib/rubocop/cop/sidekiq/retry_zero.rb +48 -0
  38. data/lib/rubocop/cop/sidekiq/self_scheduling_job.rb +54 -0
  39. data/lib/rubocop/cop/sidekiq/sensitive_data_in_arguments.rb +92 -0
  40. data/lib/rubocop/cop/sidekiq/sidekiq_over_active_job.rb +30 -0
  41. data/lib/rubocop/cop/sidekiq/silent_rescue.rb +63 -0
  42. data/lib/rubocop/cop/sidekiq/sleep_in_jobs.rb +46 -0
  43. data/lib/rubocop/cop/sidekiq/symbol_argument.rb +69 -0
  44. data/lib/rubocop/cop/sidekiq/thread_in_job.rb +53 -0
  45. data/lib/rubocop/cop/sidekiq/transaction_leak.rb +72 -0
  46. data/lib/rubocop/cop/sidekiq/unknown_sidekiq_option.rb +52 -0
  47. data/lib/rubocop/cop/sidekiq_cops.rb +71 -0
  48. data/lib/rubocop/cop/sidekiq_ent/base.rb +43 -0
  49. data/lib/rubocop/cop/sidekiq_ent/encryption_with_many_arguments.rb +71 -0
  50. data/lib/rubocop/cop/sidekiq_ent/encryption_without_secret_bag.rb +83 -0
  51. data/lib/rubocop/cop/sidekiq_ent/leader_election_without_block.rb +75 -0
  52. data/lib/rubocop/cop/sidekiq_ent/limiter_not_reused.rb +79 -0
  53. data/lib/rubocop/cop/sidekiq_ent/limiter_without_lock_timeout.rb +46 -0
  54. data/lib/rubocop/cop/sidekiq_ent/limiter_without_wait_timeout.rb +49 -0
  55. data/lib/rubocop/cop/sidekiq_ent/periodic_job_invalid_cron.rb +108 -0
  56. data/lib/rubocop/cop/sidekiq_ent/periodic_job_with_arguments.rb +94 -0
  57. data/lib/rubocop/cop/sidekiq_ent/unique_job_too_short_ttl.rb +80 -0
  58. data/lib/rubocop/cop/sidekiq_ent/unique_job_without_ttl.rb +52 -0
  59. data/lib/rubocop/cop/sidekiq_ent/unique_until_mismatch.rb +59 -0
  60. data/lib/rubocop/cop/sidekiq_pro/base.rb +39 -0
  61. data/lib/rubocop/cop/sidekiq_pro/batch_callback_method.rb +66 -0
  62. data/lib/rubocop/cop/sidekiq_pro/batch_retry_in_callback.rb +54 -0
  63. data/lib/rubocop/cop/sidekiq_pro/batch_status_polling.rb +68 -0
  64. data/lib/rubocop/cop/sidekiq_pro/batch_without_callback.rb +92 -0
  65. data/lib/rubocop/cop/sidekiq_pro/empty_batch.rb +108 -0
  66. data/lib/rubocop/cop/sidekiq_pro/expiring_job_without_ttl.rb +101 -0
  67. data/lib/rubocop/cop/sidekiq_pro/large_argument_in_batch.rb +93 -0
  68. data/lib/rubocop/cop/sidekiq_pro/nested_batch_without_parent.rb +55 -0
  69. data/lib/rubocop/cop/sidekiq_pro/reliability_not_enabled.rb +67 -0
  70. data/lib/rubocop/sidekiq/config_formatter.rb +60 -0
  71. data/lib/rubocop/sidekiq/description_extractor.rb +70 -0
  72. data/lib/rubocop/sidekiq/language.rb +79 -0
  73. data/lib/rubocop/sidekiq/plugin.rb +30 -0
  74. data/lib/rubocop/sidekiq/version.rb +7 -0
  75. data/lib/rubocop/sidekiq.rb +10 -0
  76. data/lib/rubocop-sidekiq.rb +5 -0
  77. data/lib/rubocop-sidekiq_plus.rb +9 -0
  78. 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