maintenance_tasks 1.7.0 → 1.8.2

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: 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: []