Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,16 @@ enabling this feature. See [upgrading](#upgrading) for more information.
MaintenanceTasks.serialize_cursors_as_json = true
```

#### Configure staleness threshold

To specify a staleness threshold date interval which will mark task runs as stale, you can
configure `MaintenanceTasks.task_staleness_threshold`. Tasks that have last run
successfully after this threshold will be marked stale.

The value for `MaintenanceTasks.task_staleness_threshold` must be an
`ActiveSupport::Duration`. If no value is specified, it will default to 30 days. This can be disabled
by setting `MaintenanceTasks.task_staleness_threshold` to `false`.

## Upgrading

Use bundler to check for and upgrade to newer versions. After installing a new
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/maintenance_tasks/tasks_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def progress(run)
def status_tag(status)
tag.span(
status.capitalize,
class: ["tag", "has-text-weight-medium", "pr-2", "mr-4"] + STATUS_COLOURS.fetch(status),
class: ["tag", "has-text-weight-medium", "px-2", "mx-4"] + STATUS_COLOURS.fetch(status),
)
end

Expand Down
11 changes: 11 additions & 0 deletions app/models/concerns/maintenance_tasks/run_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,17 @@ def configure_cursor_encoding!
self.cursor_is_json = true
end

# Returns whether the run is stale based on the staleness threshold.
#
# @return [Boolean]
def stale?
return false unless MaintenanceTasks.task_staleness_threshold.present?
return false unless succeeded?
return false unless ended_at.present?

ended_at < MaintenanceTasks.task_staleness_threshold.ago
end

private

def instrument_status_change
Expand Down
9 changes: 9 additions & 0 deletions app/models/maintenance_tasks/task_data_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ def initialize(name, related_run = nil)

alias_method :to_s, :name

# Delegates to the related run's stale? method when available.
#
# @return [Boolean] whether the related run is stale.
def stale?
return false unless related_run.present?

related_run.stale?
end

# Returns the status of the latest active or completed Run, if present.
# If the Task does not have any Runs, the Task status is `new`.
#
Expand Down
14 changes: 13 additions & 1 deletion app/views/maintenance_tasks/tasks/_task.html.erb
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
<div class="cell box">
<h3 class="title is-5 has-text-weight-medium">
<h3 class="title is-5 has-text-weight-medium is-flex is-align-items-center">
<%= link_to task, task_path(task) %>
<%= status_tag(task.status) %>
</h3>

<% if (run = task.related_run) %>
<% if task.stale? %>
<div class="content is-size-7 is-flex is-align-items-center has-text-warning">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icon is-small">
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>

<p class="ml-1">
This task last ran <%= MaintenanceTasks.task_staleness_threshold.inspect %> ago. Consider removing it as it may be stale.
</p>
</div>
<% end %>

<h5 class="title is-5 has-text-weight-medium">
<%= time_tag run.created_at, title: run.created_at.utc %>
</h5>
Expand Down
8 changes: 8 additions & 0 deletions lib/maintenance_tasks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ module MaintenanceTasks
# @return [Boolean] whether or not to store cursor values as JSON.
mattr_accessor :serialize_cursors_as_json, default: false

# @!attribute task_staleness_threshold
# @scope class
# The threshold after which a task is considered stale.
# Defaults to 30 days. Can be disabled by setting this to `false`.
#
# @return [ActiveSupport::Duration, false] time interval after which a task is considered stale.
mattr_accessor :task_staleness_threshold, default: 30.days

class << self
DEPRECATION_MESSAGE = "MaintenanceTasks.error_handler is deprecated and will be removed in the 3.0 release. " \
"Instead, reports will be sent to the Rails error reporter. Do not set a handler and subscribe " \
Expand Down
13 changes: 13 additions & 0 deletions test/dummy/app/tasks/maintenance/stale_task.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Maintenance
class StaleTask < MaintenanceTasks::Task
def collection
[1, 2]
end

def process(number)
Rails.logger.debug("This task is stale")
end
end
end
10 changes: 10 additions & 0 deletions test/fixtures/maintenance_tasks/runs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,13 @@ import_posts_task_succeeded_old:
created_at: '01 Jan 2020 00:20:00'
started_at: '01 Jan 2020 00:40:36'
ended_at: '01 Jan 2020 00:52:19'

stale_task:
task_name: Maintenance::StaleTask
tick_count: 10
tick_total: 10
job_id: '123abc'
status: 'succeeded'
created_at: '03 Mar 2026 00:20:00'
started_at: '03 Mar 2026 00:40:36'
ended_at: '03 Mar 2026 00:52:19'
2 changes: 1 addition & 1 deletion test/helpers/maintenance_tasks/tasks_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class TasksHelperTest < ActionView::TestCase
end

test "#status_tag renders a span with the appropriate tag based on status" do
expected = '<span class="tag has-text-weight-medium pr-2 mr-4 is-warning is-light">Pausing</span>'
expected = '<span class="tag has-text-weight-medium px-2 mx-4 is-warning is-light">Pausing</span>'
assert_equal expected, status_tag("pausing")
end

Expand Down
44 changes: 44 additions & 0 deletions test/models/maintenance_tasks/run_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,50 @@ class RunTest < ActiveSupport::TestCase
end
end

test "#stale? returns `false` when the run is not succeeded" do
MaintenanceTasks.with(task_staleness_threshold: 1.day) do
run = Run.create!(task_name: "Maintenance::UpdatePostsTask", ended_at: 2.days.ago, status: :running)
refute_predicate run, :stale?
end
end

test "#stale? returns `false` when the staleness threshold is disabled" do
MaintenanceTasks.with(task_staleness_threshold: false) do
run = Run.create!(task_name: "Maintenance::UpdatePostsTask", ended_at: 2.days.ago, status: :succeeded)
refute_predicate run, :stale?
end
end

test "#stale? returns `true` when the run is succeeded and beyond the default staleness threshold" do
run = Run.create!(
task_name: "Maintenance::UpdatePostsTask",
ended_at: (MaintenanceTasks.task_staleness_threshold + 1.day).ago,
status: :succeeded,
)
assert_predicate run, :stale?
end

test "#stale? returns `true` when the run is succeeded and beyond the staleness threshold" do
MaintenanceTasks.with(task_staleness_threshold: 1.day) do
run = Run.create!(task_name: "Maintenance::UpdatePostsTask", ended_at: 2.days.ago, status: :succeeded)
assert_predicate run, :stale?
end
end

test "#stale? returns `false` when the run is succeeded and within the staleness threshold" do
MaintenanceTasks.with(task_staleness_threshold: 2.day) do
run = Run.create!(task_name: "Maintenance::UpdatePostsTask", ended_at: 1.day.ago, status: :succeeded)
refute_predicate run, :stale?
end
end

test "#stale? returns `false` when the run is nil" do
MaintenanceTasks.with(task_staleness_threshold: 1.day) do
run = Run.new
refute_predicate run, :stale?
end
end

private

def expected_notification(run)
Expand Down
20 changes: 20 additions & 0 deletions test/models/maintenance_tasks/task_data_index_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class TaskDataIndexTest < ActiveSupport::TestCase
# duplicate due to fixtures containing two active runs of this task
"Maintenance::NoCollectionTask",
"Maintenance::ParamsTask",
"Maintenance::StaleTask",
"Maintenance::TestTask",
"Maintenance::UpdatePostsInBatchesTask",
"Maintenance::UpdatePostsModulePrependedTask",
Expand Down Expand Up @@ -51,6 +52,25 @@ class TaskDataIndexTest < ActiveSupport::TestCase
assert_equal "Maintenance::UpdatePostsTask", task_data.to_s
end

test "#stale? returns `true` for tasks outside of the staleness threshold for the related_run" do
MaintenanceTasks.with(task_staleness_threshold: 1.day) do
run = Run.create!(
task_name: "Maintenance::UpdatePostsTask",
ended_at: 2.days.ago,
status: :succeeded,
)
task_data = TaskDataIndex.new("Maintenance::UpdatePostsTask", run)
assert task_data.stale?
end
end

test "#stale? returns `false` for tasks with no related run" do
MaintenanceTasks.with(task_staleness_threshold: 1.day) do
task_data = TaskDataIndex.new("Maintenance::UpdatePostsTask", nil)
refute task_data.stale?
end
end

test "#status is new when Task does not have any Runs" do
task_data = TaskDataIndex.new("Maintenance::UpdatePostsTask")
assert_equal "new", task_data.status
Expand Down
1 change: 1 addition & 0 deletions test/models/maintenance_tasks/task_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class TaskTest < ActiveSupport::TestCase
"Maintenance::Nested::NestedTask",
"Maintenance::NoCollectionTask",
"Maintenance::ParamsTask",
"Maintenance::StaleTask",
"Maintenance::TestTask",
"Maintenance::UpdatePostsInBatchesTask",
"Maintenance::UpdatePostsModulePrependedTask",
Expand Down
16 changes: 16 additions & 0 deletions test/system/maintenance_tasks/tasks_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class TasksTest < ApplicationSystemTestCase
"Maintenance::UpdatePostsThrottledTask New",
"Completed Tasks",
"Maintenance::ImportPostsTask Succeeded",
"Maintenance::StaleTask Succeeded",
]

assert_equal expected, page.all("h3").map(&:text)
Expand Down Expand Up @@ -71,6 +72,21 @@ class TasksTest < ApplicationSystemTestCase
assert_text(/January 01, 2020 01:00 Succeeded #\d/)
end

test "show a Task with stale run" do
travel_to(maintenance_tasks_runs(:stale_task).ended_at + 2.days) do
MaintenanceTasks.with(task_staleness_threshold: 1.day) do
visit maintenance_tasks_path

within page
.find("a", text: "Maintenance::StaleTask")
.find(:xpath, "..")
.sibling(".has-text-warning") do
assert_text "This task last ran 1 day ago. Consider removing it as it may be stale."
end
end
end
end

test "task with attributes renders default values on the form" do
visit maintenance_tasks_path

Expand Down
Loading