maintenance_tasks 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +316 -0
- data/Rakefile +29 -0
- data/app/controllers/maintenance_tasks/application_controller.rb +26 -0
- data/app/controllers/maintenance_tasks/runs_controller.rb +44 -0
- data/app/controllers/maintenance_tasks/tasks_controller.rb +42 -0
- data/app/helpers/maintenance_tasks/application_helper.rb +67 -0
- data/app/helpers/maintenance_tasks/task_helper.rb +110 -0
- data/app/jobs/maintenance_tasks/task_job.rb +98 -0
- data/app/models/maintenance_tasks/application_record.rb +10 -0
- data/app/models/maintenance_tasks/progress.rb +74 -0
- data/app/models/maintenance_tasks/run.rb +180 -0
- data/app/models/maintenance_tasks/runner.rb +52 -0
- data/app/models/maintenance_tasks/task_data.rb +137 -0
- data/app/models/maintenance_tasks/ticker.rb +58 -0
- data/app/tasks/maintenance_tasks/task.rb +83 -0
- data/app/validators/maintenance_tasks/run_status_validator.rb +86 -0
- data/app/views/layouts/maintenance_tasks/_navbar.html.erb +11 -0
- data/app/views/layouts/maintenance_tasks/application.html.erb +54 -0
- data/app/views/maintenance_tasks/runs/_info.html.erb +10 -0
- data/app/views/maintenance_tasks/runs/_run.html.erb +11 -0
- data/app/views/maintenance_tasks/runs/index.html.erb +15 -0
- data/app/views/maintenance_tasks/runs/info/_cancelled.html.erb +4 -0
- data/app/views/maintenance_tasks/runs/info/_cancelling.html.erb +1 -0
- data/app/views/maintenance_tasks/runs/info/_enqueued.html.erb +1 -0
- data/app/views/maintenance_tasks/runs/info/_errored.html.erb +25 -0
- data/app/views/maintenance_tasks/runs/info/_interrupted.html.erb +1 -0
- data/app/views/maintenance_tasks/runs/info/_paused.html.erb +8 -0
- data/app/views/maintenance_tasks/runs/info/_pausing.html.erb +1 -0
- data/app/views/maintenance_tasks/runs/info/_running.html.erb +7 -0
- data/app/views/maintenance_tasks/runs/info/_succeeded.html.erb +5 -0
- data/app/views/maintenance_tasks/tasks/_task.html.erb +8 -0
- data/app/views/maintenance_tasks/tasks/actions/_cancelled.html.erb +1 -0
- data/app/views/maintenance_tasks/tasks/actions/_cancelling.html.erb +4 -0
- data/app/views/maintenance_tasks/tasks/actions/_enqueued.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_errored.html.erb +1 -0
- data/app/views/maintenance_tasks/tasks/actions/_interrupted.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_new.html.erb +1 -0
- data/app/views/maintenance_tasks/tasks/actions/_paused.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_pausing.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_running.html.erb +2 -0
- data/app/views/maintenance_tasks/tasks/actions/_succeeded.html.erb +1 -0
- data/app/views/maintenance_tasks/tasks/index.html.erb +22 -0
- data/app/views/maintenance_tasks/tasks/show.html.erb +25 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20201211151756_create_maintenance_tasks_runs.rb +22 -0
- data/exe/maintenance_tasks +13 -0
- data/lib/generators/maintenance_tasks/install_generator.rb +22 -0
- data/lib/generators/maintenance_tasks/task_generator.rb +74 -0
- data/lib/generators/maintenance_tasks/templates/task.rb.tt +21 -0
- data/lib/generators/maintenance_tasks/templates/task_spec.rb.tt +14 -0
- data/lib/generators/maintenance_tasks/templates/task_test.rb.tt +12 -0
- data/lib/maintenance_tasks.rb +58 -0
- data/lib/maintenance_tasks/cli.rb +40 -0
- data/lib/maintenance_tasks/engine.rb +28 -0
- data/lib/maintenance_tasks/integrations/bugsnag_handler.rb +4 -0
- data/lib/tasks/maintenance_tasks_tasks.rake +5 -0
- 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 @@
|
|
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 @@
|
|
1
|
+
<p>Pausing…</p>
|
@@ -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 @@
|
|
1
|
+
<%= button_to 'Run', run_task_path(task), method: :put, class: 'button is-success', disabled: task.deleted? %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= button_to 'Run', run_task_path(task), method: :put, class: 'button is-success', disabled: task.deleted? %>
|
@@ -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 %>
|
data/config/routes.rb
ADDED
@@ -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,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
|