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
 

Reply via email to