maintenance_tasks 2.4.0 → 2.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bca60024506654676fc6c094e6c4a4a1af4c87fc3a80c81f5a7be58a73b6061
4
- data.tar.gz: 351ba1281e24013a2cbdd43302498769d398e38f0a695850d988d1d4aa4987bb
3
+ metadata.gz: f26c6afe0c4b45b6d191162ab2c37e3b058e8b6ae473b17cfc3724e29e638fce
4
+ data.tar.gz: 4308f3356279fa5879429077cb482d91033336bfa62c1c4afb51fc9d957b1785
5
5
  SHA512:
6
- metadata.gz: c4cb3cfe2d3ddeb5f614976f6b2c04ce655e3b4acdf739ea133e5a303a224e170e3f6b9ea0aa58c32b71e72f30017524c172750dd86308a29bcd46d309cb45ed
7
- data.tar.gz: 1c990f1658545d4bd5e622598c5ce7a08f0dbdda9e1d8c9e0bd56c46d491fe5a440ce53100c976d196b7cc2535a839e16253a278d6e9e1ae1dc6a1121e25848e
6
+ metadata.gz: 383bc985f1a465e00889b20997d5718ff2039d5cdc6ca6b14a427a183fef4df3ff445eda2893d10b41d0bf243a6150ad5699afdfcbe3beaf280e4d35f130c1f5
7
+ data.tar.gz: d2b54c567a3d89e78f3538ca22b35d83dcb150e7520fc3014aa2040836f2e2741eb2bda700211c6411b8355b54ee19814655cf79827358d2d2f922fa855934af
data/README.md CHANGED
@@ -299,6 +299,35 @@ module Maintenance
299
299
  end
300
300
  ```
301
301
 
302
+ ### Tasks with Custom Enumerators
303
+
304
+ If you have a special use case requiring iteration over an unsupported
305
+ collection type, such as external resources fetched from some API, you can
306
+ implement the `enumerator_builder(cursor:)` method in your task.
307
+
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.
313
+
314
+ ```ruby
315
+ # app/tasks/maintenance/custom_enumerator_task.rb
316
+
317
+ module Maintenance
318
+ class CustomEnumeratorTask < MaintenanceTasks::Task
319
+ def enumerator_builder(cursor:)
320
+ after_id = cursor&.to_i
321
+ PostAPI.index(after_id: after_id).map { |post| [post, post.id] }.to_enum
322
+ end
323
+
324
+ def process(post)
325
+ Post.create!(post)
326
+ end
327
+ end
328
+ end
329
+ ```
330
+
302
331
  ### Throttling
303
332
 
304
333
  Maintenance tasks often modify a lot of data and can be taxing on your database.
@@ -897,6 +926,15 @@ controller class which **must inherit** from `ActionController::Base`.
897
926
 
898
927
  If no value is specified, it will default to `"ActionController::Base"`.
899
928
 
929
+ #### Configure time after which the task will be considered stuck
930
+
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.
934
+
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.
937
+
900
938
  ### Metadata
901
939
 
902
940
  `MaintenanceTasks.metadata` can be configured to specify a proc from which to
@@ -101,8 +101,17 @@ module MaintenanceTasks
101
101
  )
102
102
  end
103
103
 
104
- # Return the appropriate field tag for the parameter
104
+ # Return the appropriate field tag for the parameter, based on its type.
105
+ # If the parameter has a `validates_inclusion_of` validator, return a dropdown list of options instead.
105
106
  def parameter_field(form_builder, parameter_name)
107
+ inclusion_validator = form_builder.object.class.validators_on(parameter_name).find do |validator|
108
+ validator.kind == :inclusion
109
+ end
110
+
111
+ return form_builder.select(
112
+ parameter_name, inclusion_validator.options[:in], prompt: "Select a value"
113
+ ) if inclusion_validator
114
+
106
115
  case form_builder.object.class.attribute_types[parameter_name]
107
116
  when ActiveModel::Type::Integer
108
117
  form_builder.number_field(parameter_name)
@@ -32,45 +32,7 @@ module MaintenanceTasks
32
32
 
33
33
  def build_enumerator(_run, cursor:)
34
34
  cursor ||= @run.cursor
35
- collection = @task.collection
36
- @enumerator = nil
37
-
38
- @collection_enum = case collection
39
- when :no_collection
40
- enumerator_builder.build_once_enumerator(cursor: nil)
41
- when ActiveRecord::Relation
42
- enumerator_builder.active_record_on_records(collection, cursor: cursor)
43
- when ActiveRecord::Batches::BatchEnumerator
44
- if collection.start || collection.finish
45
- raise ArgumentError, <<~MSG.squish
46
- #{@task.class.name}#collection cannot support
47
- a batch enumerator with the "start" or "finish" options.
48
- MSG
49
- end
50
-
51
- # For now, only support automatic count based on the enumerator for
52
- # batches
53
- enumerator_builder.active_record_on_batch_relations(
54
- collection.relation,
55
- cursor: cursor,
56
- batch_size: collection.batch_size,
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
35
+ @collection_enum = @task.enumerator_builder(cursor: cursor)
74
36
  throttle_enumerator(@collection_enum)
75
37
  end
76
38
 
@@ -165,6 +127,7 @@ module MaintenanceTasks
165
127
  @ticker.persist if defined?(@ticker)
166
128
 
167
129
  if defined?(@run)
130
+ @run.cursor = cursor_position
168
131
  @run.persist_error(error)
169
132
 
170
133
  task_context = {
@@ -32,7 +32,6 @@ module MaintenanceTasks
32
32
  :cancelled,
33
33
  ]
34
34
  COMPLETED_STATUSES = [:succeeded, :errored, :cancelled]
35
- STUCK_TASK_TIMEOUT = 5.minutes
36
35
 
37
36
  enum status: STATUSES.to_h { |status| [status, status.to_s] }
38
37
 
@@ -342,7 +341,7 @@ module MaintenanceTasks
342
341
  #
343
342
  # @return [Boolean] whether the Run is stuck.
344
343
  def stuck?
345
- (cancelling? || pausing?) && updated_at <= STUCK_TASK_TIMEOUT.ago
344
+ (cancelling? || pausing?) && updated_at <= MaintenanceTasks.stuck_task_duration.ago
346
345
  end
347
346
 
348
347
  # Performs validation on the task_name attribute.
@@ -247,5 +247,55 @@ module MaintenanceTasks
247
247
  def count
248
248
  self.class.collection_builder_strategy.count(self)
249
249
  end
250
+
251
+ # Default enumeration builder. You may override this method to return any
252
+ # Enumerator yielding pairs of `[item, item_cursor]`.
253
+ #
254
+ # @param cursor [String, nil] cursor position to resume from, or nil on
255
+ # initial call.
256
+ #
257
+ # @return [Enumerator]
258
+ 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
299
+ end
250
300
  end
251
301
  end
@@ -60,6 +60,9 @@ module MaintenanceTasks
60
60
  # interrupted -> errored occurs when the task is deleted while it is
61
61
  # interrupted.
62
62
  "interrupted" => ["running", "pausing", "cancelling", "errored"],
63
+ # errored -> enqueued occurs when the task is retried after encounting an
64
+ # error.
65
+ "errored" => ["enqueued"],
63
66
  }
64
67
 
65
68
  # Validate whether a transition from one Run status
@@ -1,22 +1,4 @@
1
- <% if run.arguments.present? %>
2
- <div class="table-container">
3
- <h6 class="title is-6">Arguments:</h6>
4
- <table class="table">
5
- <tbody>
6
- <% run.arguments.transform_values(&:to_s).each do |key, value| %>
7
- <tr>
8
- <td class="is-family-monospace"><%= key %></td>
9
- <td>
10
- <% next if value.empty? %>
11
- <% if value.include?("\n") %>
12
- <pre><%= value %></pre>
13
- <% else %>
14
- <code><%= value %></code>
15
- <% end %>
16
- </td>
17
- </tr>
18
- <% end %>
19
- </tbody>
20
- </table>
21
- </div>
1
+ <% if arguments.present? %>
2
+ <h6 class="title is-6">Arguments:</h6>
3
+ <%= render "maintenance_tasks/runs/serializable", serializable: arguments %>
22
4
  <% end %>
@@ -0,0 +1,4 @@
1
+ <% if metadata.present? %>
2
+ <h6 class="title is-6">Metadata:</h6>
3
+ <%= render "maintenance_tasks/runs/serializable", serializable: metadata %>
4
+ <% end %>
@@ -2,6 +2,7 @@
2
2
  <h5 class="title is-5">
3
3
  <%= time_tag run.created_at, title: run.created_at.utc.iso8601 %>
4
4
  <%= status_tag run.status %>
5
+ <span class="is-pulled-right" title="Run ID">#<%= run.id %></span>
5
6
  </h5>
6
7
 
7
8
  <%= progress run %>
@@ -16,12 +17,16 @@
16
17
 
17
18
  <%= render "maintenance_tasks/runs/csv", run: run %>
18
19
  <%= tag.hr if run.csv_file.present? && run.arguments.present? %>
19
- <%= render "maintenance_tasks/runs/arguments", run: run %>
20
+ <%= render "maintenance_tasks/runs/arguments", arguments: run.arguments %>
21
+ <%= tag.hr if run.csv_file.present? || run.arguments.present? && run.metadata.present? %>
22
+ <%= render "maintenance_tasks/runs/metadata", metadata: run.metadata %>
20
23
 
21
24
  <div class="buttons">
22
25
  <% if run.paused? %>
23
26
  <%= button_to 'Resume', resume_task_run_path(@task, run), method: :put, class: 'button is-primary', disabled: @task.deleted? %>
24
27
  <%= button_to 'Cancel', cancel_task_run_path(@task, run), method: :put, class: 'button is-danger' %>
28
+ <% elsif run.errored? %>
29
+ <%= button_to 'Resume', resume_task_run_path(@task, run), method: :put, class: 'button is-primary', disabled: @task.deleted? %>
25
30
  <% elsif run.cancelling? %>
26
31
  <% if run.stuck? %>
27
32
  <%= button_to 'Cancel', cancel_task_run_path(@task, run), method: :put, class: 'button is-danger', disabled: @task.deleted? %>
@@ -0,0 +1,26 @@
1
+ <% if serializable.present? %>
2
+ <% case serializable %>
3
+ <% when Hash %>
4
+ <div class="table-container">
5
+ <table class="table">
6
+ <tbody>
7
+ <% serializable.transform_values(&:to_s).each do |key, value| %>
8
+ <tr>
9
+ <td class="is-family-monospace"><%= key %></td>
10
+ <td>
11
+ <% next if value.empty? %>
12
+ <% if value.include?("\n") %>
13
+ <pre><%= value %></pre>
14
+ <% else %>
15
+ <code><%= value %></code>
16
+ <% end %>
17
+ </td>
18
+ </tr>
19
+ <% end %>
20
+ </tbody>
21
+ </table>
22
+ </div>
23
+ <% else %>
24
+ <code><%= serializable.inspect %></code>
25
+ <% end %>
26
+ <% end %>
@@ -21,6 +21,8 @@
21
21
 
22
22
  <%= render "maintenance_tasks/runs/csv", run: run %>
23
23
  <%= tag.hr if run.csv_file.present? && run.arguments.present? %>
24
- <%= render "maintenance_tasks/runs/arguments", run: run %>
24
+ <%= render "maintenance_tasks/runs/arguments", arguments: run.arguments %>
25
+ <%= tag.hr if run.csv_file.present? || run.arguments.present? && run.metadata.present? %>
26
+ <%= render "maintenance_tasks/runs/metadata", metadata: run.metadata %>
25
27
  <% end %>
26
28
  </div>
@@ -89,4 +89,11 @@ module MaintenanceTasks
89
89
  #
90
90
  # @return [Proc] generates a hash containing the metadata to be stored on the Run
91
91
  mattr_accessor :metadata, default: nil
92
+
93
+ # @!attribute stuck_task_duration
94
+ # @scope class
95
+ # The duration after which a task is considered stuck and can be force cancelled.
96
+ #
97
+ # @return [ActiveSupport::Duration] the threshold in seconds after which a task is considered stuck.
98
+ mattr_accessor :stuck_task_duration, default: 5.minutes
92
99
  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.0
4
+ version: 2.5.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: 2023-12-20 00:00:00.000000000 Z
11
+ date: 2024-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -128,7 +128,9 @@ files:
128
128
  - app/views/layouts/maintenance_tasks/application.html.erb
129
129
  - app/views/maintenance_tasks/runs/_arguments.html.erb
130
130
  - app/views/maintenance_tasks/runs/_csv.html.erb
131
+ - app/views/maintenance_tasks/runs/_metadata.html.erb
131
132
  - app/views/maintenance_tasks/runs/_run.html.erb
133
+ - app/views/maintenance_tasks/runs/_serializable.html.erb
132
134
  - app/views/maintenance_tasks/runs/info/_cancelled.html.erb
133
135
  - app/views/maintenance_tasks/runs/info/_cancelling.html.erb
134
136
  - app/views/maintenance_tasks/runs/info/_custom.html.erb
@@ -169,7 +171,7 @@ homepage: https://github.com/Shopify/maintenance_tasks
169
171
  licenses:
170
172
  - MIT
171
173
  metadata:
172
- source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.4.0
174
+ source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.5.0
173
175
  allowed_push_host: https://rubygems.org
174
176
  post_install_message:
175
177
  rdoc_options: []
@@ -186,7 +188,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
188
  - !ruby/object:Gem::Version
187
189
  version: '0'
188
190
  requirements: []
189
- rubygems_version: 3.4.22
191
+ rubygems_version: 3.5.5
190
192
  signing_key:
191
193
  specification_version: 4
192
194
  summary: A Rails engine for queuing and managing maintenance tasks