This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_10x by this push:
new f9181508ba8 SOLR-18442: Fix OpenTelemetry Observable gauge memory leak
f9181508ba8 is described below
commit f9181508ba80fb1b66099adff24c7218e348274e
Author: Chris Hostetter <[email protected]>
AuthorDate: Thu Sep 17 15:50:55 2026 -0700
SOLR-18442: Fix OpenTelemetry Observable gauge memory leak
(cherry picked from commit 136d1bd1f22ee1aa3566b85d5bd177f886a8c2ee)
---
.../SOLR-18442-observable-gauge-leak.yml | 13 ++++
.../apache/solr/metrics/SolrMetricsContext.java | 40 +++++------
.../solr/metrics/SolrMetricsContextTest.java | 77 ++++++++++++++++++++++
3 files changed, 111 insertions(+), 19 deletions(-)
diff --git a/changelog/unreleased/SOLR-18442-observable-gauge-leak.yml
b/changelog/unreleased/SOLR-18442-observable-gauge-leak.yml
new file mode 100644
index 00000000000..1479c33371b
--- /dev/null
+++ b/changelog/unreleased/SOLR-18442-observable-gauge-leak.yml
@@ -0,0 +1,13 @@
+title: >
+ Fix a memory leak where every SolrIndexSearcher ever opened was retained by
the OpenTelemetry
+ metric registry via its "index.commit_size" observable gauge, along with its
DirectoryReader and
+ segment live-docs bitsets. Nodes with a high commit cadence would eventually
run out of heap.
+ Observable metrics registered through SolrMetricsContext with an explicit
unit are now closed
+ together with the context.
+type: fixed
+authors:
+ - name: Jan Høydahl
+ - name: Mikhail Khludnev
+links:
+ - name: SOLR-18442
+ url: https://issues.apache.org/jira/browse/SOLR-18442
diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrMetricsContext.java
b/solr/core/src/java/org/apache/solr/metrics/SolrMetricsContext.java
index 720bbeb301d..45d7c591ef9 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrMetricsContext.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrMetricsContext.java
@@ -160,9 +160,7 @@ public class SolrMetricsContext implements AutoCloseable {
public ObservableLongGauge observableLongGauge(
String metricName, String description,
Consumer<ObservableLongMeasurement> callback) {
- var observableLongGauge = observableLongGauge(metricName, description,
callback, null);
- closeables.add(observableLongGauge);
- return observableLongGauge;
+ return observableLongGauge(metricName, description, callback, null);
}
public ObservableLongGauge observableLongGauge(
@@ -170,14 +168,15 @@ public class SolrMetricsContext implements AutoCloseable {
String description,
Consumer<ObservableLongMeasurement> callback,
OtelUnit unit) {
- return metricManager.observableLongGauge(registryName, metricName,
description, callback, unit);
+ var observableLongGauge =
+ metricManager.observableLongGauge(registryName, metricName,
description, callback, unit);
+ closeables.add(observableLongGauge);
+ return observableLongGauge;
}
public ObservableDoubleGauge observableDoubleGauge(
String metricName, String description,
Consumer<ObservableDoubleMeasurement> callback) {
- var observableDoubleGauge = observableDoubleGauge(metricName, description,
callback, null);
- closeables.add(observableDoubleGauge);
- return observableDoubleGauge;
+ return observableDoubleGauge(metricName, description, callback, null);
}
public ObservableDoubleGauge observableDoubleGauge(
@@ -185,15 +184,15 @@ public class SolrMetricsContext implements AutoCloseable {
String description,
Consumer<ObservableDoubleMeasurement> callback,
OtelUnit unit) {
- return metricManager.observableDoubleGauge(
- registryName, metricName, description, callback, unit);
+ var observableDoubleGauge =
+ metricManager.observableDoubleGauge(registryName, metricName,
description, callback, unit);
+ closeables.add(observableDoubleGauge);
+ return observableDoubleGauge;
}
public ObservableLongCounter observableLongCounter(
String metricName, String description,
Consumer<ObservableLongMeasurement> callback) {
- var observableLongCounter = observableLongCounter(metricName, description,
callback, null);
- closeables.add(observableLongCounter);
- return observableLongCounter;
+ return observableLongCounter(metricName, description, callback, null);
}
public ObservableLongCounter observableLongCounter(
@@ -201,15 +200,15 @@ public class SolrMetricsContext implements AutoCloseable {
String description,
Consumer<ObservableLongMeasurement> callback,
OtelUnit unit) {
- return metricManager.observableLongCounter(
- registryName, metricName, description, callback, unit);
+ var observableLongCounter =
+ metricManager.observableLongCounter(registryName, metricName,
description, callback, unit);
+ closeables.add(observableLongCounter);
+ return observableLongCounter;
}
public ObservableDoubleCounter observableDoubleCounter(
String metricName, String description,
Consumer<ObservableDoubleMeasurement> callback) {
- var observableDoubleCounter = observableDoubleCounter(metricName,
description, callback, null);
- closeables.add(observableDoubleCounter);
- return observableDoubleCounter;
+ return observableDoubleCounter(metricName, description, callback, null);
}
public ObservableDoubleCounter observableDoubleCounter(
@@ -217,8 +216,11 @@ public class SolrMetricsContext implements AutoCloseable {
String description,
Consumer<ObservableDoubleMeasurement> callback,
OtelUnit unit) {
- return metricManager.observableDoubleCounter(
- registryName, metricName, description, callback, unit);
+ var observableDoubleCounter =
+ metricManager.observableDoubleCounter(
+ registryName, metricName, description, callback, unit);
+ closeables.add(observableDoubleCounter);
+ return observableDoubleCounter;
}
public ObservableLongMeasurement longGaugeMeasurement(String metricName,
String description) {
diff --git
a/solr/core/src/test/org/apache/solr/metrics/SolrMetricsContextTest.java
b/solr/core/src/test/org/apache/solr/metrics/SolrMetricsContextTest.java
new file mode 100644
index 00000000000..048962f7a80
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/metrics/SolrMetricsContextTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.metrics;
+
+import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.metrics.otel.OtelUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SolrMetricsContextTest extends SolrTestCase {
+ private static final String REGISTRY = "test_context_registry";
+ private SolrMetricManager metricManager;
+ private PrometheusMetricReader reader;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ metricManager = new SolrMetricManager(InMemoryMetricExporter.create());
+ metricManager.meterProvider(REGISTRY);
+ reader = metricManager.getPrometheusMetricReader(REGISTRY);
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception {
+ metricManager.closeAllRegistries();
+ super.tearDown();
+ }
+
+ /** Callbacks registered through a context must stop firing once the context
is closed. */
+ @Test
+ public void testCloseUnregistersObservableCallbacks() {
+ SolrMetricsContext ctx = new SolrMetricsContext(metricManager, REGISTRY);
+ AtomicInteger invocations = new AtomicInteger();
+
+ ctx.observableLongGauge("long_gauge", "d", m ->
invocations.incrementAndGet());
+ ctx.observableLongGauge(
+ "long_gauge_unit", "d", m -> invocations.incrementAndGet(),
OtelUnit.BYTES);
+ ctx.observableDoubleGauge("double_gauge", "d", m ->
invocations.incrementAndGet());
+ ctx.observableDoubleGauge(
+ "double_gauge_unit", "d", m -> invocations.incrementAndGet(),
OtelUnit.BYTES);
+ ctx.observableLongCounter("long_counter", "d", m ->
invocations.incrementAndGet());
+ ctx.observableLongCounter(
+ "long_counter_unit", "d", m -> invocations.incrementAndGet(),
OtelUnit.BYTES);
+ ctx.observableDoubleCounter("double_counter", "d", m ->
invocations.incrementAndGet());
+ ctx.observableDoubleCounter(
+ "double_counter_unit", "d", m -> invocations.incrementAndGet(),
OtelUnit.BYTES);
+
+ reader.collect();
+ assertEquals("all 8 callbacks should fire while the context is open", 8,
invocations.get());
+
+ ctx.close();
+ invocations.set(0);
+ reader.collect();
+ assertEquals("no callback should fire after the context is closed", 0,
invocations.get());
+ }
+}