Add host capacity sample viewer with filtering and pagination
This commit is contained in:
@@ -162,6 +162,13 @@ headroom**. Each calculation considers up to the latest 10,000 minute peaks.
|
|||||||
Host-local evidence is preferred to account for different hardware; new hosts
|
Host-local evidence is preferred to account for different hardware; new hosts
|
||||||
can borrow the fleet's learned allowance. Startup allowances are never lower
|
can borrow the fleet's learned allowance. Startup allowances are never lower
|
||||||
than running allowances. The host page shows both allowances and their source.
|
than running allowances. The host page shows both allowances and their source.
|
||||||
|
Expand **View samples** in Learned server capacity to inspect retained minute
|
||||||
|
peaks for the last 24 hours. Filter by startup/running phase or switch from this
|
||||||
|
host to the entire fleet. Rows show time, server and generation, CPU cores, and
|
||||||
|
memory in MiB before headroom; CPU and memory peaks may come from different
|
||||||
|
heartbeats. Results are paginated, and **Refresh samples** loads the latest stored
|
||||||
|
measurements without contacting agents. The allowance count may lag the table
|
||||||
|
until the next calculation or reflect a different source.
|
||||||
|
|
||||||
Lower allowances require at least three startup minute samples spanning two
|
Lower allowances require at least three startup minute samples spanning two
|
||||||
minutes, or twenty running minute samples spanning five minutes. Until then,
|
minutes, or twenty running minute samples spanning five minutes. Until then,
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ export interface ResourceAllowance {
|
|||||||
samples: number; observedAt: string | null;
|
samples: number; observedAt: string | null;
|
||||||
}
|
}
|
||||||
export interface CapacityCalibration { running: ResourceAllowance; startup: ResourceAllowance }
|
export interface CapacityCalibration { running: ResourceAllowance; startup: ResourceAllowance }
|
||||||
|
export interface CapacitySample {
|
||||||
|
hostId: string; hostName: string | null; instanceId: string; server: string | null;
|
||||||
|
generation: number; phase: 'startup' | 'running'; timestamp: string; cpuCores: number; memoryBytes: number;
|
||||||
|
}
|
||||||
|
export interface CapacitySamplePage {
|
||||||
|
samples: CapacitySample[]; total: number; page: number; pageSize: number;
|
||||||
|
}
|
||||||
export interface Host {
|
export interface Host {
|
||||||
id: string; name: string; hostname: string; mode: HostMode; portMin: number; portMax: number;
|
id: string; name: string; hostname: string; mode: HostMode; portMin: number; portMax: number;
|
||||||
bindAddress: string; publicAddress: string; minimumFreeBytes: number; createdAt: string;
|
bindAddress: string; publicAddress: string; minimumFreeBytes: number; createdAt: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from '../db/index.js';
|
import { db } from '../db/index.js';
|
||||||
import { ensureHostSchema } from './schema.js';
|
import { ensureHostSchema } from './schema.js';
|
||||||
import type { CapacityCalibration, HostInventory, ResourceAllowance } from '../../hosts.js';
|
import type { CapacityCalibration, CapacitySample, CapacitySamplePage, HostInventory, ResourceAllowance } from '../../hosts.js';
|
||||||
|
|
||||||
const BOOTSTRAP_CPU = 0.5;
|
const BOOTSTRAP_CPU = 0.5;
|
||||||
const BOOTSTRAP_MEMORY = 1024 ** 3;
|
const BOOTSTRAP_MEMORY = 1024 ** 3;
|
||||||
@@ -14,6 +14,27 @@ interface AllowanceRow {
|
|||||||
}
|
}
|
||||||
function database() { const handle = db(); ensureHostSchema(handle); return handle; }
|
function database() { const handle = db(); ensureHostSchema(handle); return handle; }
|
||||||
|
|
||||||
|
/** Read retained minute peaks without contacting agents or recalculating allowances. */
|
||||||
|
export function capacitySamples(hostId: string, options: { scope: 'host' | 'fleet'; phase: 'all' | Phase; page: number }): CapacitySamplePage {
|
||||||
|
const handle = database();
|
||||||
|
const clauses = ['s.bucket >= ?'];
|
||||||
|
const values: (string | number)[] = [new Date(Date.now() - WINDOW_MS).toISOString()];
|
||||||
|
if (options.scope === 'host') { clauses.push('s.host_id = ?'); values.push(hostId); }
|
||||||
|
if (options.phase !== 'all') { clauses.push('s.phase = ?'); values.push(options.phase); }
|
||||||
|
const where = clauses.join(' AND ');
|
||||||
|
const total = (handle.prepare(`SELECT COUNT(*) AS count FROM ec_instance_resource_samples s WHERE ${where}`).get(...values) as { count: number }).count;
|
||||||
|
const pageSize = 50;
|
||||||
|
const page = Math.min(options.page, Math.max(1, Math.ceil(total / pageSize)));
|
||||||
|
const samples = handle.prepare(`SELECT s.host_id AS hostId, h.name AS hostName, s.instance_id AS instanceId,
|
||||||
|
i.server, s.generation, s.phase, s.bucket AS timestamp, s.cpu_cores AS cpuCores, s.memory_bytes AS memoryBytes
|
||||||
|
FROM ec_instance_resource_samples s
|
||||||
|
LEFT JOIN ec_hosts h ON h.id=s.host_id
|
||||||
|
LEFT JOIN native_instances i ON i.id=s.instance_id AND i.host_id=s.host_id
|
||||||
|
WHERE ${where} ORDER BY s.bucket DESC, s.host_id, s.instance_id, s.generation, s.phase LIMIT ? OFFSET ?`)
|
||||||
|
.all(...values, pageSize, (page - 1) * pageSize) as CapacitySample[];
|
||||||
|
return { samples, total, page, pageSize };
|
||||||
|
}
|
||||||
|
|
||||||
export function validateInstanceResources(inventory: HostInventory): void {
|
export function validateInstanceResources(inventory: HostInventory): void {
|
||||||
if (inventory.instanceResourcesError !== undefined && (typeof inventory.instanceResourcesError !== 'string'
|
if (inventory.instanceResourcesError !== undefined && (typeof inventory.instanceResourcesError !== 'string'
|
||||||
|| inventory.instanceResourcesError.length > 500)) throw new Error('Invalid instance resource sampling error.');
|
|| inventory.instanceResourcesError.length > 500)) throw new Error('Invalid instance resource sampling error.');
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { CapacitySamplePage } from '$lib/hosts.js';
|
||||||
|
import Button from './primitives/Button.svelte';
|
||||||
|
|
||||||
|
let { hostId }: { hostId: string } = $props();
|
||||||
|
let open = $state(false);
|
||||||
|
let scope = $state('host');
|
||||||
|
let phase = $state('all');
|
||||||
|
let page = $state(1);
|
||||||
|
let revision = $state(0);
|
||||||
|
let result = $state<CapacitySamplePage | null>(null);
|
||||||
|
let loading = $state(false);
|
||||||
|
let error = $state('');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const url = `/api/hosts/${encodeURIComponent(hostId)}/samples?` + new URLSearchParams({ scope, phase, page: String(page) });
|
||||||
|
void revision;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), 15000);
|
||||||
|
let active = true;
|
||||||
|
loading = true; error = ''; result = null;
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { signal: controller.signal });
|
||||||
|
const body = await response.json();
|
||||||
|
if (!response.ok) throw new Error(body.message ?? body.error ?? 'Could not load samples.');
|
||||||
|
if (active) result = body;
|
||||||
|
} catch (cause) {
|
||||||
|
if (active) error = controller.signal.aborted ? 'Sample request timed out. Try refreshing.' : (cause as Error).message;
|
||||||
|
} finally { clearTimeout(timer); if (active) loading = false; }
|
||||||
|
})();
|
||||||
|
return () => { active = false; clearTimeout(timer); controller.abort(); };
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<details bind:open>
|
||||||
|
<summary>View samples</summary>
|
||||||
|
{#if open}
|
||||||
|
<p>Retained minute peaks from the last 24 hours, before the 25% allowance headroom. CPU and memory peaks may come from different heartbeats. Choose the fleet to inspect measurements from other hosts used by fleet allowances.</p>
|
||||||
|
<div class="controls">
|
||||||
|
<label>Source<select bind:value={scope} onchange={() => page = 1}><option value="host">This host</option><option value="fleet">Entire fleet</option></select></label>
|
||||||
|
<label>Phase<select bind:value={phase} onchange={() => page = 1}><option value="all">All phases</option><option value="startup">Starting</option><option value="running">Running</option></select></label>
|
||||||
|
<Button size="sm" disabled={loading} onclick={() => { page = 1; revision++; }}>Refresh samples</Button>
|
||||||
|
</div>
|
||||||
|
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}
|
||||||
|
{#if loading}<p role="status">Loading samples…</p>
|
||||||
|
{:else if result}
|
||||||
|
{#if result.total === 0}<p role="status">No {phase === 'all' ? '' : phase === 'startup' ? 'startup ' : 'running '}samples recorded for this source in the last 24 hours.</p>
|
||||||
|
{:else}
|
||||||
|
<p>{result.total.toLocaleString()} samples · newest first. Counts can differ from the allowance above, which is recalculated periodically.</p>
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex (The overflow region needs keyboard focus for horizontal scrolling.) -->
|
||||||
|
<div class="table-wrap" tabindex="0" role="region" aria-label="Container resource samples">
|
||||||
|
<table><caption>CPU and memory peaks per container and minute; times are local.</caption>
|
||||||
|
<thead><tr><th scope="col">Time</th>{#if scope === 'fleet'}<th scope="col">Host</th>{/if}<th scope="col">Server</th><th scope="col">Phase</th><th scope="col">CPU cores</th><th scope="col">Memory</th></tr></thead>
|
||||||
|
<tbody>{#each result.samples as sample}
|
||||||
|
<tr><td><time datetime={sample.timestamp}>{new Date(sample.timestamp).toLocaleString()}</time></td>
|
||||||
|
{#if scope === 'fleet'}<td>{sample.hostName ?? sample.hostId}</td>{/if}
|
||||||
|
<td>{sample.server ?? sample.instanceId}<small title={sample.instanceId}>Generation {sample.generation}</small></td>
|
||||||
|
<td>{sample.phase === 'startup' ? 'Starting' : 'Running'}</td><td>{sample.cpuCores.toFixed(3)}</td><td>{(sample.memoryBytes / 1048576).toFixed(1)} MiB</td></tr>
|
||||||
|
{/each}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<nav aria-label="Sample pages">
|
||||||
|
<Button size="sm" disabled={result.page <= 1} onclick={() => page = result!.page - 1}>Previous</Button>
|
||||||
|
<span>Page {result.page} of {Math.ceil(result.total / result.pageSize)}</span>
|
||||||
|
<Button size="sm" disabled={result.page * result.pageSize >= result.total} onclick={() => page = result!.page + 1}>Next</Button>
|
||||||
|
</nav>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
details { margin-top:20px; min-width:0; } summary { cursor:pointer; color:var(--accent-primary); font-weight:600; }
|
||||||
|
p, caption, small, nav { color:var(--text-secondary); font-size:var(--text-sm); line-height:1.6; }
|
||||||
|
.controls, nav { display:flex; flex-wrap:wrap; align-items:end; gap:12px; margin:16px 0; }
|
||||||
|
nav { align-items:center; } label { display:grid; gap:6px; font-size:var(--text-sm); }
|
||||||
|
select { padding:8px; color:var(--text-primary); background:var(--surface-sunken); border:1px solid var(--border-control); border-radius:var(--radius-sm); }
|
||||||
|
.table-wrap { max-width:100%; overflow:auto; } .table-wrap:focus-visible, summary:focus-visible { outline:2px solid var(--accent-primary); outline-offset:3px; }
|
||||||
|
table { width:100%; border-collapse:collapse; text-align:left; font-size:var(--text-sm); }
|
||||||
|
caption { text-align:left; padding-bottom:8px; } td,th { padding:10px; border-bottom:1px solid var(--border-subtle); }
|
||||||
|
td { white-space:nowrap; } small { display:block; font-size:var(--text-xs); }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { error, json } from '@sveltejs/kit';
|
||||||
|
import { getHost } from '$lib/server/hosts/index.js';
|
||||||
|
import { requireManagementOrigin } from '$lib/server/hosts/http.js';
|
||||||
|
import { capacitySamples } from '$lib/server/hosts/calibration.js';
|
||||||
|
|
||||||
|
export function GET({ request, params }: { request: Request; params: { id: string } }) {
|
||||||
|
requireManagementOrigin(request);
|
||||||
|
if (!getHost(params.id)) error(404, 'Host not found.');
|
||||||
|
const search = new URL(request.url).searchParams;
|
||||||
|
const scope = search.get('scope') ?? 'host';
|
||||||
|
const phase = search.get('phase') ?? 'all';
|
||||||
|
const page = Number(search.get('page') ?? '1');
|
||||||
|
if (scope !== 'host' && scope !== 'fleet') error(400, 'Invalid sample scope.');
|
||||||
|
if (phase !== 'all' && phase !== 'startup' && phase !== 'running') error(400, 'Invalid sample phase.');
|
||||||
|
if (!Number.isSafeInteger(page) || page < 1) error(400, 'Invalid sample page.');
|
||||||
|
return json(capacitySamples(params.id, { scope, phase, page }), { headers: { 'Cache-Control': 'no-store' } });
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import Button from '$ui/components/primitives/Button.svelte';
|
import Button from '$ui/components/primitives/Button.svelte';
|
||||||
import DeleteHostDialog from '$ui/components/DeleteHostDialog.svelte';
|
import DeleteHostDialog from '$ui/components/DeleteHostDialog.svelte';
|
||||||
import HostPreparation from '$ui/components/HostPreparation.svelte';
|
import HostPreparation from '$ui/components/HostPreparation.svelte';
|
||||||
|
import CapacitySamples from '$ui/components/CapacitySamples.svelte';
|
||||||
let host = $state<Host | null>(null); let operations = $state<HostOperation[]>([]);
|
let host = $state<Host | null>(null); let operations = $state<HostOperation[]>([]);
|
||||||
let calibration = $state<CapacityCalibration | null>(null);
|
let calibration = $state<CapacityCalibration | null>(null);
|
||||||
let loading = $state(true); let busy = $state(false); let error = $state(''); let notice = $state('');
|
let loading = $state(true); let busy = $state(false); let error = $state(''); let notice = $state('');
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
{/each}</dl>
|
{/each}</dl>
|
||||||
{#if host.inventory.instanceResourcesError}<p role="status">{host.inventory.instanceResourcesError}</p>{/if}
|
{#if host.inventory.instanceResourcesError}<p role="status">{host.inventory.instanceResourcesError}</p>{/if}
|
||||||
{#if calibration.startup.source === 'bootstrap' || calibration.running.source === 'bootstrap'}<p>Use agent 1.4.6 or later for resource sampling. Measurements arrive with agent heartbeats and are combined into one peak sample per container per minute. The first five minutes after a container starts count toward startup; later measurements count toward running. Temporary allowances stay in use until enough measurements are available.</p>{/if}
|
{#if calibration.startup.source === 'bootstrap' || calibration.running.source === 'bootstrap'}<p>Use agent 1.4.6 or later for resource sampling. Measurements arrive with agent heartbeats and are combined into one peak sample per container per minute. The first five minutes after a container starts count toward startup; later measurements count toward running. Temporary allowances stay in use until enough measurements are available.</p>{/if}
|
||||||
|
{#key host.id}<CapacitySamples hostId={host.id} />{/key}
|
||||||
</section>{/if}
|
</section>{/if}
|
||||||
<div class="grid"><section class="panel"><h2>Inventory</h2><dl>
|
<div class="grid"><section class="panel"><h2>Inventory</h2><dl>
|
||||||
<dt>Hostname</dt><dd>{host.hostname || 'Pending'}</dd><dt>Agent</dt><dd>{host.inventory.agentVersion ?? 'Unknown'}</dd><dt>Platform</dt><dd>{host.inventory.os ?? 'Unknown'} · {host.inventory.architecture ?? 'Unknown'}</dd><dt>CPU</dt><dd>{host.inventory.cpu ?? 'Unknown'} ({host.inventory.cpuCount ?? '?'} cores)</dd><dt>CPU usage</dt><dd title="Average across all cores since the previous inventory sample">{host.inventory.cpuUsagePercent === undefined ? 'Not reported' : `${host.inventory.cpuUsagePercent.toFixed(1)}%`}{#if !host.connected && host.inventory.cpuUsagePercent !== undefined} (last reported){/if}</dd><dt>Memory available / total</dt><dd>{bytes(host.inventory.memoryAvailable)} / {bytes(host.inventory.memoryTotal)}</dd><dt>Game storage available / total</dt><dd>{bytes(host.inventory.storageAvailable)} / {bytes(host.inventory.storageTotal)}</dd><dt>Docker</dt><dd>{host.inventory.dockerVersion ?? 'Unknown'} · {host.inventory.dockerRunning ? 'Running' : 'Unavailable'}</dd><dt>Current operation</dt><dd>{host.inventory.currentOperation ?? 'None reported'}</dd>
|
<dt>Hostname</dt><dd>{host.hostname || 'Pending'}</dd><dt>Agent</dt><dd>{host.inventory.agentVersion ?? 'Unknown'}</dd><dt>Platform</dt><dd>{host.inventory.os ?? 'Unknown'} · {host.inventory.architecture ?? 'Unknown'}</dd><dt>CPU</dt><dd>{host.inventory.cpu ?? 'Unknown'} ({host.inventory.cpuCount ?? '?'} cores)</dd><dt>CPU usage</dt><dd title="Average across all cores since the previous inventory sample">{host.inventory.cpuUsagePercent === undefined ? 'Not reported' : `${host.inventory.cpuUsagePercent.toFixed(1)}%`}{#if !host.connected && host.inventory.cpuUsagePercent !== undefined} (last reported){/if}</dd><dt>Memory available / total</dt><dd>{bytes(host.inventory.memoryAvailable)} / {bytes(host.inventory.memoryTotal)}</dd><dt>Game storage available / total</dt><dd>{bytes(host.inventory.storageAvailable)} / {bytes(host.inventory.storageTotal)}</dd><dt>Docker</dt><dd>{host.inventory.dockerVersion ?? 'Unknown'} · {host.inventory.dockerRunning ? 'Running' : 'Unavailable'}</dd><dt>Current operation</dt><dd>{host.inventory.currentOperation ?? 'None reported'}</dd>
|
||||||
|
|||||||
@@ -38,6 +38,57 @@ async function learned() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('measured instance capacity', () => {
|
describe('measured instance capacity', () => {
|
||||||
|
it('lists retained samples with phase filters, host scope, stable ordering, and bounded pages', async () => {
|
||||||
|
for (let minute = 0; minute < 101; minute++) await report(minute, { uptimeSeconds: minute < 5 ? minute * 60 : 600 });
|
||||||
|
const { capacitySamples } = await import('../../src/lib/server/hosts/calibration.js');
|
||||||
|
const first = capacitySamples(hostId, { scope: 'host', phase: 'all', page: 1 });
|
||||||
|
const second = capacitySamples(hostId, { scope: 'host', phase: 'all', page: 2 });
|
||||||
|
expect(first).toMatchObject({ total: 101, page: 1, pageSize: 50 });
|
||||||
|
expect(first.samples).toHaveLength(50);
|
||||||
|
expect(first.samples[0]).toMatchObject({ hostId, hostName: 'First', instanceId: sample.instanceId,
|
||||||
|
server: 't5_zombie_theater_instance-1', generation: 1, phase: 'running',
|
||||||
|
cpuCores: 0.1, memoryBytes: GiB / 4, timestamp: new Date(epoch + 100 * 60_000).toISOString() });
|
||||||
|
expect(new Set([...first.samples, ...second.samples].map(row => row.timestamp)).size).toBe(100);
|
||||||
|
expect(capacitySamples(hostId, { scope: 'host', phase: 'all', page: 999 })).toMatchObject({ page: 3, samples: [expect.any(Object)] });
|
||||||
|
const startup = capacitySamples(hostId, { scope: 'host', phase: 'startup', page: 1 });
|
||||||
|
expect(startup.total).toBe(5);
|
||||||
|
expect(startup.samples.every(row => row.phase === 'startup')).toBe(true);
|
||||||
|
expect(capacitySamples(hostId, { scope: 'host', phase: 'running', page: 1 }).total).toBe(96);
|
||||||
|
vi.setSystemTime(epoch + 26 * 60 * 60_000);
|
||||||
|
expect(capacitySamples(hostId, { scope: 'host', phase: 'all', page: 1 })).toMatchObject({ total: 0, samples: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports fleet samples and retains instance identity after retirement', async () => {
|
||||||
|
await report(0);
|
||||||
|
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||||
|
const other = hosts.createHost({ name: 'Second' });
|
||||||
|
hosts.enrollHost(hosts.createEnrollment(other.id).token, inventory);
|
||||||
|
await instance(other.id, 'instance-2');
|
||||||
|
await report(1, { instanceId: 'instance-2' }, other.id);
|
||||||
|
const { db } = await import('../../src/lib/server/db/index.js');
|
||||||
|
db().prepare('UPDATE native_instances SET retired_at=? WHERE id=?').run(new Date().toISOString(), sample.instanceId);
|
||||||
|
const { capacitySamples } = await import('../../src/lib/server/hosts/calibration.js');
|
||||||
|
expect(capacitySamples(hostId, { scope: 'host', phase: 'all', page: 1 }).samples.map(row => row.instanceId)).toEqual([sample.instanceId]);
|
||||||
|
const fleet = capacitySamples(hostId, { scope: 'fleet', phase: 'all', page: 1 });
|
||||||
|
expect(fleet.total).toBe(2);
|
||||||
|
expect(fleet.samples.map(row => row.hostName)).toEqual(['Second', 'First']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates the samples API and rejects agent credentials and cross-origin reads', async () => {
|
||||||
|
await report(0);
|
||||||
|
const { GET } = await import('../../src/routes/api/hosts/[id]/samples/+server.js');
|
||||||
|
const request = (query = '', headers = {}, id = hostId) => GET({ request: new Request(`https://ec.test/api/hosts/${id}/samples${query}`, { headers }), params: { id } });
|
||||||
|
const response = request();
|
||||||
|
expect(response.headers.get('cache-control')).toBe('no-store');
|
||||||
|
expect(await response.json()).toMatchObject({ total: 1, samples: [expect.objectContaining({ cpuCores: 0.1 })] });
|
||||||
|
for (const query of ['?scope=wrong', '?phase=wrong', '?page=-1', '?page=1.5', '?page=NaN', '?page=Infinity']) {
|
||||||
|
expect(() => request(query)).toThrow(expect.objectContaining({ status: 400 }));
|
||||||
|
}
|
||||||
|
expect(() => request('', {}, 'missing')).toThrow(expect.objectContaining({ status: 404 }));
|
||||||
|
expect(() => request('', { origin: 'https://other.test' })).toThrow(expect.objectContaining({ status: 403 }));
|
||||||
|
expect(() => request('', { authorization: 'Bearer ec_host_test' })).toThrow(expect.objectContaining({ status: 403 }));
|
||||||
|
});
|
||||||
|
|
||||||
it('reports sampling failure without inventing measurements and clears it after recovery', async () => {
|
it('reports sampling failure without inventing measurements and clears it after recovery', async () => {
|
||||||
const { heartbeat, getHost } = await import('../../src/lib/server/hosts/index.js');
|
const { heartbeat, getHost } = await import('../../src/lib/server/hosts/index.js');
|
||||||
const { capacityCalibration } = await import('../../src/lib/server/hosts/calibration.js');
|
const { capacityCalibration } = await import('../../src/lib/server/hosts/calibration.js');
|
||||||
|
|||||||
Reference in New Issue
Block a user