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.
- 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
|