[
https://issues.apache.org/jira/browse/CAMEL-26901?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Work on CAMEL-26901 started by Claus Ibsen.
-------------------------------------------
> camel-jbang - camel run: dependency resolution mixes library versions on the
> classpath, ignores failed downloads, and downloads jars already on the
> classpath
> -------------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-26901
> URL: https://issues.apache.org/jira/browse/CAMEL-26901
> Project: Camel
> Issue Type: Improvement
> Components: camel-jbang
> Reporter: Claus Ibsen
> Assignee: Claus Ibsen
> Priority: Major
> Fix For: 4.24.0
>
>
> An analysis of how camel run (the jbang runtime: KameletMain +
> MavenDependencyDownloader + camel-tooling-maven MavenDownloaderImpl) resolves
> and downloads dependencies. A warm run is already fast; the problems are on
> cold runs and on failures, and the most important one is a correctness issue:
> dependencies resolved one by one end up on the classpath in several versions
> at once.
> h3. Measurements (4.24.0-SNAPSHOT CLI, macOS, small YAML route with groovy,
> jsonpath and mustache)
> ||Scenario||Camel boot||Wall time||Notes||
> |Warm ~/.m2, via jbang|0.5s|2.6s|resolution 87 + 33 + 5 ms|
> |Warm, camel-launcher distribution|0.6s|2.5s|jbang adds almost nothing|
> |Warm, plus 10 --dep components|1.7s| |resolution about 40 ms per component|
> |Warm, unreachable repo in --repos|0.5s|2.6s|a warm run never contacts the
> remote repo|
> |Cold (empty local repo)|8-9.5s|10-11.5s|90 downloads, 19 MB of jars|
> |Cold, one missing dep with an unreachable repo|20.8s| |two 10s connect
> timeouts|
> h3. 1. The same library on the classpath in several versions (verified, high)
> Each dependency is resolved as its own graph, and {{alreadyOnClasspath}}
> matches on the exact version ({{MavenDependencyDownloader}} around line 350),
> so a different version of a library that is already loaded is still added to
> the {{DependencyDownloaderClassLoader}}.
> A run with
> {{--dep=camel:kafka,camel:aws2-s3,camel:sql,camel:http,camel:jms,camel:mongodb,camel:google-pubsub,camel:azure-storage-blob,camel:elasticsearch,camel:infinispan}}
> added:
> * netty in 3 versions (4.1.127, 4.1.138, 4.1.139)
> * httpclient5 in 3 versions (5.6.3, 5.6.4, 5.6.5)
> * jackson-core in 3 versions (2.18.2, 2.22.2, 2.22.3)
> * opentelemetry-api/context/common in 2 versions (1.62.0, 1.67.0)
> URLClassLoader finds each class in the first jar that has it, so a class
> missing from one version is loaded from another: NoSuchMethodError /
> IncompatibleClassChangeError depending on the order components are
> discovered. camel export lets Maven pick one version per library, so camel
> run and the exported project can have different classpaths.
> Step 1 (cheap): when a library is already loaded in another version, do not
> add the other version; log it (verbose). This does not change which version
> wins today, it only stops the mixing.
> Step 2: see 6 (resolve the known dependencies as one graph).
> h3. 2. A failed download is logged and the run continues (verified, high)
> {{DownloadThreadPool.awaitCompletion}} (line 84) logs an ERROR with the full
> stack trace and returns. {{--dep=com.example.nope:doesnotexist:1.0}} printed
> about 40 lines of stack trace, then started the routes and exited 0.
> For dependencies the user named ({{--dep}}, {{//DEPS}},
> {{camel.jbang.dependencies}}) the run should stop with a one-line message
> (the artifact and the repositories tried) and a non-zero exit code. Export
> mode may keep the lenient behaviour on purpose. For automatically detected
> components, a short message instead of the stack trace.
> h3. 3. Remote SNAPSHOTs put core jars on the classpath again (verified,
> nightly CLI users)
> {{MavenDownloaderImpl.toMavenArtifact}} uses {{Artifact.getVersion()}}, which
> for a remote snapshot is the timestamped version
> ({{4.24.0-20261112.143700-31}}). It never matches
> {{camel-api-4.24.0-SNAPSHOT.jar}} on the boot classpath (nor the URL check,
> as the file lives in the {{4.24.0-SNAPSHOT}} directory), so the cold run
> added camel-api, camel-support, camel-util, camel-core-model, ... to the
> download classloader, some of them 3 times. Fix: use {{getBaseVersion()}}.
> h3. 4. Cold runs download jars that are already on the boot classpath
> (verified, high cold-start cost)
> 9.0 of the 18.8 MB of jars (48%) in the cold run were camel-api,
> camel-support, camel-core-model, camel-core-processor, camel-base-engine,
> jackson 3, slf4j-api, jakarta.xml.bind-api, ... They are downloaded (with
> their POMs and snapshot metadata round trips) and only filtered out after the
> fact by {{alreadyOnClasspath}}.
> Fix: leave out what is already on the boot classpath during resolution,
> matching on groupId:artifactId only (a DependencySelector / filter in the
> collect request). Classes load parent-first, so the boot version wins today
> anyway.
> h3. 5. Timeouts and unreachable repositories (verified, medium)
> Maven resolver 1.9.27 defaults: connect timeout 10s, request timeout *30
> minutes* ({{DEFAULT_REQUEST_TIMEOUT = 1800000}}). Camel only sets them in
> {{MavenDownloaderImpl.customize()}}, not for camel run.
> * A mirror that accepts the connection and then stalls hangs camel run for up
> to 30 minutes, printing "Downloading: ... (elapsed: ...)" every 5s.
> * An unreachable repository costs 10s per request (POM, then jar), and it is
> not remembered for the rest of the run.
> Fix: a sane request timeout (60-120s), and skip a repository for the rest of
> the run after its first connect failure.
> h3. 6. One dependency at a time, synchronously (verified, medium on cold runs)
> {{DownloadThreadPool.download}} submits the task and waits for it; the thread
> pool only serves the progress log. Cold run: groovy 5.6s, then jsonpath 2.3s,
> then mustache 0.4s, one after another.
> Dependencies known up front ({{--dep}}, {{//DEPS}}, properties) could be
> resolved in a single request: one graph with one version per library (fixes 1
> for them) and parallel downloads. {{downloadDependencyWithParent}}
> (CAMEL-22100) is a natural place for this.
> h3. 7. Smaller items (from reading the code)
> * *Cross-process locking*: the resolver uses {{rwlock-local}} named locks,
> which only coordinate within one JVM. Several cold camel run processes (CI,
> several integrations at once) are not coordinated through them; Maven
> recommends {{file-lock}} for a shared local repository. Not reproduced.
> * *Package scan per jar*:
> {{BasePackageScanDownloadListener.onDownloadedFile}} re-runs the full base
> package scan for every downloaded jar when basePackageScan is set; once per
> resolution is enough.
> * *Misleading log lines*: "Downloaded: ... (took: 0ms)" as the elapsed time
> is hard-coded to {{0L}} in {{MavenDownloaderImpl}}; the POM and the jar are
> both reported as "Downloaded" without the extension; the repository listener
> does not check whether the event carries an error.
> * *Thread safety*: {{downloadRecords}} is a HashMap written from the download
> threads; on interrupt the future is not cancelled.
> h3. Not worth doing
> * Caching the resolved classpath for warm runs: warm resolution is about 0.5s
> for 13 components.
> * Switching to the breadth-first dependency collector:
> {{-Daether.dependencyCollector.impl=bf}} measured 9.8-10.4s against
> 10.7-11.6s for the default, within network noise.
> * jbang vs the launcher: about 0.1s apart.
> h3. Suggested order
> # 3 (one-liner) and 2 (fail fast for explicit dependencies)
> # 1 step 1 (one version per library) together with 4 (leave out boot
> classpath jars during resolution)
> # 5 (timeouts, unreachable repositories)
> # 6 (batch the known dependencies), then measure the cold start again
> _Claude Code on behalf of Claus Ibsen (davsclaus)_
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)