Skip to content

Commit 8a090ca

Browse files
mprokopchuksureshanaparti
authored andcommitted
Dedup agent connection requests
Dedupes agent connection requests. Observed scenarios where on agent with old agent code send a storm of connection requests. In such a case, to ensure other agents are not impacted, deduping the connection requests.
1 parent 05f2ab4 commit 8a090ca

6 files changed

Lines changed: 301 additions & 19 deletions

File tree

agent/src/main/java/com/cloud/agent/Agent.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ public int value() {
159159
CopyOnWriteArrayList<IAgentControlListener> controlListeners = new CopyOnWriteArrayList<>();
160160

161161
IAgentShell shell;
162-
NioConnection connection;
162+
NioClient connection;
163163
ServerResource serverResource;
164164
Link link;
165165
Long id;
@@ -857,8 +857,20 @@ public void processStartupAnswer(final StartupAnswer startup, final Response res
857857
if (serverResource != null && !serverResource.isExitOnFailures()) {
858858
logger.trace("{} does not allow exit on failure, reconnecting",
859859
serverResource.getClass().getSimpleName());
860+
// If the MS flagged this as a duplicate connection, retry the same MS using
861+
// the host we just connected to — the agent already knows the correct hostname.
862+
final String preferredHost = startup.isRetryCurrentMs() && connection != null
863+
? connection.getHost() : null;
864+
if (preferredHost != null) {
865+
logger.info("Duplicate connection rejected, will retry same MS: {}", preferredHost);
866+
}
860867
logger.info("Reconnecting for {}", link);
861-
requestHandler.submit(() -> reconnect(link, null, false));
868+
requestHandler.submit(() -> {
869+
if (preferredHost != null) {
870+
shell.getBackoffAlgorithm().waitBeforeRetry();
871+
}
872+
reconnect(link, preferredHost, false);
873+
});
862874
return;
863875
}
864876
logger.fatal("Got unsuccessful result {} from the answer {}, details: {}",

agent/src/test/java/com/cloud/agent/AgentTest.java

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,32 +22,41 @@
2222
import static org.junit.Assert.assertSame;
2323
import static org.junit.Assert.assertTrue;
2424
import static org.mockito.ArgumentMatchers.isA;
25+
import static org.mockito.ArgumentMatchers.isNull;
2526
import static org.mockito.Mockito.any;
27+
import static org.mockito.Mockito.doNothing;
2628
import static org.mockito.Mockito.doReturn;
2729
import static org.mockito.Mockito.doThrow;
2830
import static org.mockito.Mockito.mock;
2931
import static org.mockito.Mockito.eq;
32+
import static org.mockito.Mockito.never;
3033
import static org.mockito.Mockito.times;
3134
import static org.mockito.Mockito.verify;
3235
import static org.mockito.Mockito.when;
3336

3437
import java.io.IOException;
3538
import java.lang.reflect.Field;
3639
import java.net.InetSocketAddress;
40+
import java.util.concurrent.ExecutorService;
3741

3842
import javax.naming.ConfigurationException;
3943

44+
import com.cloud.agent.api.StartupAnswer;
45+
import com.cloud.agent.api.StartupRoutingCommand;
46+
import com.cloud.utils.backoff.BackoffAlgorithm;
47+
import com.cloud.utils.nio.NioClient;
4048
import org.apache.logging.log4j.Logger;
4149
import org.junit.Before;
4250
import org.junit.Test;
4351
import org.junit.runner.RunWith;
52+
import org.mockito.ArgumentCaptor;
53+
import org.mockito.Mockito;
4454
import org.mockito.junit.MockitoJUnitRunner;
4555
import org.springframework.test.util.ReflectionTestUtils;
4656

4757
import com.cloud.resource.ServerResource;
4858
import com.cloud.utils.backoff.impl.ConstantTimeBackoff;
4959
import com.cloud.utils.nio.Link;
50-
import com.cloud.utils.nio.NioConnection;
5160

5261
@RunWith(MockitoJUnitRunner.class)
5362
public class AgentTest {
@@ -222,7 +231,7 @@ public void testStopAndCleanupConnectionConnectionIsNullDoesNothing() {
222231

223232
@Test
224233
public void testStopAndCleanupConnectionValidConnectionNoWaitStopsAndCleansUp() throws IOException {
225-
NioConnection mockConnection = mock(NioConnection.class);
234+
NioClient mockConnection = mock(NioClient.class);
226235
agent.connection = mockConnection;
227236
agent.stopAndCleanupConnection();
228237
verify(mockConnection).stop();
@@ -231,7 +240,7 @@ public void testStopAndCleanupConnectionValidConnectionNoWaitStopsAndCleansUp()
231240

232241
@Test
233242
public void testStopAndCleanupConnectionCleanupThrowsIOExceptionLogsWarning() throws IOException {
234-
NioConnection mockConnection = mock(NioConnection.class);
243+
NioClient mockConnection = mock(NioClient.class);
235244
agent.connection = mockConnection;
236245
doThrow(new IOException("Cleanup failed")).when(mockConnection).cleanUp();
237246
agent.stopAndCleanupConnection();
@@ -241,7 +250,7 @@ public void testStopAndCleanupConnectionCleanupThrowsIOExceptionLogsWarning() th
241250

242251
@Test
243252
public void testStopAndCleanupConnectionValidConnectionWaitForStopWaitsForStartupToStop() throws IOException {
244-
NioConnection mockConnection = mock(NioConnection.class);
253+
NioClient mockConnection = mock(NioClient.class);
245254
ConstantTimeBackoff mockBackoff = mock(ConstantTimeBackoff.class);
246255
mockBackoff.setTimeToWait(0);
247256
agent.connection = mockConnection;
@@ -290,4 +299,77 @@ public void testSelectReconnectionHostWithNullSocketAddressUsesShellNextHost() {
290299
assertEquals("fallback.host.com", result);
291300
verify(shell, times(1)).getNextHost();
292301
}
302+
303+
private Agent setupAgentSpyForStartupAnswerTests(String currentHost) throws ConfigurationException {
304+
Agent spyAgent = Mockito.spy(agent);
305+
doNothing().when(spyAgent).reconnect(any(), any(), eq(false));
306+
307+
NioClient mockConnection = mock(NioClient.class);
308+
when(mockConnection.getHost()).thenReturn(currentHost);
309+
spyAgent.connection = mockConnection;
310+
311+
spyAgent.requestHandler = mock(ExecutorService.class);
312+
when(serverResource.isExitOnFailures()).thenReturn(false);
313+
when(shell.getBackoffAlgorithm()).thenReturn(mock(BackoffAlgorithm.class));
314+
return spyAgent;
315+
}
316+
317+
@Test
318+
public void testProcessStartupAnswerRetryCurrentMsReconnectsToCurrentHost() throws Exception {
319+
String currentHost = "ms-host-0.example.com";
320+
Agent spyAgent = setupAgentSpyForStartupAnswerTests(currentHost);
321+
Link link = mock(Link.class);
322+
323+
StartupAnswer answer = new StartupAnswer(new StartupRoutingCommand(), "duplicate");
324+
answer.setRetryCurrentMs(true);
325+
326+
spyAgent.processStartupAnswer(answer, null, link);
327+
328+
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
329+
verify(spyAgent.requestHandler).submit(captor.capture());
330+
captor.getValue().run();
331+
332+
verify(spyAgent).reconnect(eq(link), eq(currentHost), eq(false));
333+
}
334+
335+
@Test
336+
public void testProcessStartupAnswerRetryCurrentMsAppliesBackoffBeforeReconnect() throws Exception {
337+
Agent spyAgent = setupAgentSpyForStartupAnswerTests("ms-host-0.example.com");
338+
Link link = mock(Link.class);
339+
340+
BackoffAlgorithm mockBackoff = mock(BackoffAlgorithm.class);
341+
when(shell.getBackoffAlgorithm()).thenReturn(mockBackoff);
342+
343+
StartupAnswer answer = new StartupAnswer(new StartupRoutingCommand(), "duplicate");
344+
answer.setRetryCurrentMs(true);
345+
346+
spyAgent.processStartupAnswer(answer, null, link);
347+
348+
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
349+
verify(spyAgent.requestHandler).submit(captor.capture());
350+
captor.getValue().run();
351+
352+
verify(mockBackoff).waitBeforeRetry();
353+
}
354+
355+
@Test
356+
public void testProcessStartupAnswerNoRetryCurrentMsReconnectsWithNullAndNoBackoff() throws Exception {
357+
Agent spyAgent = setupAgentSpyForStartupAnswerTests("ms-host-0.example.com");
358+
Link link = mock(Link.class);
359+
360+
BackoffAlgorithm mockBackoff = mock(BackoffAlgorithm.class);
361+
when(shell.getBackoffAlgorithm()).thenReturn(mockBackoff);
362+
363+
StartupAnswer answer = new StartupAnswer(new StartupRoutingCommand(), "not a duplicate");
364+
// retryCurrentMs defaults to false
365+
366+
spyAgent.processStartupAnswer(answer, null, link);
367+
368+
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
369+
verify(spyAgent.requestHandler).submit(captor.capture());
370+
captor.getValue().run();
371+
372+
verify(spyAgent).reconnect(eq(link), isNull(), eq(false));
373+
verify(mockBackoff, never()).waitBeforeRetry();
374+
}
293375
}

api/src/main/java/com/cloud/agent/api/Answer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,18 @@ public boolean getResult() {
4343
return result;
4444
}
4545

46+
public void setResult(boolean result) {
47+
this.result = result;
48+
}
49+
4650
public String getDetails() {
4751
return details;
4852
}
4953

54+
public void setDetails(String details) {
55+
this.details = details;
56+
}
57+
5058
@Override
5159
public boolean executeInSequence() {
5260
return false;

core/src/main/java/com/cloud/agent/api/StartupAnswer.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class StartupAnswer extends Answer {
3030

3131
Integer agentHostStatusCheckDelaySec;
3232
private Map<String, String> params;
33+
private boolean retryCurrentMs;
3334

3435
protected StartupAnswer() {
3536
params = new HashMap<>();
@@ -80,4 +81,12 @@ public Integer getAgentHostStatusCheckDelaySec() {
8081
public void setAgentHostStatusCheckDelaySec(Integer agentHostStatusCheckDelaySec) {
8182
this.agentHostStatusCheckDelaySec = agentHostStatusCheckDelaySec;
8283
}
84+
85+
public boolean isRetryCurrentMs() {
86+
return retryCurrentMs;
87+
}
88+
89+
public void setRetryCurrentMs(boolean retryCurrentMs) {
90+
this.retryCurrentMs = retryCurrentMs;
91+
}
8392
}

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

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.concurrent.ExecutorService;
3838
import java.util.concurrent.Executors;
3939
import java.util.concurrent.LinkedBlockingQueue;
40+
import java.util.concurrent.RejectedExecutionException;
4041
import java.util.concurrent.ScheduledExecutorService;
4142
import java.util.concurrent.ScheduledThreadPoolExecutor;
4243
import java.util.concurrent.ThreadPoolExecutor;
@@ -251,6 +252,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl
251252
private int maxConcurrentNewAgentConnections;
252253
private final ConcurrentHashMap<String, Long> newAgentConnections = new ConcurrentHashMap<>();
253254
protected ScheduledExecutorService newAgentConnectionsMonitor;
255+
private final ConcurrentHashMap<String, Boolean> _processingAgentGuids = new ConcurrentHashMap<>();
254256

255257
private boolean _reconcileCommandsEnabled = false;
256258
private Integer _reconcileCommandInterval;
@@ -2005,37 +2007,93 @@ protected void runInContext() {
20052007
startups[i] = (StartupCommand)_cmds[i];
20062008
}
20072009

2008-
AgentAttache attache = handleConnectedAgent(_link, startups, _request);
2009-
if (attache == null) {
2010-
logger.warn("Unable to create attache for agent: {}", _request);
2010+
if (_link.isTerminated()) {
2011+
logger.warn("Link is already terminated for agent: {}, skipping connection processing", _request);
2012+
if (startups[0] != null && startups[0].getGuid() != null) {
2013+
_processingAgentGuids.remove(startups[0].getGuid());
2014+
}
2015+
unregisterNewConnection(_link.getSocketAddress());
2016+
return;
2017+
}
2018+
2019+
try {
2020+
AgentAttache attache = handleConnectedAgent(_link, startups, _request);
2021+
if (attache == null) {
2022+
logger.warn("Unable to create attache for agent: {}", _request);
2023+
}
2024+
} finally {
2025+
if (startups[0] != null && startups[0].getGuid() != null) {
2026+
_processingAgentGuids.remove(startups[0].getGuid());
2027+
}
2028+
unregisterNewConnection(_link.getSocketAddress());
20112029
}
2012-
unregisterNewConnection(_link.getSocketAddress());
20132030
}
20142031
}
20152032

20162033
protected void connectAgent(final Link link, final Command[] cmds, final Request request) {
20172034
// send startup answer to agent in the very beginning, so agent can move on without waiting for the answer for an undetermined time,
20182035
// if we put this logic into another thread pool.
2019-
Map<String, String> backoffConfiguration = ConfigKeyUtil.toMap(BackoffConfiguration.value());
2036+
// Build the startup answer — populated for all cases (success and reject)
20202037
StartupAnswer[] answers = new StartupAnswer[cmds.length];
2021-
Command cmd;
20222038
for (int i = 0; i < cmds.length; i++) {
2023-
cmd = cmds[i];
2024-
if (cmd instanceof StartupRoutingCommand || cmd instanceof StartupProxyCommand || cmd instanceof StartupSecondaryStorageCommand
2025-
|| cmd instanceof StartupStorageCommand) {
2039+
Command cmd = cmds[i];
2040+
if (cmd instanceof StartupRoutingCommand || cmd instanceof StartupProxyCommand
2041+
|| cmd instanceof StartupSecondaryStorageCommand || cmd instanceof StartupStorageCommand) {
20262042
StartupAnswer answer = new StartupAnswer((StartupCommand) cmds[i], 0, "", "", mgmtServiceConf.getPingInterval());
2027-
answer.setParams(backoffConfiguration);
2043+
answer.setParams(ConfigKeyUtil.toMap(BackoffConfiguration.value()));
20282044
answer.setAgentHostStatusCheckDelaySec(AgentHostStatusCheckDelay.value());
20292045
answers[i] = answer;
20302046
}
20312047
}
2032-
Response response = new Response(request, answers[0], _nodeId, -1);
2048+
2049+
if (answers[0] == null) {
2050+
// The leading command has no startup answer (an unhandled StartupCommand subtype, or a
2051+
// non-startup command as cmds[0]). Avoid the NullPointerException in the send/getResult
2052+
// path below and release the connection slot before returning.
2053+
logger.warn("No startup answer built for leading command {}; skipping connect processing", cmds[0] != null ? cmds[0].getClass().getSimpleName() : "null");
2054+
unregisterNewConnection(link.getSocketAddress());
2055+
return;
2056+
}
2057+
2058+
// Dedup: if this agent GUID is already being processed, reject and redirect back to this MS
2059+
String agentGuid = null;
2060+
for (Command cmd : cmds) {
2061+
if (cmd instanceof StartupCommand) {
2062+
agentGuid = ((StartupCommand) cmd).getGuid();
2063+
break;
2064+
}
2065+
}
2066+
if (agentGuid != null && _processingAgentGuids.putIfAbsent(agentGuid, Boolean.TRUE) != null) {
2067+
logger.info("Duplicate connection from agent GUID {}, rejecting and redirecting to same MS", agentGuid);
2068+
answers[0].setResult(false);
2069+
answers[0].setDetails("Duplicate connection, retry this MS");
2070+
answers[0].setRetryCurrentMs(true);
2071+
}
2072+
20332073
try {
2034-
link.send(response.toBytes());
2074+
link.send(new Response(request, answers[0], _nodeId, -1).toBytes());
20352075
} catch (ClosedChannelException e) {
20362076
logger.debug("Failed to send startup answer: {}", e.getMessage(), e);
20372077
}
2038-
_connectExecutor.execute(new HandleAgentConnectTask(link, cmds, request));
2078+
2079+
if (answers[0].getResult()) {
2080+
try {
2081+
_connectExecutor.execute(new HandleAgentConnectTask(link, cmds, request));
2082+
} catch (RejectedExecutionException e) {
2083+
// The task that would release the dedup GUID and the new-connection slot will not run,
2084+
// so clean them up here to avoid leaking the connection and permanently stranding the GUID.
2085+
logger.warn("Failed to schedule agent connect task for GUID {}; releasing dedup state", agentGuid, e);
2086+
if (agentGuid != null) {
2087+
_processingAgentGuids.remove(agentGuid);
2088+
}
2089+
unregisterNewConnection(link.getSocketAddress());
2090+
}
2091+
} else {
2092+
// Duplicate/rejected connection: HandleAgentConnectTask (which unregisters the connection) is
2093+
// not scheduled, so release the new-connection slot here. The GUID is owned by the in-flight
2094+
// task that claimed it first, so it is intentionally not removed here.
2095+
unregisterNewConnection(link.getSocketAddress());
2096+
}
20392097
}
20402098

20412099
public class AgentHandler extends Task {

0 commit comments

Comments
 (0)