maintenance_tasks 2.5.1 → 2.6.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: 8952908cba45fd0a79cfcf0d334e89eab4e3a554ba4c0b1acffb318c7e92730f
4
- data.tar.gz: bc1312f6269c51fe80480f7af90281c89c36bc9c78f052f65f4b5853effa5a41
3
+ metadata.gz: 2dca6b60a4a6d6366eaf05b43d1e7d1244c59baef10fe9d25561cab727a70638
4
+ data.tar.gz: c9827e001e131747ea2cb20301ef9749f394bd603a1d657afac0024e1feefc79
5
5
  SHA512:
6
- metadata.gz: c32f23abae224617aef9148ac7e890a95fb799997e0900b6a5ad72fbb988cadb25ef61ef6a35e803f2483cce93f6ad20d379c202f2a513f72c339d553e830a12
7
- data.tar.gz: cead5f36f1038614e23da3c6fa3661be58d00ac326a859681a3740d818977f0c3c311860f567d96701dcd04bc1019e001ad0903c20833444b2a0669dd9aac6e9
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](#processing-batch-collections) and use
14
- [throttling](#throttling) to control the load on your database.
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](https://github.com/shopify/job-iteration) and/or [custom
49
- rails_admin UIs][rails-admin-engines] instead of the Maintenance Tasks gem.
50
- Maintenance tasks should be ephemeral, to suit their intentionally limited UI.
51
- They should not repeat.
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](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html).
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
- `[item, cursor]`. Maintenance Tasks takes care of persisting the current
310
- cursor position and will provide it as the `cursor` argument if your task is
311
- interrupted or resumed. The `cursor` is stored as a `String`, so your custom
312
- enumerator should handle serializing/deserializing the value if required.
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 been updated,
932
- you can configure `MaintenanceTasks.stuck_task_duration`. This duration should account for
933
- job infrastructure events that may prevent the maintenance tasks job from being executed and cancelling the task.
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 `ActiveSupport::Duration`.
936
- If no value is specified, it will default to 5 minutes.
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
- enum status: STATUSES.to_h { |status| [status, status.to_s] }
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 enumeration builder. You may override this method to return any
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
- collection = self.collection
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.5.1
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-09 00:00:00.000000000 Z
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.5.1
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: []