Finish autoscaling diagnostics and document pilot rollout
This commit is contained in:
@@ -114,7 +114,8 @@ export async function runAutoscaling(): Promise<boolean> {
|
||||
db().transaction(() => {
|
||||
const snapshot = scalingSnapshot(policy, now);
|
||||
if (scalingGloballyPaused()) snapshot.blockers.unshift('Autoscaling is globally paused.');
|
||||
if (db().prepare('SELECT 1 FROM autoscaling_decisions WHERE policy_id=? AND resolved=0').get(policy.id)) snapshot.blockers.unshift('Waiting for the current autoscaling operation to settle.');
|
||||
const pending = db().prepare(`SELECT d.id,d.stage FROM autoscaling_decisions a JOIN server_deployments d ON d.id=a.operation_id WHERE a.policy_id=? AND a.resolved=0`).get(policy.id) as { id: string; stage: string } | undefined;
|
||||
if (pending) snapshot.blockers.unshift(`Provisioning ${pending.id}: ${pending.stage}`);
|
||||
const decision = planScaling(policy, snapshot, now);
|
||||
for (const member of snapshot.members) {
|
||||
// Keep the observation interval that justified a pending removal;
|
||||
@@ -137,16 +138,16 @@ export async function runAutoscaling(): Promise<boolean> {
|
||||
db().prepare('UPDATE autoscaling_decisions SET resolved=0 WHERE id=?').run(decisionId);
|
||||
try {
|
||||
db().transaction(() => {
|
||||
const result = decision.action === 'create'
|
||||
? queueNativeCreationIntent({ server: decision.server!, game: policy.game, mapSlug: policy.mapSlug,
|
||||
selection: nativeLaunchSelection({ automatic: true }, policy.game), createdBy: `autoscaling:${policy.id}`, autoscalingDecisionId: decisionId })
|
||||
: destroyServer(decision.server!, `autoscaling:${policy.id}`, decisionId);
|
||||
if (!result.ok) throw new Error(result.message);
|
||||
if (!result.deploymentId) throw new Error('Provisioning did not return an operation identity.');
|
||||
db().prepare('UPDATE autoscaling_decisions SET operation_id=? WHERE id=?').run(result.deploymentId, decisionId);
|
||||
db().prepare("INSERT INTO provisioning_sources(operation_id,kind,source_id,server) VALUES(?,'autoscaling',?,?)").run(result.deploymentId, policy.id, decision.server);
|
||||
db().prepare('UPDATE autoscaling_policies SET next_action_at=? WHERE id=?').run(now + policy.cooldownMinutes * 60_000, policy.id);
|
||||
submitted = true;
|
||||
const result = decision.action === 'create'
|
||||
? queueNativeCreationIntent({ server: decision.server!, game: policy.game, mapSlug: policy.mapSlug,
|
||||
selection: nativeLaunchSelection({ automatic: true }, policy.game), createdBy: `autoscaling:${policy.id}`, autoscalingDecisionId: decisionId })
|
||||
: destroyServer(decision.server!, `autoscaling:${policy.id}`, decisionId);
|
||||
if (!result.ok) throw new Error(result.message);
|
||||
if (!result.deploymentId) throw new Error('Provisioning did not return an operation identity.');
|
||||
db().prepare('UPDATE autoscaling_decisions SET operation_id=? WHERE id=?').run(result.deploymentId, decisionId);
|
||||
db().prepare("INSERT INTO provisioning_sources(operation_id,kind,source_id,server) VALUES(?,'autoscaling',?,?)").run(result.deploymentId, policy.id, decision.server);
|
||||
db().prepare('UPDATE autoscaling_policies SET next_action_at=? WHERE id=?').run(now + policy.cooldownMinutes * 60_000, policy.id);
|
||||
submitted = true;
|
||||
})();
|
||||
} catch (error) {
|
||||
db().prepare('UPDATE autoscaling_decisions SET resolved=1 WHERE id=?').run(decisionId);
|
||||
@@ -162,7 +163,8 @@ export async function runAutoscaling(): Promise<boolean> {
|
||||
|
||||
export function pruneAutoscaling(now = Date.now()): void {
|
||||
const cutoff = now - 31 * 86400_000;
|
||||
db().prepare('DELETE FROM autoscaling_decisions WHERE last_seen_at<? AND resolved=1 AND operation_id IS NULL').run(cutoff);
|
||||
db().prepare(`DELETE FROM autoscaling_authorizations WHERE operation_id IN (SELECT operation_id FROM autoscaling_decisions WHERE last_seen_at<? AND resolved=1)`).run(cutoff);
|
||||
db().prepare('DELETE FROM autoscaling_decisions WHERE last_seen_at<? AND resolved=1').run(cutoff);
|
||||
db().prepare('DELETE FROM autoscaling_samples WHERE sampled_at<?').run(cutoff);
|
||||
}
|
||||
|
||||
@@ -171,16 +173,27 @@ export function scalingReport(): ScalingReport {
|
||||
const snapshot = scalingSnapshot(policy);
|
||||
if (scalingGloballyPaused()) snapshot.blockers.unshift('Autoscaling is globally paused.');
|
||||
const decision = planScaling(policy, snapshot, Date.now());
|
||||
const history = (db().prepare('SELECT id,evaluated_at,last_seen_at,decision,operation_id FROM autoscaling_decisions WHERE policy_id=? ORDER BY evaluated_at DESC,rowid DESC LIMIT 30').all(policy.id) as
|
||||
{ id: string; evaluated_at: number; last_seen_at: number; decision: string; operation_id: string | null }[])
|
||||
.map(row => ({ id: row.id, at: row.evaluated_at, lastSeenAt: row.last_seen_at, decision: JSON.parse(row.decision), operationId: row.operation_id }));
|
||||
const history = (db().prepare(`SELECT a.id,a.evaluated_at,a.last_seen_at,a.decision,a.operation_id,d.state AS operation_state
|
||||
FROM autoscaling_decisions a LEFT JOIN server_deployments d ON d.id=a.operation_id
|
||||
WHERE a.policy_id=? ORDER BY a.evaluated_at DESC,a.rowid DESC LIMIT 30`).all(policy.id) as
|
||||
{ id: string; evaluated_at: number; last_seen_at: number; decision: string; operation_id: string | null; operation_state: string | null }[])
|
||||
.map(row => ({ id: row.id, at: row.evaluated_at, lastSeenAt: row.last_seen_at, decision: JSON.parse(row.decision), operationId: row.operation_id, operationState: row.operation_state }));
|
||||
const unsettled = db().prepare(`SELECT d.id,d.action,d.state,d.stage FROM autoscaling_decisions a JOIN server_deployments d ON d.id=a.operation_id WHERE a.policy_id=? AND a.resolved=0`).all(policy.id) as ScalingView['unsettled'];
|
||||
const ownsMap = !!(db().prepare('SELECT owns_map FROM autoscaling_policies WHERE id=?').get(policy.id) as { owns_map: number }).owns_map;
|
||||
// Integrate only bounded intervals between adjacent samples; outages contribute no invented time.
|
||||
const totals = db().prepare(`WITH intervals AS (SELECT *,LEAD(sampled_at) OVER (ORDER BY sampled_at)-sampled_at AS duration
|
||||
FROM autoscaling_samples WHERE policy_id=?) SELECT COALESCE(SUM(MIN(duration,15000))/60000.0,0) AS observedMinutes,
|
||||
COALESCE(SUM(empty*MIN(duration,15000))/3600000.0,0) AS emptyLobbyHours,
|
||||
COALESCE(SUM(CASE WHEN deficit>0 THEN MIN(duration,15000) ELSE 0 END)/60000.0,0) AS deficitMinutes,
|
||||
COALESCE(SUM(CASE WHEN unknown>0 THEN MIN(duration,15000) ELSE 0 END)/60000.0,0) AS unknownMinutes FROM intervals WHERE duration<=30000`).get(policy.id) as ScalingView['totals'];
|
||||
return { policy, snapshot, decision, token: '', blockers: [], history, lastEvaluatedAt: history[0]?.lastSeenAt ?? null, totals };
|
||||
const operations = db().prepare(`SELECT COALESCE(SUM(d.action='create'),0) AS creates,COALESCE(SUM(d.action='destroy'),0) AS removals,
|
||||
COALESCE(SUM(d.state='cancelled'),0) AS cancelled,COALESCE(SUM(d.state='failed'),0) AS failed,
|
||||
COUNT(CASE WHEN d.action='create' AND d.state IN ('done','warning') THEN 1 END) AS readinessSamples,
|
||||
AVG(CASE WHEN d.action='create' AND d.state IN ('done','warning') THEN (julianday(d.finished_at)-julianday(d.started_at))*86400 END) AS readinessMeanSeconds,
|
||||
MAX(CASE WHEN d.action='create' AND d.state IN ('done','warning') THEN (julianday(d.finished_at)-julianday(d.started_at))*86400 END) AS readinessMaxSeconds
|
||||
FROM server_deployments d JOIN provisioning_sources s ON s.operation_id=d.id
|
||||
WHERE s.kind='autoscaling' AND s.source_id=? AND d.started_at>=?`).get(policy.id, new Date(Date.now() - 31 * 86400_000).toISOString()) as ScalingView['operations'];
|
||||
return { policy, snapshot, decision, token: '', blockers: [], history, lastEvaluatedAt: history[0]?.lastSeenAt ?? null, totals, operations, unsettled, ownsMap };
|
||||
});
|
||||
return { policies, globalPaused: scalingGloballyPaused(), liveAvailable: true, removalAvailable: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user