maintenance_tasks 1.7.0 → 1.8.2
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 +30 -0
- data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +9 -2
- data/app/models/maintenance_tasks/csv_collection_builder.rb +5 -0
- data/app/models/maintenance_tasks/no_collection_builder.rb +29 -0
- data/app/models/maintenance_tasks/null_collection_builder.rb +7 -0
- data/app/models/maintenance_tasks/run.rb +12 -2
- data/app/models/maintenance_tasks/task.rb +21 -8
- data/app/models/maintenance_tasks/task_data.rb +4 -0
- data/app/models/maintenance_tasks/ticker.rb +1 -0
- data/lib/generators/maintenance_tasks/task_generator.rb +13 -0
- data/lib/generators/maintenance_tasks/templates/no_collection_task.rb.tt +13 -0
- data/lib/generators/maintenance_tasks/templates/no_collection_task_test.rb.tt +12 -0
- data/lib/generators/maintenance_tasks/templates/task_test.rb.tt +4 -0
- data/lib/maintenance_tasks.rb +1 -0
- metadata +8 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b11e5edbefa677caf704bd6af59a14185ab89ff29da35d966253b78143a76af1
|
|
4
|
+
data.tar.gz: a103be6c53d5d6dae55d63f451d293abe1c0a4e763cb1c2bd9e3dcfea3a6bc72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3415d87b545e09fc65494cecb03a7cea9a5c84838c66c209a129f8017efc611eee7521852c2460ada50c389b2885bb95dcd2bfe807ea8a84693e9b86e2123409
|
|
7
|
+
data.tar.gz: 19fce32dfc506afe512ce6ebc2ddfd0a2fd195b61e4611c4a3febae5ad4bfe09d39ac47e902a1c21f830de9e3c5f953379d8011919ba0a381dc6010599eb289e
|
data/README.md
CHANGED
|
@@ -160,6 +160,36 @@ primary keys of the records of the batch first, and then perform an additional
|
|
|
160
160
|
query to load the records when calling `each` (or any `Enumerable` method)
|
|
161
161
|
inside `#process`.
|
|
162
162
|
|
|
163
|
+
### Tasks that don't need a Collection
|
|
164
|
+
|
|
165
|
+
Sometimes, you might want to run a Task that performs a single operation, such
|
|
166
|
+
as enqueuing another background job or hitting an external API. The gem supports
|
|
167
|
+
collection-less tasks.
|
|
168
|
+
|
|
169
|
+
Generate a collection-less Task by running:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
$ bin/rails generate maintenance_tasks:task no_collection_task --no-collection
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The generated task is a subclass of `MaintenanceTasks::Task` that implements:
|
|
176
|
+
|
|
177
|
+
* `process`: do the work of your maintenance task
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# app/tasks/maintenance/no_collection_task.rb
|
|
181
|
+
|
|
182
|
+
module Maintenance
|
|
183
|
+
class NoCollectionTask < MaintenanceTasks::Task
|
|
184
|
+
no_collection
|
|
185
|
+
|
|
186
|
+
def process
|
|
187
|
+
SomeAsyncJob.perform_later
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
163
193
|
### Throttling
|
|
164
194
|
|
|
165
195
|
Maintenance Tasks often modify a lot of data and can be taxing on your database.
|
|
@@ -36,6 +36,8 @@ module MaintenanceTasks
|
|
|
36
36
|
@enumerator = nil
|
|
37
37
|
|
|
38
38
|
collection_enum = case collection
|
|
39
|
+
when :no_collection
|
|
40
|
+
enumerator_builder.build_once_enumerator(cursor: nil)
|
|
39
41
|
when ActiveRecord::Relation
|
|
40
42
|
enumerator_builder.active_record_on_records(collection, cursor: cursor)
|
|
41
43
|
when ActiveRecord::Batches::BatchEnumerator
|
|
@@ -45,6 +47,7 @@ module MaintenanceTasks
|
|
|
45
47
|
a batch enumerator with the "start" or "finish" options.
|
|
46
48
|
MSG
|
|
47
49
|
end
|
|
50
|
+
|
|
48
51
|
# For now, only support automatic count based on the enumerator for
|
|
49
52
|
# batches
|
|
50
53
|
@enumerator = enumerator_builder.active_record_on_batch_relations(
|
|
@@ -89,7 +92,11 @@ module MaintenanceTasks
|
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
def task_iteration(input)
|
|
92
|
-
@task.
|
|
95
|
+
if @task.no_collection?
|
|
96
|
+
@task.process
|
|
97
|
+
else
|
|
98
|
+
@task.process(input)
|
|
99
|
+
end
|
|
93
100
|
rescue => error
|
|
94
101
|
@errored_element = input
|
|
95
102
|
raise error
|
|
@@ -145,7 +152,7 @@ module MaintenanceTasks
|
|
|
145
152
|
def after_perform
|
|
146
153
|
@run.persist_transition
|
|
147
154
|
if defined?(@reenqueue_iteration_job) && @reenqueue_iteration_job
|
|
148
|
-
reenqueue_iteration_job(should_ignore: false)
|
|
155
|
+
reenqueue_iteration_job(should_ignore: false) unless @run.stopped?
|
|
149
156
|
end
|
|
150
157
|
end
|
|
151
158
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MaintenanceTasks
|
|
4
|
+
# Strategy for building a Task that has no collection. These Tasks
|
|
5
|
+
# consist of a single iteration.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class NoCollectionBuilder
|
|
9
|
+
# Specifies that this task does not process a collection.
|
|
10
|
+
def collection(_task)
|
|
11
|
+
:no_collection
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The number of rows to be processed. Always returns 1.
|
|
15
|
+
def count(_task)
|
|
16
|
+
1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Return that the Task does not process CSV content.
|
|
20
|
+
def has_csv_content?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns that the Task is collection-less.
|
|
25
|
+
def no_collection?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module MaintenanceTasks
|
|
4
4
|
# Base strategy for building a collection-based Task to be performed.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
5
7
|
class NullCollectionBuilder
|
|
6
8
|
# Placeholder method to raise in case a subclass fails to implement the
|
|
7
9
|
# expected instance method.
|
|
@@ -27,5 +29,10 @@ module MaintenanceTasks
|
|
|
27
29
|
def has_csv_content?
|
|
28
30
|
false
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
# Returns that the Task processes a collection.
|
|
34
|
+
def no_collection?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
30
37
|
end
|
|
31
38
|
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
|
|
|
@@ -131,8 +132,8 @@ module MaintenanceTasks
|
|
|
131
132
|
self.started_at ||= Time.now
|
|
132
133
|
update!(
|
|
133
134
|
status: :errored,
|
|
134
|
-
error_class: error.class.
|
|
135
|
-
error_message: error.message,
|
|
135
|
+
error_class: truncate(:error_class, error.class.name),
|
|
136
|
+
error_message: truncate(:error_message, error.message),
|
|
136
137
|
backtrace: MaintenanceTasks.backtrace_cleaner.clean(error.backtrace),
|
|
137
138
|
ended_at: Time.now,
|
|
138
139
|
)
|
|
@@ -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
|
|
|
@@ -422,5 +425,12 @@ module MaintenanceTasks
|
|
|
422
425
|
errors.add(:base, error_message)
|
|
423
426
|
end
|
|
424
427
|
end
|
|
428
|
+
|
|
429
|
+
def truncate(attribute_name, value)
|
|
430
|
+
limit = self.class.column_for_attribute(attribute_name).limit
|
|
431
|
+
return value unless limit
|
|
432
|
+
|
|
433
|
+
value&.first(limit)
|
|
434
|
+
end
|
|
425
435
|
end
|
|
426
436
|
end
|
|
@@ -18,6 +18,7 @@ module MaintenanceTasks
|
|
|
18
18
|
# @api private
|
|
19
19
|
class_attribute :throttle_conditions, default: []
|
|
20
20
|
|
|
21
|
+
# @api private
|
|
21
22
|
class_attribute :collection_builder_strategy,
|
|
22
23
|
default: NullCollectionBuilder.new
|
|
23
24
|
|
|
@@ -37,6 +38,7 @@ module MaintenanceTasks
|
|
|
37
38
|
unless task.is_a?(Class) && task < Task
|
|
38
39
|
raise NotFoundError.new("#{name} is not a Task.", name)
|
|
39
40
|
end
|
|
41
|
+
|
|
40
42
|
task
|
|
41
43
|
end
|
|
42
44
|
|
|
@@ -63,20 +65,23 @@ module MaintenanceTasks
|
|
|
63
65
|
MaintenanceTasks::CsvCollectionBuilder.new
|
|
64
66
|
end
|
|
65
67
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
# Make this a Task that calls #process once, instead of iterating over
|
|
69
|
+
# a collection.
|
|
70
|
+
def no_collection
|
|
71
|
+
self.collection_builder_strategy =
|
|
72
|
+
MaintenanceTasks::NoCollectionBuilder.new
|
|
71
73
|
end
|
|
72
74
|
|
|
75
|
+
delegate :has_csv_content?, :no_collection?,
|
|
76
|
+
to: :collection_builder_strategy
|
|
77
|
+
|
|
73
78
|
# Processes one item.
|
|
74
79
|
#
|
|
75
80
|
# Especially useful for tests.
|
|
76
81
|
#
|
|
77
|
-
# @param
|
|
78
|
-
def process(
|
|
79
|
-
new.process(
|
|
82
|
+
# @param args [Object, nil] the item to process
|
|
83
|
+
def process(*args)
|
|
84
|
+
new.process(*args)
|
|
80
85
|
end
|
|
81
86
|
|
|
82
87
|
# Returns the collection for this Task.
|
|
@@ -168,6 +173,7 @@ module MaintenanceTasks
|
|
|
168
173
|
def load_constants
|
|
169
174
|
namespace = MaintenanceTasks.tasks_module.safe_constantize
|
|
170
175
|
return unless namespace
|
|
176
|
+
|
|
171
177
|
namespace.constants.map { |constant| namespace.const_get(constant) }
|
|
172
178
|
end
|
|
173
179
|
end
|
|
@@ -197,6 +203,13 @@ module MaintenanceTasks
|
|
|
197
203
|
self.class.has_csv_content?
|
|
198
204
|
end
|
|
199
205
|
|
|
206
|
+
# Returns whether the Task is collection-less.
|
|
207
|
+
#
|
|
208
|
+
# @return [Boolean] whether the Task is collection-less.
|
|
209
|
+
def no_collection?
|
|
210
|
+
self.class.no_collection?
|
|
211
|
+
end
|
|
212
|
+
|
|
200
213
|
# The collection to be processed, delegated to the strategy.
|
|
201
214
|
#
|
|
202
215
|
# @return the collection.
|
|
@@ -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
|
|
|
@@ -12,10 +12,17 @@ module MaintenanceTasks
|
|
|
12
12
|
class_option :csv, type: :boolean, default: false,
|
|
13
13
|
desc: "Generate a CSV Task."
|
|
14
14
|
|
|
15
|
+
class_option :no_collection, type: :boolean, default: false,
|
|
16
|
+
desc: "Generate a collection-less Task."
|
|
17
|
+
|
|
15
18
|
check_class_collision suffix: "Task"
|
|
16
19
|
|
|
17
20
|
# Creates the Task file.
|
|
18
21
|
def create_task_file
|
|
22
|
+
if options[:csv] && options[:no_collection]
|
|
23
|
+
raise "Multiple Task type options provided. Please use either "\
|
|
24
|
+
"--csv or --no-collection."
|
|
25
|
+
end
|
|
19
26
|
template_file = File.join(
|
|
20
27
|
"app/tasks/#{tasks_module_file_path}",
|
|
21
28
|
class_path,
|
|
@@ -23,6 +30,8 @@ module MaintenanceTasks
|
|
|
23
30
|
)
|
|
24
31
|
if options[:csv]
|
|
25
32
|
template("csv_task.rb", template_file)
|
|
33
|
+
elsif no_collection?
|
|
34
|
+
template("no_collection_task.rb", template_file)
|
|
26
35
|
else
|
|
27
36
|
template("task.rb", template_file)
|
|
28
37
|
end
|
|
@@ -76,5 +85,9 @@ module MaintenanceTasks
|
|
|
76
85
|
def test_framework
|
|
77
86
|
Rails.application.config.generators.options[:rails][:test_framework]
|
|
78
87
|
end
|
|
88
|
+
|
|
89
|
+
def no_collection?
|
|
90
|
+
options[:no_collection]
|
|
91
|
+
end
|
|
79
92
|
end
|
|
80
93
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'test_helper'
|
|
3
|
+
|
|
4
|
+
module <%= tasks_module %>
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= class_name %>TaskTest < ActiveSupport::TestCase
|
|
7
|
+
# test "#process performs a task iteration" do
|
|
8
|
+
# <%= tasks_module %>::<%= class_name %>Task.process
|
|
9
|
+
# end
|
|
10
|
+
end
|
|
11
|
+
<% end -%>
|
|
12
|
+
end
|
|
@@ -5,7 +5,11 @@ module <%= tasks_module %>
|
|
|
5
5
|
<% module_namespacing do -%>
|
|
6
6
|
class <%= class_name %>TaskTest < ActiveSupport::TestCase
|
|
7
7
|
# test "#process performs a task iteration" do
|
|
8
|
+
<%- if no_collection? -%>
|
|
9
|
+
# <%= tasks_module %>::<%= class_name %>Task.process
|
|
10
|
+
<%- else -%>
|
|
8
11
|
# <%= tasks_module %>::<%= class_name %>Task.process(element)
|
|
12
|
+
<%- end -%>
|
|
9
13
|
# end
|
|
10
14
|
end
|
|
11
15
|
<% end -%>
|
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.8.2
|
|
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-03-10 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
|
|
@@ -98,6 +98,7 @@ files:
|
|
|
98
98
|
- app/jobs/maintenance_tasks/task_job.rb
|
|
99
99
|
- app/models/maintenance_tasks/application_record.rb
|
|
100
100
|
- app/models/maintenance_tasks/csv_collection_builder.rb
|
|
101
|
+
- app/models/maintenance_tasks/no_collection_builder.rb
|
|
101
102
|
- app/models/maintenance_tasks/null_collection_builder.rb
|
|
102
103
|
- app/models/maintenance_tasks/progress.rb
|
|
103
104
|
- app/models/maintenance_tasks/run.rb
|
|
@@ -135,6 +136,8 @@ files:
|
|
|
135
136
|
- lib/generators/maintenance_tasks/install_generator.rb
|
|
136
137
|
- lib/generators/maintenance_tasks/task_generator.rb
|
|
137
138
|
- lib/generators/maintenance_tasks/templates/csv_task.rb.tt
|
|
139
|
+
- lib/generators/maintenance_tasks/templates/no_collection_task.rb.tt
|
|
140
|
+
- lib/generators/maintenance_tasks/templates/no_collection_task_test.rb.tt
|
|
138
141
|
- lib/generators/maintenance_tasks/templates/task.rb.tt
|
|
139
142
|
- lib/generators/maintenance_tasks/templates/task_spec.rb.tt
|
|
140
143
|
- lib/generators/maintenance_tasks/templates/task_test.rb.tt
|
|
@@ -147,7 +150,7 @@ homepage: https://github.com/Shopify/maintenance_tasks
|
|
|
147
150
|
licenses:
|
|
148
151
|
- MIT
|
|
149
152
|
metadata:
|
|
150
|
-
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v1.
|
|
153
|
+
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v1.8.2
|
|
151
154
|
allowed_push_host: https://rubygems.org
|
|
152
155
|
post_install_message:
|
|
153
156
|
rdoc_options: []
|