1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
# frozen_string_literal: true
require_relative '../../code_reuse_helpers'
module RuboCop
module Cop
module SidekiqLoadBalancing
# This cop checks for including_scheduled: true option in idempotent Sidekiq workers that utilize load balancing capabilities.
#
# @example
#
# # bad
# class BadWorker
# include ApplicationWorker
#
# data_consistency :delayed
# idempotent!
#
# def perform
# end
# end
#
# # bad
# class BadWorker
# include ApplicationWorker
#
# data_consistency :delayed
#
# deduplicate :until_executing
# idempotent!
#
# def perform
# end
# end
#
# # good
# class GoodWorker
# include ApplicationWorker
#
# data_consistency :delayed
#
# deduplicate :until_executing, including_scheduled: true
# idempotent!
#
# def perform
# end
# end
#
class WorkerDataConsistencyWithDeduplication < RuboCop::Cop::Base
include CodeReuseHelpers
extend AutoCorrector
HELP_LINK = 'https://docs.gitlab.com/ee/development/sidekiq_style_guide.html#scheduling-jobs-in-the-future'
REPLACEMENT = ', including_scheduled: true'
DEFAULT_STRATEGY = ':until_executing'
MSG = <<~MSG
Workers that declare either `:sticky` or `:delayed` data consistency become eligible for database load-balancing.
In both cases, jobs are enqueued with a short delay.
If you do want to deduplicate jobs that utilize load-balancing, you need to specify including_scheduled: true
argument when defining deduplication strategy.
See #{HELP_LINK} for a more detailed explanation of these settings.
MSG
def_node_search :application_worker?, <<~PATTERN
`(send nil? :include (const nil? :ApplicationWorker))
PATTERN
def_node_search :idempotent_worker?, <<~PATTERN
`(send nil? :idempotent!)
PATTERN
def_node_search :data_consistency_defined?, <<~PATTERN
`(send nil? :data_consistency (sym {:sticky :delayed }))
PATTERN
def_node_matcher :including_scheduled?, <<~PATTERN
`(hash <(pair (sym :including_scheduled) (%1)) ...>)
PATTERN
def_node_matcher :deduplicate_strategy?, <<~PATTERN
`(send nil? :deduplicate (sym $_) $(...)?)
PATTERN
def on_class(node)
return unless in_worker?(node)
return unless application_worker?(node)
return unless idempotent_worker?(node)
return unless data_consistency_defined?(node)
@strategy, options = deduplicate_strategy?(node)
including_scheduled = false
if options
@deduplicate_options = options[0]
including_scheduled = including_scheduled?(@deduplicate_options, :true) # rubocop:disable Lint/BooleanSymbol
end
@offense = !(including_scheduled || @strategy == :none)
end
def on_send(node)
return unless offense
if node.children[1] == :deduplicate
add_offense(node.loc.expression) do |corrector|
autocorrect_deduplicate_strategy(node, corrector)
end
elsif node.children[1] == :idempotent! && !strategy
add_offense(node.loc.expression) do |corrector|
autocorrect_missing_deduplicate_strategy(node, corrector)
end
end
end
private
attr_reader :offense, :deduplicate_options, :strategy
def autocorrect_deduplicate_with_options(corrector)
if including_scheduled?(deduplicate_options, :false) # rubocop:disable Lint/BooleanSymbol
replacement = deduplicate_options.source.sub("including_scheduled: false", "including_scheduled: true")
corrector.replace(deduplicate_options.loc.expression, replacement)
else
corrector.insert_after(deduplicate_options.loc.expression, REPLACEMENT)
end
end
def autocorrect_deduplicate_without_options(node, corrector)
corrector.insert_after(node.loc.expression, REPLACEMENT)
end
def autocorrect_missing_deduplicate_strategy(node, corrector)
indent_found = node.source_range.source_line =~ /^( +)/
# Get indentation size
whitespaces = Regexp.last_match(1).size if indent_found
replacement = "deduplicate #{DEFAULT_STRATEGY}#{REPLACEMENT}\n"
# Add indentation in the end since we are inserting a whole line before idempotent!
replacement += ' ' * whitespaces.to_i
corrector.insert_before(node.source_range, replacement)
end
def autocorrect_deduplicate_strategy(node, corrector)
if deduplicate_options
autocorrect_deduplicate_with_options(corrector)
else
autocorrect_deduplicate_without_options(node, corrector)
end
end
end
end
end
end
|