Skip to content

Commit 90ce7bd

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 4f11707 commit 90ce7bd

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
@@ -456,8 +456,13 @@ private AgentControlAnswer handleControlCommand(final AgentAttache attache, fina
456456

457457
public void handleCommands(final AgentAttache attache, final long sequence, final Command[] cmds) {
458458
for (final Pair<Integer, Listener> listener : _cmdMonitors) {
459-
final boolean processed = listener.second().processCommands(attache.getId(), sequence, cmds);
460-
logger.trace("SeqA {}-{}: {} by {}", attache.getId(), sequence, (processed ? "processed" : "not processed"), listener.getClass());
459+
try {
460+
final boolean processed = listener.second().processCommands(attache.getId(), sequence, cmds);
461+
logger.trace("SeqA {}-{}: {} by {}", attache.getId(), sequence, (processed ? "processed" : "not processed"), listener.second().getClass());
462+
} catch (final Exception e) {
463+
logger.warn("Listener {} threw an exception processing commands for agent {} seq {}; continuing to next listener",
464+
listener.second().getClass().getName(), attache.getId(), sequence, e);
465+
}
461466
}
462467
}
463468

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.ReadyCommand;
2223
import com.cloud.agent.api.StartupCommand;
2324
import com.cloud.agent.api.StartupRoutingCommand;
@@ -137,4 +138,84 @@ public void testGetHostSshPortWithKVMHostCustomPort() {
137138
int hostSshPort = mgr.getHostSshPort(host);
138139
Assert.assertEquals(3922, hostSshPort);
139140
}
141+
142+
/*
143+
* When the first registered listener throws a RuntimeException,
144+
* handleCommands must catch it, log a warning, and continue so that
145+
* the second listener still runs.
146+
*/
147+
@Test
148+
public void testHandleCommandsListenerExceptionDoesNotAbortLoop() throws Exception {
149+
Listener throwingListener = Mockito.mock(Listener.class);
150+
Mockito.when(throwingListener.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
151+
.thenThrow(new RuntimeException("simulated NPE from power-state sync"));
152+
153+
Listener goodListener = Mockito.mock(Listener.class);
154+
Mockito.when(goodListener.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
155+
.thenReturn(true);
156+
157+
mgr._cmdMonitors = new ArrayList<>();
158+
mgr._cmdMonitors.add(new Pair<>(1, throwingListener));
159+
mgr._cmdMonitors.add(new Pair<>(2, goodListener));
160+
161+
Command[] cmds = new Command[]{Mockito.mock(Command.class)};
162+
163+
// Must not throw; the exception from throwingListener must be swallowed.
164+
mgr.handleCommands(attache, 1L, cmds);
165+
166+
// The second listener must still have been invoked.
167+
Mockito.verify(goodListener, Mockito.times(1))
168+
.processCommands(Mockito.eq(attache.getId()), Mockito.eq(1L), Mockito.eq(cmds));
169+
}
170+
171+
@Test
172+
public void testHandleCommandsAllListenersSucceed() throws Exception {
173+
Listener listenerA = Mockito.mock(Listener.class);
174+
Mockito.when(listenerA.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
175+
.thenReturn(true);
176+
177+
Listener listenerB = Mockito.mock(Listener.class);
178+
Mockito.when(listenerB.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
179+
.thenReturn(false);
180+
181+
mgr._cmdMonitors = new ArrayList<>();
182+
mgr._cmdMonitors.add(new Pair<>(1, listenerA));
183+
mgr._cmdMonitors.add(new Pair<>(2, listenerB));
184+
185+
Command[] cmds = new Command[]{Mockito.mock(Command.class)};
186+
mgr.handleCommands(attache, 1L, cmds);
187+
188+
Mockito.verify(listenerA, Mockito.times(1))
189+
.processCommands(Mockito.eq(attache.getId()), Mockito.eq(1L), Mockito.eq(cmds));
190+
Mockito.verify(listenerB, Mockito.times(1))
191+
.processCommands(Mockito.eq(attache.getId()), Mockito.eq(1L), Mockito.eq(cmds));
192+
}
193+
194+
/*
195+
* Simulates the reported failure: a power-state sync listener throws
196+
* partway through, and a downstream ping-style listener must still run
197+
* so pingBy() isn't starved.
198+
*/
199+
@Test
200+
public void testHandleCommandsThrowingListenerDoesNotStarveDownstreamListener() throws Exception {
201+
Listener powerStateSyncListener = Mockito.mock(Listener.class);
202+
Mockito.when(powerStateSyncListener.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
203+
.thenThrow(new NullPointerException("hostId was null in isPowerStateInSyncWithInstanceState"));
204+
205+
Listener pingListener = Mockito.mock(Listener.class);
206+
Mockito.when(pingListener.processCommands(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()))
207+
.thenReturn(false);
208+
209+
mgr._cmdMonitors = new ArrayList<>();
210+
mgr._cmdMonitors.add(new Pair<>(1, powerStateSyncListener));
211+
mgr._cmdMonitors.add(new Pair<>(2, pingListener));
212+
213+
Command[] cmds = new Command[]{Mockito.mock(Command.class)};
214+
215+
// Before the fix this would abort on the NPE and pingListener would never run.
216+
mgr.handleCommands(attache, 42L, cmds);
217+
218+
Mockito.verify(pingListener, Mockito.times(1))
219+
.processCommands(Mockito.eq(attache.getId()), Mockito.eq(42L), Mockito.eq(cmds));
220+
}
140221
}

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)