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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6019b1623a8cc72e877154f52e8437339598f0cab121189d78692e0989e86e61
|
4
|
+
data.tar.gz: 21563dc6dffadd2e58dd551fe2b7d71297b2da36500722f5d9fed91892b1fd6e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93228d08b49fab144297cb44a0aa4b9be53144ff13b88c2eba384ef29fdb47b9a8caefaf94aaf1fbfbf4584f4d544396720c17d86f3bad31508fd66dfd7507b8
|
7
|
+
data.tar.gz: cab4cebb685fddf978eeebd62a5c16867215f849d424b9ba948beba0e93f42a5fe11ee1eccc15174c1494fa16a2fa100d3a7745c5eacef2527d425895805cf29
|
data/README.md
CHANGED
@@ -10,7 +10,7 @@ To install the gem and run the install generator, execute:
|
|
10
10
|
|
11
11
|
```bash
|
12
12
|
$ bundle add maintenance_tasks
|
13
|
-
$ rails generate maintenance_tasks:install
|
13
|
+
$ bin/rails generate maintenance_tasks:install
|
14
14
|
```
|
15
15
|
|
16
16
|
The generator creates and runs a migration to add the necessary table to your
|
@@ -23,12 +23,12 @@ handler](#customizing-the-error-handler) for more information.
|
|
23
23
|
|
24
24
|
### Active Job Dependency
|
25
25
|
|
26
|
-
The Maintenance Tasks framework relies on
|
27
|
-
Tasks. The default queuing backend for
|
26
|
+
The Maintenance Tasks framework relies on Active Job behind the scenes to run
|
27
|
+
Tasks. The default queuing backend for Active Job is
|
28
28
|
[asynchronous][async-adapter]. It is **strongly recommended** to change this to
|
29
29
|
a persistent backend so that Task progress is not lost during code or
|
30
30
|
infrastructure changes. For more information on configuring a queuing backend,
|
31
|
-
take a look at the [
|
31
|
+
take a look at the [Active Job documentation][active-job-docs].
|
32
32
|
|
33
33
|
[async-adapter]: https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html
|
34
34
|
[active-job-docs]: https://guides.rubyonrails.org/active_job_basics.html#setting-the-backend
|
@@ -40,7 +40,7 @@ take a look at the [ActiveJob documentation][active-job-docs].
|
|
40
40
|
A generator is provided to create tasks. Generate a new task by running:
|
41
41
|
|
42
42
|
```bash
|
43
|
-
$ rails generate maintenance_tasks:task update_posts
|
43
|
+
$ bin/rails generate maintenance_tasks:task update_posts
|
44
44
|
```
|
45
45
|
|
46
46
|
This creates the task file `app/tasks/maintenance/update_posts_task.rb`.
|
@@ -57,6 +57,7 @@ Example:
|
|
57
57
|
|
58
58
|
```ruby
|
59
59
|
# app/tasks/maintenance/update_posts_task.rb
|
60
|
+
|
60
61
|
module Maintenance
|
61
62
|
class UpdatePostsTask < MaintenanceTasks::Task
|
62
63
|
def collection
|
@@ -68,7 +69,7 @@ module Maintenance
|
|
68
69
|
end
|
69
70
|
|
70
71
|
def process(post)
|
71
|
-
post.update!(content:
|
72
|
+
post.update!(content: "New content!")
|
72
73
|
end
|
73
74
|
end
|
74
75
|
end
|
@@ -77,16 +78,16 @@ end
|
|
77
78
|
### Creating a CSV Task
|
78
79
|
|
79
80
|
You can also write a Task that iterates on a CSV file. Note that writing CSV
|
80
|
-
Tasks **requires
|
81
|
-
is specified in your application's Gemfile, and that you've followed the
|
82
|
-
|
81
|
+
Tasks **requires Active Storage to be configured**. Ensure that the dependency
|
82
|
+
is specified in your application's Gemfile, and that you've followed the [setup
|
83
|
+
instuctions][setup].
|
83
84
|
|
84
85
|
[setup]: https://edgeguides.rubyonrails.org/active_storage_overview.html#setup
|
85
86
|
|
86
87
|
Generate a CSV Task by running:
|
87
88
|
|
88
89
|
```bash
|
89
|
-
$ rails generate maintenance_tasks:task import_posts --csv
|
90
|
+
$ bin/rails generate maintenance_tasks:task import_posts --csv
|
90
91
|
```
|
91
92
|
|
92
93
|
The generated task is a subclass of `MaintenanceTasks::Task` that implements:
|
@@ -95,6 +96,7 @@ The generated task is a subclass of `MaintenanceTasks::Task` that implements:
|
|
95
96
|
|
96
97
|
```ruby
|
97
98
|
# app/tasks/maintenance/import_posts_task.rb
|
99
|
+
|
98
100
|
module Maintenance
|
99
101
|
class ImportPostsTask < MaintenanceTasks::Task
|
100
102
|
csv_collection
|
@@ -112,6 +114,52 @@ title,content
|
|
112
114
|
My Title,Hello World!
|
113
115
|
```
|
114
116
|
|
117
|
+
The files uploaded to your Active Storage service provider will be renamed
|
118
|
+
to include an ISO8601 timestamp and the Task name in snake case format.
|
119
|
+
The CSV is expected to have a trailing newline at the end of the file.
|
120
|
+
|
121
|
+
### Processing Batch Collections
|
122
|
+
|
123
|
+
The Maintenance Tasks gem supports processing Active Records in batches. This
|
124
|
+
can reduce the number of calls your Task makes to the database. Use
|
125
|
+
`ActiveRecord::Batches#in_batches` on the relation returned by your collection
|
126
|
+
to specify that your Task should process batches instead of records. Active
|
127
|
+
Record defaults to 1000 records by batch, but a custom size can be specified.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
# app/tasks/maintenance/update_posts_in_batches_task.rb
|
131
|
+
|
132
|
+
module Maintenance
|
133
|
+
class UpdatePostsInBatchesTask < MaintenanceTasks::Task
|
134
|
+
def collection
|
135
|
+
Post.in_batches
|
136
|
+
end
|
137
|
+
|
138
|
+
def process(batch_of_posts)
|
139
|
+
batch_of_posts.update_all(content: "New content added on #{Time.now.utc}")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
Ensure that you've implemented the following methods:
|
146
|
+
|
147
|
+
* `collection`: return an `ActiveRecord::Batches::BatchEnumerator`.
|
148
|
+
* `process`: do the work of your Task on a batch (`ActiveRecord::Relation`).
|
149
|
+
|
150
|
+
Note that `#count` is calculated automatically based on the number of batches in
|
151
|
+
your collection, and your Task's progress will be displayed in terms of batches
|
152
|
+
(not the number of records in the relation).
|
153
|
+
|
154
|
+
**Important!** Batches should only be used if `#process` is performing a batch
|
155
|
+
operation such as `#update_all` or `#delete_all`. If you need to iterate over
|
156
|
+
individual records, you should define a collection that [returns an
|
157
|
+
`ActiveRecord::Relation`](#creating-a-task). This uses batching internally, but
|
158
|
+
loads the records with one SQL query. Conversely, batch collections load the
|
159
|
+
primary keys of the records of the batch first, and then perform an additional
|
160
|
+
query to load the records when calling `each` (or any `Enumerable` method)
|
161
|
+
inside `#process`.
|
162
|
+
|
115
163
|
### Throttling
|
116
164
|
|
117
165
|
Maintenance Tasks often modify a lot of data and can be taxing on your database.
|
@@ -122,6 +170,7 @@ Specify the throttle condition as a block:
|
|
122
170
|
|
123
171
|
```ruby
|
124
172
|
# app/tasks/maintenance/update_posts_throttled_task.rb
|
173
|
+
|
125
174
|
module Maintenance
|
126
175
|
class UpdatePostsThrottledTask < MaintenanceTasks::Task
|
127
176
|
throttle_on(backoff: 1.minute) do
|
@@ -145,13 +194,129 @@ end
|
|
145
194
|
|
146
195
|
Note that it's up to you to define a throttling condition that makes sense for
|
147
196
|
your app. Shopify implements `DatabaseStatus.healthy?` to check various MySQL
|
148
|
-
metrics such as replication lag, DB threads, whether DB writes are available,
|
197
|
+
metrics such as replication lag, DB threads, whether DB writes are available,
|
149
198
|
etc.
|
150
199
|
|
151
200
|
Tasks can define multiple throttle conditions. Throttle conditions are inherited
|
152
201
|
by descendants, and new conditions will be appended without impacting existing
|
153
202
|
conditions.
|
154
203
|
|
204
|
+
The backoff can also be specified as a proc:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
# app/tasks/maintenance/update_posts_throttled_task.rb
|
208
|
+
|
209
|
+
module Maintenance
|
210
|
+
class UpdatePostsThrottledTask < MaintenanceTasks::Task
|
211
|
+
throttle_on(backoff: -> { RandomBackoffGenerator.generate_duration } ) do
|
212
|
+
DatabaseStatus.unhealthy?
|
213
|
+
end
|
214
|
+
...
|
215
|
+
end
|
216
|
+
end
|
217
|
+
```
|
218
|
+
### Custom Task Parameters
|
219
|
+
|
220
|
+
Tasks may need additional information, supplied via parameters, to run.
|
221
|
+
Parameters can be defined as Active Model Attributes in a Task, and then become
|
222
|
+
accessible to any of Task's methods: `#collection`, `#count`, or `#process`.
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
# app/tasks/maintenance/update_posts_via_params_task.rb
|
226
|
+
|
227
|
+
module Maintenance
|
228
|
+
class UpdatePostsViaParamsTask < MaintenanceTasks::Task
|
229
|
+
attribute :updated_content, :string
|
230
|
+
validates :updated_content, presence: true
|
231
|
+
|
232
|
+
def collection
|
233
|
+
Post.all
|
234
|
+
end
|
235
|
+
|
236
|
+
def count
|
237
|
+
collection.count
|
238
|
+
end
|
239
|
+
|
240
|
+
def process(post)
|
241
|
+
post.update!(content: updated_content)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
Tasks can leverage Active Model Validations when defining parameters. Arguments
|
248
|
+
supplied to a Task accepting parameters will be validated before the Task starts
|
249
|
+
to run. Since arguments are specified in the user interface via text area
|
250
|
+
inputs, it's important to check that they conform to the format your Task
|
251
|
+
expects, and to sanitize any inputs if necessary.
|
252
|
+
|
253
|
+
### Using Task Callbacks
|
254
|
+
|
255
|
+
The Task provides callbacks that hook into its life cycle.
|
256
|
+
|
257
|
+
Available callbacks are:
|
258
|
+
|
259
|
+
* `after_start`
|
260
|
+
* `after_pause`
|
261
|
+
* `after_interrupt`
|
262
|
+
* `after_cancel`
|
263
|
+
* `after_complete`
|
264
|
+
* `after_error`
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
module Maintenance
|
268
|
+
class UpdatePostsTask < MaintenanceTasks::Task
|
269
|
+
after_start :notify
|
270
|
+
|
271
|
+
def notify
|
272
|
+
NotifyJob.perform_later(self.class.name)
|
273
|
+
end
|
274
|
+
|
275
|
+
# ...
|
276
|
+
end
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
Note: The `after_error` callback is guaranteed to complete,
|
281
|
+
so any exceptions raised in your callback code are ignored.
|
282
|
+
If your `after_error` callback code can raise an exception,
|
283
|
+
you'll need to rescue it and handle it appropriately
|
284
|
+
within the callback.
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
module Maintenance
|
288
|
+
class UpdatePostsTask < MaintenanceTasks::Task
|
289
|
+
after_error :dangerous_notify
|
290
|
+
|
291
|
+
def dangerous_notify
|
292
|
+
# This error is rescued in favour of the original error causing the error flow.
|
293
|
+
raise NotDeliveredError
|
294
|
+
end
|
295
|
+
|
296
|
+
# ...
|
297
|
+
end
|
298
|
+
end
|
299
|
+
```
|
300
|
+
|
301
|
+
If any of the other callbacks cause an exception,
|
302
|
+
it will be handled by the error handler,
|
303
|
+
and will cause the task to stop running.
|
304
|
+
|
305
|
+
Callback behaviour can be shared across all tasks using an initializer.
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
# config/initializer/maintenance_tasks.rb
|
309
|
+
Rails.autoloaders.main.on_load("MaintenanceTasks::Task") do
|
310
|
+
MaintenanceTasks::Task.class_eval do
|
311
|
+
after_start(:notify)
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
def notify; end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
155
320
|
### Considerations when writing Tasks
|
156
321
|
|
157
322
|
MaintenanceTasks relies on the queue adapter configured for your application to
|
@@ -184,7 +349,7 @@ Example:
|
|
184
349
|
```ruby
|
185
350
|
# test/tasks/maintenance/update_posts_task_test.rb
|
186
351
|
|
187
|
-
require
|
352
|
+
require "test_helper"
|
188
353
|
|
189
354
|
module Maintenance
|
190
355
|
class UpdatePostsTaskTest < ActiveSupport::TestCase
|
@@ -193,7 +358,7 @@ module Maintenance
|
|
193
358
|
|
194
359
|
Maintenance::UpdatePostsTask.process(post)
|
195
360
|
|
196
|
-
assert_equal
|
361
|
+
assert_equal "New content!", post.content
|
197
362
|
end
|
198
363
|
end
|
199
364
|
end
|
@@ -206,20 +371,50 @@ takes a `CSV::Row` as an argument. You can pass a row, or a hash with string
|
|
206
371
|
keys to `#process` from your test.
|
207
372
|
|
208
373
|
```ruby
|
209
|
-
#
|
374
|
+
# test/tasks/maintenance/import_posts_task_test.rb
|
375
|
+
|
376
|
+
require "test_helper"
|
377
|
+
|
210
378
|
module Maintenance
|
211
379
|
class ImportPostsTaskTest < ActiveSupport::TestCase
|
212
380
|
test "#process performs a task iteration" do
|
213
381
|
assert_difference -> { Post.count } do
|
214
382
|
Maintenance::UpdatePostsTask.process({
|
215
|
-
|
216
|
-
|
383
|
+
"title" => "My Title",
|
384
|
+
"content" => "Hello World!",
|
217
385
|
})
|
218
386
|
end
|
219
387
|
|
220
388
|
post = Post.last
|
221
|
-
assert_equal
|
222
|
-
assert_equal
|
389
|
+
assert_equal "My Title", post.title
|
390
|
+
assert_equal "Hello World!", post.content
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
```
|
395
|
+
|
396
|
+
### Writing tests for a Task with parameters
|
397
|
+
|
398
|
+
Tests for tasks with parameters need to instatiate the task class in order to
|
399
|
+
assign attributes. Once the task instance is setup, you may test `#process`
|
400
|
+
normally.
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
# test/tasks/maintenance/update_posts_via_params_task_test.rb
|
404
|
+
|
405
|
+
require "test_helper"
|
406
|
+
|
407
|
+
module Maintenance
|
408
|
+
class UpdatePostsViaParamsTaskTest < ActiveSupport::TestCase
|
409
|
+
setup do
|
410
|
+
@task = UpdatePostsViaParamsTask.new
|
411
|
+
@task.updated_content = "Testing"
|
412
|
+
end
|
413
|
+
|
414
|
+
test "#process performs a task iteration" do
|
415
|
+
assert_difference -> { Post.first.content } do
|
416
|
+
task.process(Post.first)
|
417
|
+
end
|
223
418
|
end
|
224
419
|
end
|
225
420
|
end
|
@@ -238,13 +433,21 @@ $ bundle exec maintenance_tasks perform Maintenance::UpdatePostsTask
|
|
238
433
|
To run a Task that processes CSVs from the command line, use the --csv option:
|
239
434
|
|
240
435
|
```bash
|
241
|
-
$ bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv
|
436
|
+
$ bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv "path/to/my_csv.csv"
|
437
|
+
```
|
438
|
+
|
439
|
+
To run a Task that takes arguments from the command line, use the --arguments
|
440
|
+
option, passing arguments as a set of \<key>:\<value> pairs:
|
441
|
+
|
442
|
+
```bash
|
443
|
+
$ bundle exec maintenance_tasks perform Maintenance::ParamsTask \
|
444
|
+
--arguments post_ids:1,2,3 content:"Hello, World!"
|
242
445
|
```
|
243
446
|
|
244
447
|
You can also run a Task in Ruby by sending `run` with a Task name to Runner:
|
245
448
|
|
246
449
|
```ruby
|
247
|
-
MaintenanceTasks::Runner.run(name:
|
450
|
+
MaintenanceTasks::Runner.run(name: "Maintenance::UpdatePostsTask")
|
248
451
|
```
|
249
452
|
|
250
453
|
To run a Task that processes CSVs using the Runner, provide a Hash containing an
|
@@ -252,8 +455,18 @@ open IO object and a filename to `run`:
|
|
252
455
|
|
253
456
|
```ruby
|
254
457
|
MaintenanceTasks::Runner.run(
|
255
|
-
name:
|
256
|
-
csv_file: { io: File.open(
|
458
|
+
name: "Maintenance::ImportPostsTask",
|
459
|
+
csv_file: { io: File.open("path/to/my_csv.csv"), filename: "my_csv.csv" }
|
460
|
+
)
|
461
|
+
```
|
462
|
+
|
463
|
+
To run a Task that takes arguments using the Runner, provide a Hash containing
|
464
|
+
the set of arguments (`{ parameter_name: argument_value }`) to `run`:
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
MaintenanceTasks::Runner.run(
|
468
|
+
name: "Maintenance::ParamsTask",
|
469
|
+
arguments: { post_ids: "1,2,3" }
|
257
470
|
)
|
258
471
|
```
|
259
472
|
|
@@ -266,8 +479,8 @@ a Task can be in:
|
|
266
479
|
* **enqueued**: A Task that is waiting to be performed after a user has
|
267
480
|
instructed it to run.
|
268
481
|
* **running**: A Task that is currently being performed by a job worker.
|
269
|
-
* **pausing**: A Task that was paused by a user, but needs to finish work
|
270
|
-
|
482
|
+
* **pausing**: A Task that was paused by a user, but needs to finish work before
|
483
|
+
stopping.
|
271
484
|
* **paused**: A Task that was paused by a user and is not performing. It can be
|
272
485
|
resumed.
|
273
486
|
* **interrupted**: A Task that has been momentarily interrupted by the job
|
@@ -341,9 +554,10 @@ you can define an error handler:
|
|
341
554
|
|
342
555
|
```ruby
|
343
556
|
# config/initializers/maintenance_tasks.rb
|
557
|
+
|
344
558
|
MaintenanceTasks.error_handler = ->(error, task_context, _errored_element) do
|
345
559
|
Bugsnag.notify(error) do |notification|
|
346
|
-
notification.
|
560
|
+
notification.add_metadata(:task, task_context)
|
347
561
|
end
|
348
562
|
end
|
349
563
|
```
|
@@ -356,18 +570,18 @@ The error handler should be a lambda that accepts three arguments:
|
|
356
570
|
* `task_name`: The name of the Task that errored
|
357
571
|
* `started_at`: The time the Task started
|
358
572
|
* `ended_at`: The time the Task errored
|
573
|
+
|
359
574
|
Note that `task_context` may be empty if the Task produced an error before any
|
360
575
|
context could be gathered (for example, if deserializing the job to process
|
361
576
|
your Task failed).
|
362
|
-
* `errored_element`: The element, if any, that was being processed when the
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
report.
|
577
|
+
* `errored_element`: The element, if any, that was being processed when the Task
|
578
|
+
raised an exception. If you would like to pass this object to your exception
|
579
|
+
monitoring service, make sure you **sanitize the object** to avoid leaking
|
580
|
+
sensitive data and **convert it to a format** that is compatible with your bug
|
581
|
+
tracker. For example, Bugsnag only sends the id and class name of Active
|
582
|
+
Record objects in order to protect sensitive data. CSV rows, on the other
|
583
|
+
hand, are converted to strings and passed raw to Bugsnag, so make sure to
|
584
|
+
filter any personal data from these objects before adding them to a report.
|
371
585
|
|
372
586
|
#### Customizing the maintenance tasks module
|
373
587
|
|
@@ -376,7 +590,8 @@ tasks will be placed.
|
|
376
590
|
|
377
591
|
```ruby
|
378
592
|
# config/initializers/maintenance_tasks.rb
|
379
|
-
|
593
|
+
|
594
|
+
MaintenanceTasks.tasks_module = "TaskModule"
|
380
595
|
```
|
381
596
|
|
382
597
|
If no value is specified, it will default to `Maintenance`.
|
@@ -389,9 +604,11 @@ maintenance tasks in your application.
|
|
389
604
|
|
390
605
|
```ruby
|
391
606
|
# config/initializers/maintenance_tasks.rb
|
607
|
+
|
392
608
|
MaintenanceTasks.job = 'CustomTaskJob'
|
393
609
|
|
394
610
|
# app/jobs/custom_task_job.rb
|
611
|
+
|
395
612
|
class CustomTaskJob < MaintenanceTasks::TaskJob
|
396
613
|
queue_as :low_priority
|
397
614
|
end
|
@@ -399,8 +616,8 @@ end
|
|
399
616
|
|
400
617
|
The Job class **must inherit** from `MaintenanceTasks::TaskJob`.
|
401
618
|
|
402
|
-
Note that `retry_on` is not supported for custom Job
|
403
|
-
|
619
|
+
Note that `retry_on` is not supported for custom Job classes, so failed jobs
|
620
|
+
cannot be retried.
|
404
621
|
|
405
622
|
#### Customizing the rate at which task progress gets updated
|
406
623
|
|
@@ -410,6 +627,7 @@ task progress gets persisted to the database. It can be a `Numeric` value or an
|
|
410
627
|
|
411
628
|
```ruby
|
412
629
|
# config/initializers/maintenance_tasks.rb
|
630
|
+
|
413
631
|
MaintenanceTasks.ticker_delay = 2.seconds
|
414
632
|
```
|
415
633
|
|
@@ -424,6 +642,7 @@ key, as specified in your application's `config/storage.yml`:
|
|
424
642
|
|
425
643
|
```yaml
|
426
644
|
# config/storage.yml
|
645
|
+
|
427
646
|
user_data:
|
428
647
|
service: GCS
|
429
648
|
credentials: <%= Rails.root.join("path/to/user/data/keyfile.json") %>
|
@@ -439,23 +658,53 @@ internal:
|
|
439
658
|
|
440
659
|
```ruby
|
441
660
|
# config/initializers/maintenance_tasks.rb
|
661
|
+
|
442
662
|
MaintenanceTasks.active_storage_service = :internal
|
443
663
|
```
|
444
664
|
|
445
665
|
There is no need to configure this option if your application uses only one
|
446
666
|
storage service per environment.
|
447
667
|
|
668
|
+
#### Customizing the backtrace cleaner
|
669
|
+
|
670
|
+
`MaintenanceTasks.backtrace_cleaner` can be configured to specify a backtrace
|
671
|
+
cleaner to use when a Task errors and the backtrace is cleaned and persisted.
|
672
|
+
An `ActiveSupport::BacktraceCleaner` should be used.
|
673
|
+
|
674
|
+
```ruby
|
675
|
+
# config/initializers/maintenance_tasks.rb
|
676
|
+
|
677
|
+
cleaner = ActiveSupport::BacktraceCleaner.new
|
678
|
+
cleaner.add_silencer { |line| line =~ /ignore_this_dir/ }
|
679
|
+
|
680
|
+
MaintenanceTasks.backtrace_cleaner = cleaner
|
681
|
+
```
|
682
|
+
|
683
|
+
If none is specified, the default `Rails.backtrace_cleaner` will be used to
|
684
|
+
clean backtraces.
|
685
|
+
|
448
686
|
## Upgrading
|
449
687
|
|
450
688
|
Use bundler to check for and upgrade to newer versions. After installing a new
|
451
689
|
version, re-run the install command:
|
452
690
|
|
453
691
|
```bash
|
454
|
-
$ rails generate maintenance_tasks:install
|
692
|
+
$ bin/rails generate maintenance_tasks:install
|
455
693
|
```
|
456
694
|
|
457
695
|
This ensures that new migrations are installed and run as well.
|
458
696
|
|
697
|
+
**What if I've deleted my previous Maintenance Task migrations?**
|
698
|
+
|
699
|
+
The install command will attempt to reinstall these old migrations and migrating
|
700
|
+
the database will cause problems. Use `bin/rails
|
701
|
+
maintenance_tasks:install:migrations` to copy the gem's migrations to your
|
702
|
+
`db/migrate` folder. Check the release notes to see if any new migrations were
|
703
|
+
added since your last gem upgrade. Ensure that these are kept, but remove any
|
704
|
+
migrations that already ran.
|
705
|
+
|
706
|
+
Run the migrations using `bin/rails db:migrate`.
|
707
|
+
|
459
708
|
## Contributing
|
460
709
|
|
461
710
|
Would you like to report an issue or contribute with code? We accept issues and
|
@@ -464,12 +713,6 @@ pull requests. You can find the contribution guidelines on
|
|
464
713
|
|
465
714
|
[contributing]: https://github.com/Shopify/maintenance_tasks/blob/main/.github/CONTRIBUTING.md
|
466
715
|
|
467
|
-
### Dependabot updates
|
468
|
-
|
469
|
-
Whenever Dependabot creates a PR for a gem bump, check out the branch locally
|
470
|
-
and run `bin/update-gemfile <gem>` to ensure all the gemfiles have the gem
|
471
|
-
updated consistently.
|
472
|
-
|
473
716
|
## Releasing new versions
|
474
717
|
|
475
718
|
Updates should be added to the latest draft release on GitHub as Pull Requests
|
@@ -478,7 +721,7 @@ are merged.
|
|
478
721
|
Once a release is ready, follow these steps:
|
479
722
|
|
480
723
|
* Update `spec.version` in `maintenance_tasks.gemspec`.
|
481
|
-
* Run `
|
724
|
+
* Run `bundle install` to bump the `Gemfile.lock` version of the gem.
|
482
725
|
* Open a PR and merge on approval.
|
483
726
|
* Deploy via [Shipit][shipit] and see the new version on
|
484
727
|
<https://rubygems.org/gems/maintenance_tasks>.
|
@@ -24,14 +24,19 @@ module MaintenanceTasks
|
|
24
24
|
end
|
25
25
|
|
26
26
|
# Runs a given Task and redirects to the Task page.
|
27
|
-
def run
|
27
|
+
def run(&block)
|
28
28
|
task = Runner.run(
|
29
29
|
name: params.fetch(:id),
|
30
|
-
csv_file: params[:csv_file]
|
30
|
+
csv_file: params[:csv_file],
|
31
|
+
arguments: params.fetch(:task_arguments, {}).permit!.to_h,
|
32
|
+
&block
|
31
33
|
)
|
32
34
|
redirect_to(task_path(task))
|
33
35
|
rescue ActiveRecord::RecordInvalid => error
|
34
36
|
redirect_to(task_path(error.record.task_name), alert: error.message)
|
37
|
+
rescue ActiveRecord::ValueTooLong => error
|
38
|
+
task_name = params.fetch(:id)
|
39
|
+
redirect_to(task_path(task_name), alert: error.message)
|
35
40
|
rescue Runner::EnqueuingError => error
|
36
41
|
redirect_to(task_path(error.run.task_name), alert: error.message)
|
37
42
|
end
|
@@ -63,20 +63,6 @@ module MaintenanceTasks
|
|
63
63
|
tag.span(status.capitalize, class: ["tag"] + STATUS_COLOURS.fetch(status))
|
64
64
|
end
|
65
65
|
|
66
|
-
# Returns the distance between now and the Run's expected completion time,
|
67
|
-
# if the Run has an estimated_completion_time.
|
68
|
-
#
|
69
|
-
# @param run [MaintenanceTasks::Run] the Run for which the estimated time to
|
70
|
-
# completion is being calculated.
|
71
|
-
# return [String, nil] the distance in words, or nil if the Run has no
|
72
|
-
# estimated completion time.
|
73
|
-
def estimated_time_to_completion(run)
|
74
|
-
estimated_completion_time = run.estimated_completion_time
|
75
|
-
if estimated_completion_time.present?
|
76
|
-
time_ago_in_words(estimated_completion_time)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
66
|
# Reports the approximate elapsed time a Run has been processed so far based
|
81
67
|
# on the Run's time running attribute.
|
82
68
|
#
|
@@ -114,5 +100,24 @@ module MaintenanceTasks
|
|
114
100
|
only_path: true
|
115
101
|
)
|
116
102
|
end
|
103
|
+
|
104
|
+
# Return the appropriate field tag for the parameter
|
105
|
+
def parameter_field(form_builder, parameter_name)
|
106
|
+
case form_builder.object.class.attribute_types[parameter_name]
|
107
|
+
when ActiveModel::Type::Integer, ActiveModel::Type::Decimal,
|
108
|
+
ActiveModel::Type::Float
|
109
|
+
form_builder.number_field(parameter_name)
|
110
|
+
when ActiveModel::Type::DateTime
|
111
|
+
form_builder.datetime_field(parameter_name)
|
112
|
+
when ActiveModel::Type::Date
|
113
|
+
form_builder.date_field(parameter_name)
|
114
|
+
when ActiveModel::Type::Time
|
115
|
+
form_builder.time_field(parameter_name)
|
116
|
+
when ActiveModel::Type::Boolean
|
117
|
+
form_builder.check_box(parameter_name)
|
118
|
+
else
|
119
|
+
form_builder.text_area(parameter_name, class: "textarea")
|
120
|
+
end
|
121
|
+
end
|
117
122
|
end
|
118
123
|
end
|