This is an automated email from the ASF dual-hosted git repository.

saihemanth-cloudera 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 f298b9ccee8 HIVE-29784: Cleaning lots of expired notifications could 
lead to OOM (#6734)
f298b9ccee8 is described below

commit f298b9ccee8fe3715196aeeeaa46f81bccf77043
Author: dengzh <[email protected]>
AuthorDate: Tue Sep 29 01:37:54 2026 +0800

    HIVE-29784: Cleaning lots of expired notifications could lead to OOM (#6734)
---
 .../apache/hadoop/hive/metastore/ObjectStore.java  |  11 +-
 .../metastore/iface/NotificationStore.java         |   6 +-
 .../metastore/impl/ColStatsStoreImpl.java          |   2 +-
 .../metastore/impl/NotificationStoreImpl.java      |  42 ++++---
 .../metastore/metastore/impl/TableStoreImpl.java   |  61 +++++-----
 .../hive/metastore/ExecutionContextTestUtils.java  |  57 ++++++++++
 .../metastore/TestPersistenceContextEviction.java  | 124 +++++++++++++++++++++
 7 files changed, 248 insertions(+), 55 deletions(-)

diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ObjectStore.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ObjectStore.java
index 3ca67b9f6b1..6e324ad4679 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ObjectStore.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ObjectStore.java
@@ -421,12 +421,11 @@ public <T> T unwrap(Class<T> iface) {
     if (descriptor == null) {
       throw new IllegalArgumentException("Unable to unwrap the store as " + 
iface);
     }
-    String implClassName =
-        conf.get("metastore." + descriptor.alias() + ".store.impl", "");
-    Class<?> ifaceImpl = descriptor.defaultImpl();
-    if (StringUtils.isNotEmpty(implClassName)) {
-      ifaceImpl = conf.getClass(implClassName, ifaceImpl);
-    }
+    String ifaceKey = "metastore." + descriptor.alias() + ".store.impl";
+    String implClassName = conf.getTrimmed(ifaceKey);
+    Class<?> ifaceImpl = StringUtils.isNotEmpty(implClassName)
+        ? conf.getClass(ifaceKey, descriptor.defaultImpl())
+        : descriptor.defaultImpl();
     T simpl = (T) JavaUtils.newInstance(ifaceImpl);
     List<Query> openQueries = new LinkedList<>();
     if (simpl instanceof RawStoreBundle rsb) {
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/iface/NotificationStore.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/iface/NotificationStore.java
index ed0b743e109..9f7737594b7 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/iface/NotificationStore.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/iface/NotificationStore.java
@@ -50,10 +50,11 @@ public interface NotificationStore {
   void addNotificationEvent(NotificationEvent event) throws MetaException;
 
   /**
-   * Remove older notification events.
+   * Remove older notification events, transaction is explicitly handled 
inside.
    *
    * @param olderThan Remove any events older or equal to a given number of 
seconds
    */
+  @MetaDescriptor.NoTransaction
   void cleanNotificationEvents(int olderThan);
 
   /**
@@ -72,9 +73,10 @@ public interface NotificationStore {
   NotificationEventsCountResponse 
getNotificationEventsCount(NotificationEventsCountRequest rqst);
 
   /**
-   * Remove older notification events.
+   * Remove older notification events, transaction is explicitly handled 
inside.
    * @param olderThan Remove any events older or equal to a given number of 
seconds
    */
+  @MetaDescriptor.NoTransaction
   void cleanWriteNotificationEvents(int olderThan);
 
   /**
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/ColStatsStoreImpl.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/ColStatsStoreImpl.java
index be1a672d354..fcfb3c9d5a1 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/ColStatsStoreImpl.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/ColStatsStoreImpl.java
@@ -484,7 +484,6 @@ private List<MTableColumnStatistics> 
getMTableColumnStatistics(Table table, List
     validateTableCols(table, colNames);
 
     List<MTableColumnStatistics> result = Collections.emptyList();
-    Query query = pm.newQuery(MTableColumnStatistics.class);
     result =
         Batchable.runBatched(batchSize, colNames, new Batchable<String, 
MTableColumnStatistics>() {
           @Override
@@ -505,6 +504,7 @@ public List<MTableColumnStatistics> run(List<String> input)
               params[i + 4] = input.get(i);
             }
             filter.append(")");
+            Query query = pm.newQuery(MTableColumnStatistics.class);
             query.setFilter(filter.toString());
             query.declareParameters(paramStr.toString());
             List<MTableColumnStatistics> paritial = 
(List<MTableColumnStatistics>) query.executeWithArray(params);
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/NotificationStoreImpl.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/NotificationStoreImpl.java
index 89c29abebaa..b0dd0d27d8d 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/NotificationStoreImpl.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/NotificationStoreImpl.java
@@ -248,16 +248,15 @@ private void cleanOlderEvents(int olderThan, Class table, 
String tableName) {
     final Optional<Integer> batchSize = (eventBatchSize > 0) ? 
Optional.of(eventBatchSize) : Optional.empty();
 
     final long start = System.nanoTime();
-    int deleteCount = doCleanNotificationEvents(tooOld, batchSize, table, 
tableName);
+    int deleteCount = 0;
+    int batchCount;
+    do {
+      batchCount = cleanNotificationEventsBatch(tooOld, batchSize, table, 
tableName);
+      deleteCount += batchCount;
+    } while (batchCount > 0);
 
     if (deleteCount == 0) {
       LOG.info("No {} events found to be cleaned with eventTime < {}", 
tableName, tooOld);
-    } else {
-      int batchCount = 0;
-      do {
-        batchCount = doCleanNotificationEvents(tooOld, batchSize, table, 
tableName);
-        deleteCount += batchCount;
-      } while (batchCount > 0);
     }
 
     final long finish = System.nanoTime();
@@ -265,6 +264,21 @@ private void cleanOlderEvents(int olderThan, Class table, 
String tableName) {
         TimeUnit.NANOSECONDS.toMillis(finish - start));
   }
 
+  private <T> int cleanNotificationEventsBatch(final int ageSec, final 
Optional<Integer> batchSize,
+      Class<T> tableClass, String tableName) {
+    boolean committed = false;
+    baseStore.openTransaction();
+    try {
+      int deleted = doCleanNotificationEvents(ageSec, batchSize, tableClass, 
tableName);
+      committed = baseStore.commitTransaction();
+      return deleted;
+    } finally {
+      if (!committed && baseStore.isActiveTransaction()) {
+        baseStore.rollbackTransaction();
+      }
+    }
+  }
+
   private <T> int doCleanNotificationEvents(final int ageSec, final 
Optional<Integer> batchSize,
       Class<T> tableClass, String tableName) {
     int eventsCount = 0;
@@ -309,6 +323,7 @@ private <T> int doCleanNotificationEvents(final int ageSec, 
final Optional<Integ
       }
       pm.deletePersistentAll(events);
     }
+    query.closeAll();
     return eventsCount;
   }
 
@@ -328,15 +343,11 @@ public NotificationEventsCountResponse 
getNotificationEventsCount(NotificationEv
     Long result = 0L;
     long fromEventId = rqst.getFromEventId();
     String inputDbName = rqst.getDbName();
-    String catName = rqst.isSetCatName() ? rqst.getCatName() : 
getDefaultCatalog(conf);
+    String catName = rqst.isSetCatName() ? 
normalizeIdentifier(rqst.getCatName()) : getDefaultCatalog(conf);
     long toEventId;
     String paramSpecs;
     List<Object> paramVals = new ArrayList<>();
 
-    // We store a catalog name in lower case in metastore and also use the 
same way everywhere in
-    // hive.
-    assert catName.equals(catName.toLowerCase());
-
     // Build the query to count events, part by part
     String queryStr = "select count(eventId) from " + 
MNotificationLog.class.getName();
     // count fromEventId onwards events
@@ -353,8 +364,7 @@ public NotificationEventsCountResponse 
getNotificationEventsCount(NotificationEv
       // counted.
       queryStr = queryStr + " && (dbName == inputDbName || dbName == null)";
       paramSpecs = paramSpecs + ", java.lang.String inputDbName";
-      // We store a database name in lower case in metastore.
-      paramVals.add(inputDbName.toLowerCase());
+      paramVals.add(normalizeIdentifier(inputDbName));
     }
 
     // catName could be NULL in case of transaction related events, which also 
need to be
@@ -374,7 +384,7 @@ public NotificationEventsCountResponse 
getNotificationEventsCount(NotificationEv
     if (rqst.isSetTableNames() && !rqst.getTableNames().isEmpty()) {
       queryStr = queryStr + " && (";
       for (String tableName : rqst.getTableNames()) {
-        paramVals.add(tableName.toLowerCase());
+        paramVals.add(normalizeIdentifier(tableName));
         queryStr = queryStr + "tableName == tableName" + paramVals.size() + " 
|| ";
         paramSpecs = paramSpecs + ", java.lang.String tableName" + 
paramVals.size();
       }
@@ -386,7 +396,7 @@ public NotificationEventsCountResponse 
getNotificationEventsCount(NotificationEv
     query.declareParameters(paramSpecs);
     result = (Long) query.executeWithArray(paramVals.toArray());
     // Cap the event count by limit if specified.
-    long  eventCount = result.longValue();
+    long eventCount = result.longValue();
     if (rqst.isSetLimit() && eventCount > rqst.getLimit()) {
       eventCount = rqst.getLimit();
     }
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/TableStoreImpl.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/TableStoreImpl.java
index 26f76bc92f0..f1fa5aaac40 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/TableStoreImpl.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/metastore/impl/TableStoreImpl.java
@@ -938,7 +938,7 @@ public Table alterTable(TableName tableName, Table 
newTable, String queryValidWr
     boolean isToTxn = isTxn && 
!TxnUtils.isTransactionalTable(oldt.getParameters());
     if (!isToTxn && isTxn && areTxnStatsSupported) {
       // Transactional table is altered without a txn. Make sure there are no 
changes to the flag.
-      String errorMsg = verifyStatsChangeCtx(TableName.getDbTable(name, 
dbname), oldt.getParameters(),
+      String errorMsg = verifyStatsChangeCtx(TableName.getDbTable(dbname, 
name), oldt.getParameters(),
           newTable.getParameters(), newTable.getWriteId(), queryValidWriteIds, 
false);
       if (errorMsg != null) {
         throw new MetaException(errorMsg);
@@ -1677,7 +1677,8 @@ protected List<Partition> getSqlResult() throws 
MetaException {
         // We couldn't do SQL filter pushdown. Get names via normal means.
         List<String> partNames = new LinkedList<>();
         hasUnknownPartitions.set(getPartitionNamesPrunedByExprNoTxn(
-            catName, dbName, tblName, partitionKeys, expr, 
args.getDefaultPartName(), (short) args.getMax(), partNames));
+            catName, dbName, tblName, partitionKeys, expr, 
args.getDefaultPartName(),
+            (short) args.getMax(), partNames));
         GetPartitionsArgs newArgs = new 
GetPartitionsArgs.GetPartitionsArgsBuilder(args).partNames(partNames).build();
         return getDirectSql().getPartitionsViaPartNames(catName, dbName, 
tblName, newArgs);
       }
@@ -1788,7 +1789,8 @@ public List<Partition> run(List<String> input) throws 
MetaException {
    * @param result The resulting names.
    * @return Whether the result contains any unknown partitions.
    */
-  private boolean getPartitionNamesPrunedByExprNoTxn(String catName, String 
dbName, String tblName, List<FieldSchema> partColumns, byte[] expr,
+  private boolean getPartitionNamesPrunedByExprNoTxn(String catName, String 
dbName, String tblName,
+      List<FieldSchema> partColumns, byte[] expr,
       String defaultPartName, short maxParts, List<String> result) throws 
MetaException {
     result.addAll(getPartitionNamesNoTxn(catName, dbName, tblName, (short) 
-1));
     return prunePartitionNamesByExpr(catName, dbName, tblName, result,
@@ -1846,25 +1848,17 @@ public Partition alterPartition(TableName tableName, 
List<String> part_vals, Par
     String dbname = normalizeIdentifier(tableName.getDb());
     String name = normalizeIdentifier(tableName.getTable());
     AtomicReference<MColumnDescriptor> oldCd = new AtomicReference<>();
-    Partition result = alterPartitionNoTxn(catName, dbname, name, part_vals, 
new_part, queryValidWriteIds, oldCd);
+    MTable table = this.getMTable(new_part.getCatName(), new_part.getDbName(), 
new_part.getTableName());
+    MPartition oldp = getMPartition(catName, dbname, name, part_vals, table);
+    Partition result = alterPartitionNoTxn(catName, dbname, name, oldp, 
new_part, queryValidWriteIds, oldCd, table);
     removeUnusedColumnDescriptor(oldCd.get());
     return result;
   }
 
   /**
    * Alters an existing partition. Initiates copy of SD. Returns the old CD.
-   * @param part_vals Partition values (of the original partition instance)
    * @param newPart Partition object containing new information
    */
-  private Partition alterPartitionNoTxn(String catName, String dbname, String 
name,
-      List<String> part_vals, Partition newPart, String validWriteIds, 
AtomicReference<MColumnDescriptor> oldCd)
-      throws InvalidObjectException, MetaException {
-    MTable table = this.getMTable(newPart.getCatName(), newPart.getDbName(), 
newPart.getTableName());
-    MPartition oldp = getMPartition(catName, dbname, name, part_vals, table);
-    return alterPartitionNoTxn(catName, dbname, name, oldp, newPart,
-        validWriteIds, oldCd, table);
-  }
-
   private Partition alterPartitionNoTxn(String catName, String dbname,
       String name, MPartition oldp, Partition newPart,
       String validWriteIds,
@@ -1873,15 +1867,15 @@ private Partition alterPartitionNoTxn(String catName, 
String dbname,
     catName = normalizeIdentifier(catName);
     name = normalizeIdentifier(name);
     dbname = normalizeIdentifier(dbname);
+    if (oldp == null) {
+      throw new InvalidObjectException("partition does not exist.");
+    }
     MPartition newp = convertToMPart(newPart, table);
     MColumnDescriptor oldCD = null;
     MStorageDescriptor oldSD = oldp.getSd();
     if (oldSD != null) {
       oldCD = oldSD.getCD();
     }
-    if (newp == null) {
-      throw new InvalidObjectException("partition does not exist.");
-    }
     oldp.setValues(newp.getValues());
     oldp.setPartitionName(newp.getPartitionName());
     boolean isTxn = TxnUtils.isTransactionalTable(table.getParameters());
@@ -1969,7 +1963,7 @@ protected List<Partition> alterPartitionsInternal(MTable 
table,
         throw new MetaException("Invalid DB name : " + tmpPart.getDbName());
       }
       if (!tmpPart.getTableName().equalsIgnoreCase(tblName)) {
-        throw new MetaException("Invalid table name : " + tmpPart.getDbName());
+        throw new MetaException("Invalid table name : " + 
tmpPart.getTableName());
       }
     }
     return new GetListHelper<TableName, Partition>(this, null) {
@@ -2004,8 +1998,9 @@ private List<Partition> alterPartitionsViaJdo(MTable 
table, List<String> partNam
       mPartitionList = (List<MPartition>) query.executeWithArray(tblName, 
dbName, partNames, catName);
       pm.retrieveAll(mPartitionList);
 
-      if (mPartitionList.size() > newParts.size()) {
-        throw new MetaException("Expecting only one partition but more than 
one partitions are found.");
+      if (mPartitionList.size() != newParts.size()) {
+        throw new MetaException("Expected " + newParts.size() + " partitions 
but found "
+            + mPartitionList.size());
       }
 
       Map<List<String>, MPartition> mPartsMap = new HashMap();
@@ -2017,8 +2012,9 @@ private List<Partition> alterPartitionsViaJdo(MTable 
table, List<String> partNam
       AtomicReference<MColumnDescriptor> oldCdRef = new AtomicReference<>();
       for (Partition tmpPart : newParts) {
         oldCdRef.set(null);
+        MPartition mPart = mPartsMap.get(tmpPart.getValues());
         Partition result = alterPartitionNoTxn(catName, dbName, tblName,
-            mPartsMap.get(tmpPart.getValues()), tmpPart, queryWriteIdList, 
oldCdRef, table);
+            mPart, tmpPart, queryWriteIdList, oldCdRef, table);
         results.add(result);
         if (oldCdRef.get() != null) {
           oldCds.add(oldCdRef.get());
@@ -2051,7 +2047,8 @@ public List<Partition> getPartitionsByFilter(TableName 
tableName, GetPartitionsA
 
       @Override
       protected boolean canUseDirectSql() throws MetaException {
-        return getDirectSql().generateSqlFilterForPushdown(catName, dbName, 
tblName, partitionKeys, tree, null, filter);
+        return getDirectSql().generateSqlFilterForPushdown(catName, dbName, 
tblName, partitionKeys,
+            tree, null, filter);
       }
 
       @Override
@@ -2106,8 +2103,8 @@ protected boolean canUseDirectSql() throws MetaException {
           // if the filter mode is BY_EXPR initialize the filter and generate 
the expression tree
           // if there are more than one filter string we AND them together
           initExpressionTree();
-          return 
getDirectSql().generateSqlFilterForPushdown(table.getCatName(), 
table.getDbName(), table.getTableName(),
-              table.getPartitionKeys(), tree, null, filter);
+          return 
getDirectSql().generateSqlFilterForPushdown(table.getCatName(), 
table.getDbName(),
+              table.getTableName(), table.getPartitionKeys(), tree, null, 
filter);
         }
         // BY_VALUES and BY_NAMES are always supported
         return true;
@@ -2363,7 +2360,8 @@ private List<MPartition> 
listMPartitionsWithProjection(List<String> fieldNames,
   }
 
   @Override
-  public int getNumPartitionsByFilter(TableName tableName, String filter) 
throws MetaException, NoSuchObjectException {
+  public int getNumPartitionsByFilter(TableName tableName, String filter)
+      throws MetaException, NoSuchObjectException {
     final ExpressionTree exprTree = 
org.apache.commons.lang3.StringUtils.isNotEmpty(filter)
         ? PartFilterExprUtil.parseFilterTree(filter) : 
ExpressionTree.EMPTY_TREE;
 
@@ -2383,7 +2381,8 @@ protected String describeResult() {
 
       @Override
       protected boolean canUseDirectSql() throws MetaException {
-        return getDirectSql().generateSqlFilterForPushdown(catName, dbName, 
tblName, partitionKeys, exprTree, null, filter);
+        return getDirectSql().generateSqlFilterForPushdown(catName, dbName, 
tblName, partitionKeys,
+            exprTree, null, filter);
       }
 
       @Override
@@ -2397,7 +2396,8 @@ protected Integer getJdoResult() throws MetaException, 
NoSuchObjectException {
     }.run(false);
   }
 
-  private Integer getNumPartitionsViaOrmFilter(String catName, String dbName, 
String tblName, ExpressionTree tree, boolean isValidatedFilter, 
List<FieldSchema> partitionKeys)
+  private Integer getNumPartitionsViaOrmFilter(String catName, String dbName, 
String tblName, ExpressionTree tree,
+      boolean isValidatedFilter, List<FieldSchema> partitionKeys)
       throws MetaException {
     Map<String, Object> params = new HashMap<>();
     String jdoFilter = makeQueryFilterString(catName, dbName, tblName, tree,
@@ -2852,7 +2852,8 @@ public int getObjectCount(String fieldName, String 
objName) {
   @Override
   public long updateParameterWithExpectedValue(Table table, String key, String 
expectedValue, String newValue)
       throws MetaException, NoSuchObjectException {
-    return new GetHelper<TableName, Long>(this, new 
TableName(table.getCatName(), table.getDbName(), table.getTableName())) {
+    return new GetHelper<TableName, Long>(this,
+        new TableName(table.getCatName(), table.getDbName(), 
table.getTableName())) {
       @Override
       protected String describeResult() {
         return "Affected rows";
@@ -3109,8 +3110,8 @@ private static StorageDescriptor 
convertToStorageDescriptor(
         msd.getLocation(), msd.getInputFormat(), msd.getOutputFormat(), msd
         .isCompressed(), msd.getNumBuckets(),
         (!isAcidTable) ? convertToSerDeInfo(msd.getSerDeInfo(), conf, true)
-            : new SerDeInfo(msd.getSerDeInfo().getName(), 
msd.getSerDeInfo().getSerializationLib(), Collections.emptyMap()),
-        bucList , orderList, sdParams);
+            : new SerDeInfo(msd.getSerDeInfo().getName(), 
msd.getSerDeInfo().getSerializationLib(),
+            Collections.emptyMap()), bucList , orderList, sdParams);
     if (!isAcidTable) {
       skewedInfo = new SkewedInfo(convertList(msd.getSkewedColNames()),
           convertToSkewedValues(msd.getSkewedColValues()),
diff --git 
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/ExecutionContextTestUtils.java
 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/ExecutionContextTestUtils.java
new file mode 100644
index 00000000000..1423788e64b
--- /dev/null
+++ 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/ExecutionContextTestUtils.java
@@ -0,0 +1,57 @@
+/*
+ * 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.hadoop.hive.metastore;
+
+import org.apache.hadoop.hive.metastore.metastore.PersistenceManagerProxy;
+import org.datanucleus.ExecutionContext;
+import org.datanucleus.api.jdo.JDOPersistenceManager;
+import org.datanucleus.cache.Level1Cache;
+import org.datanucleus.state.DNStateManager;
+
+import javax.jdo.PersistenceManager;
+
+/**
+ * Helpers for inspecting DataNucleus L1 (persistence context) cache in unit 
tests.
+ */
+public final class ExecutionContextTestUtils {
+
+  private ExecutionContextTestUtils() {
+  }
+
+  public static ExecutionContext getExecutionContext(PersistenceManager pm) {
+    if (pm instanceof JDOPersistenceManager) {
+      return ((JDOPersistenceManager) pm).getExecutionContext();
+    }
+    if (pm instanceof PersistenceManagerProxy.ExecutionContextReference) {
+      return ((PersistenceManagerProxy.ExecutionContextReference) 
pm).getExecutionContext();
+    }
+    throw new IllegalArgumentException("Unsupported PersistenceManager: " + 
pm.getClass());
+  }
+
+  public static int countCachedInstances(PersistenceManager pm, Class<?> 
clazz) {
+    Level1Cache l1Cache = getExecutionContext(pm).getLevel1Cache();
+    int count = 0;
+    for (DNStateManager stateManager : l1Cache.values()) {
+      if (clazz.isInstance(stateManager.getObject())) {
+        count++;
+      }
+    }
+    return count;
+  }
+}
diff --git 
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/metastore/TestPersistenceContextEviction.java
 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/metastore/TestPersistenceContextEviction.java
new file mode 100644
index 00000000000..d798e238c75
--- /dev/null
+++ 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/metastore/TestPersistenceContextEviction.java
@@ -0,0 +1,124 @@
+/*
+ * 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.hadoop.hive.metastore.metastore;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.metastore.ExecutionContextTestUtils;
+import org.apache.hadoop.hive.metastore.HMSHandler;
+import org.apache.hadoop.hive.metastore.MetaStoreTestUtils;
+import org.apache.hadoop.hive.metastore.ObjectStore;
+import org.apache.hadoop.hive.metastore.Warehouse;
+import org.apache.hadoop.hive.metastore.annotation.MetastoreUnitTest;
+import org.apache.hadoop.hive.metastore.api.MetaException;
+import org.apache.hadoop.hive.metastore.api.NotificationEvent;
+import org.apache.hadoop.hive.metastore.api.NotificationEventRequest;
+import org.apache.hadoop.hive.metastore.api.NotificationEventResponse;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf.ConfVars;
+import org.apache.hadoop.hive.metastore.messaging.EventMessage;
+import org.apache.hadoop.hive.metastore.model.MNotificationLog;
+import org.apache.hadoop.hive.metastore.utils.MetaStoreServerUtils;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import javax.jdo.PersistenceManager;
+import javax.jdo.Query;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Verifies batched metastore operations evict loaded JDO entities from the 
persistence context.
+ * Without eviction, long-lived RawStore instances (for example the DB 
notification cleaner thread)
+ * accumulate deleted entities in the L1 cache and can OOM.
+ */
+@Category(MetastoreUnitTest.class)
+public class TestPersistenceContextEviction {
+  private static final int BATCH_SIZE = 3;
+  private static final int NUM_EVENTS = 12;
+
+  private ObjectStore objectStore;
+  private Configuration conf;
+  private PersistenceManager pm;
+
+  @Before
+  public void setUp() throws Exception {
+    conf = MetastoreConf.newMetastoreConf();
+    MetastoreConf.setBoolVar(conf, ConfVars.HIVE_IN_TEST, true);
+    MetastoreConf.setLongVar(conf, 
MetastoreConf.ConfVars.EVENT_CLEAN_MAX_EVENTS, BATCH_SIZE);
+    MetastoreConf.setLongVar(conf, ConfVars.RAWSTORE_PARTITION_BATCH_SIZE, 
BATCH_SIZE);
+    MetaStoreTestUtils.setConfForStandloneMode(conf);
+
+    String currentUrl = MetastoreConf.getVar(conf, ConfVars.CONNECT_URL_KEY);
+    currentUrl = currentUrl.replace(MetaStoreServerUtils.JUNIT_DATABASE_PREFIX,
+        String.format("%s_%s", MetaStoreServerUtils.JUNIT_DATABASE_PREFIX, 
UUID.randomUUID()));
+    MetastoreConf.setVar(conf, ConfVars.CONNECT_URL_KEY, currentUrl);
+
+    objectStore = new ObjectStore();
+    objectStore.setConf(conf);
+    HMSHandler.createDefaultCatalog(objectStore, new Warehouse(conf));
+    pm = objectStore.createRawStoreBundle().getPersistentManager();
+  }
+
+  @Test
+  public void testExecutionContextCountsLoadedNotificationEvents() throws 
MetaException {
+    insertNotificationEvents(5, "payload");
+
+    objectStore.openTransaction();
+    try {
+      Query query = pm.newQuery(MNotificationLog.class);
+      List<MNotificationLog> events = (List<MNotificationLog>) query.execute();
+      pm.retrieveAll(events);
+      Assert.assertTrue("expected loaded events to remain in the persistence 
context",
+          ExecutionContextTestUtils.countCachedInstances(pm, 
MNotificationLog.class) >= 5);
+    } finally {
+      objectStore.rollbackTransaction();
+    }
+
+    Assert.assertEquals(0, ExecutionContextTestUtils.countCachedInstances(pm, 
MNotificationLog.class));
+  }
+
+  @Test
+  public void testCleanNotificationEventsEvictsCachedEntities() throws 
MetaException {
+    insertNotificationEvents(NUM_EVENTS, "x".repeat(50));
+
+    objectStore.openTransaction();
+    try {
+      objectStore.cleanNotificationEvents(0);
+      Assert.assertEquals("batched notification cleanup retains deleted events 
in the L1 cache", NUM_EVENTS,
+          ExecutionContextTestUtils.countCachedInstances(pm, 
MNotificationLog.class));
+    } finally {
+      objectStore.commitTransaction();
+    }
+
+    Assert.assertEquals("batched notification cleanup must not retain deleted 
events in the L1 cache", 0,
+        ExecutionContextTestUtils.countCachedInstances(pm, 
MNotificationLog.class));
+    NotificationEventResponse response = objectStore.getNextNotification(new 
NotificationEventRequest());
+    Assert.assertEquals(0, response.getEventsSize());
+  }
+
+  private void insertNotificationEvents(int count, String message) throws 
MetaException {
+    for (int i = 0; i < count; i++) {
+      NotificationEvent event = new NotificationEvent(0, 0,
+          EventMessage.EventType.CREATE_DATABASE.toString(), message);
+      objectStore.addNotificationEvent(event);
+    }
+  }
+}

Reply via email to