You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ServerSessionImpl gets stuck if the WorkManager encounteres a RejectedExecutionException #2647
ServerSessionImpl.start() only catches WorkException which should arguably be sufficient. However, the GeronimoWorkManager might throw a plain RejectedExecutionException. See GERONIMO-6911
I do think it's an upstream bug, but widening the catch clause seems like a good idea regardless.
changed the title [-]ServerSessionImpl gets stuck if the WorkManagers encounteres a RejectedExecutionException[/-][+]ServerSessionImpl gets stuck if the WorkManager encounteres a RejectedExecutionException[/+]on Oct 13, 2026
I can confirm this, and #2648 alone isn't enough. Widening the catch in ServerSessionImpl.start() gets the session out of its stuck "running" state. But the JMSException then reaches the endpoint worker's ExceptionListener, which disconnects and calls connect(). connect() schedules the reconnect on the same work manager, which is still saturated, so that is rejected too. Then:
} catch (WorkExceptione) {
running = false;
LOG.error("Work Manager did not accept work: ", e);
}
running = false means the endpoint never tries to connect again, so the consumer is gone for good. This is the sequence from a run against current main:
WARN ServerSessionImpl - Failed to schedule work for session ServerSessionImpl:{...}, marking not running
ERROR ActiveMQEndpointWorker - Connection to broker failed: Start failed: jakarta.resource.spi.work.WorkRejectedException: thread pool saturated
ERROR ActiveMQEndpointWorker - Work Manager did not accept work:
Here is a test for activemq-ra that reproduces it. Its work manager throws WorkRejectedException while a flag is set, which is what a saturated GeronimoWorkManager does with GERONIMO-6911 applied. It doesn't depend on #2648 and fails on main today:
RejectedWorkRecoveryTest.java
/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */packageorg.apache.activemq.ra;
importstaticorg.junit.Assert.assertTrue;
importjava.lang.reflect.Method;
importjava.util.Timer;
importjava.util.concurrent.CountDownLatch;
importjava.util.concurrent.TimeUnit;
importjava.util.concurrent.atomic.AtomicBoolean;
importjakarta.jms.Connection;
importjakarta.jms.Message;
importjakarta.jms.MessageListener;
importjakarta.jms.MessageProducer;
importjakarta.jms.Queue;
importjakarta.jms.Session;
importjakarta.jms.TextMessage;
importjakarta.resource.spi.BootstrapContext;
importjakarta.resource.spi.UnavailableException;
importjakarta.resource.spi.XATerminator;
importjakarta.resource.spi.endpoint.MessageEndpoint;
importjakarta.resource.spi.endpoint.MessageEndpointFactory;
importjakarta.resource.spi.work.ExecutionContext;
importjakarta.resource.spi.work.Work;
importjakarta.resource.spi.work.WorkContext;
importjakarta.resource.spi.work.WorkException;
importjakarta.resource.spi.work.WorkListener;
importjakarta.resource.spi.work.WorkManager;
importjakarta.resource.spi.work.WorkRejectedException;
importjakarta.transaction.TransactionSynchronizationRegistry;
importjavax.transaction.xa.XAResource;
importorg.apache.activemq.ActiveMQConnectionFactory;
importorg.apache.activemq.command.ActiveMQQueue;
importorg.junit.Test;
/** * Rejects the work of the work manager for a while, as a saturated thread pool does, * and checks that the endpoint consumes again once work is accepted. */publicclassRejectedWorkRecoveryTest {
privatefinalAtomicBooleanreject = newAtomicBoolean();
@Test(timeout = 60000)
publicvoidtestEndpointRecoversAfterRejectedWork() throwsException {
ActiveMQConnectionFactoryfactory = newActiveMQConnectionFactory("vm://localhost?broker.persistent=false");
Connectionconnection = factory.createConnection();
connection.start();
Sessionsession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducerproducer = session.createProducer(newActiveMQQueue("TEST"));
ActiveMQResourceAdapteradapter = newActiveMQResourceAdapter();
adapter.setServerUrl("vm://localhost?broker.persistent=false&create=false");
adapter.start(newRejectingBootstrapContext());
finalCountDownLatchrecovered = newCountDownLatch(1);
finalCountDownLatchconnected = newCountDownLatch(1);
MessageEndpointFactoryendpointFactory = newEndpointFactory(newMessageListener() {
@OverridepublicvoidonMessage(Messagemessage) {
try {
if ("ping".equals(((TextMessage) message).getText())) {
connected.countDown();
} elseif ("after".equals(((TextMessage) message).getText())) {
recovered.countDown();
}
} catch (Exceptione) {
thrownewIllegalStateException(e);
}
}
});
ActiveMQActivationSpecactivationSpec = newActiveMQActivationSpec();
activationSpec.setDestinationType(Queue.class.getName());
activationSpec.setDestination("TEST");
activationSpec.setMaxSessions("1");
activationSpec.setResourceAdapter(adapter);
activationSpec.validate();
adapter.endpointActivation(endpointFactory, activationSpec);
producer.send(session.createTextMessage("ping"));
assertTrue("endpoint did not consume before the rejection", connected.await(10, TimeUnit.SECONDS));
// the thread pool is saturated, the work for this message is rejectedreject.set(true);
producer.send(session.createTextMessage("rejected"));
Thread.sleep(2000);
// the thread pool recoversreject.set(false);
producer.send(session.createTextMessage("after"));
assertTrue("endpoint never consumed again after rejected work", recovered.await(20, TimeUnit.SECONDS));
connection.close();
adapter.endpointDeactivation(endpointFactory, activationSpec);
adapter.stop();
}
privateabstractstaticclassListenerAdapterimplementsMessageEndpoint, MessageListener {
@OverridepublicvoidbeforeDelivery(Methodmethod) {
}
@OverridepublicvoidafterDelivery() {
}
@Overridepublicvoidrelease() {
}
}
privatestaticfinalclassEndpointFactoryimplementsMessageEndpointFactory {
privatefinalMessageListenerlistener;
privateEndpointFactory(MessageListenerlistener) {
this.listener = listener;
}
@OverridepublicMessageEndpointcreateEndpoint(XAResourceresource) throwsUnavailableException {
returnnewListenerAdapter() {
@OverridepublicvoidonMessage(Messagemessage) {
listener.onMessage(message);
}
};
}
@OverridepublicMessageEndpointcreateEndpoint(XAResourceresource, longtimeout) throwsUnavailableException {
returncreateEndpoint(resource);
}
@OverridepublicbooleanisDeliveryTransacted(Methodmethod) {
returnfalse;
}
@OverridepublicStringgetActivationName() {
returnnull;
}
@OverridepublicClass<?> getEndpointClass() {
returnnull;
}
}
privatefinalclassRejectingBootstrapContextimplementsBootstrapContext {
@OverridepublicWorkManagergetWorkManager() {
returnnewWorkManager() {
@OverridepublicvoiddoWork(Workwork) throwsWorkException {
run(work);
}
@OverridepublicvoiddoWork(Workwork, longtimeout, ExecutionContextcontext, WorkListenerlistener) throwsWorkException {
run(work);
}
@OverridepubliclongstartWork(Workwork) throwsWorkException {
run(work);
return0;
}
@OverridepubliclongstartWork(Workwork, longtimeout, ExecutionContextcontext, WorkListenerlistener) throwsWorkException {
run(work);
return0;
}
@OverridepublicvoidscheduleWork(Workwork) throwsWorkException {
run(work);
}
@OverridepublicvoidscheduleWork(Workwork, longtimeout, ExecutionContextcontext, WorkListenerlistener) throwsWorkException {
run(work);
}
privatevoidrun(Workwork) throwsWorkException {
if (reject.get()) {
thrownewWorkRejectedException("thread pool saturated");
}
newThread(work).start();
}
};
}
@OverridepublicXATerminatorgetXATerminator() {
returnnull;
}
@OverridepublicTimercreateTimer() {
returnnull;
}
@OverridepublicbooleanisContextSupported(Class<? extendsWorkContext> workContextClass) {
returnfalse;
}
@OverridepublicTransactionSynchronizationRegistrygetTransactionSynchronizationRegistry() {
returnnull;
}
}
}
A rejection like this is temporary, so I don't think connect() should give up when the work manager rejects the reconnect. It could:
treat WorkRejectedException as transient and retry with the same back-off that pause() uses for broker connection failures. The retry could be scheduled through the Timer from BootstrapContext.createTimer(), so the RA doesn't create threads of its own;
keep running = false only for work exceptions that really are permanent, or when the endpoint is being stopped.
The TomEE side of the reproducer is in apache/tomee#3090. It shows both the unpatched case (RejectedExecutionException) and the case with GERONIMO-6911 applied.
Activity
otbutz commentedon Oct 12, 2026
This also affects
ActiveMQEndpointWorker.connect()[-]ServerSessionImpl gets stuck if the WorkManagers encounteres a RejectedExecutionException[/-][+]ServerSessionImpl gets stuck if the WorkManager encounteres a RejectedExecutionException[/+]otbutz commentedon Oct 14, 2026
Sadly #2648 doesn't fix the issue as confirmed by @rzo1 in apache/tomee#3090
rzo1 commentedon Oct 14, 2026
I can confirm this, and #2648 alone isn't enough. Widening the catch in
ServerSessionImpl.start()gets the session out of its stuck "running" state. But theJMSExceptionthen reaches the endpoint worker'sExceptionListener, which disconnects and callsconnect().connect()schedules the reconnect on the same work manager, which is still saturated, so that is rejected too. Then:running = falsemeans the endpoint never tries to connect again, so the consumer is gone for good. This is the sequence from a run against current main:Here is a test for
activemq-rathat reproduces it. Its work manager throwsWorkRejectedExceptionwhile a flag is set, which is what a saturatedGeronimoWorkManagerdoes with GERONIMO-6911 applied. It doesn't depend on #2648 and fails on main today:RejectedWorkRecoveryTest.java
A rejection like this is temporary, so I don't think
connect()should give up when the work manager rejects the reconnect. It could:WorkRejectedExceptionas transient and retry with the same back-off thatpause()uses for broker connection failures. The retry could be scheduled through theTimerfromBootstrapContext.createTimer(), so the RA doesn't create threads of its own;running = falseonly for work exceptions that really are permanent, or when the endpoint is being stopped.The TomEE side of the reproducer is in apache/tomee#3090. It shows both the unpatched case (
RejectedExecutionException) and the case with GERONIMO-6911 applied.otbutz commentedon Oct 14, 2026
Maybe we should use
jakarta.resource.spi.work.RetryableWorkRejectedExceptioninstead?