Joffrey JAFFEUX 01ce003b8e
FIX: logs time even when automation raises (#32254)
The previous code could attempt to log a `nil` `run_time` if the block
would raise an exception. This commit adds two safeguards:

- rescue any exception to still compute `run_time`
- defaults to `0` if we still don't have any `run_time`
2025-04-10 11:05:53 +02:00

134 lines
4.2 KiB
Ruby

# frozen_string_literal: true
#
module DiscourseAutomation
class Stat < ActiveRecord::Base
self.table_name = "discourse_automation_stats"
def self.log(automation_id, run_time = nil)
if block_given? && run_time.nil?
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
begin
result = yield
run_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
result
rescue => e
run_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
raise e
end
end
ensure
update_stats(automation_id, run_time || 0)
end
def self.fetch_period_summaries
today = Date.current
# Define our time periods
periods = {
last_day: {
start_date: today - 1.day,
end_date: today,
},
last_week: {
start_date: today - 1.week,
end_date: today,
},
last_month: {
start_date: today - 1.month,
end_date: today,
},
}
result = {}
periods.each do |period_name, date_range|
builder = DB.build <<~SQL
SELECT
automation_id,
SUM(total_runs) AS total_runs,
SUM(total_time) AS total_time,
CASE WHEN SUM(total_runs) > 0
THEN SUM(total_time) / SUM(total_runs)
ELSE 0
END AS average_run_time,
MIN(min_run_time) AS min_run_time,
MAX(max_run_time) AS max_run_time
FROM discourse_automation_stats
WHERE date >= :start_date AND date <= :end_date
GROUP BY automation_id
SQL
stats = builder.query(start_date: date_range[:start_date], end_date: date_range[:end_date])
last_run_stats = DB.query_array <<~SQL
SELECT
automation_id,
MAX(last_run_at) AS last_run_at
FROM discourse_automation_stats
GROUP BY automation_id
SQL
last_run_stats = Hash[*last_run_stats.flatten]
stats.each do |stat|
automation_id = stat.automation_id
result[automation_id] ||= {}
result[automation_id][:last_run_at] = last_run_stats[automation_id]
result[automation_id][period_name] = {
total_runs: stat.total_runs,
total_time: stat.total_time,
average_run_time: stat.average_run_time,
min_run_time: stat.min_run_time,
max_run_time: stat.max_run_time,
}
end
end
result
end
def self.update_stats(automation_id, run_time)
today = Date.current
current_time = Time.now
builder = DB.build <<~SQL
INSERT INTO discourse_automation_stats
(automation_id, date, last_run_at, total_time, average_run_time, min_run_time, max_run_time, total_runs)
VALUES (:automation_id, :date, :current_time, :run_time, :run_time, :run_time, :run_time, 1)
ON CONFLICT (automation_id, date) DO UPDATE SET
last_run_at = :current_time,
total_time = discourse_automation_stats.total_time + :run_time,
total_runs = discourse_automation_stats.total_runs + 1,
average_run_time = (discourse_automation_stats.total_time + :run_time) / (discourse_automation_stats.total_runs + 1),
min_run_time = LEAST(discourse_automation_stats.min_run_time, :run_time),
max_run_time = GREATEST(discourse_automation_stats.max_run_time, :run_time)
SQL
builder.exec(
automation_id: automation_id,
date: today,
current_time: current_time,
run_time: run_time,
)
end
end
end
# == Schema Information
#
# Table name: discourse_automation_stats
#
# id :bigint not null, primary key
# automation_id :bigint not null
# date :date not null
# last_run_at :datetime not null
# total_time :float not null
# average_run_time :float not null
# min_run_time :float not null
# max_run_time :float not null
# total_runs :integer not null
#
# Indexes
#
# index_discourse_automation_stats_on_automation_id_and_date (automation_id,date) UNIQUE
#