maintenance_tasks 1.8.0 → 1.9.0

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: e66a4f3f2755a31f9dac0125d41d63d5b1b8ca28a4f11642c8648d94dbc2a044
4
- data.tar.gz: 1cf5d80050866d6e78d4cf392c9ff888953a3d15da8c19f83f511aaaf94640d2
3
+ metadata.gz: 0a050fbc2d9dbc3eab88b027ee68163f09cae1472df9e236a7151f5fa1f4464b
4
+ data.tar.gz: c6cf0f55b868b7810c7361669d4383d0fd159dc337b270396e3123a795e75d24
5
5
  SHA512:
6
- metadata.gz: ffc019d73a707936a483349862481e5a227e270be3578f859b7d385fa7d997d327082a6f090881dcb83306feaf496d36f03d75421533ac507d7472adc5cad991
7
- data.tar.gz: ae2ca92ea5b989e11d6ffaa50bd6ce1e5fcd41f735f81e6261ae496b81de2b90e7da2f58fdc04803dc77d44f6c0a8814a86794413f2e7ce374aa557c7e0073fb
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 = MaintenanceTasks::Run.column_for_attribute(attribute_name).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
- self.collection_builder_strategy =
64
- MaintenanceTasks::CsvCollectionBuilder.new
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
 
@@ -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
@@ -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.nil? || value.empty? %>
10
+ <% next if value.empty? %>
11
11
  <% if value.include?("\n") %>
12
12
  <pre><%= value %><pre>
13
13
  <% else %>
@@ -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.8.0
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-02-10 00:00:00.000000000 Z
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: '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
@@ -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.8.0
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: []