Reconcile host preparation on heartbeats instead of polls

This commit is contained in:
2026-10-06 14:52:03 -04:00
parent d99e5979fa
commit b17673720b
4 changed files with 12 additions and 6 deletions
+2 -2
View File
@@ -52,8 +52,8 @@ An already available Docker installation (including Docker CE) is reused. If the
daemon is unavailable, the installer reports the Docker error and stops before daemon is unavailable, the installer reports the Docker error and stops before
enrollment; inspect `systemctl status docker` and `journalctl -u docker`. enrollment; inspect `systemctl status docker` and `journalctl -u docker`.
The controller reconciles automatic setup on agent heartbeats and operation The controller reconciles automatic setup on agent heartbeats. Command polls only
polls. It first pulls `PLUTAINER_IMAGE` (default deliver work already queued. Setup first pulls `PLUTAINER_IMAGE` (default
`ghcr.io/ayymoss/plutainer:latest`), then prepares the latest verified upload for `ghcr.io/ayymoss/plutainer:latest`), then prepares the latest verified upload for
T5 and for T6 when available, one setup operation at a time. Uploading new files T5 and for T6 when available, one setup operation at a time. Uploading new files
or enrolling another host needs no manual propagation. Setup waits for an active, connected, or enrolling another host needs no manual propagation. Setup waits for an active, connected,
+2 -2
View File
@@ -173,7 +173,7 @@ function supersedeUndeliveredDownloads(hostId: string): void {
} }
} }
/** Agent heartbeats and polls drive durable preparation; no open UI or in-memory worker is needed. */ /** Heartbeats drive durable preparation; idle checks do not reserve the SQLite writer. */
export function reconcileHostPreparation(hostId: string): HostPreparation { export function reconcileHostPreparation(hostId: string): HostPreparation {
return db().transaction(() => { return db().transaction(() => {
const host = getHost(hostId); const host = getHost(hostId);
@@ -203,5 +203,5 @@ export function reconcileHostPreparation(hostId: string): HostPreparation {
scheduleAvailabilityRefresh(); scheduleAvailabilityRefresh();
} }
return status; return status;
}).immediate(); })();
} }
@@ -1,10 +1,8 @@
import { pendingHostOperationRows, waitForHostWork } from '$lib/server/hosts/index.js'; import { pendingHostOperationRows, waitForHostWork } from '$lib/server/hosts/index.js';
import { authenticateAgent } from '$lib/server/hosts/http.js'; import { authenticateAgent } from '$lib/server/hosts/http.js';
import { reconcileHostPreparation } from '$lib/server/hosts/preparation.js';
import { hostOperationsResponse } from '$lib/server/hosts/delivery.js'; import { hostOperationsResponse } from '$lib/server/hosts/delivery.js';
export async function GET({ request }: { request: Request }) { export async function GET({ request }: { request: Request }) {
const host = authenticateAgent(request); const host = authenticateAgent(request);
reconcileHostPreparation(host.id);
const params = new URL(request.url).searchParams; const params = new URL(request.url).searchParams;
const lane = params.get('lane') === 'health' ? 'health' : params.get('lane') === 'read' ? 'read' : params.get('lane') === 'write' ? 'write' : 'all'; const lane = params.get('lane') === 'health' ? 'health' : params.get('lane') === 'read' ? 'read' : params.get('lane') === 'write' ? 'write' : 'all';
let operations = pendingHostOperationRows(host.id, lane, false); let operations = pendingHostOperationRows(host.id, lane, false);
+8
View File
@@ -183,6 +183,14 @@ describe('automatic host preparation', () => {
}); });
expect(operations(host.id)).toEqual([]); expect(operations(host.id)).toEqual([]);
// Poll lanes only deliver queued work. One inventory heartbeat drives setup.
const reconcile = vi.spyOn(preparation, 'reconcileHostPreparation');
expect((await (await pollOperations({ request: request('operations') })).json()).operations).toEqual([]);
expect(reconcile).not.toHaveBeenCalled();
expect((await reportHeartbeat({ request: request('heartbeat', { inventory }) })).status).toBe(200);
expect(reconcile).toHaveBeenCalledOnce();
reconcile.mockRestore();
const firstPoll = await (await pollOperations({ request: request('operations') })).json(); const firstPoll = await (await pollOperations({ request: request('operations') })).json();
expect(firstPoll.operations).toHaveLength(1); expect(firstPoll.operations).toHaveLength(1);
const runtimeOperation = firstPoll.operations[0]; const runtimeOperation = firstPoll.operations[0];