This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 23b6f520ff32 CAMEL-24227: Add volatile to JMX-writable fields read on
routing threads
23b6f520ff32 is described below
commit 23b6f520ff32eb41c2da70aebeaabf044dc9411b
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Jul 27 15:20:24 2026 +0200
CAMEL-24227: Add volatile to JMX-writable fields read on routing threads
Fix JMM-unsafe patterns across 15 engine classes where fields written
at runtime via JMX/management APIs lacked memory visibility guarantees
for routing threads. Simple fields get volatile; compound writes
(BacklogTracer trace pattern/filter, DefaultTracer trace pattern,
ThrottlingInflightRoutePolicy limits) use immutable holder records
swapped via a single volatile reference. Also fixes a pre-existing bug
where setTraceFilter(null) left a stale predicate in BacklogTracer.
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../apache/camel/impl/debugger/BacklogTracer.java | 78 ++++++++------
.../impl/debugger/DefaultBacklogDebugger.java | 16 +--
.../impl/engine/DefaultStreamCachingStrategy.java | 10 +-
.../apache/camel/impl/engine/DefaultTracer.java | 33 +++---
.../apache/camel/processor/AbstractThrottler.java | 2 +-
.../processor/BaseDelegateProcessorSupport.java | 2 +-
.../camel/processor/BaseProcessorSupport.java | 2 +-
.../java/org/apache/camel/processor/Delayer.java | 2 +-
.../camel/processor/TotalRequestsThrottler.java | 2 +-
.../org/apache/camel/processor/WrapProcessor.java | 2 +-
.../mbean/ManagedPerformanceCounter.java | 2 +-
.../management/BacklogTracerFilterClearTest.java | 116 +++++++++++++++++++++
.../camel/support/ScheduledPollConsumer.java | 14 +--
.../throttling/ThrottlingExceptionRoutePolicy.java | 6 +-
.../throttling/ThrottlingInflightRoutePolicy.java | 50 ++++++---
15 files changed, 243 insertions(+), 94 deletions(-)
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java
index 90fc97af0823..8399000cc572 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java
@@ -62,17 +62,15 @@ public class BacklogTracer extends ServiceSupport
implements org.apache.camel.sp
public static final int MAX_BACKLOG_SIZE = 1000;
private final CamelContext camelContext;
private final Language simple;
- // enabled, standby, and activityEnabled (further below) are toggled at
runtime via
- // JMX/management APIs while routing threads read them in shouldTrace()
and traceEvent().
- // Other boolean fields (removeOnDump, bodyIncludeStreams, traceRests,
etc.) are set during
- // initialization and do not change while routes are processing, so they
do not need volatile.
+ // Fields below marked volatile are writable at runtime via JMX/management
APIs
+ // while routing threads read them in shouldTrace() and traceEvent().
private volatile boolean enabled;
private volatile boolean standby;
private final AtomicLong traceCounter = new AtomicLong();
// use a queue with an upper limit to avoid storing too many messages
private final Queue<BacklogTracerEventMessage> queue = new
LinkedBlockingQueue<>(MAX_BACKLOG_SIZE);
// how many of the last messages to keep in the backlog at total
- private int backlogSize = 100;
+ private volatile int backlogSize = 100;
// use tracer to capture additional information for capturing latest
completed exchange message-history
private final Queue<BacklogTracerEventMessage> provisionalHistoryQueue =
new LinkedBlockingQueue<>(MAX_BACKLOG_SIZE);
private final Queue<BacklogTracerEventMessage> completeHistoryQueue = new
LinkedBlockingQueue<>(MAX_BACKLOG_SIZE + 1);
@@ -80,26 +78,31 @@ public class BacklogTracer extends ServiceSupport
implements org.apache.camel.sp
private final Queue<BacklogTracerActivityMessage> activityQueue = new
LinkedBlockingQueue<>(MAX_BACKLOG_SIZE);
private final ConcurrentHashMap<String,
DefaultBacklogTracerActivityMessage> inflightActivity = new
ConcurrentHashMap<>();
private final ActivityEventNotifier activityEventNotifier = new
ActivityEventNotifier();
- private int activitySize = 100;
+ private volatile int activitySize = 100;
private static final long INFLIGHT_EVICTION_MILLIS = 5 * 60 * 1000;
private final Object historyLock = new Object();
private volatile String lastCompletedBreadcrumbId;
- private boolean removeOnDump = true;
- private int bodyMaxChars = 32 * 1024;
- private boolean bodyIncludeStreams;
- private boolean bodyIncludeFiles = true;
- private boolean includeExchangeProperties = true;
- private boolean includeExchangeVariables = true;
- private boolean includeException = true;
- // volatile: toggled at runtime via JMX, same rationale as enabled/standby
above
+ private volatile boolean removeOnDump = true;
+ private volatile int bodyMaxChars = 32 * 1024;
+ private volatile boolean bodyIncludeStreams;
+ private volatile boolean bodyIncludeFiles = true;
+ private volatile boolean includeExchangeProperties = true;
+ private volatile boolean includeExchangeVariables = true;
+ private volatile boolean includeException = true;
private volatile boolean activityEnabled;
- private boolean traceRests;
- private boolean traceTemplates;
+ private volatile boolean traceRests;
+ private volatile boolean traceTemplates;
+
+ // immutable holders for compound fields that must be visible atomically
on routing threads
+ private record TracePatternHolder(String tracePattern, String[] patterns) {
+ }
+
+ private record TraceFilterHolder(String traceFilter, Predicate predicate) {
+ }
+
// a pattern to filter tracing nodes
- private String tracePattern;
- private String[] patterns;
- private String traceFilter;
- private Predicate predicate;
+ private volatile TracePatternHolder tracePatternHolder;
+ private volatile TraceFilterHolder traceFilterHolder;
BacklogTracer(CamelContext camelContext) {
this.camelContext = camelContext;
@@ -135,17 +138,20 @@ public class BacklogTracer extends ServiceSupport
implements org.apache.camel.sp
boolean pattern = true;
boolean filter = true;
- if (patterns != null) {
- pattern = shouldTracePattern(definition);
+ // snapshot the volatile holders once for consistent reads
+ TracePatternHolder ph = tracePatternHolder;
+ if (ph != null) {
+ pattern = shouldTracePattern(definition, ph.patterns());
}
- if (predicate != null) {
- filter = shouldTraceFilter(exchange);
+ TraceFilterHolder fh = traceFilterHolder;
+ if (fh != null) {
+ filter = shouldTraceFilter(exchange, fh.predicate());
}
return pattern && filter;
}
- private boolean shouldTracePattern(NamedNode definition) {
+ private boolean shouldTracePattern(NamedNode definition, String[]
patterns) {
for (String pattern : patterns) {
// match either route id, or node id
String id = definition.getId();
@@ -326,7 +332,7 @@ public class BacklogTracer extends ServiceSupport
implements org.apache.camel.sp
}
}
- private boolean shouldTraceFilter(Exchange exchange) {
+ private boolean shouldTraceFilter(Exchange exchange, Predicate predicate) {
return predicate.matches(exchange);
}
@@ -478,37 +484,41 @@ public class BacklogTracer extends ServiceSupport
implements org.apache.camel.sp
@Override
public String getTracePattern() {
- return tracePattern;
+ TracePatternHolder ph = tracePatternHolder;
+ return ph != null ? ph.tracePattern() : null;
}
@Override
public void setTracePattern(String tracePattern) {
- this.tracePattern = tracePattern;
if (tracePattern != null) {
// the pattern can have multiple nodes separated by comma
- this.patterns = tracePattern.split(",");
+ this.tracePatternHolder = new TracePatternHolder(tracePattern,
tracePattern.split(","));
} else {
- this.patterns = null;
+ this.tracePatternHolder = null;
}
}
@Override
public String getTraceFilter() {
- return traceFilter;
+ TraceFilterHolder fh = traceFilterHolder;
+ return fh != null ? fh.traceFilter() : null;
}
@Override
public void setTraceFilter(String filter) {
- this.traceFilter = filter;
if (filter != null) {
// assume simple language
+ Predicate p;
String name = StringHelper.before(filter, ":");
if (name != null) {
- predicate =
camelContext.resolveLanguage(name).createPredicate(filter);
+ p = camelContext.resolveLanguage(name).createPredicate(filter);
} else {
// use simple language by default
- predicate = simple.createPredicate(filter);
+ p = simple.createPredicate(filter);
}
+ this.traceFilterHolder = new TraceFilterHolder(filter, p);
+ } else {
+ this.traceFilterHolder = null;
}
}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogDebugger.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogDebugger.java
index 09d9aad0e903..9eed5c9af64e 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogDebugger.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogDebugger.java
@@ -68,7 +68,7 @@ public final class DefaultBacklogDebugger extends
ServiceSupport implements Back
private static final Logger LOG =
LoggerFactory.getLogger(DefaultBacklogDebugger.class);
- private long fallbackTimeout = 300;
+ private volatile long fallbackTimeout = 300;
private final CamelContext camelContext;
private LoggingLevel loggingLevel = LoggingLevel.INFO;
private final CamelLogger logger = new CamelLogger(LOG, loggingLevel);
@@ -87,13 +87,13 @@ public final class DefaultBacklogDebugger extends
ServiceSupport implements Back
private boolean suspendMode;
private String initialBreakpoints;
- private boolean singleStepIncludeStartEnd;
- private int bodyMaxChars = 32 * 1024;
- private boolean bodyIncludeStreams;
- private boolean bodyIncludeFiles = true;
- private boolean includeExchangeProperties = true;
- private boolean includeExchangeVariables = true;
- private boolean includeException = true;
+ private volatile boolean singleStepIncludeStartEnd;
+ private volatile int bodyMaxChars = 32 * 1024;
+ private volatile boolean bodyIncludeStreams;
+ private volatile boolean bodyIncludeFiles = true;
+ private volatile boolean includeExchangeProperties = true;
+ private volatile boolean includeExchangeVariables = true;
+ private volatile boolean includeException = true;
/**
* An {@link Exchange} suspended at a breakpoint.
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultStreamCachingStrategy.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultStreamCachingStrategy.java
index f64aec981ac7..b0c93e6bbd2b 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultStreamCachingStrategy.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultStreamCachingStrategy.java
@@ -64,15 +64,15 @@ public class DefaultStreamCachingStrategy extends
ServiceSupport implements Came
private boolean spoolEnabled;
private File spoolDirectory;
private transient String spoolDirectoryName =
"${java.io.tmpdir}/camel/camel-tmp-#uuid#";
- private long spoolThreshold = StreamCache.DEFAULT_SPOOL_THRESHOLD;
- private int spoolUsedHeapMemoryThreshold;
+ private volatile long spoolThreshold = StreamCache.DEFAULT_SPOOL_THRESHOLD;
+ private volatile int spoolUsedHeapMemoryThreshold;
private SpoolUsedHeapMemoryLimit spoolUsedHeapMemoryLimit;
private String spoolCipher;
- private int bufferSize = IOHelper.DEFAULT_BUFFER_SIZE;
+ private volatile int bufferSize = IOHelper.DEFAULT_BUFFER_SIZE;
private boolean removeSpoolDirectoryWhenStopping = true;
private final UtilizationStatistics statistics = new
UtilizationStatistics();
private final Set<SpoolRule> spoolRules = new LinkedHashSet<>();
- private boolean anySpoolRules;
+ private volatile boolean anySpoolRules;
@Override
public CamelContext getCamelContext() {
@@ -561,7 +561,7 @@ public class DefaultStreamCachingStrategy extends
ServiceSupport implements Came
private static final class UtilizationStatistics implements Statistics {
private final Lock lock = new ReentrantLock();
- private boolean statisticsEnabled;
+ private volatile boolean statisticsEnabled;
private final AtomicLong memoryCounter = new AtomicLong();
private final AtomicLong memorySize = new AtomicLong();
private final AtomicLong memoryAverageSize = new AtomicLong();
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultTracer.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultTracer.java
index 3a76176ca21a..f01d9af6b51a 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultTracer.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultTracer.java
@@ -55,16 +55,19 @@ public class DefaultTracer extends ServiceSupport
implements CamelContextAware,
private String tracingFormat = TRACING_OUTPUT;
private CamelContext camelContext;
- private boolean enabled = true;
- private boolean standby;
- private boolean traceRests;
- private boolean traceTemplates;
+ private volatile boolean enabled = true;
+ private volatile boolean standby;
+ private volatile boolean traceRests;
+ private volatile boolean traceTemplates;
private long traceCounter;
+ // immutable holder for compound tracePattern+patterns that must be
visible atomically
+ private record TracePatternHolder(String tracePattern, String[] patterns) {
+ }
+
private ExchangeFormatter exchangeFormatter;
- private String tracePattern;
- private transient String[] patterns;
- private boolean traceBeforeAndAfterRoute = true;
+ private volatile TracePatternHolder tracePatternHolder;
+ private volatile boolean traceBeforeAndAfterRoute = true;
public DefaultTracer() {
DefaultExchangeFormatter formatter = new DefaultExchangeFormatter();
@@ -241,8 +244,10 @@ public class DefaultTracer extends ServiceSupport
implements CamelContextAware,
boolean pattern = true;
- if (patterns != null) {
- pattern = shouldTracePattern(definition);
+ // snapshot the volatile holder once for consistent reads
+ TracePatternHolder ph = tracePatternHolder;
+ if (ph != null) {
+ pattern = shouldTracePattern(definition, ph.patterns());
}
if (LOG.isTraceEnabled()) {
@@ -303,17 +308,17 @@ public class DefaultTracer extends ServiceSupport
implements CamelContextAware,
@Override
public String getTracePattern() {
- return tracePattern;
+ TracePatternHolder ph = tracePatternHolder;
+ return ph != null ? ph.tracePattern() : null;
}
@Override
public void setTracePattern(String tracePattern) {
- this.tracePattern = tracePattern;
if (tracePattern != null) {
// the pattern can have multiple nodes separated by comma
- this.patterns = tracePattern.split(",");
+ this.tracePatternHolder = new TracePatternHolder(tracePattern,
tracePattern.split(","));
} else {
- this.patterns = null;
+ this.tracePatternHolder = null;
}
}
@@ -347,7 +352,7 @@ public class DefaultTracer extends ServiceSupport
implements CamelContextAware,
}
}
- protected boolean shouldTracePattern(NamedNode definition) {
+ protected boolean shouldTracePattern(NamedNode definition, String[]
patterns) {
for (String pattern : patterns) {
// match either route id, or node id
String id = definition.getId();
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/AbstractThrottler.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/AbstractThrottler.java
index 947dfdf990b7..725e51122aa8 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/AbstractThrottler.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/AbstractThrottler.java
@@ -49,7 +49,7 @@ public abstract class AbstractThrottler extends
BaseProcessorSupport
protected boolean rejectExecution;
protected boolean asyncDelayed;
protected boolean callerRunsWhenRejected = true;
- protected Expression maxRequestsExpression;
+ protected volatile Expression maxRequestsExpression;
AbstractThrottler(final ScheduledExecutorService asyncExecutor, final
boolean shutdownAsyncExecutor,
final CamelContext camelContext, final boolean
rejectExecution, Expression correlation,
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/BaseDelegateProcessorSupport.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/BaseDelegateProcessorSupport.java
index 52a5229fb72c..7a77d6b373ff 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/BaseDelegateProcessorSupport.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/BaseDelegateProcessorSupport.java
@@ -28,7 +28,7 @@ public abstract class BaseDelegateProcessorSupport extends
DelegateAsyncProcesso
super(processor);
}
- private boolean disabled;
+ private volatile boolean disabled;
@Override
public boolean isDisabled() {
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/BaseProcessorSupport.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/BaseProcessorSupport.java
index ed2b416ce1e6..1329b0a18fd9 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/BaseProcessorSupport.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/BaseProcessorSupport.java
@@ -24,7 +24,7 @@ import org.apache.camel.support.AsyncProcessorSupport;
*/
public abstract class BaseProcessorSupport extends AsyncProcessorSupport
implements DisabledAware {
- private boolean disabled;
+ private volatile boolean disabled;
@Override
public boolean isDisabled() {
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Delayer.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Delayer.java
index 9e588400e9a2..98043650072b 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Delayer.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Delayer.java
@@ -38,7 +38,7 @@ public class Delayer extends DelayProcessorSupport implements
Traceable, IdAware
private String routeId;
private String stepId;
private String id;
- private Expression delay;
+ private volatile Expression delay;
private long delayValue;
public Delayer(CamelContext camelContext, Processor processor, Expression
delay,
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/TotalRequestsThrottler.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/TotalRequestsThrottler.java
index aa89376ed5a0..2cd000321e1a 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/TotalRequestsThrottler.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/TotalRequestsThrottler.java
@@ -58,7 +58,7 @@ public class TotalRequestsThrottler extends AbstractThrottler
{
private static final Logger LOG =
LoggerFactory.getLogger(TotalRequestsThrottler.class);
- private long timePeriodMillis;
+ private volatile long timePeriodMillis;
private final long cleanPeriodMillis;
private final Expression correlationExpression;
private final Map<String, ThrottlingState> states = new
ConcurrentHashMap<>();
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/WrapProcessor.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/WrapProcessor.java
index 703eb8664be7..da510d1033dc 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/WrapProcessor.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/WrapProcessor.java
@@ -29,7 +29,7 @@ import org.apache.camel.support.service.ServiceHelper;
public class WrapProcessor extends DelegateAsyncProcessor implements
WrapAwareProcessor {
private final Processor wrapped;
- private boolean disabled;
+ private volatile boolean disabled;
public WrapProcessor(Processor processor, Processor wrapped) {
super(processor);
diff --git
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
index 6b7187eb27ef..9bca27e455b6 100644
---
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
+++
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
@@ -61,7 +61,7 @@ public abstract class ManagedPerformanceCounter extends
ManagedCounter
private Statistic lastExchangeFailureTimestamp;
private String lastExchangeFailureExchangeId;
private final LoadThroughput thp = new LoadThroughput();
- private boolean statisticsEnabled = true;
+ private volatile boolean statisticsEnabled = true;
// sliding window ring buffer for percentile computation (Extended
statistics only)
private long[] percentileWindow;
diff --git
a/core/camel-management/src/test/java/org/apache/camel/management/BacklogTracerFilterClearTest.java
b/core/camel-management/src/test/java/org/apache/camel/management/BacklogTracerFilterClearTest.java
new file mode 100644
index 000000000000..1192d4ac8580
--- /dev/null
+++
b/core/camel-management/src/test/java/org/apache/camel/management/BacklogTracerFilterClearTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+package org.apache.camel.management;
+
+import java.util.List;
+
+import javax.management.Attribute;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.BacklogTracerEventMessage;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DisabledOnOs(OS.AIX)
+class BacklogTracerFilterClearTest extends ManagementTestSupport {
+
+ /**
+ * Verify that clearing the trace filter via JMX (setTraceFilter(null))
actually disables filtering so that
+ * subsequent messages are traced unconditionally. Before the fix, the old
code nulled the filter string but never
+ * cleared the predicate, so shouldTrace() kept evaluating a stale
predicate.
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ void testClearTraceFilter() throws Exception {
+ MBeanServer mbeanServer = getMBeanServer();
+ ObjectName on
+ = new ObjectName(
+ "org.apache.camel:context=" +
context.getManagementName()
+ + ",type=tracer,name=BacklogTracer");
+ assertTrue(mbeanServer.isRegistered(on));
+
+ // disable removeOnDump so we can count events across phases
+ mbeanServer.setAttribute(on, new Attribute("RemoveOnDump",
Boolean.FALSE));
+
+ // enable tracing with a filter that requires header "foo"
+ mbeanServer.setAttribute(on, new Attribute("Enabled", Boolean.TRUE));
+ mbeanServer.setAttribute(on, new Attribute("TraceFilter",
"${header.foo} != null"));
+
+ getMockEndpoint("mock:foo").expectedMessageCount(1);
+ getMockEndpoint("mock:bar").expectedMessageCount(1);
+
+ // send message WITH header — should be traced
+ template.sendBodyAndHeader("direct:start", "Matched", "foo", 123);
+ assertMockEndpointsSatisfied();
+
+ List<BacklogTracerEventMessage> events
+ = (List<BacklogTracerEventMessage>) mbeanServer.invoke(on,
"dumpAllTracedMessages", null, null);
+ int tracedWithFilter = events.size();
+ assertTrue(tracedWithFilter > 0, "Message with header should be
traced");
+
+ // send message WITHOUT header — should NOT be traced due to filter
+ resetMocks();
+ getMockEndpoint("mock:foo").expectedMessageCount(1);
+ getMockEndpoint("mock:bar").expectedMessageCount(1);
+ template.sendBody("direct:start", "Not Matched");
+ assertMockEndpointsSatisfied();
+
+ events = (List<BacklogTracerEventMessage>) mbeanServer.invoke(on,
"dumpAllTracedMessages", null, null);
+ assertEquals(tracedWithFilter, events.size(), "Unmatched message
should NOT add trace events");
+
+ // clear the filter
+ mbeanServer.setAttribute(on, new Attribute("TraceFilter", null));
+ String traceFilter = (String) mbeanServer.getAttribute(on,
"TraceFilter");
+ assertNull(traceFilter, "TraceFilter should be null after clearing");
+
+ // send a message WITHOUT header — should now be traced since filter
is cleared
+ resetMocks();
+ getMockEndpoint("mock:foo").expectedMessageCount(1);
+ getMockEndpoint("mock:bar").expectedMessageCount(1);
+ template.sendBody("direct:start", "After Clear");
+ assertMockEndpointsSatisfied();
+
+ events = (List<BacklogTracerEventMessage>) mbeanServer.invoke(on,
"dumpAllTracedMessages", null, null);
+ assertFalse(events.size() == tracedWithFilter,
+ "After clearing filter, additional trace events should appear
(before=" + tracedWithFilter
+ + ", after=" +
events.size() + ")");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ context.setUseBreadcrumb(false);
+ context.setBacklogTracingStandby(true);
+
+ from("direct:start")
+ .to("mock:foo").id("foo")
+ .to("mock:bar").id("bar");
+ }
+ };
+ }
+
+}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
b/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
index 016c1994c98e..bcbfa60dfab8 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
@@ -56,14 +56,14 @@ public abstract class ScheduledPollConsumer extends
DefaultConsumer
// if adding more options then align with
org.apache.camel.support.ScheduledPollEndpoint
private boolean startScheduler = true;
- private long initialDelay = 1000;
- private long delay = 500;
- private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
- private boolean useFixedDelay = true;
+ private volatile long initialDelay = 1000;
+ private volatile long delay = 500;
+ private volatile TimeUnit timeUnit = TimeUnit.MILLISECONDS;
+ private volatile boolean useFixedDelay = true;
private PollingConsumerPollStrategy pollStrategy;
- private LoggingLevel runLoggingLevel = LoggingLevel.TRACE;
- private boolean sendEmptyMessageWhenIdle;
- private boolean greedy;
+ private volatile LoggingLevel runLoggingLevel = LoggingLevel.TRACE;
+ private volatile boolean sendEmptyMessageWhenIdle;
+ private volatile boolean greedy;
private int backoffMultiplier;
private int backoffIdleThreshold;
private int backoffErrorThreshold;
diff --git
a/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
b/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
index 0595eed5bc43..1254990c99fe 100644
---
a/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
+++
b/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
@@ -73,13 +73,13 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
// configuration
@Metadata(description = "How many failed messages within the window would
trigger the circuit breaker to open",
defaultValue = "50")
- private int failureThreshold = 50;
+ private volatile int failureThreshold = 50;
@Metadata(description = "Sliding window for how long time to go back (in
millis) when counting number of failures",
defaultValue = "60000")
- private long failureWindow = 60000;
+ private volatile long failureWindow = 60000;
@Metadata(description = "Interval (in millis) for how often to check
whether a currently open circuit breaker may work again",
defaultValue = "30000")
- private long halfOpenAfter = 30000;
+ private volatile long halfOpenAfter = 30000;
@Metadata(description = "Whether to always keep the circuit breaker open
(never closes). This is only intended for development and testing purposes.")
private boolean keepOpen;
@Metadata(description = "Allows to only throttle based on certain types of
exceptions. Multiple exceptions (use FQN class name) can be separated by
comma.")
diff --git
a/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingInflightRoutePolicy.java
b/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingInflightRoutePolicy.java
index 9cb030b919c6..9a54ef0d92c6 100644
---
a/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingInflightRoutePolicy.java
+++
b/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingInflightRoutePolicy.java
@@ -71,14 +71,21 @@ public class ThrottlingInflightRoutePolicy extends
RoutePolicySupport implements
private final Lock lock = new ReentrantLock();
@Metadata(description = "Sets which scope the throttling should be based
upon, either route or total scoped.",
enums = "Context,Route", defaultValue = "Route")
- private ThrottlingScope scope = ThrottlingScope.Route;
+ private volatile ThrottlingScope scope = ThrottlingScope.Route;
@Metadata(description = "Sets the upper limit of number of concurrent
inflight exchanges at which point reached the throttler should suspend the
route.",
defaultValue = "1000")
- private int maxInflightExchanges = 1000;
+ private volatile int maxInflightExchanges = 1000;
@Metadata(description = "Sets at which percentage of the max the throttler
should start resuming the route.",
defaultValue = "70")
- private int resumePercentOfMax = 70;
- private int resumeInflightExchanges = 700;
+ private volatile int resumePercentOfMax = 70;
+ private volatile int resumeInflightExchanges = 700;
+
+ // immutable holder for throttling limits that must be visible atomically
on routing threads
+ private record ThrottlingLimits(int maxInflightExchanges, int
resumeInflightExchanges) {
+ }
+
+ private volatile ThrottlingLimits throttlingLimits = new
ThrottlingLimits(1000, 700);
+
@Metadata(description = "Sets the logging level to report the throttling
activity.",
javaType = "org.apache.camel.LoggingLevel", defaultValue =
"INFO", enums = "TRACE,DEBUG,INFO,WARN,ERROR,OFF")
private LoggingLevel loggingLevel = LoggingLevel.INFO;
@@ -128,15 +135,20 @@ public class ThrottlingInflightRoutePolicy extends
RoutePolicySupport implements
// this works the best when this logic is executed when the exchange
is done
Consumer consumer = route.getConsumer();
+ // snapshot the volatile holder once for consistent reads
+ ThrottlingLimits limits = this.throttlingLimits;
+ int maxInflight = limits.maxInflightExchanges();
+ int resumeInflight = limits.resumeInflightExchanges();
+
int size = getSize(route, exchange);
- boolean stop = maxInflightExchanges > 0 && size > maxInflightExchanges;
+ boolean stop = maxInflight > 0 && size > maxInflight;
if (LOG.isTraceEnabled()) {
- LOG.trace("{} > 0 && {} > {} evaluated as {}",
maxInflightExchanges, size, maxInflightExchanges, stop);
+ LOG.trace("{} > 0 && {} > {} evaluated as {}", maxInflight, size,
maxInflight, stop);
}
if (stop) {
try {
lock.lock();
- stopConsumer(size, consumer);
+ stopConsumer(size, consumer, maxInflight);
} catch (Exception e) {
handleException(e);
} finally {
@@ -147,14 +159,14 @@ public class ThrottlingInflightRoutePolicy extends
RoutePolicySupport implements
// reload size in case a race condition with too many at once being
invoked
// so we need to ensure that we read the most current size and start
the consumer if we are already to low
size = getSize(route, exchange);
- boolean start = size <= resumeInflightExchanges;
+ boolean start = size <= resumeInflight;
if (LOG.isTraceEnabled()) {
- LOG.trace("{} <= {} evaluated as {}", size,
resumeInflightExchanges, start);
+ LOG.trace("{} <= {} evaluated as {}", size, resumeInflight, start);
}
if (start) {
try {
lock.lock();
- startConsumer(size, consumer);
+ startConsumer(size, consumer, resumeInflight);
} catch (Exception e) {
handleException(e);
} finally {
@@ -178,7 +190,10 @@ public class ThrottlingInflightRoutePolicy extends
RoutePolicySupport implements
public void setMaxInflightExchanges(int maxInflightExchanges) {
this.maxInflightExchanges = maxInflightExchanges;
// recalculate, must be at least at 1
- this.resumeInflightExchanges = Math.max(resumePercentOfMax *
maxInflightExchanges / 100, 1);
+ int resume = Math.max(resumePercentOfMax * maxInflightExchanges / 100,
1);
+ this.resumeInflightExchanges = resume;
+ // atomically publish both values for routing threads
+ this.throttlingLimits = new ThrottlingLimits(maxInflightExchanges,
resume);
}
public int getResumePercentOfMax() {
@@ -199,7 +214,10 @@ public class ThrottlingInflightRoutePolicy extends
RoutePolicySupport implements
this.resumePercentOfMax = resumePercentOfMax;
// recalculate, must be at least at 1
- this.resumeInflightExchanges = Math.max(resumePercentOfMax *
maxInflightExchanges / 100, 1);
+ int resume = Math.max(resumePercentOfMax * maxInflightExchanges / 100,
1);
+ this.resumeInflightExchanges = resume;
+ // atomically publish both values for routing threads
+ this.throttlingLimits = new ThrottlingLimits(maxInflightExchanges,
resume);
}
public ThrottlingScope getScope() {
@@ -258,18 +276,18 @@ public class ThrottlingInflightRoutePolicy extends
RoutePolicySupport implements
}
}
- private void startConsumer(int size, Consumer consumer) throws Exception {
+ private void startConsumer(int size, Consumer consumer, int
resumeInflight) throws Exception {
boolean started = resumeOrStartConsumer(consumer);
if (started) {
- getLogger().log("Throttling consumer: " + size + " <= " +
resumeInflightExchanges
+ getLogger().log("Throttling consumer: " + size + " <= " +
resumeInflight
+ " inflight exchange by resuming consumer: " +
consumer);
}
}
- private void stopConsumer(int size, Consumer consumer) throws Exception {
+ private void stopConsumer(int size, Consumer consumer, int maxInflight)
throws Exception {
boolean stopped = suspendOrStopConsumer(consumer);
if (stopped) {
- getLogger().log("Throttling consumer: " + size + " > " +
maxInflightExchanges
+ getLogger().log("Throttling consumer: " + size + " > " +
maxInflight
+ " inflight exchange by suspending consumer: " +
consumer);
}
}