Let agents idle for updates required by creation cancellation
This commit is contained in:
@@ -136,6 +136,43 @@ describe('native host readiness and reservations', () => {
|
||||
});
|
||||
|
||||
describe('durable host operations', () => {
|
||||
it('drains an old agent for a cancellation update while allowing receipts, heartbeats, and other hosts', async () => {
|
||||
const api = await enrolled();
|
||||
const original = api.queueHostOperation(api.host.id, 'instance.create', {}, { instanceId: 'cancelled-instance', generation: 1 });
|
||||
const inspect = api.queueHostOperation(api.host.id, 'instance.inspect', {});
|
||||
expect(api.pendingHostOperations(api.host.id).map(op => op.id)).toContain(original.id);
|
||||
api.reportHostOperation(api.host.id, original.id, { state: 'running' });
|
||||
api.reportHostOperation(api.host.id, inspect.id, { state: 'unknown' });
|
||||
const cleanup = api.queueHostOperation(api.host.id, 'instance.cancel-create', { createOperationId: original.id }, { instanceId: 'cancelled-instance', generation: 1 });
|
||||
const logs = api.queueHostOperation(api.host.id, 'logs.read', {});
|
||||
const preparation = api.queueHostOperation(api.host.id, 'library.download', {});
|
||||
const other = api.createHost({ name: 'Unaffected host' });
|
||||
api.enrollHost(api.createEnrollment(other.id).token, inventory);
|
||||
const otherWork = api.queueHostOperation(other.id, 'host.inspect', {});
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const before = db().prepare('SELECT id,state,last_delivered_at FROM ec_host_operations WHERE host_id=? ORDER BY id').all(api.host.id);
|
||||
for (let poll = 0; poll < 3; poll++) expect(api.pendingHostOperations(api.host.id)).toEqual([]);
|
||||
expect(db().prepare('SELECT id,state,last_delivered_at FROM ec_host_operations WHERE host_id=? ORDER BY id').all(api.host.id)).toEqual(before);
|
||||
expect(api.pendingHostOperations(other.id).map(op => op.id)).toEqual([otherWork.id]);
|
||||
api.reportHostOperation(api.host.id, inspect.id, { state: 'completed', result: { healthy: true } });
|
||||
api.heartbeat(api.host.id, inventory);
|
||||
expect(api.pendingHostOperations(api.host.id)).toEqual([]);
|
||||
|
||||
// The controller hold must clear on capability, without waiting for cleanup
|
||||
// or the original creation to finish (which would recreate the deadlock).
|
||||
api.heartbeat(api.host.id, { ...inventory, capabilities: ['instance.cancel-create'] });
|
||||
expect(api.pendingHostOperations(api.host.id).map(op => op.id)).toEqual([cleanup.id, logs.id, preparation.id]);
|
||||
expect(api.getHostOperation(original.id)?.state).toBe('running');
|
||||
expect(api.getHostOperation(cleanup.id)?.state).toBe('pending');
|
||||
});
|
||||
|
||||
it.each(['completed', 'failed'] as const)('does not pause an old host for a terminal %s cancellation', async state => {
|
||||
const api = await enrolled();
|
||||
const cleanup = api.queueHostOperation(api.host.id, 'instance.cancel-create', { createOperationId: 'creation' });
|
||||
api.reportHostOperation(api.host.id, cleanup.id, { state });
|
||||
const work = api.queueHostOperation(api.host.id, 'host.inspect', {});
|
||||
expect(api.pendingHostOperations(api.host.id).map(op => op.id)).toEqual([work.id]);
|
||||
});
|
||||
it('rotates unresolved operation delivery so an old batch cannot starve new work', async () => {
|
||||
const api = await enrolled();
|
||||
const operations = Array.from({ length: 30 }, () => api.queueHostOperation(api.host.id, 'instance.inspect', {}));
|
||||
|
||||
@@ -142,6 +142,12 @@ describe('native instance provisioning', () => {
|
||||
expect(pendingHostOperations(hostId).filter(op => op.kind.startsWith('instance.'))).toEqual([]);
|
||||
await advance();
|
||||
expect((await detail(cleanup)).stage).toContain('Update the host agent');
|
||||
expect((await detail(cleanup)).stage).toContain('Automatic updates are not enabled');
|
||||
heartbeat(hostId, { ...inventory(), capabilities: ['agent.update'],
|
||||
agentUpdate: { enabled: true, state: 'deferred', currentVersion: '1.2.2', targetVersion: '1.3.0' } });
|
||||
await advance();
|
||||
expect((await detail(cleanup)).stage).toContain('New command delivery is paused');
|
||||
expect((await detail(cleanup)).stage).toContain('automatic updater can run');
|
||||
db().prepare('UPDATE ec_hosts SET last_heartbeat=? WHERE id=?').run('2000-01-01T00:00:00.000Z', hostId);
|
||||
await advance();
|
||||
expect((await detail(cleanup)).stage).toContain('unavailable');
|
||||
|
||||
Reference in New Issue
Block a user