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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6019b1623a8cc72e877154f52e8437339598f0cab121189d78692e0989e86e61
4
- data.tar.gz: 21563dc6dffadd2e58dd551fe2b7d71297b2da36500722f5d9fed91892b1fd6e
3
+ metadata.gz: b11e5edbefa677caf704bd6af59a14185ab89ff29da35d966253b78143a76af1
4
+ data.tar.gz: a103be6c53d5d6dae55d63f451d293abe1c0a4e763cb1c2bd9e3dcfea3a6bc72
5
5
  SHA512:
6
- metadata.gz: 93228d08b49fab144297cb44a0aa4b9be53144ff13b88c2eba384ef29fdb47b9a8caefaf94aaf1fbfbf4584f4d544396720c17d86f3bad31508fd66dfd7507b8
7
- data.tar.gz: cab4cebb685fddf978eeebd62a5c16867215f849d424b9ba948beba0e93f42a5fe11ee1eccc15174c1494fa16a2fa100d3a7745c5eacef2527d425895805cf29
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.process(input)
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
 
@@ -29,5 +29,10 @@ module MaintenanceTasks
29
29
  def has_csv_content?
30
30
  true
31
31
  end
32
+
33
+ # Returns that the Task processes a collection.
34
+ def no_collection?
35
+ false
36
+ end
32
37
  end
33
38
  end
@@ -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.to_s,
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
- # Returns whether the Task handles CSV.
67
- #
68
- # @return [Boolean] whether the Task handles CSV.
69
- def has_csv_content?
70
- collection_builder_strategy.has_csv_content?
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 item the item to process.
78
- def process(item)
79
- new.process(item)
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
 
@@ -41,6 +41,7 @@ module MaintenanceTasks
41
41
  # row will call the block at most once (if it had been throttled).
42
42
  def persist
43
43
  return if @ticks_recorded == 0
44
+
44
45
  now = Time.now
45
46
  duration = now - @last_persisted
46
47
  @last_persisted = now
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= tasks_module %>
4
+ <% module_namespacing do -%>
5
+ class <%= class_name %>Task < MaintenanceTasks::Task
6
+ no_collection
7
+
8
+ def process
9
+ # The work to be done
10
+ end
11
+ end
12
+ <% end -%>
13
+ 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 -%>
@@ -66,6 +66,7 @@ module MaintenanceTasks
66
66
  # @private
67
67
  def self.error_handler
68
68
  return @error_handler if defined?(@error_handler)
69
+
69
70
  @error_handler = ->(_error, _task_context, _errored_element) {}
70
71
  end
71
72
 
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.7.0
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-01-28 00:00:00.000000000 Z
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: '1.1'
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: '1.1'
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.7.0
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: []