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