maintenance_tasks 2.7.1 → 2.9.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 +50 -7
- data/app/helpers/maintenance_tasks/tasks_helper.rb +29 -5
- data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +4 -3
- data/app/models/maintenance_tasks/run.rb +13 -33
- data/app/models/maintenance_tasks/task.rb +14 -0
- data/app/views/maintenance_tasks/tasks/show.html.erb +2 -2
- data/lib/generators/maintenance_tasks/install_generator.rb +1 -1
- data/lib/patches/active_record_batch_enumerator.rb +17 -16
- metadata +17 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f05710c874610ef0698738773b70923d2e6a4ec4ae4b452ac768b6bf2e696d2c
|
4
|
+
data.tar.gz: 2ab31dae65aa383d5edbccee747199960f5167cd33833230c044fd00f0ad6cfc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f0f7eee33cc6615f658f49ec9d74322f7952f2afa47b4b539dd0a49131cea10df15f072402d51d611bbfbf4d8d297ace54da1896ab457e54280bc17284641c9
|
7
|
+
data.tar.gz: af3992b2c6de8c8b70d840d8c06d6a8cb2646063a9b23222a1814bfc1cc7866a9745277aafe32ef8ee48627b7916e51c672c9321be87a49a5efd443f353722fc
|
data/README.md
CHANGED
@@ -156,6 +156,32 @@ module Maintenance
|
|
156
156
|
end
|
157
157
|
```
|
158
158
|
|
159
|
+
#### Customizing the Batch Size
|
160
|
+
|
161
|
+
When processing records from an Active Record Relation, records are fetched in
|
162
|
+
batches internally, and then each record is passed to the `#process` method.
|
163
|
+
Maintenance Tasks will query the database to fetch records in batches of 100 by
|
164
|
+
default, but the batch size can be modified using the `collection_batch_size` macro:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
# app/tasks/maintenance/update_posts_task.rb
|
168
|
+
|
169
|
+
module Maintenance
|
170
|
+
class UpdatePostsTask < MaintenanceTasks::Task
|
171
|
+
# Fetch records in batches of 1000
|
172
|
+
collection_batch_size(1000)
|
173
|
+
|
174
|
+
def collection
|
175
|
+
Post.all
|
176
|
+
end
|
177
|
+
|
178
|
+
def process(post)
|
179
|
+
post.update!(content: "New content!")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
159
185
|
### Creating a CSV Task
|
160
186
|
|
161
187
|
You can also write a Task that iterates on a CSV file. Note that writing CSV
|
@@ -459,6 +485,23 @@ to run. Since arguments are specified in the user interface via text area
|
|
459
485
|
inputs, it’s important to check that they conform to the format your Task
|
460
486
|
expects, and to sanitize any inputs if necessary.
|
461
487
|
|
488
|
+
#### Validating Task Parameters
|
489
|
+
|
490
|
+
Task attributes can be validated using Active Model Validations. Attributes are
|
491
|
+
validated before a Task is enqueued.
|
492
|
+
|
493
|
+
If an attribute uses an inclusion validator with a supported `in:` option, the
|
494
|
+
set of values will be used to populate a dropdown in the user interface. The
|
495
|
+
following types are supported:
|
496
|
+
|
497
|
+
* Arrays
|
498
|
+
* Procs and lambdas that optionally accept the Task instance, and return an Array.
|
499
|
+
* Callable objects that receive one argument, the Task instance, and return an Array.
|
500
|
+
* Methods that return an Array, called on the Task instance.
|
501
|
+
|
502
|
+
For enumerables that don't match the supported types, a text field will be
|
503
|
+
rendered instead.
|
504
|
+
|
462
505
|
### Custom cursor columns to improve performance
|
463
506
|
|
464
507
|
The [job-iteration gem][job-iteration], on which this gem depends, adds an
|
@@ -515,8 +558,8 @@ your application.
|
|
515
558
|
|
516
559
|
Usage example:
|
517
560
|
|
518
|
-
|
519
|
-
|
561
|
+
```ruby
|
562
|
+
ActiveSupport::Notifications.subscribe("succeeded.maintenance_tasks") do |*, payload|
|
520
563
|
task_name = payload[:task_name]
|
521
564
|
arguments = payload[:arguments]
|
522
565
|
metadata = payload[:metadata]
|
@@ -727,9 +770,9 @@ end
|
|
727
770
|
### Writing tests for a Task that uses a custom enumerator
|
728
771
|
|
729
772
|
Tests for tasks that use custom enumerators need to instantiate the task class
|
730
|
-
in order to call `#
|
731
|
-
that `#
|
732
|
-
as expected.
|
773
|
+
in order to call `#enumerator_builder`. Once the task instance is set up,
|
774
|
+
validate that `#enumerator_builder` returns an enumerator yielding pairs of
|
775
|
+
`[item, cursor]` as expected.
|
733
776
|
|
734
777
|
```ruby
|
735
778
|
# test/tasks/maintenance/custom_enumerating_task.rb
|
@@ -742,8 +785,8 @@ module Maintenance
|
|
742
785
|
@task = CustomEnumeratingTask.new
|
743
786
|
end
|
744
787
|
|
745
|
-
test "#
|
746
|
-
enum = @task.
|
788
|
+
test "#enumerator_builder returns enumerator yielding pairs of [item, cursor]" do
|
789
|
+
enum = @task.enumerator_builder(cursor: 0)
|
747
790
|
expected_items = [:b, :c]
|
748
791
|
|
749
792
|
assert_equal 2, enum.size
|
@@ -102,8 +102,13 @@ module MaintenanceTasks
|
|
102
102
|
end
|
103
103
|
|
104
104
|
# Resolves values covered by the inclusion validator for a task attribute.
|
105
|
-
#
|
106
|
-
#
|
105
|
+
# Supported option types:
|
106
|
+
# - Arrays
|
107
|
+
# - Procs and lambdas that optionally accept the Task instance, and return an Array.
|
108
|
+
# - Callable objects that receive one argument, the Task instance, and return an Array.
|
109
|
+
# - Methods that return an Array, called on the Task instance.
|
110
|
+
#
|
111
|
+
# Other types are not supported and will return nil.
|
107
112
|
#
|
108
113
|
# Returned values are used to populate a dropdown list of options.
|
109
114
|
#
|
@@ -111,20 +116,39 @@ module MaintenanceTasks
|
|
111
116
|
# @param parameter_name [String] The parameter name.
|
112
117
|
#
|
113
118
|
# @return [Array] value of the resolved inclusion option.
|
114
|
-
def resolve_inclusion_value(
|
119
|
+
def resolve_inclusion_value(task, parameter_name)
|
120
|
+
task_class = task.class
|
115
121
|
inclusion_validator = task_class.validators_on(parameter_name).find do |validator|
|
116
122
|
validator.kind == :inclusion
|
117
123
|
end
|
118
124
|
return unless inclusion_validator
|
119
125
|
|
120
126
|
in_option = inclusion_validator.options[:in] || inclusion_validator.options[:within]
|
121
|
-
|
127
|
+
resolved_in_option = case in_option
|
128
|
+
when Proc
|
129
|
+
if in_option.arity == 0
|
130
|
+
in_option.call
|
131
|
+
else
|
132
|
+
in_option.call(task)
|
133
|
+
end
|
134
|
+
when Symbol
|
135
|
+
method = task.method(in_option)
|
136
|
+
method.call if method.arity.zero?
|
137
|
+
else
|
138
|
+
if in_option.respond_to?(:call)
|
139
|
+
in_option.call(task)
|
140
|
+
else
|
141
|
+
in_option
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
resolved_in_option if resolved_in_option.is_a?(Array)
|
122
146
|
end
|
123
147
|
|
124
148
|
# Return the appropriate field tag for the parameter, based on its type.
|
125
149
|
# If the parameter has a `validates_inclusion_of` validator, return a dropdown list of options instead.
|
126
150
|
def parameter_field(form_builder, parameter_name)
|
127
|
-
inclusion_values = resolve_inclusion_value(form_builder.object
|
151
|
+
inclusion_values = resolve_inclusion_value(form_builder.object, parameter_name)
|
128
152
|
return form_builder.select(parameter_name, inclusion_values, prompt: "Select a value") if inclusion_values
|
129
153
|
|
130
154
|
case form_builder.object.class.attribute_types[parameter_name]
|
@@ -39,7 +39,9 @@ module MaintenanceTasks
|
|
39
39
|
when :no_collection
|
40
40
|
enumerator_builder.build_once_enumerator(cursor: nil)
|
41
41
|
when ActiveRecord::Relation
|
42
|
-
|
42
|
+
options = { cursor: cursor, columns: @task.cursor_columns }
|
43
|
+
options[:batch_size] = @task.active_record_enumerator_batch_size if @task.active_record_enumerator_batch_size
|
44
|
+
enumerator_builder.active_record_on_records(collection, **options)
|
43
45
|
when ActiveRecord::Batches::BatchEnumerator
|
44
46
|
if collection.start || collection.finish
|
45
47
|
raise ArgumentError, <<~MSG.squish
|
@@ -164,6 +166,7 @@ module MaintenanceTasks
|
|
164
166
|
end
|
165
167
|
|
166
168
|
def on_error(error)
|
169
|
+
task_context = {}
|
167
170
|
@ticker.persist if defined?(@ticker)
|
168
171
|
|
169
172
|
if defined?(@run)
|
@@ -175,8 +178,6 @@ module MaintenanceTasks
|
|
175
178
|
started_at: @run.started_at,
|
176
179
|
ended_at: @run.ended_at,
|
177
180
|
}
|
178
|
-
else
|
179
|
-
task_context = {}
|
180
181
|
end
|
181
182
|
errored_element = @errored_element if defined?(@errored_element)
|
182
183
|
ensure
|
@@ -386,40 +386,20 @@ module MaintenanceTasks
|
|
386
386
|
nil
|
387
387
|
end
|
388
388
|
|
389
|
-
#
|
390
|
-
#
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
:arguments,
|
401
|
-
"are invalid: #{error_messages.join("; ")}",
|
402
|
-
)
|
403
|
-
end
|
404
|
-
rescue Task::NotFoundError
|
405
|
-
nil
|
406
|
-
end
|
407
|
-
else
|
408
|
-
# Performs validation on the arguments to use for the Task. If the Task is
|
409
|
-
# invalid, the errors are added to the Run.
|
410
|
-
def validate_task_arguments
|
411
|
-
arguments_match_task_attributes if arguments.present?
|
412
|
-
if task.invalid?
|
413
|
-
error_messages = task.errors
|
414
|
-
.map { |error| "#{error.attribute.inspect} #{error.message}" }
|
415
|
-
errors.add(
|
416
|
-
:arguments,
|
417
|
-
"are invalid: #{error_messages.join("; ")}",
|
418
|
-
)
|
419
|
-
end
|
420
|
-
rescue Task::NotFoundError
|
421
|
-
nil
|
389
|
+
# Performs validation on the arguments to use for the Task. If the Task is
|
390
|
+
# invalid, the errors are added to the Run.
|
391
|
+
def validate_task_arguments
|
392
|
+
arguments_match_task_attributes if arguments.present?
|
393
|
+
if task.invalid?
|
394
|
+
error_messages = task.errors
|
395
|
+
.map { |error| "#{error.attribute.inspect} #{error.message}" }
|
396
|
+
errors.add(
|
397
|
+
:arguments,
|
398
|
+
"are invalid: #{error_messages.join("; ")}",
|
399
|
+
)
|
422
400
|
end
|
401
|
+
rescue Task::NotFoundError
|
402
|
+
nil
|
423
403
|
end
|
424
404
|
|
425
405
|
# Fetches the attached ActiveStorage CSV file for the run. Checks first
|
@@ -18,6 +18,12 @@ module MaintenanceTasks
|
|
18
18
|
# @api private
|
19
19
|
class_attribute :throttle_conditions, default: []
|
20
20
|
|
21
|
+
# The number of active records to fetch in a single query when iterating
|
22
|
+
# over an Active Record collection task.
|
23
|
+
#
|
24
|
+
# @api private
|
25
|
+
class_attribute :active_record_enumerator_batch_size
|
26
|
+
|
21
27
|
# @api private
|
22
28
|
class_attribute :collection_builder_strategy, default: NullCollectionBuilder.new
|
23
29
|
|
@@ -138,6 +144,14 @@ module MaintenanceTasks
|
|
138
144
|
self.throttle_conditions += [{ throttle_on: condition, backoff: backoff_as_proc }]
|
139
145
|
end
|
140
146
|
|
147
|
+
# Limit the number of records that will be fetched in a single query when
|
148
|
+
# iterating over an Active Record collection task.
|
149
|
+
#
|
150
|
+
# @param size [Integer] the number of records to fetch in a single query.
|
151
|
+
def collection_batch_size(size)
|
152
|
+
self.active_record_enumerator_batch_size = size
|
153
|
+
end
|
154
|
+
|
141
155
|
# Initialize a callback to run after the task starts.
|
142
156
|
#
|
143
157
|
# @param filter_list apply filters to the callback
|
@@ -44,7 +44,7 @@
|
|
44
44
|
|
45
45
|
<h4 class="title is-4">Active Runs</h4>
|
46
46
|
|
47
|
-
<%= render @task.active_runs %>
|
47
|
+
<%= render partial: "maintenance_tasks/runs/run", collection: @task.active_runs %>
|
48
48
|
<% end %>
|
49
49
|
|
50
50
|
<% if @runs_page.records.present? %>
|
@@ -52,7 +52,7 @@
|
|
52
52
|
|
53
53
|
<h4 class="title is-4">Previous Runs</h4>
|
54
54
|
|
55
|
-
<%= render @runs_page.records %>
|
55
|
+
<%= render partial: "maintenance_tasks/runs/run", collection: @runs_page.records %>
|
56
56
|
|
57
57
|
<%= link_to "Next page", task_path(@task, cursor: @runs_page.next_cursor) unless @runs_page.last? %>
|
58
58
|
<% end %>
|
@@ -10,7 +10,7 @@ module MaintenanceTasks
|
|
10
10
|
|
11
11
|
# Mounts the engine in the host application's config/routes.rb
|
12
12
|
def mount_engine
|
13
|
-
route("mount MaintenanceTasks::Engine
|
13
|
+
route("mount MaintenanceTasks::Engine, at: \"/maintenance_tasks\"")
|
14
14
|
end
|
15
15
|
|
16
16
|
# Copies engine migrations to host application and migrates the database
|
@@ -1,23 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
#
|
5
|
-
module ActiveRecordBatchEnumerator
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
if Rails.gem_version < Gem::Version.new("7.0")
|
4
|
+
# Add attribute readers.
|
5
|
+
module ActiveRecordBatchEnumerator
|
6
|
+
# The primary key value from which the BatchEnumerator starts,
|
7
|
+
# inclusive of the value.
|
8
|
+
attr_reader :start
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
# The primary key value at which the BatchEnumerator ends,
|
11
|
+
# inclusive of the value.
|
12
|
+
attr_reader :finish
|
13
13
|
|
14
|
-
|
15
|
-
|
14
|
+
# The relation from which the BatchEnumerator yields batches.
|
15
|
+
attr_reader :relation
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
# The size of the batches yielded by the BatchEnumerator.
|
18
|
+
def batch_size
|
19
|
+
@of
|
20
|
+
end
|
20
21
|
end
|
21
|
-
end
|
22
22
|
|
23
|
-
ActiveRecord::Batches::BatchEnumerator.include(ActiveRecordBatchEnumerator)
|
23
|
+
ActiveRecord::Batches::BatchEnumerator.include(ActiveRecordBatchEnumerator)
|
24
|
+
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.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify Engineering
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-11-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -16,42 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '6.
|
19
|
+
version: '6.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '6.
|
26
|
+
version: '6.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activejob
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '6.
|
33
|
+
version: '6.1'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '6.
|
40
|
+
version: '6.1'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: activerecord
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '6.
|
47
|
+
version: '6.1'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '6.
|
54
|
+
version: '6.1'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: csv
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,14 +86,14 @@ dependencies:
|
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '6.
|
89
|
+
version: '6.1'
|
90
90
|
type: :runtime
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '6.
|
96
|
+
version: '6.1'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: zeitwerk
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,7 +108,7 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: 2.6.2
|
111
|
-
description:
|
111
|
+
description:
|
112
112
|
email: gems@shopify.com
|
113
113
|
executables:
|
114
114
|
- maintenance_tasks
|
@@ -185,9 +185,9 @@ homepage: https://github.com/Shopify/maintenance_tasks
|
|
185
185
|
licenses:
|
186
186
|
- MIT
|
187
187
|
metadata:
|
188
|
-
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.
|
188
|
+
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.9.0
|
189
189
|
allowed_push_host: https://rubygems.org
|
190
|
-
post_install_message:
|
190
|
+
post_install_message:
|
191
191
|
rdoc_options: []
|
192
192
|
require_paths:
|
193
193
|
- lib
|
@@ -195,15 +195,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
195
195
|
requirements:
|
196
196
|
- - ">="
|
197
197
|
- !ruby/object:Gem::Version
|
198
|
-
version: '3.
|
198
|
+
version: '3.1'
|
199
199
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
200
200
|
requirements:
|
201
201
|
- - ">="
|
202
202
|
- !ruby/object:Gem::Version
|
203
203
|
version: '0'
|
204
204
|
requirements: []
|
205
|
-
rubygems_version: 3.5.
|
206
|
-
signing_key:
|
205
|
+
rubygems_version: 3.5.23
|
206
|
+
signing_key:
|
207
207
|
specification_version: 4
|
208
208
|
summary: A Rails engine for queuing and managing maintenance tasks
|
209
209
|
test_files: []
|