SOLR-18302: dependency-analysis (DAGP) — reporting + build hygiene#4612
SOLR-18302: dependency-analysis (DAGP) — reporting + build hygiene#4612serhiy-bzhezytskyy wants to merge 13 commits into
Conversation
Completes the dependency-analysis follow-up deferred from the Gradle 9 upgrade (SOLR-18289), which dropped the Gradle-9-incompatible ca.cutterslade.analyze plugin. Adds the Dependency Analysis Gradle Plugin (com.autonomousapps build-health) configured to REPORT (warn), not fail — its advice is guidance for human review, not enforcement. Applies only the uncontroversial advice: - removal of genuinely-unused dependency declarations - a few test-scope corrections (implementation -> testImplementation) Deliberately NOT included here (left for separate case-by-case review): - implementation -> api promotions (DAGP is aggressive here; per dev@ these need human judgement, as liberal api is viral/transitive) Also forces kotlin-metadata-jvm to match Solr's Kotlin so DAGP can analyze the :solr:ui (Compose/KMP) module. Lockfiles regenerated.
dsmiley
left a comment
There was a problem hiding this comment.
Thanks. I suppose for me to truly appreciate it, I'll need to run it and even intentionally make sloppy dependency choices and see how the plugin helps.
| @@ -0,0 +1,7 @@ | |||
| title: Add dependency-analysis (build-health) reporting to the build, and apply its uncontroversial advice (unused-dependency removals and test-scope corrections) | |||
| type: changed | |||
There was a problem hiding this comment.
Our documentation in 2 places define the meaning of the options for "type" -- and this is undoubtedly a "other" category change and not "changed".
There was a problem hiding this comment.
Fixed — changed to type: other. Thanks.
| - name: PR#4612 | ||
| url: https://github.com/apache/solr/pull/4612 |
There was a problem hiding this comment.
When we have JIRA issues, we favor the JIRA issue instead of PR.
There was a problem hiding this comment.
Done — created SOLR-18302 for this work and switched the link to the JIRA. (The PRs were previously mis-titled with SOLR-18296, which is actually the google-java-format issue; corrected now.)
There was a problem hiding this comment.
oooh, ok. You didn't need to file a JIRA issue over this matter but now that you did, it's fine.
| implementation libs.carrotsearch.hppc | ||
|
|
||
| implementation(libs.benmanes.caffeine) {transitive = false} | ||
| api(libs.benmanes.caffeine) {transitive = false} |
There was a problem hiding this comment.
You're right that this doesn't belong in this PR — I've reverted it (and the other external implementation → api changes) back to implementation here. This PR is now just the plugin wiring plus safe/mechanical advice; the scope promotions live in the draft PRs (#4613 for inter-module project(), #4614 for external libs) for exactly the case-by-case discussion you asked for.
One correction to my own earlier framing, in case it's useful: I checked these against the plugin's ABI dump, and caffeine genuinely is exposed in solr-core's public API today — CaffeineCache implements com.github.benmanes.caffeine.cache.RemovalListener, and onRemoval(..., RemovalCause) is a public method. So it isn't the tool being over-eager; the type really does leak through the public signature regardless of the declaration. The real question is whether we want solr-core's public API to expose caffeine, or whether that's accidental leakage worth encapsulating — which is the judgment call, and why these are split out rather than applied here.
There was a problem hiding this comment.
I'm not surprised that we have a number of dependencies like this that technically are exposed in our public API. But I have some decades here to judge that some of our dependencies are conceptually an internal / niche use and not widespread enough to warrant the "api" transitive implications.
Where I didn't comment, I implicitly concurred with your choice.
There was a problem hiding this comment.
Yeah, makes sense. These are in #4614 if it's ever worth revisiting which deserve an api contract. Thanks.
| exclude group: "org.apache.yetus", module: "audience-annotations" | ||
| }) | ||
| implementation(libs.apache.zookeeper.jute) { | ||
| api(libs.apache.zookeeper.jute) { |
There was a problem hiding this comment.
Oh? I thought this was only used for embedded ZK but maybe I'm wrong
There was a problem hiding this comment.
Reverted to implementation here (moved to #4614). For the record though, this one is heavily exposed: ~82 public methods across core declare throws org.apache.zookeeper.KeeperException, so ZooKeeper is genuinely part of the public API surface today, not just embedded-ZK internals. Whether that's desirable is #4614 discussion.
There was a problem hiding this comment.
My comment is respect to "jute", not ZK.
There was a problem hiding this comment.
Ah, my bad — misread that as ZK. Checked jute: the jar's a bit odd, it also ships org.apache.zookeeper.data.*, and core does expose Stat (getStat() returns it). So technically leaked, but agree it's niche. Left as implementation here anyway.
| runtimeOnly libs.dropwizard.metrics.core | ||
|
|
||
| implementation(libs.jayway.jsonpath, { | ||
| api(libs.jayway.jsonpath, { |
There was a problem hiding this comment.
Reverted to implementation here. The ABI reason it was flagged: public static Configuration jsonPathConfiguration() returns a com.jayway.jsonpath type — a real (if probably unintended) public exposure, so a good candidate for encapsulating rather than promoting. Parked in #4614.
| api libs.opentelemetry.api | ||
| implementation libs.opentelemetry.context | ||
| implementation(libs.opentelemetry.exporter.prometheus) { | ||
| api(libs.opentelemetry.exporter.prometheus) { |
There was a problem hiding this comment.
ehh... not sure why a solr-core user requires this?
There was a problem hiding this comment.
Agreed it's surprising — reverted here. It's flagged because FilterablePrometheusMetricReader extends io.opentelemetry.exporter.prometheus.PrometheusMetricReader (public class, public supertype). So the exposure is real, but "should a core consumer see it" is exactly the right question — #4614 territory, not this PR.
There was a problem hiding this comment.
I was hoping the new mechanism would be able to remove these super-obvious testImplementation declarations across our modules. One or two might even have a snide remark comment. of course the tests depend on the module they are testing!! It was a deficiency of the previous system that we needed to state the obvious.
There was a problem hiding this comment.
Agreed — and done. I removed testImplementation project(':solr:solrj-jetty') (the module test-depending on itself; the test source set already sees its own main classes) and the duplicate testImplementation project(':solr:solrj') (already api project(':solr:solrj')). Kept the configuration: 'testArtifacts' one since that provides solrj's test classes. Verified test compiles and all 59 tests pass. (In fairness to the tool: it flags these via transitive-usage analysis rather than a redundancy check, and solrj-jetty was the only genuinely-redundant case repo-wide — but it's exactly the kind of state-the-obvious declaration you meant, so glad to drop it.)
There was a problem hiding this comment.
I'd love to see all self testing testImplementation declarations removed; my comment is meant categorically, just just in this instance. If the tool isn't going to tell us about them all, hopefully at least it won't complain. The previous tool complained, which made me hate it. Any way, I can tend to this if you don't.
There was a problem hiding this comment.
Done — removed the self-referential testImplementation project(':self') in api, solrj, solrj-streaming, solrj-zookeeper, and core. Two still had the old // cutterslade requires we state the obvious comment, so those workarounds are gone with it.
There was a problem hiding this comment.
not critical but there was some project lazy-consensus on embracing kotlin for the gradle build. When I recently created a new build file, I found the syntax helpful to avoid problems due to groovy's untyped nature.
There was a problem hiding this comment.
Good to know, thanks. The build is currently all Groovy (solr/ui/build.gradle.kts is the only Kotlin build file, and that's because the UI is a KMP module), so I kept this consistent with the other gradle/validation/*.gradle scripts for now. Happy to write it in Kotlin DSL instead if that's the direction you'd prefer for new build files — just say the word.
There was a problem hiding this comment.
Done — converted it to .gradle.kts. buildHealth still runs and the suppressions all work.
…oss-dc-manager tests) DAGP flagged the kafka-clients test-classifier jar as an unused dependency, so the earlier commit removed it. That was a false positive: no test compiles against it, but EmbeddedKafkaCluster (from the kafka-streams test jar) loads org.apache.kafka.test.TestCondition from it at runtime, so the *IntegrationTest suites failed with ClassNotFoundException on CI. Re-add it as testRuntimeOnly (runtime-only is the accurate scope, vs the testImplementation it originally had) with a comment, and restore its license sha1. A concrete example of why DAGP runs report-not-fail and its advice needs case-by-case judgement.
|
Pushed a fix for the cross-dc-manager CI failure — and it's a nice illustration of why this PR runs DAGP in report-not-fail mode. DAGP flagged the Reproduced locally and both previously-failing suites ( |
…nts, fix changelog Responding to review feedback on the safe-advice PR: - Revert all external implementation->api promotions that had leaked in from the api-scope work (caffeine, zookeeper, jute, jsonpath, opentelemetry-sdk + exporter-prometheus in core; curator-client/jute in solrj-zookeeper; zookeeper +tests in test-framework; calcite in sql; cloud-storage in gcs; awssdk-s3 in s3). None are in the modules' public ABI and DAGP does not advise them; they belong in the separate api-scope discussion PRs, not here. Verified no downstream module relies on the former transitive leak. - Revert solrj-jetty entirely: its changes were api promotions + manual cleanups, none part of the safe mechanical advice. - Restore opentelemetry-sdk-metrics in test-framework: DAGP flagged it unused, but JettySolrRunner uses MetricReader from it and the prometheus exporter is declared transitive=false, so it is required (compile break otherwise). - Restore the gjf-SOLR-18296 changelog entry that had been deleted by a rebase artifact; it belongs to the separate google-java-format change. - Remove code comments that narrated the change/tool advice (per AGENTS.md); keep only terse why-comments matching existing style. - Changelog: type changed->other; link to SOLR-18302 (the correct JIRA). - Regenerate lockfiles/licenses.
Applies DAGP's api advice ONLY for inter-module project() dependencies that are genuinely part of the consuming module's public API (e.g. solr-core's public types expose solrj), where 'api' is correct so downstream modules compile. External-library api promotions are intentionally excluded here (see part 3/3); per dev@ feedback those need case-by-case judgement rather than blanket promotion. Builds on apache#4612. Lockfiles regenerated.
|
Please look over my changes. Not sure if we really need a part 2, let alone 3. |
|
Looked them over — all good. The opt-in And yeah — fine to drop 2 and 3. 1 is the one worth landing. The project() promotions in 2 were minor and 3 was only ever illustrative, so no loss. Thanks for jumping in on this. (CI red is the flaky |
Remove 24 project() declarations across 17 modules that solr-core already provides via its 'api'-scoped dependencies (solrj, solrj-zookeeper, solrj-streaming, api). Modules depending on solr-core receive these transitively, so re-declaring them is redundant -- leftovers from cutterslade, which didn't understand gradle's 'api'. solrj-jetty is kept where declared: core has it as 'implementation' (not re-exposed), so consumers that use it must declare it directly.
|
Folded the redundancy cleanup in here. Removed 24 redundant declarations across 17 modules — the ones core provides via One more I held off on: core exposes Full |
dsmiley
left a comment
There was a problem hiding this comment.
Thanks.
Was DAGP useful in this task? Sorry if I'm being redundant maybe but I want to ensure we're adding something useful, otherwise it's more burden/complexity in the most complex gradle build I've ever seen.
I don't want solrj-jetty to be api but I've internally debated it. Want to err on the side of concealing an implementation detail. Solrj base is increasingly capable and thus few modules truly need to reference solrj-jetty.
There was a problem hiding this comment.
@malliaridis this PR and another I have will add more Kotlin formatted build files. Our .editorconfig that I contributed says kotlin build files should have 4-space indentation, despite the general default of 2-space. I simply did that by observation of your choice. Do you think kotlin build files should be 4-space indented for some reason (why)?
| it.canBeResolved && | ||
| !it.attributes.keySet().any {attr -> attr.name.startsWith("dagp.")} | ||
| !it.attributes.keySet().any {attr -> attr.name.startsWith("dagp.")} && | ||
| !it.name.startsWith("spotless") |
There was a problem hiding this comment.
CC @malliaridis you might want to know about this line I added to get unstable spotless configuration noise out of the lockfile
| implementation platform(project(':platform')) | ||
| implementation project(':solr:core') | ||
| implementation project(':solr:solrj') | ||
| implementation project(':solr:solrj-streaming') |
There was a problem hiding this comment.
TBH, I want to revisit solrj-streaming being "api" from core. I don't think it should be.
There was a problem hiding this comment.
These classes (CatStream, ClassifyStream, AnalyzeEvaluator) are registered as streaming expression functions — SolrDefaultStreamFactory wires them: withFunctionName("cat", CatStream.class), etc. They exist specifically to plug into the streaming framework. They must extend TupleStream/Expressible — that's the whole point of them.
I just tried it — if core has solrj-streaming as implementation instead of api, it builds fine as long as sql/benchmark declare it themselves (sql tests pass). But DAGP immediately says it should be api again.
So it is part of core's public API now and I don't see simple way to fix it.
There was a problem hiding this comment.
The "sql" module is definitely the unusual exception with regards to it's dependency on solrj-streaming from within Solr. I think that should be explicit -- not "api".
There was a problem hiding this comment.
Agreed, sql is the exception — it's built directly on streaming (~11 files). Added implementation project(':solr:solrj-streaming') back to sql, explicit. Tests pass.
|
Regarding "solrj-jetty": Agreed on concealing it. Though heads up — |
|
Regarding "Was DAGP useful in this task?" — it helped a bit: finding unused declarations and test-scope mixups that compile fine so you'd never catch them by eye — the unused removals here came from it. And the solrj-streaming experiment above is a good example — it's what tells me the api scope matches what core actually exposes. But it's compile-time only — it can't see runtime usage. It flagged kafka-clients:test as unused, but removing it broke the CrossDC integration tests at runtime (EmbeddedKafkaCluster loads TestCondition from it); only the test run caught it. And the redundant-declaration cleanup was manual — DAGP wants to keep those. So does it bring big value? IMO no. Lucene doesn't use any dependency-analysis plugin and its build is fine. I'd keep it as the opt-in |
|
Double-checked before leaving it — ran buildHealth again on the cleaned-up build to see what DAGP flags now. Two things, and both are wrong for us: it calls The cleanup value it did give is already in this PR either way. So I'll stand by "opt-in, not a gate" — but no strong case that it earns ongoing keep. |
|
RE |
…lrClient Return the base HttpSolrClient type instead of the concrete HttpJettySolrClient, so consumers don't couple to the Jetty implementation. Modules generally don't care about the subtype (per @dsmiley) -- confirmed: the one module caller (cross-dc) uses only base SolrClient methods, so cross-dc can now drop its solrj-jetty dependency. Widened 4 internal callers to the base type (ColStatus, IterativeMergeStrategy, SolrConfigHandler.PerReplicaCallable) or the base API (PackageAPI now uses processWithBaseUrl). Five callers keep a cast to HttpJettySolrClient because they genuinely use Jetty-only API: Builder.withHttpClient (shares Jetty transport internals), getHttpClient() for raw proxying, and the async requestWithBaseUrl(baseUrl, fn) overload. Pure compile-time change; runtime behavior is unchanged. Experimental; for discussion on whether to keep here or split out.
|
Pushed the This PR has grown a lot though — DAGP reporting, the redundant-declaration cleanup, and now this return-type change are really three separate things. How do you want to handle it? Options I see:
Let me know what action I should take with this PR and the topics touched here. Hope I'm contributing value, not noise. |
Per review: sql is built directly on streaming expressions (it uses org.apache.solr.client.solrj.io.* across ~11 files), so it should declare solrj-streaming explicitly rather than rely on getting it transitively through solr-core's api. The earlier redundant-declaration cleanup removed it as 'provided by core'; sql is the exception where the dependency is real and direct.
Completes the dependency-analysis follow-up deferred from the Gradle 9 upgrade (SOLR-18289), which dropped the Gradle-9-incompatible
ca.cutterslade.analyzeplugin.Adds the Dependency Analysis Gradle Plugin (com.autonomousapps build-health) as an opt-in
analyzeDependenciestask — it reports advice, doesn't fail the build, since the api-vs-implementation calls need human judgement rather than blanket enforcement.Changes
com.autonomousapps.build-health; config ingradle/validation/dependency-analyze.gradle.kts, wired to an opt-inanalyzeDependenciestask (notcheck).testRuntimeOnly.project()declarations that solr-core already provides viaapi(24 across 17 modules) — cutterslade-era "state the obvious" leftovers.testImplementation(DAGP advises testRuntimeOnly, but tests compile against mock-oauth2-server's@JsonDeserialize— demoting breaks under -Werror).kotlin-metadata-jvmto Solr's Kotlin so DAGP can analyze:solr:ui(Update kotlin-metadata-jvm to 2.3+ for Kotlin 2.4 support autonomousapps/dependency-analysis-gradle-plugin#1661; temporary).api-scope promotions DAGP also suggests were considered and set aside — they need case-by-case judgement rather than blanket application (per dev@ discussion).
Testing
gradlew check -x testand fullgradlew testpass.AI assistance disclosure
Per AGENTS.md / how-to-contribute.adoc: prepared with an AI coding agent and reviewed by me for correctness, scope, and alignment with Solr's build conventions.