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,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module MaintenanceTasks
|
3
|
+
# Module for common view helpers.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
module ApplicationHelper
|
7
|
+
include Pagy::Frontend
|
8
|
+
|
9
|
+
# Renders pagination for the page, if there is more than one page present.
|
10
|
+
#
|
11
|
+
# @param pagy [Pagy] the pagy instance containing pagination details,
|
12
|
+
# including the number of pages the results are spread across.
|
13
|
+
# @return [String] the HTML to render for pagination.
|
14
|
+
def pagination(pagy)
|
15
|
+
raw(pagy_bulma_nav(pagy)) if pagy.pages > 1
|
16
|
+
end
|
17
|
+
|
18
|
+
# Renders a time element with the given datetime, worded as relative to the
|
19
|
+
# current time.
|
20
|
+
#
|
21
|
+
# The ISO 8601 version of the datetime is shown on hover
|
22
|
+
# via a title attribute.
|
23
|
+
#
|
24
|
+
# @param datetime [ActiveSupport::TimeWithZone] the time to be presented.
|
25
|
+
# @return [String] the HTML to render with the relative datetime in words.
|
26
|
+
def time_ago(datetime)
|
27
|
+
time_tag(datetime, title: datetime.utc.iso8601, class: 'is-clickable') do
|
28
|
+
time_ago_in_words(datetime) + ' ago'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Fix stylesheet_link_tag to handle integrity when preloading.
|
33
|
+
# To be reverted once fixed upstream in Rails.
|
34
|
+
def stylesheet_link_tag(*sources)
|
35
|
+
return super unless respond_to?(:send_preload_links_header, true)
|
36
|
+
options = sources.extract_options!.stringify_keys
|
37
|
+
path_options = options.extract!('protocol', 'host', 'skip_pipeline')
|
38
|
+
.symbolize_keys
|
39
|
+
preload_links = []
|
40
|
+
crossorigin = options.delete('crossorigin')
|
41
|
+
crossorigin = 'anonymous' if crossorigin == true
|
42
|
+
nopush = options['nopush'].nil? ? true : options.delete('nopush')
|
43
|
+
integrity = options['integrity']
|
44
|
+
|
45
|
+
sources_tags = sources.uniq.map do |source|
|
46
|
+
href = path_to_stylesheet(source, path_options)
|
47
|
+
preload_link = "<#{href}>; rel=preload; as=style"
|
48
|
+
preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
|
49
|
+
preload_link += "; integrity=#{integrity}" unless integrity.nil?
|
50
|
+
preload_link += '; nopush' if nopush
|
51
|
+
preload_links << preload_link
|
52
|
+
tag_options = {
|
53
|
+
'rel' => 'stylesheet',
|
54
|
+
'media' => 'screen',
|
55
|
+
'crossorigin' => crossorigin,
|
56
|
+
'href' => href,
|
57
|
+
}.merge!(options)
|
58
|
+
tag(:link, tag_options)
|
59
|
+
end
|
60
|
+
|
61
|
+
send_preload_links_header(preload_links)
|
62
|
+
|
63
|
+
safe_join(sources_tags)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
private_constant :ApplicationHelper
|
67
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ripper'
|
4
|
+
|
5
|
+
module MaintenanceTasks
|
6
|
+
# Helpers for formatting data in the maintenance_tasks views.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
module TaskHelper
|
10
|
+
STATUS_COLOURS = {
|
11
|
+
'new' => ['is-primary'],
|
12
|
+
'enqueued' => ['is-primary is-light'],
|
13
|
+
'running' => ['is-info'],
|
14
|
+
'interrupted' => ['is-info', 'is-light'],
|
15
|
+
'pausing' => ['is-warning', 'is-light'],
|
16
|
+
'paused' => ['is-warning'],
|
17
|
+
'succeeded' => ['is-success'],
|
18
|
+
'cancelling' => ['is-light'],
|
19
|
+
'cancelled' => ['is-dark'],
|
20
|
+
'errored' => ['is-danger'],
|
21
|
+
}
|
22
|
+
|
23
|
+
# Formats a run backtrace.
|
24
|
+
#
|
25
|
+
# @param backtrace [Array<String>] the backtrace associated with an
|
26
|
+
# exception on a Task that ran and raised.
|
27
|
+
# @return [String] the parsed, HTML formatted version of the backtrace.
|
28
|
+
def format_backtrace(backtrace)
|
29
|
+
safe_join(backtrace.to_a, tag.br)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Renders the progress bar.
|
33
|
+
#
|
34
|
+
# The style of the progress tag depends on the Run status. It also renders
|
35
|
+
# an infinite progress when a Run is active but there is no total
|
36
|
+
# information to estimate completion.
|
37
|
+
#
|
38
|
+
# @param run [Run] the Run which the progress bar will be based on.
|
39
|
+
#
|
40
|
+
# @return [String] the progress information properly formatted.
|
41
|
+
# @return [nil] if the run has not started yet.
|
42
|
+
def progress(run)
|
43
|
+
return unless run.started?
|
44
|
+
|
45
|
+
progress = Progress.new(run)
|
46
|
+
|
47
|
+
tag.progress(
|
48
|
+
value: progress.value,
|
49
|
+
max: progress.max,
|
50
|
+
title: progress.title,
|
51
|
+
class: ['progress'] + STATUS_COLOURS.fetch(run.status)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Renders a span with a Run's status, with the corresponding tag class
|
56
|
+
# attached.
|
57
|
+
#
|
58
|
+
# @param status [String] the status for the Run.
|
59
|
+
# @return [String] the span element containing the status, with the
|
60
|
+
# appropriate tag class attached.
|
61
|
+
def status_tag(status)
|
62
|
+
tag.span(status.capitalize, class: ['tag'] + STATUS_COLOURS.fetch(status))
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the distance between now and the Run's expected completion time,
|
66
|
+
# if the Run has an estimated_completion_time.
|
67
|
+
#
|
68
|
+
# @param run [MaintenanceTasks::Run] the Run for which the estimated time to
|
69
|
+
# completion is being calculated.
|
70
|
+
# return [String, nil] the distance in words, or nil if the Run has no
|
71
|
+
# estimated completion time.
|
72
|
+
def estimated_time_to_completion(run)
|
73
|
+
estimated_completion_time = run.estimated_completion_time
|
74
|
+
if estimated_completion_time.present?
|
75
|
+
time_ago_in_words(estimated_completion_time)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Reports the approximate elapsed time a Run has been processed so far based
|
80
|
+
# on the Run's time running attribute.
|
81
|
+
#
|
82
|
+
# @param run [Run] the source of the time to be reported.
|
83
|
+
#
|
84
|
+
# @return [String] the description of the time running attribute.
|
85
|
+
def time_running_in_words(run)
|
86
|
+
distance_of_time_in_words(0, run.time_running, include_seconds: true)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Very simple syntax highlighter based on Ripper.
|
90
|
+
#
|
91
|
+
# It returns the same code except identifiers, keywords, etc. are wrapped
|
92
|
+
# in +<span>+ tags with CSS classes that match the types returned by
|
93
|
+
# Ripper.lex.
|
94
|
+
#
|
95
|
+
# @param code [String] the Ruby code source to syntax highlight.
|
96
|
+
# @return [ActiveSupport::SafeBuffer] HTML of the code.
|
97
|
+
def highlight_code(code)
|
98
|
+
tokens = Ripper.lex(code).map do |(_position, type, content, _state)|
|
99
|
+
case type
|
100
|
+
when :on_nl, :on_sp, :on_ignored_nl
|
101
|
+
content
|
102
|
+
else
|
103
|
+
tag.span(content, class: type.to_s.sub('on_', 'ruby-').dasherize)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
safe_join(tokens)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
private_constant :TaskHelper
|
110
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MaintenanceTasks
|
4
|
+
# Base class that is inherited by the host application's task classes.
|
5
|
+
class TaskJob < ActiveJob::Base
|
6
|
+
include JobIteration::Iteration
|
7
|
+
|
8
|
+
before_perform(:before_perform)
|
9
|
+
|
10
|
+
on_start(:on_start)
|
11
|
+
on_complete(:on_complete)
|
12
|
+
on_shutdown(:on_shutdown)
|
13
|
+
|
14
|
+
after_perform(:after_perform)
|
15
|
+
|
16
|
+
rescue_from StandardError, with: :on_error
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# Overrides ActiveJob::Exceptions.retry_on to declare it unsupported.
|
20
|
+
# The use of rescue_from prevents retry_on from being usable.
|
21
|
+
def retry_on(*, **)
|
22
|
+
raise NotImplementedError, 'retry_on is not supported'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def build_enumerator(_run, cursor:)
|
29
|
+
cursor ||= @run.cursor
|
30
|
+
collection = @task.collection
|
31
|
+
|
32
|
+
case collection
|
33
|
+
when ActiveRecord::Relation
|
34
|
+
enumerator_builder.active_record_on_records(collection, cursor: cursor)
|
35
|
+
when Array
|
36
|
+
enumerator_builder.build_array_enumerator(collection, cursor: cursor)
|
37
|
+
else
|
38
|
+
raise ArgumentError, "#{@task.class.name}#collection must be either "\
|
39
|
+
'an Active Record Relation or an Array.'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Performs task iteration logic for the current input returned by the
|
44
|
+
# enumerator.
|
45
|
+
#
|
46
|
+
# @param input [Object] the current element from the enumerator.
|
47
|
+
# @param _run [Run] the current Run, passed as an argument by Job Iteration.
|
48
|
+
def each_iteration(input, _run)
|
49
|
+
throw(:abort, :skip_complete_callbacks) if @run.stopping?
|
50
|
+
@task.process(input)
|
51
|
+
@ticker.tick
|
52
|
+
@run.reload_status
|
53
|
+
end
|
54
|
+
|
55
|
+
def before_perform
|
56
|
+
@run = arguments.first
|
57
|
+
@task = Task.named(@run.task_name).new
|
58
|
+
@run.job_id = job_id
|
59
|
+
|
60
|
+
@run.running! unless @run.stopping?
|
61
|
+
|
62
|
+
@ticker = Ticker.new(MaintenanceTasks.ticker_delay) do |ticks, duration|
|
63
|
+
@run.persist_progress(ticks, duration)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def on_start
|
68
|
+
@run.update!(started_at: Time.now, tick_total: @task.count)
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_complete
|
72
|
+
@run.status = :succeeded
|
73
|
+
@run.ended_at = Time.now
|
74
|
+
end
|
75
|
+
|
76
|
+
def on_shutdown
|
77
|
+
if @run.cancelling?
|
78
|
+
@run.status = :cancelled
|
79
|
+
@run.ended_at = Time.now
|
80
|
+
else
|
81
|
+
@run.status = @run.pausing? ? :paused : :interrupted
|
82
|
+
@run.cursor = cursor_position
|
83
|
+
end
|
84
|
+
|
85
|
+
@ticker.persist
|
86
|
+
end
|
87
|
+
|
88
|
+
def after_perform
|
89
|
+
@run.save!
|
90
|
+
end
|
91
|
+
|
92
|
+
def on_error(error)
|
93
|
+
@ticker.persist
|
94
|
+
@run.persist_error(error)
|
95
|
+
MaintenanceTasks.error_handler.call(error)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MaintenanceTasks
|
4
|
+
# This class generates progress information about a Run.
|
5
|
+
class Progress
|
6
|
+
include ActiveSupport::NumberHelper
|
7
|
+
|
8
|
+
# Sets the Progress initial state with a Run.
|
9
|
+
#
|
10
|
+
# @param run [Run] the source of progress information.
|
11
|
+
def initialize(run)
|
12
|
+
@run = run
|
13
|
+
end
|
14
|
+
|
15
|
+
# Defines the value of progress information. This represents the amount that
|
16
|
+
# is already done out of the progress maximum.
|
17
|
+
#
|
18
|
+
# For indefinite-style progress information, value is nil. That highlights
|
19
|
+
# that a Run is in progress but it is not possible to estimate how close to
|
20
|
+
# completion it is.
|
21
|
+
#
|
22
|
+
# When a Run is stopped, the value is present even if there is no total.
|
23
|
+
# That represents a progress information that assumes that the current value
|
24
|
+
# is also equal to is max, showing a progress as completed.
|
25
|
+
#
|
26
|
+
# @return [Integer] if progress can be determined or the Run is stopped.
|
27
|
+
# @return [nil] if progress can't be determined and the Run isn't stopped.
|
28
|
+
def value
|
29
|
+
@run.tick_count if estimatable? || @run.stopped?
|
30
|
+
end
|
31
|
+
|
32
|
+
# The maximum amount of work expected to be done. This is extracted from the
|
33
|
+
# Run's tick total attribute when present, or it is equal to the Run's
|
34
|
+
# tick count.
|
35
|
+
#
|
36
|
+
# This amount is enqual to the Run's tick count if the tick count is greater
|
37
|
+
# than the tick total. This represents that the total was underestimated.
|
38
|
+
#
|
39
|
+
# @return [Integer] the progress maximum amount.
|
40
|
+
def max
|
41
|
+
estimatable? ? @run.tick_total : @run.tick_count
|
42
|
+
end
|
43
|
+
|
44
|
+
# The title for the progress information. This is a text that describes the
|
45
|
+
# progress of the Run so far. It includes the percentage that is done out of
|
46
|
+
# the maximum, if an estimate is possible.
|
47
|
+
#
|
48
|
+
# @return [String] the title for the Run progress.
|
49
|
+
def title
|
50
|
+
if !total?
|
51
|
+
"Processed #{@run.tick_count} #{'item'.pluralize(@run.tick_count)}."
|
52
|
+
elsif @run.tick_count > @run.tick_total
|
53
|
+
"Processed #{@run.tick_count} #{'item'.pluralize(@run.tick_count)} " \
|
54
|
+
"(expected #{@run.tick_total})."
|
55
|
+
else
|
56
|
+
percentage = 100.0 * @run.tick_count / @run.tick_total
|
57
|
+
|
58
|
+
"Processed #{@run.tick_count} out of #{@run.tick_total} "\
|
59
|
+
"(#{number_to_percentage(percentage, precision: 0)})"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def total?
|
66
|
+
@run.tick_total.to_i > 0
|
67
|
+
end
|
68
|
+
|
69
|
+
def estimatable?
|
70
|
+
total? && @run.tick_total > @run.tick_count
|
71
|
+
end
|
72
|
+
end
|
73
|
+
private_constant :Progress
|
74
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module MaintenanceTasks
|
3
|
+
# Model that persists information related to a task being run from the UI.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
class Run < ApplicationRecord
|
7
|
+
# Various statuses a run can be in.
|
8
|
+
STATUSES = [
|
9
|
+
:enqueued, # The task has been enqueued by the user.
|
10
|
+
:running, # The task is being performed by a job worker.
|
11
|
+
:succeeded, # The task finished without error.
|
12
|
+
:cancelling, # The task has been told to cancel but is finishing work.
|
13
|
+
:cancelled, # The user explicitly halted the task's execution.
|
14
|
+
:interrupted, # The task was interrupted by the job infrastructure.
|
15
|
+
:pausing, # The task has been told to pause but is finishing work.
|
16
|
+
:paused, # The task was paused in the middle of the run by the user.
|
17
|
+
:errored, # The task code produced an unhandled exception.
|
18
|
+
]
|
19
|
+
|
20
|
+
ACTIVE_STATUSES = [
|
21
|
+
:enqueued,
|
22
|
+
:running,
|
23
|
+
:paused,
|
24
|
+
:pausing,
|
25
|
+
:cancelling,
|
26
|
+
:interrupted,
|
27
|
+
]
|
28
|
+
COMPLETED_STATUSES = [:succeeded, :errored, :cancelled]
|
29
|
+
COMPLETED_RUNS_LIMIT = 10
|
30
|
+
|
31
|
+
enum status: STATUSES.to_h { |status| [status, status.to_s] }
|
32
|
+
|
33
|
+
validates :task_name, on: :create, inclusion: { in: ->(_) {
|
34
|
+
Task.available_tasks.map(&:to_s)
|
35
|
+
} }
|
36
|
+
attr_readonly :task_name
|
37
|
+
|
38
|
+
serialize :backtrace
|
39
|
+
|
40
|
+
scope :active, -> { where(status: ACTIVE_STATUSES) }
|
41
|
+
|
42
|
+
validates_with RunStatusValidator, on: :update
|
43
|
+
|
44
|
+
# Sets the run status to enqueued, making sure the transition is validated
|
45
|
+
# in case it's already enqueued.
|
46
|
+
def enqueued!
|
47
|
+
status_will_change!
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
# Increments +tick_count+ by +number_of_ticks+ and +time_running+ by
|
52
|
+
# +duration+, both directly in the DB.
|
53
|
+
# The attribute values are not set in the current instance, you need
|
54
|
+
# to reload the record.
|
55
|
+
#
|
56
|
+
# @param number_of_ticks [Integer] number of ticks to add to tick_count.
|
57
|
+
# @param duration [Float] the time in seconds that elapsed since the last
|
58
|
+
# increment of ticks.
|
59
|
+
def persist_progress(number_of_ticks, duration)
|
60
|
+
self.class.update_counters(
|
61
|
+
id,
|
62
|
+
tick_count: number_of_ticks,
|
63
|
+
time_running: duration,
|
64
|
+
touch: true
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Marks the run as errored and persists the error data.
|
69
|
+
#
|
70
|
+
# @param error [StandardError] the Error being persisted.
|
71
|
+
def persist_error(error)
|
72
|
+
update!(
|
73
|
+
status: :errored,
|
74
|
+
error_class: error.class.to_s,
|
75
|
+
error_message: error.message,
|
76
|
+
backtrace: Rails.backtrace_cleaner.clean(error.backtrace),
|
77
|
+
ended_at: Time.now,
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Refreshes just the status attribute on the Active Record object, and
|
82
|
+
# ensures ActiveModel::Dirty does not mark the object as changed.
|
83
|
+
# This allows us to get the Run's most up-to-date status without needing
|
84
|
+
# to reload the entire record.
|
85
|
+
#
|
86
|
+
# @return [MaintenanceTasks::Run] the Run record with its updated status.
|
87
|
+
def reload_status
|
88
|
+
updated_status = Run.uncached do
|
89
|
+
Run.where(id: id).pluck(:status).first
|
90
|
+
end
|
91
|
+
self.status = updated_status
|
92
|
+
clear_attribute_changes([:status])
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns whether the Run is stopping, which is defined as
|
97
|
+
# having a status of pausing or cancelled.
|
98
|
+
#
|
99
|
+
# @return [Boolean] whether the Run is stopping.
|
100
|
+
def stopping?
|
101
|
+
pausing? || cancelling?
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns whether the Run is stopped, which is defined as having a status of
|
105
|
+
# paused, succeeded, cancelled, or errored.
|
106
|
+
#
|
107
|
+
# @return [Boolean] whether the Run is stopped.
|
108
|
+
def stopped?
|
109
|
+
completed? || paused?
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns whether the Run has been started, which is indicated by the
|
113
|
+
# started_at timestamp being present.
|
114
|
+
#
|
115
|
+
# @return [Boolean] whether the Run was started.
|
116
|
+
def started?
|
117
|
+
started_at.present?
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns whether the Run is completed, which is defined as
|
121
|
+
# having a status of succeeded, cancelled, or errored.
|
122
|
+
#
|
123
|
+
# @return [Boolean] whether the Run is completed.
|
124
|
+
def completed?
|
125
|
+
COMPLETED_STATUSES.include?(status.to_sym)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns whether the Run is active, which is defined as
|
129
|
+
# having a status of enqueued, running, pausing, cancelling,
|
130
|
+
# paused or interrupted.
|
131
|
+
#
|
132
|
+
# @return [Boolean] whether the Run is active.
|
133
|
+
def active?
|
134
|
+
ACTIVE_STATUSES.include?(status.to_sym)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns the estimated time the task will finish based on the the number of
|
138
|
+
# ticks left and the average time needed to process a tick.
|
139
|
+
# Returns nil if the Run is completed, or if the tick_count or tick_total is
|
140
|
+
# zero.
|
141
|
+
#
|
142
|
+
# @return [Time] the estimated time the Run will finish.
|
143
|
+
def estimated_completion_time
|
144
|
+
return if completed? || tick_count == 0 || tick_total.to_i == 0
|
145
|
+
|
146
|
+
processed_per_second = (tick_count.to_f / time_running)
|
147
|
+
ticks_left = (tick_total - tick_count)
|
148
|
+
seconds_to_finished = ticks_left / processed_per_second
|
149
|
+
Time.now + seconds_to_finished
|
150
|
+
end
|
151
|
+
|
152
|
+
# Cancels a Run.
|
153
|
+
#
|
154
|
+
# If the Run is paused, it will transition directly to cancelled, since the
|
155
|
+
# Task is not being performed. In this case, the ended_at timestamp
|
156
|
+
# will be updated.
|
157
|
+
#
|
158
|
+
# If the Run is not paused, the Run will transition to cancelling.
|
159
|
+
#
|
160
|
+
# If the Run is already cancelling, and has last been updated more than 5
|
161
|
+
# minutes ago, it will transition to cancelled, and the ended_at timestamp
|
162
|
+
# will be updated.
|
163
|
+
def cancel
|
164
|
+
if paused? || stuck?
|
165
|
+
update!(status: :cancelled, ended_at: Time.now)
|
166
|
+
else
|
167
|
+
cancelling!
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns whether a Run is stuck, which is defined as having a status of
|
172
|
+
# cancelling, and not having been updated in the last 5 minutes.
|
173
|
+
#
|
174
|
+
# @return [Boolean] whether the Run is stuck.
|
175
|
+
def stuck?
|
176
|
+
cancelling? && updated_at <= 5.minutes.ago
|
177
|
+
end
|
178
|
+
end
|
179
|
+
private_constant :Run
|
180
|
+
end
|