Hi all,
camel-test-dst (CAMEL-25738, PR https://github.com/apache/camel/pull/28067)
runs the real consumer code of a
component under one seeded scheduler with virtual time, so a test can try
thousands of interleavings and faults in
seconds and replay a failing seed exactly. Worlds written with it found or
confirmed several timing bugs, for
example CAMEL-25683, CAMEL-25737, CAMEL-25741, CAMEL-25756 and CAMEL-25758.
The harness controls Camel's thread pools through the existing
ThreadPoolFactory SPI. Time is the gap: core reads
System directly, so today a test either rewrites the byte code of core
classes or lives with real time and real
sleeps in core. Claus suggested a small time source in camel-util, and I
would like to discuss that here before
anything goes into core.
Where core reads the clock or sleeps today (src/main/java on main,
2026-10-17):
camel-util 4 currentTimeMillis, 5 nanoTime, 3 Thread.sleep
camel-support 24 currentTimeMillis, 6 nanoTime, 6 Thread.sleep, 5
Instant.now
camel-base 43 currentTimeMillis (nearly all event timestamps)
camel-base-engine 9 currentTimeMillis, 2 Thread.sleep, 1 Instant.now,
1 new Date()
camel-core-processor 6 currentTimeMillis, 7 nanoTime, 5 Thread.sleep
camel-management 9 currentTimeMillis, 1 Thread.sleep, 2 new Date()
others (api, languages, console, health, main): a handful
About 30 of these change behaviour, and only those need to be controlled in
a test:
- waits: RedeliveryPolicy.sleep and the sleep loop of
RedeliveryErrorHandler, ForegroundTask, the wait loop of
DefaultShutdownStrategy, DelayProcessorSupport, MulticastProcessor,
OptimisticLockRetryPolicy,
DefaultStartupConditionStrategy;
- task timing: StopWatch, MonotonicClock, AbstractTask, BackgroundTask,
TimeBoundedBudget, BackOffTimerTask,
DefaultTimeoutMap;
- throttling: TotalRequestsThrottler, ConcurrentRequestsThrottler,
SamplingThrottler,
ThrottlingExceptionRoutePolicy.
Event timestamps, the debugger, the tracer, management and the consoles can
stay on System.
Options I see:
1. Byte code rewriting only (what stage 1 does). No core change, but
runtime agent attach is being phased out in the
JDK, it only covers the calls it is told about, and rewritten core
classes stay rewritten for the whole test JVM.
2. A time source for all of core: about 120 call sites, many on hot paths,
for no gain in tests.
3. In between, which is what I would propose:
- a small static, internal TimeSource in camel-util:
currentTimeMillis(), nanoTime(), instant(), sleep(long).
The default delegates to System and Thread.sleep; a test harness can
install another provider. Static rather
than per CamelContext, because StopWatch and MonotonicClock have no
context;
- move only the ~30 call sites above;
- keep byte code rewriting for the component under test, pointed at
TimeSource, with the agent loaded by
-javaagent instead of attached at runtime;
- later, components use it when they are touched anyway, and a
forbidden-apis rule keeps new
Instant.now() / Thread.sleep() out of consumers.
Rough shape:
public final class TimeSource {
public interface Provider {
long currentTimeMillis();
long nanoTime();
void sleep(long millis) throws InterruptedException;
}
private static volatile Provider provider = SYSTEM;
public static long nanoTime() { return provider.nanoTime(); }
...
public static Provider install(Provider p) { ... } // test harnesses
only
}
Open points: package and name; a volatile field or a plain static field set
once before the context starts; and
whether sleep belongs there or in a separate wait helper.
On cost: I will run a JMH benchmark (direct System.nanoTime vs. TimeSource
with a volatile and a plain field,
StopWatch, and a direct -> log route with statistics on) and post the
numbers in this thread. My expectation is
that the default path inlines to the same call plus one ordinary load, so
it should be within noise.
Thoughts?
Thanks,
Shashank