ArafatKhan2198 commented on code in PR #11097:
URL: https://github.com/apache/ozone/pull/11097#discussion_r4245102080


##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ContainerExportManager.java:
##########
@@ -0,0 +1,291 @@
+/*
+ * 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.hdds.scm.container.export;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BooleanSupplier;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleState;
+import org.apache.hadoop.hdds.scm.container.ContainerHealthState;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerManager;
+import org.apache.hadoop.hdds.scm.exceptions.SCMException;
+import org.apache.ratis.util.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages asynchronous container ID export jobs on the SCM leader.
+ *
+ * <p>Health filters read {@link 
org.apache.hadoop.hdds.scm.container.ContainerInfo#getHealthState()}
+ * as last written by Replication Manager; they are not recomputed during 
export and may be stale
+ * if RM has not yet evaluated a container.
+ *
+ * <p>Job status is kept in memory only. On SCM restart or leader failover, 
in-flight jobs are lost
+ * and the operator must re-submit on the new leader. {@link 
ExportFileManager} owns on-disk layout,
+ * locking, part files, and completed archives; this class tracks {@link 
ExportJob} state and
+ * schedules work.
+ */
+public class ContainerExportManager {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(ContainerExportManager.class);
+
+  private static final int DEFAULT_BATCH_SIZE = 100_000;
+  private static final int DEFAULT_PART_SIZE = 500_000;
+  private static final long SHUTDOWN_TIMEOUT_MS = 5_000;
+
+  private final Map<ExportJob.Id, ExportJob> jobMap = new 
ConcurrentHashMap<>();
+  private final AtomicReference<ExportJob> runningJob = new 
AtomicReference<>();
+  private final ExecutorService workerPool;
+  private final ContainerManager containerManager;
+  private final ExportFileManager fileManager;
+  private final BooleanSupplier isLeaderReady;
+  private final int partSize;
+  private final int batchSize;
+
+  public ContainerExportManager(String scmId, ContainerManager 
containerManager, BooleanSupplier isLeaderReady,
+      OzoneConfiguration conf) {
+    this(scmId, containerManager, isLeaderReady,
+        ExportFileManager.resolveExportDirectory(conf), DEFAULT_PART_SIZE, 
DEFAULT_BATCH_SIZE);
+  }
+
+  ContainerExportManager(String scmId, ContainerManager containerManager, 
BooleanSupplier isLeaderReady,
+      String exportDirectory, int partSize, int batchSize) {
+    if (partSize <= 0 || batchSize <= 0) {
+      throw new IllegalArgumentException("partSize and batchSize must be 
positive: partSize=" + partSize
+          + ", batchSize=" + batchSize);
+    }
+    this.containerManager = Objects.requireNonNull(containerManager, 
"containerManager == null");
+    this.isLeaderReady = Objects.requireNonNull(isLeaderReady, "isLeaderReady 
== null");
+    this.fileManager = new ExportFileManager(exportDirectory);
+    this.partSize = partSize;
+    this.batchSize = batchSize;
+    this.workerPool = newWorkerPool(scmId);
+  }
+
+  private static ExecutorService newWorkerPool(String scmId) {
+    return Executors.newSingleThreadExecutor(r -> {
+      Thread t = new Thread(r, scmId + "-ContainerExportWorker");
+      t.setDaemon(true);
+      return t;
+    });
+  }
+
+  /**
+   * Initializes the export directory. Must be called once before submitting 
jobs.
+   */
+  public void start() throws IOException {
+    fileManager.start();
+    LOG.info("ContainerExportManager started (dir={}, partSize={}, 
batchSize={})",
+        fileManager.getExportDirectory(), partSize, batchSize);
+  }
+
+  /**
+   * Submit a container ID export job on the SCM leader.
+   *
+   * @return the submitted job
+   * @throws SCMException with {@link SCMException.ResultCodes#SCM_NOT_LEADER} 
if this SCM is not the ready leader
+   * @throws IOException if another export is already running or the export 
worker is shut down
+   */
+  public ExportJob submitJob(ContainerID start, LifeCycleState lifeCycleState, 
ContainerHealthState healthState)
+      throws IOException {
+    if (!isLeaderReady.getAsBoolean()) {
+      throw new SCMException("Container ID export can only be submitted on the 
SCM leader",
+          SCMException.ResultCodes.SCM_NOT_LEADER);
+    }
+
+    final ExportScope scope = ExportScope.of(lifeCycleState, healthState);
+    final ExportJob job = new ExportJob(scope, start, batchSize, partSize);
+    if (!runningJob.compareAndSet(null, job)) {
+      throw new IOException("Another container ID export is already running");
+    }
+
+    jobMap.put(job.getId(), job);
+    try {
+      workerPool.submit(() -> executeExport(job));
+    } catch (RejectedExecutionException e) {
+      jobMap.remove(job.getId());
+      runningJob.compareAndSet(job, null);
+      throw new IOException("Container ID export worker is shut down", e);
+    }
+    LOG.info("Submitted container ID export job {} (scope={}, start={}, 
batchSize={}, partSize={})",
+        job, scope, start, batchSize, partSize);
+    return job;
+  }
+
+  public ExportJob.Status getExportStatus(ExportJob.Id jobId) {
+    ExportJob job = jobMap.get(jobId);
+    return job != null ? job.getStatus() : null;
+  }
+
+  public void shutdown() {
+    LOG.info("Shutting down ContainerExportManager");
+    workerPool.shutdownNow();
+    boolean terminated = false;
+    try {
+      terminated = workerPool.awaitTermination(SHUTDOWN_TIMEOUT_MS, 
TimeUnit.MILLISECONDS);
+    } catch (InterruptedException e) {
+      LOG.warn("Interrupted waiting for export worker shutdown");
+      Thread.currentThread().interrupt();
+    }
+    if (!terminated) {
+      // The worker may still be writing under the export directory; keep the 
lock so that
+      // a new manager cannot start and delete files in use.
+      LOG.warn("Export worker did not stop within {} ms (or shutdown was 
interrupted); "
+          + "keeping the container export directory lock", 
SHUTDOWN_TIMEOUT_MS);
+      return;
+    }

Review Comment:
   A shutdown timeout leaves the directory locked after the worker stops.
   
   Keeping the lock while the worker runs is correct. However, this branch 
returns without arranging its later release.
   
   **Reproduced:** the worker exceeded the timeout, then finished. A new 
manager in the **same JVM** still could not acquire the folder lock. Calling 
shutdown again released it.
   
   **Fix:** Arrange automatic lock release once the executor actually 
terminates.
   
   **Test improvement:** Block the mocked listing call with an uninterruptible 
lock, trigger the shutdown timeout, then release the worker. Verify that 
another manager can start afterward.



##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportFileManager.java:
##########
@@ -117,96 +129,66 @@ private void lock() throws IOException {
     }
   }
 
-  File resolveArchiveFile(ExportScope scope, String jobStartTime, ExportJob.Id 
jobId) {
-    return new File(exportDirectory, String.format("container-ids_%s_%s%s%s%s",
-        scope.getValue(), jobStartTime, EXPORT_ARCHIVE_JOB_INFIX, 
jobId.getValue(), EXPORT_ARCHIVE_SUFFIX));
+  void unlock() throws IOException {
+    if (exportDirectoryLock == null) {
+      return;
+    }
+    exportDirectoryLock.release();
+    exportDirectoryLock.channel().close();
+    exportDirectoryLock = null;
   }
 
-  File resolveArchiveTempFile(ExportScope scope, String jobStartTime, 
ExportJob.Id jobId) {
-    return AtomicFileOutputStream.getTemporaryFile(resolveArchiveFile(scope, 
jobStartTime, jobId));
+  static String archiveFileName(ExportScope scope, String jobStartTime, 
ExportJob.Id jobId) {
+    return String.format("container-ids_%s_%s%s%s%s",
+        scope, jobStartTime, EXPORT_ARCHIVE_JOB_INFIX, jobId, 
EXPORT_ARCHIVE_SUFFIX);
   }
 
   void createJobDirectory(ExportJob.Id jobId) throws IOException {
     Files.createDirectories(jobDirectory(jobId));
   }
 
-  BufferedWriter newPartWriter(ExportJob.Id jobId, String partFileName) throws 
IOException {
-    return Files.newBufferedWriter(jobDirectory(jobId).resolve(partFileName), 
StandardCharsets.UTF_8);
+  OutputStream newPartOutputStream(ExportJob.Id jobId, String partFileName) 
throws IOException {
+    return Files.newOutputStream(jobDirectory(jobId).resolve(partFileName));
   }
 
-  void writeArchive(ExportJob.Id jobId, String archivePath) throws IOException 
{
-    Path jobDir = jobDirectory(jobId);
+  void writeArchive(ExportJob job, BooleanSupplier isLeaderReady) throws 
IOException {
+    Path jobDir = jobDirectory(job.getId());
     File[] parts = jobDir.toFile().listFiles((dir, name) -> 
name.endsWith(".txt"));
     if (parts == null || parts.length == 0) {
-      throw new IOException("No part files found for export job " + jobId);
+      throw new IOException("No part files found for " + job);
     }
     Arrays.sort(parts, Comparator.comparing(File::getName));
-    File archiveFile = new File(archivePath);
+    File archiveFile = new File(exportDirectory, job.getFileName());
     try (AtomicFileOutputStream atomicOut = new 
AtomicFileOutputStream(archiveFile);
          GZIPOutputStream gzipOut = new GZIPOutputStream(atomicOut);
          ArchiveOutputStream<TarArchiveEntry> tarOut = Archiver.tar(gzipOut)) {
       for (File part : parts) {
+        if (!isLeaderReady.getAsBoolean()) {
+          // close() would publish the partial archive; abort() discards the 
temp file instead.
+          atomicOut.abort();
+          throw new IOException("SCM lost leadership while archiving " + job);
+        }
         Archiver.includeFile(part, part.getName(), tarOut);
       }
     }

Review Comment:
   The code checks leadership **before** copying each file. If leadership 
changes while the final file is being copied, there is no further check. 
Closing the streams publishes the archive, and the job reports success.
   
   **Fix:** Add a final leadership/cancellation check after the loop, before 
closing publishes the archive. Abort the temporary output if the check fails.
   
   **Test improvement:** The new test at [line 
353](https://github.com/apache/ozone/blob/1331b19f96951d712ca6a9dbef711f3029383d5d/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/export/TestContainerExportManager.java#L353)
 supplies `() -> false`, so it tests rejection before copying begins. Add a 
one-file test where the first check returns true and subsequent checks return 
false; expect failure and no archive.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to