This is an automated email from the ASF dual-hosted git repository.
lokiore pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/master by this push:
new e0289913b1 PHOENIX-7965 :- Guard
ConnectionQueryServicesImpl.getMetaDataCache() against a closed connection
(#2580)
e0289913b1 is described below
commit e0289913b16c5c8a4339b2e72d8a7e823f804d0d
Author: Lokesh Khurana <[email protected]>
AuthorDate: Wed Jul 29 12:04:48 2026 -0700
PHOENIX-7965 :- Guard ConnectionQueryServicesImpl.getMetaDataCache()
against a closed connection (#2580)
getMetaDataCache() returned the volatile latestMetaData field directly,
with no
null guard. When a connection is closed concurrently with query compilation
(for example connection pool teardown or cluster failover), close() sets
latestMetaData = null. An in-flight compile then reaches the cache via
PhoenixConnection.getTableRef()/getFunction()/getSchema(), which call
getMetaDataCache().getTableRef(key) (and friends) on the null return,
producing
a bare, causeless NullPointerException with no message.
Every other cache accessor/mutator on this class (addTable, removeTable,
pruneTables, the internal getTable path, etc.) already calls
throwConnectionClosedIfNullMetaData() first, which throws a descriptive
IllegalStateException("Connection to the cluster is closed").
getMetaDataCache()
was the one accessor missing that guard. This change adds the same guard so
a
closed connection surfaces the descriptive exception instead of a bare NPE.
No caller relies on a null return: every production caller of
ConnectionQueryServices.getMetaDataCache() immediately dereferences the
result,
so this only upgrades the failure mode from an opaque NPE to a descriptive
exception; it does not change behavior on an open connection.
Tests: added unit tests asserting getMetaDataCache() throws the descriptive
IllegalStateException when the cache has been nulled (closed-connection
state),
and returns the cache unchanged when present.
Generated-by: Claude Code (Opus 4.8 (1M context))
---
.../phoenix/query/ConnectionQueryServicesImpl.java | 11 ++++-
.../query/ConnectionQueryServicesImplTest.java | 54 ++++++++++++++++++++++
2 files changed, 64 insertions(+), 1 deletion(-)
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java
index 2c27a57fd3..f14ae7aa71 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java
@@ -1090,7 +1090,16 @@ public class ConnectionQueryServicesImpl extends
DelegateQueryServices
}
public PMetaData getMetaDataCache() {
- return latestMetaData;
+ // A concurrent close() (e.g. connection pool teardown or cluster
failover) nulls out
+ // latestMetaData. Read it once into a local so the null-check and the
returned reference
+ // are guaranteed to be the same value; a caller that immediately
dereferences the result
+ // then surfaces the descriptive "connection closed" exception instead of
a bare NPE,
+ // matching every other cache accessor/mutator on this class.
+ PMetaData cache = latestMetaData;
+ if (cache == null) {
+ throwConnectionClosedException();
+ }
+ return cache;
}
@Override
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java
index a6a55a5081..af03a2b0f4 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java
@@ -73,6 +73,7 @@ import org.apache.phoenix.exception.PhoenixIOException;
import org.apache.phoenix.jdbc.ConnectionInfo;
import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData;
import org.apache.phoenix.monitoring.GlobalClientMetrics;
+import org.apache.phoenix.schema.PMetaData;
import org.apache.phoenix.util.ReadOnlyProps;
import org.junit.Before;
import org.junit.ClassRule;
@@ -408,4 +409,57 @@ public class ConnectionQueryServicesImplTest {
verify(mockAdmin,
Mockito.times(1)).deleteTable(TableName.valueOf("TEST_TABLE"));
verify(mockConn).getAdmin();
}
+
+ /**
+ * When a connection is closed concurrently with query compilation (e.g.
connection pool teardown
+ * or cluster failover), the metadata cache is nulled out.
getMetaDataCache() must surface the
+ * descriptive "connection closed" exception rather than returning null and
letting callers hit a
+ * bare NPE.
+ */
+ @Test
+ public void testGetMetaDataCacheThrowsWhenClosed()
+ throws NoSuchFieldException, IllegalAccessException {
+ ConnectionQueryServicesImpl cqsi = newRealCqsi();
+ // Simulate the post-close() state where close() has set latestMetaData =
null.
+ setLatestMetaData(cqsi, null);
+ try {
+ cqsi.getMetaDataCache();
+ fail("Expected IllegalStateException for a closed connection, but no
exception was thrown");
+ } catch (IllegalStateException e) {
+ assertEquals("Connection to the cluster is closed", e.getMessage());
+ }
+ }
+
+ /**
+ * When the metadata cache is present (open connection), getMetaDataCache()
returns it unchanged.
+ */
+ @Test
+ public void testGetMetaDataCacheReturnsCacheWhenOpen()
+ throws NoSuchFieldException, IllegalAccessException {
+ ConnectionQueryServicesImpl cqsi = newRealCqsi();
+ PMetaData metaData = Mockito.mock(PMetaData.class);
+ setLatestMetaData(cqsi, metaData);
+ assertSame(metaData, cqsi.getMetaDataCache());
+ }
+
+ /**
+ * Builds a real ConnectionQueryServicesImpl so that getMetaDataCache() and
its private
+ * throwConnectionClosedIfNullMetaData() guard execute for real. A
CALLS_REAL_METHODS mock cannot
+ * be used here because Mockito does not route the internal call to the
private guard method.
+ */
+ private static ConnectionQueryServicesImpl newRealCqsi() {
+ QueryServices mockQueryServices = Mockito.mock(QueryServices.class);
+ ReadOnlyProps emptyProps = ReadOnlyProps.EMPTY_PROPS;
+ when(mockQueryServices.getProps()).thenReturn(emptyProps);
+ ConnectionInfo mockConnectionInfo = Mockito.mock(ConnectionInfo.class);
+ when(mockConnectionInfo.asProps()).thenReturn(emptyProps);
+ return new ConnectionQueryServicesImpl(mockQueryServices,
mockConnectionInfo, new Properties());
+ }
+
+ private static void setLatestMetaData(ConnectionQueryServicesImpl cqsi,
PMetaData value)
+ throws NoSuchFieldException, IllegalAccessException {
+ Field field =
ConnectionQueryServicesImpl.class.getDeclaredField("latestMetaData");
+ field.setAccessible(true);
+ field.set(cqsi, value);
+ }
}