This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 c5ddabd2a348 CAMEL-26752: camel-util - TimeSource: a check against
direct clock calls in the moved classes, and a log line for a non-default
provider (#29526)
c5ddabd2a348 is described below
commit c5ddabd2a348178e9c393a31e6ab063c74a35dd2
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Nov 10 18:38:27 2026 +0530
CAMEL-26752: camel-util - TimeSource: a check against direct clock calls in
the moved classes, and a log line for a non-default provider (#29526)
Follow-ups of CAMEL-25911, as agreed on the dev@ list for the internal time
source in camel-util. TimeSourceDirectClockCallsTest in camel-core scans the
23 core classes that read the clock and sleep through TimeSource, and fails
on direct clock calls (System.nanoTime, currentTimeMillis, Thread.sleep,
Instant.now and the other java.time forms); a second test fails when a class
uses TimeSource but is not in the list. TimeSource.install logs an INFO line
when a non-default provider is installed or the default is put back. The
TimeSource Javadoc and test-dst.adoc say how components get virtual time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/test-dst.adoc | 8 +-
.../camel-test-dst/src/main/docs/test-dst.adoc | 8 +-
.../camel/util/TimeSourceDirectClockCallsTest.java | 373 +++++++++++++++++++++
.../java/org/apache/camel/util/TimeSource.java | 29 +-
.../org/apache/camel/util/TimeSourceLogTest.java | 131 ++++++++
design/chaos.adoc | 6 +-
6 files changed, 542 insertions(+), 13 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/test-dst.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/test-dst.adoc
index 8c5136a44328..133d665a477f 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/test-dst.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/test-dst.adoc
@@ -25,7 +25,9 @@ with the same step trace.
This is a test tool. Nothing of it runs in production. Camel core has one
internal hook for it, `TimeSource` in
camel-util: with its default provider it reads `System` and sleeps with
`Thread.sleep`, and only an open run replaces
-the provider.
+the provider (installing it, and putting the default back, is logged at INFO
with the class of the provider).
+`TimeSource` is not an API for components: a component gets virtual time
through the core classes it already uses
+(the "Camel core" row of the table below); `camel-chaos` is the one exception
that reads it directly.
== A test
@@ -135,7 +137,9 @@ the synchronous redelivery (`RedeliveryPolicy`,
`RedeliveryErrorHandler`), `Fore
the budgets of both tasks, the wait loop of `DefaultShutdownStrategy`, the
synchronous delayer, `MulticastProcessor`,
`OptimisticLockRetryPolicy`, `DefaultStartupConditionStrategy`, `StopWatch`,
`MonotonicClock`, `BackOffTimerTask`,
`DefaultTimeoutMap`, the clocks of the throttlers and the counters of
`ThrottlingExceptionRoutePolicy`. A sleep there is
-a virtual wait on the loop thread, traced with its call site. Two waits of
these classes are not virtual (see below):
+a virtual wait on the loop thread, traced with its call site. A test of
camel-core (`TimeSourceDirectClockCallsTest`)
+fails when one of these classes calls `System.nanoTime()`,
`System.currentTimeMillis()` or `Thread.sleep` directly.
+Two waits of these classes are not virtual (see below):
the permit queue of `TotalRequestsThrottler` and the `Semaphore` of
`ConcurrentRequestsThrottler`. The sleep of the
route `delayer` option is not virtual either (see below). The half-open check
of `ThrottlingExceptionRoutePolicy` runs
on a scheduler of the `ExecutorServiceManager`, so the circuit is half opened
after `halfOpenAfter` of virtual time.
diff --git a/components/camel-test/camel-test-dst/src/main/docs/test-dst.adoc
b/components/camel-test/camel-test-dst/src/main/docs/test-dst.adoc
index 8c5136a44328..133d665a477f 100644
--- a/components/camel-test/camel-test-dst/src/main/docs/test-dst.adoc
+++ b/components/camel-test/camel-test-dst/src/main/docs/test-dst.adoc
@@ -25,7 +25,9 @@ with the same step trace.
This is a test tool. Nothing of it runs in production. Camel core has one
internal hook for it, `TimeSource` in
camel-util: with its default provider it reads `System` and sleeps with
`Thread.sleep`, and only an open run replaces
-the provider.
+the provider (installing it, and putting the default back, is logged at INFO
with the class of the provider).
+`TimeSource` is not an API for components: a component gets virtual time
through the core classes it already uses
+(the "Camel core" row of the table below); `camel-chaos` is the one exception
that reads it directly.
== A test
@@ -135,7 +137,9 @@ the synchronous redelivery (`RedeliveryPolicy`,
`RedeliveryErrorHandler`), `Fore
the budgets of both tasks, the wait loop of `DefaultShutdownStrategy`, the
synchronous delayer, `MulticastProcessor`,
`OptimisticLockRetryPolicy`, `DefaultStartupConditionStrategy`, `StopWatch`,
`MonotonicClock`, `BackOffTimerTask`,
`DefaultTimeoutMap`, the clocks of the throttlers and the counters of
`ThrottlingExceptionRoutePolicy`. A sleep there is
-a virtual wait on the loop thread, traced with its call site. Two waits of
these classes are not virtual (see below):
+a virtual wait on the loop thread, traced with its call site. A test of
camel-core (`TimeSourceDirectClockCallsTest`)
+fails when one of these classes calls `System.nanoTime()`,
`System.currentTimeMillis()` or `Thread.sleep` directly.
+Two waits of these classes are not virtual (see below):
the permit queue of `TotalRequestsThrottler` and the `Semaphore` of
`ConcurrentRequestsThrottler`. The sleep of the
route `delayer` option is not virtual either (see below). The half-open check
of `ThrottlingExceptionRoutePolicy` runs
on a scheduler of the `ExecutorServiceManager`, so the circuit is half opened
after `halfOpenAfter` of virtual time.
diff --git
a/core/camel-core/src/test/java/org/apache/camel/util/TimeSourceDirectClockCallsTest.java
b/core/camel-core/src/test/java/org/apache/camel/util/TimeSourceDirectClockCallsTest.java
new file mode 100644
index 000000000000..3db1b124c607
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/util/TimeSourceDirectClockCallsTest.java
@@ -0,0 +1,373 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.CodeSource;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.opentest4j.AssertionFailedError;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * The core classes that read the clock and sleep through {@link TimeSource}
(CAMEL-25911) must not call the system
+ * clock directly, or a deterministic simulation test (camel-test-dst) would
not run that wait in virtual time. The
+ * check is a textual scan of their sources, as Camel does not use the
forbidden-apis plugin: it finds the usual forms
+ * of these calls only, it is not a parser. It finds, as calls or method
references (also fully qualified):
+ * {@code System.nanoTime()}, {@code System.currentTimeMillis()}, {@code
Thread.sleep},
+ * {@code TimeUnit.SECONDS.sleep(...)} and the other units, {@code
Instant.now()}, the {@code now()} of
+ * {@code LocalDateTime}, {@code LocalDate}, {@code LocalTime}, {@code
OffsetDateTime}, {@code OffsetTime} and
+ * {@code ZonedDateTime}, {@code new Date()} and {@code Clock.systemUTC()} and
the other {@code Clock.system...} clocks;
+ * and the static imports of {@code System.nanoTime}, {@code
System.currentTimeMillis}, {@code Thread.sleep},
+ * {@code System.*} and {@code Thread.*}. It does not find other static
imports (such as of {@code Instant.now} or of a
+ * {@code TimeUnit} constant), nor a {@code sleep} on a variable or a field,
such as {@code unit.sleep(x)}: the listed
+ * classes make such calls that are fine ({@code TimeSource} calls its
provider, and the redelivery error handler
+ * {@code RedeliveryPolicy.sleep}), so that rule would only add false reports.
camel-chaos is not scanned: nothing was
+ * moved there, and the waits of its assertions run in real time on purpose.
+ */
+class TimeSourceDirectClockCallsTest {
+
+ /**
+ * The classes that read the clock and sleep through {@link TimeSource},
by source file relative to the core
+ * directory. To add a class: move its clock reads and sleeps to {@link
TimeSource}, then add its source file here
+ * (and to the list of core classes in test-dst.adoc of camel-test-dst).
+ */
+ static final List<String> CLASSES = List.of(
+ // camel-util
+ "camel-util/src/main/java/org/apache/camel/util/TimeSource.java",
+ "camel-util/src/main/java/org/apache/camel/util/StopWatch.java",
+
"camel-util/src/main/java/org/apache/camel/util/backoff/BackOffTimerTask.java",
+ // camel-support
+
"camel-support/src/main/java/org/apache/camel/support/DefaultTimeoutMap.java",
+
"camel-support/src/main/java/org/apache/camel/support/MonotonicClock.java",
+
"camel-support/src/main/java/org/apache/camel/support/SendHelper.java",
+
"camel-support/src/main/java/org/apache/camel/support/StepHelper.java",
+
"camel-support/src/main/java/org/apache/camel/support/startup/DefaultStartupConditionStrategy.java",
+
"camel-support/src/main/java/org/apache/camel/support/task/AbstractTask.java",
+
"camel-support/src/main/java/org/apache/camel/support/task/BackgroundTask.java",
+
"camel-support/src/main/java/org/apache/camel/support/task/ForegroundTask.java",
+
"camel-support/src/main/java/org/apache/camel/support/task/budget/IterationBoundedBudget.java",
+
"camel-support/src/main/java/org/apache/camel/support/task/budget/TimeBoundedBudget.java",
+
"camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java",
+ // camel-core-processor
+
"camel-core-processor/src/main/java/org/apache/camel/processor/ConcurrentRequestsThrottler.java",
+
"camel-core-processor/src/main/java/org/apache/camel/processor/DelayProcessorSupport.java",
+
"camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java",
+
"camel-core-processor/src/main/java/org/apache/camel/processor/SamplingThrottler.java",
+
"camel-core-processor/src/main/java/org/apache/camel/processor/TotalRequestsThrottler.java",
+
"camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/OptimisticLockRetryPolicy.java",
+
"camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java",
+
"camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryPolicy.java",
+ // camel-base-engine
+
"camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java");
+
+ /** The core modules of the classes in {@link #CLASSES}. */
+ static final List<String> MODULES
+ = List.of("camel-util", "camel-support", "camel-core-processor",
"camel-base-engine");
+
+ /**
+ * The nested classes allowed to call the system clock, by source file:
only the default provider of
+ * {@link TimeSource}.
+ */
+ static final Map<String, String> ALLOWED = Map.of(
+ "camel-util/src/main/java/org/apache/camel/util/TimeSource.java",
"SystemProvider");
+
+ /** A forbidden call and what to use instead. */
+ private record Rule(Pattern pattern, String call, String instead) {
+ }
+
+ private static final List<Rule> RULES = List.of(
+ rule("\\bSystem\\s*\\.\\s*nanoTime\\s*\\(", "System.nanoTime()",
"TimeSource.nanoTime()"),
+ rule("\\bSystem\\s*\\.\\s*currentTimeMillis\\s*\\(",
"System.currentTimeMillis()",
+ "TimeSource.currentTimeMillis()"),
+ rule("\\bThread\\s*\\.\\s*sleep\\s*\\(", "Thread.sleep",
"TimeSource.sleep(millis)"),
+ rule("\\bSystem\\s*::\\s*nanoTime\\b", "System::nanoTime",
"TimeSource::nanoTime"),
+ rule("\\bSystem\\s*::\\s*currentTimeMillis\\b",
"System::currentTimeMillis",
+ "TimeSource::currentTimeMillis"),
+ rule("\\bThread\\s*::\\s*sleep\\b", "Thread::sleep",
"TimeSource::sleep"),
+ rule("\\bInstant\\s*::\\s*now\\b", "Instant::now",
"TimeSource::instant"),
+ rule("\\bInstant\\s*\\.\\s*now\\s*\\(", "Instant.now()",
"TimeSource.instant()"),
+
rule("\\bTimeUnit\\s*\\.\\s*[A-Z]+\\s*(\\.\\s*sleep\\s*\\(|::\\s*sleep\\b)",
"TimeUnit.<unit>.sleep(...)",
+ "TimeSource.sleep(unit.toMillis(duration))"),
+
rule("\\bimport\\s+static\\s+java\\.lang\\.(System\\s*\\.\\s*(nanoTime|currentTimeMillis|\\*)"
+ + "|Thread\\s*\\.\\s*(sleep|\\*))",
+ "a static import of the system clock or sleep", "the
TimeSource methods"),
+
rule("\\b(LocalDateTime|LocalDate|LocalTime|OffsetDateTime|OffsetTime|ZonedDateTime)\\s*(\\.|::)\\s*now\\b",
+ "<java.time type>.now()", "TimeSource.instant()"),
+
rule("\\bnew\\s+(java\\s*\\.\\s*util\\s*\\.\\s*)?Date\\s*\\(\\s*\\)|\\bDate\\s*::\\s*new\\b",
"new Date()",
+ "new Date(TimeSource.currentTimeMillis())"),
+ rule("\\bClock\\s*(\\.|::)\\s*system\\w*\\b", "Clock.<system
clock>()",
+ "TimeSource.instant()"));
+
+ private static Rule rule(String regex, String call, String instead) {
+ return new Rule(Pattern.compile(regex), call, instead);
+ }
+
+ /**
+ * The core directory: the parent of the camel-core module, found from
where this test class is loaded (such as
+ * camel-core/target/test-classes), so the test also runs from an IDE with
another working directory.
+ */
+ static Path coreDir() {
+ CodeSource source =
TimeSourceDirectClockCallsTest.class.getProtectionDomain().getCodeSource();
+ if (source == null) {
+ return fail("The location of " +
TimeSourceDirectClockCallsTest.class.getName() + " is not known");
+ }
+ Path start;
+ try {
+ start = Paths.get(source.getLocation().toURI());
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException(e);
+ }
+ for (Path dir = start.toAbsolutePath().normalize(); dir != null; dir =
dir.getParent()) {
+ if (Files.isDirectory(dir.resolve("camel-util/src/main/java"))
+ && Files.isDirectory(dir.resolve("camel-core/src"))) {
+ return dir;
+ }
+ }
+ return fail("The core directory (with camel-util and camel-core) is
not found above " + start);
+ }
+
+ @Test
+ void theClassesReadTheClockOnlyThroughTimeSource() throws IOException {
+ Path core = coreDir();
+ List<String> missing = new ArrayList<>();
+ List<String> problems = new ArrayList<>();
+ for (String file : CLASSES) {
+ Path path = core.resolve(file);
+ if (!Files.isRegularFile(path)) {
+ missing.add(file);
+ continue;
+ }
+ problems.addAll(scan(file, Files.readString(path,
StandardCharsets.UTF_8), ALLOWED.get(file)));
+ }
+ if (!missing.isEmpty()) {
+ fail("These sources are not found under " + core + " (a class was
moved or renamed: update the list in "
+ + TimeSourceDirectClockCallsTest.class.getSimpleName() +
"):\n " + String.join("\n ", missing));
+ }
+ if (!problems.isEmpty()) {
+ fail("These classes read the clock and sleep through
org.apache.camel.util.TimeSource (so a deterministic"
+ + " simulation test runs them in virtual time), but call the
system clock directly:\n "
+ + String.join("\n ", problems));
+ }
+ }
+
+ @Test
+ void everyClassOfTheCoreModulesThatUsesTimeSourceIsInTheList() throws
IOException {
+ Path core = coreDir();
+ Pattern use = Pattern.compile("\\bTimeSource\\s*(\\.|::)");
+ List<String> unlisted = new ArrayList<>();
+ for (String module : MODULES) {
+ Path root = core.resolve(module).resolve("src/main/java");
+ assertTrue(Files.isDirectory(root), root + " is not found");
+ try (Stream<Path> files = Files.walk(root)) {
+ for (Path path : files.filter(f ->
f.toString().endsWith(".java")).toList()) {
+ String file =
core.relativize(path).toString().replace('\\', '/');
+ String code =
blankCommentsAndLiterals(Files.readString(path, StandardCharsets.UTF_8));
+ if (!CLASSES.contains(file) && use.matcher(code).find()) {
+ unlisted.add(file);
+ }
+ }
+ }
+ }
+ if (!unlisted.isEmpty()) {
+ fail("These classes use org.apache.camel.util.TimeSource but are
not in the list of "
+ + TimeSourceDirectClockCallsTest.class.getSimpleName() + ":
add them, so their direct clock calls are"
+ + " checked too:\n " + String.join("\n ", unlisted));
+ }
+ }
+
+ @Test
+ void theScanFindsDirectCallsOnly() {
+ String source = """
+ class Foo {
+ // System.nanoTime() in a comment
+ /* System.currentTimeMillis() in a block comment */
+ String s = "System.nanoTime() in a string, http://x";
+ String t = \"""
+ Instant.now() in a text block \\\\\""";
+ char u = '\\\\';
+ String v = "\\\\";
+ long a = System.nanoTime();
+ void b() throws Exception {
+ Thread.sleep (5);
+ TimeUnit.SECONDS.sleep(1);
+ }
+ long c = java.lang.System.currentTimeMillis();
+ Object d = Instant.now();
+ LongSupplier e = System::nanoTime;
+ long f = TimeSource.nanoTime();
+ Object g = LocalDateTime.now();
+ Object h = new java.util.Date();
+ Object i = Clock.systemUTC(); Object k =
TimeUnit.SECONDS::sleep; Supplier<Date> m = Date::new;
+ void j(TimeUnit unit, RedeliveryPolicy policy) throws
Exception { unit.sleep(1); policy.sleep(2); }
+ static final class SystemProvider {
+ long nanoTime() {
+ return System.nanoTime();
+ }
+ }
+ }
+ """;
+
+ assertEquals(List.of(
+ "Foo.java:9: System.nanoTime(), use TimeSource.nanoTime()
instead",
+ "Foo.java:11: Thread.sleep, use TimeSource.sleep(millis)
instead",
+ "Foo.java:12: TimeUnit.<unit>.sleep(...), use
TimeSource.sleep(unit.toMillis(duration)) instead",
+ "Foo.java:14: System.currentTimeMillis(), use
TimeSource.currentTimeMillis() instead",
+ "Foo.java:15: Instant.now(), use TimeSource.instant() instead",
+ "Foo.java:16: System::nanoTime, use TimeSource::nanoTime
instead",
+ "Foo.java:18: <java.time type>.now(), use TimeSource.instant()
instead",
+ "Foo.java:19: new Date(), use new
Date(TimeSource.currentTimeMillis()) instead",
+ "Foo.java:20: TimeUnit.<unit>.sleep(...), use
TimeSource.sleep(unit.toMillis(duration)) instead",
+ "Foo.java:20: new Date(), use new
Date(TimeSource.currentTimeMillis()) instead",
+ "Foo.java:20: Clock.<system clock>(), use TimeSource.instant()
instead"),
+ scan("Foo.java", source, "SystemProvider"));
+ assertEquals(12, scan("Foo.java", source, null).size());
+ assertEquals(List.of(
+ "Bar.java:1: a static import of the system clock or sleep, use
the TimeSource methods instead",
+ "Bar.java:2: a static import of the system clock or sleep, use
the TimeSource methods instead"),
+ scan("Bar.java", "import static java.lang.System.*;\nimport
static java.lang.Thread.*;\nclass Bar {}\n",
+ null));
+ assertThrows(AssertionFailedError.class, () -> scan("Foo.java", "class
Foo { /* no end", null));
+ assertThrows(AssertionFailedError.class, () -> scan("Foo.java", "class
Foo { String s = \"no end\n}", null));
+ }
+
+ /**
+ * The direct clock calls in the source, as {@code file:line: call, use
... instead}, outside comments, string and
+ * character literals and the body of the allowed nested class.
+ */
+ static List<String> scan(String file, String source, String
allowedNestedClass) {
+ String code = blankCommentsAndLiterals(source);
+ if (allowedNestedClass != null) {
+ code = blankNestedClass(file, code, allowedNestedClass);
+ }
+ List<Problem> problems = new ArrayList<>();
+ for (Rule rule : RULES) {
+ Matcher m = rule.pattern().matcher(code);
+ while (m.find()) {
+ problems.add(new Problem(lineOf(code, m.start()), rule));
+ }
+ }
+ problems.sort(Comparator.comparingInt(Problem::line));
+ return problems.stream()
+ .map(p -> file + ":" + p.line() + ": " + p.rule().call() + ",
use " + p.rule().instead() + " instead")
+ .toList();
+ }
+
+ private record Problem(int line, Rule rule) {
+ }
+
+ private static int lineOf(String code, int index) {
+ int line = 1;
+ for (int i = 0; i < index; i++) {
+ if (code.charAt(i) == '\n') {
+ line++;
+ }
+ }
+ return line;
+ }
+
+ /** Replaces comments and string, text block and character literals by
spaces, keeping the line breaks. */
+ static String blankCommentsAndLiterals(String source) {
+ StringBuilder sb = new StringBuilder(source);
+ int i = 0;
+ int n = source.length();
+ while (i < n) {
+ char c = source.charAt(i);
+ if (source.startsWith("//", i)) {
+ int end = source.indexOf('\n', i);
+ i = blank(sb, i, end < 0 ? n : end);
+ } else if (source.startsWith("/*", i)) {
+ int end = source.indexOf("*/", i + 2);
+ if (end < 0) {
+ fail("Unterminated comment at offset " + i);
+ }
+ i = blank(sb, i, end + 2);
+ } else if (source.startsWith("\"\"\"", i)) {
+ // a text block: skip escapes (an escaped quote does not end
it, an escaped backslash does not escape)
+ int j = i + 3;
+ while (j < n && !source.startsWith("\"\"\"", j)) {
+ j += source.charAt(j) == '\\' ? 2 : 1;
+ }
+ if (j >= n) {
+ fail("Unterminated text block at offset " + i);
+ }
+ i = blank(sb, i, j + 3);
+ } else if (c == '"' || c == '\'') {
+ int j = i + 1;
+ while (j < n && source.charAt(j) != c && source.charAt(j) !=
'\n') {
+ j += source.charAt(j) == '\\' ? 2 : 1;
+ }
+ if (j >= n || source.charAt(j) != c) {
+ fail("Unterminated literal at offset " + i);
+ }
+ i = blank(sb, i, j + 1);
+ } else {
+ i++;
+ }
+ }
+ return sb.toString();
+ }
+
+ private static int blank(StringBuilder sb, int from, int to) {
+ for (int k = from; k < to; k++) {
+ if (sb.charAt(k) != '\n') {
+ sb.setCharAt(k, ' ');
+ }
+ }
+ return to;
+ }
+
+ /** Replaces the body of the given nested class by spaces, keeping the
line breaks. */
+ private static String blankNestedClass(String file, String code, String
name) {
+ Matcher m = Pattern.compile("\\bclass\\s+" + Pattern.quote(name) +
"\\b").matcher(code);
+ if (!m.find()) {
+ fail(file + ": the allowed nested class " + name + " is not found:
update the allow-list in "
+ + TimeSourceDirectClockCallsTest.class.getSimpleName());
+ }
+ int open = code.indexOf('{', m.end());
+ int depth = 0;
+ int close = open;
+ for (int k = open; k < code.length(); k++) {
+ char c = code.charAt(k);
+ if (c == '{') {
+ depth++;
+ } else if (c == '}' && --depth == 0) {
+ close = k;
+ break;
+ }
+ }
+ StringBuilder sb = new StringBuilder(code);
+ blank(sb, open, close + 1);
+ return sb.toString();
+ }
+}
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/TimeSource.java
b/core/camel-util/src/main/java/org/apache/camel/util/TimeSource.java
index 3a4d28047ecf..82f660f9d98f 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/TimeSource.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/TimeSource.java
@@ -18,16 +18,21 @@ package org.apache.camel.util;
import java.time.Instant;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
/**
* The clock and the sleeps of the Camel core code that waits, times tasks or
throttles (for example the redelivery
* delay, the wait loop of the graceful shutdown, {@link StopWatch} and the
throttlers).
* <p>
- * <b>Internal:</b> this is not an API for Camel users or components, except
that camel-chaos reads the clock through it
- * (so its rules end in virtual time in a deterministic simulation test, and
the step and send hooks of camel-core wait
- * for its delays through it too); it never installs a provider. The default
provider delegates to
- * {@link System#currentTimeMillis()}, {@link System#nanoTime()}, {@link
Instant#now()} and {@link Thread#sleep(long)}.
- * Only a test harness, such as the deterministic simulation tests of
camel-test-dst, installs another provider with
- * {@link #install(Provider)}, for example a virtual clock, and puts the
previous one back when the test is done.
+ * <b>Internal:</b> this is not an API for Camel users or components. A
component gets virtual time through the core
+ * classes it already uses (the task classes, the delayer, the throttlers, the
redelivery), not by calling this class.
+ * camel-chaos is the one exception: it reads the clock and sleeps through it
(so its rules end in virtual time in a
+ * deterministic simulation test, and the step and send hooks of camel-core
wait for its delays through it too); it
+ * never installs a provider. The default provider delegates to {@link
System#currentTimeMillis()},
+ * {@link System#nanoTime()}, {@link Instant#now()} and {@link
Thread#sleep(long)}. Only a test harness, such as the
+ * deterministic simulation tests of camel-test-dst, installs another provider
with {@link #install(Provider)}, for
+ * example a virtual clock, and puts the previous one back when the test is
done.
* <p>
* The provider is static rather than per {@code CamelContext}, because {@link
StopWatch} and the other timers have no
* context. Event timestamps and the direct clock reads of the debugger, the
tracer, management and the developer
@@ -71,6 +76,8 @@ public final class TimeSource {
void sleep(long millis) throws InterruptedException;
}
+ private static final Logger LOG =
LoggerFactory.getLogger(TimeSource.class);
+
private static final Provider SYSTEM = new SystemProvider();
private static volatile Provider provider = SYSTEM;
@@ -114,6 +121,10 @@ public final class TimeSource {
/**
* Installs a provider, for test harnesses only.
+ * <p>
+ * The provider is used by every {@code CamelContext} that shares this
class (in a shared runtime such as Karaf, all
+ * of them), so installing a provider other than the default is logged at
INFO with its class, and so is putting the
+ * default back.
*
* @param provider the provider to use from now on
* @return the provider that was installed before, to be
installed again when the test is done
@@ -123,6 +134,12 @@ public final class TimeSource {
synchronized (TimeSource.class) {
Provider previous = TimeSource.provider;
TimeSource.provider = provider;
+ // logged under the lock, so the lines of two installs come in the
order of the installs
+ if (provider != SYSTEM) {
+ LOG.info("Installed TimeSource provider: {}",
provider.getClass().getName());
+ } else if (previous != SYSTEM) {
+ LOG.info("Installed default TimeSource provider (replacing:
{})", previous.getClass().getName());
+ }
return previous;
}
}
diff --git
a/core/camel-util/src/test/java/org/apache/camel/util/TimeSourceLogTest.java
b/core/camel-util/src/test/java/org/apache/camel/util/TimeSourceLogTest.java
new file mode 100644
index 000000000000..f7619872c319
--- /dev/null
+++ b/core/camel-util/src/test/java/org/apache/camel/util/TimeSourceLogTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.util;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.logging.log4j.core.config.Property;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Installing a provider other than the default, and putting the default back,
is logged at INFO with the class of the
+ * provider: the provider is static, so in a shared runtime it applies to
every CamelContext.
+ */
+@Isolated
+class TimeSourceLogTest {
+
+ private static final String LOGGER = TimeSource.class.getName();
+
+ /** A provider of a test harness (only its class matters here). */
+ private static final class HarnessClock implements TimeSource.Provider {
+ @Override
+ public long currentTimeMillis() {
+ return 0;
+ }
+
+ @Override
+ public long nanoTime() {
+ return 0;
+ }
+
+ @Override
+ public void sleep(long millis) {
+ // virtual
+ }
+ }
+
+ private final List<LogEvent> events = new CopyOnWriteArrayList<>();
+ private AbstractAppender appender;
+
+ @BeforeEach
+ void addAppender() {
+ TimeSource.reset();
+ appender = new AbstractAppender("TimeSourceLogTest", null, null, true,
Property.EMPTY_ARRAY) {
+ @Override
+ public void append(LogEvent event) {
+ events.add(event.toImmutable());
+ }
+ };
+ appender.start();
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ Configuration config = ctx.getConfiguration();
+ LoggerConfig loggerConfig = new LoggerConfig(LOGGER, Level.INFO, true);
+ loggerConfig.addAppender(appender, Level.INFO, null);
+ config.addLogger(LOGGER, loggerConfig);
+ ctx.updateLoggers();
+ }
+
+ @AfterEach
+ void removeAppender() {
+ TimeSource.reset();
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ ctx.getConfiguration().removeLogger(LOGGER);
+ ctx.updateLoggers();
+ appender.stop();
+ }
+
+ private List<String> messages() {
+ return events.stream().map(e -> e.getLevel() + " " +
e.getMessage().getFormattedMessage()).toList();
+ }
+
+ @Test
+ void installingAndPuttingTheDefaultBackAreLoggedWithTheProviderClass() {
+ TimeSource.Provider previous = TimeSource.install(new HarnessClock());
+ TimeSource.install(previous);
+
+ List<String> messages = messages();
+ assertEquals(2, messages.size(), messages.toString());
+ assertEquals("INFO Installed TimeSource provider: " +
HarnessClock.class.getName(), messages.get(0));
+ assertEquals("INFO Installed default TimeSource provider (replacing: "
+ HarnessClock.class.getName() + ")",
+ messages.get(1));
+ }
+
+ @Test
+ void resetIsLoggedLikeInstallingTheDefault() {
+ TimeSource.install(new HarnessClock());
+ TimeSource.reset();
+
+ List<String> messages = messages();
+ assertEquals(2, messages.size(), messages.toString());
+ assertTrue(messages.get(1).startsWith("INFO Installed default
TimeSource provider"), messages.toString());
+ }
+
+ @Test
+ void nothingIsLoggedWhenTheDefaultStaysInstalled() {
+ TimeSource.reset();
+ TimeSource.install(TimeSource.install(new HarnessClock()));
+ events.clear();
+
+ TimeSource.reset();
+
+ assertTrue(messages().isEmpty(), messages().toString());
+ }
+}
diff --git a/design/chaos.adoc b/design/chaos.adoc
index 5587dc28b895..f37ccf2da634 100644
--- a/design/chaos.adoc
+++ b/design/chaos.adoc
@@ -342,9 +342,9 @@ on `System` in a DST run (`test-dst.adoc`), so the report
does not use them. A `
one-stack model of `test-dst.adoc`: a suspend or stop cannot see an exchange
that is blocked in a synchronous call
below it on the stack finish, so such a world calls its downstream with
`DstCamel.asyncStep`. Chaos does not need DST.
-The Javadoc of `TimeSource` says it is internal and not an API for components.
`camel-chaos` may use it: it reads the
-clock and sleeps through it, and never installs a provider (only a test
harness such as `camel-test-dst` does that).
-The Javadoc is updated to say so with the first chaos pull request.
+The Javadoc of `TimeSource` says it is internal and not an API for components:
a component gets virtual time through
+the core classes it already uses. `camel-chaos` is the one exception: it reads
the clock and sleeps through it, and
+never installs a provider (only a test harness such as `camel-test-dst` does
that).
=== The report