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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +58 -21
  3. data/app/controllers/maintenance_tasks/application_controller.rb +1 -2
  4. data/app/controllers/maintenance_tasks/runs_controller.rb +29 -1
  5. data/app/controllers/maintenance_tasks/tasks_controller.rb +5 -22
  6. data/app/helpers/maintenance_tasks/tasks_helper.rb +2 -2
  7. data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +5 -5
  8. data/app/models/maintenance_tasks/batch_csv_collection_builder.rb +1 -1
  9. data/app/models/maintenance_tasks/run.rb +17 -3
  10. data/app/models/maintenance_tasks/runner.rb +18 -10
  11. data/app/models/maintenance_tasks/runs_page.rb +10 -4
  12. data/app/models/maintenance_tasks/task.rb +7 -13
  13. data/app/models/maintenance_tasks/task_data_index.rb +87 -0
  14. data/app/models/maintenance_tasks/{task_data.rb → task_data_show.rb} +26 -70
  15. data/app/validators/maintenance_tasks/run_status_validator.rb +1 -1
  16. data/app/views/maintenance_tasks/runs/_run.html.erb +17 -0
  17. data/app/views/maintenance_tasks/tasks/_task.html.erb +1 -1
  18. data/app/views/maintenance_tasks/tasks/show.html.erb +32 -62
  19. data/config/routes.rb +2 -5
  20. data/db/migrate/20220713131925_add_index_on_task_name_and_status_to_runs.rb +13 -0
  21. data/lib/generators/maintenance_tasks/task_generator.rb +3 -3
  22. data/lib/generators/maintenance_tasks/templates/no_collection_task_test.rb.tt +1 -0
  23. data/lib/generators/maintenance_tasks/templates/task_spec.rb.tt +1 -0
  24. data/lib/generators/maintenance_tasks/templates/task_test.rb.tt +1 -0
  25. data/lib/maintenance_tasks/cli.rb +19 -3
  26. data/lib/maintenance_tasks/engine.rb +3 -13
  27. data/lib/maintenance_tasks.rb +2 -21
  28. metadata +6 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de43aad0e8d7479e1f7a88d8f664e20213d42455ca85ef22e074e2321b9a183b
4
- data.tar.gz: 65bae8e5b9f793e5cd4dc08dfee03611378007d571d5fc027b349faa7c358b74
3
+ metadata.gz: 60d043a961d97e4db5e822b86bf17a29fd409cf14e3951c581ecfb92abc7e2af
4
+ data.tar.gz: 5170cb9e4bb82c82fe31f07f0dba93ca28caeacffe2b68ec592a092623d6aafd
5
5
  SHA512:
6
- metadata.gz: 5b088335a8ef4a0e1e92d376f4c5c2d2c05ca2dea2e07aa983f21783ce5a7bff53cf3b4f2a75607d67fea144119078f563882f6e67c6fd09adc3d20ebac285a1
7
- data.tar.gz: '0128f6891b9b0328b7ba80490b4843a70eb014ddb82295d50f7d22218164ac3e9ef1e7c08bb80e8aab0a4501e932a1d374e755e7f21bd709c996f33b3344575f'
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
- * `count`: return the number of rows that will be iterated over (optional, to be
54
- able to show progress)
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 --csv option:
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
- To run a Task that takes arguments from the command line, use the --arguments
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/master/guides/best-practices.md#max-job-runtime
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). Before the worker threads are terminated, Sidekiq will try
585
- to re-enqueue the job so your Task will be resumed. However, the position in the
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. Ensure that these are kept, but remove any
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 = TaskData.available_tasks.group_by(&:category)
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 = TaskData.find(params.fetch(:id))
22
- set_refresh if @task.last_run&.active?
23
- @runs_page = RunsPage.new(@task.previous_runs, params[:cursor])
24
- end
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
- @enumerator = enumerator_builder.active_record_on_batch_relations(
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 = @enumerator&.size if count == :no_count
126
+ count = @collection_enum.size if count == :no_count
127
127
  @run.start(count)
128
128
  end
129
129
 
@@ -22,7 +22,7 @@ module MaintenanceTasks
22
22
  def collection(task)
23
23
  BatchCsv.new(
24
24
  csv: CSV.new(task.csv_content, headers: true),
25
- batch_size: @batch_size
25
+ batch_size: @batch_size,
26
26
  )
27
27
  end
28
28
 
@@ -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.active.find_by(task_name: name) ||
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
- # Runs dataset that is being paginated.
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
- @runs.unscope(:includes).pluck(:id).last == next_cursor
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
- self.collection_builder_strategy =
69
- BatchCsvCollectionBuilder.new(in_batches)
66
+ self.collection_builder_strategy = if in_batches
67
+ BatchCsvCollectionBuilder.new(in_batches)
70
68
  else
71
- self.collection_builder_strategy = CsvCollectionBuilder.new
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 TaskData
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 [TaskData] a Task Data instance.
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.last_run || Task.named(name)
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 last_run.
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
- # @param last_run [MaintenanceTasks::Run] optionally, a Run record to
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
- # Retrieves the latest Run associated with the Task.
61
+ # Returns the set of currently active Run records associated with the Task.
87
62
  #
88
- # @return [MaintenanceTasks::Run] the Run record.
89
- # @return [nil] if there are no Runs associated with the Task.
90
- def last_run
91
- return @last_run if defined?(@last_run)
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 previous to the
97
- # last Run. This collection represents a historic of past Runs for
98
- # information purposes, since the base for Task Data information comes
99
- # primarily from the last Run.
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
- # record previous to the last Run.
103
- def previous_runs
104
- return Run.none unless last_run
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>
@@ -4,7 +4,7 @@
4
4
  <%= status_tag(task.status) %>
5
5
  </h3>
6
6
 
7
- <% if (run = task.last_run) %>
7
+ <% if (run = task.related_run) %>
8
8
  <h5 class="title is-5">
9
9
  <%= time_tag run.created_at, title: run.created_at %>
10
10
  </h5>
@@ -1,81 +1,51 @@
1
1
  <% content_for :page_title, @task %>
2
2
 
3
3
  <h1 class="title is-1">
4
- <%= @task %> <%= status_tag(@task.status) %>
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
- <% if last_run.nil? || last_run.completed? %>
31
- <%= form_with url: run_task_path(@task), method: :put do |form| %>
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.submit 'Run', class: "button is-success", disabled: @task.deleted? %>
11
+ <%= form.label :csv_file %>
12
+ <%= form.file_field :csv_file, accept: "text/csv" %>
56
13
  </div>
57
14
  <% end %>
58
- <% elsif last_run.cancelling? %>
59
- <%= button_to 'Run', run_task_path(@task), method: :put, class: 'button is-success', disabled: true %>
60
- <% if last_run.stuck? %>
61
- <%= button_to 'Cancel', cancel_task_run_path(@task, last_run), method: :put, class: 'button is-danger', disabled: @task.deleted? %>
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
- <% elsif last_run.pausing? %>
64
- <%= button_to 'Pausing', pause_task_run_path(@task, last_run), method: :put, class: 'button is-warning', disabled: true %>
65
- <%= button_to 'Cancel', cancel_task_run_path(@task, last_run), method: :put, class: 'button is-danger' %>
66
- <% elsif last_run.paused? %>
67
- <%= button_to 'Resume', run_task_path(@task), method: :put, class: 'button is-primary', disabled: @task.deleted? %>
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
- member do
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,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "test_helper"
3
4
 
4
5
  module <%= tasks_module %>
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "rails_helper"
3
4
 
4
5
  module <%= tasks_module %>
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "test_helper"
3
4
 
4
5
  module <%= tasks_module %>
@@ -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
- if csv_option
57
- { io: File.open(csv_option), filename: File.basename(csv_option) }
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
- ActiveSupport::Deprecation.warn(<<~MSG.squish)
14
- Autoloading in classic mode is deprecated and support will be removed in the next
15
- release of Maintenance Tasks. Please use Zeitwerk to autoload your application.
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
@@ -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: 1.10.3
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-02 00:00:00.000000000 Z
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/task_data.rb
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/v1.10.3
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: []