maintenance_tasks 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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