maintenance_tasks 1.8.1 → 1.10.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: 6dbf3120a28de1ce88571561ba3be52d160743d365f862a65573b24fcc64cc24
4
- data.tar.gz: 87fda9aaf48c7643a85cc542cbda6d89721ea0b656396329abd9a2d4a42cab68
3
+ metadata.gz: cd4ace45bec0d57e14080ddf832419d1daa1a06fc8cb7d7e79af2c14b2e9c26c
4
+ data.tar.gz: 698945e60d17b43eeab9da5a3f43aa68fb0d9841f2aad1f66fc0c1f2994a4bc5
5
5
  SHA512:
6
- metadata.gz: 77cc4fad4f591683cb3b6062265c487efb8f15b7f31aca2620730af3b74dd57296f8451d7077256fe2f5718762b8ba3de76e7f1affe2c888c202c585a781a570
7
- data.tar.gz: 15e9c05f39cc0cc6216b66081ec051eebd4a669e5c5e002672f2028d5a160fa8dd4a9144e726bc455aa7a60b96018c2fe1a572079a9d705d7b299b7762e483df
6
+ metadata.gz: 5a4c862c0eddbf87fd0196b390eb6cc01b7dbe56d08772c4afb23e7cbaf351a6a9328e4c8bdf66057b4e871c099983649b6acddb6371fc7f216bb226b9ba372c
7
+ data.tar.gz: f7da9ab1b727fc9be10900da568a59c8c7a6a9620e7619f879942a5489b56ab673c8efcfbfc0551d54f018df0baef1638220854a2f88c7c0824bcfb6ccbaf222
data/README.md CHANGED
@@ -8,9 +8,9 @@ A Rails engine for queuing and managing maintenance tasks.
8
8
 
9
9
  To install the gem and run the install generator, execute:
10
10
 
11
- ```bash
12
- $ bundle add maintenance_tasks
13
- $ bin/rails generate maintenance_tasks:install
11
+ ```sh-session
12
+ bundle add maintenance_tasks
13
+ bin/rails generate maintenance_tasks:install
14
14
  ```
15
15
 
16
16
  The generator creates and runs a migration to add the necessary table to your
@@ -39,8 +39,8 @@ take a look at the [Active Job documentation][active-job-docs].
39
39
 
40
40
  A generator is provided to create tasks. Generate a new task by running:
41
41
 
42
- ```bash
43
- $ bin/rails generate maintenance_tasks:task update_posts
42
+ ```sh-session
43
+ bin/rails generate maintenance_tasks:task update_posts
44
44
  ```
45
45
 
46
46
  This creates the task file `app/tasks/maintenance/update_posts_task.rb`.
@@ -86,8 +86,8 @@ instuctions][setup].
86
86
 
87
87
  Generate a CSV Task by running:
88
88
 
89
- ```bash
90
- $ bin/rails generate maintenance_tasks:task import_posts --csv
89
+ ```sh-session
90
+ bin/rails generate maintenance_tasks:task import_posts --csv
91
91
  ```
92
92
 
93
93
  The generated task is a subclass of `MaintenanceTasks::Task` that implements:
@@ -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
@@ -168,8 +195,8 @@ collection-less tasks.
168
195
 
169
196
  Generate a collection-less Task by running:
170
197
 
171
- ```bash
172
- $ bin/rails generate maintenance_tasks:task no_collection_task --no-collection
198
+ ```sh-session
199
+ bin/rails generate maintenance_tasks:task no_collection_task --no-collection
173
200
  ```
174
201
 
175
202
  The generated task is a subclass of `MaintenanceTasks::Task` that implements:
@@ -456,21 +483,21 @@ You can run your new Task by accessing the Web UI and clicking on "Run".
456
483
 
457
484
  Alternatively, you can run your Task in the command line:
458
485
 
459
- ```bash
460
- $ bundle exec maintenance_tasks perform Maintenance::UpdatePostsTask
486
+ ```sh-session
487
+ bundle exec maintenance_tasks perform Maintenance::UpdatePostsTask
461
488
  ```
462
489
 
463
490
  To run a Task that processes CSVs from the command line, use the --csv option:
464
491
 
465
- ```bash
466
- $ bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv "path/to/my_csv.csv"
492
+ ```sh-session
493
+ bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv "path/to/my_csv.csv"
467
494
  ```
468
495
 
469
496
  To run a Task that takes arguments from the command line, use the --arguments
470
497
  option, passing arguments as a set of \<key>:\<value> pairs:
471
498
 
472
- ```bash
473
- $ bundle exec maintenance_tasks perform Maintenance::ParamsTask \
499
+ ```sh-session
500
+ bundle exec maintenance_tasks perform Maintenance::ParamsTask \
474
501
  --arguments post_ids:1,2,3 content:"Hello, World!"
475
502
  ```
476
503
 
@@ -718,8 +745,8 @@ clean backtraces.
718
745
  Use bundler to check for and upgrade to newer versions. After installing a new
719
746
  version, re-run the install command:
720
747
 
721
- ```bash
722
- $ bin/rails generate maintenance_tasks:install
748
+ ```sh-session
749
+ bin/rails generate maintenance_tasks:install
723
750
  ```
724
751
 
725
752
  This ensures that new migrations are installed and run as well.
@@ -8,7 +8,15 @@ module MaintenanceTasks
8
8
  BULMA_CDN = "https://cdn.jsdelivr.net"
9
9
 
10
10
  content_security_policy do |policy|
11
- policy.style_src(BULMA_CDN)
11
+ policy.style_src(
12
+ BULMA_CDN,
13
+ # ruby syntax highlighting
14
+ "'sha256-y9V0na/WU44EUNI/HDP7kZ7mfEci4PAOIjYOOan6JMA='",
15
+ )
16
+ policy.script_src(
17
+ # page refresh script
18
+ "'sha256-2RPaBS4XCMLp0JJ/sW407W9l4qjC+WQAHmTOFJTGfqo='",
19
+ )
12
20
  policy.frame_ancestors(:self)
13
21
  end
14
22
 
@@ -44,7 +44,7 @@ module MaintenanceTasks
44
44
  private
45
45
 
46
46
  def set_refresh
47
- @refresh = 3
47
+ @refresh = true
48
48
  end
49
49
  end
50
50
  end
@@ -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
 
@@ -426,6 +429,7 @@ module MaintenanceTasks
426
429
  def truncate(attribute_name, value)
427
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
@@ -15,13 +15,13 @@
15
15
  <%= csrf_meta_tags %>
16
16
 
17
17
  <%=
18
- stylesheet_link_tag URI.join(controller.class::BULMA_CDN, 'npm/bulma@0.9.1/css/bulma.css'),
18
+ stylesheet_link_tag(URI.join(controller.class::BULMA_CDN, 'npm/bulma@0.9.3/css/bulma.css'),
19
19
  media: :all,
20
- integrity: 'sha256-67AR2JVjhMZCLVxapLuBSMap5RrXbksv4vlllenHBSE=',
21
- crossorigin: 'anonymous'
20
+ integrity: 'sha384-Zkr9rpl37lclZu6AwYQZZm0CxiMqLZFiibodW+UXLnAWPBr6qgIzPpcmHkpwnyWD',
21
+ crossorigin: 'anonymous') unless request.xhr?
22
22
  %>
23
23
 
24
- <style nonce="<%= content_security_policy_nonce %>">
24
+ <style>
25
25
  .ruby-comment { color: #6a737d;}
26
26
  .ruby-const { color: #e36209; }
27
27
  .ruby-embexpr-beg, .ruby-embexpr-end, .ruby-period { color: #24292e; }
@@ -31,12 +31,30 @@
31
31
  .ruby-label, .ruby-tstring-beg, .ruby-tstring-content, .ruby-tstring-end { color: #032f62; }
32
32
  </style>
33
33
 
34
- <% if defined?(@refresh) %>
35
- <meta http-equiv="refresh" content="<%= @refresh %>">
36
- <% end %>
34
+ <script>
35
+ function refresh() {
36
+ if (!("refresh" in document.body.dataset)) return
37
+ window.setTimeout(() => {
38
+ document.body.style.cursor = "wait"
39
+ fetch(document.location, { headers: { "X-Requested-With": "XMLHttpRequest" } }).then(
40
+ async response => {
41
+ const text = await response.text()
42
+ const newDocument = new DOMParser().parseFromString(text, "text/html")
43
+ document.body.replaceWith(newDocument.body)
44
+ <%# force a redraw for Safari %>
45
+ window.scrollTo({ top: document.documentElement.scrollTop + 1 })
46
+ window.scrollTo({ top: document.documentElement.scrollTop - 1 })
47
+ refresh()
48
+ },
49
+ error => location.reload()
50
+ )
51
+ }, 3000)
52
+ }
53
+ document.addEventListener('DOMContentLoaded', refresh)
54
+ </script>
37
55
  </head>
38
56
 
39
- <body>
57
+ <body <%= "data-refresh" if defined?(@refresh) && @refresh %>>
40
58
  <%= render 'layouts/maintenance_tasks/navbar' %>
41
59
 
42
60
  <section class="section">
@@ -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 %>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- require 'test_helper'
2
+ require "test_helper"
3
3
 
4
4
  module <%= tasks_module %>
5
5
  <% module_namespacing do -%>
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
- require 'rails_helper'
2
+ require "rails_helper"
3
3
 
4
4
  module <%= tasks_module %>
5
5
  <% module_namespacing do -%>
6
6
  RSpec.describe <%= class_name %>Task do
7
7
  describe "#process" do
8
8
  subject(:process) { described_class.process(element) }
9
- let(:element) {
9
+ let(:element) {
10
10
  # Object to be processed in a single iteration of this task
11
11
  }
12
12
  pending "add some examples to (or delete) #{__FILE__}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- require 'test_helper'
2
+ require "test_helper"
3
3
 
4
4
  module <%= tasks_module %>
5
5
  <% module_namespacing do -%>
@@ -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.1
4
+ version: 1.10.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-25 00:00:00.000000000 Z
11
+ date: 2022-05-06 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.1
154
+ source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v1.10.0
154
155
  allowed_push_host: https://rubygems.org
155
156
  post_install_message:
156
157
  rdoc_options: []