allthingssecurity opened a new pull request, #28382:
URL: https://github.com/apache/camel/pull/28382
## What
Stage 2 of CAMEL-25738, as discussed on dev@ ("[DISCUSS] An internal time
source in camel-util (for deterministic
simulation tests)", +1 from Claus).
1. **`org.apache.camel.util.TimeSource`** in camel-util: a small static,
internal class with `currentTimeMillis()`,
`nanoTime()`, `instant()` and `sleep(long)`. The default provider
delegates to `System.currentTimeMillis()`,
`System.nanoTime()`, `Instant.now()` and `Thread.sleep(long)`. A test
harness installs another provider with
`install(Provider)` (which returns the previous one, to put back) and
`reset()` puts the default back; the field is
`volatile`. The javadoc says it is internal and only for test harnesses
such as camel-test-dst. Static rather than
per `CamelContext`, because `StopWatch` and `MonotonicClock` have no
context.
2. **The core call sites that wait, time tasks or throttle** read the clock
and sleep through it (49 call sites in 20
classes, the list from the JIRA plus one identical neighbour, below).
Event timestamps, the clock of an exchange
(`ResetableClock`) and the direct clock reads of the debugger, the
tracer, management and the consoles stay on
`System`. The durations they measure with `StopWatch` or `MonotonicClock`
follow `TimeSource`, because both classes
are on the list: for example the processor statistics of management (one
`StopWatch` per processor and exchange
when statistics are on), the backlog debugger and tracer, and the message
history. With the default provider that
is the same `System.nanoTime()`; the `StopWatch` row under Cost is its
price.
3. **camel-test-dst** installs its virtual clock (`DstClock`) as the
provider when a run is created and puts the
previous provider back when the run is closed. The core waits then run in
virtual time without rewriting core
classes, so the `@DstInstrument(alsoRewrite)` stop-gap is no longer
needed for them (three waits are not clock
reads and stay outside, see camel-test-dst below); the byte code
rewriting of the component under test is
unchanged. A sleep through `TimeSource` is traced with its call site, as
a rewritten sleep
is.
No behaviour change with the default provider: every moved call is the same
JDK call, one static method further away.
No option, default or public signature changes, so there is no upgrade guide
entry.
## Moved call sites
| Area | Class | Calls |
|---|---|---|
| waits | `RedeliveryPolicy.sleep` | 1 `Thread.sleep` |
| | `RedeliveryErrorHandler` (the synchronous redelivery sleep loop) | 1
`Thread.sleep` |
| | `ForegroundTask` | 4 `Thread.sleep`, 2 `currentTimeMillis` |
| | `DefaultShutdownStrategy` (the wait loop for in-flight exchanges) | 1
`Thread.sleep` |
| | `DelayProcessorSupport` (synchronous delay) | 1 `Thread.sleep`, 1
`currentTimeMillis` (the protected `currentSystemTime()`, which nothing in the
repository calls; moved so that a subclass gets the same clock) |
| | `MulticastProcessor` (delay without a scheduled executor) | 1
`Thread.sleep` |
| | `OptimisticLockRetryPolicy` | 1 `Thread.sleep` |
| | `DefaultStartupConditionStrategy` | 1 `Thread.sleep` |
| task timing | `StopWatch` | 5 `nanoTime` |
| | `MonotonicClock` | 1 `currentTimeMillis`, 2 `nanoTime` |
| | `AbstractTask` | 1 `currentTimeMillis` |
| | `BackgroundTask` | 2 `currentTimeMillis` |
| | `TimeBoundedBudget` | 2 `Instant.now` |
| | `IterationBoundedBudget` (identical neighbour, see below) | 3
`Instant.now` |
| | `BackOffTimerTask` | 1 `currentTimeMillis` |
| | `DefaultTimeoutMap` | 1 `currentTimeMillis` |
| throttling | `TotalRequestsThrottler` | 5 `currentTimeMillis`, 1
`nanoTime` |
| | `ConcurrentRequestsThrottler` | 4 `nanoTime` |
| | `SamplingThrottler` | 1 `nanoTime` |
| | `ThrottlingExceptionRoutePolicy` | 6 `currentTimeMillis` |
Beyond the JIRA list: `IterationBoundedBudget`, the sibling of
`TimeBoundedBudget` with the same `Instant.now()` start
time and `elapsed()`. `IterationTimeBoundedBudget` (the budget
`ForegroundTask` usually gets) combines the two, and
`ForegroundTask.elapsed()` comes from it, so leaving it on `System` would
mix virtual and real time in one task.
Every listed class had its calls on current `main`; none was skipped.
`ResetableClock`, which the dev@ draft listed
next to `MonotonicClock`, is the clock of every exchange (its created
timestamp), so it stays on `System` like the
other timestamps; `@DstInstrument(alsoRewrite)` still covers it when a world
needs it.
## Cost
From the JMH benchmark posted to the dev@ thread (JMH 1.37, JDK 21, AArch64
laptop, 3 forks, average ns/op; a local
copy of the class, not a CI run):
| | 1 thread | 8 threads |
|---|---|---|
| `System.nanoTime()` | 8.92 | 356.1 |
| `TimeSource.nanoTime()`, volatile field | 9.10 | 354.9 |
| `TimeSource.nanoTime()`, after a second provider was used (bimorphic call
site) | 9.22 | 355.7 |
| `System.currentTimeMillis()` | 13.59 | 186.5 |
| `TimeSource.currentTimeMillis()` | 13.62 | 185.2 |
| `new StopWatch().taken()`, before / with `TimeSource` | 19.96 / 20.52 |
709.6 / 710.8 |
About 0.2 ns more per `nanoTime()` and 0.5 ns per `StopWatch` on one thread,
nothing measurable for
`currentTimeMillis()` and nothing measurable with 8 threads; a volatile
field was not measurably slower than a plain
one. For scale, the simplest `direct -> log` route takes about 410 ns per
message. Only AArch64 was measured. The
`StopWatch` row is also the cost on the processor statistics path of
management, which times each processor with a
`StopWatch` when statistics are on. The dev@ mail set aside a time source
for all of core partly because of hot paths
such as the processor statistics; with `StopWatch` on the list they still
pass through `TimeSource`, at about 0.5 ns
per `StopWatch`.
## camel-test-dst
- `Dst` installs `DstClock.TIME_SOURCE` when a run is created and puts the
previous provider back in `close()`; the
extension closes each seed's run, so the default is back after every test.
Outside a run nothing is installed.
- A core clock read or sleep from a thread outside the harness while a run
is open is an escape, as for the rewritten
calls (strict mode fails the seed). A sleep keeps the semantics of
`Thread.sleep`: a negative value throws
`IllegalArgumentException`, an interrupted thread gets
`InterruptedException` with the flag cleared.
- `DstCoreWaitsTest` no longer lists core classes in `alsoRewrite`; it adds
a `ForegroundTask` that gives up when its
time budget is spent (30 s of virtual time, which only holds when
`TimeBoundedBudget` reads the virtual clock) and a
synchronous delayer. `DstTimeSourceTest` covers the install and restore
(also of a provider installed before the
run), the core clocks without rewriting, the traced sleep, the
`Thread.sleep` semantics and the off-loop escape.
`DstAlsoRewriteTest` now uses `ResetableClock`, which stays on `System`.
- Three waits in these classes are not clock reads and stay outside the
virtual clock; `test-dst.adoc` lists them:
- `TotalRequestsThrottler` waits for a permit with `DelayQueue.take()`.
The delay of a permit is now virtual, but the
wait is a real one on the loop thread, where the virtual clock does not
move, so a seed that has to wait for a
permit ends on the watchdog. Before this PR the permit came due after
the real time period, so such a seed waited
in real time with mixed clocks; this only changes DST runs, production
is unchanged. `asyncDelayed()` does not avoid it (a delayed
exchange that finds its permit taken by another one waits on the same
queue). With
`@DstInstrument(alsoRewrite =
"org.apache.camel.processor.TotalRequestsThrottler")` the wait is virtual.
Checked
locally with a probe test that is not committed:
`throttle(1).timePeriodMillis(1000)` and three exchanges end on the
watchdog, with and without `asyncDelayed()`, and pass at virtual 0, 1000
and 2000 ms in 9 ms with that `alsoRewrite`.
- The half-open check of `ThrottlingExceptionRoutePolicy` runs on a
`java.util.Timer` thread: it fires after
`halfOpenAfter` of real time and its clock read is an escape. Only the
counters, `openedAt` and `lastFailure` are
virtual.
- `BackgroundTask.run` waits on a latch; `BackgroundTask.schedule` and the
attempts are virtual.
- `DstExtension` closes the run of a seed also when its replay trace cannot
be written, and after a test class it
closes a run that was left open and fails the class: with this PR an open
run would keep the virtual clock installed
for all of Camel core in the later tests of the JVM.
- `test-dst.adoc` (and its catalog copy): the harness controls Camel core
through `TimeSource`, and `alsoRewrite` is for
classes outside it.
- Not in this PR, each a follow-up JIRA: loading the agent with `-javaagent`
instead of attaching it at runtime
([CAMEL-26103](https://issues.apache.org/jira/browse/CAMEL-26103), named
in the JIRA proposal), and creating the
half-open timer of `ThrottlingExceptionRoutePolicy` through the
`ExecutorServiceManager`
([CAMEL-26104](https://issues.apache.org/jira/browse/CAMEL-26104); that
would also stop a new `Timer` thread being
created on every open and reopen in production). The forbidden-apis rule
for new `Thread.sleep` / `Instant.now()` in
consumers stays for later, as the JIRA says.
- Seen in the diff and left as is, so that this stays a mechanical swap:
`TotalRequestsThrottler` stores the queued
timestamp of an `asyncDelayed()` exchange as `nanoTime()` and subtracts it
from `currentTimeMillis()` values in two
TRACE lines ("Queued for {}ms"), so those numbers are wrong. TRACE only
and older than this PR; for a follow-up.
## The SQS world with core waits
`SqsRedeliveryDstTest` (new, 500 seeds) runs the SQS world of
`SqsVisibilityDstTest` with synchronous redelivery: a
failed message is retried twice by the default error handler after a
redelivery delay that the seed picks between 1x
and 3x the visibility timeout, so the visibility timeout extender has to
keep the message invisible while
`RedeliveryErrorHandler` sleeps on the consumer's thread. The consumer holds
the message during that wait, and the
world checks that no message is delivered again meanwhile and that each
redelivery came at least the redelivery delay
after the failure (by exchange). The world without redelivery draws nothing
new, so its seeds keep their configuration.
Results (strict mode, no runtime escape in any world):
| | with this PR | the same branch with a real-time provider installed
instead of the virtual clock (local negative control, not committed) |
|---|---|---|
| `SqsRedeliveryDstTest` | 500 of 500 seeds pass, 7.6 ms per seed | seeds 2
and 3 of the first 3 fail: the redelivery came 0 ms of virtual time after the
failure (the error handler slept 8.9 s and 6.4 s of real time on the test
thread); 9.7 s per seed |
A replay of seeds 1 to 40 (`-Ddst.seed=1,...,40`) writes their traces: 20 of
the 40 seeds go through the redelivery
wait, with 730 one-second sleeps traced as `sleep at
RedeliveryErrorHandler$RedeliveryTask.sleep:2079`, and the 40
seeds take 0.88 s of real time. The other worlds pass unchanged with the
core waits now virtual: `SqsVisibilityDstTest`
1000 of 1000, `EventHubsCheckpointDstTest` 1002 of 1002 (with its in-JVM
replay test), `SlackSuspendDstTest`
1000 of 1000.
## Testing
```
./mvnw test -pl
core/camel-util,core/camel-support,core/camel-core-model,core/camel-core
./mvnw test -pl
components/camel-test/camel-test-dst,components/camel-aws/camel-aws2-sqs,\
components/camel-azure/camel-azure-eventhubs,components/camel-slack
-Dtest='Dst*Test,*DstTest' \
-Dsurefire.failIfNoSpecifiedTests=false
```
- camel-util: 305 tests pass, 8 of them the new `TimeSourceTest` (the
default provider against `System`, `sleep`
with 0, a negative value and an interrupted thread, install / reset / the
previous provider, `StopWatch` on an
installed provider). camel-support: 139 pass; camel-core-model: 11 pass.
The other touched modules have no tests of
their own (their tests are in camel-core).
- camel-core: 8265 tests run in three parts (the `processor` sub packages,
the rest of `processor`, everything else),
54 skipped, 0 failures caused by this change. The third part had 4
failures, all in tests of changes on `main`
newer than the camel-bean and camel-file snapshots used locally
(CAMEL-25871 / CAMEL-25900 in
`SimpleSyntaxHintsTest`, CAMEL-25930 in
`FilePollEnrichDynamicFileNameTest`); with camel-bean and camel-file built
from this branch both classes pass (34 tests). `ControlBusFailRouteTest`
failed once and passed on the rerun (it is
disabled on CI as flaky).
- camel-test-dst: 57 self-tests pass (`DstCoreWaitsTest` 6,
`DstTimeSourceTest` 6). With a real-time provider
installed instead of the virtual clock (local negative control) all 6 of
`DstCoreWaitsTest` and all 6 of
`DstTimeSourceTest` fail.
- The worlds, strict mode, no runtime escape: `SqsRedeliveryDstTest` 500,
`SqsVisibilityDstTest` 1000,
`EventHubsCheckpointDstTest` 1002, `SlackSuspendDstTest` 1000 (seeds or
tests), all pass. The whole camel-aws2-sqs
suite passes (1595 tests, 16 skipped).
- In virtual time this PR tests the synchronous redelivery, `ForegroundTask`
with both budgets, the synchronous
delayer, the shutdown wait loop, `StopWatch` and `MonotonicClock`. The
other moved call sites (the throttlers,
`MulticastProcessor`, `OptimisticLockRetryPolicy`,
`DefaultStartupConditionStrategy`, `BackgroundTask`,
`BackOffTimerTask`, `DefaultTimeoutMap`, `ThrottlingExceptionRoutePolicy`)
are the same one-line swap; their camel-core
tests cover them on the default provider. The waits of the throttler, the
circuit breaker policy and `BackgroundTask`
that stay outside the virtual clock are listed under camel-test-dst above.
- `formatter:format impsort:sort` applied (the build runs them).
_Claude Code on behalf of allthingssecurity_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]