Add autoscaling outcomes to Metrics (Refs #140)
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { db } from '../db/index.js';
|
||||
import { parseServerName, zombieMaps } from '../../map-aliases.js';
|
||||
import type { AutoscalingMetrics, MetricsScope } from '../../metrics.js';
|
||||
|
||||
/** Provisioning sources survive policy/decision deletion and server removal. */
|
||||
export function readAutoscalingMetrics(scope: MetricsScope, since: number, now: number) {
|
||||
// Group in SQLite to avoid transferring every operation. IN deduplicates source
|
||||
// links, so one operation always counts once. Keep older groups for scope options.
|
||||
const groups = db()
|
||||
.prepare(
|
||||
`WITH operations AS (
|
||||
SELECT server, action, state, finished_at >= ? AS in_window,
|
||||
ROUND((julianday(finished_at)-julianday(started_at))*86400000)/1000.0 AS seconds
|
||||
FROM server_deployments
|
||||
WHERE id IN (SELECT operation_id FROM provisioning_sources WHERE kind='autoscaling')
|
||||
AND finished_at <= ? AND state IN ('done','warning','failed','cancelled')
|
||||
), measured AS (
|
||||
SELECT *, CASE WHEN in_window AND action='create'
|
||||
AND state IN ('done','warning') AND seconds >= 0 THEN seconds END AS readiness
|
||||
FROM operations
|
||||
)
|
||||
SELECT server,
|
||||
SUM(in_window AND action='create' AND state IN ('done','warning')) AS added,
|
||||
SUM(in_window AND action='destroy' AND state IN ('done','warning')) AS removed,
|
||||
SUM(in_window AND state='failed') AS failed,
|
||||
SUM(in_window AND state='cancelled') AS cancelled,
|
||||
COUNT(readiness) AS readinessSamples,
|
||||
COALESCE(SUM(readiness),0) AS readinessSeconds,
|
||||
MAX(readiness) AS readinessMaxSeconds
|
||||
FROM measured GROUP BY server`
|
||||
)
|
||||
.all(new Date(since).toISOString(), new Date(now).toISOString()) as (Omit<
|
||||
AutoscalingMetrics,
|
||||
'readinessMeanSeconds'
|
||||
> & { server: string; readinessSeconds: number })[];
|
||||
const summary: AutoscalingMetrics = {
|
||||
added: 0,
|
||||
removed: 0,
|
||||
failed: 0,
|
||||
cancelled: 0,
|
||||
readinessSamples: 0,
|
||||
readinessMeanSeconds: null,
|
||||
readinessMaxSeconds: null
|
||||
};
|
||||
const maps = new Map<string, { game: string; map: string }>();
|
||||
let readinessSeconds = 0;
|
||||
for (const group of groups) {
|
||||
// Autoscaling names encode the selected variant. Never use today's server
|
||||
// registration or the policy scope (which may cover every map in a game).
|
||||
const parsed = parseServerName(group.server);
|
||||
const catalog = zombieMaps(parsed?.game ?? null);
|
||||
const entry = catalog.find(
|
||||
(map) =>
|
||||
map.slug === parsed?.mapSlug ||
|
||||
map.friendly.toLowerCase().replace(/[^a-z0-9]/g, '') === parsed?.mapSlug
|
||||
);
|
||||
if (parsed && entry) {
|
||||
const pair = { game: parsed.game, map: entry.friendly };
|
||||
maps.set(JSON.stringify(pair), pair);
|
||||
}
|
||||
if (scope.game !== null && parsed?.game !== scope.game) continue;
|
||||
if (scope.map !== null) {
|
||||
const label = scope.map.toLowerCase();
|
||||
// A canonical T6 engine map can describe several variants; do not
|
||||
// silently combine Town, Farm, Bus Depot and TranZit for that label.
|
||||
const matches = catalog.filter(
|
||||
(map) =>
|
||||
map.friendly.toLowerCase() === label ||
|
||||
map.slug === label ||
|
||||
map.canonical === label
|
||||
);
|
||||
if (!entry || matches.length !== 1 || matches[0].slug !== entry.slug) continue;
|
||||
}
|
||||
summary.added += group.added;
|
||||
summary.removed += group.removed;
|
||||
summary.failed += group.failed;
|
||||
summary.cancelled += group.cancelled;
|
||||
summary.readinessSamples += group.readinessSamples;
|
||||
readinessSeconds += group.readinessSeconds;
|
||||
if (group.readinessMaxSeconds !== null)
|
||||
summary.readinessMaxSeconds = Math.max(
|
||||
summary.readinessMaxSeconds ?? 0,
|
||||
group.readinessMaxSeconds
|
||||
);
|
||||
}
|
||||
if (summary.readinessSamples > 0)
|
||||
summary.readinessMeanSeconds = readinessSeconds / summary.readinessSamples;
|
||||
return { summary, maps: [...maps.values()] };
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { fleetSettings } from '../settings.js';
|
||||
import { db } from '../db/index.js';
|
||||
import { ONLINE_MAX_MS } from '../../server-freshness.js';
|
||||
import { zombieMaps } from '../../map-aliases.js';
|
||||
import { readAutoscalingMetrics } from './metrics-autoscaling.js';
|
||||
import {
|
||||
parseMetricsQuery,
|
||||
type HeatmapCell,
|
||||
@@ -207,10 +208,11 @@ export function readMetrics(input: MetricsDays | MetricsQuery, now = Date.now())
|
||||
`SELECT DISTINCT game, map_name AS map FROM leaderboard_matches WHERE started_at <= ?`
|
||||
)
|
||||
.all(new Date(now).toISOString()) as { game: string; map: string }[];
|
||||
const autoscaling = readAutoscalingMetrics(scope, since, now);
|
||||
const compareText = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
||||
const availableMaps = [
|
||||
...new Map(
|
||||
[...groups, ...matchMaps].map(({ game, map }) => [
|
||||
[...groups, ...matchMaps, ...autoscaling.maps].map(({ game, map }) => [
|
||||
JSON.stringify([game, map]),
|
||||
{ game, map }
|
||||
])
|
||||
@@ -514,6 +516,7 @@ export function readMetrics(input: MetricsDays | MetricsQuery, now = Date.now())
|
||||
summary,
|
||||
running,
|
||||
matches: matchMetrics(totalMatches),
|
||||
autoscaling: autoscaling.summary,
|
||||
sampledServerCount: sampledServers.size,
|
||||
maps,
|
||||
points,
|
||||
|
||||
Reference in New Issue
Block a user