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

abstractdog 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 03a76ac7037 HIVE-28051: LLAP: cleanup local folders on startup and 
periodically (#5054) (Laszlo Bodor reviewed by Butao Zhang)
03a76ac7037 is described below

commit 03a76ac70370fb94a78b00496ec511e671c652f2
Author: Bodor Laszlo <[email protected]>
AuthorDate: Fri Mar 8 08:55:34 2024 +0100

    HIVE-28051: LLAP: cleanup local folders on startup and periodically (#5054) 
(Laszlo Bodor reviewed by Butao Zhang)
---
 .../java/org/apache/hadoop/hive/conf/HiveConf.java |  11 ++
 .../hadoop/hive/llap/daemon/impl/LlapDaemon.java   |   1 +
 .../hive/llap/daemon/impl/LocalDirCleaner.java     | 113 +++++++++++++++++++++
 .../hive/llap/daemon/impl/TestLlapDaemon.java      |  83 ++++++++++++---
 4 files changed, 193 insertions(+), 15 deletions(-)

diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java 
b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
index a1df4493faa..bf604e48cc9 100644
--- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
+++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
@@ -5508,6 +5508,17 @@ public class HiveConf extends Configuration {
     LLAP_TASK_TIME_SUMMARY(
         "hive.llap.task.time.print.summary", false,
         "Display queue and runtime of tasks by host for every query executed 
by the shell."),
+
+    LLAP_LOCAL_DIR_CLEANER_CLEANUP_INTERVAL(
+        "hive.llap.local.dir.cleaner.cleanup.interval", "2h", new 
TimeValidator(TimeUnit.HOURS),
+      "Interval by which the LocalDirCleaner service in LLAP daemon checks for 
stale/old files." +
+      "Under normal circumstances, local files are cleaned up properly, so 
it's not recommended to" +
+      "set this more frequent than a couple of hours. Default is 2 hours."),
+    
LLAP_LOCAL_DIR_CLEANER_FILE_MODIFY_TIME_THRESHOLD("hive.llap.local.dir.cleaner.file.modify.time.threshold",
 "24h",
+        new TimeValidator(TimeUnit.HOURS),
+      "Threshold time for LocalDirCleaner: if a regular file's modify time is 
older than this value, the file gets deleted." +
+      "Defaults to 86400s (1 day), which is a reasonable period for a local 
file to be considered as a stale one."),
+
     HIVE_TRIGGER_VALIDATION_INTERVAL("hive.trigger.validation.interval", 
"500ms",
       new TimeValidator(TimeUnit.MILLISECONDS),
       "Interval for validating triggers during execution of a query. Triggers 
defined in resource plan will get\n" +
diff --git 
a/llap-server/src/java/org/apache/hadoop/hive/llap/daemon/impl/LlapDaemon.java 
b/llap-server/src/java/org/apache/hadoop/hive/llap/daemon/impl/LlapDaemon.java
index 4f04793d33e..fe856bac406 100644
--- 
a/llap-server/src/java/org/apache/hadoop/hive/llap/daemon/impl/LlapDaemon.java
+++ 
b/llap-server/src/java/org/apache/hadoop/hive/llap/daemon/impl/LlapDaemon.java
@@ -383,6 +383,7 @@ public class LlapDaemon extends CompositeService implements 
ContainerRunner, Lla
     // AMReporter after the server so that it gets the correct address. It 
knows how to deal with
     // requests before it is started.
     addIfService(amReporter);
+    addIfService(new LocalDirCleaner(localDirs, daemonConf));
   }
 
   private static long determineXmxHeadroom(
diff --git 
a/llap-server/src/java/org/apache/hadoop/hive/llap/daemon/impl/LocalDirCleaner.java
 
b/llap-server/src/java/org/apache/hadoop/hive/llap/daemon/impl/LocalDirCleaner.java
new file mode 100644
index 00000000000..e6ae192f260
--- /dev/null
+++ 
b/llap-server/src/java/org/apache/hadoop/hive/llap/daemon/impl/LocalDirCleaner.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hive.llap.daemon.impl;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.service.AbstractService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * LocalDirCleaner is an LLAP daemon service to clean up old local files. 
Under normal circumstances,
+ * intermediate/local files are cleaned up (typically at end of the DAG), but 
daemons crash sometimes,
+ * and the attached local disk might end up being the same when a new daemon 
starts (this applies to
+ * on-prem as well as cloud scenarios).
+ */
+public class LocalDirCleaner extends AbstractService {
+  private static final Logger LOG = 
LoggerFactory.getLogger(LocalDirCleaner.class);
+
+  private List<String> localDirs;
+
+  private long cleanupIntervalSec;
+  private long fileModifyTimeThresholdSec;
+
+  ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+
+  public LocalDirCleaner(String[] localDirs, Configuration conf) {
+    super("LocalDirCleaner");
+    this.localDirs = Arrays.asList(localDirs);
+    this.cleanupIntervalSec = getInterval(conf);
+    this.fileModifyTimeThresholdSec = getFileModifyTimeThreshold(conf);
+    LOG.info("Initialized local dir cleaner: interval: {}s, threshold: {}s", 
cleanupIntervalSec,
+        fileModifyTimeThresholdSec);
+  }
+
+  @Override
+  public void serviceStart() throws IOException {
+    scheduler.scheduleAtFixedRate(this::cleanup, 0, cleanupIntervalSec, 
TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void serviceStop() throws IOException {
+    // we can shutdown this service now and ignore leftovers, because under 
normal circumstances,
+    // files from the local dirs are cleaned up (so LocalDirCleaner is a best 
effort utility)
+    scheduler.shutdownNow();
+  }
+
+  private long getFileModifyTimeThreshold(Configuration conf) {
+    return HiveConf.getTimeVar(conf, 
HiveConf.ConfVars.LLAP_LOCAL_DIR_CLEANER_FILE_MODIFY_TIME_THRESHOLD,
+        TimeUnit.SECONDS);
+  }
+
+  private long getInterval(Configuration conf) {
+    return HiveConf.getTimeVar(conf, 
HiveConf.ConfVars.LLAP_LOCAL_DIR_CLEANER_CLEANUP_INTERVAL, TimeUnit.SECONDS);
+  }
+
+  private void cleanup() {
+    Instant deleteBefore = Instant.now().minus(fileModifyTimeThresholdSec, 
ChronoUnit.SECONDS);
+
+    localDirs.forEach(localDir -> cleanupPath(deleteBefore, 
Paths.get(localDir)));
+  }
+
+  private void cleanupPath(Instant deleteBefore, Path pathLocalDir) {
+    LOG.info("Cleaning up files older than {} from {}", deleteBefore, 
pathLocalDir);
+
+    try (Stream<Path> files = Files.walk(pathLocalDir)) {
+      files.filter(f -> {
+        try {
+          FileTime modified = Files.getLastModifiedTime(f);
+          LOG.debug("Checking: {}, modified: {}", f, modified);
+          return Files.isRegularFile(f) && 
modified.toInstant().isBefore(deleteBefore);
+        } catch (IOException ex) {
+          LOG.warn("IOException caught while checking file for deletion", ex);
+          return false;
+        }
+      }).forEach(f -> {
+        try {
+          LOG.info("Delete old local file: {}", f);
+          Files.delete(f);
+        } catch (IOException ex) {
+          LOG.warn("Failed to delete file", ex);
+        }
+      });
+    } catch (IOException e) {
+      LOG.warn("IOException caught while walking over local files", e);
+    }
+  }
+}
diff --git 
a/llap-server/src/test/org/apache/hadoop/hive/llap/daemon/impl/TestLlapDaemon.java
 
b/llap-server/src/test/org/apache/hadoop/hive/llap/daemon/impl/TestLlapDaemon.java
index f1e4265a5a3..95c71df8f2a 100644
--- 
a/llap-server/src/test/org/apache/hadoop/hive/llap/daemon/impl/TestLlapDaemon.java
+++ 
b/llap-server/src/test/org/apache/hadoop/hive/llap/daemon/impl/TestLlapDaemon.java
@@ -34,9 +34,14 @@ import org.mockito.Mock;
 import org.mockito.internal.util.reflection.Fields;
 import org.mockito.internal.util.reflection.InstanceField;
 
+import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 
@@ -55,6 +60,11 @@ public class TestLlapDaemon {
       MetricsUtils.METRICS_PROCESS_NAME
   };
 
+  public static final String TEST_LOCAL_DIR = new 
File(System.getProperty("java.io.tmpdir") +
+      File.separator + TestLlapDaemon.class.getCanonicalName()
+      + "-" + System.currentTimeMillis()
+  ).getPath().replaceAll("\\\\", "/");
+
   private Configuration hiveConf = new HiveConf();
 
   @Mock
@@ -64,17 +74,19 @@ public class TestLlapDaemon {
   private ArgumentCaptor<Iterable<Map.Entry<String, String>>> captor;
 
   private LlapDaemon daemon;
+  private String[] localDirs = new String[] {TEST_LOCAL_DIR};
+  private int defaultWebPort = 
HiveConf.ConfVars.LLAP_DAEMON_WEB_PORT.defaultIntVal;
 
   @Before
   public void setUp() {
     initMocks(this);
-    HiveConf.setVar(hiveConf, HiveConf.ConfVars.LLAP_DAEMON_SERVICE_HOSTS, 
"@llap");
-    HiveConf.setVar(hiveConf, HiveConf.ConfVars.HIVE_ZOOKEEPER_QUORUM, 
"localhost");
-
-    String[] localDirs = new String[1];
+    setupConf(hiveConf);
     LlapDaemonInfo.initialize("testDaemon", hiveConf);
-    daemon = new LlapDaemon(hiveConf, 1, LlapDaemon.getTotalHeapSize(), false, 
false,
-            -1, localDirs, 0, false, 0,0, 0, -1, "TestLlapDaemon");
+  }
+
+  private void setupConf(Configuration conf) {
+    HiveConf.setVar(conf, HiveConf.ConfVars.LLAP_DAEMON_SERVICE_HOSTS, 
"localhost");
+    HiveConf.setBoolVar(conf, HiveConf.ConfVars.HIVE_IN_TEST, true);
   }
 
   @After
@@ -83,19 +95,56 @@ public class TestLlapDaemon {
     for (String mSource : METRICS_SOURCES) {
       ms.unregisterSource(mSource);
     }
-    daemon.shutdown();
+    if (daemon != null) {
+      daemon.shutdown();
+    }
   }
 
   @Test(expected = IllegalArgumentException.class)
   public void testEnforceProperNumberOfIOThreads() throws IOException {
-    Configuration thisHiveConf = new HiveConf();
-    HiveConf.setVar(thisHiveConf, HiveConf.ConfVars.LLAP_DAEMON_SERVICE_HOSTS, 
"@llap");
-    HiveConf.setIntVar(thisHiveConf, 
HiveConf.ConfVars.LLAP_DAEMON_NUM_EXECUTORS, 4);
-    HiveConf.setIntVar(thisHiveConf, 
HiveConf.ConfVars.LLAP_IO_THREADPOOL_SIZE, 3);
-
-    LlapDaemon thisDaemon = new LlapDaemon(thisHiveConf, 1, 
LlapDaemon.getTotalHeapSize(), false, false,
-            -1, new String[1], 0, false, 0,0, 0, -1, "TestLlapDaemon");
-    thisDaemon.close();
+    HiveConf.setIntVar(hiveConf, HiveConf.ConfVars.LLAP_IO_THREADPOOL_SIZE, 3);
+
+    daemon = new LlapDaemon(hiveConf, 4, LlapDaemon.getTotalHeapSize(), true, 
false,
+            -1, new String[1], 0, false, 0,0, 0, defaultWebPort, 
"TestLlapDaemon");
+  }
+
+   @Test
+  public void testLocalDirCleaner() throws IOException, InterruptedException {
+    HiveConf.setTimeVar(hiveConf, 
HiveConf.ConfVars.LLAP_LOCAL_DIR_CLEANER_CLEANUP_INTERVAL, 2, TimeUnit.SECONDS);
+    HiveConf.setTimeVar(hiveConf, 
HiveConf.ConfVars.LLAP_LOCAL_DIR_CLEANER_FILE_MODIFY_TIME_THRESHOLD, 1,
+        TimeUnit.SECONDS);
+
+    createFile(localDirs[0] + "/hive/appcache/file1");
+    createFile(localDirs[0] + "/hive/appcache/file2");
+    createFile(localDirs[0] + "/file3");
+
+    daemon = new LlapDaemon(hiveConf, 1, LlapDaemon.getTotalHeapSize(), false, 
false,
+        -1, localDirs, 0, false, 0,0, 0, defaultWebPort, "TestLlapDaemon");
+    daemon.init(hiveConf);
+
+    assertFileExists(localDirs[0] + "/hive/appcache/file1", true);
+    assertFileExists(localDirs[0] + "/hive/appcache/file2", true);
+    assertFileExists(localDirs[0] + "/file3", true);
+
+    daemon.start();
+    Thread.sleep(5000);
+
+    assertFileExists(localDirs[0] + "/hive/appcache/file1", false);
+    assertFileExists(localDirs[0] + "/hive/appcache/file2", false);
+    assertFileExists(localDirs[0] + "/file3", false);
+
+    // folder is preserved
+    assertFileExists(localDirs[0] + "/hive/appcache", true);
+  }
+
+  private void assertFileExists(String strPath, boolean exists) {
+    assertEquals(strPath + " " + (exists ? "doesn't exist" : "exists"), 
exists, Files.exists(Paths.get(strPath)));
+  }
+
+  private void createFile(String strPath) throws IOException {
+    Path path = Paths.get(strPath);
+    Files.createDirectories(path.getParent());
+    Files.createFile(path);
   }
 
   @Test
@@ -103,6 +152,10 @@ public class TestLlapDaemon {
     // Given
     int enabledExecutors = 0;
     int enabledQueue = 2;
+
+    daemon = new LlapDaemon(hiveConf, 1, LlapDaemon.getTotalHeapSize(), false, 
false,
+        -1, new String[1], 0, false, 0,0, 0, defaultWebPort, "TestLlapDaemon");
+
     trySetMock(daemon, LlapRegistryService.class, mockRegistry);
 
     // When

Reply via email to