maintenance_tasks 2.4.0 → 2.5.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: 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