maintenance_tasks 1.10.3 → 2.0.0
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 +58 -21
- data/app/controllers/maintenance_tasks/application_controller.rb +1 -2
- data/app/controllers/maintenance_tasks/runs_controller.rb +29 -1
- data/app/controllers/maintenance_tasks/tasks_controller.rb +5 -22
- data/app/helpers/maintenance_tasks/tasks_helper.rb +2 -2
- data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +5 -5
- data/app/models/maintenance_tasks/batch_csv_collection_builder.rb +1 -1
- data/app/models/maintenance_tasks/run.rb +17 -3
- data/app/models/maintenance_tasks/runner.rb +18 -10
- data/app/models/maintenance_tasks/runs_page.rb +10 -4
- data/app/models/maintenance_tasks/task.rb +7 -13
- data/app/models/maintenance_tasks/task_data_index.rb +87 -0
- data/app/models/maintenance_tasks/{task_data.rb → task_data_show.rb} +26 -70
- data/app/validators/maintenance_tasks/run_status_validator.rb +1 -1
- data/app/views/maintenance_tasks/runs/_run.html.erb +17 -0
- data/app/views/maintenance_tasks/tasks/_task.html.erb +1 -1
- data/app/views/maintenance_tasks/tasks/show.html.erb +32 -62
- data/config/routes.rb +2 -5
- data/db/migrate/20220713131925_add_index_on_task_name_and_status_to_runs.rb +13 -0
- data/lib/generators/maintenance_tasks/task_generator.rb +3 -3
- data/lib/generators/maintenance_tasks/templates/no_collection_task_test.rb.tt +1 -0
- data/lib/generators/maintenance_tasks/templates/task_spec.rb.tt +1 -0
- data/lib/generators/maintenance_tasks/templates/task_test.rb.tt +1 -0
- data/lib/maintenance_tasks/cli.rb +19 -3
- data/lib/maintenance_tasks/engine.rb +3 -13
- data/lib/maintenance_tasks.rb +2 -21
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60d043a961d97e4db5e822b86bf17a29fd409cf14e3951c581ecfb92abc7e2af
|
4
|
+
data.tar.gz: 5170cb9e4bb82c82fe31f07f0dba93ca28caeacffe2b68ec592a092623d6aafd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be30600ed5e353ef1fba3bc484d5349be4567249b0cbb808503e73e0f42af76ae0befe6403fc4bdead3fb3ce0a5cd36e920f427eb013ac5215900571ddbb976a
|
7
|
+
data.tar.gz: 1242c76047b52eae2463ea562c0ac48d09d773f9d4b069a781a140e49c39c9175e25b1eb7d21db5dce6ee96d43844323337761575909db396ccf4bc044a8355b
|
data/README.md
CHANGED
@@ -33,8 +33,27 @@ take a look at the [Active Job documentation][active-job-docs].
|
|
33
33
|
[async-adapter]: https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html
|
34
34
|
[active-job-docs]: https://guides.rubyonrails.org/active_job_basics.html#setting-the-backend
|
35
35
|
|
36
|
+
|
37
|
+
### Autoloading
|
38
|
+
|
39
|
+
The Maintenance Tasks framework does not support autoloading in `:classic` mode.
|
40
|
+
Please ensure your application is using [Zeitwerk](https://github.com/fxn/zeitwerk) to load your code.
|
41
|
+
For more information, please consult the [Rails guides on autoloading and reloading constants](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html).
|
42
|
+
|
36
43
|
## Usage
|
37
44
|
|
45
|
+
The typical Maintenance Tasks workflow is as follows:
|
46
|
+
|
47
|
+
1. [Generate a class describing the Task](#creating-a-task) and the work to be done.
|
48
|
+
2. Run the Task
|
49
|
+
- either by [using the included web UI](#running-a-task-from-the-web-ui),
|
50
|
+
- or by [using the command line](#running-a-task-from-the-command-line),
|
51
|
+
- or by [using Ruby](#running-a-task-from-ruby).
|
52
|
+
3. [Monitor the Task](#monitoring-your-tasks-status)
|
53
|
+
- either by using the included web UI,
|
54
|
+
- or by manually checking your task's run's status in your database.
|
55
|
+
4. Optionally, delete the Task code if you no longer need it.
|
56
|
+
|
38
57
|
### Creating a Task
|
39
58
|
|
40
59
|
A generator is provided to create tasks. Generate a new task by running:
|
@@ -50,8 +69,12 @@ The generated task is a subclass of `MaintenanceTasks::Task` that implements:
|
|
50
69
|
* `collection`: return an Active Record Relation or an Array to be iterated
|
51
70
|
over.
|
52
71
|
* `process`: do the work of your maintenance task on a single record
|
53
|
-
|
54
|
-
|
72
|
+
|
73
|
+
Optionally, tasks can also implement a custom `#count` method, defining the number
|
74
|
+
of elements that will be iterated over. Your task's `tick_total` will be calculated
|
75
|
+
automatically based on the collection size, but this value may be overriden if desired
|
76
|
+
using the `#count` method (this might be done, for example, to avoid the query that would
|
77
|
+
be produced to determine the size of your collection).
|
55
78
|
|
56
79
|
Example:
|
57
80
|
|
@@ -64,10 +87,6 @@ module Maintenance
|
|
64
87
|
Post.all
|
65
88
|
end
|
66
89
|
|
67
|
-
def count
|
68
|
-
collection.count
|
69
|
-
end
|
70
|
-
|
71
90
|
def process(post)
|
72
91
|
post.update!(content: "New content!")
|
73
92
|
end
|
@@ -238,10 +257,6 @@ module Maintenance
|
|
238
257
|
Post.all
|
239
258
|
end
|
240
259
|
|
241
|
-
def count
|
242
|
-
collection.count
|
243
|
-
end
|
244
|
-
|
245
260
|
def process(post)
|
246
261
|
post.update!(content: "New content added on #{Time.now.utc}")
|
247
262
|
end
|
@@ -290,10 +305,6 @@ module Maintenance
|
|
290
305
|
Post.all
|
291
306
|
end
|
292
307
|
|
293
|
-
def count
|
294
|
-
collection.count
|
295
|
-
end
|
296
|
-
|
297
308
|
def process(post)
|
298
309
|
post.update!(content: updated_content)
|
299
310
|
end
|
@@ -394,6 +405,20 @@ depend on the queue adapter but in general, you should follow these rules:
|
|
394
405
|
|
395
406
|
[sidekiq-idempotent]: https://github.com/mperham/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional
|
396
407
|
|
408
|
+
#### Task object life cycle and memoization
|
409
|
+
|
410
|
+
When the Task runs or resumes, the Runner enqueues a job, which processes the
|
411
|
+
Task. That job will instantiate a Task object which will live for the duration
|
412
|
+
of the job. The first time the job runs, it will call `count`. Every time a job
|
413
|
+
runs, it will call `collection` on the Task object, and then `process`
|
414
|
+
for each item in the collection, until the job stops. The job stops when either the
|
415
|
+
collection is finished processing or after the maximum job runtime has expired.
|
416
|
+
|
417
|
+
This means memoization can be misleading within `process`, since the memoized
|
418
|
+
values will be available for subsequent calls to `process` within the same job.
|
419
|
+
Still, memoization can be used for throttling or reporting, and you can use [Task
|
420
|
+
callbacks](#using-task-callbacks) to persist or log a report for example.
|
421
|
+
|
397
422
|
### Writing tests for a Task
|
398
423
|
|
399
424
|
The task generator will also create a test file for your task in the folder
|
@@ -479,21 +504,31 @@ end
|
|
479
504
|
|
480
505
|
### Running a Task
|
481
506
|
|
507
|
+
#### Running a Task from the Web UI
|
508
|
+
|
482
509
|
You can run your new Task by accessing the Web UI and clicking on "Run".
|
483
510
|
|
511
|
+
#### Running a Task from the command line
|
512
|
+
|
484
513
|
Alternatively, you can run your Task in the command line:
|
485
514
|
|
486
515
|
```sh-session
|
487
516
|
bundle exec maintenance_tasks perform Maintenance::UpdatePostsTask
|
488
517
|
```
|
489
518
|
|
490
|
-
To run a Task that processes CSVs from the command line, use the
|
519
|
+
To run a Task that processes CSVs from the command line, use the `--csv` option:
|
491
520
|
|
492
521
|
```sh-session
|
493
522
|
bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv "path/to/my_csv.csv"
|
494
523
|
```
|
495
524
|
|
496
|
-
|
525
|
+
The `--csv` option also works with CSV content coming from the standard input:
|
526
|
+
|
527
|
+
```sh-session
|
528
|
+
curl "some/remote/csv" | bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv
|
529
|
+
```
|
530
|
+
|
531
|
+
To run a Task that takes arguments from the command line, use the `--arguments`
|
497
532
|
option, passing arguments as a set of \<key>:\<value> pairs:
|
498
533
|
|
499
534
|
```sh-session
|
@@ -501,6 +536,8 @@ bundle exec maintenance_tasks perform Maintenance::ParamsTask \
|
|
501
536
|
--arguments post_ids:1,2,3 content:"Hello, World!"
|
502
537
|
```
|
503
538
|
|
539
|
+
#### Running a Task from Ruby
|
540
|
+
|
504
541
|
You can also run a Task in Ruby by sending `run` with a Task name to Runner:
|
505
542
|
|
506
543
|
```ruby
|
@@ -566,7 +603,7 @@ By default, a running Task will be interrupted after running for more 5 minutes.
|
|
566
603
|
This is [configured in the `job-iteration` gem][max-job-runtime] and can be
|
567
604
|
tweaked in an initializer if necessary.
|
568
605
|
|
569
|
-
[max-job-runtime]: https://github.com/Shopify/job-iteration/blob
|
606
|
+
[max-job-runtime]: https://github.com/Shopify/job-iteration/blob/-/guides/best-practices.md#max-job-runtime
|
570
607
|
|
571
608
|
Running tasks will also be interrupted and re-enqueued when needed. For example
|
572
609
|
[when Sidekiq workers shuts down for a deploy][sidekiq-deploy]:
|
@@ -581,9 +618,9 @@ Running tasks will also be interrupted and re-enqueued when needed. For example
|
|
581
618
|
|
582
619
|
When Sidekiq is stopping, it will give workers 25 seconds to finish before
|
583
620
|
forcefully terminating them (this is the default but can be configured with the
|
584
|
-
`--timeout` option).
|
585
|
-
to re-enqueue the job so your Task will be resumed. However, the position in
|
586
|
-
collection won't be persisted so at least one iteration may run again.
|
621
|
+
`--timeout` option). Before the worker threads are terminated, Sidekiq will try
|
622
|
+
to re-enqueue the job so your Task will be resumed. However, the position in
|
623
|
+
the collection won't be persisted so at least one iteration may run again.
|
587
624
|
|
588
625
|
#### Help! My Task is stuck
|
589
626
|
|
@@ -757,7 +794,7 @@ The install command will attempt to reinstall these old migrations and migrating
|
|
757
794
|
the database will cause problems. Use `bin/rails
|
758
795
|
maintenance_tasks:install:migrations` to copy the gem's migrations to your
|
759
796
|
`db/migrate` folder. Check the release notes to see if any new migrations were
|
760
|
-
added since your last gem upgrade.
|
797
|
+
added since your last gem upgrade. Ensure that these are kept, but remove any
|
761
798
|
migrations that already ran.
|
762
799
|
|
763
800
|
Run the migrations using `bin/rails db:migrate`.
|
@@ -21,8 +21,7 @@ module MaintenanceTasks
|
|
21
21
|
end
|
22
22
|
|
23
23
|
before_action do
|
24
|
-
request.content_security_policy_nonce_generator ||=
|
25
|
-
->(_request) { SecureRandom.base64(16) }
|
24
|
+
request.content_security_policy_nonce_generator ||= ->(_request) { SecureRandom.base64(16) }
|
26
25
|
request.content_security_policy_nonce_directives = ["style-src"]
|
27
26
|
end
|
28
27
|
|
@@ -6,7 +6,25 @@ module MaintenanceTasks
|
|
6
6
|
#
|
7
7
|
# @api private
|
8
8
|
class RunsController < ApplicationController
|
9
|
-
before_action :set_run
|
9
|
+
before_action :set_run, except: :create
|
10
|
+
|
11
|
+
# Creates a Run for a given Task and redirects to the Task page.
|
12
|
+
def create(&block)
|
13
|
+
task = Runner.run(
|
14
|
+
name: params.fetch(:task_id),
|
15
|
+
csv_file: params[:csv_file],
|
16
|
+
arguments: params.fetch(:task_arguments, {}).permit!.to_h,
|
17
|
+
&block
|
18
|
+
)
|
19
|
+
redirect_to(task_path(task))
|
20
|
+
rescue ActiveRecord::RecordInvalid => error
|
21
|
+
redirect_to(task_path(error.record.task_name), alert: error.message)
|
22
|
+
rescue ActiveRecord::ValueTooLong => error
|
23
|
+
task_name = params.fetch(:id)
|
24
|
+
redirect_to(task_path(task_name), alert: error.message)
|
25
|
+
rescue Runner::EnqueuingError => error
|
26
|
+
redirect_to(task_path(error.run.task_name), alert: error.message)
|
27
|
+
end
|
10
28
|
|
11
29
|
# Updates a Run status to paused.
|
12
30
|
def pause
|
@@ -24,6 +42,16 @@ module MaintenanceTasks
|
|
24
42
|
redirect_to(task_path(@run.task_name), alert: error.message)
|
25
43
|
end
|
26
44
|
|
45
|
+
# Resumes a previously paused Run.
|
46
|
+
def resume
|
47
|
+
Runner.resume(@run)
|
48
|
+
redirect_to(task_path(@run.task_name))
|
49
|
+
rescue ActiveRecord::RecordInvalid => error
|
50
|
+
redirect_to(task_path(@run.task_name), alert: error.message)
|
51
|
+
rescue Runner::EnqueuingError => error
|
52
|
+
redirect_to(task_path(@run.task_name), alert: error.message)
|
53
|
+
end
|
54
|
+
|
27
55
|
private
|
28
56
|
|
29
57
|
def set_run
|
@@ -12,33 +12,16 @@ module MaintenanceTasks
|
|
12
12
|
# Renders the maintenance_tasks/tasks page, displaying
|
13
13
|
# available tasks to users, grouped by category.
|
14
14
|
def index
|
15
|
-
@available_tasks =
|
15
|
+
@available_tasks = TaskDataIndex.available_tasks.group_by(&:category)
|
16
16
|
end
|
17
17
|
|
18
18
|
# Renders the page responsible for providing Task actions to users.
|
19
19
|
# Shows running and completed instances of the Task.
|
20
20
|
def show
|
21
|
-
@task =
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
# Runs a given Task and redirects to the Task page.
|
27
|
-
def run(&block)
|
28
|
-
task = Runner.run(
|
29
|
-
name: params.fetch(:id),
|
30
|
-
csv_file: params[:csv_file],
|
31
|
-
arguments: params.fetch(:task_arguments, {}).permit!.to_h,
|
32
|
-
&block
|
33
|
-
)
|
34
|
-
redirect_to(task_path(task))
|
35
|
-
rescue ActiveRecord::RecordInvalid => error
|
36
|
-
redirect_to(task_path(error.record.task_name), alert: error.message)
|
37
|
-
rescue ActiveRecord::ValueTooLong => error
|
38
|
-
task_name = params.fetch(:id)
|
39
|
-
redirect_to(task_path(task_name), alert: error.message)
|
40
|
-
rescue Runner::EnqueuingError => error
|
41
|
-
redirect_to(task_path(error.run.task_name), alert: error.message)
|
21
|
+
@task = TaskDataShow.find(params.fetch(:id))
|
22
|
+
@active_runs = @task.active_runs
|
23
|
+
set_refresh if @active_runs.any?
|
24
|
+
@runs_page = RunsPage.new(@task.completed_runs, params[:cursor])
|
42
25
|
end
|
43
26
|
|
44
27
|
private
|
@@ -47,7 +47,7 @@ module MaintenanceTasks
|
|
47
47
|
progress_bar = tag.progress(
|
48
48
|
value: progress.value,
|
49
49
|
max: progress.max,
|
50
|
-
class: ["progress"] + STATUS_COLOURS.fetch(run.status)
|
50
|
+
class: ["progress"] + STATUS_COLOURS.fetch(run.status),
|
51
51
|
)
|
52
52
|
progress_text = tag.p(tag.i(progress.text))
|
53
53
|
tag.div(progress_bar + progress_text, class: "block")
|
@@ -97,7 +97,7 @@ module MaintenanceTasks
|
|
97
97
|
def csv_file_download_path(run)
|
98
98
|
Rails.application.routes.url_helpers.rails_blob_path(
|
99
99
|
run.csv_file,
|
100
|
-
only_path: true
|
100
|
+
only_path: true,
|
101
101
|
)
|
102
102
|
end
|
103
103
|
|
@@ -35,7 +35,7 @@ module MaintenanceTasks
|
|
35
35
|
collection = @task.collection
|
36
36
|
@enumerator = nil
|
37
37
|
|
38
|
-
collection_enum = case collection
|
38
|
+
@collection_enum = case collection
|
39
39
|
when :no_collection
|
40
40
|
enumerator_builder.build_once_enumerator(cursor: nil)
|
41
41
|
when ActiveRecord::Relation
|
@@ -50,7 +50,7 @@ module MaintenanceTasks
|
|
50
50
|
|
51
51
|
# For now, only support automatic count based on the enumerator for
|
52
52
|
# batches
|
53
|
-
|
53
|
+
enumerator_builder.active_record_on_batch_relations(
|
54
54
|
collection.relation,
|
55
55
|
cursor: cursor,
|
56
56
|
batch_size: collection.batch_size,
|
@@ -71,7 +71,7 @@ module MaintenanceTasks
|
|
71
71
|
Array, or CSV.
|
72
72
|
MSG
|
73
73
|
end
|
74
|
-
throttle_enumerator(collection_enum)
|
74
|
+
throttle_enumerator(@collection_enum)
|
75
75
|
end
|
76
76
|
|
77
77
|
def throttle_enumerator(collection_enum)
|
@@ -79,7 +79,7 @@ module MaintenanceTasks
|
|
79
79
|
enumerator_builder.build_throttle_enumerator(
|
80
80
|
enum,
|
81
81
|
throttle_on: condition[:throttle_on],
|
82
|
-
backoff: condition[:backoff].call
|
82
|
+
backoff: condition[:backoff].call,
|
83
83
|
)
|
84
84
|
end
|
85
85
|
end
|
@@ -123,7 +123,7 @@ module MaintenanceTasks
|
|
123
123
|
|
124
124
|
def on_start
|
125
125
|
count = @task.count
|
126
|
-
count = @
|
126
|
+
count = @collection_enum.size if count == :no_count
|
127
127
|
@run.start(count)
|
128
128
|
end
|
129
129
|
|
@@ -41,6 +41,7 @@ module MaintenanceTasks
|
|
41
41
|
Task.available_tasks.map(&:to_s)
|
42
42
|
} }
|
43
43
|
validate :csv_attachment_presence, on: :create
|
44
|
+
validate :csv_content_type, on: :create
|
44
45
|
validate :validate_task_arguments, on: :create
|
45
46
|
|
46
47
|
attr_readonly :task_name
|
@@ -49,6 +50,7 @@ module MaintenanceTasks
|
|
49
50
|
serialize :arguments, JSON
|
50
51
|
|
51
52
|
scope :active, -> { where(status: ACTIVE_STATUSES) }
|
53
|
+
scope :completed, -> { where(status: COMPLETED_STATUSES) }
|
52
54
|
|
53
55
|
# Ensure ActiveStorage is in use before preloading the attachments
|
54
56
|
scope :with_attached_csv, -> do
|
@@ -117,7 +119,7 @@ module MaintenanceTasks
|
|
117
119
|
id,
|
118
120
|
tick_count: number_of_ticks,
|
119
121
|
time_running: duration,
|
120
|
-
touch: true
|
122
|
+
touch: true,
|
121
123
|
)
|
122
124
|
if locking_enabled?
|
123
125
|
locking_column = self.class.locking_column
|
@@ -346,6 +348,18 @@ module MaintenanceTasks
|
|
346
348
|
nil
|
347
349
|
end
|
348
350
|
|
351
|
+
# Performs validation on the content type of the :csv_file attachment.
|
352
|
+
# A Run for a Task that uses CsvCollection must have a present :csv_file
|
353
|
+
# and a content type of "text/csv" to be valid. The appropriate error is
|
354
|
+
# added if the Run does not meet the above criteria.
|
355
|
+
def csv_content_type
|
356
|
+
if csv_file.present? && csv_file.content_type != "text/csv"
|
357
|
+
errors.add(:csv_file, "must be a CSV")
|
358
|
+
end
|
359
|
+
rescue Task::NotFoundError
|
360
|
+
nil
|
361
|
+
end
|
362
|
+
|
349
363
|
# Support iterating over ActiveModel::Errors in Rails 6.0 and Rails 6.1+.
|
350
364
|
# To be removed when Rails 6.0 is no longer supported.
|
351
365
|
if Rails::VERSION::STRING.match?(/^6.0/)
|
@@ -358,7 +372,7 @@ module MaintenanceTasks
|
|
358
372
|
.map { |attribute, message| "#{attribute.inspect} #{message}" }
|
359
373
|
errors.add(
|
360
374
|
:arguments,
|
361
|
-
"are invalid: #{error_messages.join("; ")}"
|
375
|
+
"are invalid: #{error_messages.join("; ")}",
|
362
376
|
)
|
363
377
|
end
|
364
378
|
rescue Task::NotFoundError
|
@@ -374,7 +388,7 @@ module MaintenanceTasks
|
|
374
388
|
.map { |error| "#{error.attribute.inspect} #{error.message}" }
|
375
389
|
errors.add(
|
376
390
|
:arguments,
|
377
|
-
"are invalid: #{error_messages.join("; ")}"
|
391
|
+
"are invalid: #{error_messages.join("; ")}",
|
378
392
|
)
|
379
393
|
end
|
380
394
|
rescue Task::NotFoundError
|
@@ -5,14 +5,6 @@ module MaintenanceTasks
|
|
5
5
|
module Runner
|
6
6
|
extend self
|
7
7
|
|
8
|
-
# @deprecated Use {Runner} directly instead.
|
9
|
-
def new
|
10
|
-
ActiveSupport::Deprecation.warn(
|
11
|
-
"Use Runner.run instead of Runner.new.run"
|
12
|
-
)
|
13
|
-
self
|
14
|
-
end
|
15
|
-
|
16
8
|
# Exception raised when a Task Job couldn't be enqueued.
|
17
9
|
class EnqueuingError < StandardError
|
18
10
|
# Initializes a Enqueuing Error.
|
@@ -48,8 +40,7 @@ module MaintenanceTasks
|
|
48
40
|
# @raise [ActiveRecord::ValueTooLong] if the creation of the Run fails due
|
49
41
|
# to a value being too long for the column type.
|
50
42
|
def run(name:, csv_file: nil, arguments: {}, run_model: Run)
|
51
|
-
run = run_model.
|
52
|
-
run_model.new(task_name: name, arguments: arguments)
|
43
|
+
run = run_model.new(task_name: name, arguments: arguments)
|
53
44
|
if csv_file
|
54
45
|
run.csv_file.attach(csv_file)
|
55
46
|
run.csv_file.filename = filename(name)
|
@@ -62,6 +53,23 @@ module MaintenanceTasks
|
|
62
53
|
Task.named(name)
|
63
54
|
end
|
64
55
|
|
56
|
+
# Resumes a Task.
|
57
|
+
#
|
58
|
+
# This method re-instantiates and re-enqueues a job for a Run that was
|
59
|
+
# previously paused.
|
60
|
+
#
|
61
|
+
# @param run [MaintenanceTasks::Run] the Run record to be resumed.
|
62
|
+
#
|
63
|
+
# @return [TaskJob] the enqueued Task job.
|
64
|
+
#
|
65
|
+
# @raise [EnqueuingError] if an error occurs while enqueuing the Run.
|
66
|
+
def resume(run)
|
67
|
+
job = instantiate_job(run)
|
68
|
+
run.job_id = job.job_id
|
69
|
+
run.enqueued!
|
70
|
+
enqueue(run, job)
|
71
|
+
end
|
72
|
+
|
65
73
|
private
|
66
74
|
|
67
75
|
def enqueue(run, job)
|
@@ -24,6 +24,8 @@ module MaintenanceTasks
|
|
24
24
|
# Returns the records for a Page, taking into account the cursor if one is
|
25
25
|
# present. Limits the number of records to 20.
|
26
26
|
#
|
27
|
+
# An extra Run is loaded so that we can verify whether we're on the last Page.
|
28
|
+
#
|
27
29
|
# @return [ActiveRecord::Relation<MaintenanceTasks::Run>] a limited amount
|
28
30
|
# of Run records.
|
29
31
|
def records
|
@@ -33,7 +35,9 @@ module MaintenanceTasks
|
|
33
35
|
else
|
34
36
|
@runs
|
35
37
|
end
|
36
|
-
runs_after_cursor.limit(RUNS_PER_PAGE)
|
38
|
+
limited_runs = runs_after_cursor.limit(RUNS_PER_PAGE + 1).load
|
39
|
+
@extra_run = limited_runs.length > RUNS_PER_PAGE ? limited_runs.last : nil
|
40
|
+
limited_runs.take(RUNS_PER_PAGE)
|
37
41
|
end
|
38
42
|
end
|
39
43
|
|
@@ -47,10 +51,12 @@ module MaintenanceTasks
|
|
47
51
|
|
48
52
|
# Returns whether this Page is the last one.
|
49
53
|
#
|
50
|
-
# @return [Boolean] whether this Page contains the last Run record in the
|
51
|
-
#
|
54
|
+
# @return [Boolean] whether this Page contains the last Run record in the Runs
|
55
|
+
# dataset that is being paginated. This is done by checking whether an extra
|
56
|
+
# Run was loaded by #records - if no extra Run was loaded, this is the last page.
|
52
57
|
def last?
|
53
|
-
|
58
|
+
records
|
59
|
+
@extra_run.nil?
|
54
60
|
end
|
55
61
|
end
|
56
62
|
end
|
@@ -19,8 +19,7 @@ module MaintenanceTasks
|
|
19
19
|
class_attribute :throttle_conditions, default: []
|
20
20
|
|
21
21
|
# @api private
|
22
|
-
class_attribute :collection_builder_strategy,
|
23
|
-
default: NullCollectionBuilder.new
|
22
|
+
class_attribute :collection_builder_strategy, default: NullCollectionBuilder.new
|
24
23
|
|
25
24
|
define_callbacks :start, :complete, :error, :cancel, :pause, :interrupt
|
26
25
|
|
@@ -64,23 +63,20 @@ module MaintenanceTasks
|
|
64
63
|
"To resolve this issue run: bin/rails active_storage:install"
|
65
64
|
end
|
66
65
|
|
67
|
-
if in_batches
|
68
|
-
|
69
|
-
BatchCsvCollectionBuilder.new(in_batches)
|
66
|
+
self.collection_builder_strategy = if in_batches
|
67
|
+
BatchCsvCollectionBuilder.new(in_batches)
|
70
68
|
else
|
71
|
-
|
69
|
+
CsvCollectionBuilder.new
|
72
70
|
end
|
73
71
|
end
|
74
72
|
|
75
73
|
# Make this a Task that calls #process once, instead of iterating over
|
76
74
|
# a collection.
|
77
75
|
def no_collection
|
78
|
-
self.collection_builder_strategy =
|
79
|
-
MaintenanceTasks::NoCollectionBuilder.new
|
76
|
+
self.collection_builder_strategy = MaintenanceTasks::NoCollectionBuilder.new
|
80
77
|
end
|
81
78
|
|
82
|
-
delegate :has_csv_content?, :no_collection?,
|
83
|
-
to: :collection_builder_strategy
|
79
|
+
delegate :has_csv_content?, :no_collection?, to: :collection_builder_strategy
|
84
80
|
|
85
81
|
# Processes one item.
|
86
82
|
#
|
@@ -122,9 +118,7 @@ module MaintenanceTasks
|
|
122
118
|
backoff_as_proc = backoff
|
123
119
|
backoff_as_proc = -> { backoff } unless backoff.respond_to?(:call)
|
124
120
|
|
125
|
-
self.throttle_conditions += [
|
126
|
-
{ throttle_on: condition, backoff: backoff_as_proc },
|
127
|
-
]
|
121
|
+
self.throttle_conditions += [{ throttle_on: condition, backoff: backoff_as_proc }]
|
128
122
|
end
|
129
123
|
|
130
124
|
# Initialize a callback to run after the task starts.
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MaintenanceTasks
|
4
|
+
# Class that represents the data related to a Task. Such information can be
|
5
|
+
# sourced from a Task or from existing Run records for a Task that was since
|
6
|
+
# deleted. This class contains higher-level information about the Task, such
|
7
|
+
# as its status and category.
|
8
|
+
#
|
9
|
+
# Instances of this class replace a Task class instance in cases where we
|
10
|
+
# don't need the actual Task subclass.
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class TaskDataIndex
|
14
|
+
class << self
|
15
|
+
# Returns a list of sorted Task Data objects that represent the
|
16
|
+
# available Tasks.
|
17
|
+
#
|
18
|
+
# Tasks are sorted by category, and within a category, by Task name.
|
19
|
+
# Determining a Task's category requires their latest Run records.
|
20
|
+
# Two queries are done to get the currently active and completed Run
|
21
|
+
# records, and Task Data instances are initialized with these related run
|
22
|
+
# values.
|
23
|
+
#
|
24
|
+
# @return [Array<TaskDataIndex>] the list of Task Data.
|
25
|
+
def available_tasks
|
26
|
+
tasks = []
|
27
|
+
|
28
|
+
task_names = Task.available_tasks.map(&:name)
|
29
|
+
|
30
|
+
active_runs = Run.with_attached_csv.active.where(task_name: task_names)
|
31
|
+
active_runs.each do |run|
|
32
|
+
tasks << TaskDataIndex.new(run.task_name, run)
|
33
|
+
task_names.delete(run.task_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
completed_runs = Run.completed.where(task_name: task_names)
|
37
|
+
last_runs = Run.with_attached_csv.where(id: completed_runs.select("MAX(id) as id").group(:task_name))
|
38
|
+
task_names.map do |task_name|
|
39
|
+
last_run = last_runs.find { |run| run.task_name == task_name }
|
40
|
+
tasks << TaskDataIndex.new(task_name, last_run)
|
41
|
+
end
|
42
|
+
|
43
|
+
# We add an additional sorting key (status) to avoid possible
|
44
|
+
# inconsistencies across database adapters when a Task has
|
45
|
+
# multiple active Runs.
|
46
|
+
tasks.sort_by! { |task| [task.name, task.status] }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Initializes a Task Data with a name and optionally a related run.
|
51
|
+
#
|
52
|
+
# @param name [String] the name of the Task subclass.
|
53
|
+
# @param related_run [MaintenanceTasks::Run] optionally, a Run record to
|
54
|
+
# set for the Task.
|
55
|
+
def initialize(name, related_run = nil)
|
56
|
+
@name = name
|
57
|
+
@related_run = related_run
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [String] the name of the Task.
|
61
|
+
attr_reader :name
|
62
|
+
attr_reader :related_run
|
63
|
+
|
64
|
+
alias_method :to_s, :name
|
65
|
+
|
66
|
+
# Returns the status of the latest active or completed Run, if present.
|
67
|
+
# If the Task does not have any Runs, the Task status is `new`.
|
68
|
+
#
|
69
|
+
# @return [String] the Task status.
|
70
|
+
def status
|
71
|
+
related_run&.status || "new"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Retrieves the Task's category, which is one of active, new, or completed.
|
75
|
+
#
|
76
|
+
# @return [Symbol] the category of the Task.
|
77
|
+
def category
|
78
|
+
if related_run.present? && related_run.active?
|
79
|
+
:active
|
80
|
+
elsif related_run.nil?
|
81
|
+
:new
|
82
|
+
else
|
83
|
+
:completed
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -3,13 +3,14 @@
|
|
3
3
|
module MaintenanceTasks
|
4
4
|
# Class that represents the data related to a Task. Such information can be
|
5
5
|
# sourced from a Task or from existing Run records for a Task that was since
|
6
|
-
# deleted.
|
6
|
+
# deleted. This class contains detailed information such as the source code,
|
7
|
+
# associated runs, parameters, etc.
|
7
8
|
#
|
8
9
|
# Instances of this class replace a Task class instance in cases where we
|
9
10
|
# don't need the actual Task subclass.
|
10
11
|
#
|
11
12
|
# @api private
|
12
|
-
class
|
13
|
+
class TaskDataShow
|
13
14
|
class << self
|
14
15
|
# Initializes a Task Data by name, raising if the Task does not exist.
|
15
16
|
#
|
@@ -19,52 +20,26 @@ module MaintenanceTasks
|
|
19
20
|
# non-existent since we don't have interesting data to show.
|
20
21
|
#
|
21
22
|
# @param name [String] the name of the Task subclass.
|
22
|
-
# @return [
|
23
|
+
# @return [TaskDataShow] a Task Data instance.
|
23
24
|
# @raise [Task::NotFoundError] if the Task does not exist and doesn't have
|
24
25
|
# a Run.
|
25
26
|
def find(name)
|
26
27
|
task_data = new(name)
|
27
|
-
task_data.
|
28
|
+
task_data.active_runs.load
|
29
|
+
task_data.has_any_run? || Task.named(name)
|
28
30
|
task_data
|
29
31
|
end
|
30
|
-
|
31
|
-
# Returns a list of sorted Task Data objects that represent the
|
32
|
-
# available Tasks.
|
33
|
-
#
|
34
|
-
# Tasks are sorted by category, and within a category, by Task name.
|
35
|
-
# Determining a Task's category require its latest Run record.
|
36
|
-
# To optimize calls to the database, a single query is done to get the
|
37
|
-
# last Run for each Task, and Task Data instances are initialized with
|
38
|
-
# these last_run values.
|
39
|
-
#
|
40
|
-
# @return [Array<TaskData>] the list of Task Data.
|
41
|
-
def available_tasks
|
42
|
-
task_names = Task.available_tasks.map(&:name)
|
43
|
-
available_task_runs = Run.where(task_name: task_names)
|
44
|
-
last_runs = Run.with_attached_csv.where(
|
45
|
-
id: available_task_runs.select("MAX(id) as id").group(:task_name)
|
46
|
-
)
|
47
|
-
|
48
|
-
task_names.map do |task_name|
|
49
|
-
last_run = last_runs.find { |run| run.task_name == task_name }
|
50
|
-
TaskData.new(task_name, last_run)
|
51
|
-
end.sort_by!(&:name)
|
52
|
-
end
|
53
32
|
end
|
54
33
|
|
55
|
-
# Initializes a Task Data with a name and optionally a
|
34
|
+
# Initializes a Task Data with a name and optionally a related run.
|
56
35
|
#
|
57
36
|
# @param name [String] the name of the Task subclass.
|
58
|
-
|
59
|
-
# set for the Task.
|
60
|
-
def initialize(name, last_run = :none_passed)
|
37
|
+
def initialize(name)
|
61
38
|
@name = name
|
62
|
-
@last_run = last_run unless last_run == :none_passed
|
63
39
|
end
|
64
40
|
|
65
41
|
# @return [String] the name of the Task.
|
66
42
|
attr_reader :name
|
67
|
-
|
68
43
|
alias_method :to_s, :name
|
69
44
|
|
70
45
|
# The Task's source code.
|
@@ -83,27 +58,23 @@ module MaintenanceTasks
|
|
83
58
|
File.read(file)
|
84
59
|
end
|
85
60
|
|
86
|
-
#
|
61
|
+
# Returns the set of currently active Run records associated with the Task.
|
87
62
|
#
|
88
|
-
# @return [MaintenanceTasks::Run] the
|
89
|
-
#
|
90
|
-
def
|
91
|
-
|
92
|
-
|
93
|
-
@last_run = runs.first
|
63
|
+
# @return [ActiveRecord::Relation<MaintenanceTasks::Run>] the relation of
|
64
|
+
# active Run records.
|
65
|
+
def active_runs
|
66
|
+
@active_runs ||= runs.active
|
94
67
|
end
|
95
68
|
|
96
|
-
# Returns the set of Run records associated with the Task
|
97
|
-
#
|
98
|
-
#
|
99
|
-
# primarily from
|
69
|
+
# Returns the set of completed Run records associated with the Task.
|
70
|
+
# This collection represents a historic of past Runs for information
|
71
|
+
# purposes, since the base for Task Data information comes
|
72
|
+
# primarily from currently active runs.
|
100
73
|
#
|
101
74
|
# @return [ActiveRecord::Relation<MaintenanceTasks::Run>] the relation of
|
102
|
-
#
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
runs.where.not(id: last_run.id)
|
75
|
+
# completed Run records.
|
76
|
+
def completed_runs
|
77
|
+
@completed_runs ||= runs.completed
|
107
78
|
end
|
108
79
|
|
109
80
|
# @return [Boolean] whether the Task has been deleted.
|
@@ -114,27 +85,6 @@ module MaintenanceTasks
|
|
114
85
|
true
|
115
86
|
end
|
116
87
|
|
117
|
-
# The Task status. It returns the status of the last Run, if present. If the
|
118
|
-
# Task does not have any Runs, the Task status is `new`.
|
119
|
-
#
|
120
|
-
# @return [String] the Task status.
|
121
|
-
def status
|
122
|
-
last_run&.status || "new"
|
123
|
-
end
|
124
|
-
|
125
|
-
# Retrieves the Task's category, which is one of active, new, or completed.
|
126
|
-
#
|
127
|
-
# @return [Symbol] the category of the Task.
|
128
|
-
def category
|
129
|
-
if last_run.present? && last_run.active?
|
130
|
-
:active
|
131
|
-
elsif last_run.nil?
|
132
|
-
:new
|
133
|
-
else
|
134
|
-
:completed
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
88
|
# @return [Boolean] whether the Task inherits from CsvTask.
|
139
89
|
def csv_task?
|
140
90
|
!deleted? && Task.named(name).has_csv_content?
|
@@ -157,6 +107,12 @@ module MaintenanceTasks
|
|
157
107
|
MaintenanceTasks::Task.named(name).new
|
158
108
|
end
|
159
109
|
|
110
|
+
# @return [Boolean] whether the Task has any Run.
|
111
|
+
# @api private
|
112
|
+
def has_any_run?
|
113
|
+
active_runs.any? || completed_runs.any?
|
114
|
+
end
|
115
|
+
|
160
116
|
private
|
161
117
|
|
162
118
|
def runs
|
@@ -81,7 +81,7 @@ module MaintenanceTasks
|
|
81
81
|
def add_invalid_status_error(record, previous_status, new_status)
|
82
82
|
record.errors.add(
|
83
83
|
:status,
|
84
|
-
"Cannot transition run from status #{previous_status} to #{new_status}"
|
84
|
+
"Cannot transition run from status #{previous_status} to #{new_status}",
|
85
85
|
)
|
86
86
|
end
|
87
87
|
end
|
@@ -17,4 +17,21 @@
|
|
17
17
|
<%= render "maintenance_tasks/runs/csv", run: run %>
|
18
18
|
<%= tag.hr if run.csv_file.present? && run.arguments.present? %>
|
19
19
|
<%= render "maintenance_tasks/runs/arguments", run: run %>
|
20
|
+
|
21
|
+
<div class="buttons">
|
22
|
+
<% if run.paused? %>
|
23
|
+
<%= button_to 'Resume', resume_task_run_path(@task, run), method: :put, class: 'button is-primary', disabled: @task.deleted? %>
|
24
|
+
<%= button_to 'Cancel', cancel_task_run_path(@task, run), method: :put, class: 'button is-danger' %>
|
25
|
+
<% elsif run.cancelling? %>
|
26
|
+
<% if run.stuck? %>
|
27
|
+
<%= button_to 'Cancel', cancel_task_run_path(@task, run), method: :put, class: 'button is-danger', disabled: @task.deleted? %>
|
28
|
+
<% end %>
|
29
|
+
<% elsif run.pausing? %>
|
30
|
+
<%= button_to 'Pausing', pause_task_run_path(@task, run), method: :put, class: 'button is-warning', disabled: true %>
|
31
|
+
<%= button_to 'Cancel', cancel_task_run_path(@task, run), method: :put, class: 'button is-danger' %>
|
32
|
+
<% elsif run.active? %>
|
33
|
+
<%= button_to 'Pause', pause_task_run_path(@task, run), method: :put, class: 'button is-warning', disabled: @task.deleted? %>
|
34
|
+
<%= button_to 'Cancel', cancel_task_run_path(@task, run), method: :put, class: 'button is-danger' %>
|
35
|
+
<% end%>
|
36
|
+
</div>
|
20
37
|
</div>
|
@@ -1,81 +1,51 @@
|
|
1
1
|
<% content_for :page_title, @task %>
|
2
2
|
|
3
3
|
<h1 class="title is-1">
|
4
|
-
<%= @task %>
|
4
|
+
<%= @task %>
|
5
5
|
</h1>
|
6
6
|
|
7
|
-
<% last_run = @task.last_run %>
|
8
|
-
<% if last_run %>
|
9
|
-
<h5 class="title is-5">
|
10
|
-
<%= time_tag last_run.created_at, title: last_run.created_at %>
|
11
|
-
</h5>
|
12
|
-
|
13
|
-
<%= progress last_run %>
|
14
|
-
|
15
|
-
<div class="content">
|
16
|
-
<%= render "maintenance_tasks/runs/info/#{last_run.status}", run: last_run %>
|
17
|
-
</div>
|
18
|
-
|
19
|
-
<div class="content" id="custom-content">
|
20
|
-
<%= render "maintenance_tasks/runs/info/custom", run: last_run %>
|
21
|
-
</div>
|
22
|
-
|
23
|
-
<%= render "maintenance_tasks/runs/csv", run: last_run %>
|
24
|
-
<%= tag.hr if last_run.csv_file.present? %>
|
25
|
-
<%= render "maintenance_tasks/runs/arguments", run: last_run %>
|
26
|
-
<%= tag.hr if last_run.arguments.present? %>
|
27
|
-
<% end %>
|
28
|
-
|
29
7
|
<div class="buttons">
|
30
|
-
|
31
|
-
|
32
|
-
<% if @task.csv_task? %>
|
33
|
-
<div class="block">
|
34
|
-
<%= form.label :csv_file %>
|
35
|
-
<%= form.file_field :csv_file %>
|
36
|
-
</div>
|
37
|
-
<% end %>
|
38
|
-
<% parameter_names = @task.parameter_names %>
|
39
|
-
<% if parameter_names.any? %>
|
40
|
-
<div class="block">
|
41
|
-
<%= form.fields_for :task_arguments, @task.new do |ff| %>
|
42
|
-
<% parameter_names.each do |parameter_name| %>
|
43
|
-
<div class="field">
|
44
|
-
<%= ff.label parameter_name, parameter_name, class: "label is-family-monospace" %>
|
45
|
-
<div class="control">
|
46
|
-
<%= parameter_field(ff, parameter_name) %>
|
47
|
-
</div>
|
48
|
-
</div>
|
49
|
-
<% end %>
|
50
|
-
<% end %>
|
51
|
-
</div>
|
52
|
-
<% end %>
|
53
|
-
<%= render "maintenance_tasks/tasks/custom", form: form %>
|
8
|
+
<%= form_with url: task_runs_path(@task), method: :post do |form| %>
|
9
|
+
<% if @task.csv_task? %>
|
54
10
|
<div class="block">
|
55
|
-
<%= form.
|
11
|
+
<%= form.label :csv_file %>
|
12
|
+
<%= form.file_field :csv_file, accept: "text/csv" %>
|
56
13
|
</div>
|
57
14
|
<% end %>
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
15
|
+
<% parameter_names = @task.parameter_names %>
|
16
|
+
<% if parameter_names.any? %>
|
17
|
+
<div class="block">
|
18
|
+
<%= form.fields_for :task_arguments, @task.new do |ff| %>
|
19
|
+
<% parameter_names.each do |parameter_name| %>
|
20
|
+
<div class="field">
|
21
|
+
<%= ff.label parameter_name, parameter_name, class: "label is-family-monospace" %>
|
22
|
+
<div class="control">
|
23
|
+
<%= parameter_field(ff, parameter_name) %>
|
24
|
+
</div>
|
25
|
+
</div>
|
26
|
+
<% end %>
|
27
|
+
<% end %>
|
28
|
+
</div>
|
62
29
|
<% end %>
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
<%= button_to 'Cancel', cancel_task_run_path(@task, last_run), method: :put, class: 'button is-danger' %>
|
69
|
-
<% else %>
|
70
|
-
<%= button_to 'Pause', pause_task_run_path(@task, last_run), method: :put, class: 'button is-warning', disabled: @task.deleted? %>
|
71
|
-
<%= button_to 'Cancel', cancel_task_run_path(@task, last_run), method: :put, class: 'button is-danger' %>
|
72
|
-
<% end%>
|
30
|
+
<%= render "maintenance_tasks/tasks/custom", form: form %>
|
31
|
+
<div class="block">
|
32
|
+
<%= form.submit 'Run', class: "button is-success", disabled: @task.deleted? %>
|
33
|
+
</div>
|
34
|
+
<% end %>
|
73
35
|
</div>
|
74
36
|
|
75
37
|
<% if (code = @task.code) %>
|
76
38
|
<pre><code><%= highlight_code(code) %></code></pre>
|
77
39
|
<% end %>
|
78
40
|
|
41
|
+
<% if @active_runs.any? %>
|
42
|
+
<hr/>
|
43
|
+
|
44
|
+
<h4 class="title is-4">Active Runs</h4>
|
45
|
+
|
46
|
+
<%= render @active_runs %>
|
47
|
+
<% end %>
|
48
|
+
|
79
49
|
<% if @runs_page.records.present? %>
|
80
50
|
<hr/>
|
81
51
|
|
data/config/routes.rb
CHANGED
@@ -2,14 +2,11 @@
|
|
2
2
|
|
3
3
|
MaintenanceTasks::Engine.routes.draw do
|
4
4
|
resources :tasks, only: [:index, :show], format: false do
|
5
|
-
|
6
|
-
put "run"
|
7
|
-
end
|
8
|
-
|
9
|
-
resources :runs, only: [], format: false do
|
5
|
+
resources :runs, only: [:create], format: false do
|
10
6
|
member do
|
11
7
|
put "pause"
|
12
8
|
put "cancel"
|
9
|
+
put "resume"
|
13
10
|
end
|
14
11
|
end
|
15
12
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AddIndexOnTaskNameAndStatusToRuns < ActiveRecord::Migration[6.0]
|
4
|
+
def change
|
5
|
+
remove_index(:maintenance_tasks_runs,
|
6
|
+
column: [:task_name, :created_at], order: { created_at: :desc },
|
7
|
+
name: :index_maintenance_tasks_runs_on_task_name_and_created_at)
|
8
|
+
|
9
|
+
add_index(:maintenance_tasks_runs, [:task_name, :status, :created_at],
|
10
|
+
name: :index_maintenance_tasks_runs,
|
11
|
+
order: { created_at: :desc })
|
12
|
+
end
|
13
|
+
end
|
@@ -26,7 +26,7 @@ module MaintenanceTasks
|
|
26
26
|
template_file = File.join(
|
27
27
|
"app/tasks/#{tasks_module_file_path}",
|
28
28
|
class_path,
|
29
|
-
"#{file_name}_task.rb"
|
29
|
+
"#{file_name}_task.rb",
|
30
30
|
)
|
31
31
|
if options[:csv]
|
32
32
|
template("csv_task.rb", template_file)
|
@@ -56,7 +56,7 @@ module MaintenanceTasks
|
|
56
56
|
template_file = File.join(
|
57
57
|
"test/tasks/#{tasks_module_file_path}",
|
58
58
|
class_path,
|
59
|
-
"#{file_name}_task_test.rb"
|
59
|
+
"#{file_name}_task_test.rb",
|
60
60
|
)
|
61
61
|
template("task_test.rb", template_file)
|
62
62
|
end
|
@@ -65,7 +65,7 @@ module MaintenanceTasks
|
|
65
65
|
template_file = File.join(
|
66
66
|
"spec/tasks/#{tasks_module_file_path}",
|
67
67
|
class_path,
|
68
|
-
"#{file_name}_task_spec.rb"
|
68
|
+
"#{file_name}_task_spec.rb",
|
69
69
|
)
|
70
70
|
template("task_spec.rb", template_file)
|
71
71
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "stringio"
|
3
4
|
require "thor"
|
4
5
|
|
5
6
|
module MaintenanceTasks
|
@@ -27,7 +28,7 @@ module MaintenanceTasks
|
|
27
28
|
# Specify the CSV file to process for CSV Tasks
|
28
29
|
desc = "Supply a CSV file to be processed by a CSV Task, "\
|
29
30
|
"--csv path/to/csv/file.csv"
|
30
|
-
option :csv, desc: desc
|
31
|
+
option :csv, lazy_default: :stdin, desc: desc
|
31
32
|
# Specify arguments to supply to a Task supporting parameters
|
32
33
|
desc = "Supply arguments for a Task that accepts parameters as a set of "\
|
33
34
|
"<key>:<value> pairs."
|
@@ -52,10 +53,25 @@ module MaintenanceTasks
|
|
52
53
|
private
|
53
54
|
|
54
55
|
def csv_file
|
56
|
+
return unless options.key?(:csv)
|
57
|
+
|
55
58
|
csv_option = options[:csv]
|
56
|
-
|
57
|
-
|
59
|
+
|
60
|
+
if csv_option == :stdin
|
61
|
+
{
|
62
|
+
io: StringIO.new($stdin.read),
|
63
|
+
filename: "stdin.csv",
|
64
|
+
content_type: "text/csv",
|
65
|
+
}
|
66
|
+
else
|
67
|
+
{
|
68
|
+
io: File.open(csv_option),
|
69
|
+
filename: File.basename(csv_option),
|
70
|
+
content_type: "text/csv",
|
71
|
+
}
|
58
72
|
end
|
73
|
+
rescue Errno::ENOENT
|
74
|
+
raise ArgumentError, "CSV file not found: #{csv_option}"
|
59
75
|
end
|
60
76
|
end
|
61
77
|
end
|
@@ -10,29 +10,19 @@ module MaintenanceTasks
|
|
10
10
|
|
11
11
|
initializer "maintenance_tasks.warn_classic_autoloader" do
|
12
12
|
unless Rails.autoloaders.zeitwerk_enabled?
|
13
|
-
|
14
|
-
Autoloading in classic mode is
|
15
|
-
|
13
|
+
raise <<~MSG.squish
|
14
|
+
Autoloading in classic mode is not supported.
|
15
|
+
Please use Zeitwerk to autoload your application.
|
16
16
|
MSG
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
initializer "maintenance_tasks.eager_load_for_classic_autoloader" do
|
21
|
-
eager_load! unless Rails.autoloaders.zeitwerk_enabled?
|
22
|
-
end
|
23
|
-
|
24
20
|
initializer "maintenance_tasks.configs" do
|
25
21
|
MaintenanceTasks.backtrace_cleaner = Rails.backtrace_cleaner
|
26
22
|
end
|
27
23
|
|
28
24
|
config.to_prepare do
|
29
25
|
_ = TaskJobConcern # load this for JobIteration compatibility check
|
30
|
-
unless Rails.autoloaders.zeitwerk_enabled?
|
31
|
-
tasks_module = MaintenanceTasks.tasks_module.underscore
|
32
|
-
Dir["#{Rails.root}/app/tasks/#{tasks_module}/*.rb"].each do |file|
|
33
|
-
require_dependency(file)
|
34
|
-
end
|
35
|
-
end
|
36
26
|
end
|
37
27
|
|
38
28
|
config.after_initialize do
|
data/lib/maintenance_tasks.rb
CHANGED
@@ -63,27 +63,6 @@ module MaintenanceTasks
|
|
63
63
|
# use when cleaning a Run's backtrace.
|
64
64
|
mattr_accessor :backtrace_cleaner
|
65
65
|
|
66
|
-
# @private
|
67
|
-
def self.error_handler
|
68
|
-
return @error_handler if defined?(@error_handler)
|
69
|
-
|
70
|
-
@error_handler = ->(_error, _task_context, _errored_element) {}
|
71
|
-
end
|
72
|
-
|
73
|
-
# @private
|
74
|
-
def self.error_handler=(error_handler)
|
75
|
-
unless error_handler.arity == 3
|
76
|
-
ActiveSupport::Deprecation.warn(
|
77
|
-
"MaintenanceTasks.error_handler should be a lambda that takes three "\
|
78
|
-
"arguments: error, task_context, and errored_element."
|
79
|
-
)
|
80
|
-
@error_handler = ->(error, _task_context, _errored_element) do
|
81
|
-
error_handler.call(error)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
@error_handler = error_handler
|
85
|
-
end
|
86
|
-
|
87
66
|
# @!attribute error_handler
|
88
67
|
# @scope class
|
89
68
|
#
|
@@ -91,4 +70,6 @@ module MaintenanceTasks
|
|
91
70
|
# {file:README#label-Customizing+the+error+handler} for details.
|
92
71
|
#
|
93
72
|
# @return [Proc] the callback to perform when an error occurs in the Task.
|
73
|
+
mattr_accessor :error_handler, default:
|
74
|
+
->(_error, _task_context, _errored_element) {}
|
94
75
|
end
|
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:
|
4
|
+
version: 2.0.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-09-
|
11
|
+
date: 2022-09-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -106,7 +106,8 @@ files:
|
|
106
106
|
- app/models/maintenance_tasks/runner.rb
|
107
107
|
- app/models/maintenance_tasks/runs_page.rb
|
108
108
|
- app/models/maintenance_tasks/task.rb
|
109
|
-
- app/models/maintenance_tasks/
|
109
|
+
- app/models/maintenance_tasks/task_data_index.rb
|
110
|
+
- app/models/maintenance_tasks/task_data_show.rb
|
110
111
|
- app/models/maintenance_tasks/ticker.rb
|
111
112
|
- app/validators/maintenance_tasks/run_status_validator.rb
|
112
113
|
- app/views/layouts/maintenance_tasks/_navbar.html.erb
|
@@ -134,6 +135,7 @@ files:
|
|
134
135
|
- db/migrate/20210517131953_add_arguments_to_maintenance_tasks_runs.rb
|
135
136
|
- db/migrate/20211210152329_add_lock_version_to_maintenance_tasks_runs.rb
|
136
137
|
- db/migrate/20220706101937_change_runs_tick_columns_to_bigints.rb
|
138
|
+
- db/migrate/20220713131925_add_index_on_task_name_and_status_to_runs.rb
|
137
139
|
- exe/maintenance_tasks
|
138
140
|
- lib/generators/maintenance_tasks/install_generator.rb
|
139
141
|
- lib/generators/maintenance_tasks/task_generator.rb
|
@@ -152,7 +154,7 @@ homepage: https://github.com/Shopify/maintenance_tasks
|
|
152
154
|
licenses:
|
153
155
|
- MIT
|
154
156
|
metadata:
|
155
|
-
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/
|
157
|
+
source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.0.0
|
156
158
|
allowed_push_host: https://rubygems.org
|
157
159
|
post_install_message:
|
158
160
|
rdoc_options: []
|