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 c536c99caaaa CAMEL-25911: camel-test-dst - stage 2: an internal 
TimeSource in camel-util for core waits, task timing and throttling (#28382)
c536c99caaaa is described below

commit c536c99caaaa81a6e309b20b30881a3c9ec60dab
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Oct 25 15:14:02 2026 +0530

    CAMEL-25911: camel-test-dst - stage 2: an internal TimeSource in camel-util 
for core waits, task timing and throttling (#28382)
    
    Core code that waits, times tasks or throttles (redelivery, delayer, 
throttlers, ForegroundTask, the shutdown wait loop, StopWatch, MonotonicClock) 
now reads time and sleeps through an internal TimeSource in camel-util. The 
default provider makes the same JDK calls as before; camel-test-dst installs a 
virtual-time provider so these waits run on the seeded scheduler. The throttler 
TRACE "Queued for {}ms" lines now log milliseconds, and test-dst.adoc lists the 
waits that stay outside vi [...]
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/catalog/docs/test-dst.adoc    |  77 ++++++---
 .../aws2/sqs/dst/SqsRedeliveryDstTest.java         |  37 +++++
 .../camel/component/aws2/sqs/dst/SqsWorld.java     |  58 ++++++-
 .../camel-test-dst/src/main/docs/test-dst.adoc     |  77 ++++++---
 .../main/java/org/apache/camel/test/dst/Dst.java   |  21 ++-
 .../java/org/apache/camel/test/dst/DstClock.java   |  53 ++++++
 .../org/apache/camel/test/dst/DstExtension.java    |  31 ++--
 .../org/apache/camel/test/dst/DstInstrument.java   |  19 ++-
 .../apache/camel/test/dst/DstInstrumentation.java  |  16 +-
 .../apache/camel/test/dst/DstAlsoRewriteTest.java  |  34 ++--
 .../apache/camel/test/dst/DstCoreWaitsTest.java    |  71 ++++++--
 .../apache/camel/test/dst/DstTimeSourceTest.java   | 179 +++++++++++++++++++++
 .../camel/impl/engine/DefaultShutdownStrategy.java |   3 +-
 .../processor/ConcurrentRequestsThrottler.java     |  11 +-
 .../camel/processor/DelayProcessorSupport.java     |   5 +-
 .../apache/camel/processor/MulticastProcessor.java |   3 +-
 .../apache/camel/processor/SamplingThrottler.java  |   5 +-
 .../camel/processor/TotalRequestsThrottler.java    |  13 +-
 .../aggregate/OptimisticLockRetryPolicy.java       |   4 +-
 .../errorhandler/RedeliveryErrorHandler.java       |   3 +-
 .../processor/errorhandler/RedeliveryPolicy.java   |   3 +-
 .../apache/camel/support/DefaultTimeoutMap.java    |   3 +-
 .../org/apache/camel/support/MonotonicClock.java   |   7 +-
 .../startup/DefaultStartupConditionStrategy.java   |   3 +-
 .../apache/camel/support/task/AbstractTask.java    |   4 +-
 .../apache/camel/support/task/BackgroundTask.java  |   5 +-
 .../apache/camel/support/task/ForegroundTask.java  |  13 +-
 .../task/budget/IterationBoundedBudget.java        |   7 +-
 .../support/task/budget/TimeBoundedBudget.java     |   6 +-
 .../throttling/ThrottlingExceptionRoutePolicy.java |  13 +-
 .../main/java/org/apache/camel/util/StopWatch.java |  10 +-
 .../java/org/apache/camel/util/TimeSource.java     | 172 ++++++++++++++++++++
 .../camel/util/backoff/BackOffTimerTask.java       |   3 +-
 .../java/org/apache/camel/util/TimeSourceTest.java | 162 +++++++++++++++++++
 34 files changed, 986 insertions(+), 145 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 0d13aa8d264a..16bf1c96d140 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
@@ -16,13 +16,16 @@ with the same step trace.
 * Every thread pool created through the `ExecutorServiceManager` is backed by 
one event loop on the test thread
   (`DstThreadPoolFactory`). Tasks run one at a time; when several can run, the 
seed picks one.
 * Time is virtual: it jumps to the next due task. The time reads, sleeps and 
blocking waits of the code under test are
-  rewritten at test time (`@DstInstrument`), so "wait 30 seconds" costs 
nothing.
+  rewritten at test time (`@DstInstrument`), and the Camel core code that 
waits, times tasks or throttles reads the
+  virtual clock through its internal time source, so "wait 30 seconds" costs 
nothing.
 * A fake of the external system (a queue, a checkpoint store, a web API) 
injects faults chosen by the same seed.
 * Invariants are checked at the end of each run.
 * `@DstTest(seeds = 1000)` runs a test method once per seed. A failing seed is 
printed with its trace and is replayed
   exactly with `-Ddst.seed=4711` or `@DstSeed(4711)`.
 
-This is a test tool. Nothing of it runs in production, and it changes nothing 
in Camel.
+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.
 
 == A test
 
@@ -54,6 +57,8 @@ These worlds are in the Camel code base and are the examples 
to copy:
 |Component |Test |Regression test for
 
 |camel-aws2-sqs |`SqsVisibilityDstTest` (the visibility timeout extender) 
|CAMEL-25700
+|camel-aws2-sqs |`SqsRedeliveryDstTest` (the extender while Camel's error 
handler waits to redeliver, in virtual
+time) |-
 |camel-azure-eventhubs |`EventHubsCheckpointDstTest` (checkpointing by batch 
size and timeout) |CAMEL-25683
 |camel-pulsar |`PulsarConsumerDstTest` (subscribe failures, broker and route 
restarts while the route starts and
 consumes) |CAMEL-25869
@@ -78,13 +83,25 @@ virtual time. A wait that no task of the run can end fails 
the seed at once and
 |Timed waits on tasks |`Future.get(timeout)` on a task of the run and 
`awaitTermination` time out in virtual time.
 |Parallel streams |`parallelStream()` of a `java.util` collection is rewritten 
to `stream()`: the elements are handled
 in order by the calling task, not on the common fork-join pool.
+|Camel core |The code of Camel core that waits, times tasks or throttles reads 
the clock and sleeps through
+`org.apache.camel.util.TimeSource`, an internal class of camel-util. A run 
installs its virtual clock there (`Dst`
+does it when the run is created) and puts the previous provider back when the 
run is closed, so no rewriting is needed:
+the synchronous redelivery (`RedeliveryPolicy`, `RedeliveryErrorHandler`), 
`ForegroundTask`, `BackgroundTask.schedule`,
+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. Three waits of 
these classes are not virtual (see below):
+the permit queue of `TotalRequestsThrottler`, the `Semaphore` of 
`ConcurrentRequestsThrottler` and the half-open timer
+of `ThrottlingExceptionRoutePolicy`. The sleep of the route `delayer` option 
is not virtual either (see below).
 |===
 
-`@DstInstrument(alsoRewrite = ...)` applies the same rewriting to classes 
outside the code under test, for example
-`org.apache.camel.util.StopWatch`, `org.apache.camel.support.MonotonicClock`, 
`org.apache.camel.support.task`
-(`ForegroundTask`), `org.apache.camel.processor.errorhandler` (synchronous 
redelivery) or
-`org.apache.camel.impl.engine.DefaultShutdownStrategy` (its wait for in-flight 
exchanges). This is a stop-gap: Camel
-core reads `System` directly, and a time source in core that the harness can 
replace needs a dev@ discussion first.
+Event timestamps, the clock of an exchange and the direct clock reads of the 
debugger, the tracer, management and the
+developer consoles stay on `System`. The durations they measure with 
`StopWatch` or `MonotonicClock` follow `TimeSource`
+(with the default provider this is the same `System.nanoTime()`), and so does 
the created time of `MonotonicClock`: the
+message history times (`MessageHistory.getTime()`) are virtual in a DST run. 
`@DstInstrument(alsoRewrite = ...)` applies the
+rewriting of the code under test to classes outside it, for example 
`org.apache.camel.support.ResetableClock` (the
+clock of an exchange) or a library the component calls. It is not needed for 
the core classes listed above, except for
+the permit queue of `TotalRequestsThrottler` (see below).
 
 === Escapes and strict mode
 
@@ -93,15 +110,34 @@ Before the first seed, the harness scans the packages 
under test and writes ever
 cannot run the loop for (`Object.wait`, `LockSupport.park`, `Mono.block`), 
wall clock reads it does not rewrite.
 
 Strict mode is the default: when a seed reaches one of these at runtime, or a 
thread outside the harness submits a task
-to the loop, reads the virtual clock or sleeps, the seed fails with the escape 
as its cause. A clock read from such a
+to the loop, reads the virtual clock or sleeps, the seed fails with the escape 
as its cause. This includes Camel core:
+a `StopWatch` started on such a thread reads the virtual clock through 
`TimeSource`. A clock read from such a
 thread (a Reactor or Netty thread, for example) still gets the virtual time 
and its sleep still takes real time, so
 that thread mixes the two; this is what strict mode is there to make visible. 
`@DstInstrument(allowEscapes = "Foo.bar")` allows an escape
 that cannot change the result (say why in a comment); `@DstInstrument(strict = 
false)` or `-Ddst.strict=false` only
 records escapes in `target/dst/<Test>.summary.txt`.
 
-A wait in code that is not rewritten (a latch in Camel core, for example) 
blocks the loop thread for real. A watchdog
-interrupts the loop thread after 10 s of real time without progress 
(`-Ddst.watchdogMillis`) and the seed fails with the
-stack where it was blocked.
+A wait in code that is not rewritten blocks the loop thread for real. A 
watchdog interrupts the loop thread after 10 s
+of real time without progress (`-Ddst.watchdogMillis`) and the seed fails with 
the stack where it was blocked. In Camel
+core:
+
+* `BackgroundTask.run` waits on a latch until its scheduled attempts are done, 
so it blocks the loop thread.
+  `BackgroundTask.schedule` does not wait, and its attempts run in virtual 
time.
+* `TotalRequestsThrottler` waits for a permit on its `DelayQueue`. The delay 
of a permit follows the virtual clock,
+  which does not move while the loop thread waits, so a seed that has to wait 
for a permit always ends on the watchdog.
+  `asyncDelayed()` does not avoid it: a delayed exchange that finds its permit 
taken by another one waits on the same
+  queue. Add `org.apache.camel.processor.TotalRequestsThrottler` to 
`@DstInstrument(alsoRewrite)`: the permit wait is
+  then a virtual wait.
+* `ConcurrentRequestsThrottler` blocks on its `Semaphore` once 
`maxConcurrentRequests` exchanges are in flight. No task
+  of the run releases a permit while the loop thread waits, so the seed ends 
on the watchdog. `alsoRewrite` does not
+  help here (the call goes through a `Semaphore` subclass): keep 
`maxConcurrentRequests` above the number of exchanges
+  the world has in flight.
+* The route `delayer` option (`CamelInternalProcessor.DelayerAdvice`) calls 
`Thread.sleep` directly, not through
+  `TimeSource`: the delay takes real time on the loop thread and the virtual 
clock does not move. Use the `delay` EIP
+  in a world instead.
+* The half-open check of `ThrottlingExceptionRoutePolicy` runs on a 
`java.util.Timer` thread, outside the harness. The
+  timer fires after `halfOpenAfter` of real time (30 s by default, longer than 
the watchdog) and its clock read is an
+  escape, so a world does not see the circuit half-open in virtual time.
 
 === The one-stack model
 
@@ -404,12 +440,15 @@ seed of the in-JVM replay test of the Event Hubs world, 
default 1).
 * Code on its own threads or on a third-party scheduler is outside the 
harness; strict mode makes reaching it fail the
   seed.
 * One run per JVM at a time: the active run, and so the virtual clock, is a 
static field, and every rewritten clock read
-  in the JVM sees it. A world must therefore not go into a module that runs 
its tests in parallel
-  (`camel.surefire.parallel=true`, as camel-jms and camel-sql do): the other 
tests in that surefire fork would read the
-  virtual clock while a run is active. The harness enforces this: a second run 
while one is open fails, and a DST test
-  refuses to start when JUnit runs tests in parallel. A local world in such a 
module (camel-sjms, for example) can
-  still be run alone, with `-Dtest=<the world> 
-Dcamel.surefire.parallel=false`, but it cannot be committed there:
-  the module's own build would fail it.
+  in the JVM sees it, as does Camel core through `TimeSource` while the run is 
open. A world must therefore not go into
+  a module that runs its tests in parallel (`camel.surefire.parallel=true`, as 
camel-jms and camel-sql do): the other
+  tests in that surefire fork would read the virtual clock while a run is 
active. For the same reason, a
+  `CamelContext` left running in the same fork by an earlier non-DST test 
would read the virtual time too and could
+  fail a strict seed; stop it in that test. The harness enforces this: a second
+  run while one is open fails, and a DST test refuses to start when JUnit runs 
tests in parallel. A local world in
+  such a module (camel-sjms, for example) can still be run alone, with
+  `-Dtest=<the world> -Dcamel.surefire.parallel=false`, but it cannot be 
committed there: the module's own build would
+  fail it.
 * Locks do not exclude: every task runs on the test thread, so a 
`ReentrantLock` or `synchronized` block held by a
   blocked task is entered again by the tasks nested above it. A critical 
section is atomic only because the fake does
   not block inside it, and races between two code paths that each hold a lock 
cannot be explored.
@@ -427,7 +466,7 @@ seed of the in-JVM replay test of the Event Hubs world, 
default 1).
 strict mode
 |`DstThreadPoolFactory`, `DstExecutor`, `DstTask` |Camel's pools and 
schedulers backed by the loop
 |`DstInstrumentation`, `DstClock`, `DstWaits` |the byte code rewrite of the 
code under test: time reads, sleeps and
-blocking waits go to the loop
+blocking waits go to the loop; `DstClock` is also the `TimeSource` provider of 
Camel core during a run
 |`DstEscapes`, `DstWatchdog` |the escape scan, runtime escapes (strict mode), 
and the watchdog for waits in code that is
 not rewritten
 |`DstLedger`, `Invariant`, `Invariants` |what happened to each message, and 
reusable invariants
@@ -446,7 +485,7 @@ not rewritten
 |`dst.seed` |comma separated seeds to replay; their full traces are written |-
 |`dst.traceDir` |where the reports and traces go |`target/dst`
 |`dst.strict` |strict mode |`@DstInstrument(strict)`, true
-|`dst.alsoRewrite` |comma separated classes or packages to rewrite too 
(stop-gap) |-
+|`dst.alsoRewrite` |comma separated classes or packages outside the code under 
test to rewrite too |-
 |`dst.watchdogMillis` |real time without progress after which a waiting loop 
thread fails the seed |10000
 |===
 
diff --git 
a/components/camel-aws/camel-aws2-sqs/src/test/java/org/apache/camel/component/aws2/sqs/dst/SqsRedeliveryDstTest.java
 
b/components/camel-aws/camel-aws2-sqs/src/test/java/org/apache/camel/component/aws2/sqs/dst/SqsRedeliveryDstTest.java
new file mode 100644
index 000000000000..c0fef3d9415d
--- /dev/null
+++ 
b/components/camel-aws/camel-aws2-sqs/src/test/java/org/apache/camel/component/aws2/sqs/dst/SqsRedeliveryDstTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.component.aws2.sqs.dst;
+
+import org.apache.camel.test.dst.Dst;
+import org.apache.camel.test.dst.DstInstrument;
+import org.apache.camel.test.dst.DstTest;
+
+/**
+ * The SQS world of {@link SqsVisibilityDstTest} with synchronous redelivery: 
a failed message is retried by Camel's
+ * error handler after a redelivery delay longer than the visibility timeout, 
so the visibility timeout extender has to
+ * keep the message invisible while the error handler waits. The wait is a 
sleep in Camel core, which runs in virtual
+ * time through Camel's internal time source; without it each wait would block 
the test thread for real (and the seed
+ * would fail on the watchdog or on the invariant that the redelivery delay 
passed).
+ */
+@DstInstrument("org.apache.camel.component.aws2.sqs")
+class SqsRedeliveryDstTest {
+
+    @DstTest(seeds = 500)
+    void noMessageIsDeliveredAgainWhileTheErrorHandlerWaitsToRetryIt(Dst dst) 
throws Exception {
+        new SqsWorld(dst, true).run();
+    }
+}
diff --git 
a/components/camel-aws/camel-aws2-sqs/src/test/java/org/apache/camel/component/aws2/sqs/dst/SqsWorld.java
 
b/components/camel-aws/camel-aws2-sqs/src/test/java/org/apache/camel/component/aws2/sqs/dst/SqsWorld.java
index 56873b7dc080..64787fc96ef1 100644
--- 
a/components/camel-aws/camel-aws2-sqs/src/test/java/org/apache/camel/component/aws2/sqs/dst/SqsWorld.java
+++ 
b/components/camel-aws/camel-aws2-sqs/src/test/java/org/apache/camel/component/aws2/sqs/dst/SqsWorld.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.camel.Exchange;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.aws2.sqs.Sqs2Constants;
 import org.apache.camel.impl.DefaultCamelContext;
@@ -42,10 +43,15 @@ import org.apache.camel.test.dst.Invariants;
  * The property under test: while the consumer holds a message (from the 
receive until the route has finished with it),
  * SQS must not deliver it again, because the extender keeps it invisible. The 
only exception is a message whose
  * visibility change was lost by a fault of the fake.
+ * <p>
+ * With synchronous redelivery ({@link #SqsWorld(Dst, boolean)}) a failed 
message is retried by Camel's error handler
+ * after a redelivery delay longer than the visibility timeout, on the 
consumer's thread: the consumer holds the message
+ * during the wait, and the wait of the error handler (a sleep in Camel core) 
runs in virtual time.
  */
 final class SqsWorld {
 
     static final String ROUTE = "sqs";
+    static final int MAX_REDELIVERIES = 2;
 
     private final Dst dst;
     private final DstLedger ledger;
@@ -54,6 +60,10 @@ final class SqsWorld {
     private final int maxMessagesPerPoll;
     private final int concurrentConsumers;
     private final long delay;
+    private final boolean redelivery;
+    private final long redeliveryDelay;
+    private final Map<String, Long> failedAt = new HashMap<>();
+    private final List<String> redeliveredTooEarly = new ArrayList<>();
     private final Map<String, Integer> held = new HashMap<>();
     private final Set<String> extensionFaulted = new HashSet<>();
     private final List<String> redeliveredWhileHeld = new ArrayList<>();
@@ -61,7 +71,16 @@ final class SqsWorld {
     private boolean faults = true;
 
     SqsWorld(Dst dst) {
+        this(dst, false);
+    }
+
+    /**
+     * @param redelivery whether the route retries a failed message twice with 
the default error handler (synchronous
+     *                   redelivery), after a delay longer than the visibility 
timeout
+     */
+    SqsWorld(Dst dst, boolean redelivery) {
         this.dst = dst;
+        this.redelivery = redelivery;
         this.ledger = new DstLedger(dst);
         this.sqs = new FakeSqs(dst, ledger, this::received, 
extensionFaulted::addAll);
         // 1 s is left out by default: the extender is always late with 1 s 
(CAMEL-25737, open);
@@ -71,8 +90,11 @@ final class SqsWorld {
         this.maxMessagesPerPoll = dst.pick(List.of(1, 2, 5, 10));
         this.concurrentConsumers = dst.pick(List.of(1, 2, 3));
         this.delay = dst.pick(List.of(100L, 500L, 1000L));
+        // only drawn with redelivery, so the seeds of the world without it 
keep their configuration
+        this.redeliveryDelay = redelivery ? dst.between(visibilityTimeout * 
1000L, 3L * visibilityTimeout * 1000) : 0;
         dst.trace("world: visibilityTimeout=" + visibilityTimeout + " 
maxMessagesPerPoll=" + maxMessagesPerPoll
-                  + " concurrentConsumers=" + concurrentConsumers + " delay=" 
+ delay);
+                  + " concurrentConsumers=" + concurrentConsumers + " delay=" 
+ delay
+                  + (redelivery ? " redeliveryDelay=" + redeliveryDelay : ""));
     }
 
     void run() throws Exception {
@@ -81,6 +103,11 @@ final class SqsWorld {
         context.addRoutes(new RouteBuilder() {
             @Override
             public void configure() {
+                if (redelivery) {
+                    // synchronous redelivery: the error handler sleeps on the 
consumer's thread between the attempts
+                    
errorHandler(defaultErrorHandler().maximumRedeliveries(MAX_REDELIVERIES)
+                            .redeliveryDelay(redeliveryDelay));
+                }
                 from("aws2-sqs://dst?amazonSQSClient=#sqs&queueUrl=RAW(" + 
FakeSqs.QUEUE_URL + ")"
                      + "&extendMessageVisibility=true&visibilityTimeout=" + 
visibilityTimeout
                      + "&maxMessagesPerPoll=" + maxMessagesPerPoll + 
"&concurrentConsumers=" + concurrentConsumers
@@ -88,6 +115,10 @@ final class SqsWorld {
                         .routeId(ROUTE)
                         .process(exchange -> {
                             String id = 
exchange.getMessage().getHeader(Sqs2Constants.MESSAGE_ID, String.class);
+                            int attempt = 
exchange.getMessage().getHeader(Exchange.REDELIVERY_COUNTER, 0, Integer.class);
+                            if (attempt > 0) {
+                                redelivered(exchange.getExchangeId(), id, 
attempt);
+                            }
                             ledger.record("routed", "q", id);
                             // usually quick; sometimes longer than the 
visibility timeout, which the extender covers
                             long work = faults && dst.chance(0.15)
@@ -95,11 +126,17 @@ final class SqsWorld {
                                     : dst.between(1, 200);
                             // a blocking downstream call: the other consumers 
and the extender run meanwhile
                             dst.block(work, "process " + id);
-                            release(id);
                             if (faults && dst.chance(0.05)) {
+                                if (redelivery && attempt < MAX_REDELIVERIES) {
+                                    // the error handler retries it: the 
consumer still holds the message
+                                    failedAt.put(exchange.getExchangeId(), 
dst.now());
+                                } else {
+                                    release(id);
+                                }
                                 ledger.record("failed", "q", id);
                                 throw Dst.injected(new 
IllegalStateException("dst: processing failed for " + id));
                             }
+                            release(id);
                             ledger.record("completed", "q", id);
                         });
             }
@@ -133,6 +170,10 @@ final class SqsWorld {
                     Invariants.everyOfferedRoutedAtLeastOnce(ledger, "sent", 
"completed"),
                     Invariants.of("the queue is drained once the faults 
cleared",
                             () -> sqs.allDeleted() ? null : "not deleted: " + 
sqs.notDeleted()));
+            if (redelivery) {
+                dst.check(Invariants.of("a redelivery waits at least the 
redelivery delay",
+                        () -> redeliveredTooEarly.isEmpty() ? null : 
redeliveredTooEarly.toString()));
+            }
         } finally {
             dst.dropPending();
             context.stop();
@@ -171,6 +212,19 @@ final class SqsWorld {
         held.put(m.id, holders + 1);
     }
 
+    /**
+     * Camel's error handler tries a failed exchange again: the redelivery 
delay must have passed in virtual time. By
+     * exchange, as a message whose visibility change was lost can be in 
flight twice.
+     */
+    private void redelivered(String exchangeId, String id, int attempt) {
+        Long failed = failedAt.remove(exchangeId);
+        long waited = failed == null ? -1 : dst.now() - failed;
+        if (waited < redeliveryDelay) {
+            redeliveredTooEarly.add(id + " (attempt " + attempt + " after " + 
waited + " ms, redeliveryDelay="
+                                    + redeliveryDelay + ")");
+        }
+    }
+
     /** The route has finished with one delivery of the message. */
     private void release(String id) {
         held.computeIfPresent(id, (k, v) -> v > 1 ? v - 1 : null);
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 0d13aa8d264a..16bf1c96d140 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
@@ -16,13 +16,16 @@ with the same step trace.
 * Every thread pool created through the `ExecutorServiceManager` is backed by 
one event loop on the test thread
   (`DstThreadPoolFactory`). Tasks run one at a time; when several can run, the 
seed picks one.
 * Time is virtual: it jumps to the next due task. The time reads, sleeps and 
blocking waits of the code under test are
-  rewritten at test time (`@DstInstrument`), so "wait 30 seconds" costs 
nothing.
+  rewritten at test time (`@DstInstrument`), and the Camel core code that 
waits, times tasks or throttles reads the
+  virtual clock through its internal time source, so "wait 30 seconds" costs 
nothing.
 * A fake of the external system (a queue, a checkpoint store, a web API) 
injects faults chosen by the same seed.
 * Invariants are checked at the end of each run.
 * `@DstTest(seeds = 1000)` runs a test method once per seed. A failing seed is 
printed with its trace and is replayed
   exactly with `-Ddst.seed=4711` or `@DstSeed(4711)`.
 
-This is a test tool. Nothing of it runs in production, and it changes nothing 
in Camel.
+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.
 
 == A test
 
@@ -54,6 +57,8 @@ These worlds are in the Camel code base and are the examples 
to copy:
 |Component |Test |Regression test for
 
 |camel-aws2-sqs |`SqsVisibilityDstTest` (the visibility timeout extender) 
|CAMEL-25700
+|camel-aws2-sqs |`SqsRedeliveryDstTest` (the extender while Camel's error 
handler waits to redeliver, in virtual
+time) |-
 |camel-azure-eventhubs |`EventHubsCheckpointDstTest` (checkpointing by batch 
size and timeout) |CAMEL-25683
 |camel-pulsar |`PulsarConsumerDstTest` (subscribe failures, broker and route 
restarts while the route starts and
 consumes) |CAMEL-25869
@@ -78,13 +83,25 @@ virtual time. A wait that no task of the run can end fails 
the seed at once and
 |Timed waits on tasks |`Future.get(timeout)` on a task of the run and 
`awaitTermination` time out in virtual time.
 |Parallel streams |`parallelStream()` of a `java.util` collection is rewritten 
to `stream()`: the elements are handled
 in order by the calling task, not on the common fork-join pool.
+|Camel core |The code of Camel core that waits, times tasks or throttles reads 
the clock and sleeps through
+`org.apache.camel.util.TimeSource`, an internal class of camel-util. A run 
installs its virtual clock there (`Dst`
+does it when the run is created) and puts the previous provider back when the 
run is closed, so no rewriting is needed:
+the synchronous redelivery (`RedeliveryPolicy`, `RedeliveryErrorHandler`), 
`ForegroundTask`, `BackgroundTask.schedule`,
+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. Three waits of 
these classes are not virtual (see below):
+the permit queue of `TotalRequestsThrottler`, the `Semaphore` of 
`ConcurrentRequestsThrottler` and the half-open timer
+of `ThrottlingExceptionRoutePolicy`. The sleep of the route `delayer` option 
is not virtual either (see below).
 |===
 
-`@DstInstrument(alsoRewrite = ...)` applies the same rewriting to classes 
outside the code under test, for example
-`org.apache.camel.util.StopWatch`, `org.apache.camel.support.MonotonicClock`, 
`org.apache.camel.support.task`
-(`ForegroundTask`), `org.apache.camel.processor.errorhandler` (synchronous 
redelivery) or
-`org.apache.camel.impl.engine.DefaultShutdownStrategy` (its wait for in-flight 
exchanges). This is a stop-gap: Camel
-core reads `System` directly, and a time source in core that the harness can 
replace needs a dev@ discussion first.
+Event timestamps, the clock of an exchange and the direct clock reads of the 
debugger, the tracer, management and the
+developer consoles stay on `System`. The durations they measure with 
`StopWatch` or `MonotonicClock` follow `TimeSource`
+(with the default provider this is the same `System.nanoTime()`), and so does 
the created time of `MonotonicClock`: the
+message history times (`MessageHistory.getTime()`) are virtual in a DST run. 
`@DstInstrument(alsoRewrite = ...)` applies the
+rewriting of the code under test to classes outside it, for example 
`org.apache.camel.support.ResetableClock` (the
+clock of an exchange) or a library the component calls. It is not needed for 
the core classes listed above, except for
+the permit queue of `TotalRequestsThrottler` (see below).
 
 === Escapes and strict mode
 
@@ -93,15 +110,34 @@ Before the first seed, the harness scans the packages 
under test and writes ever
 cannot run the loop for (`Object.wait`, `LockSupport.park`, `Mono.block`), 
wall clock reads it does not rewrite.
 
 Strict mode is the default: when a seed reaches one of these at runtime, or a 
thread outside the harness submits a task
-to the loop, reads the virtual clock or sleeps, the seed fails with the escape 
as its cause. A clock read from such a
+to the loop, reads the virtual clock or sleeps, the seed fails with the escape 
as its cause. This includes Camel core:
+a `StopWatch` started on such a thread reads the virtual clock through 
`TimeSource`. A clock read from such a
 thread (a Reactor or Netty thread, for example) still gets the virtual time 
and its sleep still takes real time, so
 that thread mixes the two; this is what strict mode is there to make visible. 
`@DstInstrument(allowEscapes = "Foo.bar")` allows an escape
 that cannot change the result (say why in a comment); `@DstInstrument(strict = 
false)` or `-Ddst.strict=false` only
 records escapes in `target/dst/<Test>.summary.txt`.
 
-A wait in code that is not rewritten (a latch in Camel core, for example) 
blocks the loop thread for real. A watchdog
-interrupts the loop thread after 10 s of real time without progress 
(`-Ddst.watchdogMillis`) and the seed fails with the
-stack where it was blocked.
+A wait in code that is not rewritten blocks the loop thread for real. A 
watchdog interrupts the loop thread after 10 s
+of real time without progress (`-Ddst.watchdogMillis`) and the seed fails with 
the stack where it was blocked. In Camel
+core:
+
+* `BackgroundTask.run` waits on a latch until its scheduled attempts are done, 
so it blocks the loop thread.
+  `BackgroundTask.schedule` does not wait, and its attempts run in virtual 
time.
+* `TotalRequestsThrottler` waits for a permit on its `DelayQueue`. The delay 
of a permit follows the virtual clock,
+  which does not move while the loop thread waits, so a seed that has to wait 
for a permit always ends on the watchdog.
+  `asyncDelayed()` does not avoid it: a delayed exchange that finds its permit 
taken by another one waits on the same
+  queue. Add `org.apache.camel.processor.TotalRequestsThrottler` to 
`@DstInstrument(alsoRewrite)`: the permit wait is
+  then a virtual wait.
+* `ConcurrentRequestsThrottler` blocks on its `Semaphore` once 
`maxConcurrentRequests` exchanges are in flight. No task
+  of the run releases a permit while the loop thread waits, so the seed ends 
on the watchdog. `alsoRewrite` does not
+  help here (the call goes through a `Semaphore` subclass): keep 
`maxConcurrentRequests` above the number of exchanges
+  the world has in flight.
+* The route `delayer` option (`CamelInternalProcessor.DelayerAdvice`) calls 
`Thread.sleep` directly, not through
+  `TimeSource`: the delay takes real time on the loop thread and the virtual 
clock does not move. Use the `delay` EIP
+  in a world instead.
+* The half-open check of `ThrottlingExceptionRoutePolicy` runs on a 
`java.util.Timer` thread, outside the harness. The
+  timer fires after `halfOpenAfter` of real time (30 s by default, longer than 
the watchdog) and its clock read is an
+  escape, so a world does not see the circuit half-open in virtual time.
 
 === The one-stack model
 
@@ -404,12 +440,15 @@ seed of the in-JVM replay test of the Event Hubs world, 
default 1).
 * Code on its own threads or on a third-party scheduler is outside the 
harness; strict mode makes reaching it fail the
   seed.
 * One run per JVM at a time: the active run, and so the virtual clock, is a 
static field, and every rewritten clock read
-  in the JVM sees it. A world must therefore not go into a module that runs 
its tests in parallel
-  (`camel.surefire.parallel=true`, as camel-jms and camel-sql do): the other 
tests in that surefire fork would read the
-  virtual clock while a run is active. The harness enforces this: a second run 
while one is open fails, and a DST test
-  refuses to start when JUnit runs tests in parallel. A local world in such a 
module (camel-sjms, for example) can
-  still be run alone, with `-Dtest=<the world> 
-Dcamel.surefire.parallel=false`, but it cannot be committed there:
-  the module's own build would fail it.
+  in the JVM sees it, as does Camel core through `TimeSource` while the run is 
open. A world must therefore not go into
+  a module that runs its tests in parallel (`camel.surefire.parallel=true`, as 
camel-jms and camel-sql do): the other
+  tests in that surefire fork would read the virtual clock while a run is 
active. For the same reason, a
+  `CamelContext` left running in the same fork by an earlier non-DST test 
would read the virtual time too and could
+  fail a strict seed; stop it in that test. The harness enforces this: a second
+  run while one is open fails, and a DST test refuses to start when JUnit runs 
tests in parallel. A local world in
+  such a module (camel-sjms, for example) can still be run alone, with
+  `-Dtest=<the world> -Dcamel.surefire.parallel=false`, but it cannot be 
committed there: the module's own build would
+  fail it.
 * Locks do not exclude: every task runs on the test thread, so a 
`ReentrantLock` or `synchronized` block held by a
   blocked task is entered again by the tasks nested above it. A critical 
section is atomic only because the fake does
   not block inside it, and races between two code paths that each hold a lock 
cannot be explored.
@@ -427,7 +466,7 @@ seed of the in-JVM replay test of the Event Hubs world, 
default 1).
 strict mode
 |`DstThreadPoolFactory`, `DstExecutor`, `DstTask` |Camel's pools and 
schedulers backed by the loop
 |`DstInstrumentation`, `DstClock`, `DstWaits` |the byte code rewrite of the 
code under test: time reads, sleeps and
-blocking waits go to the loop
+blocking waits go to the loop; `DstClock` is also the `TimeSource` provider of 
Camel core during a run
 |`DstEscapes`, `DstWatchdog` |the escape scan, runtime escapes (strict mode), 
and the watchdog for waits in code that is
 not rewritten
 |`DstLedger`, `Invariant`, `Invariants` |what happened to each message, and 
reusable invariants
@@ -446,7 +485,7 @@ not rewritten
 |`dst.seed` |comma separated seeds to replay; their full traces are written |-
 |`dst.traceDir` |where the reports and traces go |`target/dst`
 |`dst.strict` |strict mode |`@DstInstrument(strict)`, true
-|`dst.alsoRewrite` |comma separated classes or packages to rewrite too 
(stop-gap) |-
+|`dst.alsoRewrite` |comma separated classes or packages outside the code under 
test to rewrite too |-
 |`dst.watchdogMillis` |real time without progress after which a waiting loop 
thread fails the seed |10000
 |===
 
diff --git 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/Dst.java
 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/Dst.java
index 1844447890cc..7df58495f69e 100644
--- 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/Dst.java
+++ 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/Dst.java
@@ -30,6 +30,8 @@ import java.util.Random;
 import java.util.concurrent.Callable;
 import java.util.function.BooleanSupplier;
 
+import org.apache.camel.util.TimeSource;
+
 /**
  * One deterministic simulation run: a single event loop that runs every task 
of every executor created through
  * {@link DstThreadPoolFactory}, a virtual clock, a seeded random source for 
the scheduling choices and the faults, and
@@ -58,6 +60,8 @@ public final class Dst {
     private static final int MAX_TRACE_LINES = 200_000;
 
     private static volatile Dst active;
+    /** The {@link TimeSource} provider before the active run installed the 
virtual clock, put back when it closes. */
+    private static TimeSource.Provider previousTimeSource;
 
     private final long seed;
     private final Random random;
@@ -114,8 +118,11 @@ public final class Dst {
 
     /**
      * Makes the run the active one. The virtual clock is JVM-wide (every 
rewritten clock read in the JVM sees the
-     * active run), so only one run may be open at a time: a second run while 
another one is not closed (tests that run
-     * in parallel, or a run that was not closed) fails fast instead of 
sharing the clock.
+     * active run, and so does Camel core through {@link TimeSource}), so only 
one run may be open at a time: a second
+     * run while another one is not closed (tests that run in parallel, or a 
run that was not closed) fails fast instead
+     * of sharing the clock.
+     * <p>
+     * The virtual clock is installed as the {@link TimeSource} provider of 
Camel core until the run is closed.
      */
     private static synchronized void activate(Dst dst) {
         Dst previous = active;
@@ -123,6 +130,9 @@ public final class Dst {
             throw new IllegalStateException(
                     "DST: another run (seed " + previous.seed() + ") is still 
active; one run per JVM at a time");
         }
+        if (previous == null) {
+            previousTimeSource = TimeSource.install(DstClock.TIME_SOURCE);
+        }
         active = dst;
     }
 
@@ -640,7 +650,10 @@ public final class Dst {
         pending.clear();
     }
 
-    /** Drops every pending task and detaches the virtual clock. */
+    /**
+     * Drops every pending task and detaches the virtual clock: Camel core 
gets back the {@link TimeSource} provider it
+     * had before the run (the system clock, unless a test installed another 
one).
+     */
     public void close() {
         synchronized (this) {
             closed = true;
@@ -649,6 +662,8 @@ public final class Dst {
         synchronized (Dst.class) {
             if (active == this) {
                 active = null;
+                TimeSource.install(previousTimeSource);
+                previousTimeSource = null;
             }
         }
     }
diff --git 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstClock.java
 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstClock.java
index 0018badfbffa..bdb0bb6481c6 100644
--- 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstClock.java
+++ 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstClock.java
@@ -29,10 +29,17 @@ import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.camel.util.TimeSource;
+
 /**
  * The target of the rewritten time reads of the code under test (see {@link 
DstInstrumentation}): the virtual clock of
  * the active run, else the real clock.
  * <p>
+ * The same clock is the {@link TimeSource} provider of Camel core while a run 
is open ({@code TIME_SOURCE}, installed
+ * by {@link Dst}): the core code that waits, times tasks or throttles (the 
redelivery delay, the wait loop of the
+ * graceful shutdown, {@code ForegroundTask}, {@code StopWatch}, the clocks of 
the throttlers) reads the virtual time
+ * and sleeps virtual sleeps without any rewriting.
+ * <p>
  * Rewritten: {@code System.currentTimeMillis()}, {@code System.nanoTime()}, 
{@code new Date()}, {@code Instant.now()},
  * {@code Clock.systemUTC()}, {@code Clock.systemDefaultZone()}, {@code 
Clock.system(ZoneId)} and {@code now()} /
  * {@code now(ZoneId)} of {@code LocalDateTime}, {@code ZonedDateTime}, {@code 
OffsetDateTime}, {@code LocalDate},
@@ -50,6 +57,37 @@ public final class DstClock {
 
     private static final long NANO_OFFSET = 1_000_000_000_000L;
 
+    /**
+     * The {@link TimeSource} provider of Camel core while a run is open: the 
same as the rewritten calls, with the
+     * caller of {@link TimeSource#sleep(long)} as the call site of a sleep in 
the trace.
+     */
+    static final TimeSource.Provider TIME_SOURCE = new TimeSource.Provider() {
+        @Override
+        public long currentTimeMillis() {
+            return DstClock.currentTimeMillis();
+        }
+
+        @Override
+        public long nanoTime() {
+            return DstClock.nanoTime();
+        }
+
+        @Override
+        public Instant instant() {
+            return instantNow();
+        }
+
+        @Override
+        public void sleep(long millis) throws InterruptedException {
+            DstClock.sleep(millis, caller());
+        }
+
+        @Override
+        public String toString() {
+            return "DstClock";
+        }
+    };
+
     private DstClock() {
     }
 
@@ -78,6 +116,21 @@ public final class DstClock {
         return dst == null ? Instant.now() : 
Instant.ofEpochMilli(dst.currentTimeMillis());
     }
 
+    /** The call site of a sleep through {@link TimeSource}, as the rewriting 
writes it: {@code Class.method:line}. */
+    private static String caller() {
+        return StackWalker.getInstance().walk(frames -> frames
+                .filter(f -> !isTimeSourceFrame(f.getClassName()))
+                .findFirst()
+                .map(f -> 
f.getClassName().substring(f.getClassName().lastIndexOf('.') + 1) + "." + 
f.getMethodName()
+                          + ":" + f.getLineNumber())
+                .orElse("TimeSource.sleep"));
+    }
+
+    private static boolean isTimeSourceFrame(String className) {
+        return className.equals(TimeSource.class.getName()) || 
className.startsWith(TimeSource.class.getName() + "$")
+                || className.equals(DstClock.class.getName()) || 
className.startsWith(DstClock.class.getName() + "$");
+    }
+
     // 
------------------------------------------------------------------------------------------------------------
     // sleeps: Thread.sleep(long), Thread.sleep(long, int), 
Thread.sleep(Duration) and TimeUnit.sleep(long), with the
     // call site added by the rewriting
diff --git 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstExtension.java
 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstExtension.java
index d2a90dd11619..48786ea04647 100644
--- 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstExtension.java
+++ 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstExtension.java
@@ -113,7 +113,7 @@ public class DstExtension implements 
TestTemplateInvocationContextProvider, Befo
         DstInstrumentation.alsoRewrite(System.getProperty("dst.alsoRewrite", 
"").split(","));
         StringBuilder sb = new StringBuilder();
         if (!DstInstrumentation.alsoRewritten().isEmpty()) {
-            sb.append("DST time reads also rewritten outside the code under 
test (stop-gap): ")
+            sb.append("DST time reads also rewritten outside the code under 
test: ")
                     .append(DstInstrumentation.alsoRewritten()).append('\n');
         }
         for (String p : instrument.value()) {
@@ -151,6 +151,14 @@ public class DstExtension implements 
TestTemplateInvocationContextProvider, Befo
         DstEscapes.loaded().forEach(e -> sb.append("  
").append(e).append('\n'));
         System.out.print(sb);
         
write(traceDir().resolve(context.getRequiredTestClass().getSimpleName() + 
".summary.txt"), sb.toString());
+        Dst left = Dst.active();
+        if (left != null) {
+            // a run that was not closed keeps the virtual clock installed in 
TimeSource for the later tests of the JVM
+            left.close();
+            throw new IllegalStateException(
+                    "DST: the run of seed " + left.seed() + " was still open 
after "
+                                            + 
context.getRequiredTestClass().getSimpleName() + "; it is closed now");
+        }
     }
 
     @Override
@@ -313,16 +321,21 @@ public class DstExtension implements 
TestTemplateInvocationContextProvider, Befo
             stats.runs++;
             stats.nanos += System.nanoTime() - start;
             if (dst != null) {
-                if (replay) {
-                    if (written == null) {
-                        written = writeTrace(failed ? "FAILED" : "PASSED");
+                try {
+                    if (replay) {
+                        if (written == null) {
+                            written = writeTrace(failed ? "FAILED" : "PASSED");
+                        }
+                        String line = String.format("seed %d %s trace %s (%d 
lines) -> %s", seed,
+                                failed ? "FAILED" : "PASSED", dst.traceHash(), 
dst.traceLines().size(), written);
+                        stats.replayHashes.add(line);
+                        System.out.println("DST replay " + line);
                     }
-                    String line = String.format("seed %d %s trace %s (%d 
lines) -> %s", seed, failed ? "FAILED" : "PASSED",
-                            dst.traceHash(), dst.traceLines().size(), written);
-                    stats.replayHashes.add(line);
-                    System.out.println("DST replay " + line);
+                } finally {
+                    // also when the trace cannot be written: an open run 
keeps the virtual clock installed in
+                    // TimeSource for the rest of the JVM
+                    dst.close();
                 }
-                dst.close();
             }
         }
 
diff --git 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstInstrument.java
 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstInstrument.java
index 4b90a65cab53..9f9c9c558456 100644
--- 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstInstrument.java
+++ 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstInstrument.java
@@ -37,14 +37,19 @@ public @interface DstInstrument {
     String[] value();
 
     /**
-     * Classes or packages outside the code under test whose time reads are 
rewritten too, without an escape report, for
-     * example {@code org.apache.camel.support.MonotonicClock} or {@code 
org.apache.camel.util.StopWatch} of Camel core.
-     * An entry whose last segment starts with an upper case letter is a class 
(with its nested classes), else a
-     * package.
+     * Classes or packages outside the code under test whose time reads, 
sleeps and blocking waits are rewritten too,
+     * without an escape report, for example {@code 
org.apache.camel.support.ResetableClock} of Camel core or a library
+     * the component calls. An entry whose last segment starts with an upper 
case letter is a class (with its nested
+     * classes), else a package.
      * <p>
-     * This is a stop-gap: Camel core reads {@code System} directly, and the 
rewriting of core classes stays for the
-     * rest of the test JVM (outside a run the rewritten calls read the real 
clock). The system property
-     * {@code dst.alsoRewrite} (comma separated) adds entries without changing 
the test.
+     * Not needed for the Camel core code that waits, times tasks or throttles 
({@code StopWatch},
+     * {@code MonotonicClock}, the synchronous redelivery, {@code 
ForegroundTask}, the wait loop of the graceful
+     * shutdown, the clocks of the throttlers): it reads the clock and sleeps 
through
+     * {@code org.apache.camel.util.TimeSource}, whose provider is the virtual 
clock while a run is open. The permit
+     * wait of {@code org.apache.camel.processor.TotalRequestsThrottler} is a 
{@code DelayQueue.take()}, which is not a
+     * clock read: list that class when a world uses it. The rewriting stays 
for the rest of the test JVM (outside a run
+     * the rewritten calls read the real clock). The system property {@code 
dst.alsoRewrite} (comma separated) adds
+     * entries without changing the test.
      */
     String[] alsoRewrite() default {};
 
diff --git 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstInstrumentation.java
 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstInstrumentation.java
index df260e69d3ef..e0d172978882 100644
--- 
a/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstInstrumentation.java
+++ 
b/components/camel-test/camel-test-dst/src/main/java/org/apache/camel/test/dst/DstInstrumentation.java
@@ -69,8 +69,10 @@ import net.bytebuddy.jar.asm.Opcodes;
  * seed.</li>
  * </ul>
  * {@link #alsoRewrite(String...)} applies the same rewriting, without the 
report, to classes outside the code under
- * test, for example {@code org.apache.camel.support.MonotonicClock} and 
{@code org.apache.camel.util.StopWatch} of
- * Camel core. That is a stop-gap until Camel core has a time source the 
harness can replace.
+ * test, for example {@code org.apache.camel.support.ResetableClock} of Camel 
core. The core code that waits, times
+ * tasks or throttles reads the clock and sleeps through {@code 
org.apache.camel.util.TimeSource}, whose provider is the
+ * virtual clock while a run is open (see {@link Dst}); only its blocking 
waits, such as the permit queue of
+ * {@code org.apache.camel.processor.TotalRequestsThrottler}, still need the 
rewriting.
  * <p>
  * Uses a runtime-attached agent (as Mockito's inline mock maker does) and the 
ASM copy shipped in Byte Buddy.
  */
@@ -139,11 +141,11 @@ public final class DstInstrumentation {
     }
 
     /**
-     * Rewrites the time reads of these classes or packages too, without 
reporting their escape points: a stop-gap for
-     * code outside the code under test that reads the clock, such as {@code 
org.apache.camel.support.MonotonicClock} or
-     * {@code org.apache.camel.util.StopWatch}. An entry whose last segment 
starts with an upper case letter is a class
-     * (with its nested classes), else a package (with its sub packages). The 
rewriting stays for the rest of the JVM;
-     * outside a run the rewritten calls read the real clock.
+     * Rewrites the time reads of these classes or packages too, without 
reporting their escape points: for code outside
+     * the code under test that reads the clock and does not use {@code 
org.apache.camel.util.TimeSource}, such as
+     * {@code org.apache.camel.support.ResetableClock}. An entry whose last 
segment starts with an upper case letter is
+     * a class (with its nested classes), else a package (with its sub 
packages). The rewriting stays for the rest of
+     * the JVM; outside a run the rewritten calls read the real clock.
      */
     public static synchronized void alsoRewrite(String... classesOrPackages) {
         List<String> added = new ArrayList<>();
diff --git 
a/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstAlsoRewriteTest.java
 
b/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstAlsoRewriteTest.java
index c76b8d27a81f..0ab9ac52a789 100644
--- 
a/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstAlsoRewriteTest.java
+++ 
b/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstAlsoRewriteTest.java
@@ -16,49 +16,45 @@
  */
 package org.apache.camel.test.dst;
 
-import org.apache.camel.support.MonotonicClock;
-import org.apache.camel.util.StopWatch;
+import org.apache.camel.support.ResetableClock;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
- * The stop-gap for time read outside the code under test: core classes listed 
in {@link DstInstrument#alsoRewrite()}
- * read virtual time too.
+ * Time read outside the code under test and outside {@code TimeSource}: core 
classes listed in
+ * {@link DstInstrument#alsoRewrite()} read virtual time too. {@code 
ResetableClock} (the clock of an exchange) reads
+ * {@code System} directly, as event timestamps do.
  */
-@DstInstrument(value = "org.apache.camel.test.dst.sample",
-               alsoRewrite = { "org.apache.camel.util.StopWatch", 
"org.apache.camel.support.MonotonicClock" })
+@DstInstrument(value = "org.apache.camel.test.dst.sample", alsoRewrite = 
"org.apache.camel.support.ResetableClock")
 class DstAlsoRewriteTest {
 
     @Test
     void listedCoreClocksReadVirtualTime() {
-        
assertTrue(DstInstrumentation.alsoRewritten().contains("org.apache.camel.util.StopWatch"),
+        
assertTrue(DstInstrumentation.alsoRewritten().contains("org.apache.camel.support.ResetableClock"),
                 DstInstrumentation.alsoRewritten().toString());
         Dst dst = new Dst(1);
         try {
-            StopWatch[] watch = new StopWatch[1];
-            MonotonicClock[] clock = new MonotonicClock[1];
+            ResetableClock[] clock = new ResetableClock[1];
             long[] taken = new long[2];
             DstExecutor ex = dst.executor("ex", 1);
-            ex.schedule("start", 1_000, () -> {
-                watch[0] = new StopWatch();
-                clock[0] = new MonotonicClock();
-            });
+            ex.schedule("start", 1_000, () -> clock[0] = new ResetableClock());
             ex.schedule("read", 3_601_000, () -> {
-                taken[0] = watch[0].taken();
-                taken[1] = clock[0].elapsed();
+                taken[0] = clock[0].elapsed();
+                clock[0].reset();
             });
+            ex.schedule("read again", 3_601_500, () -> taken[1] = 
clock[0].elapsed());
             long start = System.nanoTime();
             dst.runUntilQuiet();
             assertEquals(3_600_000L, taken[0]);
-            assertEquals(3_600_000L, taken[1]);
-            assertEquals(Dst.EPOCH_MILLIS + 1_000, clock[0].getCreated());
+            assertEquals(500L, taken[1]);
+            assertEquals(Dst.EPOCH_MILLIS + 3_601_000, clock[0].getCreated());
             assertTrue(System.nanoTime() - start < 5_000_000_000L, "an hour of 
virtual time takes no real time");
         } finally {
             dst.close();
         }
-        // outside a run the rewritten classes read the real clock
-        assertTrue(new StopWatch().taken() < 60_000);
+        // outside a run the rewritten class reads the real clock
+        assertTrue(new ResetableClock().elapsed() < 60_000);
     }
 }
diff --git 
a/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstCoreWaitsTest.java
 
b/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstCoreWaitsTest.java
index bdea5f9bc63e..71eccb35cfa0 100644
--- 
a/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstCoreWaitsTest.java
+++ 
b/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstCoreWaitsTest.java
@@ -36,16 +36,13 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
- * The waits of Camel core that sleep on the calling thread, made virtual 
through the stop-gap list of classes to
- * rewrite: the synchronous redelivery (RedeliveryPolicy.sleep and the sleep 
loop of RedeliveryErrorHandler, which
- * measures with StopWatch), ForegroundTask, and the wait loop of 
DefaultShutdownStrategy.
+ * The waits of Camel core that sleep on the calling thread run in virtual 
time without any rewriting: core sleeps and
+ * reads the clock through {@code TimeSource}, and the run installs its 
virtual clock there. Covered: the synchronous
+ * redelivery (RedeliveryPolicy.sleep and the sleep loop of 
RedeliveryErrorHandler, which measures with StopWatch),
+ * ForegroundTask with an iteration and a time budget, the synchronous 
delayer, and the wait loop of
+ * DefaultShutdownStrategy.
  */
-@DstInstrument(value = "org.apache.camel.test.dst.sample",
-               alsoRewrite = {
-                       "org.apache.camel.processor.errorhandler",
-                       "org.apache.camel.support.task",
-                       "org.apache.camel.impl.engine.DefaultShutdownStrategy",
-                       "org.apache.camel.util.StopWatch" })
+@DstInstrument("org.apache.camel.test.dst.sample")
 class DstCoreWaitsTest {
 
     private static final long REAL_LIMIT = 5_000_000_000L;
@@ -168,6 +165,62 @@ class DstCoreWaitsTest {
         }
     }
 
+    @Test
+    void foregroundTaskGivesUpWhenItsTimeBudgetIsSpentInVirtualTime() {
+        Dst dst = new Dst(1);
+        try {
+            boolean[] completed = { true };
+            long[] finished = new long[1];
+            ForegroundTask[] task = new ForegroundTask[1];
+            dst.executor("waiter", 1).execute(() -> {
+                // the time budget (TimeBoundedBudget) ends the task, long 
before the iteration budget would
+                task[0] = Tasks.foregroundTask().withName("never ready")
+                        
.withBudget(Budgets.iterationTimeBudget().withInterval(Duration.ofSeconds(1))
+                                
.withMaxDuration(Duration.ofSeconds(30)).withMaxIterations(1_000).build())
+                        .build();
+                completed[0] = task[0].run(null, () -> false);
+                finished[0] = dst.now();
+            });
+            long start = System.nanoTime();
+            dst.runUntilQuiet();
+            assertFalse(completed[0]);
+            assertEquals(30_000, finished[0]);
+            assertEquals(Duration.ofSeconds(30), task[0].elapsed());
+            assertEquals(30, task[0].iteration());
+            assertTrue(System.nanoTime() - start < REAL_LIMIT);
+        } finally {
+            dst.close();
+        }
+    }
+
+    @Test
+    void synchronousDelayWaitsInVirtualTime() throws Exception {
+        Dst dst = new Dst(1);
+        DefaultCamelContext context = DstCamel.newContext(dst);
+        try {
+            long[] reached = { -1 };
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    
from("direct:in").routeId("r").delay(4_000).syncDelayed().process(e -> 
reached[0] = dst.now());
+                }
+            });
+            context.start();
+            Exchange[] done = new Exchange[1];
+            dst.executor("client", 1).schedule("send", 1_000, () -> 
send(context, "r", done));
+            long start = System.nanoTime();
+            dst.runUntilQuiet();
+            assertEquals(5_000, reached[0]);
+            assertNull(done[0].getException());
+            assertTraced(dst, "sleep at DelayProcessorSupport.sleep");
+            assertTrue(System.nanoTime() - start < REAL_LIMIT);
+        } finally {
+            dst.dropPending();
+            context.stop();
+            dst.close();
+        }
+    }
+
     private static void assertTraced(Dst dst, String text) {
         assertTrue(dst.traceLines().stream().anyMatch(l -> l.contains(text)), 
"no trace line contains: " + text);
     }
diff --git 
a/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstTimeSourceTest.java
 
b/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstTimeSourceTest.java
new file mode 100644
index 000000000000..9f39d0e4ce81
--- /dev/null
+++ 
b/components/camel-test/camel-test-dst/src/test/java/org/apache/camel/test/dst/DstTimeSourceTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.test.dst;
+
+import java.time.Instant;
+import java.util.List;
+
+import org.apache.camel.support.MonotonicClock;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.TimeSource;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A run installs its virtual clock as the {@code TimeSource} provider of 
Camel core and puts the previous provider back
+ * when it closes: the core clocks read virtual time without any rewriting.
+ */
+@DstInstrument("org.apache.camel.test.dst.sample")
+class DstTimeSourceTest {
+
+    @Test
+    void aRunInstallsTheVirtualClockUntilItIsClosed() {
+        assertTrue(TimeSource.isDefault());
+        Dst dst = new Dst(1);
+        try {
+            assertFalse(TimeSource.isDefault());
+            assertEquals(dst.currentTimeMillis(), 
TimeSource.currentTimeMillis());
+            assertEquals(Instant.ofEpochMilli(dst.currentTimeMillis()), 
TimeSource.instant());
+            long nanos = TimeSource.nanoTime();
+            dst.runFor(1_500);
+            assertEquals(1_500_000_000L, TimeSource.nanoTime() - nanos);
+        } finally {
+            dst.close();
+        }
+        assertTrue(TimeSource.isDefault(), "the system clock is back after the 
run");
+        long real = System.currentTimeMillis();
+        assertTrue(Math.abs(TimeSource.currentTimeMillis() - real) < 60_000);
+    }
+
+    @Test
+    void coreClocksReadVirtualTimeWithoutRewriting() {
+        
assertFalse(DstInstrumentation.alsoRewritten().contains("org.apache.camel.util.StopWatch"));
+        Dst dst = new Dst(1);
+        try {
+            StopWatch[] watch = new StopWatch[1];
+            MonotonicClock[] clock = new MonotonicClock[1];
+            long[] taken = new long[2];
+            DstExecutor ex = dst.executor("ex", 1);
+            ex.schedule("start", 1_000, () -> {
+                watch[0] = new StopWatch();
+                clock[0] = new MonotonicClock();
+            });
+            ex.schedule("read", 3_601_000, () -> {
+                taken[0] = watch[0].taken();
+                taken[1] = clock[0].elapsed();
+            });
+            long start = System.nanoTime();
+            dst.runUntilQuiet();
+            assertEquals(3_600_000L, taken[0]);
+            assertEquals(3_600_000L, taken[1]);
+            assertEquals(Dst.EPOCH_MILLIS + 1_000, clock[0].getCreated());
+            assertTrue(System.nanoTime() - start < 5_000_000_000L, "an hour of 
virtual time takes no real time");
+        } finally {
+            dst.close();
+        }
+        // outside a run the core clocks read the real clock
+        assertTrue(new StopWatch().taken() < 60_000);
+    }
+
+    @Test
+    void aSleepThroughTheTimeSourceIsAVirtualWaitWithItsCallSite() {
+        Dst dst = new Dst(1);
+        try {
+            long[] woke = new long[1];
+            dst.executor("sleeper", 1).execute(() -> {
+                try {
+                    TimeSource.sleep(7_000);
+                } catch (InterruptedException e) {
+                    throw new IllegalStateException(e);
+                }
+                woke[0] = dst.now();
+            });
+            dst.executor("other", 1).schedule("meanwhile", 3_000, () -> 
dst.trace("other task at " + dst.now()));
+            long start = System.nanoTime();
+            dst.runUntilQuiet();
+            assertEquals(7_000, woke[0]);
+            assertTrue(dst.traceLines().stream()
+                    .anyMatch(l -> l.contains("block 7000ms sleep at 
DstTimeSourceTest.lambda$")),
+                    String.join("\n", dst.traceLines()));
+            assertTrue(dst.traceLines().stream().anyMatch(l -> 
l.contains("other task at 3000")));
+            assertTrue(System.nanoTime() - start < 5_000_000_000L);
+        } finally {
+            dst.close();
+        }
+    }
+
+    @Test
+    void aSleepThroughTheTimeSourceKeepsTheSemanticsOfThreadSleep() {
+        Dst dst = new Dst(1);
+        try {
+            assertThrows(IllegalArgumentException.class, () -> 
TimeSource.sleep(-1));
+            Thread.currentThread().interrupt();
+            assertThrows(InterruptedException.class, () -> 
TimeSource.sleep(1_000));
+            assertFalse(Thread.currentThread().isInterrupted(), "the 
interrupted status is cleared");
+            assertEquals(0, dst.now(), "no virtual time passed");
+        } finally {
+            Thread.interrupted();
+            dst.close();
+        }
+    }
+
+    @Test
+    void aCoreClockReadOffTheLoopIsAnEscape() throws Exception {
+        Dst dst = new Dst(1);
+        try {
+            dst.strict(true, List.of());
+            Thread outside = new Thread(StopWatch::new, "outside-watch");
+            outside.start();
+            outside.join();
+            assertTrue(dst.escape().contains("virtual clock read from a thread 
outside the harness: outside-watch"),
+                    dst.escape());
+            assertThrows(DstViolation.class, dst::runUntilQuiet);
+        } finally {
+            dst.close();
+        }
+    }
+
+    @Test
+    void theProviderInstalledBeforeTheRunIsPutBack() {
+        TimeSource.Provider fixed = new TimeSource.Provider() {
+            @Override
+            public long currentTimeMillis() {
+                return 42;
+            }
+
+            @Override
+            public long nanoTime() {
+                return 42;
+            }
+
+            @Override
+            public void sleep(long millis) {
+                // no sleep
+            }
+        };
+        TimeSource.Provider previous = TimeSource.install(fixed);
+        try {
+            Dst dst = new Dst(1);
+            try {
+                assertEquals(dst.currentTimeMillis(), 
TimeSource.currentTimeMillis());
+            } finally {
+                dst.close();
+            }
+            assertEquals(42, TimeSource.currentTimeMillis());
+        } finally {
+            assertSame(fixed, TimeSource.install(previous));
+        }
+        assertTrue(TimeSource.isDefault());
+    }
+}
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java
index 0c7b058cbfc1..dc45a9770d47 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java
@@ -56,6 +56,7 @@ import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.support.service.ServiceSupport;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.TimeSource;
 import org.apache.camel.util.TimeUtils;
 import org.apache.camel.util.URISupport;
 import org.slf4j.Logger;
@@ -704,7 +705,7 @@ public class DefaultShutdownStrategy extends ServiceSupport 
implements ShutdownS
                         // log verbose if DEBUG logging is enabled
                         logInflightExchanges(context, routes, 
logInflightExchangesOnTimeout);
 
-                        Thread.sleep(loopDelaySeconds * 1000);
+                        TimeSource.sleep(loopDelaySeconds * 1000);
                     } catch (InterruptedException e) {
                         Thread.currentThread().interrupt();
                         if (abortAfterTimeout) {
diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/ConcurrentRequestsThrottler.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/ConcurrentRequestsThrottler.java
index 2a5f9a0526c5..82e7033650ba 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/ConcurrentRequestsThrottler.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/ConcurrentRequestsThrottler.java
@@ -36,6 +36,7 @@ import org.apache.camel.RouteStoppingException;
 import org.apache.camel.RuntimeExchangeException;
 import org.apache.camel.spi.Synchronization;
 import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.TimeSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -133,7 +134,7 @@ public class ConcurrentRequestsThrottler extends 
AbstractThrottler {
             // permit acquired
             if (state == State.ASYNC) {
                 if (LOG.isTraceEnabled()) {
-                    long queuedTime = Duration.ofNanos(System.nanoTime() - 
queuedStart).toMillis();
+                    long queuedTime = Duration.ofNanos(TimeSource.nanoTime() - 
queuedStart).toMillis();
                     LOG.trace("Queued for {}ms, No throttling applied 
(throttle cleared while queued), for exchangeId: {}",
                             queuedTime, exchange.getExchangeId());
                 }
@@ -154,15 +155,15 @@ public class ConcurrentRequestsThrottler extends 
AbstractThrottler {
         long start = 0;
         long elapsed = 0;
         if (LOG.isTraceEnabled()) {
-            start = System.nanoTime();
+            start = TimeSource.nanoTime();
         }
         throttlingState.acquire(exchange);
         if (LOG.isTraceEnabled()) {
-            elapsed = System.nanoTime() - start;
+            elapsed = Duration.ofNanos(TimeSource.nanoTime() - 
start).toMillis();
         }
         if (state == State.ASYNC) {
             if (LOG.isTraceEnabled()) {
-                long queuedTime = start - queuedStart;
+                long queuedTime = Duration.ofNanos(start - 
queuedStart).toMillis();
                 if (LOG.isTraceEnabled()) {
                     LOG.trace("Queued for {}ms, Throttled for {}ms, 
exchangeId: {}", queuedTime, elapsed,
                             exchange.getExchangeId());
@@ -184,7 +185,7 @@ public class ConcurrentRequestsThrottler extends 
AbstractThrottler {
             final Exchange exchange, final AsyncCallback callback, 
ThrottlingState throttlingState) {
         try {
             if (LOG.isTraceEnabled()) {
-                exchange.setProperty(PROPERTY_EXCHANGE_QUEUED_TIME, 
System.nanoTime());
+                exchange.setProperty(PROPERTY_EXCHANGE_QUEUED_TIME, 
TimeSource.nanoTime());
             }
             exchange.setProperty(PROPERTY_EXCHANGE_STATE, State.ASYNC);
             asyncExecutor.submit(() -> process(exchange, callback));
diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/DelayProcessorSupport.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/DelayProcessorSupport.java
index 7193c9ab1a72..41163d794b60 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/DelayProcessorSupport.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/DelayProcessorSupport.java
@@ -27,6 +27,7 @@ import org.apache.camel.Exchange;
 import org.apache.camel.Processor;
 import org.apache.camel.RouteStoppingException;
 import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.TimeSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -251,7 +252,7 @@ public abstract class DelayProcessorSupport extends 
BaseDelegateProcessorSupport
     }
 
     protected long currentSystemTime() {
-        return System.currentTimeMillis();
+        return TimeSource.currentTimeMillis();
     }
 
     private void sleep(long delay) throws InterruptedException {
@@ -259,7 +260,7 @@ public abstract class DelayProcessorSupport extends 
BaseDelegateProcessorSupport
             return;
         }
         LOG.trace("Sleeping for: {} millis", delay);
-        Thread.sleep(delay);
+        TimeSource.sleep(delay);
     }
 
     @Override
diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java
index 243129e08ac9..c96b75160450 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java
@@ -76,6 +76,7 @@ import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.util.CastUtils;
 import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.TimeSource;
 import org.apache.camel.util.concurrent.AsyncCompletionService;
 import org.apache.camel.util.concurrent.Rejectable;
 import org.slf4j.Logger;
@@ -880,7 +881,7 @@ public class MulticastProcessor extends BaseProcessorSupport
         } else {
             executor.execute(() -> {
                 try {
-                    Thread.sleep(delay);
+                    TimeSource.sleep(delay);
                 } catch (InterruptedException e) {
                     Thread.currentThread().interrupt();
                 }
diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/SamplingThrottler.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/SamplingThrottler.java
index c46d7ee12464..3e5a3b0d9b12 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/SamplingThrottler.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/SamplingThrottler.java
@@ -28,6 +28,7 @@ import org.apache.camel.Traceable;
 import org.apache.camel.spi.IdAware;
 import org.apache.camel.spi.RouteIdAware;
 import org.apache.camel.spi.StepIdAware;
+import org.apache.camel.util.TimeSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -145,8 +146,8 @@ public class SamplingThrottler extends BaseProcessorSupport 
implements Traceable
                     doSend = true;
                 }
             } else {
-                long now = Duration.ofNanos(System.nanoTime()).toMillis();
-                // System.nanoTime() has an arbitrary origin, so the first 
exchange cannot be compared with a time
+                long now = Duration.ofNanos(TimeSource.nanoTime()).toMillis();
+                // nanoTime() has an arbitrary origin, so the first exchange 
cannot be compared with a time
                 if (!sampledOnce || now >= timeOfLastExchange + 
periodInMillis) {
                     sampledOnce = true;
                     doSend = true;
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 06363fa377cc..f949915439ad 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
@@ -35,6 +35,7 @@ import org.apache.camel.Expression;
 import org.apache.camel.RouteStoppingException;
 import org.apache.camel.RuntimeExchangeException;
 import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.TimeSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -129,7 +130,7 @@ public class TotalRequestsThrottler extends 
AbstractThrottler {
                     long start = 0;
                     long elapsed = 0;
                     if (LOG.isTraceEnabled()) {
-                        start = System.currentTimeMillis();
+                        start = TimeSource.currentTimeMillis();
                     }
                     permit = throttlingState.take();
                     while (throttlingState.isRemoved()) {
@@ -138,7 +139,7 @@ public class TotalRequestsThrottler extends 
AbstractThrottler {
                         permit = throttlingState.take();
                     }
                     if (LOG.isTraceEnabled()) {
-                        elapsed = System.currentTimeMillis() - start;
+                        elapsed = TimeSource.currentTimeMillis() - start;
                     }
                     throttlingState.enqueue(permit, exchange);
 
@@ -161,7 +162,7 @@ public class TotalRequestsThrottler extends 
AbstractThrottler {
 
                 if (state == State.ASYNC) {
                     if (LOG.isTraceEnabled()) {
-                        long queuedTime = System.currentTimeMillis() - 
queuedStart;
+                        long queuedTime = TimeSource.currentTimeMillis() - 
queuedStart;
                         LOG.trace("Queued for {}ms, No throttling applied 
(throttle cleared while queued), for exchangeId: {}",
                                 queuedTime, exchange.getExchangeId());
                     }
@@ -192,7 +193,7 @@ public class TotalRequestsThrottler extends 
AbstractThrottler {
             final Exchange exchange, final AsyncCallback callback, 
ThrottlingState throttlingState) {
         try {
             if (LOG.isTraceEnabled()) {
-                exchange.setProperty(PROPERTY_EXCHANGE_QUEUED_TIMESTAMP, 
System.nanoTime());
+                exchange.setProperty(PROPERTY_EXCHANGE_QUEUED_TIMESTAMP, 
TimeSource.currentTimeMillis());
             }
             exchange.setProperty(PROPERTY_EXCHANGE_STATE, State.ASYNC);
             ThrottlePermit next = throttlingState.peek();
@@ -413,12 +414,12 @@ public class TotalRequestsThrottler extends 
AbstractThrottler {
         }
 
         public void setDelayMs(final long delayMs) {
-            this.scheduledTime = System.currentTimeMillis() + delayMs;
+            this.scheduledTime = TimeSource.currentTimeMillis() + delayMs;
         }
 
         @Override
         public long getDelay(final TimeUnit unit) {
-            return unit.convert(scheduledTime - System.currentTimeMillis(), 
TimeUnit.MILLISECONDS);
+            return unit.convert(scheduledTime - 
TimeSource.currentTimeMillis(), TimeUnit.MILLISECONDS);
         }
 
         @Override
diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/OptimisticLockRetryPolicy.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/OptimisticLockRetryPolicy.java
index 24a0032c9196..aee8b1fd6c9f 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/OptimisticLockRetryPolicy.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/OptimisticLockRetryPolicy.java
@@ -18,6 +18,8 @@ package org.apache.camel.processor.aggregate;
 
 import java.util.concurrent.ThreadLocalRandom;
 
+import org.apache.camel.util.TimeSource;
+
 /**
  * Class to control how failed optimistic locks are tried. This policy 
supports random and exponential back-off delays.
  * <p/>
@@ -60,7 +62,7 @@ public class OptimisticLockRetryPolicy {
     public void doDelay(final int retryCounter) throws InterruptedException {
         long sleepFor = getDelay(retryCounter);
         if (sleepFor > 0) {
-            Thread.sleep(sleepFor);
+            TimeSource.sleep(sleepFor);
         }
     }
 
diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
index f0281e9857b6..99e655477a45 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
@@ -59,6 +59,7 @@ import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.TimeSource;
 import org.apache.camel.util.URISupport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -2075,7 +2076,7 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
                 long max = Math.min(1000, delta);
                 if (max > 0) {
                     LOG.trace("Sleeping for: {} millis until waking up for 
re-check", max);
-                    Thread.sleep(max);
+                    TimeSource.sleep(max);
                 }
 
                 // are we preparing for shutdown then only do redelivery if 
allowed
diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryPolicy.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryPolicy.java
index ffb3605f4375..21ae719c3cfc 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryPolicy.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryPolicy.java
@@ -28,6 +28,7 @@ import org.apache.camel.Predicate;
 import org.apache.camel.spi.Configurer;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.TimeSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -205,7 +206,7 @@ public class RedeliveryPolicy implements Cloneable, 
Serializable {
      */
     public void sleep(long redeliveryDelay) throws InterruptedException {
         LOG.debug("Sleeping for: {} millis until attempting redelivery", 
redeliveryDelay);
-        Thread.sleep(redeliveryDelay);
+        TimeSource.sleep(redeliveryDelay);
     }
 
     /**
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultTimeoutMap.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultTimeoutMap.java
index e64f980078df..3e3ac2fe785b 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultTimeoutMap.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultTimeoutMap.java
@@ -30,6 +30,7 @@ import java.util.concurrent.locks.ReentrantLock;
 import org.apache.camel.TimeoutMap;
 import org.apache.camel.support.service.ServiceSupport;
 import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.TimeSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -268,7 +269,7 @@ public class DefaultTimeoutMap<K, V> extends ServiceSupport 
implements TimeoutMa
     }
 
     protected long currentTime() {
-        return System.currentTimeMillis();
+        return TimeSource.currentTimeMillis();
     }
 
     @Override
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/MonotonicClock.java 
b/core/camel-support/src/main/java/org/apache/camel/support/MonotonicClock.java
index 01ab4f8beb89..1cde49f88fd1 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/MonotonicClock.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/MonotonicClock.java
@@ -19,6 +19,7 @@ package org.apache.camel.support;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.camel.clock.Clock;
+import org.apache.camel.util.TimeSource;
 
 /**
  * A clock that increases monotonically (i.e.: does not go back in time) for 
precise elapsed calculations.
@@ -30,13 +31,13 @@ public final class MonotonicClock implements Clock {
     private final long createdNano;
 
     public MonotonicClock() {
-        this.created = System.currentTimeMillis();
-        this.createdNano = System.nanoTime();
+        this.created = TimeSource.currentTimeMillis();
+        this.createdNano = TimeSource.nanoTime();
     }
 
     @Override
     public long elapsed() {
-        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - createdNano);
+        return TimeUnit.NANOSECONDS.toMillis(TimeSource.nanoTime() - 
createdNano);
     }
 
     @Override
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/startup/DefaultStartupConditionStrategy.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/startup/DefaultStartupConditionStrategy.java
index de7c8aeeafab..6e5edd3eab1b 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/startup/DefaultStartupConditionStrategy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/startup/DefaultStartupConditionStrategy.java
@@ -31,6 +31,7 @@ import org.apache.camel.spi.StartupStepRecorder;
 import org.apache.camel.support.OrderedComparator;
 import org.apache.camel.support.service.ServiceSupport;
 import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.TimeSource;
 import org.apache.camel.util.TimeUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -195,7 +196,7 @@ public class DefaultStartupConditionStrategy extends 
ServiceSupport implements S
 
             // wait a bit before next loop
             try {
-                Thread.sleep(interval);
+                TimeSource.sleep(interval);
             } catch (InterruptedException e) {
                 if (LOG.isDebugEnabled()) {
                     LOG.debug("Sleep interrupted, are we stopping? {}", 
isCamelStopping());
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/task/AbstractTask.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/task/AbstractTask.java
index 2ea8f365c8a9..4d14a2c017ce 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/task/AbstractTask.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/task/AbstractTask.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.support.task;
 
+import org.apache.camel.util.TimeSource;
+
 abstract class AbstractTask implements Task {
 
     static final long NEVER = -1L;
@@ -63,7 +65,7 @@ abstract class AbstractTask implements Task {
     @Override
     public long getCurrentElapsedTime() {
         if (firstAttemptTime > 0) {
-            return System.currentTimeMillis() - firstAttemptTime;
+            return TimeSource.currentTimeMillis() - firstAttemptTime;
         }
         return NEVER;
     }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/task/BackgroundTask.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/task/BackgroundTask.java
index 41fade98bf95..834a9affe11c 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/task/BackgroundTask.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/task/BackgroundTask.java
@@ -31,6 +31,7 @@ import org.apache.camel.CamelContext;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.task.budget.TimeBoundedBudget;
 import org.apache.camel.support.task.budget.TimeBudget;
+import org.apache.camel.util.TimeSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -128,7 +129,7 @@ public class BackgroundTask extends AbstractTask implements 
BlockingTask {
             return;
         }
 
-        lastAttemptTime = System.currentTimeMillis();
+        lastAttemptTime = TimeSource.currentTimeMillis();
         if (firstAttemptTime < 0) {
             firstAttemptTime = lastAttemptTime;
         }
@@ -156,7 +157,7 @@ public class BackgroundTask extends AbstractTask implements 
BlockingTask {
             throw e;
         }
         // scheduleWithFixedDelay waits interval after this run finishes, so 
compute from now
-        nextAttemptTime = System.currentTimeMillis() + budget.interval();
+        nextAttemptTime = TimeSource.currentTimeMillis() + budget.interval();
     }
 
     /**
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/task/ForegroundTask.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/task/ForegroundTask.java
index fc423a3c2a65..1ae2983fb91f 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/task/ForegroundTask.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/task/ForegroundTask.java
@@ -26,6 +26,7 @@ import java.util.function.Supplier;
 import org.apache.camel.CamelContext;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.task.budget.IterationBudget;
+import org.apache.camel.util.TimeSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -96,11 +97,11 @@ public class ForegroundTask extends AbstractTask implements 
BlockingTask {
         }
         try {
             if (budget.initialDelay() > 0) {
-                Thread.sleep(budget.initialDelay());
+                TimeSource.sleep(budget.initialDelay());
             }
 
             while (budget.next()) {
-                lastAttemptTime = System.currentTimeMillis();
+                lastAttemptTime = TimeSource.currentTimeMillis();
                 if (firstAttemptTime < 0) {
                     firstAttemptTime = lastAttemptTime;
                 }
@@ -114,7 +115,7 @@ public class ForegroundTask extends AbstractTask implements 
BlockingTask {
                 }
 
                 if (budget.canContinue()) {
-                    Thread.sleep(budget.interval());
+                    TimeSource.sleep(budget.interval());
                 } else {
                     status = Status.Exhausted;
                 }
@@ -169,11 +170,11 @@ public class ForegroundTask extends AbstractTask 
implements BlockingTask {
         }
         try {
             if (budget.initialDelay() > 0) {
-                Thread.sleep(budget.initialDelay());
+                TimeSource.sleep(budget.initialDelay());
             }
 
             while (budget.next()) {
-                lastAttemptTime = System.currentTimeMillis();
+                lastAttemptTime = TimeSource.currentTimeMillis();
                 if (firstAttemptTime < 0) {
                     firstAttemptTime = lastAttemptTime;
                 }
@@ -187,7 +188,7 @@ public class ForegroundTask extends AbstractTask implements 
BlockingTask {
                 nextAttemptTime = lastAttemptTime + budget.interval();
 
                 if (budget.canContinue()) {
-                    Thread.sleep(budget.interval());
+                    TimeSource.sleep(budget.interval());
                 } else {
                     status = Status.Exhausted;
                 }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/task/budget/IterationBoundedBudget.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/task/budget/IterationBoundedBudget.java
index 7acc0f5f6ecf..6f4cde765d38 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/task/budget/IterationBoundedBudget.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/task/budget/IterationBoundedBudget.java
@@ -21,6 +21,7 @@ import java.time.Instant;
 
 import org.apache.camel.support.task.budget.backoff.BackOffStrategy;
 import org.apache.camel.support.task.budget.backoff.FixedBackOffStrategy;
+import org.apache.camel.util.TimeSource;
 
 /**
  * This task budget limits the execution by a given number of iterations or an 
unlimited number if configured to do so.
@@ -42,14 +43,14 @@ public class IterationBoundedBudget implements 
IterationBudget {
         this.initialDelay = initialDelay;
         this.maxIterations = maxIterations;
         this.backOffStrategy = new FixedBackOffStrategy(interval);
-        this.startTime = Instant.now();
+        this.startTime = TimeSource.instant();
     }
 
     IterationBoundedBudget(long initialDelay, int maxIterations, 
BackOffStrategy backOffStrategy) {
         this.initialDelay = initialDelay;
         this.maxIterations = maxIterations;
         this.backOffStrategy = backOffStrategy;
-        this.startTime = Instant.now();
+        this.startTime = TimeSource.instant();
     }
 
     @Override
@@ -92,6 +93,6 @@ public class IterationBoundedBudget implements 
IterationBudget {
 
     @Override
     public Duration elapsed() {
-        return Duration.between(startTime, Instant.now());
+        return Duration.between(startTime, TimeSource.instant());
     }
 }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/task/budget/TimeBoundedBudget.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/task/budget/TimeBoundedBudget.java
index 6606d041a545..40ce06482bfb 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/task/budget/TimeBoundedBudget.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/task/budget/TimeBoundedBudget.java
@@ -19,6 +19,8 @@ package org.apache.camel.support.task.budget;
 import java.time.Duration;
 import java.time.Instant;
 
+import org.apache.camel.util.TimeSource;
+
 /**
  * This task budget limits the execution by both a maximum amount of time for 
the execution.
  */
@@ -36,7 +38,7 @@ public class TimeBoundedBudget implements TimeBudget {
         this.initialDelay = initialDelay;
         this.interval = interval;
         this.maxDuration = maxDuration;
-        this.startTime = Instant.now();
+        this.startTime = TimeSource.instant();
     }
 
     @Override
@@ -82,6 +84,6 @@ public class TimeBoundedBudget implements TimeBudget {
 
     @Override
     public Duration elapsed() {
-        return Duration.between(startTime, Instant.now());
+        return Duration.between(startTime, TimeSource.instant());
     }
 }
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 1f10d577692b..ab9e81f85e1f 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
@@ -36,6 +36,7 @@ import org.apache.camel.spi.Configurer;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.RoutePolicy;
 import org.apache.camel.support.RoutePolicySupport;
+import org.apache.camel.util.TimeSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -203,7 +204,7 @@ public class ThrottlingExceptionRoutePolicy extends 
RoutePolicySupport implement
             if (hasFailed(exchange)) {
                 // record the failure
                 failures.incrementAndGet();
-                lastFailure = System.currentTimeMillis();
+                lastFailure = TimeSource.currentTimeMillis();
             } else {
                 success.incrementAndGet();
             }
@@ -269,7 +270,7 @@ public class ThrottlingExceptionRoutePolicy extends 
RoutePolicySupport implement
             }
         } else if (state.get() == STATE_OPEN) {
             if (!keepOpenBool.get()) {
-                long elapsedTimeSinceOpened = System.currentTimeMillis() - 
openedAt;
+                long elapsedTimeSinceOpened = TimeSource.currentTimeMillis() - 
openedAt;
                 if (halfOpenAfter <= elapsedTimeSinceOpened) {
                     LOG.debug("Checking an open circuit...");
                     if (halfOpenHandler != null) {
@@ -301,7 +302,7 @@ public class ThrottlingExceptionRoutePolicy extends 
RoutePolicySupport implement
         logState();
         // failures exceed the threshold
         // AND the last of those failures occurred within window
-        if (failures.get() >= failureThreshold && lastFailure >= 
System.currentTimeMillis() - failureWindow) {
+        if (failures.get() >= failureThreshold && lastFailure >= 
TimeSource.currentTimeMillis() - failureWindow) {
             output = true;
         }
 
@@ -316,7 +317,7 @@ public class ThrottlingExceptionRoutePolicy extends 
RoutePolicySupport implement
             }
             suspendOrStopConsumer(route.getConsumer());
             state.set(STATE_OPEN);
-            openedAt = System.currentTimeMillis();
+            openedAt = TimeSource.currentTimeMillis();
             this.addHalfOpenTimer(route);
             logState();
         } catch (Exception e) {
@@ -336,7 +337,7 @@ public class ThrottlingExceptionRoutePolicy extends 
RoutePolicySupport implement
             lock.lock();
             suspendOrStopConsumer(route.getConsumer());
             state.set(STATE_OPEN);
-            openedAt = System.currentTimeMillis();
+            openedAt = TimeSource.currentTimeMillis();
             this.addHalfOpenTimer(route);
             logState();
         } catch (Exception e) {
@@ -414,7 +415,7 @@ public class ThrottlingExceptionRoutePolicy extends 
RoutePolicySupport implement
         String routeState = getStateAsString();
         if (failures.get() > 0) {
             return String.format("State %s, failures %d, last failure %d ms 
ago", routeState, failures.get(),
-                    System.currentTimeMillis() - lastFailure);
+                    TimeSource.currentTimeMillis() - lastFailure);
         } else {
             return String.format("State %s, failures %d", routeState, 
failures.get());
         }
diff --git a/core/camel-util/src/main/java/org/apache/camel/util/StopWatch.java 
b/core/camel-util/src/main/java/org/apache/camel/util/StopWatch.java
index be49ceeba714..9e92ac81b0c2 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/StopWatch.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/StopWatch.java
@@ -31,7 +31,7 @@ public final class StopWatch {
      * Starts the stop watch
      */
     public StopWatch() {
-        this.start = System.nanoTime();
+        this.start = TimeSource.nanoTime();
     }
 
     /**
@@ -41,7 +41,7 @@ public final class StopWatch {
      */
     public StopWatch(boolean start) {
         if (start) {
-            this.start = System.nanoTime();
+            this.start = TimeSource.nanoTime();
         }
     }
 
@@ -49,7 +49,7 @@ public final class StopWatch {
      * Starts or restarts the stop watch
      */
     public void restart() {
-        start = System.nanoTime();
+        start = TimeSource.nanoTime();
     }
 
     /**
@@ -66,7 +66,7 @@ public final class StopWatch {
      */
     public long taken() {
         if (start > 0) {
-            long delta = System.nanoTime() - start;
+            long delta = TimeSource.nanoTime() - start;
             return Duration.ofNanos(delta).toMillis();
         }
         return 0;
@@ -79,7 +79,7 @@ public final class StopWatch {
      */
     public long takenAndRestart() {
         long answer = taken();
-        start = System.nanoTime();
+        start = TimeSource.nanoTime();
         return answer;
     }
 
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
new file mode 100644
index 000000000000..de67666aaa8c
--- /dev/null
+++ b/core/camel-util/src/main/java/org/apache/camel/util/TimeSource.java
@@ -0,0 +1,172 @@
+/*
+ * 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.time.Instant;
+
+/**
+ * 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. 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
+ * consoles stay on {@link System}; the durations they measure with {@link 
StopWatch} or {@code MonotonicClock} follow
+ * this class (with the default provider this is the same {@link 
System#nanoTime()}). The created time of
+ * {@code MonotonicClock} follows this class too, so the message history times 
({@code MessageHistory.getTime()}) are
+ * virtual in a deterministic simulation test.
+ */
+public final class TimeSource {
+
+    /**
+     * The clock and the sleep behind {@link TimeSource}. Implemented only by 
test harnesses.
+     */
+    public interface Provider {
+
+        /**
+         * The current time in milliseconds since the epoch, as {@link 
System#currentTimeMillis()}.
+         */
+        long currentTimeMillis();
+
+        /**
+         * A monotonic time in nanoseconds with an arbitrary origin, as {@link 
System#nanoTime()}: only the difference
+         * of two values means something.
+         */
+        long nanoTime();
+
+        /**
+         * The current instant, as {@link Instant#now()}. By default {@link 
#currentTimeMillis()} as an instant.
+         */
+        default Instant instant() {
+            return Instant.ofEpochMilli(currentTimeMillis());
+        }
+
+        /**
+         * Sleeps for the given milliseconds, as {@link Thread#sleep(long)}.
+         *
+         * @throws IllegalArgumentException if the value of millis is negative
+         * @throws InterruptedException     if the thread is interrupted 
before or while it sleeps; the interrupted
+         *                                  status of the thread is cleared 
when this exception is thrown
+         */
+        void sleep(long millis) throws InterruptedException;
+    }
+
+    private static final Provider SYSTEM = new SystemProvider();
+
+    private static volatile Provider provider = SYSTEM;
+
+    private TimeSource() {
+    }
+
+    /**
+     * The current time in milliseconds since the epoch: {@link 
System#currentTimeMillis()} unless a test harness
+     * installed another provider.
+     */
+    public static long currentTimeMillis() {
+        return provider.currentTimeMillis();
+    }
+
+    /**
+     * A monotonic time in nanoseconds with an arbitrary origin: {@link 
System#nanoTime()} unless a test harness
+     * installed another provider. Only the difference of two values means 
something.
+     */
+    public static long nanoTime() {
+        return provider.nanoTime();
+    }
+
+    /**
+     * The current instant: {@link Instant#now()} unless a test harness 
installed another provider.
+     */
+    public static Instant instant() {
+        return provider.instant();
+    }
+
+    /**
+     * Sleeps for the given milliseconds: {@link Thread#sleep(long)} unless a 
test harness installed another provider.
+     *
+     * @throws IllegalArgumentException if the value of millis is negative
+     * @throws InterruptedException     if the thread is interrupted before or 
while it sleeps; the interrupted status
+     *                                  of the thread is cleared when this 
exception is thrown
+     */
+    public static void sleep(long millis) throws InterruptedException {
+        provider.sleep(millis);
+    }
+
+    /**
+     * Installs a provider, for test harnesses only.
+     *
+     * @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
+     */
+    public static Provider install(Provider provider) {
+        ObjectHelper.notNull(provider, "provider");
+        synchronized (TimeSource.class) {
+            Provider previous = TimeSource.provider;
+            TimeSource.provider = provider;
+            return previous;
+        }
+    }
+
+    /**
+     * Installs the default provider again ({@link System} and {@link 
Thread#sleep(long)}), for test harnesses only.
+     */
+    public static void reset() {
+        install(SYSTEM);
+    }
+
+    /**
+     * Whether the default provider ({@link System} and {@link 
Thread#sleep(long)}) is installed.
+     */
+    public static boolean isDefault() {
+        return provider == SYSTEM;
+    }
+
+    /**
+     * The default provider: {@link System} and {@link Thread#sleep(long)}.
+     */
+    private static final class SystemProvider implements Provider {
+
+        @Override
+        public long currentTimeMillis() {
+            return System.currentTimeMillis();
+        }
+
+        @Override
+        public long nanoTime() {
+            return System.nanoTime();
+        }
+
+        @Override
+        public Instant instant() {
+            return Instant.now();
+        }
+
+        @Override
+        public void sleep(long millis) throws InterruptedException {
+            Thread.sleep(millis);
+        }
+
+        @Override
+        public String toString() {
+            return "SystemTimeSource";
+        }
+    }
+}
diff --git 
a/core/camel-util/src/main/java/org/apache/camel/util/backoff/BackOffTimerTask.java
 
b/core/camel-util/src/main/java/org/apache/camel/util/backoff/BackOffTimerTask.java
index 27015c0c3505..6e0c3d71198a 100644
--- 
a/core/camel-util/src/main/java/org/apache/camel/util/backoff/BackOffTimerTask.java
+++ 
b/core/camel-util/src/main/java/org/apache/camel/util/backoff/BackOffTimerTask.java
@@ -26,6 +26,7 @@ import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.BiConsumer;
 
+import org.apache.camel.util.TimeSource;
 import org.apache.camel.util.function.ThrowingFunction;
 
 public final class BackOffTimerTask implements BackOffTimer.Task, Runnable {
@@ -171,7 +172,7 @@ public final class BackOffTimerTask implements 
BackOffTimer.Task, Runnable {
     public void run() {
         if (status == Status.Active) {
             try {
-                lastAttemptTime = System.currentTimeMillis();
+                lastAttemptTime = TimeSource.currentTimeMillis();
                 if (firstAttemptTime < 0) {
                     firstAttemptTime = lastAttemptTime;
                 }
diff --git 
a/core/camel-util/src/test/java/org/apache/camel/util/TimeSourceTest.java 
b/core/camel-util/src/test/java/org/apache/camel/util/TimeSourceTest.java
new file mode 100644
index 000000000000..3ac078fbe3d3
--- /dev/null
+++ b/core/camel-util/src/test/java/org/apache/camel/util/TimeSourceTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The provider of {@link TimeSource} is JVM-wide: the tests that install one 
run isolated from the other tests.
+ */
+@Isolated
+class TimeSourceTest {
+
+    /** A clock that only moves when asked to, and records the sleeps. */
+    private static final class ManualProvider implements TimeSource.Provider {
+        long millis = 1_000_000L;
+        final List<Long> sleeps = new ArrayList<>();
+
+        @Override
+        public long currentTimeMillis() {
+            return millis;
+        }
+
+        @Override
+        public long nanoTime() {
+            return millis * 1_000_000L;
+        }
+
+        @Override
+        public void sleep(long millis) {
+            sleeps.add(millis);
+            this.millis += millis;
+        }
+    }
+
+    @AfterEach
+    void restoreDefault() {
+        TimeSource.reset();
+    }
+
+    @Test
+    void defaultProviderReadsTheSystemClock() {
+        assertTrue(TimeSource.isDefault());
+        long before = System.currentTimeMillis();
+        long millis = TimeSource.currentTimeMillis();
+        long after = System.currentTimeMillis();
+        assertTrue(before <= millis && millis <= after, before + " <= " + 
millis + " <= " + after);
+
+        long nanosBefore = System.nanoTime();
+        long nanos = TimeSource.nanoTime();
+        assertTrue(nanos - nanosBefore >= 0);
+        assertTrue(System.nanoTime() - nanos >= 0);
+
+        Instant instantBefore = Instant.now();
+        Instant instant = TimeSource.instant();
+        assertFalse(instant.isBefore(instantBefore));
+        assertFalse(instant.isAfter(Instant.now()));
+    }
+
+    @Test
+    void defaultSleepReturnsForZero() throws InterruptedException {
+        StopWatch watch = new StopWatch();
+        TimeSource.sleep(0);
+        assertTrue(watch.taken() < 1_000, "took " + watch.taken());
+    }
+
+    @Test
+    void defaultSleepRejectsANegativeValueAsThreadSleep() {
+        assertThrows(IllegalArgumentException.class, () -> 
TimeSource.sleep(-1));
+    }
+
+    @Test
+    void defaultSleepOfAnInterruptedThreadThrowsAndClearsTheFlag() {
+        Thread.currentThread().interrupt();
+        try {
+            assertThrows(InterruptedException.class, () -> 
TimeSource.sleep(10_000));
+            assertFalse(Thread.currentThread().isInterrupted(), "the 
interrupted status is cleared, as Thread.sleep");
+        } finally {
+            // never leave the flag set for the next test
+            Thread.interrupted();
+        }
+    }
+
+    @Test
+    void installedProviderIsUsedUntilReset() throws InterruptedException {
+        ManualProvider manual = new ManualProvider();
+        TimeSource.Provider previous = TimeSource.install(manual);
+        assertNotSame(manual, previous);
+        assertFalse(TimeSource.isDefault());
+
+        assertEquals(1_000_000L, TimeSource.currentTimeMillis());
+        assertEquals(1_000_000L * 1_000_000L, TimeSource.nanoTime());
+        assertEquals(Instant.ofEpochMilli(1_000_000L), TimeSource.instant());
+
+        TimeSource.sleep(2_500);
+        assertEquals(List.of(2_500L), manual.sleeps);
+        assertEquals(1_002_500L, TimeSource.currentTimeMillis());
+
+        TimeSource.reset();
+        assertTrue(TimeSource.isDefault());
+        TimeSource.sleep(0);
+        assertEquals(List.of(2_500L), manual.sleeps, "the default provider is 
back");
+    }
+
+    @Test
+    void installReturnsThePreviousProviderToPutBack() {
+        ManualProvider first = new ManualProvider();
+        ManualProvider second = new ManualProvider();
+        second.millis = 42;
+        TimeSource.Provider original = TimeSource.install(first);
+        assertSame(first, TimeSource.install(second));
+        assertEquals(42, TimeSource.currentTimeMillis());
+        assertSame(second, TimeSource.install(first));
+        assertEquals(1_000_000L, TimeSource.currentTimeMillis());
+        assertSame(first, TimeSource.install(original));
+        assertTrue(TimeSource.isDefault());
+    }
+
+    @Test
+    void installRejectsNull() {
+        assertThrows(IllegalArgumentException.class, () -> 
TimeSource.install(null));
+        assertTrue(TimeSource.isDefault());
+    }
+
+    @Test
+    void stopWatchReadsTheInstalledProvider() {
+        ManualProvider manual = new ManualProvider();
+        TimeSource.install(manual);
+        StopWatch watch = new StopWatch();
+        manual.millis += 3_600_000L;
+        assertEquals(3_600_000L, watch.taken());
+        assertEquals(3_600_000L, watch.takenAndRestart());
+        manual.millis += 5;
+        assertEquals(5, watch.taken());
+    }
+}

Reply via email to