shashank created CAMEL-25680:
--------------------------------

             Summary: camel-docker - the events and stats consumers never close 
their stream (a restarted route gets every event twice), and the events 
consumer stays started without a stream after the Docker daemon restarts
                 Key: CAMEL-25680
                 URL: https://issues.apache.org/jira/browse/CAMEL-25680
             Project: Camel
          Issue Type: Bug
          Components: camel-docker
            Reporter: shashank
            Assignee: shashank


The {{docker:events}} and {{docker:stats}} consumers start a streaming command 
with {{exec(callback)}}. docker-java reads the stream on its own thread and the 
stream belongs to the callback ({{ResultCallbackTemplate.onStart(Closeable)}} 
keeps it, {{close()}} closes it).

h3. 1. The stream is never closed

{{DockerEventsConsumer.doStop()}} and {{DockerStatsConsumer.doStop()}} (main 
cd2ce899764e, line 80 and 70) call {{close()}} on the command only. In 
docker-java 3.7.1 {{AbstrDockerCmd.close()}} does nothing (read with javap: the 
method body is {{return}}), and the callback is not kept anywhere. So after a 
stop:
* the HTTP connection of the stream stays open, one per stop (a leak on every 
route stop or context reload);
* the old callback keeps calling {{getAsyncProcessor().process(...)}} for every 
event it receives. When the route is started again, the new consumer opens a 
second stream, and every Docker event is routed twice; after n restarts, n + 1 
times.

Trigger: default options, any {{stopRoute}} / {{startRoute}} of a route that 
consumes from {{docker:events}} (route controller, a supervising route 
controller restarting a route, a route reload).

h3. 2. A stream that fails or ends is not opened again

{{DockerEventsConsumer.EventsCallback}} (85-99) implements only {{onNext}}. 
When the stream fails (the daemon restarts, the connection is lost) 
docker-java's {{ResultCallbackTemplate.onError}} logs one ERROR ("Error during 
callback") and closes it; when the daemon ends the stream ({{onComplete}}) 
nothing is logged. The consumer stays started and never receives an event 
again, until the route is restarted. The same happens when the daemon cannot be 
reached when the route starts.

h3. Reproduction

New {{DockerEventsConsumerStreamTest}} (mocked {{DockerClient}} that captures 
the callbacks; the test calls {{onStart}} / {{onNext}} / {{onError}} / 
{{onComplete}} itself). On main:
* stop and start the route, then the daemon sends one event to each open 
stream: {{mock://result Received message count. Expected: <1> but was: <2>}};
* the stream opens only after the route was stopped: {{the stream opened after 
the stop was not closed ==> expected: <1> but was: <0>}};
* {{docker:stats}}: {{stopping the route did not close the stats stream ==> 
expected: <1> but was: <0>}};
* {{onError(IOException)}} on the open stream, and {{onComplete()}}: no new 
{{eventsCmd().exec(...)}} within 10 seconds ({{ConditionTimeout}}).

h3. Proposed fix

* Both consumers keep their callback, close it on stop (the callback also 
closes a stream that only opens after the stop, as {{onStart}} reopens a closed 
template), and a closed callback routes nothing.
* The events consumer handles {{onError}} and {{onComplete}}: it subscribes 
again after 1 second, then with a delay that doubles up to 30 seconds while 
subscribing fails (the same delays as the kubernetes consumers' 
{{KubernetesHelper.watchAgain}}). A failed open stream is logged once with its 
stack trace (WARN), a failed attempt to subscribe again as a WARN line without 
stack trace, a clean end as a WARN line. The new stream starts with the events 
from then on: events of the time without a stream are not received (as with the 
kubernetes watches). The delay runs on a scheduled executor of the consumer, 
shut down on stop.

Upgrade guide entry (4.24): the events consumer subscribes again when its 
stream fails or ends; stopping a consumer closes its stream.

camel-docker unit tests with the fix: 54 run, 0 failures, 4 skipped (the 
integration tests need a daemon). Not run: a real Docker daemon (the module's 
integration tests), a daemon restart.

How it was found: a sweep of the reconnect code of the components (onError / 
onComplete missing); while writing the test, the no-op {{close()}} of the 
command.

Not changed (follow-ups): the stats stream also ends when the container stops 
and is not opened again (subscribing again to a stopped container would fail 
every time, so it needs its own rule); 
{{withSince(String.valueOf(System.currentTimeMillis()))}} passes milliseconds 
where the Docker API expects seconds, so the {{CamelDockerInitialRange}} option 
likely never replays past events (read, not checked against a daemon).

Affected: main (cd2ce899764e; tested). camel-4.23.x, camel-4.22.x, camel-4.18.x 
and camel-4.14.x have the same {{doStop}} (only {{eventsCmd.close()}}) and an 
{{EventsCallback}} with only {{onNext}} (read, not run).

Duplicate check (2026-10-15): JIRA component camel-docker with text "events" 
(CAMEL-7834 creation 2014, CAMEL-9671 API 1.22, CAMEL-10304 OSGi, CAMEL-10307 
upgrade), text "DockerEventsConsumer" (nothing); GitHub pull requests 
"DockerEventsConsumer", "DockerStatsConsumer", "camel-docker events" (nothing 
related).

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to