maintenance_tasks 2.14.0 → 2.15.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 +47 -1
- data/app/helpers/maintenance_tasks/tasks_helper.rb +1 -1
- data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +15 -3
- data/app/models/concerns/maintenance_tasks/run_concern.rb +35 -0
- data/app/models/maintenance_tasks/runner.rb +2 -0
- data/app/models/maintenance_tasks/task_data_index.rb +9 -0
- data/app/views/layouts/maintenance_tasks/_navbar.html.erb +1 -1
- data/app/views/maintenance_tasks/runs/_run.html.erb +1 -1
- data/app/views/maintenance_tasks/runs/info/_errored.html.erb +1 -1
- data/app/views/maintenance_tasks/runs/info/_paused.html.erb +1 -1
- data/app/views/maintenance_tasks/runs/info/_running.html.erb +1 -1
- data/app/views/maintenance_tasks/runs/info/_succeeded.html.erb +0 -1
- data/app/views/maintenance_tasks/tasks/_task.html.erb +13 -1
- data/app/views/maintenance_tasks/tasks/index.html.erb +2 -2
- data/app/views/maintenance_tasks/tasks/show.html.erb +2 -2
- data/db/migrate/20251128180556_add_cursor_is_json_flag_to_runs.rb +7 -0
- data/lib/maintenance_tasks.rb +36 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f74ab248a9b57875d5e7b47234598a46fbc37033fb7d96c95e26b03c55d83793
|
|
4
|
+
data.tar.gz: cf2322966430eee4215131e9885aba07591d2e6f5081f6ab1683188ae5be8e80
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22bde697fa7b38899ff5dc4c7fc353776a29766f479a3fac4f988b921aaf62bd58534f55a2c9caa1b9bf227ddbbef249c1d521b863afc5ab59527ad32528a4e9
|
|
7
|
+
data.tar.gz: 1f51f3e1ead4af15d02886b950562e0a103baf055839e1e5e6f661b2dfd29d47626859fbd350e8d311293c88d4b4f59189ae70e32a8dbca54559d75ce5fafaa5
|
data/README.md
CHANGED
|
@@ -391,7 +391,10 @@ This method should return an `Enumerator`, yielding pairs of `[item, cursor]`.
|
|
|
391
391
|
Maintenance Tasks takes care of persisting the current cursor position and will
|
|
392
392
|
provide it as the `cursor` argument if your task is interrupted or resumed. The
|
|
393
393
|
`cursor` is stored as a `String`, so your custom enumerator should handle
|
|
394
|
-
serializing/deserializing the value if required.
|
|
394
|
+
serializing/deserializing the value if required. Note that if you've enabled
|
|
395
|
+
[JSON cursors](#json-cursors), the cursor will be automatically serialized and
|
|
396
|
+
deserialized. However for this to work, the cursor values must be
|
|
397
|
+
JSON-serializable.
|
|
395
398
|
|
|
396
399
|
```ruby
|
|
397
400
|
# app/tasks/maintenance/custom_enumerator_task.rb
|
|
@@ -561,6 +564,9 @@ control the cursor columns, through the `cursor_columns` method in the
|
|
|
561
564
|
the query is ordered by the primary key. If cursor columns values change during
|
|
562
565
|
an iteration, records may be skipped or yielded multiple times.
|
|
563
566
|
|
|
567
|
+
Note that in order to use this feature, you must first enable
|
|
568
|
+
[JSON cursors](#json-cursors).
|
|
569
|
+
|
|
564
570
|
```ruby
|
|
565
571
|
module Maintenance
|
|
566
572
|
class UpdatePostsTask < MaintenanceTasks::Task
|
|
@@ -1317,6 +1323,46 @@ MaintenanceTasks.metadata = ->() do
|
|
|
1317
1323
|
end
|
|
1318
1324
|
```
|
|
1319
1325
|
|
|
1326
|
+
#### JSON Cursors
|
|
1327
|
+
|
|
1328
|
+
By default, cursor values are persisted in the database as a string. If you
|
|
1329
|
+
want to iterate over collections that require multiple values to keep track of
|
|
1330
|
+
the position, you can enable JSON cursors. This will cause the cursor to be
|
|
1331
|
+
serialized as JSON when persisted, and deserialized when read back.
|
|
1332
|
+
|
|
1333
|
+
This is especially useful when you need to iterate over a model that uses a
|
|
1334
|
+
composite primary key.
|
|
1335
|
+
|
|
1336
|
+
Be advised that this feature comes with a few caveats:
|
|
1337
|
+
|
|
1338
|
+
1. Cursor values must be capable of being serialized to JSON and parsed from
|
|
1339
|
+
JSON. If they are not, errors will occur during task execution.
|
|
1340
|
+
2. If a cursor contains a value that loses precision when serialized, it may
|
|
1341
|
+
lead to unexpected behaviour.
|
|
1342
|
+
3. This feature utilizes a string column to store the JSON data. If your
|
|
1343
|
+
database has a hard limit on how big a string value can be, be mindful that
|
|
1344
|
+
certain cursor structures could result in a value that could exceed that
|
|
1345
|
+
limit and cause issues.
|
|
1346
|
+
|
|
1347
|
+
A new column was added to discern JSON cursors from plain string cursors. Make
|
|
1348
|
+
sure you have run the latest database migrations provided by the gem before
|
|
1349
|
+
enabling this feature. See [upgrading](#upgrading) for more information.
|
|
1350
|
+
|
|
1351
|
+
```ruby
|
|
1352
|
+
# config/initializers/maintenance_tasks.rb
|
|
1353
|
+
MaintenanceTasks.serialize_cursors_as_json = true
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
#### Configure staleness threshold
|
|
1357
|
+
|
|
1358
|
+
To specify a staleness threshold date interval which will mark task runs as stale, you can
|
|
1359
|
+
configure `MaintenanceTasks.task_staleness_threshold`. Tasks that have last run
|
|
1360
|
+
successfully after this threshold will be marked stale.
|
|
1361
|
+
|
|
1362
|
+
The value for `MaintenanceTasks.task_staleness_threshold` must be an
|
|
1363
|
+
`ActiveSupport::Duration`. If no value is specified, it will default to 30 days. This can be disabled
|
|
1364
|
+
by setting `MaintenanceTasks.task_staleness_threshold` to `false`.
|
|
1365
|
+
|
|
1320
1366
|
## Upgrading
|
|
1321
1367
|
|
|
1322
1368
|
Use bundler to check for and upgrade to newer versions. After installing a new
|
|
@@ -62,7 +62,7 @@ module MaintenanceTasks
|
|
|
62
62
|
def status_tag(status)
|
|
63
63
|
tag.span(
|
|
64
64
|
status.capitalize,
|
|
65
|
-
class: ["tag", "has-text-weight-medium", "
|
|
65
|
+
class: ["tag", "has-text-weight-medium", "px-2", "mx-4"] + STATUS_COLOURS.fetch(status),
|
|
66
66
|
)
|
|
67
67
|
end
|
|
68
68
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module MaintenanceTasks
|
|
4
6
|
# Concern that holds the behaviour of the job that runs the tasks. It is
|
|
5
7
|
# included in {TaskJob} and if MaintenanceTasks.job is overridden, it must be
|
|
@@ -30,8 +32,18 @@ module MaintenanceTasks
|
|
|
30
32
|
|
|
31
33
|
private
|
|
32
34
|
|
|
35
|
+
def serialized_cursor_position
|
|
36
|
+
cursor_position && @run.cursor_is_json? ? cursor_position.to_json : cursor_position
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def deserialized_run_cursor
|
|
40
|
+
return JSON.parse(@run.cursor) if @run.cursor && @run.cursor_is_json?
|
|
41
|
+
|
|
42
|
+
@run.cursor
|
|
43
|
+
end
|
|
44
|
+
|
|
33
45
|
def build_enumerator(_run, cursor:)
|
|
34
|
-
cursor ||=
|
|
46
|
+
cursor ||= deserialized_run_cursor
|
|
35
47
|
self.cursor_position = cursor
|
|
36
48
|
enumerator_builder = self.enumerator_builder
|
|
37
49
|
@collection_enum = @task.enumerator_builder(cursor: cursor)
|
|
@@ -140,7 +152,7 @@ module MaintenanceTasks
|
|
|
140
152
|
|
|
141
153
|
def on_shutdown
|
|
142
154
|
@run.job_shutdown
|
|
143
|
-
@run.cursor =
|
|
155
|
+
@run.cursor = serialized_cursor_position
|
|
144
156
|
@ticker.persist
|
|
145
157
|
end
|
|
146
158
|
|
|
@@ -177,7 +189,7 @@ module MaintenanceTasks
|
|
|
177
189
|
@ticker.persist if defined?(@ticker)
|
|
178
190
|
|
|
179
191
|
if defined?(@run)
|
|
180
|
-
@run.cursor =
|
|
192
|
+
@run.cursor = serialized_cursor_position
|
|
181
193
|
@run.persist_error(error)
|
|
182
194
|
|
|
183
195
|
task_context = {
|
|
@@ -447,6 +447,41 @@ module MaintenanceTasks
|
|
|
447
447
|
argument_filter.filter(arguments)
|
|
448
448
|
end
|
|
449
449
|
|
|
450
|
+
# @return [Boolean]
|
|
451
|
+
# True when the cursor value should be treated as serialized JSON.
|
|
452
|
+
def cursor_is_json?
|
|
453
|
+
MaintenanceTasks.serialize_cursors_as_json && cursor_is_json
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Configures the Run to use the appropriate type of cursor encoding based
|
|
457
|
+
# on `MaintenanceTasks.serialize_cursors_as_json`.
|
|
458
|
+
#
|
|
459
|
+
# This method exists to gracefully handle the situation where the
|
|
460
|
+
# `cursor_is_json` column does not exist. As long as the application is not
|
|
461
|
+
# configured to use JSON cursors (the default), the Run will continue to
|
|
462
|
+
# function even without the new column.
|
|
463
|
+
#
|
|
464
|
+
# * When `MaintenanceTasks.serialize_cursors_as_json` is false, this method
|
|
465
|
+
# no-ops.
|
|
466
|
+
# * When `MaintenanceTasks.serialize_cursors_as_json` is true, this method
|
|
467
|
+
# will mutate the Run so that `cursor_is_json` is set to true.
|
|
468
|
+
def configure_cursor_encoding!
|
|
469
|
+
return unless MaintenanceTasks.serialize_cursors_as_json
|
|
470
|
+
|
|
471
|
+
self.cursor_is_json = true
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Returns whether the run is stale based on the staleness threshold.
|
|
475
|
+
#
|
|
476
|
+
# @return [Boolean]
|
|
477
|
+
def stale?
|
|
478
|
+
return false unless MaintenanceTasks.task_staleness_threshold.present?
|
|
479
|
+
return false unless succeeded?
|
|
480
|
+
return false unless ended_at.present?
|
|
481
|
+
|
|
482
|
+
ended_at < MaintenanceTasks.task_staleness_threshold.ago
|
|
483
|
+
end
|
|
484
|
+
|
|
450
485
|
private
|
|
451
486
|
|
|
452
487
|
def instrument_status_change
|
|
@@ -41,6 +41,8 @@ module MaintenanceTasks
|
|
|
41
41
|
# to a value being too long for the column type.
|
|
42
42
|
def run(name:, csv_file: nil, arguments: {}, run_model: Run, metadata: nil)
|
|
43
43
|
run = run_model.new(task_name: name, arguments: arguments, metadata: metadata)
|
|
44
|
+
run.configure_cursor_encoding!
|
|
45
|
+
|
|
44
46
|
if csv_file
|
|
45
47
|
run.csv_file.attach(csv_file)
|
|
46
48
|
run.csv_file.filename = filename(name)
|
|
@@ -64,6 +64,15 @@ module MaintenanceTasks
|
|
|
64
64
|
|
|
65
65
|
alias_method :to_s, :name
|
|
66
66
|
|
|
67
|
+
# Delegates to the related run's stale? method when available.
|
|
68
|
+
#
|
|
69
|
+
# @return [Boolean] whether the related run is stale.
|
|
70
|
+
def stale?
|
|
71
|
+
return false unless related_run.present?
|
|
72
|
+
|
|
73
|
+
related_run.stale?
|
|
74
|
+
end
|
|
75
|
+
|
|
67
76
|
# Returns the status of the latest active or completed Run, if present.
|
|
68
77
|
# If the Task does not have any Runs, the Task status is `new`.
|
|
69
78
|
#
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<nav class="navbar" role="navigation" aria-label="
|
|
1
|
+
<nav class="navbar" role="navigation" aria-label="Main navigation">
|
|
2
2
|
<div class="navbar-brand">
|
|
3
3
|
<%= link_to 'Maintenance Tasks', root_path, class: 'navbar-item is-size-4 has-text-weight-semibold has-text-danger' %>
|
|
4
4
|
</div>
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
<% elsif run.active? %>
|
|
47
47
|
<%= button_to 'Pause', pause_task_run_path(@task, run), class: 'button is-warning', disabled: @task.deleted? %>
|
|
48
48
|
<%= button_to 'Cancel', cancel_task_run_path(@task, run), class: 'button is-danger' %>
|
|
49
|
-
<% end%>
|
|
49
|
+
<% end %>
|
|
50
50
|
</div>
|
|
51
51
|
</details>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<% if (time_to_completion = run.time_to_completion) %>
|
|
2
|
-
<p>Running for <%= time_running_in_words(run)
|
|
2
|
+
<p>Running for <%= time_running_in_words(run) %>. <%= distance_of_time_in_words(time_to_completion).capitalize %> remaining.</p>
|
|
3
3
|
<% end %>
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
<div class="cell box">
|
|
2
|
-
<h3 class="title is-5 has-text-weight-medium">
|
|
2
|
+
<h3 class="title is-5 has-text-weight-medium is-flex is-align-items-center">
|
|
3
3
|
<%= link_to task, task_path(task) %>
|
|
4
4
|
<%= status_tag(task.status) %>
|
|
5
5
|
</h3>
|
|
6
6
|
|
|
7
7
|
<% if (run = task.related_run) %>
|
|
8
|
+
<% if task.stale? %>
|
|
9
|
+
<div class="content is-size-7 is-flex is-align-items-center has-text-warning">
|
|
10
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icon is-small">
|
|
11
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
|
|
12
|
+
</svg>
|
|
13
|
+
|
|
14
|
+
<p class="ml-1">
|
|
15
|
+
This task last ran <%= MaintenanceTasks.task_staleness_threshold.inspect %> ago. Consider removing it as it may be stale.
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
8
20
|
<h5 class="title is-5 has-text-weight-medium">
|
|
9
21
|
<%= time_tag run.created_at, title: run.created_at.utc %>
|
|
10
22
|
</h5>
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
<% end %>
|
|
15
15
|
<% if new_tasks = @available_tasks[:new] %>
|
|
16
16
|
<h3 class="title is-4 has-text-weight-bold">New Tasks</h3>
|
|
17
|
-
|
|
17
|
+
<div class="grid is-col-min-20">
|
|
18
18
|
<%= render partial: 'task', collection: new_tasks %>
|
|
19
|
-
|
|
19
|
+
</div>
|
|
20
20
|
<% end %>
|
|
21
21
|
<% if completed_tasks = @available_tasks[:completed] %>
|
|
22
22
|
<h3 class="title is-4 has-text-weight-bold">Completed Tasks</h3>
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
<%= tag.div(data: { refresh: @task.refresh? || "" }) do %>
|
|
46
46
|
<% if @task.active_runs.any? %>
|
|
47
|
-
<hr
|
|
47
|
+
<hr>
|
|
48
48
|
|
|
49
49
|
<h4 class="title is-4">Active Runs</h4>
|
|
50
50
|
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
<% end %>
|
|
53
53
|
|
|
54
54
|
<% if @task.runs_page.records.present? %>
|
|
55
|
-
<hr
|
|
55
|
+
<hr>
|
|
56
56
|
|
|
57
57
|
<h4 class="title is-5 has-text-weight-bold">Previous Runs</h4>
|
|
58
58
|
|
data/lib/maintenance_tasks.rb
CHANGED
|
@@ -109,6 +109,42 @@ module MaintenanceTasks
|
|
|
109
109
|
# @return [Boolean] whether to report unexpected errors as handled (true) or unhandled (false).
|
|
110
110
|
mattr_accessor :report_errors_as_handled, default: true
|
|
111
111
|
|
|
112
|
+
# @!attribute serialize_cursors_as_json
|
|
113
|
+
# @scope class
|
|
114
|
+
# Controls whether or not cursor values are stored as JSON in the database.
|
|
115
|
+
# Defaults to false.
|
|
116
|
+
#
|
|
117
|
+
# Storing cursors as JSON enables more complex cursor structures. For
|
|
118
|
+
# example, with JSON cursors a task can iterate over collections using
|
|
119
|
+
# multiple fields or columns to track progress. This is particularly useful
|
|
120
|
+
# for iterating over models with composite primary keys.
|
|
121
|
+
#
|
|
122
|
+
# Be advised that this feature comes with a few caveats:
|
|
123
|
+
#
|
|
124
|
+
# 1. Cursor values must be capable of being serialized to JSON and parsed
|
|
125
|
+
# from JSON. If they are not, errors will occur during task execution.
|
|
126
|
+
# 2. If a cursor contains a value that loses precision when serialized, it
|
|
127
|
+
# may lead to unexpected behaviour.
|
|
128
|
+
# 3. This feature utilizes a string column to store the JSON data. If your
|
|
129
|
+
# database has a hard limit on how big a string value can be, be mindful
|
|
130
|
+
# that certain cursor structures could result in a value that could exceed
|
|
131
|
+
# that limit and cause issues.
|
|
132
|
+
#
|
|
133
|
+
# A new column was added to discern JSON cursors from plain string cursors.
|
|
134
|
+
# Make sure you have run the latest database migrations provided by the gem
|
|
135
|
+
# before enabling this feature.
|
|
136
|
+
#
|
|
137
|
+
# @return [Boolean] whether or not to store cursor values as JSON.
|
|
138
|
+
mattr_accessor :serialize_cursors_as_json, default: false
|
|
139
|
+
|
|
140
|
+
# @!attribute task_staleness_threshold
|
|
141
|
+
# @scope class
|
|
142
|
+
# The threshold after which a task is considered stale.
|
|
143
|
+
# Defaults to 30 days. Can be disabled by setting this to `false`.
|
|
144
|
+
#
|
|
145
|
+
# @return [ActiveSupport::Duration, false] time interval after which a task is considered stale.
|
|
146
|
+
mattr_accessor :task_staleness_threshold, default: 30.days
|
|
147
|
+
|
|
112
148
|
class << self
|
|
113
149
|
DEPRECATION_MESSAGE = "MaintenanceTasks.error_handler is deprecated and will be removed in the 3.0 release. " \
|
|
114
150
|
"Instead, reports will be sent to the Rails error reporter. Do not set a handler and subscribe " \
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: maintenance_tasks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify Engineering
|
|
@@ -167,6 +167,7 @@ files:
|
|
|
167
167
|
- db/migrate/20220706101937_change_runs_tick_columns_to_bigints.rb
|
|
168
168
|
- db/migrate/20220713131925_add_index_on_task_name_and_status_to_runs.rb
|
|
169
169
|
- db/migrate/20230622035229_add_metadata_to_runs.rb
|
|
170
|
+
- db/migrate/20251128180556_add_cursor_is_json_flag_to_runs.rb
|
|
170
171
|
- exe/maintenance_tasks
|
|
171
172
|
- lib/generators/maintenance_tasks/install_generator.rb
|
|
172
173
|
- lib/generators/maintenance_tasks/task_generator.rb
|
|
@@ -183,7 +184,7 @@ homepage: https://github.com/Shopify/maintenance_tasks
|
|
|
183
184
|
licenses:
|
|
184
185
|
- MIT
|
|
185
186
|
metadata:
|
|
186
|
-
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.
|
|
187
|
+
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.15.0
|
|
187
188
|
allowed_push_host: https://rubygems.org
|
|
188
189
|
rdoc_options: []
|
|
189
190
|
require_paths:
|
|
@@ -199,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
199
200
|
- !ruby/object:Gem::Version
|
|
200
201
|
version: '0'
|
|
201
202
|
requirements: []
|
|
202
|
-
rubygems_version: 4.0.
|
|
203
|
+
rubygems_version: 4.0.10
|
|
203
204
|
specification_version: 4
|
|
204
205
|
summary: A Rails engine for queuing and managing maintenance tasks
|
|
205
206
|
test_files: []
|