Skip to content

Commit abb53e3

Browse files
committed
Terminate management receive loop when idle
Applications are likely to configure their topology when they start and modify it sporadically at best during the application lifetime. Terminating the receive loop saves a thread. There is now a dedicated, non-configurable thread pool for management instances.
1 parent 31aed8d commit abb53e3

File tree

7 files changed

+159
-59
lines changed

7 files changed

+159
-59
lines changed

src/main/java/com/rabbitmq/client/amqp/impl/AmqpConnection.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import java.util.concurrent.atomic.AtomicBoolean;
3030
import java.util.concurrent.atomic.AtomicLong;
3131
import java.util.concurrent.atomic.AtomicReference;
32+
import java.util.concurrent.locks.Lock;
33+
import java.util.concurrent.locks.ReentrantLock;
3234
import java.util.function.BiConsumer;
3335
import java.util.function.Predicate;
3436
import java.util.function.Supplier;
@@ -69,6 +71,7 @@ final class AmqpConnection extends ResourceBase implements Connection {
6971
private final ConnectionUtils.AffinityContext affinity;
7072
private final ConnectionSettings.AffinityStrategy affinityStrategy;
7173
private final String name;
74+
private final Lock instanceLock = new ReentrantLock();
7275

7376
AmqpConnection(AmqpConnectionBuilder builder) {
7477
super(builder.listeners());
@@ -523,14 +526,17 @@ Session nativeSession(boolean check) {
523526
}
524527
Session result = this.nativeSession;
525528
if (result == null) {
526-
synchronized (this) {
529+
this.instanceLock.lock();
530+
try {
527531
result = this.nativeSession;
528532
if (result == null) {
529533
if (check) {
530534
checkOpen();
531535
}
532536
this.nativeSession = result = this.openSession(this.nativeConnection);
533537
}
538+
} finally {
539+
this.instanceLock.unlock();
534540
}
535541
}
536542
return result;
@@ -548,6 +554,10 @@ org.apache.qpid.protonj2.client.Connection nativeConnection() {
548554
return this.nativeConnection;
549555
}
550556

557+
AmqpEnvironment environment() {
558+
return this.environment;
559+
}
560+
551561
ExecutorService executorService() {
552562
return this.environment.executorService();
553563
}

src/main/java/com/rabbitmq/client/amqp/impl/AmqpEnvironment.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class AmqpEnvironment implements Environment {
4949
private final ConnectionUtils.AffinityCache affinityCache = new ConnectionUtils.AffinityCache();
5050
private final EventLoop recoveryEventLoop;
5151
private final ExecutorService recoveryEventLoopExecutorService;
52+
private final ExecutorService managementExecutorService;
5253

5354
AmqpEnvironment(
5455
ExecutorService executorService,
@@ -91,6 +92,14 @@ class AmqpEnvironment implements Environment {
9192
new LinkedBlockingQueue<>(),
9293
Utils.threadFactory(threadPrefix + "event-loop-"));
9394
this.recoveryEventLoop = new EventLoop(this.recoveryEventLoopExecutorService);
95+
this.managementExecutorService =
96+
new ThreadPoolExecutor(
97+
0,
98+
Integer.MAX_VALUE,
99+
30,
100+
TimeUnit.SECONDS,
101+
new SynchronousQueue<>(),
102+
Utils.threadFactory(threadPrefix + "management-loop-"));
94103
}
95104

96105
DefaultConnectionSettings<?> connectionSettings() {
@@ -117,6 +126,7 @@ public void close() {
117126
this.client.close();
118127
this.recoveryEventLoop.close();
119128
this.recoveryEventLoopExecutorService.shutdownNow();
129+
this.managementExecutorService.shutdownNow();
120130
if (this.internalExecutor) {
121131
this.executorService.shutdownNow();
122132
}
@@ -139,6 +149,10 @@ ExecutorService executorService() {
139149
return this.executorService;
140150
}
141151

152+
ExecutorService managementExecutorService() {
153+
return this.managementExecutorService;
154+
}
155+
142156
ScheduledExecutorService scheduledExecutorService() {
143157
return this.scheduledExecutorService;
144158
}

src/main/java/com/rabbitmq/client/amqp/impl/AmqpManagement.java

Lines changed: 86 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.concurrent.atomic.AtomicBoolean;
2929
import java.util.concurrent.atomic.AtomicLong;
3030
import java.util.concurrent.atomic.AtomicReference;
31+
import java.util.concurrent.locks.Lock;
32+
import java.util.concurrent.locks.ReentrantLock;
3133
import java.util.function.Supplier;
3234
import java.util.stream.Collectors;
3335
import java.util.stream.IntStream;
@@ -72,6 +74,8 @@ class AmqpManagement implements Management {
7274
private final Supplier<String> nameSupplier;
7375
private final AtomicReference<State> state = new AtomicReference<>(CREATED);
7476
private final AtomicBoolean initializing = new AtomicBoolean(false);
77+
private final Duration receiveLoopIdleTimeout;
78+
private final Lock instanceLock = new ReentrantLock();
7579

7680
AmqpManagement(AmqpManagementParameters parameters) {
7781
this.id = ID_SEQUENCE.getAndIncrement();
@@ -81,6 +85,10 @@ class AmqpManagement implements Management {
8185
? TopologyListener.NO_OP
8286
: parameters.topologyListener();
8387
this.nameSupplier = parameters.nameSupplier();
88+
this.receiveLoopIdleTimeout =
89+
parameters.receiveLoopIdleTimeout() == null
90+
? Duration.ofSeconds(20)
91+
: parameters.receiveLoopIdleTimeout();
8492
}
8593

8694
@Override
@@ -190,6 +198,10 @@ void init() {
190198
LOGGER.debug("Initializing management ({}).", this);
191199
this.state(UNAVAILABLE);
192200
try {
201+
if (this.receiveLoop != null) {
202+
this.receiveLoop.cancel(true);
203+
this.receiveLoop = null;
204+
}
193205
LOGGER.debug("Creating management session ({}).", this);
194206
this.session = this.connection.nativeConnection().openSession();
195207
String linkPairName = "management-link-pair";
@@ -217,49 +229,6 @@ void init() {
217229
LOGGER.debug("Management sender created ({}).", this);
218230
this.receiver.openFuture().get(this.rpcTimeout.toMillis(), MILLISECONDS);
219231
LOGGER.debug("Management receiver created ({}).", this);
220-
Runnable receiveTask =
221-
() -> {
222-
try {
223-
while (!Thread.currentThread().isInterrupted()) {
224-
Delivery delivery = receiver.receive(100, MILLISECONDS);
225-
if (delivery != null) {
226-
Object correlationId = delivery.message().correlationId();
227-
if (correlationId instanceof UUID) {
228-
OutstandingRequest request = outstandingRequests.remove(correlationId);
229-
if (request != null) {
230-
request.complete(delivery.message());
231-
} else {
232-
LOGGER.info("Could not find outstanding request {}", correlationId);
233-
}
234-
} else {
235-
LOGGER.info("Could not correlate inbound message with management request");
236-
}
237-
}
238-
}
239-
} catch (ClientConnectionRemotelyClosedException
240-
| ClientLinkRemotelyClosedException e) {
241-
// receiver is closed
242-
} catch (ClientSessionRemotelyClosedException e) {
243-
this.state(UNAVAILABLE);
244-
LOGGER.info(
245-
"Management session closed in receive loop: {} ({})", e.getMessage(), this);
246-
AmqpException exception = ExceptionUtils.convert(e);
247-
this.failRequests(exception);
248-
if (exception instanceof AmqpException.AmqpSecurityException) {
249-
LOGGER.debug(
250-
"Recovering AMQP management because the failure was a security exception ({}).",
251-
this);
252-
this.init();
253-
}
254-
} catch (ClientException e) {
255-
java.util.function.Consumer<String> log =
256-
this.closed.get() ? m -> LOGGER.debug(m, e) : m -> LOGGER.info(m, e);
257-
log.accept("Error while polling AMQP receiver");
258-
}
259-
};
260-
LOGGER.debug("Starting management receive loop ({}).", this);
261-
this.receiveLoop = this.connection.executorService().submit(receiveTask);
262-
LOGGER.debug("Management initialized ({}).", this);
263232
this.state(OPEN);
264233
this.initializing.set(false);
265234
} catch (Exception e) {
@@ -269,6 +238,58 @@ void init() {
269238
}
270239
}
271240

241+
private Runnable receiveTask() {
242+
return () -> {
243+
try {
244+
Duration waitDuration = Duration.ofMillis(100);
245+
long idleTime = 0;
246+
while (!Thread.currentThread().isInterrupted()) {
247+
Delivery delivery = receiver.receive(waitDuration.toMillis(), MILLISECONDS);
248+
if (delivery != null) {
249+
idleTime = 0;
250+
Object correlationId = delivery.message().correlationId();
251+
if (correlationId instanceof UUID) {
252+
OutstandingRequest request = outstandingRequests.remove(correlationId);
253+
if (request != null) {
254+
request.complete(delivery.message());
255+
} else {
256+
LOGGER.info("Could not find outstanding request {}", correlationId);
257+
}
258+
} else {
259+
LOGGER.info("Could not correlate inbound message with management request");
260+
}
261+
} else {
262+
idleTime += waitDuration.toMillis();
263+
if (idleTime > receiveLoopIdleTimeout.toMillis()) {
264+
LOGGER.debug(
265+
"Management receive loop has been idle for more than {}, finishing it.",
266+
this.receiveLoopIdleTimeout);
267+
this.receiveLoop = null;
268+
return;
269+
}
270+
}
271+
}
272+
} catch (ClientConnectionRemotelyClosedException | ClientLinkRemotelyClosedException e) {
273+
// receiver is closed
274+
} catch (ClientSessionRemotelyClosedException e) {
275+
this.state(UNAVAILABLE);
276+
LOGGER.info("Management session closed in receive loop: {} ({})", e.getMessage(), this);
277+
AmqpException exception = ExceptionUtils.convert(e);
278+
this.failRequests(exception);
279+
if (exception instanceof AmqpException.AmqpSecurityException) {
280+
LOGGER.debug(
281+
"Recovering AMQP management because the failure was a security exception ({}).",
282+
this);
283+
this.init();
284+
}
285+
} catch (ClientException e) {
286+
java.util.function.Consumer<String> log =
287+
this.closed.get() ? m -> LOGGER.debug(m, e) : m -> LOGGER.info(m, e);
288+
log.accept("Error while polling AMQP receiver");
289+
}
290+
};
291+
}
292+
272293
private void failRequests(AmqpException exception) {
273294
Iterator<Map.Entry<UUID, OutstandingRequest>> iterator =
274295
this.outstandingRequests.entrySet().iterator();
@@ -284,6 +305,7 @@ void releaseResources() {
284305
this.markUnavailable();
285306
if (this.receiveLoop != null) {
286307
this.receiveLoop.cancel(true);
308+
this.receiveLoop = null;
287309
}
288310
}
289311

@@ -338,6 +360,22 @@ OutstandingRequest request(Message<?> request, UUID requestId) throws ClientExce
338360
this.outstandingRequests.put(requestId, outstandingRequest);
339361
LOGGER.debug("Sending request {}", requestId);
340362
this.sender.send(request);
363+
Future<?> loop = this.receiveLoop;
364+
if (loop == null) {
365+
this.instanceLock.lock();
366+
try {
367+
loop = this.receiveLoop;
368+
if (loop == null) {
369+
Runnable receiveTask = receiveTask();
370+
LOGGER.debug("Starting management receive loop ({}).", this);
371+
this.receiveLoop =
372+
this.connection.environment().managementExecutorService().submit(receiveTask);
373+
LOGGER.debug("Management initialized ({}).", this);
374+
}
375+
} finally {
376+
this.instanceLock.unlock();
377+
}
378+
}
341379
return outstandingRequest;
342380
}
343381

@@ -548,6 +586,11 @@ TopologyListener recovery() {
548586
return this.topologyListener;
549587
}
550588

589+
// for testing
590+
boolean hasReceiveLoop() {
591+
return this.receiveLoop != null;
592+
}
593+
551594
private static class DefaultQueueInfo implements QueueInfo {
552595

553596
private final String name;

src/main/java/com/rabbitmq/client/amqp/impl/AmqpManagementParameters.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
1818
package com.rabbitmq.client.amqp.impl;
1919

20+
import java.time.Duration;
2021
import java.util.function.Supplier;
2122

2223
class AmqpManagementParameters {
2324

2425
private final AmqpConnection connection;
2526
private TopologyListener topologyListener;
2627
private Supplier<String> nameSupplier = Utils.NAME_SUPPLIER;
28+
private Duration receiveLoopIdleTimeout;
2729

2830
AmqpManagementParameters(AmqpConnection connection) {
2931
this.connection = connection;
@@ -39,6 +41,15 @@ AmqpManagementParameters nameSupplier(Supplier<String> nameSupplier) {
3941
return this;
4042
}
4143

44+
public AmqpManagementParameters receiveLoopIdleTimeout(Duration receiveLoopIdleTimeout) {
45+
this.receiveLoopIdleTimeout = receiveLoopIdleTimeout;
46+
return this;
47+
}
48+
49+
Duration receiveLoopIdleTimeout() {
50+
return receiveLoopIdleTimeout;
51+
}
52+
4253
AmqpConnection connection() {
4354
return this.connection;
4455
}

src/test/java/com/rabbitmq/client/amqp/impl/AmqpConnectionRecoveryTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@ void connectionShouldRecoverAfterClosingIt(boolean isolateResources, TestInfo in
165165
assertThat(receivedMessageIds)
166166
.hasSameSizeAs(publishedMessageIds)
167167
.containsAll(publishedMessageIds);
168-
169168
} finally {
170169
c.management().queueDeletion().delete(q);
171170
c.close();

src/test/java/com/rabbitmq/client/amqp/impl/ManagementTest.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
1818
package com.rabbitmq.client.amqp.impl;
1919

20+
import static com.rabbitmq.client.amqp.impl.Assertions.assertThat;
2021
import static org.assertj.core.api.Assertions.assertThat;
2122
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2223

2324
import com.rabbitmq.client.amqp.AmqpException;
2425
import com.rabbitmq.client.amqp.Management;
26+
import java.time.Duration;
2527
import java.util.concurrent.atomic.AtomicInteger;
2628
import java.util.function.Supplier;
2729
import org.junit.jupiter.api.*;
@@ -58,7 +60,7 @@ void queueDeclareWithClientNamedQueueShouldBeRetriedIfNameAlreadyExists() {
5860
new AmqpManagement(new AmqpManagementParameters(connection).nameSupplier(nameSupplier));
5961
management.init();
6062
Management.QueueInfo queueInfo = management.queue().exclusive(true).autoDelete(true).declare();
61-
Assertions.assertThat(queueInfo).hasName(q);
63+
assertThat(queueInfo).hasName(q);
6264
assertThat(nameSupplierCallCount).hasValue(1);
6365
queueInfo = management.queue().exclusive(true).autoDelete(false).declare();
6466
assertThat(queueInfo.name()).isNotEqualTo(q);
@@ -70,4 +72,21 @@ void queueInfoShouldThrowDoesNotExistExceptionWhenQueueDoesNotExist() {
7072
assertThatThrownBy(() -> connection.management().queueInfo("do not exists"))
7173
.isInstanceOf(AmqpException.AmqpEntityDoesNotExistException.class);
7274
}
75+
76+
@Test
77+
void receiveLoopShouldStopAfterBeingIdle() {
78+
management =
79+
new AmqpManagement(
80+
new AmqpManagementParameters(connection)
81+
.receiveLoopIdleTimeout(Duration.ofMillis(500)));
82+
management.init();
83+
assertThat(management.hasReceiveLoop()).isFalse();
84+
Management.QueueInfo info1 = management.queue().exclusive(true).autoDelete(true).declare();
85+
assertThat(management.hasReceiveLoop()).isTrue();
86+
TestUtils.waitAtMost(() -> !management.hasReceiveLoop());
87+
Management.QueueInfo info2 = management.queue().exclusive(true).autoDelete(true).declare();
88+
assertThat(management.hasReceiveLoop()).isTrue();
89+
assertThat(management.queueInfo(info1.name())).hasName(info1.name());
90+
assertThat(management.queueInfo(info2.name())).hasName(info2.name());
91+
}
7392
}

0 commit comments

Comments
 (0)