Skip to content

Commit ba53cba

Browse files
mprokopchuksb-abhish3k
authored andcommitted
Fix concurrent publish bottleneck and agent ping task leaks
MessageBusBase: replaced exclusive Gate with ReadWriteLock so multiple publishers can run in parallel. Subscriber callbacks are now invoked outside the lock to prevent slow hypervisor callbacks from starving write lock holders (subscribe/unsubscribe). Under heavy reconnect load (pod restart with many hosts) the old design serialized all publish() callers through a single gate, blocking API threads and causing full management server unresponsiveness. DirectAgentAttache: fixed a race where PingTask could be scheduled after disconnect() had already cleared _futures, causing it to never be cancelled. Switched PingTask scheduling from scheduleAtFixedRate to scheduleWithFixedDelay so slow hypervisor responses do not pile up concurrent ping executions and exhaust the cron thread pool.
1 parent 8261bec commit ba53cba

4 files changed

Lines changed: 318 additions & 258 deletions

File tree

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

Lines changed: 81 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
import java.util.concurrent.ScheduledFuture;
2323
import java.util.concurrent.TimeUnit;
2424
import java.util.concurrent.atomic.AtomicInteger;
25+
import java.util.concurrent.atomic.AtomicLong;
2526

2627
import org.apache.cloudstack.framework.config.ConfigKey;
2728
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
29+
import org.apache.logging.log4j.ThreadContext;
2830

2931
import com.cloud.agent.api.Answer;
3032
import com.cloud.agent.api.Command;
@@ -36,20 +38,26 @@
3638
import com.cloud.exception.AgentUnavailableException;
3739
import com.cloud.host.Status;
3840
import com.cloud.resource.ServerResource;
39-
import org.apache.logging.log4j.ThreadContext;
4041

4142
public class DirectAgentAttache extends AgentAttache {
4243

4344
protected final ConfigKey<Integer> _HostPingRetryCount = new ConfigKey<Integer>("Advanced", Integer.class, "host.ping.retry.count", "0",
4445
"Number of times retrying a host ping while waiting for check results", true);
4546
protected final ConfigKey<Integer> _HostPingRetryTimer = new ConfigKey<Integer>("Advanced", Integer.class, "host.ping.retry.timer", "5",
4647
"Interval to wait before retrying a host ping while waiting for check results", true);
47-
ServerResource _resource;
48-
List<ScheduledFuture<?>> _futures = new ArrayList<ScheduledFuture<?>>();
49-
long _seq = 0;
50-
LinkedList<Task> tasks = new LinkedList<Task>();
51-
AtomicInteger _outstandingTaskCount;
52-
AtomicInteger _outstandingCronTaskCount;
48+
// volatile so that isClosed() and PingTask/CronTask can read it without a lock.
49+
// All writes go through _futuresLock to stay atomic with futures management.
50+
private volatile ServerResource _resource;
51+
private final List<ScheduledFuture<?>> _futures = new ArrayList<ScheduledFuture<?>>();
52+
// Separate lock for futures and _resource state. We intentionally do NOT use
53+
// synchronized(this) here because disconnect() calls resource.disconnected() which
54+
// can be slow (hypervisor roundtrip). A dedicated lock keeps that slow call outside
55+
// the critical section so process() and send() are not blocked by it.
56+
private final Object _futuresLock = new Object();
57+
private final AtomicLong _seq = new AtomicLong(0);
58+
private final LinkedList<Task> tasks = new LinkedList<Task>();
59+
private final AtomicInteger _outstandingTaskCount;
60+
private final AtomicInteger _outstandingCronTaskCount;
5361

5462
public DirectAgentAttache(AgentManagerImpl agentMgr, long id, String uuid,String name, ServerResource resource, boolean maintenance) {
5563
super(agentMgr, id, uuid, name, maintenance);
@@ -62,15 +70,21 @@ public DirectAgentAttache(AgentManagerImpl agentMgr, long id, String uuid,String
6270
public void disconnect(Status state) {
6371
logger.debug("Processing disconnect [id: {}, uuid: {}, name: {}]", _id, _uuid, _name);
6472

65-
for (ScheduledFuture<?> future : _futures) {
66-
future.cancel(false);
67-
}
68-
69-
synchronized (this) {
70-
if (_resource != null) {
71-
_resource.disconnected();
72-
_resource = null;
73+
// Capture the resource reference and null it out atomically with futures cleanup,
74+
// so that process() and send() cannot sneak in a new scheduled task after we clear.
75+
// We call resource.disconnected() outside the lock intentionally - it can be slow
76+
// (calls into the hypervisor driver), and we don't want to hold _futuresLock during that.
77+
ServerResource resource;
78+
synchronized (_futuresLock) {
79+
for (ScheduledFuture<?> future : _futures) {
80+
future.cancel(false);
7381
}
82+
_futures.clear();
83+
resource = _resource;
84+
_resource = null;
85+
}
86+
if (resource != null) {
87+
resource.disconnected();
7488
}
7589
}
7690

@@ -83,7 +97,7 @@ public boolean equals(Object obj) {
8397
}
8498

8599
@Override
86-
public synchronized boolean isClosed() {
100+
public boolean isClosed() {
87101
return _resource == null;
88102
}
89103

@@ -96,7 +110,14 @@ public void send(Request req) throws AgentUnavailableException {
96110
if (answers != null && answers[0] instanceof StartupAnswer) {
97111
StartupAnswer startup = (StartupAnswer)answers[0];
98112
int interval = startup.getPingInterval();
99-
_futures.add(_agentMgr.getCronJobPool().scheduleAtFixedRate(new PingTask(), interval, interval, TimeUnit.SECONDS));
113+
synchronized (_futuresLock) {
114+
if (!isClosed()) {
115+
// scheduleWithFixedDelay - next ping starts only after the previous one
116+
// finishes. scheduleAtFixedRate would pile up concurrent pings if the
117+
// hypervisor is slow, eventually exhausting the cron thread pool.
118+
_futures.add(_agentMgr.getCronJobPool().scheduleWithFixedDelay(new PingTask(), interval, interval, TimeUnit.SECONDS));
119+
}
120+
}
100121
}
101122
} else {
102123
Command[] cmds = req.getCommands();
@@ -105,7 +126,11 @@ public void send(Request req) throws AgentUnavailableException {
105126
scheduleFromQueue();
106127
} else {
107128
CronCommand cmd = (CronCommand)cmds[0];
108-
_futures.add(_agentMgr.getCronJobPool().scheduleAtFixedRate(new CronTask(req), cmd.getInterval(), cmd.getInterval(), TimeUnit.SECONDS));
129+
synchronized (_futuresLock) {
130+
if (!isClosed()) {
131+
_futures.add(_agentMgr.getCronJobPool().scheduleAtFixedRate(new CronTask(req), cmd.getInterval(), cmd.getInterval(), TimeUnit.SECONDS));
132+
}
133+
}
109134
}
110135
}
111136
}
@@ -115,20 +140,27 @@ public void process(Answer[] answers) {
115140
if (answers != null && answers[0] instanceof StartupAnswer) {
116141
StartupAnswer startup = (StartupAnswer)answers[0];
117142
int interval = startup.getPingInterval();
118-
logger.info("StartupAnswer received [id: {}, uuid: {}, name: {}, interval: {}]", startup.getHostId(), startup.getHostUuid(), startup.getHostName(), interval);
119-
_futures.add(_agentMgr.getCronJobPool().scheduleAtFixedRate(new PingTask(), interval, interval, TimeUnit.SECONDS));
143+
logger.info(String.format(
144+
"StartupAnswer received [id: %d, uuid: %s, name: %s, interval: %d]",
145+
startup.getHostId(), startup.getHostUuid(), startup.getHostName(), interval));
146+
synchronized (_futuresLock) {
147+
if (!isClosed()) {
148+
// scheduleWithFixedDelay - next ping starts only after the previous one
149+
// finishes. scheduleAtFixedRate would pile up concurrent pings if the
150+
// hypervisor is slow, eventually exhausting the cron thread pool.
151+
_futures.add(_agentMgr.getCronJobPool().scheduleWithFixedDelay(new PingTask(), interval, interval, TimeUnit.SECONDS));
152+
}
153+
}
120154
}
121155
}
122156

123157
@Override
124158
protected void finalize() throws Throwable {
125159
try {
126160
assert _resource == null : "Come on now....If you're going to dabble in agent code, you better know how to close out our resources. Ever considered why there's a method called disconnect()?";
127-
synchronized (this) {
128-
if (_resource != null) {
129-
logger.warn("Lost attache for [id: {}, uuid: {}, name: {}]", _id, _uuid, _name);
130-
disconnect(Status.Alert);
131-
}
161+
if (_resource != null) {
162+
logger.warn(String.format("Lost attache for [id: %d, uuid: %s, name: %s]", _id, _uuid, _name));
163+
disconnect(Status.Alert);
132164
}
133165
} finally {
134166
super.finalize();
@@ -143,8 +175,19 @@ private synchronized void scheduleFromQueue() {
143175
logger.trace("Agent attache [id: {}, uuid: {}, name: {}], task queue size={}, outstanding tasks={}",
144176
_id, _uuid, _name, tasks.size(), _outstandingTaskCount.get());
145177
while (!tasks.isEmpty() && _outstandingTaskCount.get() < _agentMgr.getDirectAgentThreadCap()) {
178+
Task task = tasks.removeFirst();
146179
_outstandingTaskCount.incrementAndGet();
147-
_agentMgr.getDirectAgentPool().execute(tasks.remove());
180+
try {
181+
_agentMgr.getDirectAgentPool().execute(task);
182+
} catch (RuntimeException e) {
183+
// The thread pool rejected the task (likely full or shutting down).
184+
// Roll back: return the slot and put the task back at the head of the queue
185+
// so it gets a chance to run on the next scheduleFromQueue() call.
186+
_outstandingTaskCount.decrementAndGet();
187+
tasks.addFirst(task);
188+
logger.warn("Failed to submit direct agent task, will retry on next schedule", e);
189+
break;
190+
}
148191
}
149192
}
150193

@@ -155,12 +198,12 @@ public int hashCode() {
155198

156199
protected class PingTask extends ManagedContextRunnable {
157200
@Override
158-
protected synchronized void runInContext() {
201+
protected void runInContext() {
159202
try {
160-
if (_outstandingCronTaskCount.incrementAndGet() >= _agentMgr.getDirectAgentThreadCap()) {
161-
logger.warn(
162-
"PingTask execution for direct attache [id: {}, uuid: {}, name: {}] has reached maximum outstanding limit({}), bailing out",
163-
_id, _uuid, _name, _agentMgr.getDirectAgentThreadCap());
203+
if (_outstandingCronTaskCount.incrementAndGet() > _agentMgr.getDirectAgentThreadCap()) {
204+
logger.warn(String.format(
205+
"PingTask execution for direct attache [id: %d, uuid: %s, name: %s] has reached maximum outstanding limit(%d), bailing out",
206+
_id, _uuid, _name, _agentMgr.getDirectAgentThreadCap()));
164207
return;
165208
}
166209

@@ -183,7 +226,7 @@ protected synchronized void runInContext() {
183226
ThreadContext.put("logcontextid", cmd.getContextParam("logid"));
184227
}
185228
logger.debug("Ping from [id: {}, uuid: {}, name: {}]", _id, _uuid, _name);
186-
long seq = _seq++;
229+
long seq = _seq.getAndIncrement();
187230

188231
logger.trace("SeqA {}-{}: {}", _id, seq, new Request(_id, -1, cmd, false).toString());
189232

@@ -200,8 +243,7 @@ protected synchronized void runInContext() {
200243
}
201244

202245
protected class CronTask extends ManagedContextRunnable {
203-
Request _req;
204-
246+
private final Request _req;
205247
public CronTask(Request req) {
206248
_req = req;
207249
}
@@ -226,10 +268,10 @@ private void bailout() {
226268
protected void runInContext() {
227269
long seq = _req.getSequence();
228270
try {
229-
if (_outstandingCronTaskCount.incrementAndGet() >= _agentMgr.getDirectAgentThreadCap()) {
230-
logger.warn(
231-
"CronTask execution for direct attache [id: {}, uuid: {}, name: {}] has reached maximum outstanding limit({}), bailing out",
232-
_id, _uuid, _name, _agentMgr.getDirectAgentThreadCap());
271+
if (_outstandingCronTaskCount.incrementAndGet() > _agentMgr.getDirectAgentThreadCap()) {
272+
logger.warn(String.format(
273+
"CronTask execution for direct attache [id: %d, uuid: %s, name: %s] has reached maximum outstanding limit(%d), bailing out",
274+
_id, _uuid, _name, _agentMgr.getDirectAgentThreadCap()));
233275
bailout();
234276
return;
235277
}
@@ -282,8 +324,7 @@ protected void runInContext() {
282324
}
283325

284326
protected class Task extends ManagedContextRunnable {
285-
Request _req;
286-
327+
private final Request _req;
287328
public Task(Request req) {
288329
_req = req;
289330
}

engine/orchestration/src/test/java/com/cloud/agent/manager/DirectAgentAttacheTest.java

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616
// under the License.
1717
package com.cloud.agent.manager;
1818

19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.ArgumentMatchers.anyLong;
21+
import static org.mockito.ArgumentMatchers.eq;
22+
23+
import java.util.UUID;
24+
import java.util.concurrent.ScheduledExecutorService;
25+
import java.util.concurrent.ScheduledFuture;
26+
import java.util.concurrent.TimeUnit;
27+
1928
import org.junit.Before;
2029
import org.junit.Test;
2130
import org.junit.runner.RunWith;
@@ -24,10 +33,11 @@
2433
import org.mockito.MockitoAnnotations;
2534
import org.mockito.junit.MockitoJUnitRunner;
2635

36+
import com.cloud.agent.api.Answer;
37+
import com.cloud.agent.api.StartupAnswer;
38+
import com.cloud.host.Status;
2739
import com.cloud.resource.ServerResource;
2840

29-
import java.util.UUID;
30-
3141
@RunWith(MockitoJUnitRunner.class)
3242
public class DirectAgentAttacheTest {
3343
@Mock
@@ -36,6 +46,12 @@ public class DirectAgentAttacheTest {
3646
@Mock
3747
private ServerResource _resource;
3848

49+
@Mock
50+
private ScheduledExecutorService _cronJobPool;
51+
52+
@Mock
53+
private ScheduledFuture<?> _future;
54+
3955
long _id = 0L;
4056

4157
String _uuid = UUID.randomUUID().toString();
@@ -55,4 +71,31 @@ public void testPingTask() throws Exception {
5571
pt.runInContext();
5672
Mockito.verify(_resource, Mockito.times(1)).getCurrentStatus(_id);
5773
}
74+
75+
@Test
76+
public void testProcessSchedulesPingWhenConnected() {
77+
Mockito.doReturn(_cronJobPool).when(_agentMgr).getCronJobPool();
78+
Mockito.doReturn(_future).when(_cronJobPool).scheduleWithFixedDelay(any(Runnable.class), anyLong(), anyLong(), any(TimeUnit.class));
79+
80+
directAgentAttache.process(new Answer[] {buildStartupAnswer()});
81+
82+
Mockito.verify(_cronJobPool, Mockito.times(1)).scheduleWithFixedDelay(any(Runnable.class), eq(60L), eq(60L), eq(TimeUnit.SECONDS));
83+
}
84+
85+
@Test
86+
public void testProcessDoesNotSchedulePingAfterDisconnect() {
87+
// Once disconnect() has cleared the resource, a late StartupAnswer must not schedule
88+
// a PingTask - otherwise the future would never be cancelled (the leak this fix targets).
89+
directAgentAttache.disconnect(Status.Disconnected);
90+
91+
directAgentAttache.process(new Answer[] {buildStartupAnswer()});
92+
93+
Mockito.verify(_cronJobPool, Mockito.never()).scheduleWithFixedDelay(any(Runnable.class), anyLong(), anyLong(), any(TimeUnit.class));
94+
}
95+
96+
private StartupAnswer buildStartupAnswer() {
97+
StartupAnswer startup = Mockito.mock(StartupAnswer.class);
98+
Mockito.doReturn(60).when(startup).getPingInterval();
99+
return startup;
100+
}
58101
}

0 commit comments

Comments
 (0)