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]
