This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { db } from '../db/index.js';
|
import { db } from '../db/index.js';
|
||||||
import type { CapacityHistory, CapacityQuery, CapacityRange } from '../../capacity.js';
|
import type { CapacityHistory, CapacityQuery, CapacityRange } from '../../capacity.js';
|
||||||
|
import type { HostMode } from '../../hosts.js';
|
||||||
import { readTemperatureHistory } from './temperatures.js';
|
import { readTemperatureHistory } from './temperatures.js';
|
||||||
|
|
||||||
const windows: Record<CapacityRange, { duration: number; bucketMs: number }> = {
|
const windows: Record<CapacityRange, { duration: number; bucketMs: number }> = {
|
||||||
@@ -16,13 +17,14 @@ export function readCapacityHistory(query: CapacityQuery, now = Date.now()): Cap
|
|||||||
const count = duration / bucketMs;
|
const count = duration / bucketMs;
|
||||||
const hosts = handle
|
const hosts = handle
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT id,name,created_at FROM ec_hosts
|
`SELECT id,name,created_at,mode FROM ec_hosts
|
||||||
WHERE deleted_at IS NULL ${query.hostId ? 'AND id=?' : ''} ORDER BY id`
|
WHERE deleted_at IS NULL ${query.hostId ? 'AND id=?' : ''} ORDER BY id`
|
||||||
)
|
)
|
||||||
.all(...(query.hostId ? [query.hostId] : [])) as {
|
.all(...(query.hostId ? [query.hostId] : [])) as {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
mode: HostMode;
|
||||||
}[];
|
}[];
|
||||||
// The primary key bounds each host scan to the selected interval. Aggregate
|
// The primary key bounds each host scan to the selected interval. Aggregate
|
||||||
// before crossing the worker boundary; do not return raw heartbeat rows.
|
// before crossing the worker boundary; do not return raw heartbeat rows.
|
||||||
@@ -44,6 +46,9 @@ export function readCapacityHistory(query: CapacityQuery, now = Date.now()): Cap
|
|||||||
expectedHosts: 0
|
expectedHosts: 0
|
||||||
}));
|
}));
|
||||||
for (const host of hosts) {
|
for (const host of hosts) {
|
||||||
|
// Current mode determines available capacity; keep inactive hosts in
|
||||||
|
// the separate temperature history for diagnostics.
|
||||||
|
if (host.mode !== 'active') continue;
|
||||||
const createdAt = Date.parse(host.created_at);
|
const createdAt = Date.parse(host.created_at);
|
||||||
for (const point of points) {
|
for (const point of points) {
|
||||||
if (createdAt <= Math.min(now, point.at + bucketMs)) point.expectedHosts++;
|
if (createdAt <= Math.min(now, point.at + bucketMs)) point.expectedHosts++;
|
||||||
|
|||||||
@@ -234,10 +234,11 @@
|
|||||||
<details class="about">
|
<details class="about">
|
||||||
<summary>About this chart</summary>
|
<summary>About this chart</summary>
|
||||||
<p class="note">
|
<p class="note">
|
||||||
Available host resources after the operating-system reserve, averaged per period. Gaps
|
Available resources from currently active hosts after the operating-system reserve,
|
||||||
mean reporting is incomplete. Reporting-host coverage is shown for each selected period.
|
averaged per period. Disabled and draining hosts are excluded. Gaps mean reporting is
|
||||||
This history includes all workloads on the selected hosts; it does not reconstruct
|
incomplete. Reporting-host coverage is shown for each selected period. This history
|
||||||
historical launch eligibility, queued jobs, or port availability.
|
includes all workloads on the selected hosts; it does not reconstruct historical launch
|
||||||
|
eligibility, queued jobs, or port availability.
|
||||||
</p>
|
</p>
|
||||||
<details bind:open={tableOpen}>
|
<details bind:open={tableOpen}>
|
||||||
<summary>View history data · {$timeZone.replaceAll('_', ' ')}</summary>
|
<summary>View history data · {$timeZone.replaceAll('_', ' ')}</summary>
|
||||||
|
|||||||
@@ -161,6 +161,82 @@ describe('capacity history', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('excludes inactive hosts from resource totals and coverage while retaining their temperatures', async () => {
|
||||||
|
await host('active');
|
||||||
|
await host('disabled');
|
||||||
|
await host('draining');
|
||||||
|
await host('disabled-without-samples');
|
||||||
|
await sample('active', epoch - 45_000);
|
||||||
|
await sample('disabled', epoch - 45_000);
|
||||||
|
await sample('disabled', epoch - 90_000);
|
||||||
|
await sample('draining', epoch - 45_000);
|
||||||
|
const { db } = await import('../../src/lib/server/db/index.js');
|
||||||
|
db().prepare("UPDATE ec_hosts SET mode='disabled' WHERE id LIKE 'disabled%'").run();
|
||||||
|
db().prepare("UPDATE ec_hosts SET mode='draining' WHERE id='draining'").run();
|
||||||
|
const { recordTemperatures } = await import('../../src/lib/server/hosts/temperatures.js');
|
||||||
|
recordTemperatures(
|
||||||
|
'disabled',
|
||||||
|
{
|
||||||
|
temperatures: [
|
||||||
|
{
|
||||||
|
id: 'pkg',
|
||||||
|
device: 'coretemp',
|
||||||
|
kind: 'cpu-package',
|
||||||
|
label: 'Package id 0',
|
||||||
|
celsius: 65
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
new Date(epoch - 45_000).toISOString()
|
||||||
|
);
|
||||||
|
const { readCapacityHistory } =
|
||||||
|
await import('../../src/lib/server/hosts/capacity-history.js');
|
||||||
|
const history = readCapacityHistory(query, epoch);
|
||||||
|
expect(history.points.at(-1)).toMatchObject({
|
||||||
|
memoryBytesAvailable: 4 * GiB - 500_000_000,
|
||||||
|
reportingHosts: 1,
|
||||||
|
expectedHosts: 1
|
||||||
|
});
|
||||||
|
expect(history.points.at(-1)?.cpuCoresAvailable).toBeCloseTo(5.6);
|
||||||
|
expect(history.points.at(-2)).toMatchObject({
|
||||||
|
cpuCoresAvailable: null,
|
||||||
|
memoryBytesAvailable: null,
|
||||||
|
reportingHosts: 0,
|
||||||
|
expectedHosts: 1
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
history.temperatures.find((series) => series.hostId === 'disabled')?.points.at(-1)
|
||||||
|
).toMatchObject({ celsius: 65 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['disabled', 'draining'])(
|
||||||
|
'omits a selected %s host until it is active again',
|
||||||
|
async (mode) => {
|
||||||
|
await host('one');
|
||||||
|
await sample('one', epoch - 45_000);
|
||||||
|
const { db } = await import('../../src/lib/server/db/index.js');
|
||||||
|
db().prepare('UPDATE ec_hosts SET mode=? WHERE id=?').run(mode, 'one');
|
||||||
|
const { readCapacityHistory } =
|
||||||
|
await import('../../src/lib/server/hosts/capacity-history.js');
|
||||||
|
for (const hostId of [null, 'one']) {
|
||||||
|
const history = readCapacityHistory({ ...query, hostId }, epoch);
|
||||||
|
expect(
|
||||||
|
history.points.every(
|
||||||
|
(point) =>
|
||||||
|
point.cpuCoresAvailable === null &&
|
||||||
|
point.memoryBytesAvailable === null &&
|
||||||
|
point.reportingHosts === 0 &&
|
||||||
|
point.expectedHosts === 0
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
db().prepare("UPDATE ec_hosts SET mode='active' WHERE id='one'").run();
|
||||||
|
const history = readCapacityHistory({ ...query, hostId: 'one' }, epoch);
|
||||||
|
expect(history.points.at(-1)?.cpuCoresAvailable).toBeCloseTo(5.6);
|
||||||
|
expect(history.points.at(-1)).toMatchObject({ reportingHosts: 1, expectedHosts: 1 });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it('scopes history to the selected registered host and does not add hosts before they existed', async () => {
|
it('scopes history to the selected registered host and does not add hosts before they existed', async () => {
|
||||||
await host('one');
|
await host('one');
|
||||||
await host('two', epoch - 30_000);
|
await host('two', epoch - 30_000);
|
||||||
|
|||||||
Reference in New Issue
Block a user