This is an automated email from the ASF dual-hosted git repository.
ayushtkn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git
The following commit(s) were added to refs/heads/master by this push:
new f6e3e77f2b7 HIVE-29697: Add support for Persistable Sessions (#6687)
f6e3e77f2b7 is described below
commit f6e3e77f2b70edb1adacbb9122b1e95e009cbb26
Author: Ayush Saxena <[email protected]>
AuthorDate: Thu Sep 17 00:00:08 2026 +0530
HIVE-29697: Add support for Persistable Sessions (#6687)
---
.../java/org/apache/hadoop/hive/conf/HiveConf.java | 20 +-
itests/hive-unit/pom.xml | 12 +
.../cli/session/TestPersistableSessionBase.java | 667 +++++++++++++++++++++
.../session/TestPersistableSessionWithRedis.java | 82 +++
.../TestPersistableSessionWithZooKeeper.java | 84 +++
.../java/org/apache/hive/jdbc/HiveConnection.java | 26 +-
.../java/org/apache/hive/jdbc/HiveStatement.java | 171 +++++-
.../org/apache/hive/jdbc/TestHiveStatement.java | 53 ++
packaging/src/main/assembly/src.xml | 1 +
pom.xml | 2 +
.../hadoop/hive/ql/session/SessionState.java | 8 +-
service-session-store/pom.xml | 91 +++
.../cli/session/store/HiveSessionSnapshot.java | 237 ++++++++
.../cli/session/store/RedisSessionStateStore.java | 125 ++++
.../cli/session/store/SessionStateStore.java | 34 ++
.../session/store/TempTablePartitionSnapshot.java | 54 ++
.../session/store/ZooKeeperSessionStateStore.java | 146 +++++
.../session/store/TestRedisSessionStateStore.java | 66 ++
.../session/store/TestSessionStateStoreBase.java | 180 ++++++
.../store/TestZooKeeperSessionStateStore.java | 56 ++
service/pom.xml | 5 +
.../cli/operation/HiveCommandOperation.java | 13 +
.../hive/service/cli/operation/SQLOperation.java | 6 +
.../hive/service/cli/session/HiveSessionImpl.java | 20 +
.../hive/service/cli/session/HiveSessionProxy.java | 4 +
.../cli/session/PersistableSessionUtils.java | 467 +++++++++++++++
.../hive/service/cli/session/SessionManager.java | 160 ++++-
.../hive/service/cli/thrift/ThriftCLIService.java | 2 +
.../cli/session/TestPersistableSessionUtils.java | 74 +++
29 files changed, 2833 insertions(+), 33 deletions(-)
diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
index e7648c621a6..01f82922c9b 100644
--- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
+++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
@@ -3904,8 +3904,26 @@ public static enum ConfVars {
"hs2ActivePassiveHA",
"When HiveServer2 Active/Passive High Availability is enabled, uses this
namespace for registering HS2\n" +
"instances with zookeeper"),
-
HIVE_SERVER2_ACTIVE_PASSIVE_HA_HEALTHCHECK_PORT("hive.server2.active.passive.ha.healthcheck.port",
11002,
+
HIVE_SERVER2_ACTIVE_PASSIVE_HA_HEALTHCHECK_PORT("hive.server2.active.passive.ha.healthcheck.port",
11002,
"The port the HiveServer2 ha-healthcheck web app will listen on"),
+
+ // Persistable session state store configs
+
HIVE_SERVER2_SESSION_STATE_STORE_CLASS("hive.server2.session.state.store.class",
+ "",
+ "Implementation class for the session state store. Empty means
disabled. Options:\n" +
+ "
org.apache.hive.service.cli.session.store.ZooKeeperSessionStateStore\n" +
+ " org.apache.hive.service.cli.session.store.RedisSessionStateStore"),
+
HIVE_SERVER2_SESSION_STATE_STORE_FETCH_STRATEGY("hive.server2.session.state.store.fetch.strategy",
+ "NEVER",
+ new StringSet("NEVER", "ALWAYS", "FETCH_WHEN_MISSING"),
+ "Session fetch strategy from shared store:\n" +
+ " NEVER - only use local session state\n" +
+ " ALWAYS - on every access, compare local lastAccessTime with remote;
if remote is newer, re-hydrate\n" +
+ " FETCH_WHEN_MISSING - fetch from store only when session not found
locally"),
+
HIVE_SERVER2_SESSION_STATE_STORE_TTL("hive.server2.session.state.store.ttl",
"24h",
+ new TimeValidator(TimeUnit.SECONDS),
+ "TTL for session snapshots in the state store. Abandoned sessions
auto-expire after this duration."),
+
HIVE_SERVER2_TEZ_INTERACTIVE_QUEUE("hive.server2.tez.interactive.queue",
"",
"A single YARN queues to use for Hive Interactive sessions. When this
is specified,\n" +
"workload management is enabled and used for these sessions."),
diff --git a/itests/hive-unit/pom.xml b/itests/hive-unit/pom.xml
index 33f5a9ed43d..9a4fa9cb8e5 100644
--- a/itests/hive-unit/pom.xml
+++ b/itests/hive-unit/pom.xml
@@ -58,6 +58,18 @@
<groupId>org.apache.hive</groupId>
<artifactId>hive-service</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.hive</groupId>
+ <artifactId>hive-service-session-store</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>redis.clients</groupId>
+ <artifactId>jedis</artifactId>
+ <version>${jedis.version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-llap-server</artifactId>
diff --git
a/itests/hive-unit/src/test/java/org/apache/hive/service/cli/session/TestPersistableSessionBase.java
b/itests/hive-unit/src/test/java/org/apache/hive/service/cli/session/TestPersistableSessionBase.java
new file mode 100644
index 00000000000..48c3cb8ad75
--- /dev/null
+++
b/itests/hive-unit/src/test/java/org/apache/hive/service/cli/session/TestPersistableSessionBase.java
@@ -0,0 +1,667 @@
+/*
+ * 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.hive.service.cli.session;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.apache.hive.jdbc.miniHS2.MiniHS2;
+import org.apache.hive.service.cli.CLIServiceClient;
+import org.apache.hive.service.cli.HiveSQLException;
+import org.apache.hive.service.cli.OperationHandle;
+import org.apache.hive.service.cli.OperationState;
+import org.apache.hive.service.cli.RowSet;
+import org.apache.hive.service.cli.SessionHandle;
+import org.apache.hive.service.cli.session.store.HiveSessionSnapshot;
+import org.apache.hive.service.cli.session.store.SessionStateStore;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Abstract integration test for Persistable Sessions feature.
+ * Tests that session state (configs, temp tables, data) is persisted to a
shared
+ * store and recovered by another HS2 instance on failover.
+ *
+ * Subclasses provide the store implementation class and infrastructure setup
+ * (e.g. ZooKeeper TestingServer, Redis container) via {@link
#getStoreClassName()}
+ * and {@link #configureStore(HiveConf)}.
+ */
+public abstract class TestPersistableSessionBase {
+
+ protected MiniHS2 miniHs2First;
+ protected MiniHS2 miniHs2Second;
+ protected HiveConf hiveConf1;
+ protected HiveConf hiveConf2;
+
+ protected abstract String getStoreClassName();
+
+ protected abstract void configureStore(HiveConf conf);
+
+ protected abstract SessionStateStore createVerifyStore() throws Exception;
+
+ @Before
+ public void setUp() throws Exception {
+ hiveConf1 = new HiveConf();
+ hiveConf1.setBoolVar(ConfVars.HIVE_SUPPORT_CONCURRENCY, false);
+ configurePersistableSession(hiveConf1);
+ miniHs2First = new
MiniHS2.Builder().withConf(hiveConf1).withHTTPTransport()
+ .cleanupLocalDirOnStartup(false).build();
+
+ hiveConf2 = new HiveConf();
+ hiveConf2.setBoolVar(ConfVars.HIVE_SUPPORT_CONCURRENCY, false);
+ configurePersistableSession(hiveConf2);
+ miniHs2Second = new
MiniHS2.Builder().withConf(hiveConf2).withHTTPTransport()
+ .cleanupLocalDirOnStartup(false).build();
+ }
+
+ @After
+ public void tearDown() {
+ if (miniHs2First != null && miniHs2First.isStarted()) {
+ miniHs2First.stop();
+ }
+ if (miniHs2Second != null && miniHs2Second.isStarted()) {
+ miniHs2Second.stop();
+ }
+ }
+
+ private void executeStatementAndWait(CLIServiceClient client, SessionHandle
sessHandle,
+ String statement, Map<String, String> confOverlay) throws Exception {
+ OperationHandle opHandle = client.executeStatementAsync(sessHandle,
statement, confOverlay);
+ long timeout = System.currentTimeMillis() + 60000;
+ while (true) {
+ OperationState state = client.getOperationStatus(opHandle,
false).getState();
+ if (state == OperationState.FINISHED) {
+ break;
+ }
+ if (state == OperationState.ERROR || state == OperationState.CANCELED) {
+ fail("Operation failed with state: " + state + " for statement: " +
statement);
+ }
+ if (System.currentTimeMillis() > timeout) {
+ fail("Timed out waiting for: " + statement);
+ }
+ Thread.sleep(200);
+ }
+ }
+
+ /**
+ * Polls the session state store until the snapshot satisfies the given
condition or times out.
+ * Needed because the snapshot save runs asynchronously on the background
thread after the
+ * operation state becomes FINISHED (there is a small window between state
visibility and
+ * save completion).
+ */
+ private HiveSessionSnapshot waitForSnapshotCondition(SessionStateStore store,
+ String storeKey, java.util.function.Predicate<HiveSessionSnapshot>
condition,
+ long timeoutMs) throws Exception {
+ long deadline = System.currentTimeMillis() + timeoutMs;
+ HiveSessionSnapshot snapshot = null;
+ while (System.currentTimeMillis() < deadline) {
+ snapshot = store.getSnapshot(storeKey);
+ if (snapshot != null && condition.test(snapshot)) {
+ return snapshot;
+ }
+ Thread.sleep(100);
+ }
+ return snapshot;
+ }
+
+ private void configurePersistableSession(HiveConf conf) {
+ conf.setVar(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_CLASS,
getStoreClassName());
+ conf.setVar(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_FETCH_STRATEGY,
"FETCH_WHEN_MISSING");
+ conf.setVar(ConfVars.HIVE_EXECUTION_ENGINE, "tez");
+ conf.setBoolean("tez.local.mode", true);
+ conf.setBoolean("tez.local.mode.without.network", true);
+ conf.setVar(ConfVars.HIVE_JAR_DIRECTORY,
System.getProperty("java.io.tmpdir"));
+ configureStore(conf);
+ }
+
+ @Test(timeout = 120000)
+ public void testSessionRecoveryOnFailover() throws Exception {
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+ executeStatementAndWait(client1, sessHandle, "SET
hive.exec.dynamic.partition=true", confOverlay);
+ executeStatementAndWait(client1, sessHandle, "SET
hive.exec.dynamic.partition.mode=nonstrict", confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "CREATE TEMPORARY TABLE tmp_failover_test (id INT, name STRING)",
confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "INSERT INTO tmp_failover_test VALUES (1, 'alice'), (2, 'bob'), (3,
'charlie')", confOverlay);
+
+ miniHs2First.stop();
+ miniHs2Second.start(confOverlay);
+
+ CLIServiceClient client2 = miniHs2Second.getServiceClient();
+
+ // Present the SAME session handle — triggers recovery from shared store
+ OperationHandle opHandle = client2.executeStatement(sessHandle, "SELECT
1", confOverlay);
+ RowSet rowSet = client2.fetchResults(opHandle);
+ assertEquals(1, rowSet.numRows());
+
+ // Verify configs recovered
+ opHandle = client2.executeStatement(sessHandle, "SET
hive.exec.dynamic.partition", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertTrue(rowSet.numRows() > 0);
+ assertTrue(rowSet.iterator().next()[0].toString().contains("true"));
+
+ opHandle = client2.executeStatement(sessHandle,
+ "SET hive.exec.dynamic.partition.mode", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertTrue(rowSet.numRows() > 0);
+ assertTrue(rowSet.iterator().next()[0].toString().contains("nonstrict"));
+
+ // Verify temp table data is recovered (shared filesystem, LOCATION
preserved)
+ opHandle = client2.executeStatement(sessHandle,
+ "SELECT id FROM tmp_failover_test ORDER BY id", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertEquals(3, rowSet.numRows());
+
+ client2.closeSession(sessHandle);
+ }
+
+ @Test(timeout = 120000)
+ public void testFetchStrategyNeverNoRecovery() throws Exception {
+ hiveConf1.setVar(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_FETCH_STRATEGY,
"NEVER");
+ miniHs2First = new
MiniHS2.Builder().withConf(hiveConf1).withHTTPTransport()
+ .cleanupLocalDirOnStartup(false).build();
+
+ hiveConf2.setVar(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_FETCH_STRATEGY,
"NEVER");
+ miniHs2Second = new
MiniHS2.Builder().withConf(hiveConf2).withHTTPTransport()
+ .cleanupLocalDirOnStartup(false).build();
+
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+ executeStatementAndWait(client1, sessHandle, "SET
hive.exec.dynamic.partition=true", confOverlay);
+
+ miniHs2First.stop();
+ miniHs2Second.start(confOverlay);
+
+ CLIServiceClient client2 = miniHs2Second.getServiceClient();
+ try {
+ client2.executeStatement(sessHandle, "SELECT 1", confOverlay);
+ fail("Expected HiveSQLException for invalid session handle");
+ } catch (HiveSQLException e) {
+ assertTrue("Expected 'Invalid SessionHandle' error, got: " +
e.getMessage(),
+ e.getMessage().contains("Invalid SessionHandle"));
+ }
+ }
+
+ @Test(timeout = 120000)
+ public void testConfigsRecoveredAfterFailover() throws Exception {
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+ executeStatementAndWait(client1, sessHandle, "SET
hive.exec.dynamic.partition=true", confOverlay);
+ executeStatementAndWait(client1, sessHandle, "SET
hive.exec.dynamic.partition.mode=nonstrict", confOverlay);
+ executeStatementAndWait(client1, sessHandle, "SET
hive.mapred.mode=strict", confOverlay);
+
+ miniHs2First.stop();
+ miniHs2Second.start(confOverlay);
+
+ CLIServiceClient client2 = miniHs2Second.getServiceClient();
+
+ OperationHandle opHandle = client2.executeStatement(sessHandle,
+ "SET hive.exec.dynamic.partition", confOverlay);
+ RowSet rowSet = client2.fetchResults(opHandle);
+ assertTrue(rowSet.numRows() > 0);
+ assertTrue(rowSet.iterator().next()[0].toString().contains("true"));
+
+ opHandle = client2.executeStatement(sessHandle,
+ "SET hive.exec.dynamic.partition.mode", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertTrue(rowSet.numRows() > 0);
+ assertTrue(rowSet.iterator().next()[0].toString().contains("nonstrict"));
+
+ opHandle = client2.executeStatement(sessHandle, "SET hive.mapred.mode",
confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertTrue(rowSet.numRows() > 0);
+ assertTrue(rowSet.iterator().next()[0].toString().contains("strict"));
+
+ client2.closeSession(sessHandle);
+ }
+
+ @Test(timeout = 120000)
+ public void testTempTableWithDataRecoveredAfterFailover() throws Exception {
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+ executeStatementAndWait(client1, sessHandle,
+ "CREATE TEMPORARY TABLE tmp_data_test (id INT, name STRING)",
confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "INSERT INTO tmp_data_test VALUES (10, 'hive'), (20, 'hadoop')",
confOverlay);
+
+ miniHs2First.stop();
+ miniHs2Second.start(confOverlay);
+
+ CLIServiceClient client2 = miniHs2Second.getServiceClient();
+
+ // Verify temp table schema is recovered (table exists after failover)
+ OperationHandle opHandle = client2.executeStatement(sessHandle,
+ "DESCRIBE tmp_data_test", confOverlay);
+ RowSet rowSet = client2.fetchResults(opHandle);
+ assertTrue("Temp table schema should be recovered", rowSet.numRows() > 0);
+
+ // Verify the data is recovered (shared filesystem, LOCATION preserved in
DDL)
+ opHandle = client2.executeStatement(sessHandle,
+ "SELECT id, name FROM tmp_data_test ORDER BY id", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertEquals(2, rowSet.numRows());
+ Iterator<Object[]> it = rowSet.iterator();
+ Object[] row1 = it.next();
+ assertEquals("10", row1[0].toString());
+ assertEquals("hive", row1[1].toString());
+ Object[] row2 = it.next();
+ assertEquals("20", row2[0].toString());
+ assertEquals("hadoop", row2[1].toString());
+
+ client2.closeSession(sessHandle);
+ }
+
+ @Test(timeout = 120000)
+ public void testQueryRetrySucceedsOnSecondHS2AfterCrash() throws Exception {
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+
+ // Create a regular table and insert data on HS2-1
+ client1.executeStatement(sessHandle,
+ "CREATE TABLE IF NOT EXISTS retry_test (id INT, val STRING)",
confOverlay);
+ client1.executeStatement(sessHandle,
+ "INSERT INTO retry_test VALUES (1, 'one'), (2, 'two'), (3, 'three')",
confOverlay);
+
+ // Verify query works on HS2-1
+ OperationHandle opHandle = client1.executeStatement(sessHandle,
+ "SELECT count(*) FROM retry_test", confOverlay);
+ RowSet rowSet = client1.fetchResults(opHandle);
+ assertEquals(3L, Long.parseLong(rowSet.iterator().next()[0].toString()));
+
+ // HS2-1 crashes
+ miniHs2First.stop();
+
+ // HS2-2 comes up — same shared store
+ miniHs2Second.start(confOverlay);
+ CLIServiceClient client2 = miniHs2Second.getServiceClient();
+
+ // Retry the query with the SAME session handle on HS2-2
+ // Without persistable sessions this would throw "Invalid SessionHandle"
+ opHandle = client2.executeStatement(sessHandle,
+ "SELECT count(*) FROM retry_test", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertTrue(rowSet.numRows() > 0);
+ assertEquals(3L, Long.parseLong(rowSet.iterator().next()[0].toString()));
+
+ // Further queries on the recovered session also work
+ opHandle = client2.executeStatement(sessHandle,
+ "SELECT val FROM retry_test WHERE id = 2", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertTrue(rowSet.numRows() > 0);
+ assertEquals("two", rowSet.iterator().next()[0].toString());
+
+ // Cleanup
+ client2.executeStatement(sessHandle, "DROP TABLE retry_test", confOverlay);
+ client2.closeSession(sessHandle);
+ }
+
+ @Test(timeout = 120000)
+ public void testSnapshotWipedOnSessionClose() throws Exception {
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+ executeStatementAndWait(client1, sessHandle, "SET
hive.exec.dynamic.partition=true", confOverlay);
+
+ SessionStateStore verifyStore = createVerifyStore();
+ String storeKey =
sessHandle.getHandleIdentifier().getPublicId().toString() + ":"
+ + sessHandle.getHandleIdentifier().getSecretId().toString();
+ assertNotNull("Snapshot should exist before close",
verifyStore.getSnapshot(storeKey));
+
+ client1.closeSession(sessHandle);
+
+ assertNull("Snapshot should be wiped after session close",
verifyStore.getSnapshot(storeKey));
+ verifyStore.close();
+ }
+
+ @Test(timeout = 120000)
+ public void testTempTablesAcrossDifferentDatabasesRecovered() throws
Exception {
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+
+ // Create two databases and a temp table in each
+ executeStatementAndWait(client1, sessHandle, "CREATE DATABASE IF NOT
EXISTS db_alpha", confOverlay);
+ executeStatementAndWait(client1, sessHandle, "CREATE DATABASE IF NOT
EXISTS db_beta", confOverlay);
+
+ executeStatementAndWait(client1, sessHandle, "USE db_alpha", confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "CREATE TEMPORARY TABLE tmp_cross_db (id INT, label STRING)",
confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "INSERT INTO tmp_cross_db VALUES (1, 'alpha_row')", confOverlay);
+
+ executeStatementAndWait(client1, sessHandle, "USE db_beta", confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "CREATE TEMPORARY TABLE tmp_cross_db (id INT, label STRING)",
confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "INSERT INTO tmp_cross_db VALUES (2, 'beta_row')", confOverlay);
+
+ // Failover
+ miniHs2First.stop();
+ miniHs2Second.start(confOverlay);
+
+ CLIServiceClient client2 = miniHs2Second.getServiceClient();
+
+ // Verify the temp table in db_alpha has the correct data
+ client2.executeStatement(sessHandle, "USE db_alpha", confOverlay);
+ OperationHandle opHandle = client2.executeStatement(sessHandle,
+ "SELECT id, label FROM tmp_cross_db", confOverlay);
+ RowSet rowSet = client2.fetchResults(opHandle);
+ assertEquals(1, rowSet.numRows());
+ Object[] row = rowSet.iterator().next();
+ assertEquals("1", row[0].toString());
+ assertEquals("alpha_row", row[1].toString());
+
+ // Verify the temp table in db_beta has the correct data
+ client2.executeStatement(sessHandle, "USE db_beta", confOverlay);
+ opHandle = client2.executeStatement(sessHandle,
+ "SELECT id, label FROM tmp_cross_db", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertEquals(1, rowSet.numRows());
+ row = rowSet.iterator().next();
+ assertEquals("2", row[0].toString());
+ assertEquals("beta_row", row[1].toString());
+
+ // Verify the current database is restored to what it was before failover
(db_beta)
+ opHandle = client2.executeStatement(sessHandle, "SELECT
current_database()", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertEquals("db_beta", rowSet.iterator().next()[0].toString());
+
+ client2.closeSession(sessHandle);
+ }
+
+ @Test(timeout = 120000)
+ public void testTempTableWithPartitionsAndComplexTypesRecovered() throws
Exception {
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+
+ executeStatementAndWait(client1, sessHandle,
+ "CREATE TEMPORARY TABLE tmp_complex ("
+ + "id INT, "
+ + "info STRUCT<name:STRING,age:INT>, "
+ + "tags ARRAY<STRING>, "
+ + "metadata MAP<STRING,STRING>"
+ + ") PARTITIONED BY (dt STRING) "
+ + "TBLPROPERTIES ('custom.key'='custom.value',
'transient_lastDdlTime'='0')",
+ confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "INSERT INTO tmp_complex PARTITION(dt='2024-01-01') "
+ + "VALUES (1, named_struct('name','alice','age',30), "
+ + "array('tag1','tag2'), map('k1','v1'))",
+ confOverlay);
+
+ // Verify partition metadata is captured in the snapshot after INSERT
+ SessionStateStore verifyStore = createVerifyStore();
+ String storeKey =
sessHandle.getHandleIdentifier().getPublicId().toString() + ":"
+ + sessHandle.getHandleIdentifier().getSecretId().toString();
+ HiveSessionSnapshot snapshot = waitForSnapshotCondition(verifyStore,
storeKey,
+ s -> s.getTempTablePartitionDefinitions() != null
+ && !s.getTempTablePartitionDefinitions().isEmpty(),
+ 10000);
+ assertNotNull("Snapshot should exist after INSERT into partitioned temp
table", snapshot);
+ assertTrue("tempTablePartitionDefinitions should contain partition
metadata for tmp_complex",
+ snapshot.getTempTablePartitionDefinitions().values().stream()
+ .flatMap(List::stream)
+ .anyMatch(p -> p.getValues().contains("2024-01-01")
+ && p.getLocation() != null && !p.getLocation().isEmpty()));
+ verifyStore.close();
+
+ miniHs2First.stop();
+ miniHs2Second.start(confOverlay);
+
+ CLIServiceClient client2 = miniHs2Second.getServiceClient();
+
+ // Verify table schema is recovered with complex types and partitions
+ OperationHandle opHandle = client2.executeStatement(sessHandle,
+ "DESCRIBE tmp_complex", confOverlay);
+ RowSet rowSet = client2.fetchResults(opHandle);
+ assertTrue("Partitioned temp table with complex types should be recovered",
+ rowSet.numRows() > 0);
+
+ // Verify pre-failover data is queryable (partition metadata + LOCATION
restored)
+ opHandle = client2.executeStatement(sessHandle,
+ "SELECT id, info.name, tags[0], metadata['k1'], dt FROM tmp_complex "
+ + "WHERE dt='2024-01-01'", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertEquals(1, rowSet.numRows());
+ Object[] row = rowSet.iterator().next();
+ assertEquals("1", row[0].toString());
+ assertEquals("alice", row[1].toString());
+ assertEquals("tag1", row[2].toString());
+ assertEquals("v1", row[3].toString());
+ assertEquals("2024-01-01", row[4].toString());
+
+ // Verify new inserts still work after recovery
+ executeStatementAndWait(client2, sessHandle,
+ "INSERT INTO tmp_complex PARTITION(dt='2024-02-01') "
+ + "VALUES (2, named_struct('name','bob','age',25), "
+ + "array('x'), map('k2','v2'))",
+ confOverlay);
+ opHandle = client2.executeStatement(sessHandle,
+ "SELECT id, info.name, tags[0], metadata['k2'], dt FROM tmp_complex "
+ + "WHERE dt='2024-02-01'", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertEquals(1, rowSet.numRows());
+ row = rowSet.iterator().next();
+ assertEquals("2", row[0].toString());
+ assertEquals("bob", row[1].toString());
+ assertEquals("x", row[2].toString());
+ assertEquals("v2", row[3].toString());
+ assertEquals("2024-02-01", row[4].toString());
+
+ client2.closeSession(sessHandle);
+ }
+
+ @Test(timeout = 120000)
+ public void testAddedFilesRecoveredAfterFailover() throws Exception {
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+
+ // Create the file before ADD FILE
+ executeStatementAndWait(client1, sessHandle,
+ "CREATE TEMPORARY TABLE tmp_file_helper (line STRING)", confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "INSERT INTO tmp_file_helper VALUES ('hello')", confOverlay);
+
+ executeStatementAndWait(client1, sessHandle,
+ "INSERT OVERWRITE LOCAL DIRECTORY '/tmp/test_persistable_file_dir' "
+ + "SELECT 'test_content' FROM tmp_file_helper LIMIT 1",
confOverlay);
+ executeStatementAndWait(client1, sessHandle,
+ "ADD FILE /tmp/test_persistable_file_dir/000000_0", confOverlay);
+
+ // Verify the snapshot contains the file — poll briefly because the
snapshot save
+ // runs on the background thread after the operation state becomes FINISHED
+ SessionStateStore verifyStore = createVerifyStore();
+ String storeKey =
sessHandle.getHandleIdentifier().getPublicId().toString() + ":"
+ + sessHandle.getHandleIdentifier().getSecretId().toString();
+ HiveSessionSnapshot snapshot = waitForSnapshotCondition(verifyStore,
storeKey,
+ s -> !s.getAddedFiles().isEmpty(), 5000);
+ assertNotNull("Snapshot should exist", snapshot);
+ assertTrue("addedFiles should contain the file",
+ snapshot.getAddedFiles().stream()
+ .anyMatch(f -> f.contains("000000_0")));
+
+ // Failover to second HS2
+ miniHs2First.stop();
+ miniHs2Second.start(confOverlay);
+
+ CLIServiceClient client2 = miniHs2Second.getServiceClient();
+
+ // Verify session is recovered and file resource is available
+ OperationHandle opHandle = client2.executeStatement(sessHandle,
+ "SELECT 1", confOverlay);
+ RowSet rowSet = client2.fetchResults(opHandle);
+ assertEquals(1, rowSet.numRows());
+
+ // Verify the recovered snapshot on the second HS2 also has the file
+ HiveSessionSnapshot recoveredSnapshot = verifyStore.getSnapshot(storeKey);
+ assertNotNull("Snapshot should exist after recovery", recoveredSnapshot);
+ assertTrue("addedFiles should be preserved after recovery",
+ recoveredSnapshot.getAddedFiles().stream()
+ .anyMatch(f -> f.contains("000000_0")));
+
+ client2.closeSession(sessHandle);
+ verifyStore.close();
+ }
+
+ @Test(timeout = 120000)
+ public void testTempFunctionRecoveredAfterFailover() throws Exception {
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+
+ // Register a temporary function using GenericUDFUpper (always on HS2
classpath)
+ executeStatementAndWait(client1, sessHandle,
+ "CREATE TEMPORARY FUNCTION tmp_my_upper AS "
+ + "'org.apache.hadoop.hive.ql.udf.generic.GenericUDFUpper'",
+ confOverlay);
+
+ // Verify snapshot has the function captured — poll briefly because the
snapshot save
+ // runs on the background thread after the operation state becomes FINISHED
+ SessionStateStore verifyStore = createVerifyStore();
+ String storeKey =
sessHandle.getHandleIdentifier().getPublicId().toString() + ":"
+ + sessHandle.getHandleIdentifier().getSecretId().toString();
+ HiveSessionSnapshot snapshot = waitForSnapshotCondition(verifyStore,
storeKey,
+ s -> !s.getTempFunctionDefinitions().isEmpty(), 5000);
+ assertNotNull("Snapshot should exist after CREATE TEMPORARY FUNCTION",
snapshot);
+ assertTrue("tempFunctionDefinitions should contain tmp_my_upper, but got: "
+ + snapshot.getTempFunctionDefinitions(),
+ snapshot.getTempFunctionDefinitions().stream()
+ .anyMatch(d -> d.contains("tmp_my_upper")));
+ verifyStore.close();
+
+ // Verify it works before failover
+ OperationHandle opHandle = client1.executeStatement(sessHandle,
+ "SELECT tmp_my_upper('hello')", confOverlay);
+ RowSet rowSet = client1.fetchResults(opHandle);
+ assertEquals(1, rowSet.numRows());
+ assertEquals("HELLO", rowSet.iterator().next()[0].toString());
+
+ // Failover
+ miniHs2First.stop();
+ miniHs2Second.start(confOverlay);
+
+ CLIServiceClient client2 = miniHs2Second.getServiceClient();
+
+ // Verify the temp function is usable after recovery
+ opHandle = client2.executeStatement(sessHandle,
+ "SELECT tmp_my_upper('recovered')", confOverlay);
+ rowSet = client2.fetchResults(opHandle);
+ assertEquals(1, rowSet.numRows());
+ assertEquals("RECOVERED", rowSet.iterator().next()[0].toString());
+
+ client2.closeSession(sessHandle);
+ }
+
+ @Test(timeout = 120000)
+ public void testAlwaysStrategySyncsFromRemoteWhenStale() throws Exception {
+ hiveConf1.setVar(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_FETCH_STRATEGY,
"ALWAYS");
+ miniHs2First = new
MiniHS2.Builder().withConf(hiveConf1).withHTTPTransport()
+ .cleanupLocalDirOnStartup(false).build();
+
+ Map<String, String> confOverlay = new HashMap<>();
+ miniHs2First.start(confOverlay);
+
+ CLIServiceClient client1 = miniHs2First.getServiceClient();
+ SessionHandle sessHandle = client1.openSession("foo", "bar");
+ executeStatementAndWait(client1, sessHandle, "SET
hive.exec.dynamic.partition=true", confOverlay);
+
+ // Now simulate another HS2 updating the store with a newer snapshot
+ // that has an additional config and a later lastAccessTime
+ SessionStateStore verifyStore = createVerifyStore();
+ String storeKey =
sessHandle.getHandleIdentifier().getPublicId().toString() + ":"
+ + sessHandle.getHandleIdentifier().getSecretId().toString();
+ HiveSessionSnapshot current = verifyStore.getSnapshot(storeKey);
+ assertNotNull(current);
+
+ Map<String, String> updatedConfigs = new HashMap<>();
+ if (current.getOverriddenConfigurations() != null) {
+ updatedConfigs.putAll(current.getOverriddenConfigurations());
+ }
+ updatedConfigs.put("hive.mapred.mode", "strict");
+
+ HiveSessionSnapshot newerSnapshot = HiveSessionSnapshot.builder()
+ .sessionHandleId(current.getSessionHandleId())
+ .username(current.getUsername())
+ .ipAddress(current.getIpAddress())
+ .currentDatabase(current.getCurrentDatabase())
+ .overriddenConfigurations(updatedConfigs)
+ .addedJars(current.getAddedJars() != null ? current.getAddedJars() :
new ArrayList<>())
+ .addedFiles(current.getAddedFiles() != null ? current.getAddedFiles()
: new ArrayList<>())
+ .tempTableDefinitions(current.getTempTableDefinitions())
+
.tempTablePartitionDefinitions(current.getTempTablePartitionDefinitions())
+ .tempFunctionDefinitions(current.getTempFunctionDefinitions() != null
+ ? current.getTempFunctionDefinitions() : new ArrayList<>())
+ .protocolVersion(current.getProtocolVersion())
+ .creationTime(current.getCreationTime())
+ .lastAccessTime(System.currentTimeMillis() + 60000)
+ .build();
+ verifyStore.saveSnapshot(storeKey, newerSnapshot);
+
+ // Access the session again — ALWAYS strategy should detect the remote is
newer and re-hydrate
+ OperationHandle opHandle = client1.executeStatement(sessHandle,
+ "SET hive.mapred.mode", confOverlay);
+ RowSet rowSet = client1.fetchResults(opHandle);
+ assertTrue(rowSet.numRows() > 0);
+ assertTrue("ALWAYS strategy should have synced hive.mapred.mode=strict
from remote",
+ rowSet.iterator().next()[0].toString().contains("strict"));
+
+ client1.closeSession(sessHandle);
+ verifyStore.close();
+ }
+}
diff --git
a/itests/hive-unit/src/test/java/org/apache/hive/service/cli/session/TestPersistableSessionWithRedis.java
b/itests/hive-unit/src/test/java/org/apache/hive/service/cli/session/TestPersistableSessionWithRedis.java
new file mode 100644
index 00000000000..c92296c583d
--- /dev/null
+++
b/itests/hive-unit/src/test/java/org/apache/hive/service/cli/session/TestPersistableSessionWithRedis.java
@@ -0,0 +1,82 @@
+/*
+ * 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.hive.service.cli.session;
+
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.apache.hive.jdbc.miniHS2.MiniHS2;
+import org.apache.hive.service.cli.session.store.RedisSessionStateStore;
+import org.apache.hive.service.cli.session.store.SessionStateStore;
+import org.junit.AfterClass;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+public class TestPersistableSessionWithRedis extends
TestPersistableSessionBase {
+
+ private static GenericContainer<?> redisContainer;
+
+ @BeforeClass
+ public static void beforeTest() throws Exception {
+ MiniHS2.cleanupLocalDir();
+ try {
+ redisContainer = new
GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
+ .withExposedPorts(6379);
+ redisContainer.start();
+ } catch (Exception e) {
+ Assume.assumeTrue("Docker not available, skipping Redis integration
tests", false);
+ }
+ }
+
+ @AfterClass
+ public static void afterTest() throws Exception {
+ if (redisContainer != null) {
+ redisContainer.stop();
+ }
+ MiniHS2.cleanupLocalDir();
+ }
+
+ @Override
+ protected String getStoreClassName() {
+ return "org.apache.hive.service.cli.session.store.RedisSessionStateStore";
+ }
+
+ @Override
+ protected void configureStore(HiveConf conf) {
+ Assume.assumeTrue("Redis container not running",
+ redisContainer != null && redisContainer.isRunning());
+ conf.set(RedisSessionStateStore.CONF_REDIS_HOST, redisContainer.getHost());
+ conf.set(RedisSessionStateStore.CONF_REDIS_PORT,
+ String.valueOf(redisContainer.getMappedPort(6379)));
+ conf.set(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_TTL.varname, "3600s");
+ }
+
+ @Override
+ protected SessionStateStore createVerifyStore() throws Exception {
+ HiveConf verifyConf = new HiveConf();
+ verifyConf.set(RedisSessionStateStore.CONF_REDIS_HOST,
redisContainer.getHost());
+ verifyConf.set(RedisSessionStateStore.CONF_REDIS_PORT,
+ String.valueOf(redisContainer.getMappedPort(6379)));
+ verifyConf.set(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_TTL.varname,
"3600s");
+ RedisSessionStateStore store = new RedisSessionStateStore();
+ store.init(verifyConf);
+ return store;
+ }
+}
diff --git
a/itests/hive-unit/src/test/java/org/apache/hive/service/cli/session/TestPersistableSessionWithZooKeeper.java
b/itests/hive-unit/src/test/java/org/apache/hive/service/cli/session/TestPersistableSessionWithZooKeeper.java
new file mode 100644
index 00000000000..ef6cd3b6d29
--- /dev/null
+++
b/itests/hive-unit/src/test/java/org/apache/hive/service/cli/session/TestPersistableSessionWithZooKeeper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.hive.service.cli.session;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.curator.test.TestingServer;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.apache.hive.jdbc.miniHS2.MiniHS2;
+import org.apache.hive.service.cli.session.store.SessionStateStore;
+import org.apache.hive.service.cli.session.store.ZooKeeperSessionStateStore;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class TestPersistableSessionWithZooKeeper extends
TestPersistableSessionBase {
+
+ private static TestingServer zkServer;
+ private static final String ZK_SESSION_PATH = "/test_persistable_sessions";
+
+ @BeforeClass
+ public static void beforeTest() throws Exception {
+ System.setProperty("zookeeper.extendedTypesEnabled", "true");
+ MiniHS2.cleanupLocalDir();
+ zkServer = new TestingServer();
+ zkServer.start();
+ }
+
+ @AfterClass
+ public static void afterTest() throws Exception {
+ if (zkServer != null) {
+ zkServer.close();
+ zkServer = null;
+ }
+ MiniHS2.cleanupLocalDir();
+ }
+
+ @Override
+ protected String getStoreClassName() {
+ return
"org.apache.hive.service.cli.session.store.ZooKeeperSessionStateStore";
+ }
+
+ @Override
+ protected void configureStore(HiveConf conf) {
+ conf.setVar(ConfVars.HIVE_ZOOKEEPER_QUORUM, zkServer.getConnectString());
+ conf.set(ZooKeeperSessionStateStore.CONF_ZK_PATH, ZK_SESSION_PATH);
+ conf.setTimeDuration(ConfVars.HIVE_ZOOKEEPER_SESSION_TIMEOUT.varname, 2,
TimeUnit.SECONDS);
+ conf.setTimeDuration(ConfVars.HIVE_ZOOKEEPER_CONNECTION_TIMEOUT.varname,
2, TimeUnit.SECONDS);
+
conf.setTimeDuration(ConfVars.HIVE_ZOOKEEPER_CONNECTION_BASESLEEPTIME.varname,
+ 100, TimeUnit.MILLISECONDS);
+ conf.setInt(ConfVars.HIVE_ZOOKEEPER_CONNECTION_MAX_RETRIES.varname, 1);
+ }
+
+ @Override
+ protected SessionStateStore createVerifyStore() throws Exception {
+ HiveConf verifyConf = new HiveConf();
+ verifyConf.setVar(ConfVars.HIVE_ZOOKEEPER_QUORUM,
zkServer.getConnectString());
+ verifyConf.set(ZooKeeperSessionStateStore.CONF_ZK_PATH, ZK_SESSION_PATH);
+
verifyConf.setTimeDuration(ConfVars.HIVE_ZOOKEEPER_SESSION_TIMEOUT.varname, 2,
TimeUnit.SECONDS);
+
verifyConf.setTimeDuration(ConfVars.HIVE_ZOOKEEPER_CONNECTION_TIMEOUT.varname,
2, TimeUnit.SECONDS);
+
verifyConf.setTimeDuration(ConfVars.HIVE_ZOOKEEPER_CONNECTION_BASESLEEPTIME.varname,
+ 100, TimeUnit.MILLISECONDS);
+ verifyConf.setInt(ConfVars.HIVE_ZOOKEEPER_CONNECTION_MAX_RETRIES.varname,
1);
+ ZooKeeperSessionStateStore store = new ZooKeeperSessionStateStore();
+ store.init(verifyConf);
+ return store;
+ }
+}
diff --git a/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
b/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
index 60bb39eea4b..888b5ca5c8c 100644
--- a/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
+++ b/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
@@ -307,11 +307,23 @@ public HiveConnection(HiveConnection hiveConnection)
throws SQLException {
this.fetchThreads = hiveConnection.fetchThreads;
}
- @VisibleForTesting
protected int getNumRetries() {
return maxRetries;
}
+ boolean isPersistableSession() {
+ String varname =
ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_FETCH_STRATEGY.varname;
+ String prefixedKey = Utils.JdbcConnectionParams.HIVE_CONF_PREFIX + varname;
+ String fetchStrategy = connParams.getHiveConfs().get(prefixedKey);
+ if (fetchStrategy == null) {
+ fetchStrategy = connParams.getHiveConfs().get(varname);
+ }
+ if (fetchStrategy == null) {
+ fetchStrategy = connParams.getSessionVars().get(prefixedKey);
+ }
+ return fetchStrategy != null && !"NEVER".equalsIgnoreCase(fetchStrategy);
+ }
+
@VisibleForTesting
protected HiveConnection(String uri, Properties info,
IJdbcBrowserClientFactory browserClientFactory) throws SQLException {
@@ -561,6 +573,18 @@ private void openTransport() throws Exception {
logZkDiscoveryMessage("Connected to " + connParams.getHost() + ":" +
connParams.getPort());
}
+ void reconnect() throws SQLException {
+ try {
+ if (transport != null && transport.isOpen()) {
+ transport.close();
+ }
+ openTransport();
+ client = newSynchronizedClient(new TCLIService.Client(new
TBinaryProtocol(transport)));
+ } catch (Exception e) {
+ throw new SQLException("Failed to reconnect transport", "08S01", e);
+ }
+ }
+
public String getConnectedUrl() {
return jdbcUriString;
}
diff --git a/jdbc/src/java/org/apache/hive/jdbc/HiveStatement.java
b/jdbc/src/java/org/apache/hive/jdbc/HiveStatement.java
index 0d0463a88f7..cdd4d23aa89 100644
--- a/jdbc/src/java/org/apache/hive/jdbc/HiveStatement.java
+++ b/jdbc/src/java/org/apache/hive/jdbc/HiveStatement.java
@@ -35,6 +35,7 @@
import org.apache.hive.service.rpc.thrift.TFetchResultsReq;
import org.apache.hive.service.rpc.thrift.TFetchResultsResp;
import org.apache.hive.service.rpc.thrift.TGetOperationStatusReq;
+import org.apache.hive.service.rpc.thrift.TStatus;
import org.apache.hive.service.rpc.thrift.TGetOperationStatusResp;
import org.apache.hive.service.rpc.thrift.TGetQueryIdReq;
import org.apache.hive.service.rpc.thrift.TOperationHandle;
@@ -124,6 +125,8 @@ public class HiveStatement implements java.sql.Statement {
private int queryTimeout = 0;
+ private String lastSql;
+
private Optional<InPlaceUpdateStream> inPlaceUpdateStream;
public HiveStatement(HiveConnection connection, TCLIService.Iface client,
@@ -207,7 +210,11 @@ private void closeStatementIfNeeded() throws SQLException {
}
}
} catch (SQLException e) {
- throw e;
+ if (connection.isPersistableSession() &&
isInvalidOperationHandleError(e)) {
+ LOG.warn("Ignoring Invalid OperationHandle during close after
failover: {}", e.getMessage());
+ } else {
+ throw e;
+ }
} catch (TApplicationException tae) {
String errorMsg = "Failed to close statement";
if (tae.getType() == TApplicationException.BAD_SEQUENCE_ID) {
@@ -217,7 +224,12 @@ private void closeStatementIfNeeded() throws SQLException {
}
throw new SQLException(errorMsg, "08S01", tae);
} catch (Exception e) {
- throw new SQLException("Failed to close statement", "08S01", e);
+ if (connection.isPersistableSession()) {
+ LOG.warn("Ignoring close failure for persistable session (server
unreachable): {}",
+ e.getMessage());
+ } else {
+ throw new SQLException("Failed to close statement", "08S01", e);
+ }
} finally {
stmtHandle = Optional.empty();
}
@@ -230,13 +242,24 @@ private void closeStatementIfNeeded() throws SQLException
{
* @return true, if the response from server contains "Invalid
OperationHandle"
*/
private boolean checkInvalidOperationHandle(TCloseOperationResp closeResp) {
- List<String> messages = closeResp.getStatus().getInfoMessages();
- if (messages != null && messages.size() > 0) {
+ TStatus status = closeResp.getStatus();
+ if (status == null) {
+ return false;
+ }
+ if (status.isSetErrorMessage()) {
+ String errorMsg = status.getErrorMessage();
+ if (errorMsg.contains("Invalid OperationHandle") ||
errorMsg.contains("Operation does not exist")) {
+ LOG.warn("Ignoring benign close operation error: {}", errorMsg);
+ return true;
+ }
+ }
+ List<String> messages = status.getInfoMessages();
+ if (messages != null && !messages.isEmpty()) {
/*
* Here we need to handle 2 different cases, which can happen in
CLIService.closeOperation, which actually does:
*
sessionManager.getOperationManager().getOperation(opHandle).getParentSession().closeOperation(opHandle);
*/
- String message = messages.get(0);
+ String message = messages.getFirst();
if (message.contains("Invalid OperationHandle")) {
/*
* This happens when the first request properly removes the operation
handle, then second request arrives, calls
@@ -255,7 +278,6 @@ private boolean
checkInvalidOperationHandle(TCloseOperationResp closeResp) {
return true;
}
}
-
return false;
}
@@ -342,10 +364,13 @@ public boolean executeAsync(String sql) throws
SQLException {
return true;
}
+ private static final String DECOMMISSIONED_ERROR = "HiveServer2 is
decommissioned or inactive";
+
private void runAsyncOnServer(String sql) throws SQLException {
checkConnection("execute");
reInitState();
+ this.lastSql = sql;
TExecuteStatementReq execReq = new TExecuteStatementReq(sessHandle, sql);
/**
@@ -357,27 +382,57 @@ private void runAsyncOnServer(String sql) throws
SQLException {
execReq.setRunAsync(true);
execReq.setConfOverlay(sessConf);
execReq.setQueryTimeout(queryTimeout);
- try {
- LOG.debug("Submitting statement [{}]: {}", sessHandle, sql);
- TExecuteStatementResp execResp = client.ExecuteStatement(execReq);
- Utils.verifySuccessWithInfo(execResp.getStatus());
- List<String> infoMessages = execResp.getStatus().getInfoMessages();
- if (infoMessages != null) {
- for (String message : infoMessages) {
- LOG.info(message);
+
+ int maxRetries = connection.getNumRetries();
+ for (int attempt = 0; ; attempt++) {
+ try {
+ LOG.debug("Submitting statement [{}]: {}", sessHandle, sql);
+ TExecuteStatementResp execResp = client.ExecuteStatement(execReq);
+ Utils.verifySuccessWithInfo(execResp.getStatus());
+ List<String> infoMessages = execResp.getStatus().getInfoMessages();
+ if (infoMessages != null) {
+ for (String message : infoMessages) {
+ LOG.info(message);
+ }
+ }
+ stmtHandle = Optional.of(execResp.getOperationHandle());
+ LOG.debug("Running with statement handle: {}", stmtHandle.get());
+ return;
+ } catch (SQLException eS) {
+ if (isDecommissionedError(eS) && attempt < maxRetries &&
connection.isPersistableSession()) {
+ LOG.warn("HiveServer2 is decommissioned. Reconnecting and retrying
attempt {} of {}.",
+ attempt + 1, maxRetries);
+ try {
+ Thread.sleep(1000L);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ isLogBeingGenerated = false;
+ throw eS;
+ }
+ try {
+ connection.reconnect();
+ client = connection.getClient();
+ } catch (SQLException reconnectEx) {
+ LOG.error("Failed to reconnect after decommissioning error",
reconnectEx);
+ isLogBeingGenerated = false;
+ throw eS;
+ }
+ continue;
}
+ isLogBeingGenerated = false;
+ throw eS;
+ } catch (Exception ex) {
+ isLogBeingGenerated = false;
+ throw new SQLException("Failed to run async statement", "08S01", ex);
}
- stmtHandle = Optional.of(execResp.getOperationHandle());
- LOG.debug("Running with statement handle: {}", stmtHandle.get());
- } catch (SQLException eS) {
- isLogBeingGenerated = false;
- throw eS;
- } catch (Exception ex) {
- isLogBeingGenerated = false;
- throw new SQLException("Failed to run async statement", "08S01", ex);
}
}
+ private static boolean isDecommissionedError(SQLException e) {
+ String msg = e.getMessage();
+ return msg != null && msg.contains(DECOMMISSIONED_ERROR);
+ }
+
/**
* Poll the result set status by checking if isSetHasResultSet is set
* @return
@@ -457,7 +512,7 @@ private void
processOperationStatusResponse(TGetOperationStatusResp statusResp)
TGetOperationStatusResp waitForOperationToComplete() throws SQLException {
TGetOperationStatusResp statusResp = null;
- final TGetOperationStatusReq statusReq = new
TGetOperationStatusReq(stmtHandle.get());
+ TGetOperationStatusReq statusReq = new
TGetOperationStatusReq(stmtHandle.get());
boolean progressUpdates = inPlaceUpdateStream.isPresent();
statusReq.setGetProgressUpdate(progressUpdates);
@@ -467,6 +522,9 @@ TGetOperationStatusResp waitForOperationToComplete() throws
SQLException {
LOG.debug("Waiting on operation to complete: Polling operation status");
+ int failoverRetries = 0;
+ int maxFailoverRetries = connection.getNumRetries();
+
do {
try {
if (Thread.currentThread().isInterrupted()) {
@@ -481,8 +539,32 @@ TGetOperationStatusResp waitForOperationToComplete()
throws SQLException {
LOG.debug("Status response: {}", statusResp);
processOperationStatusResponse(statusResp);
} catch (SQLException e) {
+ if (connection.isPersistableSession() && lastSql != null
+ && failoverRetries < maxFailoverRetries
+ && (isInvalidOperationHandleError(e) ||
isRetriableExecutionError(e))) {
+ failoverRetries++;
+ LOG.info("Operation lost after failover, reconnecting and
re-executing (attempt {} of {}): {}",
+ failoverRetries, maxFailoverRetries, lastSql);
+ retryAfterFailover();
+ statusReq = new TGetOperationStatusReq(stmtHandle.get());
+ statusReq.setGetProgressUpdate(progressUpdates);
+ continue;
+ }
isLogBeingGenerated = false;
throw e;
+ } catch (TException e) {
+ if (connection.isPersistableSession() && lastSql != null
+ && failoverRetries < maxFailoverRetries && isTransportError(e)) {
+ failoverRetries++;
+ LOG.info("Connection lost while polling operation status,
reconnecting and re-executing "
+ + "(attempt {} of {}): {}", failoverRetries, maxFailoverRetries,
lastSql);
+ retryAfterFailover();
+ statusReq = new TGetOperationStatusReq(stmtHandle.get());
+ statusReq.setGetProgressUpdate(progressUpdates);
+ continue;
+ }
+ isLogBeingGenerated = false;
+ throw new SQLException("Failed to wait for operation to complete",
"08S01", e);
} catch (Exception e) {
isLogBeingGenerated = false;
throw new SQLException("Failed to wait for operation to complete",
"08S01", e);
@@ -495,6 +577,49 @@ TGetOperationStatusResp waitForOperationToComplete()
throws SQLException {
return statusResp;
}
+ /**
+ * Reconnects to HS2 and re-submits the last SQL after session failover.
+ * Only used when the in-flight operation handle is lost (HS2 crash /
reconnect).
+ */
+ private void retryAfterFailover() throws SQLException {
+ try {
+ connection.reconnect();
+ client = connection.getClient();
+ } catch (SQLException e) {
+ LOG.error("Failed to reconnect after failover", e);
+ throw e;
+ }
+ stmtHandle = Optional.empty();
+ runAsyncOnServer(lastSql);
+ }
+
+ private static boolean isInvalidOperationHandleError(SQLException e) {
+ String msg = e.getMessage();
+ return msg != null && msg.contains("Invalid OperationHandle");
+ }
+
+ private static boolean isRetriableExecutionError(SQLException e) {
+ String msg = e.getMessage();
+ return msg != null && msg.contains("Execution Error");
+ }
+
+ private static boolean isTransportError(Throwable t) {
+ while (t != null) {
+ String name = t.getClass().getName();
+ if (name.contains("NoHttpResponseException")
+ || name.contains("SocketException")
+ || name.contains("ConnectException")
+ || name.contains("TTransportException")
+ || name.contains("SSLException")
+ || name.contains("ConnectionClosedException")) {
+ return true;
+ }
+ t = t.getCause();
+ }
+ return false;
+ }
+
+
private void checkConnection(String action) throws SQLException {
if (isClosed) {
throw new SQLException("Cannot " + action + " after statement has been
closed");
diff --git a/jdbc/src/test/org/apache/hive/jdbc/TestHiveStatement.java
b/jdbc/src/test/org/apache/hive/jdbc/TestHiveStatement.java
index 95dfd7a6fe6..eb6e8dbed83 100644
--- a/jdbc/src/test/org/apache/hive/jdbc/TestHiveStatement.java
+++ b/jdbc/src/test/org/apache/hive/jdbc/TestHiveStatement.java
@@ -19,14 +19,22 @@
package org.apache.hive.jdbc;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hive.service.rpc.thrift.TCLIService.Iface;
+import org.apache.hive.service.rpc.thrift.TExecuteStatementReq;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hive.service.rpc.thrift.TSessionHandle;
import org.junit.Test;
@@ -140,4 +148,49 @@ public void testaddBatch() throws SQLException {
stmt.addBatch(null);
}
}
+
+ @Test
+ public void testDecommissionRetryWhenPersistableSessionEnabled() throws
Exception {
+ verifyDecommissionRetryBehavior(true);
+ }
+
+ @Test
+ public void testDecommissionNoRetryWhenPersistableSessionDisabled() throws
Exception {
+ verifyDecommissionRetryBehavior(false);
+ }
+
+ private void verifyDecommissionRetryBehavior(boolean persistableEnabled)
throws Exception {
+ final HiveConnection connection = mock(HiveConnection.class);
+ final Iface client = mock(Iface.class);
+ final TSessionHandle handle = mock(TSessionHandle.class);
+ connection.fetchSize = 100;
+
+ when(connection.getNumRetries()).thenReturn(1);
+ when(connection.isPersistableSession()).thenReturn(persistableEnabled);
+ when(connection.getClient()).thenReturn(client);
+
+ AtomicInteger callCount = new AtomicInteger(0);
+
when(client.ExecuteStatement(any(TExecuteStatementReq.class))).thenAnswer(invocation
-> {
+ if (callCount.getAndIncrement() == 0) {
+ throw new SQLException(
+ "Unable to run new queries as HiveServer2 is decommissioned or
inactive");
+ }
+ throw new SQLException("Some other error after reconnect");
+ });
+
+ try (HiveStatement stmt = new HiveStatement(connection, client, handle,
false, 100)) {
+ stmt.executeAsync("SELECT 1");
+ fail("Expected SQLException");
+ } catch (SQLException e) {
+ if (persistableEnabled) {
+ assertEquals("Some other error after reconnect", e.getMessage());
+ } else {
+ assertTrue(e.getMessage().contains("decommissioned or inactive"));
+ }
+ }
+
+ int expectedCalls = persistableEnabled ? 2 : 1;
+ verify(connection, times(persistableEnabled ? 1 : 0)).reconnect();
+ verify(client,
times(expectedCalls)).ExecuteStatement(any(TExecuteStatementReq.class));
+ }
}
diff --git a/packaging/src/main/assembly/src.xml
b/packaging/src/main/assembly/src.xml
index fac8a76515f..a7eb633f8e0 100644
--- a/packaging/src/main/assembly/src.xml
+++ b/packaging/src/main/assembly/src.xml
@@ -100,6 +100,7 @@
<include>ql/**/*</include>
<include>serde/**/*</include>
<include>service-rpc/**/*</include>
+ <include>service-session-store/**/*</include>
<include>service/**/*</include>
<include>shims/**/*</include>
<include>storage-api/**/*</include>
diff --git a/pom.xml b/pom.xml
index 14f82f05b0b..2313c30e5e2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,6 +51,7 @@
<module>serde</module>
<module>service-rpc</module>
<module>service</module>
+ <module>service-session-store</module>
<module>streaming</module>
<module>llap-common</module>
<module>llap-client</module>
@@ -249,6 +250,7 @@
<project.build.outputTimestamp>2025-01-01T00:00:00Z</project.build.outputTimestamp>
<keycloak.version>26.0.6</keycloak.version>
<nimbus-oauth.version>11.28</nimbus-oauth.version>
+ <jedis.version>5.1.0</jedis.version>
</properties>
<repositories>
<!-- This needs to be removed before checking in-->
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/session/SessionState.java
b/ql/src/java/org/apache/hadoop/hive/ql/session/SessionState.java
index 021a7f63e9b..b31ddf6c26a 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/session/SessionState.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/session/SessionState.java
@@ -833,7 +833,9 @@ private void createSessionDirs(String userName) throws
IOException {
String sessionId = getSessionId();
// 4. HDFS session path
hdfsSessionPath = new Path(hdfsScratchDirURIString, sessionId);
- createPath(conf, hdfsSessionPath, scratchDirPermission, false, true);
+ boolean deleteOnExit = StringUtils.isBlank(
+ conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_CLASS));
+ createPath(conf, hdfsSessionPath, scratchDirPermission, false,
deleteOnExit);
conf.set(HDFS_SESSION_PATH_KEY, hdfsSessionPath.toUri().toString());
// 5. hold a lock file in HDFS session dir to indicate the it is in use
if (conf.getBoolVar(HiveConf.ConfVars.HIVE_SCRATCH_DIR_LOCK)) {
@@ -1137,6 +1139,10 @@ public static Registry getRegistry() {
return session != null ? session.registry : null;
}
+ public Registry getSessionRegistry() {
+ return registry;
+ }
+
public static Registry getRegistryForWrite() {
Registry registry = getRegistry();
if (registry == null) {
diff --git a/service-session-store/pom.xml b/service-session-store/pom.xml
new file mode 100644
index 00000000000..3ad6a9ec701
--- /dev/null
+++ b/service-session-store/pom.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.hive</groupId>
+ <artifactId>hive</artifactId>
+ <version>4.3.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <artifactId>hive-service-session-store</artifactId>
+ <packaging>jar</packaging>
+ <name>Hive Service Session Store</name>
+ <properties>
+ <hive.path.to.root>..</hive.path.to.root>
+ </properties>
+ <dependencies>
+ <!-- intra-project -->
+ <dependency>
+ <groupId>org.apache.hive</groupId>
+ <artifactId>hive-common</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <!-- Jackson for JSON serialization -->
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+ <!-- ZooKeeper implementation (provided - add to HS2 classpath if needed)
-->
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-client</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-framework</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-recipes</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <!-- Redis implementation (provided - add to HS2 classpath if needed) -->
+ <dependency>
+ <groupId>redis.clients</groupId>
+ <artifactId>jedis</artifactId>
+ <version>${jedis.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <!-- Logging -->
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-mapreduce-client-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git
a/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/HiveSessionSnapshot.java
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/HiveSessionSnapshot.java
new file mode 100644
index 00000000000..8c10772cd78
--- /dev/null
+++
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/HiveSessionSnapshot.java
@@ -0,0 +1,237 @@
+/*
+ * 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.hive.service.cli.session.store;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ArrayList;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class HiveSessionSnapshot {
+
+ private final String sessionHandleId;
+ private final String username;
+ private final String ipAddress;
+ private final String currentDatabase;
+ private final Map<String, String> overriddenConfigurations;
+ private final List<String> addedJars;
+ private final List<String> addedFiles;
+ private final Map<String, String> tempTableDefinitions;
+ private final Map<String, List<TempTablePartitionSnapshot>>
tempTablePartitionDefinitions;
+ private final List<String> tempFunctionDefinitions;
+ private final int protocolVersion;
+ private final long creationTime;
+ private final long lastAccessTime;
+
+ @JsonCreator
+ public HiveSessionSnapshot(
+ @JsonProperty("sessionHandleId") String sessionHandleId,
+ @JsonProperty("username") String username,
+ @JsonProperty("ipAddress") String ipAddress,
+ @JsonProperty("currentDatabase") String currentDatabase,
+ @JsonProperty("overriddenConfigurations") Map<String, String>
overriddenConfigurations,
+ @JsonProperty("addedJars") List<String> addedJars,
+ @JsonProperty("addedFiles") List<String> addedFiles,
+ @JsonProperty("tempTableDefinitions") Map<String, String>
tempTableDefinitions,
+ @JsonProperty("tempTablePartitionDefinitions") Map<String,
List<TempTablePartitionSnapshot>> tempTablePartitionDefinitions,
+ @JsonProperty("tempFunctionDefinitions") List<String>
tempFunctionDefinitions,
+ @JsonProperty("protocolVersion") int protocolVersion,
+ @JsonProperty("creationTime") long creationTime,
+ @JsonProperty("lastAccessTime") long lastAccessTime) {
+ this.sessionHandleId = sessionHandleId;
+ this.username = username;
+ this.ipAddress = ipAddress;
+ this.currentDatabase = currentDatabase;
+ this.overriddenConfigurations = overriddenConfigurations != null
+ ? new HashMap<>(overriddenConfigurations) : Collections.emptyMap();
+ this.addedJars = addedJars != null ? new ArrayList<>(addedJars) :
Collections.emptyList();
+ this.addedFiles = addedFiles != null ? new ArrayList<>(addedFiles) :
Collections.emptyList();
+ this.tempTableDefinitions = tempTableDefinitions != null
+ ? new HashMap<>(tempTableDefinitions) : Collections.emptyMap();
+ this.tempTablePartitionDefinitions = tempTablePartitionDefinitions != null
+ ? new HashMap<>(tempTablePartitionDefinitions) :
Collections.emptyMap();
+ this.tempFunctionDefinitions = tempFunctionDefinitions != null
+ ? new ArrayList<>(tempFunctionDefinitions) : Collections.emptyList();
+ this.protocolVersion = protocolVersion;
+ this.creationTime = creationTime;
+ this.lastAccessTime = lastAccessTime;
+ }
+
+ @JsonProperty("sessionHandleId")
+ public String getSessionHandleId() {
+ return sessionHandleId;
+ }
+
+ @JsonProperty("username")
+ public String getUsername() {
+ return username;
+ }
+
+ @JsonProperty("ipAddress")
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ @JsonProperty("currentDatabase")
+ public String getCurrentDatabase() {
+ return currentDatabase;
+ }
+
+ @JsonProperty("overriddenConfigurations")
+ public Map<String, String> getOverriddenConfigurations() {
+ return overriddenConfigurations;
+ }
+
+ @JsonProperty("addedJars")
+ public List<String> getAddedJars() {
+ return addedJars;
+ }
+
+ @JsonProperty("addedFiles")
+ public List<String> getAddedFiles() {
+ return addedFiles;
+ }
+
+ @JsonProperty("tempTableDefinitions")
+ public Map<String, String> getTempTableDefinitions() {
+ return tempTableDefinitions;
+ }
+
+ @JsonProperty("tempTablePartitionDefinitions")
+ public Map<String, List<TempTablePartitionSnapshot>>
getTempTablePartitionDefinitions() {
+ return tempTablePartitionDefinitions;
+ }
+
+ @JsonProperty("tempFunctionDefinitions")
+ public List<String> getTempFunctionDefinitions() {
+ return tempFunctionDefinitions;
+ }
+
+ @JsonProperty("protocolVersion")
+ public int getProtocolVersion() {
+ return protocolVersion;
+ }
+
+ @JsonProperty("creationTime")
+ public long getCreationTime() {
+ return creationTime;
+ }
+
+ @JsonProperty("lastAccessTime")
+ public long getLastAccessTime() {
+ return lastAccessTime;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String sessionHandleId;
+ private String username;
+ private String ipAddress;
+ private String currentDatabase;
+ private Map<String, String> overriddenConfigurations;
+ private List<String> addedJars;
+ private List<String> addedFiles;
+ private Map<String, String> tempTableDefinitions;
+ private Map<String, List<TempTablePartitionSnapshot>>
tempTablePartitionDefinitions;
+ private List<String> tempFunctionDefinitions;
+ private int protocolVersion;
+ private long creationTime;
+ private long lastAccessTime;
+
+ public Builder sessionHandleId(String sessionHandleId) {
+ this.sessionHandleId = sessionHandleId;
+ return this;
+ }
+
+ public Builder username(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public Builder ipAddress(String ipAddress) {
+ this.ipAddress = ipAddress;
+ return this;
+ }
+
+ public Builder currentDatabase(String currentDatabase) {
+ this.currentDatabase = currentDatabase;
+ return this;
+ }
+
+ public Builder overriddenConfigurations(Map<String, String>
overriddenConfigurations) {
+ this.overriddenConfigurations = overriddenConfigurations;
+ return this;
+ }
+
+ public Builder addedJars(List<String> addedJars) {
+ this.addedJars = addedJars;
+ return this;
+ }
+
+ public Builder addedFiles(List<String> addedFiles) {
+ this.addedFiles = addedFiles;
+ return this;
+ }
+
+ public Builder tempTableDefinitions(Map<String, String>
tempTableDefinitions) {
+ this.tempTableDefinitions = tempTableDefinitions;
+ return this;
+ }
+
+ public Builder tempTablePartitionDefinitions(
+ Map<String, List<TempTablePartitionSnapshot>>
tempTablePartitionDefinitions) {
+ this.tempTablePartitionDefinitions = tempTablePartitionDefinitions;
+ return this;
+ }
+
+ public Builder tempFunctionDefinitions(List<String>
tempFunctionDefinitions) {
+ this.tempFunctionDefinitions = tempFunctionDefinitions;
+ return this;
+ }
+
+ public Builder protocolVersion(int protocolVersion) {
+ this.protocolVersion = protocolVersion;
+ return this;
+ }
+
+ public Builder creationTime(long creationTime) {
+ this.creationTime = creationTime;
+ return this;
+ }
+
+ public Builder lastAccessTime(long lastAccessTime) {
+ this.lastAccessTime = lastAccessTime;
+ return this;
+ }
+
+ public HiveSessionSnapshot build() {
+ return new HiveSessionSnapshot(sessionHandleId, username, ipAddress,
currentDatabase,
+ overriddenConfigurations, addedJars, addedFiles,
tempTableDefinitions,
+ tempTablePartitionDefinitions, tempFunctionDefinitions,
protocolVersion, creationTime,
+ lastAccessTime);
+ }
+ }
+}
diff --git
a/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/RedisSessionStateStore.java
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/RedisSessionStateStore.java
new file mode 100644
index 00000000000..08bda6abfa5
--- /dev/null
+++
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/RedisSessionStateStore.java
@@ -0,0 +1,125 @@
+/*
+ * 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.hive.service.cli.session.store;
+
+import java.util.concurrent.TimeUnit;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import redis.clients.jedis.DefaultJedisClientConfig;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.Jedis;
+
+public class RedisSessionStateStore implements SessionStateStore {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(RedisSessionStateStore.class);
+ private static final String KEY_PREFIX = "hive:session:";
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ public static final String CONF_REDIS_HOST =
"hive.server2.session.state.store.redis.host";
+ public static final String CONF_REDIS_HOST_DEFAULT = "localhost";
+ public static final String CONF_REDIS_PORT =
"hive.server2.session.state.store.redis.port";
+ public static final int CONF_REDIS_PORT_DEFAULT = 6379;
+ public static final String CONF_REDIS_PASSWORD =
"hive.server2.session.state.store.redis.password";
+ public static final String CONF_REDIS_SSL =
"hive.server2.session.state.store.redis.ssl";
+
+ private JedisPool jedisPool;
+ private long ttlSeconds;
+
+ @Override
+ public void init(HiveConf conf) {
+ String host = conf.get(CONF_REDIS_HOST, CONF_REDIS_HOST_DEFAULT);
+ int port = Integer.parseInt(conf.get(CONF_REDIS_PORT,
+ String.valueOf(CONF_REDIS_PORT_DEFAULT)));
+ String password = conf.get(CONF_REDIS_PASSWORD);
+ boolean useSsl = Boolean.parseBoolean(conf.get(CONF_REDIS_SSL, "false"));
+ this.ttlSeconds = conf.getTimeVar(
+ ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_TTL, TimeUnit.SECONDS);
+
+ JedisPoolConfig poolConfig = new JedisPoolConfig();
+ poolConfig.setMaxTotal(16);
+ poolConfig.setMaxIdle(8);
+ poolConfig.setMinIdle(2);
+
+ DefaultJedisClientConfig.Builder clientConfigBuilder =
DefaultJedisClientConfig.builder();
+ if (password != null && !password.isEmpty()) {
+ clientConfigBuilder.password(password);
+ }
+ if (useSsl) {
+ clientConfigBuilder.ssl(true);
+ }
+ jedisPool = new JedisPool(poolConfig, new HostAndPort(host, port),
+ clientConfigBuilder.build());
+ LOG.info("Initialized RedisSessionStateStore with host={}:{}, ssl={},
ttl={}s",
+ host, port, useSsl, ttlSeconds);
+ }
+
+ @Override
+ public void saveSnapshot(String sessionHandleId, HiveSessionSnapshot
snapshot) {
+ String key = KEY_PREFIX + sessionHandleId;
+ try (Jedis jedis = jedisPool.getResource()) {
+ String json = OBJECT_MAPPER.writeValueAsString(snapshot);
+ jedis.setex(key, ttlSeconds, json);
+ LOG.debug("Saved session snapshot to Redis: {}", key);
+ } catch (Exception e) {
+ LOG.error("Failed to save session snapshot to Redis: {}", key, e);
+ throw new RuntimeException("Failed to save session snapshot", e);
+ }
+ }
+
+ @Override
+ public HiveSessionSnapshot getSnapshot(String sessionHandleId) {
+ String key = KEY_PREFIX + sessionHandleId;
+ try (Jedis jedis = jedisPool.getResource()) {
+ String json = jedis.get(key);
+ if (json == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.readValue(json, HiveSessionSnapshot.class);
+ } catch (Exception e) {
+ LOG.error("Failed to get session snapshot from Redis: {}", key, e);
+ throw new RuntimeException("Failed to get session snapshot", e);
+ }
+ }
+
+ @Override
+ public void deleteSnapshot(String sessionHandleId) {
+ String key = KEY_PREFIX + sessionHandleId;
+ try (Jedis jedis = jedisPool.getResource()) {
+ jedis.del(key);
+ LOG.debug("Deleted session snapshot from Redis: {}", key);
+ } catch (Exception e) {
+ LOG.error("Failed to delete session snapshot from Redis: {}", key, e);
+ throw new RuntimeException("Failed to delete session snapshot", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ if (jedisPool != null) {
+ jedisPool.close();
+ jedisPool = null;
+ }
+ }
+}
diff --git
a/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/SessionStateStore.java
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/SessionStateStore.java
new file mode 100644
index 00000000000..ce6d96b16cb
--- /dev/null
+++
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/SessionStateStore.java
@@ -0,0 +1,34 @@
+/*
+ * 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.hive.service.cli.session.store;
+
+import org.apache.hadoop.hive.conf.HiveConf;
+
+public interface SessionStateStore {
+
+ void init(HiveConf conf);
+
+ void saveSnapshot(String sessionHandleId, HiveSessionSnapshot snapshot);
+
+ HiveSessionSnapshot getSnapshot(String sessionHandleId);
+
+ void deleteSnapshot(String sessionHandleId);
+
+ void close();
+}
diff --git
a/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/TempTablePartitionSnapshot.java
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/TempTablePartitionSnapshot.java
new file mode 100644
index 00000000000..0668b168a31
--- /dev/null
+++
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/TempTablePartitionSnapshot.java
@@ -0,0 +1,54 @@
+/*
+ * 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.hive.service.cli.session.store;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Session-local partition metadata for a partitioned temporary table.
+ * Temp table partitions are not stored in HMS and must be persisted
separately.
+ */
+public class TempTablePartitionSnapshot {
+
+ private final List<String> values;
+ private final String location;
+
+ @JsonCreator
+ public TempTablePartitionSnapshot(
+ @JsonProperty("values") List<String> values,
+ @JsonProperty("location") String location) {
+ this.values = values != null ? new ArrayList<>(values) :
Collections.emptyList();
+ this.location = location;
+ }
+
+ @JsonProperty("values")
+ public List<String> getValues() {
+ return values;
+ }
+
+ @JsonProperty("location")
+ public String getLocation() {
+ return location;
+ }
+}
diff --git
a/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/ZooKeeperSessionStateStore.java
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/ZooKeeperSessionStateStore.java
new file mode 100644
index 00000000000..35add9e395e
--- /dev/null
+++
b/service-session-store/src/main/java/org/apache/hive/service/cli/session/store/ZooKeeperSessionStateStore.java
@@ -0,0 +1,146 @@
+/*
+ * 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.hive.service.cli.session.store;
+
+import java.util.concurrent.TimeUnit;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ZooKeeperSessionStateStore implements SessionStateStore {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(ZooKeeperSessionStateStore.class);
+
+ public static final String CONF_ZK_PATH =
"hive.server2.session.state.store.zk.path";
+ public static final String CONF_ZK_PATH_DEFAULT = "/hive_sessions";
+
+ private CuratorFramework zkClient;
+ private String zkBasePath;
+ private long ttlMillis;
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ @Override
+ public void init(HiveConf conf) {
+ String quorum = conf.getVar(ConfVars.HIVE_ZOOKEEPER_QUORUM);
+ int sessionTimeout = (int) conf.getTimeVar(
+ ConfVars.HIVE_ZOOKEEPER_SESSION_TIMEOUT, TimeUnit.MILLISECONDS);
+ int connectionTimeout = (int) conf.getTimeVar(
+ ConfVars.HIVE_ZOOKEEPER_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
+ int baseSleepTime = (int) conf.getTimeVar(
+ ConfVars.HIVE_ZOOKEEPER_CONNECTION_BASESLEEPTIME,
TimeUnit.MILLISECONDS);
+ int maxRetries =
conf.getIntVar(ConfVars.HIVE_ZOOKEEPER_CONNECTION_MAX_RETRIES);
+
+ this.zkBasePath = conf.get(CONF_ZK_PATH, CONF_ZK_PATH_DEFAULT);
+ this.ttlMillis = conf.getTimeVar(
+ ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_TTL, TimeUnit.MILLISECONDS);
+
+ zkClient = CuratorFrameworkFactory.builder()
+ .connectString(quorum)
+ .sessionTimeoutMs(sessionTimeout)
+ .connectionTimeoutMs(connectionTimeout)
+ .retryPolicy(new ExponentialBackoffRetry(baseSleepTime, maxRetries))
+ .build();
+ zkClient.start();
+
+ try {
+ zkClient.create().creatingParentsIfNeeded().forPath(zkBasePath);
+ } catch (KeeperException.NodeExistsException e) {
+ LOG.debug("ZooKeeper base path already exists: {}", zkBasePath);
+ } catch (Exception e) {
+ LOG.error("Failed to create ZooKeeper base path: {}", zkBasePath, e);
+ throw new RuntimeException("Failed to initialize
ZooKeeperSessionStateStore", e);
+ }
+
+ LOG.info("Initialized ZooKeeperSessionStateStore with quorum={},
basePath={}, ttl={}ms",
+ quorum, zkBasePath, ttlMillis);
+ }
+
+ @Override
+ public void saveSnapshot(String sessionHandleId, HiveSessionSnapshot
snapshot) {
+ String path = getNodePath(sessionHandleId);
+ try {
+ byte[] data = OBJECT_MAPPER.writeValueAsBytes(snapshot);
+ try {
+ zkClient.delete().forPath(path);
+ } catch (KeeperException.NoNodeException e) {
+ // Node doesn't exist yet, fine
+ }
+ zkClient.create().withTtl(ttlMillis).creatingParentsIfNeeded()
+ .withMode(CreateMode.PERSISTENT_WITH_TTL)
+ .forPath(path, data);
+ LOG.debug("Saved session snapshot to ZooKeeper: {}", path);
+ } catch (Exception e) {
+ LOG.error("Failed to save session snapshot to ZooKeeper: {}", path, e);
+ throw new RuntimeException("Failed to save session snapshot", e);
+ }
+ }
+
+ @Override
+ public HiveSessionSnapshot getSnapshot(String sessionHandleId) {
+ String path = getNodePath(sessionHandleId);
+ try {
+ if (zkClient.checkExists().forPath(path) == null) {
+ return null;
+ }
+ byte[] data = zkClient.getData().forPath(path);
+ return OBJECT_MAPPER.readValue(data, HiveSessionSnapshot.class);
+ } catch (KeeperException.NoNodeException e) {
+ return null;
+ } catch (Exception e) {
+ LOG.error("Failed to get session snapshot from ZooKeeper: {}", path, e);
+ throw new RuntimeException("Failed to get session snapshot", e);
+ }
+ }
+
+ @Override
+ public void deleteSnapshot(String sessionHandleId) {
+ String path = getNodePath(sessionHandleId);
+ try {
+ if (zkClient.checkExists().forPath(path) != null) {
+ zkClient.delete().forPath(path);
+ }
+ LOG.debug("Deleted session snapshot from ZooKeeper: {}", path);
+ } catch (KeeperException.NoNodeException e) {
+ // Already gone, ignore
+ } catch (Exception e) {
+ LOG.error("Failed to delete session snapshot from ZooKeeper: {}", path,
e);
+ throw new RuntimeException("Failed to delete session snapshot", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ if (zkClient != null) {
+ zkClient.close();
+ zkClient = null;
+ }
+ }
+
+ private String getNodePath(String sessionHandleId) {
+ return zkBasePath + "/" + sessionHandleId;
+ }
+}
diff --git
a/service-session-store/src/test/java/org/apache/hive/service/cli/session/store/TestRedisSessionStateStore.java
b/service-session-store/src/test/java/org/apache/hive/service/cli/session/store/TestRedisSessionStateStore.java
new file mode 100644
index 00000000000..bd3393a3bfa
--- /dev/null
+++
b/service-session-store/src/test/java/org/apache/hive/service/cli/session/store/TestRedisSessionStateStore.java
@@ -0,0 +1,66 @@
+/*
+ * 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.hive.service.cli.session.store;
+
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.junit.AfterClass;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import static
org.apache.hive.service.cli.session.store.RedisSessionStateStore.CONF_REDIS_HOST;
+import static
org.apache.hive.service.cli.session.store.RedisSessionStateStore.CONF_REDIS_PORT;
+
+public class TestRedisSessionStateStore extends TestSessionStateStoreBase {
+
+ private static GenericContainer<?> redisContainer;
+
+ @BeforeClass
+ public static void startRedis() {
+ try {
+ redisContainer = new
GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
+ .withExposedPorts(6379);
+ redisContainer.start();
+ } catch (Exception e) {
+ // Docker not available, skip tests
+ Assume.assumeTrue("Docker not available, skipping Redis tests", false);
+ }
+ }
+
+ @AfterClass
+ public static void stopRedis() {
+ if (redisContainer != null) {
+ redisContainer.stop();
+ }
+ }
+
+ @Override
+ protected SessionStateStore createStore() {
+ Assume.assumeTrue("Redis container not running", redisContainer != null &&
redisContainer.isRunning());
+ HiveConf conf = new HiveConf();
+ conf.set(CONF_REDIS_HOST, redisContainer.getHost());
+ conf.set(CONF_REDIS_PORT,
String.valueOf(redisContainer.getMappedPort(6379)));
+ conf.set(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_TTL.varname, "3600s");
+ RedisSessionStateStore store = new RedisSessionStateStore();
+ store.init(conf);
+ return store;
+ }
+}
diff --git
a/service-session-store/src/test/java/org/apache/hive/service/cli/session/store/TestSessionStateStoreBase.java
b/service-session-store/src/test/java/org/apache/hive/service/cli/session/store/TestSessionStateStoreBase.java
new file mode 100644
index 00000000000..4268d770cbc
--- /dev/null
+++
b/service-session-store/src/test/java/org/apache/hive/service/cli/session/store/TestSessionStateStoreBase.java
@@ -0,0 +1,180 @@
+/*
+ * 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.hive.service.cli.session.store;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public abstract class TestSessionStateStoreBase {
+
+ protected SessionStateStore store;
+
+ protected abstract SessionStateStore createStore() throws Exception;
+
+ @Before
+ public void setUp() throws Exception {
+ store = createStore();
+ }
+
+ @After
+ public void tearDown() {
+ if (store != null) {
+ store.close();
+ }
+ }
+
+ @Test
+ public void testSaveAndGetSnapshot() {
+ String sessionId = UUID.randomUUID().toString();
+ HiveSessionSnapshot snapshot = createTestSnapshot(sessionId, "testuser",
"test_db");
+
+ store.saveSnapshot(sessionId, snapshot);
+ HiveSessionSnapshot retrieved = store.getSnapshot(sessionId);
+
+ assertNotNull(retrieved);
+ assertEquals(sessionId, retrieved.getSessionHandleId());
+ assertEquals("testuser", retrieved.getUsername());
+ assertEquals("127.0.0.1", retrieved.getIpAddress());
+ assertEquals("test_db", retrieved.getCurrentDatabase());
+ assertEquals(2, retrieved.getOverriddenConfigurations().size());
+ assertEquals("true",
retrieved.getOverriddenConfigurations().get("hive.exec.dynamic.partition"));
+ assertEquals("nonstrict",
retrieved.getOverriddenConfigurations().get("hive.exec.dynamic.partition.mode"));
+ assertEquals(2, retrieved.getAddedJars().size());
+ assertEquals("hdfs:///user/hive/udfs/my-udf.jar",
retrieved.getAddedJars().get(0));
+ assertEquals(2, retrieved.getAddedFiles().size());
+ assertEquals("hdfs:///user/hive/files/data.csv",
retrieved.getAddedFiles().get(0));
+ assertEquals("/tmp/local_file.txt", retrieved.getAddedFiles().get(1));
+ assertEquals(1, retrieved.getTempTableDefinitions().size());
+ assertEquals("CREATE TEMPORARY TABLE tmp_t (col1 STRING, col2 INT)",
+ retrieved.getTempTableDefinitions().get("tmp_t"));
+ assertEquals(10, retrieved.getProtocolVersion());
+ assertEquals(1000L, retrieved.getCreationTime());
+ assertEquals(2000L, retrieved.getLastAccessTime());
+ }
+
+ @Test
+ public void testDeleteSnapshot() {
+ String sessionId = UUID.randomUUID().toString();
+ HiveSessionSnapshot snapshot = createTestSnapshot(sessionId, "testuser",
"default");
+
+ store.saveSnapshot(sessionId, snapshot);
+ assertNotNull(store.getSnapshot(sessionId));
+
+ store.deleteSnapshot(sessionId);
+ assertNull(store.getSnapshot(sessionId));
+ }
+
+ @Test
+ public void testOverwriteSnapshot() {
+ String sessionId = UUID.randomUUID().toString();
+
+ HiveSessionSnapshot snapshot1 = createTestSnapshot(sessionId, "user1",
"db1");
+ store.saveSnapshot(sessionId, snapshot1);
+
+ HiveSessionSnapshot snapshot2 = createTestSnapshot(sessionId, "user1",
"db2");
+ store.saveSnapshot(sessionId, snapshot2);
+
+ HiveSessionSnapshot retrieved = store.getSnapshot(sessionId);
+ assertNotNull(retrieved);
+ assertEquals("db2", retrieved.getCurrentDatabase());
+ }
+
+ @Test
+ public void testGetNonExistent() {
+ String sessionId = UUID.randomUUID().toString();
+ assertNull(store.getSnapshot(sessionId));
+ }
+
+ @Test
+ public void testMultipleSessions() {
+ String sessionId1 = UUID.randomUUID().toString();
+ String sessionId2 = UUID.randomUUID().toString();
+ String sessionId3 = UUID.randomUUID().toString();
+
+ store.saveSnapshot(sessionId1, createTestSnapshot(sessionId1, "user1",
"db1"));
+ store.saveSnapshot(sessionId2, createTestSnapshot(sessionId2, "user2",
"db2"));
+ store.saveSnapshot(sessionId3, createTestSnapshot(sessionId3, "user3",
"db3"));
+
+ assertEquals("db1", store.getSnapshot(sessionId1).getCurrentDatabase());
+ assertEquals("db2", store.getSnapshot(sessionId2).getCurrentDatabase());
+ assertEquals("db3", store.getSnapshot(sessionId3).getCurrentDatabase());
+
+ store.deleteSnapshot(sessionId2);
+ assertNotNull(store.getSnapshot(sessionId1));
+ assertNull(store.getSnapshot(sessionId2));
+ assertNotNull(store.getSnapshot(sessionId3));
+ }
+
+ @Test
+ public void testDeleteNonExistent() {
+ String sessionId = UUID.randomUUID().toString();
+ store.deleteSnapshot(sessionId);
+ assertNull(store.getSnapshot(sessionId));
+ }
+
+ @Test
+ public void testSnapshotWipedOnSessionClose() {
+ String sessionId1 = UUID.randomUUID().toString();
+ String sessionId2 = UUID.randomUUID().toString();
+
+ store.saveSnapshot(sessionId1, createTestSnapshot(sessionId1, "user1",
"db1"));
+ store.saveSnapshot(sessionId2, createTestSnapshot(sessionId2, "user2",
"db2"));
+
+ // Simulate session close — snapshot should be completely removed
+ store.deleteSnapshot(sessionId1);
+
+ assertNull("Snapshot should be wiped after session close",
store.getSnapshot(sessionId1));
+ // Other sessions remain unaffected
+ assertNotNull("Other session should still exist",
store.getSnapshot(sessionId2));
+ assertEquals("db2", store.getSnapshot(sessionId2).getCurrentDatabase());
+ }
+
+ protected HiveSessionSnapshot createTestSnapshot(String sessionId, String
username, String database) {
+ Map<String, String> configs = new HashMap<>();
+ configs.put("hive.exec.dynamic.partition", "true");
+ configs.put("hive.exec.dynamic.partition.mode", "nonstrict");
+
+ Map<String, String> tempTables = new HashMap<>();
+ tempTables.put("tmp_t", "CREATE TEMPORARY TABLE tmp_t (col1 STRING, col2
INT)");
+
+ return HiveSessionSnapshot.builder()
+ .sessionHandleId(sessionId)
+ .username(username)
+ .ipAddress("127.0.0.1")
+ .currentDatabase(database)
+ .overriddenConfigurations(configs)
+ .addedJars(Arrays.asList("hdfs:///user/hive/udfs/my-udf.jar",
"hdfs:///user/hive/udfs/other.jar"))
+ .addedFiles(Arrays.asList("hdfs:///user/hive/files/data.csv",
"/tmp/local_file.txt"))
+ .tempTableDefinitions(tempTables)
+ .protocolVersion(10)
+ .creationTime(1000L)
+ .lastAccessTime(2000L)
+ .build();
+ }
+}
diff --git
a/service-session-store/src/test/java/org/apache/hive/service/cli/session/store/TestZooKeeperSessionStateStore.java
b/service-session-store/src/test/java/org/apache/hive/service/cli/session/store/TestZooKeeperSessionStateStore.java
new file mode 100644
index 00000000000..24d19aa5c84
--- /dev/null
+++
b/service-session-store/src/test/java/org/apache/hive/service/cli/session/store/TestZooKeeperSessionStateStore.java
@@ -0,0 +1,56 @@
+/*
+ * 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.hive.service.cli.session.store;
+
+import org.apache.curator.test.TestingServer;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import static
org.apache.hive.service.cli.session.store.ZooKeeperSessionStateStore.CONF_ZK_PATH;
+
+public class TestZooKeeperSessionStateStore extends TestSessionStateStoreBase {
+
+ private static TestingServer zkServer;
+
+ @BeforeClass
+ public static void startZk() throws Exception {
+ System.setProperty("zookeeper.extendedTypesEnabled", "true");
+ zkServer = new TestingServer();
+ zkServer.start();
+ }
+
+ @AfterClass
+ public static void stopZk() throws Exception {
+ if (zkServer != null) {
+ zkServer.close();
+ }
+ }
+
+ @Override
+ protected SessionStateStore createStore() {
+ HiveConf conf = new HiveConf();
+ conf.setVar(ConfVars.HIVE_ZOOKEEPER_QUORUM, zkServer.getConnectString());
+ conf.set(CONF_ZK_PATH, "/test_hive_sessions");
+ ZooKeeperSessionStateStore store = new ZooKeeperSessionStateStore();
+ store.init(conf);
+ return store;
+ }
+}
diff --git a/service/pom.xml b/service/pom.xml
index 601e8756bf0..172bae331c5 100644
--- a/service/pom.xml
+++ b/service/pom.xml
@@ -60,6 +60,11 @@
<artifactId>hive-service-rpc</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.hive</groupId>
+ <artifactId>hive-service-session-store</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-llap-server</artifactId>
diff --git
a/service/src/java/org/apache/hive/service/cli/operation/HiveCommandOperation.java
b/service/src/java/org/apache/hive/service/cli/operation/HiveCommandOperation.java
index 0a5028de645..9a8fab510d0 100644
---
a/service/src/java/org/apache/hive/service/cli/operation/HiveCommandOperation.java
+++
b/service/src/java/org/apache/hive/service/cli/operation/HiveCommandOperation.java
@@ -45,6 +45,8 @@
import org.apache.hive.service.cli.RowSetFactory;
import org.apache.hive.service.cli.TableSchema;
import org.apache.hive.service.cli.session.HiveSession;
+import org.apache.hive.service.cli.session.HiveSessionImpl;
+import org.apache.hive.service.cli.session.PersistableSessionUtils;
/**
* Executes a HiveCommand
@@ -133,6 +135,17 @@ public void runInternal() throws HiveSQLException {
setState(OperationState.FINISHED);
}
+ @Override
+ protected void onNewState(OperationState state, OperationState prevState) {
+ super.onNewState(state, prevState);
+ if (state == OperationState.FINISHED) {
+ HiveSessionImpl impl =
PersistableSessionUtils.unwrapSession(parentSession);
+ if (impl != null) {
+ impl.onOperationFinished(statement);
+ }
+ }
+ }
+
/* (non-Javadoc)
* @see org.apache.hive.service.cli.operation.Operation#close()
*/
diff --git
a/service/src/java/org/apache/hive/service/cli/operation/SQLOperation.java
b/service/src/java/org/apache/hive/service/cli/operation/SQLOperation.java
index 1a6fbe0f538..71806f58214 100644
--- a/service/src/java/org/apache/hive/service/cli/operation/SQLOperation.java
+++ b/service/src/java/org/apache/hive/service/cli/operation/SQLOperation.java
@@ -79,6 +79,8 @@
import org.apache.hive.service.cli.RowSetFactory;
import org.apache.hive.service.cli.TableSchema;
import org.apache.hive.service.cli.session.HiveSession;
+import org.apache.hive.service.cli.session.HiveSessionImpl;
+import org.apache.hive.service.cli.session.PersistableSessionUtils;
import org.apache.hive.service.server.ThreadWithGarbageCleanup;
import static org.apache.hadoop.hive.shims.HadoopShims.USER_ID;
@@ -649,6 +651,10 @@ protected void onNewState(final OperationState state,
final OperationState prevS
}
markQueryMetric(MetricsFactory.getInstance(),
MetricsConstant.HS2_SUCCEEDED_QUERIES);
queryInfo.updateState(state.toString());
+ HiveSessionImpl impl =
PersistableSessionUtils.unwrapSession(parentSession);
+ if (impl != null) {
+ impl.onOperationFinished(statement);
+ }
break;
case INITIALIZED:
/* fall through */
diff --git
a/service/src/java/org/apache/hive/service/cli/session/HiveSessionImpl.java
b/service/src/java/org/apache/hive/service/cli/session/HiveSessionImpl.java
index 4002fd1a573..3a5ab4c2d4d 100644
--- a/service/src/java/org/apache/hive/service/cli/session/HiveSessionImpl.java
+++ b/service/src/java/org/apache/hive/service/cli/session/HiveSessionImpl.java
@@ -65,6 +65,7 @@
import org.apache.hive.service.cli.RowSet;
import org.apache.hive.service.cli.SessionHandle;
import org.apache.hive.service.cli.TableSchema;
+import org.apache.hive.service.cli.session.store.HiveSessionSnapshot;
import org.apache.hive.service.cli.operation.ExecuteStatementOperation;
import org.apache.hive.service.cli.operation.GetCatalogsOperation;
import org.apache.hive.service.cli.operation.GetColumnsOperation;
@@ -820,6 +821,25 @@ private void cleanupSessionLogDir() {
}
}
+ public void onOperationFinished(String statement) {
+ if (sessionManager == null ||
!sessionManager.isPersistableSessionsEnabled()) {
+ return;
+ }
+ notifyIfStateChanging(statement);
+ }
+
+ private void notifyIfStateChanging(String statement) {
+ if (PersistableSessionUtils.shouldPersistSnapshot(statement, sessionState)
+ && sessionManager != null) {
+ sessionManager.notifySessionStateChanged(sessionHandle);
+ }
+ }
+
+ public HiveSessionSnapshot captureSnapshot() {
+ return PersistableSessionUtils.captureSnapshot(sessionHandle, username,
ipAddress,
+ sessionState, getProtocolVersion(), creationTime, lastAccessTime);
+ }
+
@Override
public SessionState getSessionState() {
return sessionState;
diff --git
a/service/src/java/org/apache/hive/service/cli/session/HiveSessionProxy.java
b/service/src/java/org/apache/hive/service/cli/session/HiveSessionProxy.java
index 847d2e38210..3e4a036e45a 100644
--- a/service/src/java/org/apache/hive/service/cli/session/HiveSessionProxy.java
+++ b/service/src/java/org/apache/hive/service/cli/session/HiveSessionProxy.java
@@ -43,6 +43,10 @@ public HiveSessionProxy(HiveSession hiveSession,
UserGroupInformation ugi) {
this.ugi = ugi;
}
+ public HiveSession getBaseSession() {
+ return base;
+ }
+
public static HiveSession getProxy(HiveSession hiveSession,
UserGroupInformation ugi)
throws IllegalArgumentException, HiveSQLException {
return
(HiveSession)Proxy.newProxyInstance(HiveSession.class.getClassLoader(),
diff --git
a/service/src/java/org/apache/hive/service/cli/session/PersistableSessionUtils.java
b/service/src/java/org/apache/hive/service/cli/session/PersistableSessionUtils.java
new file mode 100644
index 00000000000..dbdb21f394b
--- /dev/null
+++
b/service/src/java/org/apache/hive/service/cli/session/PersistableSessionUtils.java
@@ -0,0 +1,467 @@
+/*
+ * 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.hive.service.cli.session;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.LinkedHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hive.common.TableName;
+import org.apache.hadoop.hive.metastore.Warehouse;
+import org.apache.hadoop.hive.metastore.api.AlreadyExistsException;
+import org.apache.hadoop.hive.metastore.api.FieldSchema;
+import org.apache.hadoop.hive.metastore.api.MetaException;
+import org.apache.hadoop.hive.ql.exec.DDLPlanUtils;
+import org.apache.hadoop.hive.ql.exec.FunctionInfo;
+import org.apache.hadoop.hive.ql.exec.FunctionInfo.FunctionResource;
+import org.apache.hadoop.hive.ql.exec.Registry;
+import org.apache.hadoop.hive.ql.metadata.HiveException;
+import org.apache.hadoop.hive.ql.metadata.Partition;
+import org.apache.hadoop.hive.ql.metadata.Table;
+import org.apache.hadoop.hive.ql.metadata.TempTable;
+import org.apache.hadoop.hive.ql.session.SessionState;
+import org.apache.hive.service.cli.HiveSQLException;
+import org.apache.hive.service.cli.OperationHandle;
+import org.apache.hive.service.cli.SessionHandle;
+import org.apache.hive.service.cli.session.store.HiveSessionSnapshot;
+import org.apache.hive.service.cli.session.store.SessionStateStore;
+import org.apache.hive.service.cli.session.store.TempTablePartitionSnapshot;
+import org.apache.hive.service.rpc.thrift.TProtocolVersion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility methods for the Persistable Sessions feature.
+ * Extracted from SessionManager and HiveSessionImpl to keep
+ * the feature's logic isolated from existing core classes.
+ */
+public final class PersistableSessionUtils {
+
+ public enum FetchStrategy {
+ NEVER,
+ ALWAYS,
+ FETCH_WHEN_MISSING
+ }
+
+ private static final Logger LOG =
LoggerFactory.getLogger(PersistableSessionUtils.class);
+
+ /**
+ * Returns a store key that incorporates both the public and secret UUIDs
+ * of the session handle, preventing hijacking via the public ID alone.
+ */
+ public static String storeKey(SessionHandle sessionHandle) {
+ return sessionHandle.getHandleIdentifier().getPublicId().toString() + ":"
+ + sessionHandle.getHandleIdentifier().getSecretId().toString();
+ }
+
+ private static final Pattern STATE_CHANGING_PATTERN = Pattern.compile(
+ "(?i)^\\s*(USE\\b|SET\\b|ADD\\s+(JAR|FILE)\\b|DELETE\\s+(JAR|FILE)\\b" +
+ "|(CREATE|DROP)\\s+TEMPORARY\\s+(TABLE|FUNCTION)\\b).*");
+
+ private static final Pattern INSERT_TARGET_PATTERN = Pattern.compile(
+ "(?i)^\\s*INSERT\\s+(?:INTO|OVERWRITE)\\s+(?:TABLE\\s+)?([^\\s(]+)");
+ private static final Pattern LOAD_TARGET_PATTERN = Pattern.compile(
+
"(?i)^\\s*LOAD\\s+DATA\\s+(?:LOCAL\\s+)?INPATH\\s+.+\\s+INTO\\s+TABLE\\s+([^\\s(]+)");
+
+ private PersistableSessionUtils() {
+ }
+
+ /**
+ * Determines whether a SQL statement changes session state that should
+ * be persisted (database, configs, JARs, temp tables, temp functions).
+ */
+ public static boolean isStateChangingCommand(String statement) {
+ if (statement == null) {
+ return false;
+ }
+ return STATE_CHANGING_PATTERN.matcher(statement).matches();
+ }
+
+ /**
+ * Returns true when a finished statement may have changed persisted session
state,
+ * including DML that adds data or partition metadata to temporary tables.
+ */
+ public static boolean shouldPersistSnapshot(String statement, SessionState
sessionState) {
+ if (statement == null) {
+ return false;
+ }
+ if (isStateChangingCommand(statement)) {
+ return true;
+ }
+ String targetTable = extractTempTableDmlTarget(statement);
+ return targetTable != null && isSessionTempTable(sessionState,
targetTable);
+ }
+
+ /**
+ * Extracts the target table from INSERT or LOAD DATA statements, or returns
null.
+ */
+ static String extractTempTableDmlTarget(String statement) {
+ if (statement == null) {
+ return null;
+ }
+ Matcher insertMatcher = INSERT_TARGET_PATTERN.matcher(statement);
+ if (insertMatcher.find()) {
+ return normalizeTableReference(insertMatcher.group(1));
+ }
+ Matcher loadMatcher = LOAD_TARGET_PATTERN.matcher(statement);
+ if (loadMatcher.find()) {
+ return normalizeTableReference(loadMatcher.group(1));
+ }
+ return null;
+ }
+
+ private static String normalizeTableReference(String tableRef) {
+ return tableRef.replace("`", "");
+ }
+
+ /**
+ * Returns true if the given table reference resolves to a session-local
temp table.
+ */
+ static boolean isSessionTempTable(SessionState sessionState, String
tableReference) {
+ if (sessionState == null || tableReference == null) {
+ return false;
+ }
+ try {
+ TableName tableName = TableName.fromString(tableReference, null,
+ sessionState.getCurrentDatabase());
+ Map<String, Map<String, Table>> tempTables =
sessionState.getTempTables();
+ if (tempTables == null || tempTables.isEmpty()) {
+ return false;
+ }
+ Map<String, Table> dbTables =
tempTables.get(tableName.getDb().toLowerCase());
+ return dbTables != null &&
dbTables.containsKey(tableName.getTable().toLowerCase());
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Captures the current session state into a snapshot DTO.
+ */
+ public static HiveSessionSnapshot captureSnapshot(SessionHandle
sessionHandle, String username, String ipAddress,
+ SessionState sessionState, TProtocolVersion protocol, long creationTime,
long lastAccessTime) {
+ List<String> jars = new ArrayList<>();
+ List<String> files = new ArrayList<>();
+ if (sessionState != null) {
+ Set<String> jarSet =
sessionState.list_resource(SessionState.ResourceType.JAR, null);
+ if (jarSet != null) {
+ jars.addAll(jarSet);
+ }
+ Set<String> fileSet =
sessionState.list_resource(SessionState.ResourceType.FILE, null);
+ if (fileSet != null) {
+ files.addAll(fileSet);
+ }
+ }
+
+ Map<String, String> tempTableDefs = new HashMap<>();
+ Map<String, List<TempTablePartitionSnapshot>> tempTablePartitionDefs = new
HashMap<>();
+ if (sessionState != null && sessionState.getTempTables() != null) {
+ DDLPlanUtils ddlPlanUtils = new DDLPlanUtils();
+ for (Map.Entry<String, Map<String, Table>> dbEntry :
+ sessionState.getTempTables().entrySet()) {
+ String dbName = dbEntry.getKey();
+ for (Map.Entry<String, Table> tableEntry :
dbEntry.getValue().entrySet()) {
+ String tableName = tableEntry.getKey();
+ Table table = tableEntry.getValue();
+ String tableKey = TableName.getDbTable(dbName, tableName);
+ String ddl = generateTempTableDDL(ddlPlanUtils, table);
+ if (ddl != null) {
+ tempTableDefs.put(tableKey, ddl);
+ }
+ List<TempTablePartitionSnapshot> partitionSnapshots =
captureTempTablePartitions(
+ sessionState, dbName, tableName, table);
+ if (!partitionSnapshots.isEmpty()) {
+ tempTablePartitionDefs.put(tableKey, partitionSnapshots);
+ }
+ }
+ }
+ }
+
+ List<String> tempFuncDefs = captureTempFunctions(sessionState);
+
+ return HiveSessionSnapshot.builder()
+ .sessionHandleId(storeKey(sessionHandle))
+ .username(username)
+ .ipAddress(ipAddress)
+ .currentDatabase(sessionState != null ?
sessionState.getCurrentDatabase() : null)
+ .overriddenConfigurations(sessionState != null
+ ? new HashMap<>(sessionState.getOverriddenConfigurations()) : null)
+ .addedJars(jars)
+ .addedFiles(files)
+ .tempTableDefinitions(tempTableDefs)
+ .tempTablePartitionDefinitions(tempTablePartitionDefs)
+ .tempFunctionDefinitions(tempFuncDefs)
+ .protocolVersion(protocol.getValue())
+ .creationTime(creationTime)
+ .lastAccessTime(lastAccessTime)
+ .build();
+ }
+
+ /**
+ * Generates the CREATE TEMPORARY TABLE DDL for a temp table using
DDLPlanUtils,
+ * which handles partitions, table properties, complex types, bucket specs,
etc.
+ */
+ static String generateTempTableDDL(DDLPlanUtils ddlPlanUtils, Table table) {
+ try {
+ return ddlPlanUtils.getCreateTableCommand(table, true);
+ } catch (Exception e) {
+ LOG.warn("Failed to generate DDL for temp table: {}",
table.getTableName(), e);
+ return null;
+ }
+ }
+
+ /**
+ * Captures session-local partition metadata for a partitioned temp table.
+ */
+ static List<TempTablePartitionSnapshot>
captureTempTablePartitions(SessionState sessionState,
+ String dbName, String tableName, Table table) {
+ List<TempTablePartitionSnapshot> partitions = new ArrayList<>();
+ if (sessionState == null || !table.isPartitioned()) {
+ return partitions;
+ }
+ Map<String, TempTable> tempPartitions = sessionState.getTempPartitions();
+ if (tempPartitions == null || tempPartitions.isEmpty()) {
+ return partitions;
+ }
+ String qualifiedKey = Warehouse.getQualifiedName(dbName.toLowerCase(),
tableName.toLowerCase());
+ TempTable tempTable = tempPartitions.get(qualifiedKey);
+ if (tempTable == null) {
+ return partitions;
+ }
+ for (org.apache.hadoop.hive.metastore.api.Partition apiPartition :
tempTable.listPartitions()) {
+ String location = apiPartition.getSd() != null ?
apiPartition.getSd().getLocation() : null;
+ partitions.add(new TempTablePartitionSnapshot(
+ new ArrayList<>(apiPartition.getValues()), location));
+ }
+ return partitions;
+ }
+
+ /**
+ * Captures temporary function definitions as CREATE TEMPORARY FUNCTION DDL
statements.
+ * Uses the passed sessionState's registry directly rather than the
thread-local,
+ * since the snapshot may be captured on a thread different from the
session's own.
+ */
+ static List<String> captureTempFunctions(SessionState sessionState) {
+ List<String> funcDefs = new ArrayList<>();
+ if (sessionState == null) {
+ return funcDefs;
+ }
+ Registry registry = sessionState.getSessionRegistry();
+ if (registry == null) {
+ return funcDefs;
+ }
+ for (String funcName : registry.getCurrentFunctionNames()) {
+ try {
+ FunctionInfo info = registry.getFunctionInfo(funcName);
+ if (info == null || info.getFunctionType() !=
FunctionInfo.FunctionType.TEMPORARY) {
+ continue;
+ }
+ String className = info.getClassName();
+ if (className == null) {
+ Class<?> funcClass = info.getFunctionClass();
+ if (funcClass != null) {
+ className = funcClass.getName();
+ }
+ }
+ if (className == null) {
+ continue;
+ }
+ StringBuilder ddl = new StringBuilder("CREATE TEMPORARY FUNCTION ");
+ ddl.append(funcName).append(" AS '").append(className).append("'");
+ FunctionResource[] resources = info.getResources();
+ if (resources != null && resources.length > 0) {
+ ddl.append(" USING");
+ for (int i = 0; i < resources.length; i++) {
+ if (i > 0) {
+ ddl.append(",");
+ }
+ ddl.append(" ").append(resources[i].getResourceType().name())
+ .append("
'").append(resources[i].getResourceURI()).append("'");
+ }
+ }
+ funcDefs.add(ddl.toString());
+ } catch (Exception e) {
+ LOG.warn("Failed to capture temp function: {}", funcName, e);
+ }
+ }
+ return funcDefs;
+ }
+
+ /**
+ * Hydrates a recovered session from a snapshot: restores database, configs,
+ * JARs, files, temp functions, and temp tables.
+ */
+ public static void hydrateSession(HiveSession session, HiveSessionSnapshot
snapshot)
+ throws HiveSQLException {
+ try {
+ SessionState sessionState = session.getSessionState();
+ if (snapshot.getCurrentDatabase() != null) {
+ sessionState.setCurrentDatabase(snapshot.getCurrentDatabase());
+ }
+ if (snapshot.getOverriddenConfigurations() != null) {
+ for (Map.Entry<String, String> entry :
snapshot.getOverriddenConfigurations().entrySet()) {
+ session.getHiveConf().set(entry.getKey(), entry.getValue());
+ sessionState.getOverriddenConfigurations().put(entry.getKey(),
entry.getValue());
+ }
+ }
+ if (snapshot.getAddedJars() != null) {
+ for (String jar : snapshot.getAddedJars()) {
+ sessionState.add_resource(SessionState.ResourceType.JAR, jar);
+ }
+ }
+ if (snapshot.getAddedFiles() != null) {
+ for (String file : snapshot.getAddedFiles()) {
+ sessionState.add_resource(SessionState.ResourceType.FILE, file);
+ }
+ }
+ if (snapshot.getTempFunctionDefinitions() != null) {
+ restoreTempFunctions(session, snapshot.getTempFunctionDefinitions());
+ }
+ if (snapshot.getTempTableDefinitions() != null) {
+ restoreTempTables(session, sessionState,
snapshot.getTempTableDefinitions(),
+ snapshot.getTempTablePartitionDefinitions());
+ }
+ } catch (Exception e) {
+ LOG.error("Failed to hydrate session: {}", session.getSessionHandle(),
e);
+ throw new HiveSQLException("Failed to hydrate recovered session", e);
+ }
+ }
+
+ private static void restoreTempFunctions(HiveSession session, List<String>
tempFuncDefs) {
+ for (String ddl : tempFuncDefs) {
+ try {
+ OperationHandle opHandle = session.executeStatement(ddl, null);
+ session.closeOperation(opHandle);
+ } catch (Exception e) {
+ LOG.warn("Failed to restore temporary function: {}", ddl, e);
+ }
+ }
+ }
+
+ private static void restoreTempTables(HiveSession session, SessionState
sessionState,
+ Map<String, String> tempTableDefs, Map<String,
List<TempTablePartitionSnapshot>> tempTablePartitionDefs) {
+ String currentDb = sessionState.getCurrentDatabase();
+ for (Map.Entry<String, String> entry : tempTableDefs.entrySet()) {
+ try {
+ TableName tn = TableName.fromString(entry.getKey(), null, currentDb);
+ String db = tn.getDb();
+ if (!db.equals(sessionState.getCurrentDatabase())) {
+ sessionState.setCurrentDatabase(db);
+ }
+ OperationHandle opHandle = session.executeStatement(entry.getValue(),
null);
+ session.closeOperation(opHandle);
+ List<TempTablePartitionSnapshot> partitions = tempTablePartitionDefs
!= null
+ ? tempTablePartitionDefs.get(entry.getKey()) : null;
+ if (partitions != null && !partitions.isEmpty()) {
+ Table table =
sessionState.getTempTables().get(db).get(tn.getTable());
+ restoreTempTablePartitions(sessionState, table, partitions);
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to restore temporary table {}", entry.getKey(), e);
+ }
+ }
+ sessionState.setCurrentDatabase(currentDb);
+ }
+
+ private static void restoreTempTablePartitions(SessionState sessionState,
Table table,
+ List<TempTablePartitionSnapshot> partitions)
+ throws HiveException, MetaException, AlreadyExistsException {
+ String qualifiedKey = Warehouse.getQualifiedName(
+ table.getDbName().toLowerCase(), table.getTableName().toLowerCase());
+ TempTable tempTable = sessionState.getTempPartitions().get(qualifiedKey);
+ if (tempTable == null) {
+ LOG.warn("TempTable partition metadata missing for {}, skipping
partition restore", qualifiedKey);
+ return;
+ }
+ List<org.apache.hadoop.hive.metastore.api.Partition> toAdd = new
ArrayList<>(partitions.size());
+ List<FieldSchema> partCols = table.getPartitionKeys();
+ for (TempTablePartitionSnapshot snapshot : partitions) {
+ Map<String, String> partSpec = new LinkedHashMap<>();
+ List<String> values = snapshot.getValues();
+ for (int i = 0; i < partCols.size(); i++) {
+ partSpec.put(partCols.get(i).getName(), values.get(i));
+ }
+ Path location = snapshot.getLocation() != null ? new
Path(snapshot.getLocation()) : null;
+ Partition qlPart = new Partition(table, partSpec, location);
+ toAdd.add(qlPart.getTPartition());
+ }
+ tempTable.addPartitions(toAdd, true);
+ }
+
+ /**
+ * Unwraps a HiveSession proxy to get the underlying HiveSessionImpl.
+ * Returns null if the session cannot be unwrapped.
+ */
+ public static HiveSessionImpl unwrapSession(HiveSession session) {
+ if (session instanceof HiveSessionImpl impl) {
+ return impl;
+ }
+ if (Proxy.isProxyClass(session.getClass())) {
+ InvocationHandler handler = Proxy.getInvocationHandler(session);
+ if (handler instanceof HiveSessionProxy proxy) {
+ HiveSession base = proxy.getBaseSession();
+ if (base instanceof HiveSessionImpl impl) {
+ return impl;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Saves the session snapshot to the state store.
+ */
+ public static void saveSnapshot(SessionStateStore store, HiveSession
session) {
+ if (store == null) {
+ return;
+ }
+ try {
+ HiveSessionImpl impl = unwrapSession(session);
+ if (impl == null) {
+ return;
+ }
+ HiveSessionSnapshot snapshot = impl.captureSnapshot();
+ store.saveSnapshot(storeKey(session.getSessionHandle()), snapshot);
+ } catch (Exception e) {
+ LOG.warn("Failed to save session snapshot for: {}",
session.getSessionHandle(), e);
+ }
+ }
+
+ /**
+ * Deletes the session snapshot from the state store.
+ */
+ public static void deleteSnapshot(SessionStateStore store, SessionHandle
sessionHandle) {
+ if (store == null) {
+ return;
+ }
+ try {
+ store.deleteSnapshot(storeKey(sessionHandle));
+ } catch (Exception e) {
+ LOG.warn("Failed to delete session snapshot for: {}", sessionHandle, e);
+ }
+ }
+}
diff --git
a/service/src/java/org/apache/hive/service/cli/session/SessionManager.java
b/service/src/java/org/apache/hive/service/cli/session/SessionManager.java
index 49545f3e640..6cd246f550f 100644
--- a/service/src/java/org/apache/hive/service/cli/session/SessionManager.java
+++ b/service/src/java/org/apache/hive/service/cli/session/SessionManager.java
@@ -29,6 +29,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
@@ -59,6 +60,9 @@
import org.apache.hive.service.cli.operation.Operation;
import org.apache.hive.service.cli.operation.OperationLogManager;
import org.apache.hive.service.cli.operation.OperationManager;
+import org.apache.hive.service.cli.session.store.HiveSessionSnapshot;
+import org.apache.hive.service.cli.session.store.SessionStateStore;
+import
org.apache.hive.service.cli.session.PersistableSessionUtils.FetchStrategy;
import org.apache.hive.service.rpc.thrift.TOpenSessionReq;
import org.apache.hive.service.rpc.thrift.TProtocolVersion;
import org.apache.hive.service.server.HiveServer2;
@@ -111,7 +115,12 @@ public class SessionManager extends CompositeService {
private String sessionImplclassName;
private CleanupService cleanupService;
// Tracks which LLAP target gauges have been lazily registered.
- private final java.util.Set<String> registeredLlapTargetGauges =
ConcurrentHashMap.newKeySet();
+ private final Set<String> registeredLlapTargetGauges =
ConcurrentHashMap.newKeySet();
+ // Persistable session state store
+ private SessionStateStore sessionStateStore;
+ private FetchStrategy fetchStrategy;
+ private final ThreadLocal<Boolean> recoveringSession =
ThreadLocal.withInitial(() -> false);
+ private final ConcurrentHashMap<SessionHandle, HiveSession> recoverySessions
= new ConcurrentHashMap<>();
public SessionManager(HiveServer2 hiveServer2, boolean allowSessions) {
super(SessionManager.class.getSimpleName());
@@ -137,8 +146,8 @@ public synchronized void init(HiveConf hiveConf) {
initSessionImplClassName();
Metrics metrics = MetricsFactory.getInstance();
if(metrics != null){
- registerOpenSesssionMetrics(metrics);
- registerActiveSesssionMetrics(metrics);
+ registerOpenSessionMetrics(metrics);
+ registerActiveSessionMetrics(metrics);
}
userLimit =
hiveConf.getIntVar(ConfVars.HIVE_SERVER2_LIMIT_CONNECTIONS_PER_USER);
@@ -155,10 +164,36 @@ public synchronized void init(HiveConf hiveConf) {
cleanupService = SyncCleanupService.INSTANCE;
}
cleanupService.start();
+ initSessionStateStore();
super.init(hiveConf);
}
- private void registerOpenSesssionMetrics(Metrics metrics) {
+ private void initSessionStateStore() {
+ String storeClassName =
hiveConf.getVar(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_CLASS);
+ if (storeClassName == null || storeClassName.isEmpty()) {
+ LOG.info("Session state store not configured. Persistable sessions
disabled.");
+ this.sessionStateStore = null;
+ this.fetchStrategy = FetchStrategy.NEVER;
+ return;
+ }
+ this.fetchStrategy = FetchStrategy.valueOf(
+
hiveConf.getVar(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_FETCH_STRATEGY));
+ try {
+ Class<?> storeClass = Class.forName(storeClassName);
+ this.sessionStateStore = (SessionStateStore)
storeClass.getDeclaredConstructor().newInstance();
+ this.sessionStateStore.init(hiveConf);
+ LOG.info("Initialized session state store: {}, fetch strategy: {}",
storeClassName, fetchStrategy);
+ } catch (ClassNotFoundException e) {
+ LOG.warn("Session state store class not found: {}. Persistable sessions
disabled.", storeClassName);
+ this.sessionStateStore = null;
+ this.fetchStrategy = FetchStrategy.NEVER;
+ } catch (Exception e) {
+ LOG.error("Failed to initialize session state store: {}",
storeClassName, e);
+ throw new RuntimeException("Failed to initialize session state store",
e);
+ }
+ }
+
+ private void registerOpenSessionMetrics(Metrics metrics) {
MetricsVariable<Integer> openSessionCnt = new MetricsVariable<Integer>() {
@Override
public Integer getValue() {
@@ -181,7 +216,7 @@ public Integer getValue() {
metrics.addRatio(MetricsConstant.HS2_AVG_OPEN_SESSION_TIME,
openSessionTime, openSessionCnt);
}
- private void registerActiveSesssionMetrics(Metrics metrics) {
+ private void registerActiveSessionMetrics(Metrics metrics) {
MetricsVariable<Integer> activeSessionCnt = new MetricsVariable<Integer>()
{
@Override
public Integer getValue() {
@@ -433,6 +468,13 @@ public synchronized void stop() {
}
cleanupLoggingRootDir();
logManager.ifPresent(lm -> lm.stop());
+ if (sessionStateStore != null) {
+ try {
+ sessionStateStore.close();
+ } catch (Exception e) {
+ LOG.warn("Error closing session state store", e);
+ }
+ }
}
private void cleanupLoggingRootDir() {
@@ -578,6 +620,9 @@ public HiveSession createSession(SessionHandle
sessionHandle, TProtocolVersion p
throw new HiveSQLException(FAIL_CLOSE_ERROR_MESSAGE);
}
registerLlapTargetGaugeIfNeeded(session);
+ if (!recoveringSession.get()) {
+ saveSessionSnapshot(session);
+ }
LOG.info("Session opened, " + session.getSessionHandle()
+ ", current sessions:" + getOpenSessionCount());
return session;
@@ -685,6 +730,7 @@ public void closeSession(SessionHandle sessionHandle)
throws HiveSQLException {
}
LOG.info("Session closed, " + sessionHandle + ", current sessions:" +
getOpenSessionCount());
}
+ deleteSessionSnapshot(sessionHandle);
closeSessionInternal(session);
}
@@ -716,10 +762,80 @@ public void run() {
public HiveSession getSession(SessionHandle sessionHandle) throws
HiveSQLException {
HiveSession session = handleToSession.get(sessionHandle);
- if (session == null) {
+ if (session != null) {
+ if (fetchStrategy == FetchStrategy.ALWAYS && sessionStateStore != null) {
+ syncFromRemoteIfStale(session);
+ }
+ return session;
+ }
+ if (fetchStrategy == FetchStrategy.NEVER) {
throw new HiveSQLException("Invalid SessionHandle: " + sessionHandle);
}
- return session;
+ try {
+ return recoverySessions.computeIfAbsent(sessionHandle, handle -> {
+ try {
+ return recoverSession(handle);
+ } catch (HiveSQLException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } catch (RuntimeException e) {
+ if (e.getCause() instanceof HiveSQLException hivesqlexception) {
+ throw hivesqlexception;
+ }
+ throw e;
+ } finally {
+ recoverySessions.remove(sessionHandle);
+ }
+ }
+
+ private void syncFromRemoteIfStale(HiveSession session) {
+ try {
+ String handleId =
PersistableSessionUtils.storeKey(session.getSessionHandle());
+ HiveSessionSnapshot remoteSnapshot =
sessionStateStore.getSnapshot(handleId);
+ if (remoteSnapshot == null) {
+ return;
+ }
+ if (remoteSnapshot.getLastAccessTime() > session.getLastAccessTime()) {
+ LOG.info("Remote snapshot is newer for session {}, re-hydrating",
session.getSessionHandle());
+ hydrateSession(session, remoteSnapshot);
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to sync session from remote store: {}",
session.getSessionHandle(), e);
+ }
+ }
+
+ private HiveSession recoverSession(SessionHandle sessionHandle) throws
HiveSQLException {
+ String handleId = PersistableSessionUtils.storeKey(sessionHandle);
+ HiveSessionSnapshot snapshot = sessionStateStore.getSnapshot(handleId);
+ if (snapshot == null) {
+ throw new HiveSQLException("Invalid SessionHandle: " + sessionHandle);
+ }
+ LOG.info("Recovering session from state store: {}", sessionHandle);
+ TProtocolVersion protocol =
TProtocolVersion.findByValue(snapshot.getProtocolVersion());
+ if (protocol == null) {
+ protocol = TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V1;
+ }
+ SessionHandle recoveredHandle = new SessionHandle(
+ sessionHandle.getHandleIdentifier(), protocol);
+ boolean withImpersonation =
hiveConf.getBoolVar(ConfVars.HIVE_SERVER2_ENABLE_DOAS)
+ && snapshot.getUsername() != null;
+ recoveringSession.set(true);
+ try {
+ HiveSession recovered = createSession(recoveredHandle, protocol,
+ snapshot.getUsername(), null, snapshot.getIpAddress(),
+ null, withImpersonation, null);
+ hydrateSession(recovered, snapshot);
+ saveSessionSnapshot(recovered);
+ LOG.info("Successfully recovered session: {}", sessionHandle);
+ return recovered;
+ } finally {
+ recoveringSession.remove();
+ }
+ }
+
+ private void hydrateSession(HiveSession session, HiveSessionSnapshot
snapshot) throws HiveSQLException {
+ PersistableSessionUtils.hydrateSession(session, snapshot);
}
public OperationManager getOperationManager() {
@@ -839,5 +955,35 @@ public void allowSessions(boolean b) {
this.allowSessions = b;
}
}
+
+ public boolean isPersistableSessionsEnabled() {
+ return sessionStateStore != null;
+ }
+
+
+ public void notifySessionStateChanged(SessionHandle sessionHandle) {
+ HiveSession session = handleToSession.get(sessionHandle);
+ if (session != null) {
+ PersistableSessionUtils.saveSnapshot(sessionStateStore, session);
+ }
+ }
+
+ private void saveSessionSnapshot(HiveSession session) {
+ PersistableSessionUtils.saveSnapshot(sessionStateStore, session);
+ }
+
+ private void deleteSessionSnapshot(SessionHandle sessionHandle) {
+ PersistableSessionUtils.deleteSnapshot(sessionStateStore, sessionHandle);
+ }
+
+ @VisibleForTesting
+ public SessionStateStore getSessionStateStore() {
+ return sessionStateStore;
+ }
+
+ @VisibleForTesting
+ public FetchStrategy getFetchStrategy() {
+ return fetchStrategy;
+ }
}
diff --git
a/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java
b/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java
index 6d121613af8..9d30bc4ca79 100644
--- a/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java
+++ b/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java
@@ -411,6 +411,8 @@ public TOpenSessionResp OpenSession(TOpenSessionReq req)
throws TException {
map.put(HiveConf.ConfVars.HIVE_SERVER2_THRIFT_RESULTSET_DEFAULT_FETCH_SIZE.varname,
Integer.toString(fetchSize));
map.put(HiveConf.ConfVars.HIVE_DEFAULT_NULLS_LAST.varname,
String.valueOf(hiveConf.getBoolVar(ConfVars.HIVE_DEFAULT_NULLS_LAST)));
+
map.put(HiveConf.ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_FETCH_STRATEGY.varname,
+
hiveConf.getVar(ConfVars.HIVE_SERVER2_SESSION_STATE_STORE_FETCH_STRATEGY));
resp.setSessionHandle(sessionHandle.toTSessionHandle());
resp.setConfiguration(map);
resp.setStatus(OK_STATUS);
diff --git
a/service/src/test/org/apache/hive/service/cli/session/TestPersistableSessionUtils.java
b/service/src/test/org/apache/hive/service/cli/session/TestPersistableSessionUtils.java
new file mode 100644
index 00000000000..4d13ab8d038
--- /dev/null
+++
b/service/src/test/org/apache/hive/service/cli/session/TestPersistableSessionUtils.java
@@ -0,0 +1,74 @@
+/*
+ * 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.hive.service.cli.session;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.ql.metadata.Table;
+import org.apache.hadoop.hive.ql.session.SessionState;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestPersistableSessionUtils {
+
+ private SessionState sessionState;
+
+ @Before
+ public void setUp() {
+ sessionState = new SessionState(new HiveConf());
+ sessionState.setCurrentDatabase("default");
+ Map<String, Table> tempTables = new HashMap<>();
+ tempTables.put("tmp", new Table("default", "tmp"));
+ sessionState.getTempTables().put("default", tempTables);
+ }
+
+ @Test
+ public void testShouldPersistSnapshotForInsertIntoTempTable() {
+ assertTrue(PersistableSessionUtils.shouldPersistSnapshot(
+ "INSERT INTO tmp PARTITION(dt='2024-01-01') VALUES (1)",
sessionState));
+ assertTrue(PersistableSessionUtils.shouldPersistSnapshot(
+ "insert overwrite table tmp partition (dt='x') select 1",
sessionState));
+ assertTrue(PersistableSessionUtils.shouldPersistSnapshot(
+ "LOAD DATA INPATH '/tmp/data' INTO TABLE tmp PARTITION (dt='x')",
sessionState));
+ }
+
+ @Test
+ public void testShouldNotPersistSnapshotForDmlOnPersistentTable() {
+ assertFalse(PersistableSessionUtils.shouldPersistSnapshot(
+ "INSERT INTO permanent_table VALUES (1)", sessionState));
+ assertFalse(PersistableSessionUtils.shouldPersistSnapshot(
+ "INSERT OVERWRITE TABLE permanent_table SELECT 1", sessionState));
+ assertFalse(PersistableSessionUtils.shouldPersistSnapshot(
+ "LOAD DATA INPATH '/tmp/data' INTO TABLE permanent_table",
sessionState));
+ }
+
+ @Test
+ public void testShouldPersistSnapshotForStateChangingCommands() {
+ assertTrue(PersistableSessionUtils.shouldPersistSnapshot(
+ "SET hive.exec.mode=strict", sessionState));
+ assertTrue(PersistableSessionUtils.shouldPersistSnapshot(
+ "CREATE TEMPORARY TABLE tmp (id INT)", sessionState));
+ assertFalse(PersistableSessionUtils.shouldPersistSnapshot("SELECT 1",
sessionState));
+ }
+}