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

dengzhhu653 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 21dc6c134b2  HIVE-30053: Support multi-threaded CachedStore prewarm 
(#6785)
21dc6c134b2 is described below

commit 21dc6c134b2e2e64ab62e5e4996587af146aac44
Author: neatHyperTxt-meesho <[email protected]>
AuthorDate: Mon Sep 21 05:47:08 2026 +0530

     HIVE-30053: Support multi-threaded CachedStore prewarm (#6785)
---
 .../hadoop/hive/metastore/conf/MetastoreConf.java  |   6 +
 .../hadoop/hive/metastore/cache/CachedStore.java   | 220 ++--------
 .../metastore/cache/DefaultMetaCachePreWarm.java   | 455 +++++++++++++++++++++
 .../hive/metastore/cache/MetaCachePreWarm.java     |  56 +++
 .../hadoop/hive/metastore/cache/SharedCache.java   |  33 +-
 .../hive/metastore/cache/TestCachedStore.java      |  38 ++
 6 files changed, 605 insertions(+), 203 deletions(-)

diff --git 
a/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
 
b/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
index 96f1db1db2b..83542c02c4e 100644
--- 
a/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
+++ 
b/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
@@ -405,6 +405,12 @@ public enum ConfVars {
         "The maximum memory in bytes that the cached objects can use. "
         + "Memory used is calculated based on estimated size of tables and 
partitions in the cache. "
         + "Setting it to a negative value disables memory estimation."),
+    
CACHED_RAW_STORE_PREWARM_THREADS("metastore.cached.rawstore.prewarm.threads",
+        "hive.metastore.cached.rawstore.prewarm.threads", 1,
+        "Number of threads CachedStore uses to prewarm the cache from the 
backing database at startup. "
+        + "Each thread opens its own connection to the backing database, so 
this value should be kept "
+        + "below the connection pool size, and the effective concurrent load 
on the database during "
+        + "prewarm scales with it. The default of 1 preserves the original 
single-threaded prewarm."),
     CAPABILITY_CHECK("metastore.client.capability.check",
         "hive.metastore.client.capability.check", true,
         "Whether to check client capabilities for potentially breaking API 
usage."),
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/CachedStore.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/CachedStore.java
index 1e091b7d671..dae29e47f08 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/CachedStore.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/CachedStore.java
@@ -21,14 +21,12 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.EmptyStackException;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Stack;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ThreadFactory;
@@ -44,7 +42,6 @@
 import org.apache.hadoop.conf.Configurable;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.Path;
-import org.apache.hadoop.hive.common.DatabaseName;
 import org.apache.hadoop.hive.common.StatsSetupConst;
 import org.apache.hadoop.hive.common.TableName;
 import org.apache.hadoop.hive.metastore.Deadline;
@@ -120,7 +117,9 @@ public class CachedStore implements RawStore, Configurable {
   // This is set to true only if we were able to cache all the metadata.
   // We may not be able to cache all metadata if we hit 
CACHED_RAW_STORE_MAX_CACHE_MEMORY limit.
   private static AtomicBoolean isCachedAllMetadata = new AtomicBoolean(false);
-  private static TablesPendingPrewarm tblsPendingPrewarm = new 
TablesPendingPrewarm();
+  // The prewarmer while a prewarm is in flight, so that getTable can promote 
requested tables
+  // to the front of the prewarm queue; null once prewarm has completed
+  private static volatile MetaCachePreWarm activePreWarmer = null;
   private RawStore rawStore = null;
   private Configuration conf;
   private static boolean areTxnStatsSupported;
@@ -469,171 +468,23 @@ static void prewarm(RawStore rawStore) {
     }
     long startTime = System.nanoTime();
     LOG.info("Prewarming CachedStore");
-    long sleepTime = 100;
-    while (!isCachePrewarmed.get()) {
-      // Prevents throwing exceptions in our raw store calls since we're not 
using RawStoreProxy
-      Deadline.registerIfNot(1000000);
-      Collection<String> catalogsToCache;
-      try {
-        catalogsToCache = catalogsToCache(rawStore);
-        LOG.info("Going to cache catalogs: " + 
org.apache.commons.lang3.StringUtils.join(catalogsToCache, ", "));
-        List<Catalog> catalogs = new ArrayList<>(catalogsToCache.size());
-        for (String catName : catalogsToCache) {
-          catalogs.add(rawStore.getCatalog(catName));
-        }
-        sharedCache.populateCatalogsInCache(catalogs);
-      } catch (MetaException | NoSuchObjectException e) {
-        LOG.warn("Failed to populate catalogs in cache, going to try again", 
e);
-        try {
-          Thread.sleep(sleepTime);
-          sleepTime = sleepTime * 2;
-        } catch (InterruptedException timerEx) {
-          LOG.info("sleep interrupted", timerEx.getMessage());
-        }
-        // try again
-        continue;
-      }
-      LOG.info("Finished prewarming catalogs, starting on databases");
-      List<Database> databases = new ArrayList<>();
-      for (String catName : catalogsToCache) {
-        try {
-          List<String> dbNames = rawStore.getAllDatabases(catName);
-          LOG.info("Number of databases to prewarm in catalog {}: {}", 
catName, dbNames.size());
-          for (String dbName : dbNames) {
-            try {
-              databases.add(rawStore.getDatabase(catName, dbName));
-            } catch (NoSuchObjectException e) {
-              // Continue with next database
-              LOG.warn("Failed to cache database " + 
DatabaseName.getQualified(catName, dbName) + ", moving on", e);
-            }
-          }
-        } catch (MetaException e) {
-          LOG.warn("Failed to cache databases in catalog " + catName + ", 
moving on", e);
-        }
-      }
-      sharedCache.populateDatabasesInCache(databases);
-      LOG.info("Databases cache is now prewarmed. Now adding tables, 
partitions and statistics to the cache");
-      int numberOfDatabasesCachedSoFar = 0;
-      for (Database db : databases) {
-        String catName = StringUtils.normalizeIdentifier(db.getCatalogName());
-        String dbName = StringUtils.normalizeIdentifier(db.getName());
-        List<String> tblNames;
-        try {
-          tblNames = rawStore.getAllTables(catName, dbName);
-        } catch (MetaException e) {
-          LOG.warn("Failed to cache tables for database " + 
DatabaseName.getQualified(catName, dbName) + ", moving on");
-          // Continue with next database
-          continue;
-        }
-        tblsPendingPrewarm.addTableNamesForPrewarming(tblNames);
-        int totalTablesToCache = tblNames.size();
-        int numberOfTablesCachedSoFar = 0;
-        while (tblsPendingPrewarm.hasMoreTablesToPrewarm()) {
-          try {
-            String tblName = 
StringUtils.normalizeIdentifier(tblsPendingPrewarm.getNextTableNameToPrewarm());
-            if (!shouldCacheTable(catName, dbName, tblName)) {
-              continue;
-            }
-            Table table;
-            try {
-              table = rawStore.getTable(catName, dbName, tblName);
-            } catch (MetaException e) {
-              LOG.debug(ExceptionUtils.getStackTrace(e));
-              // It is possible the table is deleted during fetching tables of 
the database,
-              // in that case, continue with the next table
-              continue;
-            }
-            List<String> colNames = 
MetaStoreUtils.getColumnNamesForTable(table);
-            try {
-              ColumnStatistics tableColStats = null;
-              List<Partition> partitions = null;
-              List<ColumnStatistics> partitionColStats = null;
-              AggrStats aggrStatsAllPartitions = null;
-              AggrStats aggrStatsAllButDefaultPartition = null;
-              TableCacheObjects cacheObjects = new TableCacheObjects();
-              if (!table.getPartitionKeys().isEmpty()) {
-                Deadline.startTimer("getPartitions");
-                partitions = rawStore.getPartitions(catName, dbName, tblName, 
GetPartitionsArgs.getAllPartitions());
-                Deadline.stopTimer();
-                cacheObjects.setPartitions(partitions);
-                List<String> partNames = new ArrayList<>(partitions.size());
-                for (Partition p : partitions) {
-                  
partNames.add(Warehouse.makePartName(table.getPartitionKeys(), p.getValues()));
-                }
-                if (!partNames.isEmpty()) {
-                  // Get partition column stats for this table
-                  Deadline.startTimer("getPartitionColumnStatistics");
-                  partitionColStats =
-                      rawStore.getPartitionColumnStatistics(catName, dbName, 
tblName, partNames, colNames, CacheUtils.HIVE_ENGINE);
-                  Deadline.stopTimer();
-                  cacheObjects.setPartitionColStats(partitionColStats);
-                  // Get aggregate stats for all partitions of a table and for 
all but default
-                  // partition
-                  Deadline.startTimer("getAggrPartitionColumnStatistics");
-                  aggrStatsAllPartitions = 
rawStore.get_aggr_stats_for(catName, dbName, tblName, partNames, colNames, 
CacheUtils.HIVE_ENGINE);
-                  Deadline.stopTimer();
-                  
cacheObjects.setAggrStatsAllPartitions(aggrStatsAllPartitions);
-                  // Remove default partition from partition names and get 
aggregate
-                  // stats again
-                  List<FieldSchema> partKeys = table.getPartitionKeys();
-                  String defaultPartitionValue =
-                      MetastoreConf.getVar(rawStore.getConf(), 
ConfVars.DEFAULTPARTITIONNAME);
-                  List<String> partCols = new ArrayList<>();
-                  List<String> partVals = new ArrayList<>();
-                  for (FieldSchema fs : partKeys) {
-                    partCols.add(fs.getName());
-                    partVals.add(defaultPartitionValue);
-                  }
-                  String defaultPartitionName = 
FileUtils.makePartName(partCols, partVals);
-                  partNames.remove(defaultPartitionName);
-                  Deadline.startTimer("getAggrPartitionColumnStatistics");
-                  aggrStatsAllButDefaultPartition =
-                      rawStore.get_aggr_stats_for(catName, dbName, tblName, 
partNames, colNames, CacheUtils.HIVE_ENGINE);
-                  Deadline.stopTimer();
-                  
cacheObjects.setAggrStatsAllButDefaultPartition(aggrStatsAllButDefaultPartition);
-                }
-              } else {
-                Deadline.startTimer("getTableColumnStatistics");
-                tableColStats = rawStore.getTableColumnStatistics(catName, 
dbName, tblName, colNames, CacheUtils.HIVE_ENGINE);
-                Deadline.stopTimer();
-                cacheObjects.setTableColStats(tableColStats);
-              }
-
-              Deadline.startTimer("getAllTableConstraints");
-              SQLAllTableConstraints tableConstraints = 
rawStore.getAllTableConstraints(
-                  new AllTableConstraintsRequest(catName, dbName, tblName));
-              Deadline.stopTimer();
-              cacheObjects.setTableConstraints(tableConstraints);
-
-              // If the table could not cached due to memory limit, stop 
prewarm
-              boolean isSuccess = sharedCache
-                  .populateTableInCache(table, cacheObjects);
-              if (isSuccess) {
-                LOG.trace("Cached Database: {}'s Table: {}.", dbName, tblName);
-              } else {
-                LOG.info("Unable to cache Database: {}'s Table: {}, since the 
cache memory is full. "
-                    + "Will stop attempting to cache any more tables.", 
dbName, tblName);
-                completePrewarm(startTime, false);
-                return;
-              }
-            } catch (MetaException | NoSuchObjectException e) {
-              LOG.debug(ExceptionUtils.getStackTrace(e));
-              // Continue with next table
-              continue;
-            }
-            LOG.debug("Processed database: {}'s table: {}. Cached {} / {}  
tables so far.", dbName, tblName,
-                ++numberOfTablesCachedSoFar, totalTablesToCache);
-          } catch (EmptyStackException e) {
-            // We've prewarmed this database, continue with the next one
-            continue;
-          }
-        }
-        LOG.debug("Processed database: {}. Cached {} / {} databases so far.", 
dbName, ++numberOfDatabasesCachedSoFar,
-            databases.size());
-      }
+    boolean cachedAllMetadata;
+    // The prewarmer is closed (its workers terminated, their stores shut 
down) before completion
+    // is published, so nothing can still be mutating the cache once 
isCachePrewarmed is set
+    try (MetaCachePreWarm preWarmer = new DefaultMetaCachePreWarm(rawStore, 
sharedCache)) {
+      preWarmer.setConf(rawStore.getConf());
+      preWarmer.initialize();
+      activePreWarmer = preWarmer;
+      cachedAllMetadata = preWarmer.preWarm();
+    } catch (MetaException e) {
+      throw new RuntimeException("CachedStore prewarm failed", e);
+    } finally {
+      activePreWarmer = null;
+    }
+    if (cachedAllMetadata) {
       sharedCache.clearDirtyFlags();
-      completePrewarm(startTime, true);
     }
+    completePrewarm(startTime, cachedAllMetadata);
   }
 
   /**
@@ -658,32 +509,6 @@ static void completePrewarm(long startTime, boolean 
cachedAllMetadata) {
     sharedCache.completeTableCachePrewarm();
   }
 
-  static class TablesPendingPrewarm {
-    private Stack<String> tableNames = new Stack<>();
-
-    private synchronized void addTableNamesForPrewarming(List<String> 
tblNames) {
-      tableNames.clear();
-      if (tblNames != null) {
-        tableNames.addAll(tblNames);
-      }
-    }
-
-    private synchronized boolean hasMoreTablesToPrewarm() {
-      return !tableNames.empty();
-    }
-
-    private synchronized String getNextTableNameToPrewarm() {
-      return tableNames.pop();
-    }
-
-    private synchronized void prioritizeTableForPrewarm(String tblName) {
-      // If the table is in the pending prewarm list, move it to the top
-      if (tableNames.remove(tblName)) {
-        tableNames.push(tblName);
-      }
-    }
-  }
-
   @VisibleForTesting static void setCachePrewarmedState(boolean state) {
     isCachePrewarmed.set(state);
   }
@@ -695,7 +520,7 @@ private static void initBlackListWhiteList(Configuration 
conf) {
         MetastoreConf.getAsString(conf, 
MetastoreConf.ConfVars.CACHED_RAW_STORE_CACHED_OBJECTS_BLACKLIST));
   }
 
-  private static Collection<String> catalogsToCache(RawStore rs) throws 
MetaException {
+  static Collection<String> catalogsToCache(RawStore rs) {
     Collection<String> confValue = 
MetastoreConf.getStringCollection(rs.getConf(), ConfVars.CATALOGS_TO_CACHE);
     if (confValue == null || confValue.isEmpty() || (confValue.size() == 1 && 
confValue.contains(""))) {
       return rs.getCatalogs();
@@ -1364,9 +1189,12 @@ public Table getTable(String catName, String dbName, 
String tblName, String vali
     if (tbl == null) {
       // This table is not yet loaded in cache
       // If the prewarm thread is working on this table's database,
-      // let's move this table to the top of tblNamesBeingPrewarmed stack,
+      // let's move this table to the top of the prewarm queue,
       // so that it gets loaded to the cache faster and is available for 
subsequent requests
-      tblsPendingPrewarm.prioritizeTableForPrewarm(tblName);
+      MetaCachePreWarm preWarmer = activePreWarmer;
+      if (preWarmer != null) {
+        preWarmer.prioritizeTableForPrewarm(new TableName(catName, dbName, 
tblName));
+      }
       Table t = rawStore.getTable(catName, dbName, tblName, validWriteIds);
       if (t != null) {
         sharedCache.addTableToCache(catName, dbName, tblName, t);
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/DefaultMetaCachePreWarm.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/DefaultMetaCachePreWarm.java
new file mode 100644
index 00000000000..f41b5251c9f
--- /dev/null
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/DefaultMetaCachePreWarm.java
@@ -0,0 +1,455 @@
+/*
+ * 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.cache;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EmptyStackException;
+import java.util.List;
+import java.util.Stack;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.common.DatabaseName;
+import org.apache.hadoop.hive.common.TableName;
+import org.apache.hadoop.hive.metastore.Deadline;
+import org.apache.hadoop.hive.metastore.ObjectStore;
+import org.apache.hadoop.hive.metastore.RawStore;
+import org.apache.hadoop.hive.metastore.Warehouse;
+import org.apache.hadoop.hive.metastore.api.AggrStats;
+import org.apache.hadoop.hive.metastore.api.AllTableConstraintsRequest;
+import org.apache.hadoop.hive.metastore.api.Catalog;
+import org.apache.hadoop.hive.metastore.api.ColumnStatistics;
+import org.apache.hadoop.hive.metastore.api.Database;
+import org.apache.hadoop.hive.metastore.api.FieldSchema;
+import org.apache.hadoop.hive.metastore.api.MetaException;
+import org.apache.hadoop.hive.metastore.api.NoSuchObjectException;
+import org.apache.hadoop.hive.metastore.api.Partition;
+import org.apache.hadoop.hive.metastore.api.SQLAllTableConstraints;
+import org.apache.hadoop.hive.metastore.api.Table;
+import org.apache.hadoop.hive.metastore.client.builder.GetPartitionsArgs;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf.ConfVars;
+import org.apache.hadoop.hive.metastore.utils.FileUtils;
+import org.apache.hadoop.hive.metastore.utils.JavaUtils;
+import org.apache.hadoop.hive.metastore.utils.MetaStoreUtils;
+import org.apache.hadoop.hive.metastore.utils.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default {@link MetaCachePreWarm} implementation: walks the catalogs, 
databases and tables of
+ * the backing database through RawStore and populates the given {@link 
SharedCache}. Warming the
+ * tables can be spread over metastore.cached.rawstore.prewarm.threads worker 
threads, each with
+ * its own RawStore instance and hence its own connection to the backing 
database.
+ */
+class DefaultMetaCachePreWarm implements MetaCachePreWarm {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(DefaultMetaCachePreWarm.class);
+  // How long to wait for the prewarm workers to terminate before giving up on 
a clean shutdown
+  private static final long WORKER_SHUTDOWN_TIMEOUT_MS = 10000;
+
+  private final RawStore rawStore;
+  private final SharedCache sharedCache;
+  private final TablesPendingPrewarm tblsPendingPrewarm = new 
TablesPendingPrewarm();
+  private Configuration conf;
+  private ExecutorService prewarmPool;
+  private final List<RawStore> workerStores = new ArrayList<>();
+
+  DefaultMetaCachePreWarm(RawStore rawStore, SharedCache sharedCache) {
+    this.rawStore = rawStore;
+    this.sharedCache = sharedCache;
+  }
+
+  @Override public void prioritizeTableForPrewarm(TableName... tableNames) {
+    for (TableName tableName : tableNames) {
+      tblsPendingPrewarm.prioritizeTableForPrewarm(tableName.getTable());
+    }
+  }
+
+  @Override public void setConf(Configuration conf) {
+    this.conf = conf;
+  }
+
+  @Override public Configuration getConf() {
+    return conf;
+  }
+
+  @Override public void initialize() throws MetaException {
+    int prewarmThreads = Math.max(1, MetastoreConf.getIntVar(conf, 
ConfVars.CACHED_RAW_STORE_PREWARM_THREADS));
+    if (prewarmThreads > 1) {
+      try {
+        // RawStore implementations (ObjectStore) are not thread safe, so each 
worker gets its
+        // own instance and hence its own connection to the backing database
+        for (int i = 0; i < prewarmThreads; i++) {
+          workerStores.add(createRawStore(conf));
+        }
+        LOG.info("Prewarming table cache with {} threads", prewarmThreads);
+        prewarmPool = Executors.newFixedThreadPool(prewarmThreads, new 
ThreadFactory() {
+          private final AtomicInteger threadCount = new AtomicInteger();
+          @Override public Thread newThread(Runnable r) {
+            Thread t = Executors.defaultThreadFactory().newThread(r);
+            t.setName("CachedStore-PrewarmWorker-" + 
threadCount.getAndIncrement());
+            t.setDaemon(true);
+            return t;
+          }
+        });
+      } catch (RuntimeException e) {
+        LOG.warn("Failed to create RawStores for prewarm workers, falling back 
to single threaded prewarm", e);
+        close();
+      }
+    }
+  }
+
+  @Override public boolean preWarm() throws MetaException {
+    long sleepTime = 100;
+    while (true) {
+      // Prevents throwing exceptions in our raw store calls since we're not 
using RawStoreProxy
+      Deadline.registerIfNot(1000000);
+      Collection<String> catalogsToCache;
+      try {
+        catalogsToCache = preWarmCatalogs();
+      } catch (MetaException | NoSuchObjectException e) {
+        LOG.warn("Failed to populate catalogs in cache, going to try again", 
e);
+        try {
+          Thread.sleep(sleepTime);
+          sleepTime = sleepTime * 2;
+        } catch (InterruptedException timerEx) {
+          Thread.currentThread().interrupt();
+          LOG.warn("Interrupted while waiting to retry the catalog prewarm, 
stopping prewarm");
+          return false;
+        }
+        // try again
+        continue;
+      }
+      LOG.info("Finished prewarming catalogs, starting on databases");
+      List<Database> databases = listDatabases(catalogsToCache);
+      sharedCache.populateDatabasesInCache(databases);
+      LOG.info("Databases cache is now prewarmed. Now adding tables, 
partitions and statistics to the cache");
+      return preWarmTables(databases);
+    }
+  }
+
+  /** Caches all catalogs and returns their names. */
+  private Collection<String> preWarmCatalogs() throws MetaException, 
NoSuchObjectException {
+    Collection<String> catalogsToCache = CachedStore.catalogsToCache(rawStore);
+    LOG.info("Going to cache catalogs: {}", 
org.apache.commons.lang3.StringUtils.join(catalogsToCache, ", "));
+    List<Catalog> catalogs = new ArrayList<>(catalogsToCache.size());
+    for (String catName : catalogsToCache) {
+      catalogs.add(rawStore.getCatalog(catName));
+    }
+    sharedCache.populateCatalogsInCache(catalogs);
+    return catalogsToCache;
+  }
+
+  /** Lists the databases of the given catalogs, skipping the ones that cannot 
be read. */
+  private List<Database> listDatabases(Collection<String> catalogsToCache) {
+    List<Database> databases = new ArrayList<>();
+    for (String catName : catalogsToCache) {
+      try {
+        List<String> dbNames = rawStore.getAllDatabases(catName);
+        LOG.info("Number of databases to prewarm in catalog {}: {}", catName, 
dbNames.size());
+        for (String dbName : dbNames) {
+          try {
+            databases.add(rawStore.getDatabase(catName, dbName));
+          } catch (NoSuchObjectException e) {
+            // Continue with next database
+            LOG.warn("Failed to cache database {}, moving on", 
DatabaseName.getQualified(catName, dbName), e);
+          }
+        }
+      } catch (MetaException e) {
+        LOG.warn("Failed to cache databases in catalog {}, moving on", 
catName, e);
+      }
+    }
+    return databases;
+  }
+
+  /**
+   * Warms all cacheable tables of the given databases, using the worker pool 
when one was
+   * created. Returns true if all metadata was cached, false if warming 
stopped early because the
+   * cache memory limit was reached or the thread was interrupted. The caller 
must {@link #close()}
+   * this prewarmer, which awaits worker termination, before publishing 
completion.
+   */
+  private boolean preWarmTables(List<Database> databases) {
+    int numberOfDatabasesCachedSoFar = 0;
+    for (Database db : databases) {
+      String catName = StringUtils.normalizeIdentifier(db.getCatalogName());
+      String dbName = StringUtils.normalizeIdentifier(db.getName());
+      List<String> tblNames;
+      try {
+        tblNames = rawStore.getAllTables(catName, dbName);
+      } catch (MetaException e) {
+        LOG.warn("Failed to cache tables for database {}, moving on", 
DatabaseName.getQualified(catName, dbName));
+        // Continue with next database
+        continue;
+      }
+      tblsPendingPrewarm.addTableNamesForPrewarming(tblNames);
+      int totalTablesToCache = tblNames.size();
+      AtomicBoolean stopPrewarm = new AtomicBoolean(false);
+      AtomicInteger tablesCachedSoFar = new AtomicInteger();
+      if (prewarmPool != null) {
+        List<Future<?>> workers = new ArrayList<>(workerStores.size());
+        for (RawStore workerStore : workerStores) {
+          workers.add(prewarmPool.submit(
+              () -> drainTablesPendingPrewarm(workerStore, catName, dbName, 
stopPrewarm, tablesCachedSoFar,
+                  totalTablesToCache)));
+        }
+        if (!awaitWorkers(workers, dbName, stopPrewarm)) {
+          return false;
+        }
+      } else {
+        drainTablesPendingPrewarm(rawStore, catName, dbName, stopPrewarm, 
tablesCachedSoFar, totalTablesToCache);
+      }
+      if (stopPrewarm.get()) {
+        // The cache is full: stop here and serve with whatever has been 
cached so far
+        return false;
+      }
+      LOG.debug("Processed database: {}. Cached {} / {} databases so far.", 
dbName, ++numberOfDatabasesCachedSoFar,
+          databases.size());
+    }
+    return true;
+  }
+
+  /**
+   * Waits for all the given workers to finish. On interruption, tells the 
remaining workers to
+   * stop (close() then awaits their termination) and returns false; a failed 
worker is only
+   * logged, the tables it could not cache are served from the raw store until 
the cache update
+   * service refreshes them.
+   */
+  private boolean awaitWorkers(List<Future<?>> workers, String dbName, 
AtomicBoolean stopPrewarm) {
+    for (Future<?> worker : workers) {
+      try {
+        worker.get();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        LOG.warn("Interrupted while waiting for prewarm workers on database 
{}; "
+            + "completing prewarm with the metadata cached so far", dbName);
+        stopPrewarm.set(true);
+        return false;
+      } catch (ExecutionException e) {
+        LOG.warn("Prewarm worker failed for database {}, moving on", dbName, 
e);
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Drains tables from tblsPendingPrewarm for the given database, caching 
each one, until the
+   * pending list is empty or the shared cache reports its memory limit is 
reached. Safe to run
+   * from multiple threads concurrently: the pending-table stack hands out 
each table exactly once,
+   * and hot tables promoted by prioritizeTableForPrewarm are picked up by 
whichever worker pops next.
+   */
+  private void drainTablesPendingPrewarm(RawStore rawStore, String catName, 
String dbName,
+      AtomicBoolean stopPrewarm, AtomicInteger tablesCachedSoFar, int 
totalTablesToCache) {
+    // Deadline is thread local; register it for prewarm worker threads
+    Deadline.registerIfNot(1000000);
+    while (!stopPrewarm.get() && !Thread.currentThread().isInterrupted()
+        && tblsPendingPrewarm.hasMoreTablesToPrewarm()) {
+      String tblName;
+      try {
+        tblName = 
StringUtils.normalizeIdentifier(tblsPendingPrewarm.getNextTableNameToPrewarm());
+      } catch (EmptyStackException e) {
+        // Another worker drained the remaining tables between our check and 
pop
+        break;
+      }
+      if (CachedStore.shouldCacheTable(catName, dbName, tblName)) {
+        if (!preWarmTable(rawStore, catName, dbName, tblName)) {
+          LOG.info("Unable to cache Database: {}'s Table: {}, since the cache 
memory is full. "
+              + "Will stop attempting to cache any more tables.", dbName, 
tblName);
+          stopPrewarm.set(true);
+          return;
+        }
+        LOG.debug("Processed database: {}'s table: {}. Cached {} / {}  tables 
so far.", dbName, tblName,
+            tablesCachedSoFar.incrementAndGet(), totalTablesToCache);
+      }
+    }
+  }
+
+  /**
+   * Fetches one table with its partitions, statistics and constraints from 
the backing database
+   * and populates it in the shared cache. Returns false only when the shared 
cache reports that
+   * its memory limit is reached; a table that vanished or failed to load is 
skipped by returning
+   * true so that prewarm continues with the next table.
+   */
+  private boolean preWarmTable(RawStore rawStore, String catName, String 
dbName, String tblName) {
+    Table table;
+    try {
+      table = rawStore.getTable(catName, dbName, tblName);
+    } catch (MetaException e) {
+      LOG.debug(ExceptionUtils.getStackTrace(e));
+      // It is possible the table is deleted during fetching tables of the 
database,
+      // in that case, continue with the next table
+      return true;
+    }
+    List<String> colNames = MetaStoreUtils.getColumnNamesForTable(table);
+    try {
+      ColumnStatistics tableColStats = null;
+      List<Partition> partitions = null;
+      List<ColumnStatistics> partitionColStats = null;
+      AggrStats aggrStatsAllPartitions = null;
+      AggrStats aggrStatsAllButDefaultPartition = null;
+      TableCacheObjects cacheObjects = new TableCacheObjects();
+      if (!table.getPartitionKeys().isEmpty()) {
+        Deadline.startTimer("getPartitions");
+        partitions = rawStore.getPartitions(catName, dbName, tblName, 
GetPartitionsArgs.getAllPartitions());
+        Deadline.stopTimer();
+        cacheObjects.setPartitions(partitions);
+        List<String> partNames = new ArrayList<>(partitions.size());
+        for (Partition p : partitions) {
+          partNames.add(Warehouse.makePartName(table.getPartitionKeys(), 
p.getValues()));
+        }
+        if (!partNames.isEmpty()) {
+          // Get partition column stats for this table
+          Deadline.startTimer("getPartitionColumnStatistics");
+          partitionColStats = rawStore.getPartitionColumnStatistics(catName, 
dbName, tblName, partNames, colNames,
+              CacheUtils.HIVE_ENGINE);
+          Deadline.stopTimer();
+          cacheObjects.setPartitionColStats(partitionColStats);
+          // Get aggregate stats for all partitions of a table and for all but 
default
+          // partition
+          Deadline.startTimer("getAggrPartitionColumnStatistics");
+          aggrStatsAllPartitions = rawStore.get_aggr_stats_for(catName, 
dbName, tblName, partNames, colNames,
+              CacheUtils.HIVE_ENGINE);
+          Deadline.stopTimer();
+          cacheObjects.setAggrStatsAllPartitions(aggrStatsAllPartitions);
+          // Remove default partition from partition names and get aggregate
+          // stats again
+          List<FieldSchema> partKeys = table.getPartitionKeys();
+          String defaultPartitionValue =
+              MetastoreConf.getVar(rawStore.getConf(), 
ConfVars.DEFAULTPARTITIONNAME);
+          List<String> partCols = new ArrayList<>();
+          List<String> partVals = new ArrayList<>();
+          for (FieldSchema fs : partKeys) {
+            partCols.add(fs.getName());
+            partVals.add(defaultPartitionValue);
+          }
+          String defaultPartitionName = FileUtils.makePartName(partCols, 
partVals);
+          partNames.remove(defaultPartitionName);
+          Deadline.startTimer("getAggrPartitionColumnStatistics");
+          aggrStatsAllButDefaultPartition =
+              rawStore.get_aggr_stats_for(catName, dbName, tblName, partNames, 
colNames, CacheUtils.HIVE_ENGINE);
+          Deadline.stopTimer();
+          
cacheObjects.setAggrStatsAllButDefaultPartition(aggrStatsAllButDefaultPartition);
+        }
+      } else {
+        Deadline.startTimer("getTableColumnStatistics");
+        tableColStats = rawStore.getTableColumnStatistics(catName, dbName, 
tblName, colNames, CacheUtils.HIVE_ENGINE);
+        Deadline.stopTimer();
+        cacheObjects.setTableColStats(tableColStats);
+      }
+
+      Deadline.startTimer("getAllTableConstraints");
+      SQLAllTableConstraints tableConstraints = 
rawStore.getAllTableConstraints(
+          new AllTableConstraintsRequest(catName, dbName, tblName));
+      Deadline.stopTimer();
+      cacheObjects.setTableConstraints(tableConstraints);
+
+      // If the table could not be cached due to memory limit, stop prewarm
+      boolean isSuccess = sharedCache
+          .populateTableInCache(table, cacheObjects);
+      if (isSuccess) {
+        LOG.trace("Cached Database: {}'s Table: {}.", dbName, tblName);
+      } else {
+        return false;
+      }
+    } catch (MetaException | NoSuchObjectException e) {
+      LOG.debug(ExceptionUtils.getStackTrace(e));
+      // Continue with next table
+    }
+    return true;
+  }
+
+  /**
+   * Creates a fresh RawStore instance for a prewarm worker thread, mirroring 
the way
+   * CacheUpdateMasterWork creates its own store.
+   */
+  private static RawStore createRawStore(Configuration conf) {
+    String rawStoreClassName = MetastoreConf.getVar(conf, 
ConfVars.CACHED_RAW_STORE_IMPL, ObjectStore.class.getName());
+    try {
+      RawStore rs = JavaUtils.getClass(rawStoreClassName, 
RawStore.class).newInstance();
+      rs.setConf(conf);
+      return rs;
+    } catch (InstantiationException | IllegalAccessException | MetaException 
e) {
+      throw new RuntimeException("Cannot instantiate " + rawStoreClassName, e);
+    }
+  }
+
+  /**
+   * Stops the prewarm workers and waits for them to terminate before their 
RawStores are closed,
+   * so that no worker can still be reading from a closed store or writing to 
the shared cache
+   * once prewarm reports completion. Idempotent.
+   */
+  @Override public void close() {
+    if (prewarmPool != null) {
+      prewarmPool.shutdownNow();
+      try {
+        if (!prewarmPool.awaitTermination(WORKER_SHUTDOWN_TIMEOUT_MS, 
TimeUnit.MILLISECONDS)) {
+          LOG.warn("Prewarm workers did not terminate within {} ms", 
WORKER_SHUTDOWN_TIMEOUT_MS);
+        }
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        LOG.warn("Interrupted while waiting for the prewarm workers to 
terminate");
+      }
+      prewarmPool = null;
+    }
+    for (RawStore workerStore : workerStores) {
+      try {
+        workerStore.shutdown();
+      } catch (RuntimeException e) {
+        LOG.warn("Failed to shut down a prewarm worker RawStore", e);
+      }
+    }
+    workerStores.clear();
+  }
+
+  /** The tables of the database currently being prewarmed that have not been 
cached yet. */
+  private static class TablesPendingPrewarm {
+    private final Stack<String> tableNames = new Stack<>();
+
+    synchronized void addTableNamesForPrewarming(List<String> tblNames) {
+      tableNames.clear();
+      if (tblNames != null) {
+        tableNames.addAll(tblNames);
+      }
+    }
+
+    synchronized boolean hasMoreTablesToPrewarm() {
+      return !tableNames.empty();
+    }
+
+    synchronized String getNextTableNameToPrewarm() {
+      return tableNames.pop();
+    }
+
+    synchronized void prioritizeTableForPrewarm(String tblName) {
+      // If the table is in the pending prewarm list, move it to the top
+      if (tableNames.remove(tblName)) {
+        tableNames.push(tblName);
+      }
+    }
+  }
+}
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/MetaCachePreWarm.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/MetaCachePreWarm.java
new file mode 100644
index 00000000000..a6753ca7781
--- /dev/null
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/MetaCachePreWarm.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.hadoop.hive.metastore.cache;
+
+import org.apache.hadoop.conf.Configurable;
+import org.apache.hadoop.hive.common.TableName;
+import org.apache.hadoop.hive.metastore.api.MetaException;
+
+/**
+ * Warms up a metadata cache from the backing database before the cache starts 
serving.
+ * Implementations own the resources they need (worker RawStores, thread 
pools) and release them
+ * in {@link #close()}, which is expected to be called before the cache 
reports itself warm.
+ */
+public interface MetaCachePreWarm extends AutoCloseable, Configurable {
+
+  /**
+   * Creates the resources needed to prewarm the cache, e.g. worker stores and 
thread pools.
+   */
+  void initialize() throws MetaException;
+
+  /**
+   * Populates the cache from the backing database.
+   * @return true if all metadata was cached; false if prewarm stopped early 
(the cache memory
+   *         limit was reached or the thread was interrupted) with only part 
of the metadata cached
+   */
+  boolean preWarm() throws MetaException;
+
+  /**
+   * Moves the given tables, when they are still pending prewarm, to the front 
of the prewarm
+   * queue, so that a table a client is asking for right now becomes available 
in the cache as
+   * soon as possible.
+   */
+  void prioritizeTableForPrewarm(TableName... tableNames);
+
+  /**
+   * Releases the prewarm resources, waiting for any still running workers to 
terminate so that
+   * nothing keeps mutating the cache after prewarm reports completion.
+   */
+  @Override void close();
+}
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/SharedCache.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/SharedCache.java
index 97e4a4f375d..2aa2428317c 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/SharedCache.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/cache/SharedCache.java
@@ -116,7 +116,10 @@ public class SharedCache {
   private static final Logger LOG = 
LoggerFactory.getLogger(SharedCache.class.getName());
   private AtomicLong cacheUpdateCount = new AtomicLong(0);
   private long maxCacheSizeInBytes = -1;
-  private HashMap<Class<?>, ObjectEstimator> sizeEstimators = null;
+  // volatile + copy-on-write in getMemorySizeEstimator: tables are cached 
concurrently during a
+  // multi threaded prewarm, so this map must never be mutated in place while 
others read it
+  private volatile Map<Class<?>, ObjectEstimator> sizeEstimators = null;
+  private final Object sizeEstimatorsLock = new Object();
   private Set<String> tableToUpdateSize = new ConcurrentHashSet<>();
   private ScheduledExecutorService executor = null;
   private Map<String, Integer> tableSizeMap = null;
@@ -247,14 +250,29 @@ public Thread newThread(Runnable r) {
 
   }
 
+  /**
+   * Returns the estimator for the given class, creating it on first use. 
Estimators are added
+   * copy-on-write under {@link #sizeEstimatorsLock}: callers can run 
concurrently (prewarm caches
+   * tables in parallel), and readers always see a map that is no longer being 
mutated.
+   */
   private ObjectEstimator getMemorySizeEstimator(Class<?> clazz) {
-    if (sizeEstimators == null) {
+    Map<Class<?>, ObjectEstimator> estimators = sizeEstimators;
+    if (estimators == null) {
       return null;
     }
-    ObjectEstimator estimator = sizeEstimators.get(clazz);
+    ObjectEstimator estimator = estimators.get(clazz);
     if (estimator == null) {
-      IncrementalObjectSizeEstimator.createEstimators(clazz, sizeEstimators);
-      estimator = sizeEstimators.get(clazz);
+      synchronized (sizeEstimatorsLock) {
+        estimators = sizeEstimators;
+        estimator = estimators.get(clazz);
+        if (estimator == null) {
+          // IncrementalObjectSizeEstimator's API is HashMap typed, hence the 
casts at its boundary
+          Map<Class<?>, ObjectEstimator> updated = new HashMap<>(estimators);
+          IncrementalObjectSizeEstimator.createEstimators(clazz, 
(HashMap<Class<?>, ObjectEstimator>) updated);
+          estimator = updated.get(clazz);
+          sizeEstimators = updated;
+        }
+      }
     }
     return estimator;
   }
@@ -266,7 +284,8 @@ public int getObjectSize(Class<?> clazz, Object obj) {
 
     try {
       ObjectEstimator oe = getMemorySizeEstimator(clazz);
-      return oe.estimate(obj, sizeEstimators);
+      // Read the field again: getMemorySizeEstimator may have published a map 
with more entries
+      return oe.estimate(obj, (HashMap<Class<?>, ObjectEstimator>) 
sizeEstimators);
     } catch (Exception e) {
       LOG.error("Error while getting object size.", e);
     }
@@ -364,7 +383,7 @@ private int getTableWrapperSizeWithoutMaps() {
           Object val = field.get(this);
           ObjectEstimator oe = getMemorySizeEstimator(field.getType());
           if (oe != null) {
-            size += oe.estimate(val, sizeEstimators);
+            size += oe.estimate(val, (HashMap<Class<?>, ObjectEstimator>) 
sizeEstimators);
           }
         } catch (Exception ex) {
           LOG.error("Not able to estimate size.", ex);
diff --git 
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/cache/TestCachedStore.java
 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/cache/TestCachedStore.java
index bc051f5b7d9..56d14e76101 100644
--- 
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/cache/TestCachedStore.java
+++ 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/cache/TestCachedStore.java
@@ -188,6 +188,44 @@
     cachedStore.shutdown();
   }
 
+  /**
+   * HIVE-30053: same coverage as testPrewarm, but with the multi-threaded 
prewarm enabled.
+   * All databases, tables and partitions must be cached regardless of how the 
tables are
+   * distributed among the worker threads.
+   */
+  @Test public void testPrewarmMultiThreaded() throws Exception {
+    Configuration conf = MetastoreConf.newMetastoreConf();
+    MetastoreConf.setBoolVar(conf, MetastoreConf.ConfVars.HIVE_IN_TEST, true);
+    MetastoreConf.setVar(conf, 
MetastoreConf.ConfVars.CACHED_RAW_STORE_MAX_CACHE_MEMORY, "-1Kb");
+    MetastoreConf.setLongVar(conf, 
MetastoreConf.ConfVars.CACHED_RAW_STORE_PREWARM_THREADS, 4);
+    MetaStoreTestUtils.setConfForStandloneMode(conf);
+    CachedStore cachedStore = new CachedStore();
+    CachedStore.clearSharedCache();
+    cachedStore.setConfForTest(conf);
+    ObjectStore objectStore = (ObjectStore) cachedStore.getRawStore();
+    CachedStore.setCachePrewarmedState(false);
+    CachedStore.prewarm(objectStore);
+    List<String> allDatabases = 
cachedStore.getAllDatabases(DEFAULT_CATALOG_NAME);
+    Assert.assertEquals(2, allDatabases.size());
+    Assert.assertTrue(allDatabases.contains(db1.getName()));
+    Assert.assertTrue(allDatabases.contains(db2.getName()));
+    // All four tables must be in the shared cache after a parallel prewarm
+    SharedCache sharedCache = CachedStore.getSharedCache();
+    Assert.assertNotNull(sharedCache.getTableFromCache(DEFAULT_CATALOG_NAME, 
db1.getName(), db1Utbl1.getTableName()));
+    Assert.assertNotNull(sharedCache.getTableFromCache(DEFAULT_CATALOG_NAME, 
db1.getName(), db1Ptbl1.getTableName()));
+    Assert.assertNotNull(sharedCache.getTableFromCache(DEFAULT_CATALOG_NAME, 
db2.getName(), db2Utbl1.getTableName()));
+    Assert.assertNotNull(sharedCache.getTableFromCache(DEFAULT_CATALOG_NAME, 
db2.getName(), db2Ptbl1.getTableName()));
+    List<Partition> db1Ptbl1Partitions =
+        cachedStore.getPartitions(DEFAULT_CATALOG_NAME, db1.getName(), 
db1Ptbl1.getTableName(),
+            GetPartitionsArgs.getAllPartitions());
+    Assert.assertEquals(25, db1Ptbl1Partitions.size());
+    List<Partition> db2Ptbl1Partitions =
+        cachedStore.getPartitions(DEFAULT_CATALOG_NAME, db2.getName(), 
db2Ptbl1.getTableName(),
+            GetPartitionsArgs.getAllPartitions());
+    Assert.assertEquals(25, db2Ptbl1Partitions.size());
+    cachedStore.shutdown();
+  }
+
   @Test public void testPrewarmBlackList() {
     Configuration conf = MetastoreConf.newMetastoreConf();
     MetastoreConf.setBoolVar(conf, MetastoreConf.ConfVars.HIVE_IN_TEST, true);

Reply via email to