shashank created CAMEL-26310:
--------------------------------

             Summary: camel-core - a component created on first use can be used 
by another thread before it has started (endpoints created on a component that 
is still starting)
                 Key: CAMEL-26310
                 URL: https://issues.apache.org/jira/browse/CAMEL-26310
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank
            Assignee: shashank


A component that is created on first use after CamelContext has started is 
started by the thread that created it, after {{components.computeIfAbsent}} has 
returned ({{AbstractCamelContext.getComponent}}, camel-base-engine, main 
c536c99caaaa). Another thread that asks for the same component meanwhile gets 
it from the map before it has started ({{created}} is false for that thread, so 
it does not start it), and {{doGetEndpoint}} goes on to 
{{component.createEndpoint(...)}} and starts the new endpoint on a component 
that is still starting (or not started at all yet).

A component that sets up in {{doStart}} what its endpoints use then fails for 
that thread. For example {{VertxHttpComponent.doStart}} creates its managed 
{{Vertx}}, and {{VertxHttpEndpoint.doStart}} creates its {{WebClient}} from 
{{getComponent().getVertx()}}: the second thread gets {{WebClient.create(null, 
...)}}. The first sends to a new scheme after startup are usually made from 
several threads at once (a ProducerTemplate in a web service, a toD, recipient 
list, routing slip or enrich with a scheme that is only known at runtime), so 
they fail intermittently with {{ResolveEndpointFailedException}} or a 
NullPointerException from the component.

CAMEL-14891 (3.3.0, toD) and CAMEL-20230 (4.4.0, enrich and pollEnrich) work 
around it by resolving the component of a uri with a fixed scheme when the 
route starts; the race in {{getComponent}} remains for the other paths (a 
{{ProducerTemplate}}, recipient list, routing slip, or a toD / enrich whose 
scheme is only known at runtime). It also happens while CamelContext is 
starting: the consumers of the routes that have already started can send while 
the later routes start.

Reproduced on main (latches, no sleeps): a component bound in the registry 
whose {{doStart}} waits on a latch and creates a client its {{createEndpoint}} 
uses; after context start, thread 1 calls {{context.getEndpoint("slow:a")}} 
(creates and starts the component, held in {{doStart}}), then thread 2 calls 
{{context.getEndpoint("slow:b")}}: thread 2 creates its endpoint while the 
component is {{Starting}} and fails ({{ResolveEndpointFailedException}} ... 
{{Cannot invoke "Object.hashCode()" because "this.client" is null}}). The same 
from a {{StartupListener.onCamelContextStarting}} (CamelContext starting).

*Fix*: when {{getComponent}} returns a component that another thread has just 
created and that has not started yet or is starting (CamelContext started or 
starting, {{autoStart}}), it starts it with {{ServiceHelper.startService}}: the 
service lock of the component makes it wait while the creating thread is in 
{{doStart}}, and it is a no-op once started. If the creating thread failed to 
start it, the waiting thread tries to start it once more and gets an exception 
or a started component. A component that has been stopped (such as by 
{{removeComponent}}) or that failed to start before is returned as it is, as 
before. The component is still started outside {{computeIfAbsent}} (the 
deadlock the comment there mentions).

*Tests*: new {{ComponentStartedBeforeUseTest}} (camel-core; latches, no sleeps; 
a component whose {{doStart}} waits on a latch and then creates the client its 
{{createEndpoint}} uses): {{testOtherThreadWaitsForTheComponentToStart}} (after 
CamelContext has started) and 
{{testOtherThreadWaitsForTheComponentToStartWhileCamelContextStarts}} (from 
{{StartupListener.onCamelContextStarting}}) fail on main 
({{ResolveEndpointFailedException}} caused by a null client) and pass with the 
fix; {{testStoppedComponentIsNotStartedAgain}} is a control that passes on 
both. camel-management component, endpoint and CamelContext tests: 94, 0 
failures.

*How it was found*: by reading the check-then-act uses of {{ConcurrentHashMap}} 
in camel-base-engine ({{computeIfAbsent}} with a side effect after it returns); 
reproduced on HotSpot with the test above.

*Affected*: main, camel-4.22.x, camel-4.18.x and camel-4.14.x have the same 
{{getComponent}} (read, not run).

_Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to