2222import java .util .concurrent .ScheduledFuture ;
2323import java .util .concurrent .TimeUnit ;
2424import java .util .concurrent .atomic .AtomicInteger ;
25+ import java .util .concurrent .atomic .AtomicLong ;
2526
2627import org .apache .cloudstack .framework .config .ConfigKey ;
2728import org .apache .cloudstack .managed .context .ManagedContextRunnable ;
29+ import org .apache .logging .log4j .ThreadContext ;
2830
2931import com .cloud .agent .api .Answer ;
3032import com .cloud .agent .api .Command ;
3638import com .cloud .exception .AgentUnavailableException ;
3739import com .cloud .host .Status ;
3840import com .cloud .resource .ServerResource ;
39- import org .apache .logging .log4j .ThreadContext ;
4041
4142public 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 }
0 commit comments