Skip to content

SOLR-18442: Fix SolrIndexSearcher leak via unit-bearing observable metrics - #4917

Open
janhoy wants to merge 1 commit into
apache:mainfrom
janhoy:SOLR-18442-observable-gauge-leak
Open

janhoy wants to merge 1 commit into
apache:mainfrom
janhoy:SOLR-18442-observable-gauge-leak

Conversation

@janhoy

@janhoy janhoy commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18442

This research and PR is 100% the work of Claude Code Fable 5.1

Root cause

The SolrMetricsContext.observable{Long,Double}{Gauge,Counter} overloads that take an OtelUnit never added the instrument to the context's closeables, so SolrMetricsContext.close() could not unregister them. SolrIndexSearcher registers its index.commit_size gauge with OtelUnit.MEGABYTES, so exactly one CallbackRegistration per searcher survived close(), pinning the searcher, its DirectoryReader and every segment's live-docs bitset in the OTel meter. This matches the one-registration-per-searcher growth in the JIRA heap histograms. UpdateLog, SuggestComponent and ZkContainer register metrics with a unit through the same path.

Fix

The unit-bearing overloads now register the closeable; the no-unit overloads delegate to them. No changes to callers.

Test

SolrMetricsContextTest registers all eight overload variants, closes the context and asserts no callback fires on the next collect. Fails without the fix (the four unit-bearing callbacks keep firing), passes with it, no GC dependence.

Compared with #4902: this fixes the missing unregistration instead of weakening the reader reference, and also covers the other leaking callers. The <metrics enabled="false"/> question from the JIRA is left out of this PR.

…trics

SolrMetricsContext only registered observable gauges/counters for closing
in the overloads without an OtelUnit. SolrIndexSearcher's
"index.commit_size" gauge passes OtelUnit.MEGABYTES, so its callback
(capturing the DirectoryReader) survived SolrMetricsContext.close() and
pinned every searcher ever opened in the OTel meter. Same gap affected
UpdateLog, SuggestComponent and ZkContainer metrics registered with a unit.

Registration of the closeable now happens in the unit-bearing overloads,
which the no-unit overloads delegate to.
import org.junit.Before;
import org.junit.Test;

public class SolrMetricsContextTest extends SolrTestCaseJ4 {

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.

can't we use SolrTestCase ? Fable didn't read our AGENTS.md :-(

@dsmiley
dsmiley requested a review from mlbiscoc September 16, 2026 17:42
Comment on lines +171 to +174
var observableLongGauge =
metricManager.observableLongGauge(registryName, metricName, description, callback, unit);
closeables.add(observableLongGauge);
return observableLongGauge;

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.

Ah oh wow this overload made me tripped me up when writing this and I added the closables in the wrong place! :(

Thank you @janhoy and @mkhludnev for fixing and finding this.

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.

I think we should better protect devs from this in the future or if new observable instruments get created and the author forgets to do closeables.add or places it in the wrong place (like I did).

What if these observable instruments here in SolrMetricsContext just return void instead so the users of these instruments don't need to remember to close them or figure out what to do with the object. Each of the observable instrument goes through a registerObservable which takes care of adding to closeable for the caller. Then make the observable instruments from metricManager package-private so only SolrMetricsContext can use it. Wdyt?

This is a bigger scoped changed but I think it is worth it.

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.

@mlbiscoc +1, Solr 9 used a similar mechanism to solve a similar problem - Dropwizard gauges were constructed with lambdas that would keep references to closed searchers, thus causing similar leaks. The solution we came up with was to hide this mechanism in SolrMetricManager - always register gauges with a helper method in SolrMetricManager that would wrap the gauge instance + a unique tag and add these lambdas to a list of closables to be called on SolrMetricsContext.unregister().

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.

As for the timing wrt 10.1 release... This minimal fix here addresses the immediate problem, perhaps refactoring could be scheduled for later?

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.

Understandable. Approved then because it does address the actual problem.

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.

@janhoy

janhoy commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Guys, I don't have time to run with this, so I'm happy to let you commit to this PR branch or just fork it into your own, add yourself to changelog and see it to completion.

Also I have not checked whether Solr 9 has a similar issue.

invocations.set(0);
reader.collect();
assertEquals("no callback should fire after the context is closed", 0, invocations.get());
}

@sigram sigram Sep 17, 2026

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.

I wonder if we could somehow add a test to actually detect leaks... I imagine something like using the DiagnosticCommand MXBean with GC.class_histogram command to do a full GC and list the class/instance histogram, then do several known-to-be-tricky operations like add/commit/commitWithin, core reload, schema change etc, repeat this 100 times or so, and then do the histogram again (which again performs a full GC) to see if differences are "reasonable".

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.

4 participants