Skip to content

Commit 57a5ff8

Browse files
committed
Enhance resilience in command handling by catching exceptions from listeners and ensuring downstream listeners are invoked. Add tests to verify behavior with null hostId and exception handling.
1 parent 7ed0cec commit 57a5ff8

4 files changed

Lines changed: 136 additions & 2 deletions

File tree

‎engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,13 @@ private AgentControlAnswer handleControlCommand(final AgentAttache attache, fina
460460

461461
public void handleCommands(final AgentAttache attache, final long sequence, final Command[] cmds) {
462462
for (final Pair<Integer, Listener> listener : _cmdMonitors) {
463-
final boolean processed = listener.second().processCommands(attache.getId(), sequence, cmds);
464-
logger.trace("SeqA {}-{}: {} by {}", attache.getId(), sequence, (processed ? "processed" : "not processed"), listener.getClass());
463+
try {
464+
final boolean processed = listener.second().processCommands(attache.getId(), sequence, cmds);
465+
logger.trace("SeqA {}-{}: {} by {}", attache.getId(), sequence, (processed ? "processed" : "not processed"), listener.second().getClass());
466+
} catch (final Exception e) {
467+
logger.warn("Listener {} threw an exception processing commands for agent {} seq {}; continuing to next listener",
468+
listener.second().getClass().getName(), attache.getId(), sequence, e);
469+
}
465470
}
466471
}
467472

‎engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.cloud.agent.Listener;
2020
import com.cloud.agent.api.Answer;
21+
import com.cloud.agent.api.Command;
2122
import com.cloud.agent.api.ReadyAnswer;
2223
import com.cloud.agent.api.ReadyCommand;
2324
import com.cloud.agent.api.StartupCommand;
@@ -172,4 +173,84 @@ public void testGetHostSshPortWithKVMHostCustomPort() {
172173
int hostSshPort = mgr.getHostSshPort(host);
173174
Assert.assertEquals(3922, hostSshPort);
174175
}
176+
177+
/*
178+
* When the first registered listener throws a RuntimeException,
179+
* handleCommands must catch it, log a warning, and continue so that
180+
* the second listener still runs.
181+
*/
182+
@Test
183+
public void testHandleCommandsListenerExceptionDoesNotAbortLoop() throws Exception {
184+
Listener throwingListener = Mockito.mock(Listener.class);
185+
Mockito.when(throwingListener.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
186+
.thenThrow(new RuntimeException("simulated NPE from power-state sync"));
187+
188+
Listener goodListener = Mockito.mock(Listener.class);
189+
Mockito.when(goodListener.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
190+
.thenReturn(true);
191+
192+
mgr._cmdMonitors = new ArrayList<>();
193+
mgr._cmdMonitors.add(new Pair<>(1, throwingListener));
194+
mgr._cmdMonitors.add(new Pair<>(2, goodListener));
195+
196+
Command[] cmds = new Command[]{Mockito.mock(Command.class)};
197+
198+
// Must not throw; the exception from throwingListener must be swallowed.
199+
mgr.handleCommands(attache, 1L, cmds);
200+
201+
// The second listener must still have been invoked.
202+
Mockito.verify(goodListener, Mockito.times(1))
203+
.processCommands(Mockito.eq(attache.getId()), Mockito.eq(1L), Mockito.eq(cmds));
204+
}
205+
206+
@Test
207+
public void testHandleCommandsAllListenersSucceed() throws Exception {
208+
Listener listenerA = Mockito.mock(Listener.class);
209+
Mockito.when(listenerA.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
210+
.thenReturn(true);
211+
212+
Listener listenerB = Mockito.mock(Listener.class);
213+
Mockito.when(listenerB.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
214+
.thenReturn(false);
215+
216+
mgr._cmdMonitors = new ArrayList<>();
217+
mgr._cmdMonitors.add(new Pair<>(1, listenerA));
218+
mgr._cmdMonitors.add(new Pair<>(2, listenerB));
219+
220+
Command[] cmds = new Command[]{Mockito.mock(Command.class)};
221+
mgr.handleCommands(attache, 1L, cmds);
222+
223+
Mockito.verify(listenerA, Mockito.times(1))
224+
.processCommands(Mockito.eq(attache.getId()), Mockito.eq(1L), Mockito.eq(cmds));
225+
Mockito.verify(listenerB, Mockito.times(1))
226+
.processCommands(Mockito.eq(attache.getId()), Mockito.eq(1L), Mockito.eq(cmds));
227+
}
228+
229+
/*
230+
* Simulates the reported failure: a power-state sync listener throws
231+
* partway through, and a downstream ping-style listener must still run
232+
* so pingBy() isn't starved.
233+
*/
234+
@Test
235+
public void testHandleCommandsThrowingListenerDoesNotStarveDownstreamListener() throws Exception {
236+
Listener powerStateSyncListener = Mockito.mock(Listener.class);
237+
Mockito.when(powerStateSyncListener.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
238+
.thenThrow(new NullPointerException("hostId was null in isPowerStateInSyncWithInstanceState"));
239+
240+
Listener pingListener = Mockito.mock(Listener.class);
241+
Mockito.when(pingListener.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
242+
.thenReturn(false);
243+
244+
mgr._cmdMonitors = new ArrayList<>();
245+
mgr._cmdMonitors.add(new Pair<>(1, powerStateSyncListener));
246+
mgr._cmdMonitors.add(new Pair<>(2, pingListener));
247+
248+
Command[] cmds = new Command[]{Mockito.mock(Command.class)};
249+
250+
// Before the fix this would abort on the NPE and pingListener would never run.
251+
mgr.handleCommands(attache, 42L, cmds);
252+
253+
Mockito.verify(pingListener, Mockito.times(1))
254+
.processCommands(Mockito.eq(attache.getId()), Mockito.eq(42L), Mockito.eq(cmds));
255+
}
175256
}

‎engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ protected void init() {
355355
IdsPowerStateSelectSearch = createSearchBuilder();
356356
IdsPowerStateSelectSearch.and("id", IdsPowerStateSelectSearch.entity().getId(), Op.IN);
357357
IdsPowerStateSelectSearch.selectFields(IdsPowerStateSelectSearch.entity().getId(),
358+
IdsPowerStateSelectSearch.entity().getHostId(),
358359
IdsPowerStateSelectSearch.entity().getPowerHostId(),
359360
IdsPowerStateSelectSearch.entity().getPowerState(),
360361
IdsPowerStateSelectSearch.entity().getPowerStateUpdateCount(),

‎engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDaoImplTest.java‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,53 @@ public void testUpdatePowerStateNoChangeMaxUpdatesInvalidStateVmRunning() {
210210
assertTrue(result);
211211
}
212212

213+
/*
214+
* Mirrors testUpdatePowerStateNoChangeMaxUpdatesInvalidStateVmStopped but with a null
215+
* hostId (the default from setUp), which is what happens when the row was loaded through
216+
* a partial-select projection that doesn't include hostId. isPowerStateInSyncWithInstanceState
217+
* must not NPE on the null hostId, and must still detect the state is out-of-sync.
218+
*/
219+
@Test
220+
public void testUpdatePowerStateNoChangeMaxUpdatesInvalidStateVmStoppedNullHostId() {
221+
when(vm.getPowerStateUpdateTime()).thenReturn(null);
222+
when(vm.getPowerHostId()).thenReturn(1L);
223+
when(vm.getPowerState()).thenReturn(VirtualMachine.PowerState.PowerOn);
224+
when(vm.getState()).thenReturn(Stopped);
225+
doReturn(vm).when(vmInstanceDao).findById(anyLong());
226+
doReturn(true).when(vmInstanceDao).update(anyLong(), any());
227+
228+
boolean result = vmInstanceDao.updatePowerState(1L, 1L, VirtualMachine.PowerState.PowerOn, new Date());
229+
230+
verify(vm, times(1)).setPowerState(any());
231+
verify(vm, times(1)).setPowerHostId(anyLong());
232+
verify(vm, times(1)).setPowerStateUpdateCount(1);
233+
verify(vm, times(1)).setPowerStateUpdateTime(any(Date.class));
234+
235+
assertTrue(result);
236+
}
237+
238+
/*
239+
* Mirrors testUpdatePowerStateNoChangeMaxUpdatesInvalidStateVmRunning but with a null hostId.
240+
*/
241+
@Test
242+
public void testUpdatePowerStateNoChangeMaxUpdatesInvalidStateVmRunningNullHostId() {
243+
when(vm.getPowerStateUpdateTime()).thenReturn(null);
244+
when(vm.getPowerHostId()).thenReturn(1L);
245+
when(vm.getPowerState()).thenReturn(VirtualMachine.PowerState.PowerOff);
246+
when(vm.getState()).thenReturn(Running);
247+
doReturn(vm).when(vmInstanceDao).findById(anyLong());
248+
doReturn(true).when(vmInstanceDao).update(anyLong(), any());
249+
250+
boolean result = vmInstanceDao.updatePowerState(1L, 1L, VirtualMachine.PowerState.PowerOff, new Date());
251+
252+
verify(vm, times(1)).setPowerState(any());
253+
verify(vm, times(1)).setPowerHostId(anyLong());
254+
verify(vm, times(1)).setPowerStateUpdateCount(1);
255+
verify(vm, times(1)).setPowerStateUpdateTime(any(Date.class));
256+
257+
assertTrue(result);
258+
}
259+
213260
@Test
214261
public void testSearchRemovedByRemoveDate() {
215262
SearchBuilder<VMInstanceVO> sb = Mockito.mock(SearchBuilder.class);

0 commit comments

Comments
 (0)