maintenance_tasks 1.8.0 → 1.9.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 +27 -0
- data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +7 -1
- data/app/models/maintenance_tasks/batch_csv_collection_builder.rb +39 -0
- data/app/models/maintenance_tasks/run.rb +5 -1
- data/app/models/maintenance_tasks/task.rb +12 -3
- data/app/models/maintenance_tasks/task_data.rb +4 -0
- data/app/models/maintenance_tasks/ticker.rb +1 -0
- data/app/views/maintenance_tasks/runs/_arguments.html.erb +2 -2
- data/lib/maintenance_tasks.rb +1 -0
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a050fbc2d9dbc3eab88b027ee68163f09cae1472df9e236a7151f5fa1f4464b
|
4
|
+
data.tar.gz: c6cf0f55b868b7810c7361669d4383d0fd159dc337b270396e3123a795e75d24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 903da6a69576ffcf022a09248fd7945ec816150505d36c65d96f70cfeec969cb3315e820317c5bff8c882d4e0c09ad33758f9221b2d4d2cf08292a0415219c3a
|
7
|
+
data.tar.gz: 811d79fa5d58252649faea6ed0bc82b6f8fa471d77ae02535cf73fb873a9e663deb893fc81d2528863f68194123db7f7dc55c1a9dc299143da9586fccb4570a6
|
data/README.md
CHANGED
@@ -118,6 +118,33 @@ The files uploaded to your Active Storage service provider will be renamed
|
|
118
118
|
to include an ISO8601 timestamp and the Task name in snake case format.
|
119
119
|
The CSV is expected to have a trailing newline at the end of the file.
|
120
120
|
|
121
|
+
#### Batch CSV Tasks
|
122
|
+
|
123
|
+
Tasks can process CSVs in batches. Add the `in_batches` option to your task's
|
124
|
+
`csv_collection` macro:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
# app/tasks/maintenance/batch_import_posts_task.rb
|
128
|
+
|
129
|
+
module Maintenance
|
130
|
+
class BatchImportPostsTask < MaintenanceTasks::Task
|
131
|
+
csv_collection(in_batches: 50)
|
132
|
+
|
133
|
+
def process(batch_of_rows)
|
134
|
+
Post.insert_all(post_rows.map(&:to_h))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
As with a regular CSV task, ensure you've implemented the following method:
|
141
|
+
|
142
|
+
* `process`: do the work of your Task on a batch (array of `CSV::Row` objects).
|
143
|
+
|
144
|
+
Note that `#count` is calculated automatically based on the number of batches in
|
145
|
+
your collection, and your Task's progress will be displayed in terms of batches
|
146
|
+
(not the total number of rows in your CSV).
|
147
|
+
|
121
148
|
### Processing Batch Collections
|
122
149
|
|
123
150
|
The Maintenance Tasks gem supports processing Active Records in batches. This
|
@@ -47,6 +47,7 @@ module MaintenanceTasks
|
|
47
47
|
a batch enumerator with the "start" or "finish" options.
|
48
48
|
MSG
|
49
49
|
end
|
50
|
+
|
50
51
|
# For now, only support automatic count based on the enumerator for
|
51
52
|
# batches
|
52
53
|
@enumerator = enumerator_builder.active_record_on_batch_relations(
|
@@ -56,6 +57,11 @@ module MaintenanceTasks
|
|
56
57
|
)
|
57
58
|
when Array
|
58
59
|
enumerator_builder.build_array_enumerator(collection, cursor: cursor)
|
60
|
+
when BatchCsvCollectionBuilder::BatchCsv
|
61
|
+
JobIteration::CsvEnumerator.new(collection.csv).batches(
|
62
|
+
batch_size: collection.batch_size,
|
63
|
+
cursor: cursor,
|
64
|
+
)
|
59
65
|
when CSV
|
60
66
|
JobIteration::CsvEnumerator.new(collection).rows(cursor: cursor)
|
61
67
|
else
|
@@ -151,7 +157,7 @@ module MaintenanceTasks
|
|
151
157
|
def after_perform
|
152
158
|
@run.persist_transition
|
153
159
|
if defined?(@reenqueue_iteration_job) && @reenqueue_iteration_job
|
154
|
-
reenqueue_iteration_job(should_ignore: false)
|
160
|
+
reenqueue_iteration_job(should_ignore: false) unless @run.stopped?
|
155
161
|
end
|
156
162
|
end
|
157
163
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "csv"
|
4
|
+
|
5
|
+
module MaintenanceTasks
|
6
|
+
# Strategy for building a Task that processes CSV files in batches.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class BatchCsvCollectionBuilder < CsvCollectionBuilder
|
10
|
+
BatchCsv = Struct.new(:csv, :batch_size, keyword_init: true)
|
11
|
+
|
12
|
+
# Initialize a BatchCsvCollectionBuilder with a batch size.
|
13
|
+
#
|
14
|
+
# @param batch_size [Integer] the number of CSV rows in a batch.
|
15
|
+
def initialize(batch_size)
|
16
|
+
@batch_size = batch_size
|
17
|
+
super()
|
18
|
+
end
|
19
|
+
|
20
|
+
# Defines the collection to be iterated over, based on the provided CSV.
|
21
|
+
# Includes the CSV and the batch size.
|
22
|
+
def collection(task)
|
23
|
+
BatchCsv.new(
|
24
|
+
csv: CSV.new(task.csv_content, headers: true),
|
25
|
+
batch_size: @batch_size
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# The number of batches to be processed. Excludes the header row from the
|
30
|
+
# count and assumes a trailing newline is at the end of the CSV file.
|
31
|
+
# Note that this number is an approximation based on the number of
|
32
|
+
# newlines.
|
33
|
+
#
|
34
|
+
# @return [Integer] the approximate number of batches to process.
|
35
|
+
def count(task)
|
36
|
+
(task.csv_content.count("\n") + @batch_size - 1) / @batch_size
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -52,6 +52,7 @@ module MaintenanceTasks
|
|
52
52
|
# Ensure ActiveStorage is in use before preloading the attachments
|
53
53
|
scope :with_attached_csv, -> do
|
54
54
|
return unless defined?(ActiveStorage)
|
55
|
+
|
55
56
|
with_attached_csv_file if ActiveStorage::Attachment.table_exists?
|
56
57
|
end
|
57
58
|
|
@@ -240,6 +241,7 @@ module MaintenanceTasks
|
|
240
241
|
# Preserve swap-and-replace solution for data races until users
|
241
242
|
# run migration to upgrade to optimistic locking solution
|
242
243
|
return if stopping?
|
244
|
+
|
243
245
|
updated = self.class.where(id: id).where.not(status: STOPPING_STATUSES)
|
244
246
|
.update_all(status: :running, updated_at: Time.now) > 0
|
245
247
|
if updated
|
@@ -384,6 +386,7 @@ module MaintenanceTasks
|
|
384
386
|
def csv_file
|
385
387
|
return unless defined?(ActiveStorage)
|
386
388
|
return unless ActiveStorage::Attachment.table_exists?
|
389
|
+
|
387
390
|
super
|
388
391
|
end
|
389
392
|
|
@@ -424,8 +427,9 @@ module MaintenanceTasks
|
|
424
427
|
end
|
425
428
|
|
426
429
|
def truncate(attribute_name, value)
|
427
|
-
limit =
|
430
|
+
limit = self.class.column_for_attribute(attribute_name).limit
|
428
431
|
return value unless limit
|
432
|
+
|
429
433
|
value&.first(limit)
|
430
434
|
end
|
431
435
|
end
|
@@ -38,6 +38,7 @@ module MaintenanceTasks
|
|
38
38
|
unless task.is_a?(Class) && task < Task
|
39
39
|
raise NotFoundError.new("#{name} is not a Task.", name)
|
40
40
|
end
|
41
|
+
|
41
42
|
task
|
42
43
|
end
|
43
44
|
|
@@ -52,16 +53,23 @@ module MaintenanceTasks
|
|
52
53
|
|
53
54
|
# Make this Task a task that handles CSV.
|
54
55
|
#
|
56
|
+
# @param in_batches [Integer] optionally, supply a batch size if the CSV
|
57
|
+
# should be processed in batches.
|
58
|
+
#
|
55
59
|
# An input to upload a CSV will be added in the form to start a Run. The
|
56
60
|
# collection and count method are implemented.
|
57
|
-
def csv_collection
|
61
|
+
def csv_collection(in_batches: nil)
|
58
62
|
unless defined?(ActiveStorage)
|
59
63
|
raise NotImplementedError, "Active Storage needs to be installed\n"\
|
60
64
|
"To resolve this issue run: bin/rails active_storage:install"
|
61
65
|
end
|
62
66
|
|
63
|
-
|
64
|
-
|
67
|
+
if in_batches
|
68
|
+
self.collection_builder_strategy =
|
69
|
+
BatchCsvCollectionBuilder.new(in_batches)
|
70
|
+
else
|
71
|
+
self.collection_builder_strategy = CsvCollectionBuilder.new
|
72
|
+
end
|
65
73
|
end
|
66
74
|
|
67
75
|
# Make this a Task that calls #process once, instead of iterating over
|
@@ -172,6 +180,7 @@ module MaintenanceTasks
|
|
172
180
|
def load_constants
|
173
181
|
namespace = MaintenanceTasks.tasks_module.safe_constantize
|
174
182
|
return unless namespace
|
183
|
+
|
175
184
|
namespace.constants.map { |constant| namespace.const_get(constant) }
|
176
185
|
end
|
177
186
|
end
|
@@ -73,6 +73,7 @@ module MaintenanceTasks
|
|
73
73
|
# @return [nil] if the Task file was deleted.
|
74
74
|
def code
|
75
75
|
return if deleted?
|
76
|
+
|
76
77
|
task = Task.named(name)
|
77
78
|
file = if Object.respond_to?(:const_source_location)
|
78
79
|
Object.const_source_location(task.name).first
|
@@ -88,6 +89,7 @@ module MaintenanceTasks
|
|
88
89
|
# @return [nil] if there are no Runs associated with the Task.
|
89
90
|
def last_run
|
90
91
|
return @last_run if defined?(@last_run)
|
92
|
+
|
91
93
|
@last_run = runs.first
|
92
94
|
end
|
93
95
|
|
@@ -100,6 +102,7 @@ module MaintenanceTasks
|
|
100
102
|
# record previous to the last Run.
|
101
103
|
def previous_runs
|
102
104
|
return Run.none unless last_run
|
105
|
+
|
103
106
|
runs.where.not(id: last_run.id)
|
104
107
|
end
|
105
108
|
|
@@ -150,6 +153,7 @@ module MaintenanceTasks
|
|
150
153
|
# @return [nil] if the Task file was deleted.
|
151
154
|
def new
|
152
155
|
return if deleted?
|
156
|
+
|
153
157
|
MaintenanceTasks::Task.named(name).new
|
154
158
|
end
|
155
159
|
|
@@ -3,11 +3,11 @@
|
|
3
3
|
<h6 class="title is-6">Arguments:</h6>
|
4
4
|
<table class="table">
|
5
5
|
<tbody>
|
6
|
-
<% run.arguments.each do |key, value| %>
|
6
|
+
<% run.arguments.transform_values(&:to_s).each do |key, value| %>
|
7
7
|
<tr>
|
8
8
|
<td class="is-family-monospace"><%= key %></td>
|
9
9
|
<td>
|
10
|
-
<% next if value.
|
10
|
+
<% next if value.empty? %>
|
11
11
|
<% if value.include?("\n") %>
|
12
12
|
<pre><%= value %><pre>
|
13
13
|
<% else %>
|
data/lib/maintenance_tasks.rb
CHANGED
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: 1.
|
4
|
+
version: 1.9.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: 2022-
|
11
|
+
date: 2022-04-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 1.3.6
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 1.3.6
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: railties
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -97,6 +97,7 @@ files:
|
|
97
97
|
- app/jobs/concerns/maintenance_tasks/task_job_concern.rb
|
98
98
|
- app/jobs/maintenance_tasks/task_job.rb
|
99
99
|
- app/models/maintenance_tasks/application_record.rb
|
100
|
+
- app/models/maintenance_tasks/batch_csv_collection_builder.rb
|
100
101
|
- app/models/maintenance_tasks/csv_collection_builder.rb
|
101
102
|
- app/models/maintenance_tasks/no_collection_builder.rb
|
102
103
|
- app/models/maintenance_tasks/null_collection_builder.rb
|
@@ -150,7 +151,7 @@ homepage: https://github.com/Shopify/maintenance_tasks
|
|
150
151
|
licenses:
|
151
152
|
- MIT
|
152
153
|
metadata:
|
153
|
-
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v1.
|
154
|
+
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v1.9.0
|
154
155
|
allowed_push_host: https://rubygems.org
|
155
156
|
post_install_message:
|
156
157
|
rdoc_options: []
|