maintenance_tasks 1.10.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|