Skip to content

CAMEL-26957: camel-nats - a DST fake world for the JetStream pull consumer - #29995

Merged
davsclaus merged 2 commits into
apache:mainfrom
allthingssecurity:camel-nats-dst-world-pr
Nov 15, 2026
Merged

davsclaus merged 2 commits into
apache:mainfrom
allthingssecurity:camel-nats-dst-world-pr

Conversation

@allthingssecurity

@allthingssecurity allthingssecurity commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

CAMEL-26957, part of CAMEL-26610. Two differences from the JIRA text: the world drives the pull consumer only, as the push consumer and core NATS subjects deliver on threads of jnats that the harness does not run; and a server restart is modelled as the server deleting the ephemeral consumers while the connection stays up, not as a reconnect of the connection itself.

A deterministic simulation test (camel-test-dst) of the camel-nats JetStream pull consumer, in the dst package of its tests, in the same form as the SQS world: NatsConsumerDstTest (1000 seeds), NatsWorld and FakeJetStream. Test only; no main code changes.

What the world models: the real consumer (NatsConsumer and its consuming task) and Camel's routing run on the virtual-time loop; only the NATS server is faked, behind a jnats Connection given to the endpoint with the connection option (dynamic proxies of the jnats interfaces, Mockito mocks of the jnats value classes ServerInfo, StreamInfo, ConsumerInfo and NatsJetStreamMetaData). The fake is written from the NATS JetStream documentation and the jnats client, and cites them: a pull fetch holds a partial batch until its pull expires, the batch is full or a status ends it, as jnats 2.26.4 NatsJetStreamPullSubscription._fetch does, and the AckWait of a message runs from when the server could send it (when it became available, or the start of the fetch), explicit ack, nak and nakWithDelay, redelivery after AckWait, MaxDeliver, ephemeral and durable consumers, the deletion of the ephemeral consumers, where the seed picks between a server restart (a waiting pull gets 409 Server Shutdown, a warning, and returns at its timeout what it got) and a deletion while connected (a waiting pull gets 409 Consumer Deleted); a later pull gets no message and getConsumerInfo answers null, so the consumer finds the consumer gone and subscribes again), a flush that times out, and acks that are lost (an ack is a publish without an answer). A new ephemeral consumer starts with no delivery state of its own; the fake keeps one state per message for all consumers, which is written down where it matters.

The seed picks pullBatchSize, pullFetchTimeout, ackWait, maxDeliver, nackWait, a durable or an ephemeral consumer and the fault rates, and injects fetches that end empty at their timeout while the connection is down, lost acks, flushes that time out during the stop, failed and slow processing (now and then longer than AckWait), route restarts (also while the consumer waits in a fetch) and server restarts.

Invariants: the consumer passes its ack policy, AckWait and MaxDeliver to the server and uses the connection as documented (it never closes the shared connection); a message is acked only after its exchange completed, and nak'd only after it failed; a stopped consumer leaves no subscription active, checked when the route starts again and after the final stop; once the faults cleared every message is acked, or given up after MaxDeliver, and a message published then is acked (so a fetch loop that ended is caught). One case is accepted: when an ack was lost and the server then deleted the ephemeral consumer, the consumer subscribes again after that message, as it counts it as acknowledged.

Limits, written in the world's Javadoc: the operator stops the route only while no exchange is in it, so a stop between two messages of a fetched batch is not driven; the harness interrupts no thread, so the interrupt part of CAMEL-26884 is not checked (its flush and subscribe parts are); the route starts again 100 ms or more after a stop, so a start that comes while the stopped consumer task still subscribes is not driven. Not explored: push subscriptions and core NATS subjects (their dispatchers deliver on jnats threads), manualAck, AckAll and AckNone, a connection created by the consumer itself.

Other changes:

  • camel-test-dst and mockito-core as test dependencies of camel-nats, and the byte code agent of camel-test-dst on the surefire command line, as for the other worlds.
  • The test log of camel-nats goes to a size-capped rolling file (50 MB, one backup), as a failing world can log a lot.
  • The @DstWorld annotation lists the world in the component JSON (and its catalog copy) and in the table of the camel-test-dst page (and its catalog copy, byte-identical).

Tests:

  • On main: 5000 of 5000 seeds pass (about 9 ms a seed). Seeds 8 and 77 replayed in two JVMs give the same trace hashes.
  • Self-check against a fixed bug: on the code before CAMEL-26884 (6bc46f9 reverted) 346 of 2000 seeds fail, all on "a stopped consumer leaves no subscription active".
  • After the review follow-up (the fetch holds a partial batch, AckWait from availability, the end message, the disconnect fault, Server Shutdown or Consumer Deleted): 5000 of 5000 seeds pass on main, and the self-check fails 339 of 2000 seeds, all on the same invariant. CAMEL-26462 and CAMEL-25847 from the JIRA's list of candidates are fixes of the push consumer and its dispatcher, which this world does not drive, so they are not used.
  • The open PR for CAMEL-26915 (camel-nats-consumer-run-token) also changes NatsConsumer: this branch merges with it without a conflict, and no invariant of the world depends on it. Merged locally with that branch, the world passes 2000 of 2000 seeds.
  • camel-nats unit tests: 1044 run (44, plus 1000 seeds), 0 failures, 0 errors, 0 skipped.

What was not run locally: the ITs (they need Docker) and the full root build. I built and tested offline with the versions cached on my machine: commons-lang3 3.21.0 and Guava 33.7.2 (main has 3.22.0 and 33.8.0-jre).

Target

  • I checked that the commit is targeting the correct branch (Camel 4 uses the main branch)

Tracking

  • If this is a large change, bug fix, or code improvement, I checked there is a JIRA issue filed for the change (usually before you start working on it): CAMEL-26957.

Apache Camel coding standards and style

  • I checked that each commit in the pull request has a meaningful subject line and body.
  • I have run mvn clean install -DskipTests locally from root folder and I have committed all auto-generated changes.
    (I built and tested camel-test-dst and camel-nats, including the formatter and import-sort plugins. The generated files touched are the component JSON with its catalog copy, and the table of the camel-test-dst page with its catalog copy (byte-identical). I did not run the full root build.)

AI-assisted contributions

  • If this PR includes AI-generated code, commits have proper co-authorship attribution (e.g., Co-authored-by trailers) and the PR description identifies the AI tool used.
    This PR was prepared with Claude Code (Claude Opus 5.5). The commit carries a Co-Authored-By trailer.

Claude Code on behalf of allthingssecurity

🤖 Generated with Claude Code

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @allthingssecurity, there is a lot to like: the jnats proxies record any method they do not model as a violation, subscribe checks that the ack policy, AckWait and MaxDeliver reach the server, both paths for a deleted consumer are there (the 409 Consumer Deleted and the empty fetch followed by getConsumerInfo), the lost-ack excuse is written down, and there is a self-check against CAMEL-26884. Test-only, test-scoped deps, no Thread.sleep, and the catalog copies are identical.

One change I'd like, about how faithful the fake is:

  1. The fetch returns on the first message instead of holding a partial batch. The [P] Javadoc (FakeJetStream.java:71-72) says the fetch "waits up to the timeout for up to batch messages and returns what it got", which is what jnats does, but the wait at FakeJetStream.java:178 ends as soon as one message is available. In jnats 2.26.4, NatsJetStreamPullSubscription._fetch sends one pull with expiresIn and loops while batchLeft > 0 && timeLeft > 0, so a partial batch comes back only at expiry, when the batch is full, or on a terminus status. The server sends each message as soon as it is available, so its AckWait already runs during that hold, while the fake starts AckWait only when the fetch returns (FakeJetStream.java:198). With the default pullBatchSize=10 this is the common path, and seeds with pullFetchTimeout=5000 and ackWait=5000 are where AckWait runs out before the route sees the message on a real server. A sketch:
    // [P] jnats holds a partial batch until the pull expires; the server sends each message when it is
    // available, so its AckWait runs from then
    long fetchStart = dst.now();
    dst.awaitUntil(() -> !active || deleted || availableCount(startSeq) >= batch, Math.max(1, timeout), name + " fetch");
    ...
    m.redeliverAt = Math.max(fetchStart, m.availableSince) + ackWaitMillis;
    with availableSince set at publish, release and the end of a nak delay. After that the seeds and the CAMEL-26884 self-check need a re-run, and any new failing seed triaged (fake or consumer).

Smaller points, optional:

  1. No message published once the faults cleared. PubsubWorld (#29996) publishes an END message then and requires it to be acked, which catches a pull loop that ended. Here, if the fetch loop exits after the last message was acked (e.g. the IllegalStateException return at NatsConsumer.java:494-497), the world still passes. One more nats.publish(...) in the -2 step of the finisher (NatsWorld.java:226-232), required to end ACKED, would close that gap.
  2. The injected fetch failure is an IllegalArgumentException (FakeJetStream.java:173-177), which jnats does not throw for a valid fetch. During a disconnect the pull is buffered and the fetch ends empty at its timeout; a 409 that PullMessageManager.manageStatus reports as an error comes out as a JetStreamStatusException, which would also reach the else branch at NatsConsumer.java:484-490 that no fault drives today.
  3. The server restart. In jnats 2.26.4 a 409 "Server Shutdown" is handled as a warning and the fetch keeps waiting, so on a real restart a waiting fetch ends empty at its timeout; 409 Consumer Deleted comes when the consumer is deleted while connected. If the seed picks between the two at FakeJetStream.java:183-185, the getConsumerInfo path is also reached from a waiting fetch.
  4. Follow-up idea, not for this PR: #29998 (CAMEL-26915) fixes a stop and start of the same consumer while its task is still subscribing. This world only stops the route, which makes a new consumer each time, so it cannot reach that bug. Once #29998 is merged, an operator step that stops and starts the route's consumer (ServiceHelper.stopService + startService, sometimes right after a start) would let the world fail on the code before #29998.
  5. Nit: the last invariant in @DstWorld (NatsWorld.java:80-81) ends with "(or skipped by a consumer made again after its ack was lost)", but the dst.check name (NatsWorld.java:174) does not. The guide wants the same names.

Note: GitHub has not started any CI run on this PR yet; it needs one before merge.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

@davsclaus
davsclaus force-pushed the camel-nats-dst-world-pr branch from 828c0df to d57b040 Compare November 15, 2026 07:25
@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions

github-actions Bot commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • catalog/camel-catalog
  • components/camel-nats
  • components/camel-test/camel-test-dst

🔁 1 test passed only after a retry on JDK 21 (1 retried attempt)

Recovered flaky tests on JDK 21 (1)
Module Test Failed attempts First failure
dsl/camel-jbang/camel-jbang-plugin-tui ArchitectureViewTest.followsAChangedSummaryWithoutAKey 1 Cannot read field "totalHeight" because "this.layout" is null

ℹ️ These tests did not fail the build. Surefire retried them and they passed.
Retries are enabled project-wide by surefire.rerunFailingTestsCount in parent/pom.xml.


🔬 Scalpel shadow comparison — Scalpel: 20 of 726 tested, 3 compile-only — current: 20 all tested

Maveniverse Scalpel detected 20 affected modules (current approach: 20).

Skip-tests mode would test 20 modules (3 direct + 0 downstream), skip tests for 3 (generated code, meta-modules)

Modules Scalpel would test (20)
  • camel-aws2-eventbridge ← depends on affected reactor module org.apache.camel:camel-aws2-sqs
  • camel-aws2-sqs ← depends on affected reactor module org.apache.camel:camel-test-dst
  • camel-azure-eventhubs ← depends on affected reactor module org.apache.camel:camel-test-dst
  • camel-chaos ← depends on affected reactor module org.apache.camel:camel-test-dst
  • camel-cli-connector ← depends on affected reactor module org.apache.camel:camel-chaos
  • camel-cli-debug ← depends on affected reactor module org.apache.camel:camel-cli-connector
  • camel-google-pubsub ← depends on affected reactor module org.apache.camel:camel-test-dst
  • camel-jbang-mcp ← depends on affected reactor module org.apache.camel:camel-chaos
  • camel-jbang-plugin-mcp ← depends on affected reactor module org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-tui ← depends on affected reactor module org.apache.camel:camel-chaos
  • camel-jbang-plugin-validate ← depends on affected reactor module org.apache.camel:camel-jbang-core
  • camel-jbang-run ← depends on affected reactor module org.apache.camel:camel-catalog
  • camel-kafka-share ← depends on affected reactor module org.apache.camel:camel-test-dst
  • camel-launcher-container ← depends on affected reactor module org.apache.camel:camel-launcher
  • camel-nats ← components/camel-nats/src/generated/resources/META-INF/org/apache/camel/component/nats/nats.json, components/camel-nats/src/test/java/org/apache/camel/component/nats/dst/FakeJetStream.java, components/camel-nats/src/test/java/org/apache/camel/component/nats/dst/NatsConsumerDstTest.java, components/camel-nats/src/test/java/org/apache/camel/component/nats/dst/NatsWorld.java, components/camel-nats/src/test/resources/log4j2.properties, own pom components/camel-nats/pom.xml changed
  • camel-pulsar ← depends on affected reactor module org.apache.camel:camel-test-dst
  • camel-slack ← depends on affected reactor module org.apache.camel:camel-test-dst
  • camel-test-dst ← components/camel-test/camel-test-dst/src/main/docs/test-dst.adoc
  • camel-yaml-dsl-validator ← depends on affected reactor module org.apache.camel:camel-catalog
  • camel-yaml-dsl-validator-maven-plugin ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
Modules with tests skipped (3)
  • camel-itest
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

All tested modules (47 modules, 9m 10s total)

Total reactor time: 9m 10s

Module Duration Status
Camel :: DSL :: CLI Connector 60.0s SUCCESS
Camel :: JBang :: Plugin :: TUI 49.6s SUCCESS
Camel :: Kafka :: Share 48.0s SUCCESS
Camel :: JBang :: MCP 46.9s SUCCESS
Camel :: Component DSL 42.8s SUCCESS
Camel :: Slack 42.4s SUCCESS
Camel :: Docs 29.5s SUCCESS
Camel :: JBang :: Run 26.2s SUCCESS
Camel :: YAML DSL :: Validator 25.4s SUCCESS
Camel :: JBang :: Plugin :: Validate 23.4s SUCCESS
Camel :: YAML DSL 19.0s SUCCESS
Camel :: Catalog :: Camel Catalog 16.9s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 16.0s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 13.9s SUCCESS
Camel :: JBang :: Plugin :: Testing 11.9s SUCCESS
Camel :: AWS2 Eventbridge 11.5s SUCCESS
Camel :: Kamelet Main 10.1s SUCCESS
Camel :: Test :: DST 9.0s SUCCESS
Camel :: JBang :: Plugin :: Generate 6.8s SUCCESS
Camel :: Catalog :: Maven 5.5s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 5.2s SUCCESS
Camel :: All Components Sync point 5.2s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 4.0s SUCCESS
Camel :: YAML DSL :: Deserializers 3.8s SUCCESS
Camel :: Catalog :: Camel Route Parser 3.4s SUCCESS
Camel :: Coverage 2.1s SUCCESS
Camel :: JBang :: Plugin :: MCP 1.6s SUCCESS
Camel :: YAML DSL :: Maven Plugins 1.5s SUCCESS
Camel :: Endpoint DSL :: Support 1.4s SUCCESS
Camel :: Assembly 1.3s SUCCESS
Camel :: DSL :: CLI Debug 1.2s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.1s SUCCESS
Camel :: JBang :: Integration tests 0.9s SUCCESS
Camel :: Launcher :: Container 0.9s SUCCESS
Camel :: JBang :: Main 0.8s SUCCESS
Camel :: Catalog :: Dummy Component 0.7s SUCCESS
Camel :: Catalog :: Console 0.4s SUCCESS
Camel :: AWS2 SQS n/a
Camel :: Azure :: Event Hubs n/a
Camel :: Chaos n/a
Camel :: Endpoint DSL n/a
Camel :: Google :: PubSub n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: Launcher n/a
Camel :: Nats n/a
Camel :: Pulsar n/a

Top 20 slowest modules:

  • Camel :: DSL :: CLI Connector (60.0s)
  • Camel :: JBang :: Plugin :: TUI (49.6s)
  • Camel :: Kafka :: Share (48.0s)
  • Camel :: JBang :: MCP (46.9s)
  • Camel :: Component DSL (42.8s)
  • Camel :: Slack (42.4s)
  • Camel :: Docs (29.5s)
  • Camel :: JBang :: Run (26.2s)
  • Camel :: YAML DSL :: Validator (25.4s)
  • Camel :: JBang :: Plugin :: Validate (23.4s)
  • Camel :: YAML DSL (19.0s)
  • Camel :: Catalog :: Camel Catalog (16.9s)
  • Camel :: Catalog :: Camel Report Maven Plugin (16.0s)
  • Camel :: JBang :: Plugin :: Kubernetes (13.9s)
  • Camel :: JBang :: Plugin :: Testing (11.9s)
  • Camel :: AWS2 Eventbridge (11.5s)
  • Camel :: Kamelet Main (10.1s)
  • Camel :: Test :: DST (9.0s)
  • Camel :: JBang :: Plugin :: Generate (6.8s)
  • Camel :: Catalog :: Maven (5.5s)

⚙️ View full build and test results

@allthingssecurity

Copy link
Copy Markdown
Contributor Author

Thanks @davsclaus. Pushed 80a2391 on top of your rebase:

  1. Fetch holds a partial batch: done along your sketch. I checked it against jnats 2.26.4 (javap -c of NatsJetStreamPullSubscription._fetch and PullMessageManager.manageStatus): one pull with expiresIn, a loop while batchLeft > 0 && timeLeft > 0, a terminus status returns what it has, an error status throws JetStreamStatusException. The fake fetch now waits until the timeout, availableCount(startSeq) >= batch, the consumer is deleted or the subscription is inactive, and sets redeliverAt = Math.max(fetchStart, m.availableSince) + ackWait. availableSince is set at publish, when AckWait or a nak releases a message (the end of the nak delay for a delayed nak), and when a new ephemeral consumer makes a message available again. The [P] Javadoc and the timing paragraph say this.
    Re-run: 5000 of 5000 seeds pass on main (no new failing seed to triage). Self-check with 6bc46f9 (CAMEL-26884) reverted: 339 of 2000 seeds fail, all on "a stopped consumer leaves no subscription active" (346 before; the seed stream changed with the new draws).
  2. Message after the faults cleared: done. The -2 step of the finisher publishes an end message, and the world passes only if it ends ACKED, so a fetch loop that ended is caught.
  3. Realistic fetch failure: the injected fault is now a fetch while the connection is down: no message comes and the fetch ends empty at its timeout. I did not add a JetStreamStatusException fault. In 2.26.4 the only 409s that manageStatus reports as errors are Consumer Deleted, which the deletion already drives, and "Consumer is push based", which needs the consumer reconfigured as push. So the else branch of that catch stays undriven, and IllegalArgumentException is gone.
  4. Server Shutdown vs Consumer Deleted: done. When the ephemeral consumers are deleted, the seed picks per event whether a waiting fetch sees 409 Server Shutdown (a warning: it keeps waiting and returns at its timeout the messages it got before) or 409 Consumer Deleted (the fetch throws). With Server Shutdown the next fetch gets the 503 and the consumer reaches getConsumerInfo from there.
  5. Agreed as a follow-up: once CAMEL-26915: camel-nats - each start of the consumer has its own run, so the task of a stopped start does not take over the next one #29998 is merged, an operator step that stops and starts the route's consumer itself (ServiceHelper.stopService + startService, sometimes right after a start) would let the world fail on the code before CAMEL-26915: camel-nats - each start of the consumer has its own run, so the task of a stopped start does not take over the next one #29998.
  6. Invariant name: done; the dst.check name now ends with "(or skipped by a consumer made again after its ack was lost)", as in @DstWorld.

The faults list of @DstWorld changed with 3 and 4, so the component JSON and its catalog copy are regenerated (byte-identical); the doc table only lists covers and issues, which did not change.

Claude Code on behalf of allthingssecurity

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-constructed DST world for the NATS JetStream pull consumer. The fake models real NATS semantics faithfully.

Items from davsclaus's review — all confirmed addressed:

  1. fetch() awaitUntil — All three awaitUntil calls include !active as a termination condition; the full-batch wait at line 271 uses !active || deleted || availableCount(startSeq) >= batch, correctly holding a partial batch until expiry or completion, with redeliverAt anchored to availableSince.
  2. ack() state handling — ack() accepts PENDING and AVAILABLE (lines 482-483); double-ack (already ACKED) is silently ignored, consistent with the comment "an ack of an older delivery is accepted." Intentional design, documented.
  3. nak() stale-delivery guard — nak() guards with m.state == State.PENDING && m.deliveries == number (lines 490-492), preventing stale naks from resetting the current delivery. The asymmetry with ack() is correct per NATS semantics.
  4. subscribe() PENDING reset race — subscribe() resets PENDING→AVAILABLE (lines 589-590); stopWhenIdle() checks inRoute > 0 and reschedules if an exchange is in-flight (lines 960-966), preventing premature reset.
  5. restartServer() 409 paths — Both paths confirmed: deleted && serverShutdown → warning + continue waiting; deleted alone → JetStreamStatusException(409, CONSUMER_DELETED) (lines 276-290).
  6. settled() ack-lost + new consumer — Returns true for ACKED/GIVEN_UP, or ack-lost where allMatch(s -> s.startSeq > m.seq) && anyMatch(s -> s.active && !s.deleted) (lines 416-421); the anyMatch guard prevents vacuous truth on an empty stream.

Test-only PR, test-scoped dependencies, no Thread.sleep, pom.xml vmargs use the standard byte-buddy-agent pattern.

Claude Code on behalf of gnodet. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @allthingssecurity, 80a2391 covers every point. I read it against my review:

  1. The fetch now holds a partial batch: awaitUntil(... availableCount(startSeq) >= batch, wait, ...), and take sets redeliverAt = Math.max(fetchStart, m.availableSince) + ackWaitMillis. availableSince is set at publish, in giveUpOrRelease (the end of AckWait, and of a nak delay, since nak sets redeliverAt before it releases), and when a new ephemeral consumer makes a message available again. So AckWait can run out while the fetch holds a message, as on a real server.
  2. The end message is published in the -2 step, and the finisher and the invariant require it to end ACKED, so a fetch loop that ended now fails the seed.
  3. The fetch fault is a fetch that ends empty at its timeout while the connection is down. Keeping the else branch of the catch undriven, rather than inventing a 409 that 2.26.4 does not report as an error, is the right call.
  4. restartServer picks Server Shutdown (a warning: it keeps waiting to the timeout and returns what it took) or Consumer Deleted (it throws), and the 503 on the next fetch reaches getConsumerInfo from both.
  5. Noted as a follow-up after #29998 (CAMEL-26915: stop and start of the same consumer).
  6. The invariant names now match.

The re-run (5000 of 5000 seeds pass on main, 339 of 2000 fail on the CAMEL-26884 self-check) is the evidence I hoped for. Looks good to me.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

allthingssecurity and others added 2 commits November 15, 2026 14:32
…sumer

Adds a camel-test-dst world for the camel-nats JetStream pull consumer, in
the dst package of its tests: NatsConsumerDstTest (1000 seeds), NatsWorld
and FakeJetStream. The real consumer runs against a fake NATS server with
one JetStream stream, given as a shared jnats Connection with the connection
option, written from the NATS documentation and the jnats client: pull
fetches that wait up to their timeout for up to a batch, explicit ack, nak
and nak with a delay, AckWait redelivery, MaxDeliver, ephemeral and durable
consumers, a server restart that deletes the ephemeral consumers (409
Consumer Deleted for a waiting pull, an empty fetch and no consumer info
afterwards), a flush that times out, and acks that are lost.

The seed picks pullBatchSize, pullFetchTimeout, ackWait, maxDeliver,
nackWait, a durable or an ephemeral consumer and the fault rates, and
injects failed fetches, lost acks, flushes that time out, failed and slow
processing (now and then longer than AckWait), route restarts (also while
the consumer waits in a fetch) and server restarts.

Invariants: the consumer passes its ack policy, AckWait and MaxDeliver to
the server and uses the connection as documented; a message is acked only
after its exchange completed and nak'd only after it failed; a stopped
consumer leaves no subscription active; once the faults cleared every
message is acked, or given up after MaxDeliver (or skipped by a consumer
made again after its ack was lost).

Not explored: push subscriptions and core NATS subjects, a stop between two
messages of a fetched batch, the interrupt of the consumer thread by the
stop, and a start that comes while the stopped consumer task still
subscribes.

Self-check: on the code before CAMEL-26884 (6bc46f9 reverted) 346 of
2000 seeds fail; main passes 5000 of 5000 seeds.

The module's pom loads the byte code agent of camel-test-dst, the test log
is capped, and the catalog lists the world.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h, AckWait from availability

Review of davsclaus:
- As jnats 2.26.4 NatsJetStreamPullSubscription._fetch: the fake fetch
  holds a partial batch until the pull expires, the batch is full or a
  status ends it, and the AckWait of a message runs from when it became
  available (published, released, end of a nak delay), or from the start
  of the fetch if later, as the server sends it then.
- A message published once the faults cleared must end acknowledged, so
  a fetch loop that ended is caught.
- The injected fetch fault is a fetch that ends empty at its timeout
  while the connection is down, instead of an IllegalArgumentException.
- The seed picks whether a waiting fetch sees 409 Server Shutdown (a
  warning: it keeps waiting and returns what it got at its timeout) or
  409 Consumer Deleted when the ephemeral consumers are deleted.
- The last invariant name of dst.check matches @DstWorld.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@davsclaus
davsclaus force-pushed the camel-nats-dst-world-pr branch from 80a2391 to 527beec Compare November 15, 2026 13:32
@davsclaus davsclaus added this to the 4.24.0 milestone Nov 15, 2026
@davsclaus davsclaus added the test label Nov 15, 2026
@davsclaus
davsclaus merged commit bb38302 into apache:main Nov 15, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants