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 54516007d32a..e82794f39f82 100644
--- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
+++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
@@ -3886,8 +3886,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 978c1788a4a9..357a32376609 100644
--- a/itests/hive-unit/pom.xml
+++ b/itests/hive-unit/pom.xml
@@ -54,6 +54,18 @@
org.apache.hive
hive-service
+
+ org.apache.hive
+ hive-service-session-store
+ ${project.version}
+ test
+
+
+ redis.clients
+ jedis
+ ${jedis.version}
+ test
+
org.apache.hive
hive-llap-server
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 000000000000..48c3cb8ad755
--- /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 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 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 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 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 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 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
+
+ org.apache.hive
+ hive-service-session-store
+ ${project.version}
+
org.apache.hive
hive-llap-server
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 c216851dc808..d3f3a2a5aecb 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
@@ -44,6 +44,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
@@ -132,6 +134,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 4caa963e2a6f..3ff59f2489ac 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
@@ -78,6 +78,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;
@@ -648,6 +650,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 77c3d321f416..8df76d349628 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
@@ -64,6 +64,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;
@@ -819,6 +820,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, sessionConf, 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 a01bef71ca62..51b5bfbd5901 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
@@ -42,6 +42,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 000000000000..ab335d2782ce
--- /dev/null
+++ b/service/src/java/org/apache/hive/service/cli/session/PersistableSessionUtils.java
@@ -0,0 +1,471 @@
+/*
+ * 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.regex.Pattern;
+
+import java.util.LinkedHashMap;
+
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hive.common.TableName;
+import org.apache.hadoop.hive.conf.HiveConf;
+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;
+ }
+ java.util.regex.Matcher insertMatcher = INSERT_TARGET_PATTERN.matcher(statement);
+ if (insertMatcher.find()) {
+ return normalizeTableReference(insertMatcher.group(1));
+ }
+ java.util.regex.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> tempTables = sessionState.getTempTables();
+ if (tempTables == null || tempTables.isEmpty()) {
+ return false;
+ }
+ Map 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,
+ HiveConf sessionConf, TProtocolVersion protocol,
+ long creationTime, long lastAccessTime) {
+ List jars = new ArrayList<>();
+ List files = new ArrayList<>();
+ if (sessionState != null) {
+ Set jarSet = sessionState.list_resource(SessionState.ResourceType.JAR, null);
+ if (jarSet != null) {
+ jars.addAll(jarSet);
+ }
+ Set fileSet = sessionState.list_resource(SessionState.ResourceType.FILE, null);
+ if (fileSet != null) {
+ files.addAll(fileSet);
+ }
+ }
+
+ Map tempTableDefs = new HashMap<>();
+ Map> tempTablePartitionDefs = new HashMap<>();
+ if (sessionState != null && sessionState.getTempTables() != null) {
+ DDLPlanUtils ddlPlanUtils = new DDLPlanUtils();
+ for (Map.Entry> dbEntry :
+ sessionState.getTempTables().entrySet()) {
+ String dbName = dbEntry.getKey();
+ for (Map.Entry 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 partitionSnapshots = captureTempTablePartitions(
+ sessionState, dbName, tableName, table);
+ if (!partitionSnapshots.isEmpty()) {
+ tempTablePartitionDefs.put(tableKey, partitionSnapshots);
+ }
+ }
+ }
+ }
+
+ List 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 captureTempTablePartitions(SessionState sessionState,
+ String dbName, String tableName, Table table) {
+ List partitions = new ArrayList<>();
+ if (sessionState == null || !table.isPartitioned()) {
+ return partitions;
+ }
+ Map 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 captureTempFunctions(SessionState sessionState) {
+ List 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 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 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 tempTableDefs, Map> tempTablePartitionDefs)
+ throws HiveSQLException {
+ String currentDb = sessionState.getCurrentDatabase();
+ for (Map.Entry 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 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);
+ throw new HiveSQLException("Failed to restore temporary table " + entry.getKey(), e);
+ }
+ }
+ sessionState.setCurrentDatabase(currentDb);
+ }
+
+ private static void restoreTempTablePartitions(SessionState sessionState, Table table,
+ List partitions)
+ throws HiveException, MetaException, AlreadyExistsException {
+ String qualifiedKey = Warehouse.getQualifiedName(
+ table.getDbName().toLowerCase(), table.getTableName().toLowerCase());
+ TempTable tempTable = sessionState.getTempPartitions().get(qualifiedKey);
+ if (tempTable == null) {
+ throw new HiveException("TempTable partition metadata missing for " + qualifiedKey);
+ }
+ List toAdd = new ArrayList<>(partitions.size());
+ List partCols = table.getPartitionKeys();
+ for (TempTablePartitionSnapshot snapshot : partitions) {
+ Map partSpec = new LinkedHashMap<>();
+ List 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 c792eb6bbd92..210d94b0b7ae 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
@@ -28,6 +28,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;
@@ -58,6 +59,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;
@@ -110,7 +114,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 registeredLlapTargetGauges = ConcurrentHashMap.newKeySet();
+ private final Set registeredLlapTargetGauges = ConcurrentHashMap.newKeySet();
+ // Persistable session state store
+ private SessionStateStore sessionStateStore;
+ private FetchStrategy fetchStrategy;
+ private final ThreadLocal recoveringSession = ThreadLocal.withInitial(() -> false);
+ private final ConcurrentHashMap recoverySessions = new ConcurrentHashMap<>();
public SessionManager(HiveServer2 hiveServer2, boolean allowSessions) {
super(SessionManager.class.getSimpleName());
@@ -136,8 +145,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);
@@ -154,10 +163,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 openSessionCnt = new MetricsVariable() {
@Override
public Integer getValue() {
@@ -180,7 +215,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 activeSessionCnt = new MetricsVariable() {
@Override
public Integer getValue() {
@@ -432,6 +467,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() {
@@ -577,6 +619,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;
@@ -684,6 +729,7 @@ public void closeSession(SessionHandle sessionHandle) throws HiveSQLException {
}
LOG.info("Session closed, " + sessionHandle + ", current sessions:" + getOpenSessionCount());
}
+ deleteSessionSnapshot(sessionHandle);
closeSessionInternal(session);
}
@@ -715,10 +761,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) {
+ throw (HiveSQLException) e.getCause();
+ }
+ 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() {
@@ -838,5 +954,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/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 000000000000..4d13ab8d038c
--- /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 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));
+ }
+}