maintenance_tasks 1.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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +316 -0
  3. data/Rakefile +29 -0
  4. data/app/controllers/maintenance_tasks/application_controller.rb +26 -0
  5. data/app/controllers/maintenance_tasks/runs_controller.rb +44 -0
  6. data/app/controllers/maintenance_tasks/tasks_controller.rb +42 -0
  7. data/app/helpers/maintenance_tasks/application_helper.rb +67 -0
  8. data/app/helpers/maintenance_tasks/task_helper.rb +110 -0
  9. data/app/jobs/maintenance_tasks/task_job.rb +98 -0
  10. data/app/models/maintenance_tasks/application_record.rb +10 -0
  11. data/app/models/maintenance_tasks/progress.rb +74 -0
  12. data/app/models/maintenance_tasks/run.rb +180 -0
  13. data/app/models/maintenance_tasks/runner.rb +52 -0
  14. data/app/models/maintenance_tasks/task_data.rb +137 -0
  15. data/app/models/maintenance_tasks/ticker.rb +58 -0
  16. data/app/tasks/maintenance_tasks/task.rb +83 -0
  17. data/app/validators/maintenance_tasks/run_status_validator.rb +86 -0
  18. data/app/views/layouts/maintenance_tasks/_navbar.html.erb +11 -0
  19. data/app/views/layouts/maintenance_tasks/application.html.erb +54 -0
  20. data/app/views/maintenance_tasks/runs/_info.html.erb +10 -0
  21. data/app/views/maintenance_tasks/runs/_run.html.erb +11 -0
  22. data/app/views/maintenance_tasks/runs/index.html.erb +15 -0
  23. data/app/views/maintenance_tasks/runs/info/_cancelled.html.erb +4 -0
  24. data/app/views/maintenance_tasks/runs/info/_cancelling.html.erb +1 -0
  25. data/app/views/maintenance_tasks/runs/info/_enqueued.html.erb +1 -0
  26. data/app/views/maintenance_tasks/runs/info/_errored.html.erb +25 -0
  27. data/app/views/maintenance_tasks/runs/info/_interrupted.html.erb +1 -0
  28. data/app/views/maintenance_tasks/runs/info/_paused.html.erb +8 -0
  29. data/app/views/maintenance_tasks/runs/info/_pausing.html.erb +1 -0
  30. data/app/views/maintenance_tasks/runs/info/_running.html.erb +7 -0
  31. data/app/views/maintenance_tasks/runs/info/_succeeded.html.erb +5 -0
  32. data/app/views/maintenance_tasks/tasks/_task.html.erb +8 -0
  33. data/app/views/maintenance_tasks/tasks/actions/_cancelled.html.erb +1 -0
  34. data/app/views/maintenance_tasks/tasks/actions/_cancelling.html.erb +4 -0
  35. data/app/views/maintenance_tasks/tasks/actions/_enqueued.html.erb +2 -0
  36. data/app/views/maintenance_tasks/tasks/actions/_errored.html.erb +1 -0
  37. data/app/views/maintenance_tasks/tasks/actions/_interrupted.html.erb +2 -0
  38. data/app/views/maintenance_tasks/tasks/actions/_new.html.erb +1 -0
  39. data/app/views/maintenance_tasks/tasks/actions/_paused.html.erb +2 -0
  40. data/app/views/maintenance_tasks/tasks/actions/_pausing.html.erb +2 -0
  41. data/app/views/maintenance_tasks/tasks/actions/_running.html.erb +2 -0
  42. data/app/views/maintenance_tasks/tasks/actions/_succeeded.html.erb +1 -0
  43. data/app/views/maintenance_tasks/tasks/index.html.erb +22 -0
  44. data/app/views/maintenance_tasks/tasks/show.html.erb +25 -0
  45. data/config/routes.rb +19 -0
  46. data/db/migrate/20201211151756_create_maintenance_tasks_runs.rb +22 -0
  47. data/exe/maintenance_tasks +13 -0
  48. data/lib/generators/maintenance_tasks/install_generator.rb +22 -0
  49. data/lib/generators/maintenance_tasks/task_generator.rb +74 -0
  50. data/lib/generators/maintenance_tasks/templates/task.rb.tt +21 -0
  51. data/lib/generators/maintenance_tasks/templates/task_spec.rb.tt +14 -0
  52. data/lib/generators/maintenance_tasks/templates/task_test.rb.tt +12 -0
  53. data/lib/maintenance_tasks.rb +58 -0
  54. data/lib/maintenance_tasks/cli.rb +40 -0
  55. data/lib/maintenance_tasks/engine.rb +28 -0
  56. data/lib/maintenance_tasks/integrations/bugsnag_handler.rb +4 -0
  57. data/lib/tasks/maintenance_tasks_tasks.rake +5 -0
  58. metadata +187 -0
@@ -0,0 +1,10 @@
1
+ <h5 class="title is-5">
2
+ <%= time_tag run.created_at, title: run.created_at %>
3
+ <%= status_tag run.status if with_status %>
4
+ </h5>
5
+
6
+ <%= progress run %>
7
+
8
+ <div class="content">
9
+ <%= render "maintenance_tasks/runs/info/#{run.status}", run: run %>
10
+ </div>
@@ -0,0 +1,11 @@
1
+ <div class="box">
2
+ <% if with_task_name %>
3
+ <h3 class="title is-3">
4
+ <%= link_to run.task_name, task_path(run.task_name) %>
5
+ <%= status_tag run.status %>
6
+ </h3>
7
+ <%= render 'maintenance_tasks/runs/info', run: run, with_status: false %>
8
+ <% else %>
9
+ <%= render 'maintenance_tasks/runs/info', run: run, with_status: true %>
10
+ <% end %>
11
+ </div>
@@ -0,0 +1,15 @@
1
+ <div class="block">
2
+ <%= form_with url: runs_path, method: :get do |form| %>
3
+ <div class="field has-addons">
4
+ <div class="control">
5
+ <%= form.search_field :task_name, value: params[:task_name], placeholder: "Task name", class: "input" %>
6
+ </div>
7
+ <div class="control">
8
+ <%= form.submit "Search", class: "button is-link" %>
9
+ </div>
10
+ </div>
11
+ <% end %>
12
+ </div>
13
+
14
+ <%= render @runs, with_task_name: true %>
15
+ <%= pagination(@pagy) %>
@@ -0,0 +1,4 @@
1
+ <p>
2
+ Ran for <%= time_running_in_words run %>,
3
+ cancelled <%= time_ago run.ended_at %>.
4
+ </p>
@@ -0,0 +1 @@
1
+ <p>Cancelling…</p>
@@ -0,0 +1 @@
1
+ <p>Waiting to start.</p>
@@ -0,0 +1,25 @@
1
+ <p>
2
+ Ran for <%= time_running_in_words run %> until an error happened
3
+ <%= time_ago run.ended_at %>.
4
+ </p>
5
+
6
+ </p>
7
+
8
+ <div class="card">
9
+ <header class="card-header">
10
+ <p class="card-header-title">
11
+ <%= run.error_class %>
12
+ </p>
13
+ </header>
14
+
15
+ <div class="card-content">
16
+ <div class="content">
17
+ <p><%= run.error_message %></p>
18
+
19
+ <% if run.backtrace.present? %>
20
+ <code><%= format_backtrace(run.backtrace) %></code>
21
+ <% end %>
22
+
23
+ </div>
24
+ </div>
25
+ </div>
@@ -0,0 +1 @@
1
+ <p>Interrupted momentarily, resuming shortly…</p>
@@ -0,0 +1,8 @@
1
+ <p>
2
+ Ran for <%= time_running_in_words run %> until paused,
3
+ <% if run.estimated_completion_time %>
4
+ <%= estimated_time_to_completion(run) %> remaining.
5
+ <% else %>
6
+ processed <%= pluralize run.tick_count, 'item' %> so far.
7
+ <% end %>
8
+ </p>
@@ -0,0 +1,7 @@
1
+ <p>
2
+ <% if run.estimated_completion_time %>
3
+ <%= estimated_time_to_completion(run).capitalize %> remaining.
4
+ <% else %>
5
+ Processed <%= pluralize run.tick_count, 'item' %> so far.
6
+ <% end %>
7
+ </p>
@@ -0,0 +1,5 @@
1
+ <p>
2
+ Ran for <%= time_running_in_words run %>,
3
+ finished <%= time_ago run.ended_at %>.
4
+ </p>
5
+
@@ -0,0 +1,8 @@
1
+ <div class="box">
2
+ <h3 class="title is-3">
3
+ <%= link_to task, task_path(task) %>
4
+ <%= status_tag(task.status) %>
5
+ </h3>
6
+
7
+ <%= render 'maintenance_tasks/runs/info', run: task.last_run, with_status: false if task.last_run %>
8
+ </div>
@@ -0,0 +1 @@
1
+ <%= button_to 'Run', run_task_path(task), method: :put, class: 'button is-success', disabled: task.deleted? %>
@@ -0,0 +1,4 @@
1
+ <%= button_to 'Run', run_task_path(task), method: :put, class: 'button is-success', disabled: true %>
2
+ <% if task.last_run.stuck? %>
3
+ <%= button_to 'Cancel', cancel_task_run_path(task, task.last_run), method: :put, class: 'button is-danger', disabled: task.deleted? %>
4
+ <% end %>
@@ -0,0 +1,2 @@
1
+ <%= button_to 'Pause', pause_task_run_path(task, task.last_run), method: :put, class: 'button is-warning', disabled: task.deleted? %>
2
+ <%= button_to 'Cancel', cancel_task_run_path(task, task.last_run), method: :put, class: 'button is-danger' %>
@@ -0,0 +1 @@
1
+ <%= button_to 'Run', run_task_path(task), method: :put, class: 'button is-success', disabled: task.deleted? %>
@@ -0,0 +1,2 @@
1
+ <%= button_to 'Pause', pause_task_run_path(task, task.last_run), method: :put, class: 'button is-warning', disabled: task.deleted? %>
2
+ <%= button_to 'Cancel', cancel_task_run_path(task, task.last_run), method: :put, class: 'button is-danger' %>
@@ -0,0 +1 @@
1
+ <%= button_to 'Run', run_task_path(task), method: :put, class: 'button is-success', disabled: task.deleted? %>
@@ -0,0 +1,2 @@
1
+ <%= button_to 'Resume', run_task_path(task), method: :put, class: 'button is-primary', disabled: task.deleted? %>
2
+ <%= button_to 'Cancel', cancel_task_run_path(task, task.last_run), method: :put, class: 'button is-danger' %>
@@ -0,0 +1,2 @@
1
+ <%= button_to 'Pausing', pause_task_run_path(task, task.last_run), method: :put, class: 'button is-warning', disabled: true %>
2
+ <%= button_to 'Cancel', cancel_task_run_path(task, task.last_run), method: :put, class: 'button is-danger', disabled: task.deleted? %>
@@ -0,0 +1,2 @@
1
+ <%= button_to 'Pause', pause_task_run_path(task, task.last_run), method: :put, class: 'button is-warning', disabled: task.deleted? %>
2
+ <%= button_to 'Cancel', cancel_task_run_path(task, task.last_run), method: :put, class: 'button is-danger' %>
@@ -0,0 +1 @@
1
+ <%= button_to 'Run', run_task_path(task), method: :put, class: 'button is-success', disabled: task.deleted? %>
@@ -0,0 +1,22 @@
1
+ <% if @available_tasks.empty? %>
2
+ <div class="content is-large">
3
+ <h3 class="title is-3"> The MaintenanceTasks gem has been successfully installed! </h3>
4
+ <p>
5
+ Any new Tasks will show up here. To start writing your first Task,
6
+ run <code>rails generate maintenance_tasks:task my_task</code>.
7
+ </p>
8
+ </div>
9
+ <% else %>
10
+ <% if active_tasks = @available_tasks[:active] %>
11
+ <h3 class="title is-3">Active Tasks</h3>
12
+ <%= render partial: 'task', collection: active_tasks %>
13
+ <% end %>
14
+ <% if new_tasks = @available_tasks[:new] %>
15
+ <h3 class="title is-3">New Tasks</h3>
16
+ <%= render partial: 'task', collection: new_tasks %>
17
+ <% end %>
18
+ <% if completed_tasks = @available_tasks[:completed] %>
19
+ <h3 class="title is-3">Completed Tasks</h3>
20
+ <%= render partial: 'task', collection: completed_tasks %>
21
+ <% end %>
22
+ <% end %>
@@ -0,0 +1,25 @@
1
+ <% content_for :page_title, @task %>
2
+
3
+ <h1 class="title is-1">
4
+ <%= @task %> <%= status_tag(@task.status) %>
5
+ </h1>
6
+
7
+ <%= render 'maintenance_tasks/runs/info', run: @task.last_run, with_status: false if @task.last_run %>
8
+
9
+ <div class="buttons">
10
+ <%= render "maintenance_tasks/tasks/actions/#{@task.status}", task: @task %>
11
+ </div>
12
+
13
+ <% if (code = @task.code) %>
14
+ <pre><code><%= highlight_code(code) %></code></pre>
15
+ <% end %>
16
+
17
+ <% if @previous_runs.present? %>
18
+ <hr/>
19
+
20
+ <h4 class="title is-4">Previous Runs</h4>
21
+
22
+ <%= render @previous_runs, with_task_name: false %>
23
+
24
+ <%= pagination(@pagy) %>
25
+ <% end %>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ MaintenanceTasks::Engine.routes.draw do
3
+ resources :runs, only: [:index], format: false
4
+
5
+ resources :tasks, only: [:index, :show], format: false do
6
+ member do
7
+ put 'run'
8
+ end
9
+
10
+ resources :runs, only: [], format: false do
11
+ member do
12
+ put 'pause'
13
+ put 'cancel'
14
+ end
15
+ end
16
+ end
17
+
18
+ root to: 'tasks#index'
19
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ class CreateMaintenanceTasksRuns < ActiveRecord::Migration[6.0]
3
+ def change
4
+ create_table(:maintenance_tasks_runs) do |t|
5
+ t.string(:task_name, null: false)
6
+ t.datetime(:started_at)
7
+ t.datetime(:ended_at)
8
+ t.float(:time_running, default: 0.0, null: false)
9
+ t.integer(:tick_count, default: 0, null: false)
10
+ t.integer(:tick_total)
11
+ t.string(:job_id)
12
+ t.bigint(:cursor)
13
+ t.string(:status, default: :enqueued, null: false)
14
+ t.string(:error_class)
15
+ t.string(:error_message)
16
+ t.text(:backtrace)
17
+ t.timestamps
18
+ t.index(:task_name)
19
+ t.index([:task_name, :created_at], order: { created_at: :desc })
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require File.expand_path('config/application', Dir.pwd)
6
+
7
+ Rails.application.require_environment!
8
+
9
+ require 'maintenance_tasks/cli'
10
+
11
+ module MaintenanceTasks
12
+ CLI.start(ARGV)
13
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ module MaintenanceTasks
3
+ # Generator used to set up the engine in the host application. It handles
4
+ # mounting the engine and installing migrations.
5
+ #
6
+ # @api private
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ # Mounts the engine in the host application's config/routes.rb
11
+ def mount_engine
12
+ route("mount MaintenanceTasks::Engine => '/maintenance_tasks'")
13
+ end
14
+
15
+ # Copies engine migrations to host application and migrates the database
16
+ def install_migrations
17
+ rake('maintenance_tasks:install:migrations')
18
+ rake('db:migrate')
19
+ end
20
+ end
21
+ private_constant :InstallGenerator
22
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaintenanceTasks
4
+ # Generator used for creating maintenance tasks in the host application.
5
+ #
6
+ # @api private
7
+ class TaskGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path('templates', __dir__)
9
+ desc 'This generator creates a task file at app/tasks and a corresponding '\
10
+ 'test.'
11
+
12
+ check_class_collision suffix: 'Task'
13
+
14
+ # Creates the Task file.
15
+ def create_task_file
16
+ template_file = File.join(
17
+ "app/tasks/#{tasks_module_file_path}",
18
+ class_path,
19
+ "#{file_name}_task.rb"
20
+ )
21
+ template('task.rb', template_file)
22
+ end
23
+
24
+ # Creates the Task test file, according to the app's test framework.
25
+ # A spec file is created if the app uses RSpec.
26
+ # Otherwise, an ActiveSupport::TestCase test is created.
27
+ def create_test_file
28
+ return unless test_framework
29
+
30
+ if test_framework == :rspec
31
+ create_task_spec_file
32
+ else
33
+ create_task_test_file
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def create_task_test_file
40
+ template_file = File.join(
41
+ "test/tasks/#{tasks_module_file_path}",
42
+ class_path,
43
+ "#{file_name}_task_test.rb"
44
+ )
45
+ template('task_test.rb', template_file)
46
+ end
47
+
48
+ def create_task_spec_file
49
+ template_file = File.join(
50
+ "spec/tasks/#{tasks_module_file_path}",
51
+ class_path,
52
+ "#{file_name}_task_spec.rb"
53
+ )
54
+ template('task_spec.rb', template_file)
55
+ end
56
+
57
+ def file_name
58
+ super.sub(/_task\z/i, '')
59
+ end
60
+
61
+ def tasks_module
62
+ MaintenanceTasks.tasks_module
63
+ end
64
+
65
+ def tasks_module_file_path
66
+ tasks_module.underscore
67
+ end
68
+
69
+ def test_framework
70
+ Rails.application.config.generators.options[:rails][:test_framework]
71
+ end
72
+ end
73
+ private_constant :TaskGenerator
74
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= tasks_module %>
4
+ <% module_namespacing do -%>
5
+ class <%= class_name %>Task < MaintenanceTasks::Task
6
+ def collection
7
+ # Collection to be iterated over
8
+ # Must be Active Record Relation or Array
9
+ end
10
+
11
+ def process(element)
12
+ # The work to be done in a single iteration of the task
13
+ end
14
+
15
+ def count
16
+ # Optionally, define the number of rows that will be iterated over
17
+ # This is used to track the task's progress
18
+ end
19
+ end
20
+ <% end -%>
21
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ require 'rails_helper'
3
+
4
+ module <%= tasks_module %>
5
+ <% module_namespacing do -%>
6
+ RSpec.describe <%= class_name %>Task do
7
+ # describe '#process' do
8
+ # it 'performs a task iteration' do
9
+ # <%= tasks_module %>::<%= class_name %>Task.new.process(element)
10
+ # end
11
+ # end
12
+ end
13
+ <% end -%>
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ require 'test_helper'
3
+
4
+ module <%= tasks_module %>
5
+ <% module_namespacing do -%>
6
+ class <%= class_name %>TaskTest < ActiveSupport::TestCase
7
+ # test "#process performs a task iteration" do
8
+ # <%= tasks_module %>::<%= class_name %>Task.new.process(element)
9
+ # end
10
+ end
11
+ <% end -%>
12
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ require 'action_controller'
3
+ require 'action_view'
4
+ require 'active_job'
5
+ require 'active_record'
6
+
7
+ require 'job-iteration'
8
+ require 'maintenance_tasks/engine'
9
+ require 'pagy'
10
+ require 'pagy/extras/bulma'
11
+
12
+ # The engine's namespace module. It provides isolation between the host
13
+ # application's code and the engine-specific code. Top-level engine constants
14
+ # and variables are defined under this module.
15
+ module MaintenanceTasks
16
+ # The module to namespace Tasks in, as a String. Defaults to 'Maintenance'.
17
+ # @param [String] the tasks_module value.
18
+ mattr_accessor :tasks_module, default: 'Maintenance'
19
+
20
+ # Defines the job to be used to perform Tasks. This job must be either
21
+ # `MaintenanceTasks::TaskJob` or a class that inherits from it.
22
+ #
23
+ # @param [String] the name of the job class.
24
+ mattr_writer :job, default: 'MaintenanceTasks::TaskJob'
25
+
26
+ # After each iteration, the progress of the task may be updated. This duration
27
+ # in seconds limits these updates, skipping if the duration since the last
28
+ # update is lower than this value, except if the job is interrupted, in which
29
+ # case the progress will always be recorded.
30
+ #
31
+ # @param [ActiveSupport::Duration, Numeric] Duration of the delay to update
32
+ # the ticker during Task iterations.
33
+ mattr_accessor :ticker_delay, default: 1.second
34
+
35
+ # Defines a callback to be performed when an error occurs in the task.
36
+ mattr_accessor :error_handler, default: ->(_error) {}
37
+
38
+ class << self
39
+ # Retrieves the class that is configured as the Task Job to be used to
40
+ # perform Tasks.
41
+ #
42
+ # @return [TaskJob] the job class.
43
+ def job
44
+ @@job.constantize
45
+ end
46
+
47
+ # Attempts to configure Bugsnag integration. If the application uses
48
+ # Bugsnag, it is automatically configured to report on errors raised while
49
+ # a Task is performing.
50
+ def configure_bugsnag_integration
51
+ load('maintenance_tasks/integrations/bugsnag_handler.rb')
52
+ rescue LoadError
53
+ nil
54
+ end
55
+ end
56
+ end
57
+
58
+ MaintenanceTasks.configure_bugsnag_integration