Add configurable history and storage retention policies
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { db } from '../db/index.js';
|
||||
import { enrichGameChatTimeline, type ChatMatch, type ChatRoundReport, type ContextualChatEntry } from '../../game-chat.js';
|
||||
import { parseGameEvent } from '../../telemetry.js';
|
||||
import { TELEMETRY_REQUEST_RETENTION } from '../../telemetry-diagnostics.js';
|
||||
import { telemetryRetention, telemetryCutoff } from './telemetry-requests.js';
|
||||
import type { ServerRow } from './servers.js';
|
||||
import type { TailResult } from './logs.js';
|
||||
|
||||
@@ -15,7 +15,7 @@ export function serverChatTimeline(server: ServerRow, tail: TailResult): Context
|
||||
|
||||
// Diagnostic history survives deletion. Its receipt time must belong to the
|
||||
// current registration so a reused server name cannot inherit old rounds.
|
||||
const cutoff = new Date(Date.now() - TELEMETRY_REQUEST_RETENTION.maxAgeDays * 86_400_000).toISOString();
|
||||
const cutoff = telemetryCutoff();
|
||||
const reports = handle.prepare(`SELECT received_at AS receivedAt, event, round,
|
||||
player_count AS playerCount FROM telemetry_requests
|
||||
WHERE server = ? AND status = 200 AND received_at >= ?
|
||||
@@ -23,7 +23,7 @@ export function serverChatTimeline(server: ServerRow, tail: TailResult): Context
|
||||
AND event IN ('game_start', 'round_change', 'heartbeat', 'game_end')
|
||||
AND player_count >= 0 AND (event = 'game_start' OR round >= 0)
|
||||
ORDER BY id DESC LIMIT ?`).all(server.name, cutoff, server.created_at,
|
||||
TELEMETRY_REQUEST_RETENTION.maxPerServer) as ChatRoundReport[];
|
||||
(telemetryRetention().maxPerServer || 10000)) as ChatRoundReport[];
|
||||
|
||||
// Saving gameplay does not depend on diagnostic logging succeeding. Keep the
|
||||
// surviving accepted snapshot useful when its request log was not recorded.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fleetSettings } from '../settings.js';
|
||||
import { db } from '../db/index.js';
|
||||
import { ONLINE_MAX_MS } from '../../server-freshness.js';
|
||||
import { parseMetricsQuery, type HeatmapCell, type MapMetrics, type MetricPoint, type MetricsData,
|
||||
@@ -18,7 +19,8 @@ export function recordMetrics(samples: MetricSample[], at = Date.now()): void {
|
||||
handle.transaction(() => {
|
||||
const insert = handle.prepare('INSERT OR REPLACE INTO metric_samples (at, server, game, map, players, capacity) VALUES (?, ?, ?, ?, ?, ?)');
|
||||
for (const s of samples) insert.run(at, s.server, s.game, s.map, s.players, s.capacity);
|
||||
handle.prepare('DELETE FROM metric_samples WHERE at < ?').run(at - 31 * 86400_000);
|
||||
const days = fleetSettings().metricsKeepDays;
|
||||
if (days) handle.prepare('DELETE FROM metric_samples WHERE at < ?').run(at - days * 86400_000);
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { cancelStagedFiles, isFileApplied, queueFileChange, stagedFileChanges }
|
||||
// pre-multi-script legacy rows.
|
||||
|
||||
export class RollbackError extends Error {
|
||||
constructor(message: string, public readonly kind: 'no-such-deployment' | 'not-promote' | 'already-rolled-back') {
|
||||
constructor(message: string, public readonly kind: 'history-expired' | 'no-such-deployment' | 'not-promote' | 'already-rolled-back') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,9 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
FROM deployments WHERE id = ?`)
|
||||
.get(promoteId) as DeploymentRow | undefined;
|
||||
if (!dep) throw new RollbackError(`no such deployment ${promoteId}`, 'no-such-deployment');
|
||||
if (db().prepare("SELECT 1 FROM retention_expirations WHERE category='file-content' AND record_id=?").get(promoteId)) {
|
||||
throw new RollbackError('Rollback file contents have expired under the retention policy.', 'history-expired');
|
||||
}
|
||||
if (dep.action !== 'promote') {
|
||||
throw new RollbackError(`deployment ${promoteId} is action='${dep.action}', not 'promote'`, 'not-promote');
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { fleetSettings } from '../settings.js';
|
||||
import { db } from '../db/index.js';
|
||||
import { TELEMETRY_REQUEST_RETENTION, type TelemetryRequestLogEntry } from '../../telemetry-diagnostics.js';
|
||||
import { type TelemetryRequestLogEntry } from '../../telemetry-diagnostics.js';
|
||||
|
||||
const MAX_PAYLOAD_LENGTH = 16 * 1024;
|
||||
// Bound storage even when requests claim many different, unregistered names.
|
||||
const MAX_REQUESTS = 10_000;
|
||||
const cutoff = () => new Date(Date.now() - TELEMETRY_REQUEST_RETENTION.maxAgeDays * 86_400_000).toISOString();
|
||||
export function telemetryRetention() {
|
||||
const settings = fleetSettings();
|
||||
return { maxAgeDays: settings.telemetryKeepDays, maxPerServer: settings.telemetryMaxPerServer, maxTotal: settings.telemetryMaxTotal };
|
||||
}
|
||||
export const telemetryCutoff = () => telemetryRetention().maxAgeDays ? new Date(Date.now() - telemetryRetention().maxAgeDays * 86_400_000).toISOString() : '';
|
||||
const cutoff = telemetryCutoff;
|
||||
const textField = (value: unknown) => typeof value === 'string' ? value.slice(0, 255) : null;
|
||||
|
||||
export function recordTelemetryRequest(input: {
|
||||
@@ -31,13 +36,13 @@ export function recordTelemetryRequest(input: {
|
||||
Array.isArray(body.players) ? body.players.length : null,
|
||||
textField(input.contentType), payload.slice(0, MAX_PAYLOAD_LENGTH), Number(payload.length > MAX_PAYLOAD_LENGTH)
|
||||
);
|
||||
handle.prepare('DELETE FROM telemetry_requests WHERE received_at < ?').run(cutoff());
|
||||
handle.prepare(`DELETE FROM telemetry_requests WHERE server = ? AND id <= (
|
||||
if (telemetryRetention().maxAgeDays) handle.prepare('DELETE FROM telemetry_requests WHERE received_at < ?').run(cutoff());
|
||||
if (telemetryRetention().maxPerServer) handle.prepare(`DELETE FROM telemetry_requests WHERE server = ? AND id <= (
|
||||
SELECT id FROM telemetry_requests WHERE server = ? ORDER BY id DESC LIMIT 1 OFFSET ?
|
||||
)`).run(body.server, body.server, TELEMETRY_REQUEST_RETENTION.maxPerServer);
|
||||
handle.prepare(`DELETE FROM telemetry_requests WHERE id <= (
|
||||
)`).run(body.server, body.server, telemetryRetention().maxPerServer);
|
||||
if (telemetryRetention().maxTotal) handle.prepare(`DELETE FROM telemetry_requests WHERE id <= (
|
||||
SELECT id FROM telemetry_requests ORDER BY id DESC LIMIT 1 OFFSET ?
|
||||
)`).run(MAX_REQUESTS);
|
||||
)`).run(telemetryRetention().maxTotal);
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -45,6 +50,6 @@ export function listTelemetryRequests(server: string): TelemetryRequestLogEntry[
|
||||
const rows = db().prepare(`SELECT id, server, received_at AS receivedAt, status, error, event, map, round,
|
||||
player_count AS playerCount, content_type AS contentType, payload, payload_truncated AS payloadTruncated
|
||||
FROM telemetry_requests WHERE server = ? AND received_at >= ? ORDER BY id DESC LIMIT ?`)
|
||||
.all(server, cutoff(), TELEMETRY_REQUEST_RETENTION.maxPerServer) as Array<Omit<TelemetryRequestLogEntry, 'payloadTruncated'> & { payloadTruncated: number }>;
|
||||
.all(server, cutoff(), telemetryRetention().maxPerServer || 10000) as Array<Omit<TelemetryRequestLogEntry, 'payloadTruncated'> & { payloadTruncated: number }>;
|
||||
return rows.map(row => ({ ...row, payloadTruncated: !!row.payloadTruncated }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user