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
enrollment; inspect `systemctl status docker` and `journalctl -u docker`.
The controller reconciles automatic setup on agent heartbeats and operation
polls. It first pulls `PLUTAINER_IMAGE` (default
The controller reconciles automatic setup on agent heartbeats. Command polls only
deliver work already queued. Setup first pulls `PLUTAINER_IMAGE` (default
`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
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 {
return db().transaction(() => {
const host = getHost(hostId);
@@ -203,5 +203,5 @@ export function reconcileHostPreparation(hostId: string): HostPreparation {
scheduleAvailabilityRefresh();
}
return status;
}).immediate();
})();
}
@@ -1,10 +1,8 @@
import { pendingHostOperationRows, waitForHostWork } from '$lib/server/hosts/index.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';
export async function GET({ request }: { request: Request }) {
const host = authenticateAgent(request);
reconcileHostPreparation(host.id);
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';
let operations = pendingHostOperationRows(host.id, lane, false);
+8
View File
@@ -183,6 +183,14 @@ describe('automatic host preparation', () => {
});
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();
expect(firstPoll.operations).toHaveLength(1);
const runtimeOperation = firstPoll.operations[0];