maintenance_tasks 2.7.1 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dac65eae0e9b5f8066a994408059eb42689792fed29f36321632ff42a804a7cd
4
- data.tar.gz: a8df371985f28c8d97a2e8363f263e93fa40c2291370c63da8bfa115bad0e21a
3
+ metadata.gz: f05710c874610ef0698738773b70923d2e6a4ec4ae4b452ac768b6bf2e696d2c
4
+ data.tar.gz: 2ab31dae65aa383d5edbccee747199960f5167cd33833230c044fd00f0ad6cfc
5
5
  SHA512:
6
- metadata.gz: 5144b9f8ab67ae9ea05b16c74178a96239cf22611627822e6ca00205c145e1767bc51015f796fd95dd40b973f3e5b6b1e1e67a950c72c628995602c631e0e49a
7
- data.tar.gz: caeefe52b00431e3ba41297cc10f3dfb17c184b57237e74e6825f50b683be58431c079bf4921ac2529286a0b926fdbbe2ee6e61dc4350e3457d477ce18dc1ba1
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
- ```ruby
519
- ActiveSupport::Notifications.subscribe("succeeded.maintenance_tasks") do |*, payload|
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 `#build_enumerator`. Once the task instance is set up, validate
731
- that `#build_enumerator` returns an enumerator yielding pairs of [item, cursor]
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 "#build_enumerator returns enumerator yielding pairs of [item, cursor]" do
746
- enum = @task.build_enumerator(cursor: 0)
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
- # Only Arrays are supported, option types such as:
106
- # Procs, lambdas, symbols, and Range are not supported and return nil.
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(task_class, parameter_name)
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
- in_option if in_option.is_a?(Array)
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.class, parameter_name)
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
- enumerator_builder.active_record_on_records(collection, cursor: cursor, columns: @task.cursor_columns)
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
- # Support iterating over ActiveModel::Errors in Rails 6.0 and Rails 6.1+.
390
- # To be removed when Rails 6.0 is no longer supported.
391
- if Rails::VERSION::STRING.match?(/^6.0/)
392
- # Performs validation on the arguments to use for the Task. If the Task is
393
- # invalid, the errors are added to the Run.
394
- def validate_task_arguments
395
- arguments_match_task_attributes if arguments.present?
396
- if task.invalid?
397
- error_messages = task.errors
398
- .map { |attribute, message| "#{attribute.inspect} #{message}" }
399
- errors.add(
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 => \"/maintenance_tasks\"")
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
- # 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
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
- # The primary key value at which the BatchEnumerator ends,
11
- # inclusive of the value.
12
- attr_reader :finish
10
+ # The primary key value at which the BatchEnumerator ends,
11
+ # inclusive of the value.
12
+ attr_reader :finish
13
13
 
14
- # The relation from which the BatchEnumerator yields batches.
15
- attr_reader :relation
14
+ # The relation from which the BatchEnumerator yields batches.
15
+ attr_reader :relation
16
16
 
17
- # The size of the batches yielded by the BatchEnumerator.
18
- def batch_size
19
- @of
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.7.1
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-06-12 00:00:00.000000000 Z
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.0'
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.0'
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.0'
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.0'
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.0'
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.0'
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.0'
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.0'
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.7.1
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.0'
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.11
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: []