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,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for retry: 0 usage and suggests retry: false for clarity.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# sidekiq_options retry: 0
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# sidekiq_options retry: false
|
|
14
|
+
#
|
|
15
|
+
class RetryZero < Base
|
|
16
|
+
MSG = 'Use `retry: false` instead of `retry: 0` for clarity.'
|
|
17
|
+
|
|
18
|
+
def on_send(node)
|
|
19
|
+
sidekiq_options_call?(node) do |args|
|
|
20
|
+
args.each { |arg| check_hash(arg) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
alias on_csend on_send
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def check_hash(arg)
|
|
28
|
+
return unless arg.hash_type?
|
|
29
|
+
|
|
30
|
+
arg.pairs.each { |pair| check_pair(pair) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check_pair(pair)
|
|
34
|
+
return unless retry_key?(pair.key)
|
|
35
|
+
|
|
36
|
+
value = pair.value
|
|
37
|
+
return unless value&.int_type? && value.value.zero?
|
|
38
|
+
|
|
39
|
+
add_offense(value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def retry_key?(node)
|
|
43
|
+
node&.sym_type? && node.value == :retry
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for jobs that reschedule themselves.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# def perform
|
|
11
|
+
# self.class.perform_in(1.hour)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
class SelfSchedulingJob < Base
|
|
15
|
+
include ClassNameHelper
|
|
16
|
+
|
|
17
|
+
MSG = 'Avoid self-scheduling jobs. Use Sidekiq Cron or scheduler instead.'
|
|
18
|
+
|
|
19
|
+
def on_def(node)
|
|
20
|
+
return unless node.method?(:perform)
|
|
21
|
+
return unless in_sidekiq_job?(node)
|
|
22
|
+
|
|
23
|
+
class_node = node.each_ancestor(:class).first
|
|
24
|
+
class_name = class_name(class_node)
|
|
25
|
+
|
|
26
|
+
node.each_descendant(:send) do |send|
|
|
27
|
+
next unless PerformMethods.all.include?(send.method_name)
|
|
28
|
+
next unless self_receiver?(send.receiver, class_name)
|
|
29
|
+
|
|
30
|
+
add_offense(send)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def self_receiver?(receiver, class_name)
|
|
37
|
+
return false unless receiver
|
|
38
|
+
|
|
39
|
+
self_class_receiver?(receiver) || const_self_receiver?(receiver, class_name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self_class_receiver?(receiver)
|
|
43
|
+
receiver.send_type? &&
|
|
44
|
+
receiver.method?(:class) &&
|
|
45
|
+
receiver.receiver&.self_type?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def const_self_receiver?(receiver, class_name)
|
|
49
|
+
receiver.const_type? && class_name && receiver.const_name.split('::').last == class_name
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for sensitive data passed as Sidekiq job arguments.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# UserJob.perform_async(user_id, password)
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# UserJob.perform_async(user_id)
|
|
14
|
+
#
|
|
15
|
+
class SensitiveDataInArguments < Base
|
|
16
|
+
include ArgumentTraversal
|
|
17
|
+
|
|
18
|
+
MSG = 'Avoid passing sensitive data in Sidekiq job arguments.'
|
|
19
|
+
|
|
20
|
+
DEFAULT_PATTERNS = %w[
|
|
21
|
+
password passwd pwd token api_key secret credit_card card_number cvv ssn
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
RESTRICT_ON_SEND = PerformMethods.all
|
|
25
|
+
|
|
26
|
+
def on_send(node)
|
|
27
|
+
perform_call?(node) do
|
|
28
|
+
check_arguments(node.arguments, allow_literal: true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
alias on_csend on_send
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def check_argument(arg, allow_literal:)
|
|
36
|
+
return check_literal(arg, allow_literal) if literal_node?(arg)
|
|
37
|
+
return check_variable(arg) if var_node?(arg)
|
|
38
|
+
return check_send(arg) if arg.send_type?
|
|
39
|
+
return check_hash(arg) if arg.hash_type?
|
|
40
|
+
|
|
41
|
+
check_array_elements(arg, allow_literal: allow_literal) if arg.array_type?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def literal_node?(arg)
|
|
45
|
+
arg.type?(:sym, :str)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def var_node?(arg)
|
|
49
|
+
%i[lvar ivar gvar cvar].include?(arg.type)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def check_literal(arg, allow_literal)
|
|
53
|
+
return unless allow_literal
|
|
54
|
+
return unless sensitive_name?(arg.value.to_s)
|
|
55
|
+
|
|
56
|
+
add_offense(arg)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_variable(arg)
|
|
60
|
+
add_offense(arg) if sensitive_name?(arg.name.to_s)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_send(arg)
|
|
64
|
+
return unless arg.receiver.nil? && arg.arguments.empty?
|
|
65
|
+
return unless sensitive_name?(arg.method_name.to_s)
|
|
66
|
+
|
|
67
|
+
add_offense(arg)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_hash(arg)
|
|
71
|
+
arg.pairs.each do |pair|
|
|
72
|
+
check_hash_pair(pair)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def check_hash_pair(pair)
|
|
77
|
+
key = pair.key
|
|
78
|
+
add_offense(key) if key&.type?(:sym, :str) && sensitive_name?(key.value.to_s)
|
|
79
|
+
check_argument(pair.value, allow_literal: false) if pair.value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def sensitive_name?(name)
|
|
83
|
+
patterns.any? { |pattern| name.downcase.include?(pattern) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def patterns
|
|
87
|
+
Array(cop_config.fetch('SensitivePatterns', DEFAULT_PATTERNS))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Recommends using Sidekiq::Job over ActiveJob.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# class MyJob < ApplicationJob
|
|
11
|
+
# def perform; end
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# class MyJob
|
|
16
|
+
# include Sidekiq::Job
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
class SidekiqOverActiveJob < Base
|
|
20
|
+
MSG = 'Prefer Sidekiq::Job over ActiveJob for Sidekiq-specific features.'
|
|
21
|
+
|
|
22
|
+
def on_class(node)
|
|
23
|
+
return unless active_job_class?(node)
|
|
24
|
+
|
|
25
|
+
add_offense(node.loc.keyword)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for rescue blocks that swallow exceptions in Sidekiq jobs.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# def perform
|
|
11
|
+
# do_work
|
|
12
|
+
# rescue => e
|
|
13
|
+
# Rails.logger.error(e)
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# def perform
|
|
18
|
+
# do_work
|
|
19
|
+
# rescue => e
|
|
20
|
+
# Rails.logger.error(e)
|
|
21
|
+
# raise
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
class SilentRescue < Base
|
|
25
|
+
MSG = 'Do not silently swallow exceptions in Sidekiq jobs. Re-raise or handle explicitly.'
|
|
26
|
+
RERAISE_METHODS = %i[raise fail].freeze
|
|
27
|
+
|
|
28
|
+
def on_def(node)
|
|
29
|
+
return unless node.method?(:perform)
|
|
30
|
+
return unless in_sidekiq_job?(node)
|
|
31
|
+
|
|
32
|
+
node.each_descendant(:resbody) do |resbody|
|
|
33
|
+
next if allowed_exception?(resbody)
|
|
34
|
+
next if re_raises?(resbody)
|
|
35
|
+
|
|
36
|
+
add_offense(resbody)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def allowed_exception?(resbody)
|
|
43
|
+
allowed = Array(cop_config.fetch('AllowedExceptions', []))
|
|
44
|
+
exceptions = resbody.exceptions
|
|
45
|
+
return false if exceptions.nil? || exceptions.empty?
|
|
46
|
+
|
|
47
|
+
exceptions.all? do |exception|
|
|
48
|
+
exception.const_type? && allowed.include?(exception.const_name)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def re_raises?(resbody)
|
|
53
|
+
body = resbody.body
|
|
54
|
+
return false unless body
|
|
55
|
+
|
|
56
|
+
body.each_descendant(:send).any? do |send|
|
|
57
|
+
send.receiver.nil? && RERAISE_METHODS.include?(send.method_name)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for `sleep` calls inside Sidekiq jobs.
|
|
7
|
+
#
|
|
8
|
+
# Using `sleep` in a Sidekiq job blocks the worker thread and prevents
|
|
9
|
+
# it from processing other jobs. Instead, use `perform_in` or
|
|
10
|
+
# `perform_at` to schedule the job for later.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# class MyJob
|
|
15
|
+
# include Sidekiq::Job
|
|
16
|
+
#
|
|
17
|
+
# def perform
|
|
18
|
+
# sleep 5
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # good - use perform_in instead
|
|
23
|
+
# MyJob.perform_in(5.seconds, args)
|
|
24
|
+
#
|
|
25
|
+
class SleepInJobs < Base
|
|
26
|
+
MSG = 'Do not use `sleep` in Sidekiq jobs. ' \
|
|
27
|
+
'It blocks the worker thread. Use `perform_in` or `perform_at` instead.'
|
|
28
|
+
|
|
29
|
+
RESTRICT_ON_SEND = %i[sleep].freeze
|
|
30
|
+
|
|
31
|
+
# @!method sleep_call?(node)
|
|
32
|
+
def_node_matcher :sleep_call?, <<~PATTERN
|
|
33
|
+
(send {nil? (const {nil? cbase} :Kernel)} :sleep ...)
|
|
34
|
+
PATTERN
|
|
35
|
+
|
|
36
|
+
def on_send(node)
|
|
37
|
+
return unless sleep_call?(node)
|
|
38
|
+
return unless in_sidekiq_job?(node)
|
|
39
|
+
|
|
40
|
+
add_offense(node)
|
|
41
|
+
end
|
|
42
|
+
alias on_csend on_send
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for symbol arguments passed to Sidekiq job methods.
|
|
7
|
+
#
|
|
8
|
+
# Symbols cannot be properly serialized to JSON and will be converted
|
|
9
|
+
# to strings. This can lead to unexpected behavior.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# MyJob.perform_async(:status)
|
|
14
|
+
# MyJob.perform_async(key: :value)
|
|
15
|
+
# MyJob.perform_in(1.hour, :pending)
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# MyJob.perform_async('status')
|
|
19
|
+
# MyJob.perform_async(key: 'value')
|
|
20
|
+
# MyJob.perform_in(1.hour, 'pending')
|
|
21
|
+
#
|
|
22
|
+
class SymbolArgument < Base
|
|
23
|
+
extend AutoCorrector
|
|
24
|
+
include ArgumentTraversal
|
|
25
|
+
|
|
26
|
+
MSG = 'Do not pass symbols to Sidekiq jobs. Use strings instead.'
|
|
27
|
+
|
|
28
|
+
RESTRICT_ON_SEND = PerformMethods.all
|
|
29
|
+
|
|
30
|
+
# @!method sidekiq_perform_call?(node)
|
|
31
|
+
def_node_matcher :sidekiq_perform_call?, <<~PATTERN
|
|
32
|
+
(send _ {#{RESTRICT_ON_SEND.map(&:inspect).join(' ')}} $...)
|
|
33
|
+
PATTERN
|
|
34
|
+
|
|
35
|
+
def on_send(node)
|
|
36
|
+
sidekiq_perform_call?(node) do |args|
|
|
37
|
+
check_arguments(args)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
alias on_csend on_send
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def check_argument(arg)
|
|
45
|
+
case arg.type
|
|
46
|
+
when :sym
|
|
47
|
+
register_symbol_offense(arg)
|
|
48
|
+
when :dsym
|
|
49
|
+
add_offense(arg)
|
|
50
|
+
when :hash
|
|
51
|
+
check_hash_values(arg)
|
|
52
|
+
when :array
|
|
53
|
+
check_array_elements(arg)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def register_symbol_offense(node)
|
|
58
|
+
add_offense(node) do |corrector|
|
|
59
|
+
corrector.replace(node, symbol_to_string(node))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def symbol_to_string(node)
|
|
64
|
+
node.value.to_s.inspect
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for thread creation inside Sidekiq jobs.
|
|
7
|
+
#
|
|
8
|
+
# Creating threads inside Sidekiq jobs is problematic because:
|
|
9
|
+
# - Sidekiq already manages its own thread pool
|
|
10
|
+
# - Threads may not complete before the job finishes
|
|
11
|
+
# - It can lead to resource leaks and unexpected behavior
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad
|
|
15
|
+
# class MyJob
|
|
16
|
+
# include Sidekiq::Job
|
|
17
|
+
#
|
|
18
|
+
# def perform
|
|
19
|
+
# Thread.new { do_work }
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # good - use separate jobs instead
|
|
24
|
+
# class MyJob
|
|
25
|
+
# include Sidekiq::Job
|
|
26
|
+
#
|
|
27
|
+
# def perform
|
|
28
|
+
# SubJob.perform_async
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
class ThreadInJob < Base
|
|
33
|
+
MSG = 'Do not create threads inside Sidekiq jobs. ' \
|
|
34
|
+
"Use separate jobs or Sidekiq's built-in concurrency instead."
|
|
35
|
+
|
|
36
|
+
RESTRICT_ON_SEND = %i[new fork].freeze
|
|
37
|
+
|
|
38
|
+
# @!method thread_creation?(node)
|
|
39
|
+
def_node_matcher :thread_creation?, <<~PATTERN
|
|
40
|
+
(send (const {nil? cbase} :Thread) {:new :fork} ...)
|
|
41
|
+
PATTERN
|
|
42
|
+
|
|
43
|
+
def on_send(node)
|
|
44
|
+
return unless thread_creation?(node)
|
|
45
|
+
return unless in_sidekiq_job?(node)
|
|
46
|
+
|
|
47
|
+
add_offense(node)
|
|
48
|
+
end
|
|
49
|
+
alias on_csend on_send
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for Sidekiq jobs enqueued inside database transactions.
|
|
7
|
+
#
|
|
8
|
+
# Enqueuing jobs inside a transaction can lead to race conditions where
|
|
9
|
+
# the job runs before the transaction commits, causing the job to see
|
|
10
|
+
# stale data or fail to find the record.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# ActiveRecord::Base.transaction do
|
|
15
|
+
# user.save!
|
|
16
|
+
# NotificationJob.perform_async(user.id)
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # good - enqueue after transaction
|
|
20
|
+
# user.save!
|
|
21
|
+
# NotificationJob.perform_async(user.id)
|
|
22
|
+
#
|
|
23
|
+
# # good - use after_commit callback
|
|
24
|
+
# class User < ApplicationRecord
|
|
25
|
+
# after_commit :send_notification, on: :create
|
|
26
|
+
#
|
|
27
|
+
# def send_notification
|
|
28
|
+
# NotificationJob.perform_async(id)
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
class TransactionLeak < Base
|
|
33
|
+
MSG = 'Do not enqueue Sidekiq jobs inside database transactions. ' \
|
|
34
|
+
'The job may run before the transaction commits.'
|
|
35
|
+
|
|
36
|
+
RESTRICT_ON_SEND = PerformMethods.all
|
|
37
|
+
|
|
38
|
+
# @!method perform_call?(node)
|
|
39
|
+
def_node_matcher :perform_call?, <<~PATTERN
|
|
40
|
+
(send _ {#{RESTRICT_ON_SEND.map(&:inspect).join(' ')}} ...)
|
|
41
|
+
PATTERN
|
|
42
|
+
|
|
43
|
+
def on_send(node)
|
|
44
|
+
return unless perform_call?(node)
|
|
45
|
+
return unless inside_transaction?(node)
|
|
46
|
+
|
|
47
|
+
add_offense(node)
|
|
48
|
+
end
|
|
49
|
+
alias on_csend on_send
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def inside_transaction?(node)
|
|
54
|
+
node.each_ancestor(:block).any? do |block_node|
|
|
55
|
+
transaction_block?(block_node)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def transaction_block?(block_node)
|
|
60
|
+
return false unless block_node.send_node
|
|
61
|
+
|
|
62
|
+
transaction_call?(block_node.send_node)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @!method transaction_call?(node)
|
|
66
|
+
def_node_matcher :transaction_call?, <<~PATTERN
|
|
67
|
+
(send _ :transaction ...)
|
|
68
|
+
PATTERN
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for unknown or unsupported options passed to sidekiq_options.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# sidekiq_options priorty: :high
|
|
11
|
+
# sidekiq_options unique: true
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# sidekiq_options queue: :critical, retry: 5
|
|
15
|
+
#
|
|
16
|
+
class UnknownSidekiqOption < Base
|
|
17
|
+
MSG = 'Unknown or unsupported Sidekiq option `%<option>s` in `sidekiq_options`.'
|
|
18
|
+
|
|
19
|
+
ALLOWED_OPTIONS = %w[queue retry dead backtrace pool tags].freeze
|
|
20
|
+
|
|
21
|
+
def on_send(node)
|
|
22
|
+
sidekiq_options_call?(node) do |args|
|
|
23
|
+
args.each do |arg|
|
|
24
|
+
check_options_hash(arg)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
alias on_csend on_send
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def check_options_hash(node)
|
|
33
|
+
return unless node.hash_type?
|
|
34
|
+
|
|
35
|
+
node.pairs.each do |pair|
|
|
36
|
+
option = option_name(pair.key)
|
|
37
|
+
next unless option
|
|
38
|
+
next if ALLOWED_OPTIONS.include?(option)
|
|
39
|
+
|
|
40
|
+
add_offense(pair, message: format(MSG, option: option))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def option_name(node)
|
|
45
|
+
return node.value.to_s if node.sym_type?
|
|
46
|
+
|
|
47
|
+
node.value if node.str_type?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../sidekiq/language'
|
|
4
|
+
require_relative 'sidekiq/base'
|
|
5
|
+
require_relative 'sidekiq/mixin/argument_traversal'
|
|
6
|
+
require_relative 'sidekiq/mixin/class_name_helper'
|
|
7
|
+
require_relative 'sidekiq/mixin/processed_source_path'
|
|
8
|
+
require_relative 'sidekiq/active_record_argument'
|
|
9
|
+
require_relative 'sidekiq/find_each_in_job'
|
|
10
|
+
require_relative 'sidekiq/consistent_job_suffix'
|
|
11
|
+
require_relative 'sidekiq/constant_job_class_name'
|
|
12
|
+
require_relative 'sidekiq/database_connection_leak'
|
|
13
|
+
require_relative 'sidekiq/date_time_argument'
|
|
14
|
+
require_relative 'sidekiq/deprecated_default_worker_options'
|
|
15
|
+
require_relative 'sidekiq/deprecated_delay_extension'
|
|
16
|
+
require_relative 'sidekiq/deprecated_worker_module'
|
|
17
|
+
require_relative 'sidekiq/excessive_retry'
|
|
18
|
+
require_relative 'sidekiq/huge_job_arguments'
|
|
19
|
+
require_relative 'sidekiq/enqueue_inefficiency'
|
|
20
|
+
require_relative 'sidekiq/job_dependency'
|
|
21
|
+
require_relative 'sidekiq/job_file_location'
|
|
22
|
+
require_relative 'sidekiq/job_file_naming'
|
|
23
|
+
require_relative 'sidekiq/missing_logging'
|
|
24
|
+
require_relative 'sidekiq/missing_timeout'
|
|
25
|
+
require_relative 'sidekiq/mixed_retry_strategies'
|
|
26
|
+
require_relative 'sidekiq/perform_method_parameters'
|
|
27
|
+
require_relative 'sidekiq/async_in_test'
|
|
28
|
+
require_relative 'sidekiq/pii_in_arguments'
|
|
29
|
+
require_relative 'sidekiq/sidekiq_over_active_job'
|
|
30
|
+
require_relative 'sidekiq/redis_in_job'
|
|
31
|
+
require_relative 'sidekiq/retry_zero'
|
|
32
|
+
require_relative 'sidekiq/self_scheduling_job'
|
|
33
|
+
require_relative 'sidekiq/sensitive_data_in_arguments'
|
|
34
|
+
require_relative 'sidekiq/silent_rescue'
|
|
35
|
+
require_relative 'sidekiq/symbol_argument'
|
|
36
|
+
require_relative 'sidekiq/thread_in_job'
|
|
37
|
+
require_relative 'sidekiq/job_include'
|
|
38
|
+
require_relative 'sidekiq/sleep_in_jobs'
|
|
39
|
+
require_relative 'sidekiq/no_rescue_all'
|
|
40
|
+
require_relative 'sidekiq/perform_inline_usage'
|
|
41
|
+
require_relative 'sidekiq/queue_specified'
|
|
42
|
+
require_relative 'sidekiq/retry_specified'
|
|
43
|
+
require_relative 'sidekiq/transaction_leak'
|
|
44
|
+
require_relative 'sidekiq/unknown_sidekiq_option'
|
|
45
|
+
require_relative 'sidekiq/puts_or_print_usage'
|
|
46
|
+
|
|
47
|
+
# Sidekiq Pro Cops
|
|
48
|
+
require_relative 'sidekiq_pro/base'
|
|
49
|
+
require_relative 'sidekiq_pro/batch_callback_method'
|
|
50
|
+
require_relative 'sidekiq_pro/batch_retry_in_callback'
|
|
51
|
+
require_relative 'sidekiq_pro/batch_status_polling'
|
|
52
|
+
require_relative 'sidekiq_pro/batch_without_callback'
|
|
53
|
+
require_relative 'sidekiq_pro/empty_batch'
|
|
54
|
+
require_relative 'sidekiq_pro/expiring_job_without_ttl'
|
|
55
|
+
require_relative 'sidekiq_pro/large_argument_in_batch'
|
|
56
|
+
require_relative 'sidekiq_pro/nested_batch_without_parent'
|
|
57
|
+
require_relative 'sidekiq_pro/reliability_not_enabled'
|
|
58
|
+
|
|
59
|
+
# Sidekiq Enterprise Cops
|
|
60
|
+
require_relative 'sidekiq_ent/base'
|
|
61
|
+
require_relative 'sidekiq_ent/encryption_with_many_arguments'
|
|
62
|
+
require_relative 'sidekiq_ent/encryption_without_secret_bag'
|
|
63
|
+
require_relative 'sidekiq_ent/leader_election_without_block'
|
|
64
|
+
require_relative 'sidekiq_ent/limiter_not_reused'
|
|
65
|
+
require_relative 'sidekiq_ent/limiter_without_lock_timeout'
|
|
66
|
+
require_relative 'sidekiq_ent/limiter_without_wait_timeout'
|
|
67
|
+
require_relative 'sidekiq_ent/periodic_job_invalid_cron'
|
|
68
|
+
require_relative 'sidekiq_ent/periodic_job_with_arguments'
|
|
69
|
+
require_relative 'sidekiq_ent/unique_job_too_short_ttl'
|
|
70
|
+
require_relative 'sidekiq_ent/unique_job_without_ttl'
|
|
71
|
+
require_relative 'sidekiq_ent/unique_until_mismatch'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SidekiqEnt
|
|
6
|
+
# @abstract
|
|
7
|
+
# Base class for Sidekiq Enterprise cops.
|
|
8
|
+
# Provides common functionality for detecting Enterprise-specific patterns.
|
|
9
|
+
class Base < ::RuboCop::Cop::Base
|
|
10
|
+
abstract! if respond_to?(:abstract!)
|
|
11
|
+
include RuboCop::Sidekiq::Language
|
|
12
|
+
|
|
13
|
+
# Detect Sidekiq::Limiter.concurrent/bucket/window/leaky
|
|
14
|
+
# @!method limiter_creation?(node)
|
|
15
|
+
def_node_matcher :limiter_creation?, <<~PATTERN
|
|
16
|
+
(send
|
|
17
|
+
(const (const {nil? cbase} :Sidekiq) :Limiter)
|
|
18
|
+
${:concurrent :bucket :window :leaky}
|
|
19
|
+
$_
|
|
20
|
+
$_
|
|
21
|
+
...
|
|
22
|
+
)
|
|
23
|
+
PATTERN
|
|
24
|
+
|
|
25
|
+
# Detect sidekiq_options with unique_for
|
|
26
|
+
# @!method unique_for_option?(node)
|
|
27
|
+
def_node_matcher :unique_for_option?, <<~PATTERN
|
|
28
|
+
(send nil? :sidekiq_options
|
|
29
|
+
(hash <(pair (sym :unique_for) $_) ...>)
|
|
30
|
+
)
|
|
31
|
+
PATTERN
|
|
32
|
+
|
|
33
|
+
# Detect sidekiq_options with unique_until
|
|
34
|
+
# @!method unique_until_option?(node)
|
|
35
|
+
def_node_matcher :unique_until_option?, <<~PATTERN
|
|
36
|
+
(send nil? :sidekiq_options
|
|
37
|
+
(hash <(pair (sym :unique_until) $_) ...>)
|
|
38
|
+
)
|
|
39
|
+
PATTERN
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|