maintenance_tasks 2.5.1 → 2.6.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 +99 -21
- data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +39 -0
- data/app/models/maintenance_tasks/run.rb +5 -1
- data/app/models/maintenance_tasks/task.rb +14 -41
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2dca6b60a4a6d6366eaf05b43d1e7d1244c59baef10fe9d25561cab727a70638
|
4
|
+
data.tar.gz: c9827e001e131747ea2cb20301ef9749f394bd603a1d657afac0024e1feefc79
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 269fe1ab563047cf368cb552b91305c0a99b25affa6bea95553f55541c6e5a947726e441b3487181146d90d75bf9e9e1e853fc3302fda483ce0d00032ad7bceb
|
7
|
+
data.tar.gz: 7b1ae78d3049cb3c28c17bcf068acc735441e4b4a3de6babeb8107294408a1ee139c03b6bad12e1004f0bef59c834314429079df0a11c6384148f007bc3a44a9
|
data/README.md
CHANGED
@@ -10,8 +10,8 @@ engine helps with the second part of this process, backfilling.
|
|
10
10
|
|
11
11
|
Maintenance tasks are collection-based tasks, usually using Active Record, that
|
12
12
|
update the data in your database. They can be paused or interrupted. Maintenance
|
13
|
-
tasks can operate [in batches]
|
14
|
-
[throttling]
|
13
|
+
tasks can operate [in batches][#processing-batch-collections] and use
|
14
|
+
[throttling][#throttling] to control the load on your database.
|
15
15
|
|
16
16
|
Maintenance tasks aren't meant to happen on a regular basis. They're used as
|
17
17
|
needed, or as one-offs. Normally maintenance tasks are ephemeral, so they are
|
@@ -45,10 +45,12 @@ If your task updates your database schema instead of data, use a migration
|
|
45
45
|
instead of a maintenance task.
|
46
46
|
|
47
47
|
If your task happens regularly, consider Active Jobs with a scheduler or cron,
|
48
|
-
[job-iteration jobs]
|
49
|
-
|
50
|
-
|
51
|
-
|
48
|
+
[job-iteration jobs][job-iteration] and/or [custom rails_admin
|
49
|
+
UIs][rails-admin-engines] instead of the Maintenance Tasks gem. Maintenance
|
50
|
+
tasks should be ephemeral, to suit their intentionally limited UI. They should
|
51
|
+
not repeat.
|
52
|
+
|
53
|
+
[job-iteration]: https://github.com/shopify/job-iteration
|
52
54
|
|
53
55
|
To create seed data for a new application, use the provided Rails `db/seeds.rb`
|
54
56
|
file instead.
|
@@ -91,10 +93,12 @@ take a look at the [Active Job documentation][active-job-docs].
|
|
91
93
|
### Autoloading
|
92
94
|
|
93
95
|
The Maintenance Tasks framework does not support autoloading in `:classic` mode.
|
94
|
-
Please ensure your application is using
|
95
|
-
[Zeitwerk](https://github.com/fxn/zeitwerk) to load your code. For more
|
96
|
+
Please ensure your application is using [Zeitwerk][] to load your code. For more
|
96
97
|
information, please consult the [Rails guides on autoloading and reloading
|
97
|
-
constants]
|
98
|
+
constants][autoloading].
|
99
|
+
|
100
|
+
[Zeitwerk]: https://github.com/fxn/zeitwerk
|
101
|
+
[autoloading]: https://guides.rubyonrails.org/autoloading_and_reloading_constants.html
|
98
102
|
|
99
103
|
## Usage
|
100
104
|
|
@@ -305,11 +309,11 @@ If you have a special use case requiring iteration over an unsupported
|
|
305
309
|
collection type, such as external resources fetched from some API, you can
|
306
310
|
implement the `enumerator_builder(cursor:)` method in your task.
|
307
311
|
|
308
|
-
This method should return an `Enumerator`, yielding pairs of
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
312
|
+
This method should return an `Enumerator`, yielding pairs of `[item, cursor]`.
|
313
|
+
Maintenance Tasks takes care of persisting the current cursor position and will
|
314
|
+
provide it as the `cursor` argument if your task is interrupted or resumed. The
|
315
|
+
`cursor` is stored as a `String`, so your custom enumerator should handle
|
316
|
+
serializing/deserializing the value if required.
|
313
317
|
|
314
318
|
```ruby
|
315
319
|
# app/tasks/maintenance/custom_enumerator_task.rb
|
@@ -377,7 +381,7 @@ module Maintenance
|
|
377
381
|
throttle_on(backoff: -> { RandomBackoffGenerator.generate_duration } ) do
|
378
382
|
DatabaseStatus.unhealthy?
|
379
383
|
end
|
380
|
-
...
|
384
|
+
# ...
|
381
385
|
end
|
382
386
|
end
|
383
387
|
```
|
@@ -413,6 +417,42 @@ to run. Since arguments are specified in the user interface via text area
|
|
413
417
|
inputs, it’s important to check that they conform to the format your Task
|
414
418
|
expects, and to sanitize any inputs if necessary.
|
415
419
|
|
420
|
+
### Custom cursor columns to improve performance
|
421
|
+
|
422
|
+
The [job-iteration gem][job-iteration], on which this gem depends, adds an
|
423
|
+
`order by` clause to the relation returned by the `collection` method, in order
|
424
|
+
to iterate through records. It defaults to order on the `id` column.
|
425
|
+
|
426
|
+
The [job-iteration gem][job-iteration] supports configuring which columns are
|
427
|
+
used to order the cursor, as documented in
|
428
|
+
[`build_active_record_enumerator_on_records`][ji-ar-enumerator-doc].
|
429
|
+
|
430
|
+
[ji-ar-enumerator-doc]: https://www.rubydoc.info/gems/job-iteration/JobIteration/EnumeratorBuilder#build_active_record_enumerator_on_records-instance_method
|
431
|
+
|
432
|
+
The `maintenance-tasks` gem exposes the ability that `job-iteration` provides to
|
433
|
+
control the cursor columns, through the `cursor_columns` method in the
|
434
|
+
`MaintenanceTasks::Task` class. If the `cursor_columns` method returns `nil`,
|
435
|
+
the query is ordered by the primary key. If cursor columns values change during
|
436
|
+
an iteration, records may be skipped or yielded multiple times.
|
437
|
+
|
438
|
+
```ruby
|
439
|
+
module Maintenance
|
440
|
+
class UpdatePostsTask < MaintenanceTasks::Task
|
441
|
+
def cursor_columns
|
442
|
+
[:created_at, :id]
|
443
|
+
end
|
444
|
+
|
445
|
+
def collection
|
446
|
+
Post.where(created_at: 2.days.ago...1.hour.ago)
|
447
|
+
end
|
448
|
+
|
449
|
+
def process(post)
|
450
|
+
post.update!(content: "updated content")
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
```
|
455
|
+
|
416
456
|
### Using Task Callbacks
|
417
457
|
|
418
458
|
The Task provides callbacks that hook into its life cycle.
|
@@ -595,6 +635,42 @@ module Maintenance
|
|
595
635
|
end
|
596
636
|
```
|
597
637
|
|
638
|
+
### Writing tests for a Task that uses a custom enumerator
|
639
|
+
|
640
|
+
Tests for tasks that use custom enumerators need to instantiate the task class
|
641
|
+
in order to call `#build_enumerator`. Once the task instance is set up, validate
|
642
|
+
that `#build_enumerator` returns an enumerator yielding pairs of [item, cursor]
|
643
|
+
as expected.
|
644
|
+
|
645
|
+
```ruby
|
646
|
+
# test/tasks/maintenance/custom_enumerating_task.rb
|
647
|
+
|
648
|
+
require "test_helper"
|
649
|
+
|
650
|
+
module Maintenance
|
651
|
+
class CustomEnumeratingTaskTest < ActiveSupport::TestCase
|
652
|
+
setup do
|
653
|
+
@task = CustomEnumeratingTask.new
|
654
|
+
end
|
655
|
+
|
656
|
+
test "#build_enumerator returns enumerator yielding pairs of [item, cursor]" do
|
657
|
+
enum = @task.build_enumerator(cursor: 0)
|
658
|
+
expected_items = [:b, :c]
|
659
|
+
|
660
|
+
assert_equal 2, enum.size
|
661
|
+
|
662
|
+
enum.each_with_index do |item, cursor|
|
663
|
+
assert_equal expected_items[cursor], item
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
test "#process performs a task iteration" do
|
668
|
+
# ...
|
669
|
+
end
|
670
|
+
end
|
671
|
+
end
|
672
|
+
```
|
673
|
+
|
598
674
|
### Running a Task
|
599
675
|
|
600
676
|
#### Running a Task from the Web UI
|
@@ -917,7 +993,7 @@ MaintenanceTasks.parent_controller = "Services::CustomController"
|
|
917
993
|
class Services::CustomController < ActionController::Base
|
918
994
|
include CustomSecurityThings
|
919
995
|
include CustomLoggingThings
|
920
|
-
...
|
996
|
+
# ...
|
921
997
|
end
|
922
998
|
```
|
923
999
|
|
@@ -928,12 +1004,14 @@ If no value is specified, it will default to `"ActionController::Base"`.
|
|
928
1004
|
|
929
1005
|
#### Configure time after which the task will be considered stuck
|
930
1006
|
|
931
|
-
To specify a time duration after which a task is considered stuck if it has not
|
932
|
-
you can configure `MaintenanceTasks.stuck_task_duration`. This
|
933
|
-
job infrastructure events that may prevent the
|
1007
|
+
To specify a time duration after which a task is considered stuck if it has not
|
1008
|
+
been updated, you can configure `MaintenanceTasks.stuck_task_duration`. This
|
1009
|
+
duration should account for job infrastructure events that may prevent the
|
1010
|
+
maintenance tasks job from being executed and cancelling the task.
|
934
1011
|
|
935
|
-
The value for `MaintenanceTasks.stuck_task_duration` must be an
|
936
|
-
If no value is specified, it will default to 5
|
1012
|
+
The value for `MaintenanceTasks.stuck_task_duration` must be an
|
1013
|
+
`ActiveSupport::Duration`. If no value is specified, it will default to 5
|
1014
|
+
minutes.
|
937
1015
|
|
938
1016
|
### Metadata
|
939
1017
|
|
@@ -33,6 +33,45 @@ module MaintenanceTasks
|
|
33
33
|
def build_enumerator(_run, cursor:)
|
34
34
|
cursor ||= @run.cursor
|
35
35
|
@collection_enum = @task.enumerator_builder(cursor: cursor)
|
36
|
+
|
37
|
+
@collection_enum ||= case (collection = @task.collection)
|
38
|
+
when :no_collection
|
39
|
+
enumerator_builder.build_once_enumerator(cursor: nil)
|
40
|
+
when ActiveRecord::Relation
|
41
|
+
enumerator_builder.active_record_on_records(collection, cursor: cursor, columns: @task.cursor_columns)
|
42
|
+
when ActiveRecord::Batches::BatchEnumerator
|
43
|
+
if collection.start || collection.finish
|
44
|
+
raise ArgumentError, <<~MSG.squish
|
45
|
+
#{@task.class.name}#collection cannot support
|
46
|
+
a batch enumerator with the "start" or "finish" options.
|
47
|
+
MSG
|
48
|
+
end
|
49
|
+
|
50
|
+
# For now, only support automatic count based on the enumerator for
|
51
|
+
# batches
|
52
|
+
enumerator_builder.active_record_on_batch_relations(
|
53
|
+
collection.relation,
|
54
|
+
cursor: cursor,
|
55
|
+
batch_size: collection.batch_size,
|
56
|
+
columns: @task.cursor_columns,
|
57
|
+
)
|
58
|
+
when Array
|
59
|
+
enumerator_builder.build_array_enumerator(collection, cursor: cursor&.to_i)
|
60
|
+
when BatchCsvCollectionBuilder::BatchCsv
|
61
|
+
JobIteration::CsvEnumerator.new(collection.csv).batches(
|
62
|
+
batch_size: collection.batch_size,
|
63
|
+
cursor: cursor&.to_i,
|
64
|
+
)
|
65
|
+
when CSV
|
66
|
+
JobIteration::CsvEnumerator.new(collection).rows(cursor: cursor&.to_i)
|
67
|
+
else
|
68
|
+
raise ArgumentError, <<~MSG.squish
|
69
|
+
#{@task.class.name}#collection must be either an
|
70
|
+
Active Record Relation, ActiveRecord::Batches::BatchEnumerator,
|
71
|
+
Array, or CSV.
|
72
|
+
MSG
|
73
|
+
end
|
74
|
+
|
36
75
|
throttle_enumerator(@collection_enum)
|
37
76
|
end
|
38
77
|
|
@@ -33,7 +33,11 @@ module MaintenanceTasks
|
|
33
33
|
]
|
34
34
|
COMPLETED_STATUSES = [:succeeded, :errored, :cancelled]
|
35
35
|
|
36
|
-
|
36
|
+
if Rails.gem_version >= Gem::Version.new("7.0.alpha")
|
37
|
+
enum :status, STATUSES.to_h { |status| [status, status.to_s] }
|
38
|
+
else
|
39
|
+
enum status: STATUSES.to_h { |status| [status, status.to_s] }
|
40
|
+
end
|
37
41
|
|
38
42
|
validate :task_name_belongs_to_a_valid_task, on: :create
|
39
43
|
validate :csv_attachment_presence, on: :create
|
@@ -230,6 +230,18 @@ module MaintenanceTasks
|
|
230
230
|
self.class.collection_builder_strategy.collection(self)
|
231
231
|
end
|
232
232
|
|
233
|
+
# The columns used to build the `ORDER BY` clause of the query for iteration.
|
234
|
+
#
|
235
|
+
# If cursor_columns returns nil, the query is ordered by the primary key.
|
236
|
+
# If cursor columns values change during an iteration, records may be skipped or yielded multiple times.
|
237
|
+
# More details in the documentation of JobIteration::EnumeratorBuilder.build_active_record_enumerator_on_records:
|
238
|
+
# https://www.rubydoc.info/gems/job-iteration/JobIteration/EnumeratorBuilder#build_active_record_enumerator_on_records-instance_method
|
239
|
+
#
|
240
|
+
# @return the cursor_columns.
|
241
|
+
def cursor_columns
|
242
|
+
nil
|
243
|
+
end
|
244
|
+
|
233
245
|
# Placeholder method to raise in case a subclass fails to implement the
|
234
246
|
# expected instance method.
|
235
247
|
#
|
@@ -248,7 +260,7 @@ module MaintenanceTasks
|
|
248
260
|
self.class.collection_builder_strategy.count(self)
|
249
261
|
end
|
250
262
|
|
251
|
-
# Default
|
263
|
+
# Default enumerator builder. You may override this method to return any
|
252
264
|
# Enumerator yielding pairs of `[item, item_cursor]`.
|
253
265
|
#
|
254
266
|
# @param cursor [String, nil] cursor position to resume from, or nil on
|
@@ -256,46 +268,7 @@ module MaintenanceTasks
|
|
256
268
|
#
|
257
269
|
# @return [Enumerator]
|
258
270
|
def enumerator_builder(cursor:)
|
259
|
-
|
260
|
-
|
261
|
-
job_iteration_builder = JobIteration::EnumeratorBuilder.new(nil)
|
262
|
-
|
263
|
-
case collection
|
264
|
-
when :no_collection
|
265
|
-
job_iteration_builder.build_once_enumerator(cursor: nil)
|
266
|
-
when ActiveRecord::Relation
|
267
|
-
job_iteration_builder.active_record_on_records(collection, cursor: cursor)
|
268
|
-
when ActiveRecord::Batches::BatchEnumerator
|
269
|
-
if collection.start || collection.finish
|
270
|
-
raise ArgumentError, <<~MSG.squish
|
271
|
-
#{self.class.name}#collection cannot support
|
272
|
-
a batch enumerator with the "start" or "finish" options.
|
273
|
-
MSG
|
274
|
-
end
|
275
|
-
|
276
|
-
# For now, only support automatic count based on the enumerator for
|
277
|
-
# batches
|
278
|
-
job_iteration_builder.active_record_on_batch_relations(
|
279
|
-
collection.relation,
|
280
|
-
cursor: cursor,
|
281
|
-
batch_size: collection.batch_size,
|
282
|
-
)
|
283
|
-
when Array
|
284
|
-
job_iteration_builder.build_array_enumerator(collection, cursor: cursor&.to_i)
|
285
|
-
when BatchCsvCollectionBuilder::BatchCsv
|
286
|
-
JobIteration::CsvEnumerator.new(collection.csv).batches(
|
287
|
-
batch_size: collection.batch_size,
|
288
|
-
cursor: cursor&.to_i,
|
289
|
-
)
|
290
|
-
when CSV
|
291
|
-
JobIteration::CsvEnumerator.new(collection).rows(cursor: cursor&.to_i)
|
292
|
-
else
|
293
|
-
raise ArgumentError, <<~MSG.squish
|
294
|
-
#{self.class.name}#collection must be either an
|
295
|
-
Active Record Relation, ActiveRecord::Batches::BatchEnumerator,
|
296
|
-
Array, or CSV.
|
297
|
-
MSG
|
298
|
-
end
|
271
|
+
nil
|
299
272
|
end
|
300
273
|
end
|
301
274
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: maintenance_tasks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify Engineering
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02-
|
11
|
+
date: 2024-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -171,7 +171,7 @@ homepage: https://github.com/Shopify/maintenance_tasks
|
|
171
171
|
licenses:
|
172
172
|
- MIT
|
173
173
|
metadata:
|
174
|
-
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.
|
174
|
+
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.6.0
|
175
175
|
allowed_push_host: https://rubygems.org
|
176
176
|
post_install_message:
|
177
177
|
rdoc_options: []
|