maintenance_tasks 1.3.0 → 1.7.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 +4 -4
- data/README.md +288 -45
- data/app/controllers/maintenance_tasks/tasks_controller.rb +7 -2
- data/app/helpers/maintenance_tasks/application_helper.rb +1 -0
- data/app/helpers/maintenance_tasks/tasks_helper.rb +19 -14
- data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +44 -23
- data/app/models/maintenance_tasks/application_record.rb +1 -0
- data/app/models/maintenance_tasks/csv_collection_builder.rb +33 -0
- data/app/models/maintenance_tasks/null_collection_builder.rb +31 -0
- data/app/models/maintenance_tasks/progress.rb +8 -3
- data/app/models/maintenance_tasks/run.rb +224 -18
- data/app/models/maintenance_tasks/runner.rb +26 -7
- data/app/models/maintenance_tasks/runs_page.rb +1 -0
- data/app/models/maintenance_tasks/task.rb +225 -0
- data/app/models/maintenance_tasks/task_data.rb +24 -3
- data/app/validators/maintenance_tasks/run_status_validator.rb +2 -2
- data/app/views/maintenance_tasks/runs/_arguments.html.erb +22 -0
- data/app/views/maintenance_tasks/runs/_csv.html.erb +5 -0
- data/app/views/maintenance_tasks/runs/_run.html.erb +18 -1
- data/app/views/maintenance_tasks/runs/info/_custom.html.erb +0 -0
- data/app/views/maintenance_tasks/runs/info/_errored.html.erb +0 -2
- data/app/views/maintenance_tasks/runs/info/_paused.html.erb +2 -2
- data/app/views/maintenance_tasks/runs/info/_running.html.erb +3 -5
- data/app/views/maintenance_tasks/tasks/_custom.html.erb +0 -0
- data/app/views/maintenance_tasks/tasks/_task.html.erb +19 -1
- data/app/views/maintenance_tasks/tasks/index.html.erb +2 -2
- data/app/views/maintenance_tasks/tasks/show.html.erb +37 -2
- data/config/routes.rb +1 -0
- data/db/migrate/20201211151756_create_maintenance_tasks_runs.rb +1 -0
- data/db/migrate/20210225152418_remove_index_on_task_name.rb +1 -0
- data/db/migrate/20210517131953_add_arguments_to_maintenance_tasks_runs.rb +7 -0
- data/db/migrate/20211210152329_add_lock_version_to_maintenance_tasks_runs.rb +8 -0
- data/lib/generators/maintenance_tasks/install_generator.rb +1 -0
- data/lib/generators/maintenance_tasks/templates/task.rb.tt +3 -1
- data/lib/maintenance_tasks/cli.rb +11 -4
- data/lib/maintenance_tasks/engine.rb +15 -1
- data/lib/maintenance_tasks.rb +14 -1
- data/lib/patches/active_record_batch_enumerator.rb +23 -0
- metadata +15 -11
- data/app/models/maintenance_tasks/csv_collection.rb +0 -33
- data/app/tasks/maintenance_tasks/task.rb +0 -133
- data/app/views/maintenance_tasks/runs/_info.html.erb +0 -16
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MaintenanceTasks
|
4
|
+
# Base class that is inherited by the host application's task classes.
|
5
|
+
class Task
|
6
|
+
extend ActiveSupport::DescendantsTracker
|
7
|
+
include ActiveSupport::Callbacks
|
8
|
+
include ActiveModel::Attributes
|
9
|
+
include ActiveModel::AttributeAssignment
|
10
|
+
include ActiveModel::Validations
|
11
|
+
|
12
|
+
class NotFoundError < NameError; end
|
13
|
+
|
14
|
+
# The throttle conditions for a given Task. This is provided as an array of
|
15
|
+
# hashes, with each hash specifying two keys: throttle_condition and
|
16
|
+
# backoff. Note that Tasks inherit conditions from their superclasses.
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
class_attribute :throttle_conditions, default: []
|
20
|
+
|
21
|
+
class_attribute :collection_builder_strategy,
|
22
|
+
default: NullCollectionBuilder.new
|
23
|
+
|
24
|
+
define_callbacks :start, :complete, :error, :cancel, :pause, :interrupt
|
25
|
+
|
26
|
+
class << self
|
27
|
+
# Finds a Task with the given name.
|
28
|
+
#
|
29
|
+
# @param name [String] the name of the Task to be found.
|
30
|
+
#
|
31
|
+
# @return [Task] the Task with the given name.
|
32
|
+
#
|
33
|
+
# @raise [NotFoundError] if a Task with the given name does not exist.
|
34
|
+
def named(name)
|
35
|
+
task = name.safe_constantize
|
36
|
+
raise NotFoundError.new("Task #{name} not found.", name) unless task
|
37
|
+
unless task.is_a?(Class) && task < Task
|
38
|
+
raise NotFoundError.new("#{name} is not a Task.", name)
|
39
|
+
end
|
40
|
+
task
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns a list of concrete classes that inherit from the Task
|
44
|
+
# superclass.
|
45
|
+
#
|
46
|
+
# @return [Array<Class>] the list of classes.
|
47
|
+
def available_tasks
|
48
|
+
load_constants
|
49
|
+
descendants
|
50
|
+
end
|
51
|
+
|
52
|
+
# Make this Task a task that handles CSV.
|
53
|
+
#
|
54
|
+
# An input to upload a CSV will be added in the form to start a Run. The
|
55
|
+
# collection and count method are implemented.
|
56
|
+
def csv_collection
|
57
|
+
unless defined?(ActiveStorage)
|
58
|
+
raise NotImplementedError, "Active Storage needs to be installed\n"\
|
59
|
+
"To resolve this issue run: bin/rails active_storage:install"
|
60
|
+
end
|
61
|
+
|
62
|
+
self.collection_builder_strategy =
|
63
|
+
MaintenanceTasks::CsvCollectionBuilder.new
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns whether the Task handles CSV.
|
67
|
+
#
|
68
|
+
# @return [Boolean] whether the Task handles CSV.
|
69
|
+
def has_csv_content?
|
70
|
+
collection_builder_strategy.has_csv_content?
|
71
|
+
end
|
72
|
+
|
73
|
+
# Processes one item.
|
74
|
+
#
|
75
|
+
# Especially useful for tests.
|
76
|
+
#
|
77
|
+
# @param item the item to process.
|
78
|
+
def process(item)
|
79
|
+
new.process(item)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the collection for this Task.
|
83
|
+
#
|
84
|
+
# Especially useful for tests.
|
85
|
+
#
|
86
|
+
# @return the collection.
|
87
|
+
def collection
|
88
|
+
new.collection
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns the count of items for this Task.
|
92
|
+
#
|
93
|
+
# Especially useful for tests.
|
94
|
+
#
|
95
|
+
# @return the count of items.
|
96
|
+
def count
|
97
|
+
new.count
|
98
|
+
end
|
99
|
+
|
100
|
+
# Add a condition under which this Task will be throttled.
|
101
|
+
#
|
102
|
+
# @param backoff [ActiveSupport::Duration, #call] a custom backoff
|
103
|
+
# can be specified. This is the time to wait before retrying the Task,
|
104
|
+
# defaulting to 30 seconds. If provided as a Duration, the backoff is
|
105
|
+
# wrapped in a proc. Alternatively,an object responding to call can be
|
106
|
+
# used. It must return an ActiveSupport::Duration.
|
107
|
+
# @yieldreturn [Boolean] where the throttle condition is being met,
|
108
|
+
# indicating that the Task should throttle.
|
109
|
+
def throttle_on(backoff: 30.seconds, &condition)
|
110
|
+
backoff_as_proc = backoff
|
111
|
+
backoff_as_proc = -> { backoff } unless backoff.respond_to?(:call)
|
112
|
+
|
113
|
+
self.throttle_conditions += [
|
114
|
+
{ throttle_on: condition, backoff: backoff_as_proc },
|
115
|
+
]
|
116
|
+
end
|
117
|
+
|
118
|
+
# Initialize a callback to run after the task starts.
|
119
|
+
#
|
120
|
+
# @param filter_list apply filters to the callback
|
121
|
+
# (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
|
122
|
+
def after_start(*filter_list, &block)
|
123
|
+
set_callback(:start, :after, *filter_list, &block)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Initialize a callback to run after the task completes.
|
127
|
+
#
|
128
|
+
# @param filter_list apply filters to the callback
|
129
|
+
# (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
|
130
|
+
def after_complete(*filter_list, &block)
|
131
|
+
set_callback(:complete, :after, *filter_list, &block)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Initialize a callback to run after the task pauses.
|
135
|
+
#
|
136
|
+
# @param filter_list apply filters to the callback
|
137
|
+
# (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
|
138
|
+
def after_pause(*filter_list, &block)
|
139
|
+
set_callback(:pause, :after, *filter_list, &block)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Initialize a callback to run after the task is interrupted.
|
143
|
+
#
|
144
|
+
# @param filter_list apply filters to the callback
|
145
|
+
# (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
|
146
|
+
def after_interrupt(*filter_list, &block)
|
147
|
+
set_callback(:interrupt, :after, *filter_list, &block)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Initialize a callback to run after the task is cancelled.
|
151
|
+
#
|
152
|
+
# @param filter_list apply filters to the callback
|
153
|
+
# (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
|
154
|
+
def after_cancel(*filter_list, &block)
|
155
|
+
set_callback(:cancel, :after, *filter_list, &block)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Initialize a callback to run after the task produces an error.
|
159
|
+
#
|
160
|
+
# @param filter_list apply filters to the callback
|
161
|
+
# (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
|
162
|
+
def after_error(*filter_list, &block)
|
163
|
+
set_callback(:error, :after, *filter_list, &block)
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def load_constants
|
169
|
+
namespace = MaintenanceTasks.tasks_module.safe_constantize
|
170
|
+
return unless namespace
|
171
|
+
namespace.constants.map { |constant| namespace.const_get(constant) }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# The contents of a CSV file to be processed by a Task.
|
176
|
+
#
|
177
|
+
# @return [String] the content of the CSV file to process.
|
178
|
+
def csv_content
|
179
|
+
raise NoMethodError unless has_csv_content?
|
180
|
+
|
181
|
+
@csv_content
|
182
|
+
end
|
183
|
+
|
184
|
+
# Set the contents of a CSV file to be processed by a Task.
|
185
|
+
#
|
186
|
+
# @param csv_content [String] the content of the CSV file to process.
|
187
|
+
def csv_content=(csv_content)
|
188
|
+
raise NoMethodError unless has_csv_content?
|
189
|
+
|
190
|
+
@csv_content = csv_content
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns whether the Task handles CSV.
|
194
|
+
#
|
195
|
+
# @return [Boolean] whether the Task handles CSV.
|
196
|
+
def has_csv_content?
|
197
|
+
self.class.has_csv_content?
|
198
|
+
end
|
199
|
+
|
200
|
+
# The collection to be processed, delegated to the strategy.
|
201
|
+
#
|
202
|
+
# @return the collection.
|
203
|
+
def collection
|
204
|
+
self.class.collection_builder_strategy.collection(self)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Placeholder method to raise in case a subclass fails to implement the
|
208
|
+
# expected instance method.
|
209
|
+
#
|
210
|
+
# @param _item [Object] the current item from the enumerator being iterated.
|
211
|
+
#
|
212
|
+
# @raise [NotImplementedError] with a message advising subclasses to
|
213
|
+
# implement an override for this method.
|
214
|
+
def process(_item)
|
215
|
+
raise NoMethodError, "#{self.class.name} must implement `process`."
|
216
|
+
end
|
217
|
+
|
218
|
+
# Total count of iterations to be performed, delegated to the strategy.
|
219
|
+
#
|
220
|
+
# @return [Integer, nil]
|
221
|
+
def count
|
222
|
+
self.class.collection_builder_strategy.count(self)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module MaintenanceTasks
|
3
4
|
# Class that represents the data related to a Task. Such information can be
|
4
5
|
# sourced from a Task or from existing Run records for a Task that was since
|
@@ -40,7 +41,7 @@ module MaintenanceTasks
|
|
40
41
|
def available_tasks
|
41
42
|
task_names = Task.available_tasks.map(&:name)
|
42
43
|
available_task_runs = Run.where(task_name: task_names)
|
43
|
-
last_runs = Run.where(
|
44
|
+
last_runs = Run.with_attached_csv.where(
|
44
45
|
id: available_task_runs.select("MAX(id) as id").group(:task_name)
|
45
46
|
)
|
46
47
|
|
@@ -73,7 +74,11 @@ module MaintenanceTasks
|
|
73
74
|
def code
|
74
75
|
return if deleted?
|
75
76
|
task = Task.named(name)
|
76
|
-
file =
|
77
|
+
file = if Object.respond_to?(:const_source_location)
|
78
|
+
Object.const_source_location(task.name).first
|
79
|
+
else
|
80
|
+
task.instance_method(:process).source_location.first
|
81
|
+
end
|
77
82
|
File.read(file)
|
78
83
|
end
|
79
84
|
|
@@ -129,7 +134,23 @@ module MaintenanceTasks
|
|
129
134
|
|
130
135
|
# @return [Boolean] whether the Task inherits from CsvTask.
|
131
136
|
def csv_task?
|
132
|
-
!deleted? && Task.named(name)
|
137
|
+
!deleted? && Task.named(name).has_csv_content?
|
138
|
+
end
|
139
|
+
|
140
|
+
# @return [Array<String>] the names of parameters the Task accepts.
|
141
|
+
def parameter_names
|
142
|
+
if deleted?
|
143
|
+
[]
|
144
|
+
else
|
145
|
+
Task.named(name).attribute_names
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [MaintenanceTasks::Task, nil] an instance of the Task class.
|
150
|
+
# @return [nil] if the Task file was deleted.
|
151
|
+
def new
|
152
|
+
return if deleted?
|
153
|
+
MaintenanceTasks::Task.named(name).new
|
133
154
|
end
|
134
155
|
|
135
156
|
private
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module MaintenanceTasks
|
3
4
|
# Custom validator class responsible for ensuring that transitions between
|
4
5
|
# Run statuses are valid.
|
@@ -48,9 +49,8 @@ module MaintenanceTasks
|
|
48
49
|
"errored",
|
49
50
|
],
|
50
51
|
# paused -> enqueued occurs when the task is resumed after being paused.
|
51
|
-
# paused -> cancelling when the user cancels the task after it is paused.
|
52
52
|
# paused -> cancelled when the user cancels the task after it is paused.
|
53
|
-
"paused" => ["enqueued", "
|
53
|
+
"paused" => ["enqueued", "cancelled"],
|
54
54
|
# interrupted -> running occurs when the task is resumed after being
|
55
55
|
# interrupted by the job infrastructure.
|
56
56
|
# interrupted -> pausing occurs when the task is paused by the user while
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<% if run.arguments.present? %>
|
2
|
+
<div class="table-container">
|
3
|
+
<h6 class="title is-6">Arguments:</h6>
|
4
|
+
<table class="table">
|
5
|
+
<tbody>
|
6
|
+
<% run.arguments.each do |key, value| %>
|
7
|
+
<tr>
|
8
|
+
<td class="is-family-monospace"><%= key %></td>
|
9
|
+
<td>
|
10
|
+
<% next if value.nil? || value.empty? %>
|
11
|
+
<% if value.include?("\n") %>
|
12
|
+
<pre><%= value %><pre>
|
13
|
+
<% else %>
|
14
|
+
<code><%= value %><code>
|
15
|
+
<% end %>
|
16
|
+
</td>
|
17
|
+
</tr>
|
18
|
+
<% end %>
|
19
|
+
</tbody>
|
20
|
+
</table>
|
21
|
+
</div>
|
22
|
+
<% end %>
|
@@ -1,3 +1,20 @@
|
|
1
1
|
<div class="box">
|
2
|
-
|
2
|
+
<h5 class="title is-5">
|
3
|
+
<%= time_tag run.created_at, title: run.created_at %>
|
4
|
+
<%= status_tag run.status %>
|
5
|
+
</h5>
|
6
|
+
|
7
|
+
<%= progress run %>
|
8
|
+
|
9
|
+
<div class="content">
|
10
|
+
<%= render "maintenance_tasks/runs/info/#{run.status}", run: run %>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<div class="content" id="custom-content">
|
14
|
+
<%= render "maintenance_tasks/runs/info/custom", run: run %>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<%= render "maintenance_tasks/runs/csv", run: run %>
|
18
|
+
<%= tag.hr if run.csv_file.present? && run.arguments.present? %>
|
19
|
+
<%= render "maintenance_tasks/runs/arguments", run: run %>
|
3
20
|
</div>
|
File without changes
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<p>
|
2
2
|
Ran for <%= time_running_in_words run %> until paused,
|
3
|
-
<% if run.
|
4
|
-
<%=
|
3
|
+
<% if (time_to_completion = run.time_to_completion) %>
|
4
|
+
<%= distance_of_time_in_words(time_to_completion) %> remaining.
|
5
5
|
<% else %>
|
6
6
|
processed <%= pluralize run.tick_count, 'item' %> so far.
|
7
7
|
<% end %>
|
@@ -1,5 +1,3 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
<% end %>
|
5
|
-
</p>
|
1
|
+
<% if (time_to_completion = run.time_to_completion) %>
|
2
|
+
<p>Running for <%= time_running_in_words(run) %>. <%= distance_of_time_in_words(time_to_completion).capitalize %> remaining.</p>
|
3
|
+
<% end %>
|
File without changes
|
@@ -4,5 +4,23 @@
|
|
4
4
|
<%= status_tag(task.status) %>
|
5
5
|
</h3>
|
6
6
|
|
7
|
-
|
7
|
+
<% if (run = task.last_run) %>
|
8
|
+
<h5 class="title is-5">
|
9
|
+
<%= time_tag run.created_at, title: run.created_at %>
|
10
|
+
</h5>
|
11
|
+
|
12
|
+
<%= progress run %>
|
13
|
+
|
14
|
+
<div class="content">
|
15
|
+
<%= render "maintenance_tasks/runs/info/#{run.status}", run: run %>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<div class="content" id="custom-content">
|
19
|
+
<%= render "maintenance_tasks/runs/info/custom", run: run %>
|
20
|
+
</div>
|
21
|
+
|
22
|
+
<%= render "maintenance_tasks/runs/csv", run: run %>
|
23
|
+
<%= tag.hr if run.csv_file.present? && run.arguments.present? %>
|
24
|
+
<%= render "maintenance_tasks/runs/arguments", run: run %>
|
25
|
+
<% end %>
|
8
26
|
</div>
|
@@ -2,8 +2,8 @@
|
|
2
2
|
<div class="content is-large">
|
3
3
|
<h3 class="title is-3"> The MaintenanceTasks gem has been successfully installed! </h3>
|
4
4
|
<p>
|
5
|
-
Any new Tasks will show up here. To start writing your first Task,
|
6
|
-
run <code>rails generate maintenance_tasks:task my_task</code>.
|
5
|
+
Any new Tasks will show up here. To start writing your first Task,
|
6
|
+
run <code>bin/rails generate maintenance_tasks:task my_task</code>.
|
7
7
|
</p>
|
8
8
|
</div>
|
9
9
|
<% else %>
|
@@ -4,10 +4,29 @@
|
|
4
4
|
<%= @task %> <%= status_tag(@task.status) %>
|
5
5
|
</h1>
|
6
6
|
|
7
|
-
|
7
|
+
<% last_run = @task.last_run %>
|
8
|
+
<% if last_run %>
|
9
|
+
<h5 class="title is-5">
|
10
|
+
<%= time_tag last_run.created_at, title: last_run.created_at %>
|
11
|
+
</h5>
|
12
|
+
|
13
|
+
<%= progress last_run %>
|
14
|
+
|
15
|
+
<div class="content">
|
16
|
+
<%= render "maintenance_tasks/runs/info/#{last_run.status}", run: last_run %>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<div class="content" id="custom-content">
|
20
|
+
<%= render "maintenance_tasks/runs/info/custom", run: last_run %>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<%= render "maintenance_tasks/runs/csv", run: last_run %>
|
24
|
+
<%= tag.hr if last_run.csv_file.present? %>
|
25
|
+
<%= render "maintenance_tasks/runs/arguments", run: last_run %>
|
26
|
+
<%= tag.hr if last_run.arguments.present? %>
|
27
|
+
<% end %>
|
8
28
|
|
9
29
|
<div class="buttons">
|
10
|
-
<% last_run = @task.last_run %>
|
11
30
|
<% if last_run.nil? || last_run.completed? %>
|
12
31
|
<%= form_with url: run_task_path(@task), method: :put do |form| %>
|
13
32
|
<% if @task.csv_task? %>
|
@@ -16,6 +35,22 @@
|
|
16
35
|
<%= form.file_field :csv_file %>
|
17
36
|
</div>
|
18
37
|
<% end %>
|
38
|
+
<% parameter_names = @task.parameter_names %>
|
39
|
+
<% if parameter_names.any? %>
|
40
|
+
<div class="block">
|
41
|
+
<%= form.fields_for :task_arguments, @task.new do |ff| %>
|
42
|
+
<% parameter_names.each do |parameter_name| %>
|
43
|
+
<div class="field">
|
44
|
+
<%= ff.label parameter_name, parameter_name, class: "label is-family-monospace" %>
|
45
|
+
<div class="control">
|
46
|
+
<%= parameter_field(ff, parameter_name) %>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
<% end %>
|
50
|
+
<% end %>
|
51
|
+
</div>
|
52
|
+
<% end %>
|
53
|
+
<%= render "maintenance_tasks/tasks/custom", form: form %>
|
19
54
|
<div class="block">
|
20
55
|
<%= form.submit 'Run', class: "button is-success", disabled: @task.deleted? %>
|
21
56
|
</div>
|
data/config/routes.rb
CHANGED
@@ -9,7 +9,9 @@ module <%= tasks_module %>
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def process(element)
|
12
|
-
# The work to be done in a single iteration of the task
|
12
|
+
# The work to be done in a single iteration of the task.
|
13
|
+
# This should be idempotent, as the same element may be processed more
|
14
|
+
# than once if the task is interrupted and resumed.
|
13
15
|
end
|
14
16
|
|
15
17
|
def count
|
@@ -25,18 +25,25 @@ module MaintenanceTasks
|
|
25
25
|
LONGDESC
|
26
26
|
|
27
27
|
# Specify the CSV file to process for CSV Tasks
|
28
|
-
|
29
|
-
|
28
|
+
desc = "Supply a CSV file to be processed by a CSV Task, "\
|
29
|
+
"--csv path/to/csv/file.csv"
|
30
|
+
option :csv, desc: desc
|
31
|
+
# Specify arguments to supply to a Task supporting parameters
|
32
|
+
desc = "Supply arguments for a Task that accepts parameters as a set of "\
|
33
|
+
"<key>:<value> pairs."
|
34
|
+
option :arguments, type: :hash, desc: desc
|
30
35
|
|
31
36
|
# Command to run a Task.
|
32
37
|
#
|
33
38
|
# It instantiates a Runner and sends a run message with the given Task name.
|
34
39
|
# If a CSV file is supplied using the --csv option, an attachable with the
|
35
|
-
# File IO object is sent along with the Task name to run.
|
40
|
+
# File IO object is sent along with the Task name to run. If arguments are
|
41
|
+
# supplied using the --arguments option, these are also passed to run.
|
36
42
|
#
|
37
43
|
# @param name [String] the name of the Task to be run.
|
38
44
|
def perform(name)
|
39
|
-
|
45
|
+
arguments = options[:arguments] || {}
|
46
|
+
task = Runner.run(name: name, csv_file: csv_file, arguments: arguments)
|
40
47
|
say_status(:success, "#{task.name} was enqueued.", :green)
|
41
48
|
rescue => error
|
42
49
|
say_status(:error, error.message, :red)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "active_record/railtie"
|
3
4
|
|
4
5
|
module MaintenanceTasks
|
@@ -7,10 +8,23 @@ module MaintenanceTasks
|
|
7
8
|
class Engine < ::Rails::Engine
|
8
9
|
isolate_namespace MaintenanceTasks
|
9
10
|
|
10
|
-
initializer "
|
11
|
+
initializer "maintenance_tasks.warn_classic_autoloader" do
|
12
|
+
unless Rails.autoloaders.zeitwerk_enabled?
|
13
|
+
ActiveSupport::Deprecation.warn(<<~MSG.squish)
|
14
|
+
Autoloading in classic mode is deprecated and support will be removed in the next
|
15
|
+
release of Maintenance Tasks. Please use Zeitwerk to autoload your application.
|
16
|
+
MSG
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
initializer "maintenance_tasks.eager_load_for_classic_autoloader" do
|
11
21
|
eager_load! unless Rails.autoloaders.zeitwerk_enabled?
|
12
22
|
end
|
13
23
|
|
24
|
+
initializer "maintenance_tasks.configs" do
|
25
|
+
MaintenanceTasks.backtrace_cleaner = Rails.backtrace_cleaner
|
26
|
+
end
|
27
|
+
|
14
28
|
config.to_prepare do
|
15
29
|
_ = TaskJobConcern # load this for JobIteration compatibility check
|
16
30
|
unless Rails.autoloaders.zeitwerk_enabled?
|
data/lib/maintenance_tasks.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "action_controller"
|
3
4
|
require "action_view"
|
4
5
|
require "active_job"
|
@@ -7,6 +8,8 @@ require "active_record"
|
|
7
8
|
require "job-iteration"
|
8
9
|
require "maintenance_tasks/engine"
|
9
10
|
|
11
|
+
require "patches/active_record_batch_enumerator"
|
12
|
+
|
10
13
|
# The engine's namespace module. It provides isolation between the host
|
11
14
|
# application's code and the engine-specific code. Top-level engine constants
|
12
15
|
# and variables are defined under this module.
|
@@ -50,6 +53,16 @@ module MaintenanceTasks
|
|
50
53
|
# app's config/storage.yml.
|
51
54
|
mattr_accessor :active_storage_service
|
52
55
|
|
56
|
+
# @!attribute backtrace_cleaner
|
57
|
+
# @scope class
|
58
|
+
#
|
59
|
+
# The Active Support backtrace cleaner that will be used to clean the
|
60
|
+
# backtrace of a Task that errors.
|
61
|
+
#
|
62
|
+
# @return [ActiveSupport::BacktraceCleaner, nil] the backtrace cleaner to
|
63
|
+
# use when cleaning a Run's backtrace.
|
64
|
+
mattr_accessor :backtrace_cleaner
|
65
|
+
|
53
66
|
# @private
|
54
67
|
def self.error_handler
|
55
68
|
return @error_handler if defined?(@error_handler)
|
@@ -61,7 +74,7 @@ module MaintenanceTasks
|
|
61
74
|
unless error_handler.arity == 3
|
62
75
|
ActiveSupport::Deprecation.warn(
|
63
76
|
"MaintenanceTasks.error_handler should be a lambda that takes three "\
|
64
|
-
|
77
|
+
"arguments: error, task_context, and errored_element."
|
65
78
|
)
|
66
79
|
@error_handler = ->(error, _task_context, _errored_element) do
|
67
80
|
error_handler.call(error)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# TODO: Remove this patch once all supported Rails versions include the changes
|
4
|
+
# upstream - https://github.com/rails/rails/pull/42312/commits/a031a43d969c87542c4ee8d0d338d55fcbb53376
|
5
|
+
module ActiveRecordBatchEnumerator
|
6
|
+
# The primary key value from which the BatchEnumerator starts,
|
7
|
+
# inclusive of the value.
|
8
|
+
attr_reader :start
|
9
|
+
|
10
|
+
# The primary key value at which the BatchEnumerator ends,
|
11
|
+
# inclusive of the value.
|
12
|
+
attr_reader :finish
|
13
|
+
|
14
|
+
# The relation from which the BatchEnumerator yields batches.
|
15
|
+
attr_reader :relation
|
16
|
+
|
17
|
+
# The size of the batches yielded by the BatchEnumerator.
|
18
|
+
def batch_size
|
19
|
+
@of
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
ActiveRecord::Batches::BatchEnumerator.include(ActiveRecordBatchEnumerator)
|