perf,feat(events),ops: third optimization pass
Overview cold start: - InstanceResourceService persists every live fetch into status_snapshots (category 'resources') and serves a snapshot younger than 2 minutes before calling upstream, so the first fleet overview after a restart costs zero device requests; instance deletion cleans up via FK cascade Bounded retention for the four fastest-growing tables (15-minute sweep): - connection_logs: one row per probe per beat had no automatic cleanup - audit_events: new pruneBefore (90d) - operation_preparations: every prepare/retry attempt inserted a row, terminal rows now expire after 7 days - notification_queue: terminal rows pruned after 30 days SSE events now cover instance lifecycle: - create/update emit instance envelopes from the routes; the two-phase delete emits a job envelope on every terminal transition plus an instance envelope when the node actually disappears, so fleet and instance views invalidate in real time Linux ops: - 'install.sh unit' writes systemd user units for API and gateway with Restart=on-failure, 0600 secret injection, and MULTI_SIMADMIN_SYSTEMD_UNIT wired so console self-update restarts via systemctl -- closing the self-update loop on Linux
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import type { EventEnvelope } from '../events/event-journal.js';
|
||||
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
||||
import type { Job, Preparation, PrepareOperationRequest } from '@multi-simadmin/contracts';
|
||||
import { InstanceService, InstanceServiceError } from '../instances/instance-service.js';
|
||||
@@ -75,6 +76,8 @@ interface Options {
|
||||
readonly now?: () => Date;
|
||||
readonly idFactory?: () => string;
|
||||
readonly tokenFactory?: () => string;
|
||||
/** Receives job/instance envelopes so the SSE journal can invalidate fleet views. */
|
||||
readonly emit?: (envelope: EventEnvelope) => void;
|
||||
}
|
||||
|
||||
const digest = (value: string): string => createHash('sha256').update(value, 'utf8').digest('hex');
|
||||
@@ -93,6 +96,7 @@ export class DeleteInstanceOperation {
|
||||
private readonly clock: () => Date;
|
||||
private readonly id: () => string;
|
||||
private readonly token: () => string;
|
||||
private readonly emit: Options['emit'];
|
||||
|
||||
constructor(options: Options) {
|
||||
this.db = options.db;
|
||||
@@ -101,6 +105,7 @@ export class DeleteInstanceOperation {
|
||||
this.clock = options.now ?? (() => new Date());
|
||||
this.id = options.idFactory ?? randomUUID;
|
||||
this.token = options.tokenFactory ?? (() => randomBytes(32).toString('base64url'));
|
||||
this.emit = options.emit;
|
||||
}
|
||||
|
||||
async prepare(input: PrepareOperationRequest, requestId = this.id()): Promise<Preparation> {
|
||||
@@ -318,10 +323,19 @@ export class DeleteInstanceOperation {
|
||||
const release = await this.releaseBindingPort(input.instanceId, input.requestId);
|
||||
if (release?.status === 'failed') {
|
||||
this.closeAsFailed(ids, input.requestId, BINDING_REFUSAL_CODE);
|
||||
this.#emitJob(ids.job, input.requestId);
|
||||
return this.job(ids.job);
|
||||
}
|
||||
try {
|
||||
await this.instances.delete(input.instanceId, input.revision, { allowJobHistory: true });
|
||||
this.#emit({
|
||||
kind: 'instance',
|
||||
id: this.id(),
|
||||
occurredAt: this.clock().toISOString(),
|
||||
requestId: input.requestId,
|
||||
instanceId: input.instanceId,
|
||||
});
|
||||
this.#emitJob(ids.job, input.requestId);
|
||||
const finished = this.clock().toISOString();
|
||||
this.db.transaction(() => {
|
||||
this.db
|
||||
@@ -379,6 +393,25 @@ export class DeleteInstanceOperation {
|
||||
}
|
||||
|
||||
/** Closes the job without touching the instance row, so the device stays listed and retryable. */
|
||||
#emit(envelope: EventEnvelope): void {
|
||||
if (!this.emit) return;
|
||||
try {
|
||||
this.emit(envelope);
|
||||
} catch {
|
||||
// A journal failure must never fail the operation itself.
|
||||
}
|
||||
}
|
||||
|
||||
#emitJob(jobId: string, requestId: string): void {
|
||||
this.#emit({
|
||||
kind: 'job',
|
||||
id: this.id(),
|
||||
occurredAt: this.clock().toISOString(),
|
||||
requestId,
|
||||
jobId,
|
||||
});
|
||||
}
|
||||
|
||||
private closeAsFailed(
|
||||
ids: { job: string; item: string; attempt: string },
|
||||
requestId: string,
|
||||
|
||||
Reference in New Issue
Block a user