This is an automated email from the ASF dual-hosted git repository.
jojochuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 4a819cc4eee HDDS-15171. Add available space check on follower during
bootstrap. (#10185)
4a819cc4eee is described below
commit 4a819cc4eee145cac67965f19f81a4186fc430bc
Author: Sadanand Shenoy <[email protected]>
AuthorDate: Thu Jul 16 03:30:52 2026 +0530
HDDS-15171. Add available space check on follower during bootstrap. (#10185)
---
.../java/org/apache/hadoop/ozone/OzoneConsts.java | 8 +
.../common/src/main/resources/ozone-default.xml | 23 ++
.../hadoop/hdds/utils/DBCheckpointServlet.java | 10 +-
.../org/apache/hadoop/ozone/om/OMConfigKeys.java | 12 +
.../hadoop/ozone/om/TestOMDbCheckpointServlet.java | 35 ++-
.../TestOMDbCheckpointServletInodeBasedXfer.java | 58 +++-
.../ozone/om/snapshot/TestOMDBCheckpointUtils.java | 17 ++
.../hadoop/ozone/om/OMDBCheckpointServlet.java | 65 +++--
.../om/OMDBCheckpointServletInodeBasedXfer.java | 26 +-
.../org/apache/hadoop/ozone/om/OzoneManager.java | 16 +-
.../om/ratis_snapshot/OmRatisSnapshotProvider.java | 300 ++++++++++++++++++---
.../ozone/om/snapshot/OMDBCheckpointUtils.java | 74 +++--
.../TestOmRatisSnapshotProvider.java | 59 ++++
13 files changed, 607 insertions(+), 96 deletions(-)
diff --git
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
index bce241f0ded..3b63ecf1974 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
@@ -130,6 +130,14 @@ public final class OzoneConsts {
public static final String OZONE_DB_CHECKPOINT_REQUEST_TO_EXCLUDE_SST =
"toExcludeSST";
+ /**
+ * Response header set by OM leader on full checkpoint responses with the
+ * estimated total uncompressed SST bytes (see OMDBCheckpointUtils); used by
+ * followers to pre-check disk space before streaming the tarball body.
+ */
+ public static final String OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER =
+ "X-Ozone-Om-Checkpoint-Estimated-Sst-Bytes";
+
public static final String RANGER_OZONE_SERVICE_VERSION_KEY =
"#RANGEROZONESERVICEVERSION";
diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml
b/hadoop-hdds/common/src/main/resources/ozone-default.xml
index dd33a02b02a..ad8ea45b3d7 100644
--- a/hadoop-hdds/common/src/main/resources/ozone-default.xml
+++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml
@@ -2405,6 +2405,29 @@
request OM snapshot from OM Leader.
</description>
</property>
+ <property>
+ <name>ozone.om.bootstrap.min.space</name>
+ <value>5GB</value>
+ <tag>OZONE, OM, HA, MANAGEMENT</tag>
+ <description>
+ Minimum free space required on the volume that holds
ozone.om.ratis.snapshot.dir
+ before an OM follower downloads a ratis/bootstrap checkpoint from the
leader,
+ when the leader does not supply the
X-Ozone-Om-Checkpoint-Estimated-Sst-Bytes header
+ (incremental checkpoint or older OM version).
+ Use storage size syntax (e.g. 10GB). Set to 0 to disable this fallback
check.
+ </description>
+ </property>
+
+ <property>
+ <name>ozone.om.bootstrap.checkpoint.estimated.space.headroom.ratio</name>
+ <value>2.0</value>
+ <tag>OZONE, OM, HA, MANAGEMENT</tag>
+ <description>
+ Multiplier applied to the leader-reported estimated uncompressed SST
byte total
+ (X-Ozone-Om-Checkpoint-Estimated-Sst-Bytes) to approximate space needed
for the
+ checkpoint tar and unpack on the follower before streaming the response
body.
+ </description>
+ </property>
<property>
<name>ozone.om.fs.snapshot.max.limit</name>
diff --git
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/DBCheckpointServlet.java
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/DBCheckpointServlet.java
index a133e5188a2..6941ea3d537 100644
---
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/DBCheckpointServlet.java
+++
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/DBCheckpointServlet.java
@@ -25,7 +25,6 @@
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.IOException;
-import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -238,8 +237,7 @@ public void
processMetadataSnapshotRequest(HttpServletRequest request, HttpServl
file + ".tar\"");
Instant start = Instant.now();
- writeDbDataToStream(checkpoint, request, response.getOutputStream(),
- receivedSstFiles, tmpdir);
+ writeDbDataToStream(checkpoint, request, response, receivedSstFiles,
tmpdir);
Instant end = Instant.now();
long duration = Duration.between(start, end).toMillis();
@@ -368,18 +366,18 @@ public void doPost(HttpServletRequest request,
HttpServletResponse response) {
* @param checkpoint The checkpoint to be written.
* @param ignoredRequest The httpRequest which generated this checkpoint.
* (Parameter is ignored in this class but used in child classes).
- * @param destination The stream to write to.
+ * @param response The HTTP response; the body is written to {@link
HttpServletResponse#getOutputStream()}.
* @param toExcludeList the files to be excluded
*
*/
public void writeDbDataToStream(DBCheckpoint checkpoint,
HttpServletRequest ignoredRequest,
- OutputStream destination,
+ HttpServletResponse response,
Set<String> toExcludeList,
Path tmpdir)
throws IOException, InterruptedException {
Objects.requireNonNull(toExcludeList);
- writeDBCheckpointToStream(checkpoint, destination, toExcludeList);
+ writeDBCheckpointToStream(checkpoint, response.getOutputStream(),
toExcludeList);
}
public DBStore getDbStore() {
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java
index b1209b6a213..02b270070ed 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java
@@ -291,6 +291,18 @@ public final class OMConfigKeys {
OZONE_OM_SNAPSHOT_PROVIDER_REQUEST_TIMEOUT_DEFAULT =
TimeDuration.valueOf(300000, TimeUnit.MILLISECONDS);
+ public static final String OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY =
+ "ozone.om.bootstrap.min.space";
+ public static final String OZONE_OM_BOOTSTRAP_MIN_SPACE_DEFAULT = "5GB";
+
+ /**
+ * Multiplier applied to the leader-reported estimated SST bytes when
deciding
+ * minimum free space before downloading a checkpoint (tar + unpack
headroom).
+ */
+ public static final String OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_KEY =
+ "ozone.om.bootstrap.checkpoint.estimated.space.headroom.ratio";
+ public static final double
OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_DEFAULT = 2.0D;
+
public static final String OZONE_OM_FS_SNAPSHOT_MAX_LIMIT =
"ozone.om.fs.snapshot.max.limit";
public static final int OZONE_OM_FS_SNAPSHOT_MAX_LIMIT_DEFAULT = 10000;
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOMDbCheckpointServlet.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOMDbCheckpointServlet.java
index 8acc63de9aa..d9fe8a7543d 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOMDbCheckpointServlet.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOMDbCheckpointServlet.java
@@ -47,6 +47,7 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
@@ -54,7 +55,6 @@
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -532,8 +532,9 @@ private void testWriteDbDataWithoutOmSnapshot()
// Get the tarball.
Path tmpdir = folder.resolve("bootstrapData");
try (OutputStream fileOutputStream =
Files.newOutputStream(tempFile.toPath())) {
+ HttpServletResponse mockResponse =
mockHttpServletResponse(fileOutputStream);
omDbCheckpointServletMock.writeDbDataToStream(dbCheckpoint, requestMock,
- fileOutputStream, new HashSet<>(), tmpdir);
+ mockResponse, new HashSet<>(), tmpdir);
}
// Untar the file into a temp folder to be examined.
@@ -577,8 +578,9 @@ private void testWriteDbDataWithToExcludeFileList()
// Get the tarball.
Path tmpdir = folder.resolve("bootstrapData");
try (OutputStream fileOutputStream =
Files.newOutputStream(tempFile.toPath())) {
+ HttpServletResponse mockResponse =
mockHttpServletResponse(fileOutputStream);
omDbCheckpointServletMock.writeDbDataToStream(dbCheckpoint, requestMock,
- fileOutputStream, toExcludeList, tmpdir);
+ mockResponse, toExcludeList, tmpdir);
}
// Untar the file into a temp folder to be examined.
@@ -598,6 +600,33 @@ private void testWriteDbDataWithToExcludeFileList()
assertThat(initialCheckpointSet).contains(dummyFile.getName());
}
+ private static HttpServletResponse mockHttpServletResponse(OutputStream out)
+ throws IOException {
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ ServletOutputStream sos = new ServletOutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ out.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ out.write(b, off, len);
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ }
+ };
+ when(response.getOutputStream()).thenReturn(sos);
+ return response;
+ }
+
/**
* Calls endpoint in regards to parametrized HTTP method.
*/
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOMDbCheckpointServletInodeBasedXfer.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOMDbCheckpointServletInodeBasedXfer.java
index 791b8e91a86..518c762976a 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOMDbCheckpointServletInodeBasedXfer.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOMDbCheckpointServletInodeBasedXfer.java
@@ -17,6 +17,7 @@
package org.apache.hadoop.ozone.om;
+import static java.net.HttpURLConnection.HTTP_OK;
import static
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ACL_ENABLED;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ADMINISTRATORS;
@@ -28,6 +29,9 @@
import static org.apache.hadoop.ozone.OzoneConsts.OM_SNAPSHOT_CHECKPOINT_DIR;
import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_INCLUDE_SNAPSHOT_DATA;
import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_REQUEST_FLUSH;
+import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_KEY;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RATIS_SNAPSHOT_MAX_TOTAL_SST_SIZE_KEY;
import static org.apache.ozone.test.OzoneTestBase.uniqueObjectName;
import static org.assertj.core.api.Assertions.assertThat;
@@ -38,10 +42,10 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
@@ -55,9 +59,12 @@
import static org.mockito.Mockito.when;
import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
@@ -97,6 +104,7 @@
import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
import org.apache.hadoop.hdds.utils.db.DBStore;
import org.apache.hadoop.hdds.utils.db.InodeMetadataRocksDBCheckpoint;
+import org.apache.hadoop.hdfs.web.URLConnectionFactory;
import org.apache.hadoop.ozone.MiniOzoneCluster;
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.TestDataUtil;
@@ -105,12 +113,14 @@
import org.apache.hadoop.ozone.client.OzoneSnapshot;
import org.apache.hadoop.ozone.lock.BootstrapStateHandler;
import org.apache.hadoop.ozone.om.codec.OMDBDefinition;
+import org.apache.hadoop.ozone.om.helpers.OMNodeDetails;
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
import org.apache.hadoop.ozone.om.lock.DAGLeveledResource;
import org.apache.hadoop.ozone.om.lock.IOzoneManagerLock;
import org.apache.hadoop.ozone.om.lock.OMLockDetails;
+import org.apache.hadoop.ozone.om.ratis_snapshot.OmRatisSnapshotProvider;
import org.apache.hadoop.ozone.om.snapshot.OmSnapshotUtils;
import org.apache.hadoop.ozone.om.snapshot.SnapshotCache;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
@@ -1043,6 +1053,52 @@ public static Map<String, List<String>>
readFileToMap(String filePath) throws IO
return dataMap;
}
+ /**
+ * Follower bootstrap must abort before streaming when the leader's SST
estimate header
+ * implies more free space than is available (v2 inode-based checkpoint URL).
+ */
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void
testBootstrapSnapshotDownloadAbortsWhenDiskSpaceBelowLeaderSstEstimate(boolean
useInodeBasedTransfer)
+ throws Exception {
+ Path snapshotDir = folder.resolve("ratis-snap-space-v2-" +
UUID.randomUUID());
+ Files.createDirectories(snapshotDir);
+ Path downloadTarget = folder.resolve("checkpoint-target-" +
UUID.randomUUID() + ".tar");
+
+ long usable = Files.getFileStore(snapshotDir).getUsableSpace();
+ long estimatedSstBytes = Math.addExact(Math.min(usable, Long.MAX_VALUE /
4), 1_000_000);
+
+ OzoneConfiguration diskCheckConf = new OzoneConfiguration();
+ diskCheckConf.set(OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY, "0B");
+ diskCheckConf.setBoolean(OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_KEY,
useInodeBasedTransfer);
+
+ Map<String, OMNodeDetails> peers = new HashMap<>();
+ OMNodeDetails leaderDetails = mock(OMNodeDetails.class);
+ String leaderId = "leader1";
+ peers.put(leaderId, leaderDetails);
+ URL checkpointUrl = mock(URL.class);
+ when(leaderDetails.getOMDBCheckpointEndpointUrl(anyBoolean(),
anyBoolean(), eq(true)))
+ .thenReturn(checkpointUrl);
+
+ HttpURLConnection connection = mock(HttpURLConnection.class);
+ URLConnectionFactory connectionFactory = mock(URLConnectionFactory.class);
+ when(connectionFactory.openConnection(any(URL.class),
anyBoolean())).thenReturn(connection);
+
+ ByteArrayOutputStream uploadBody = new ByteArrayOutputStream();
+ when(connection.getOutputStream()).thenReturn(uploadBody);
+ when(connection.getResponseCode()).thenReturn(HTTP_OK);
+
when(connection.getHeaderField(OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER))
+ .thenReturn(Long.toString(estimatedSstBytes));
+
+ try (OmRatisSnapshotProvider provider = new
OmRatisSnapshotProvider(diskCheckConf,
+ snapshotDir.toFile(), peers, connectionFactory)) {
+ IOException ex = assertThrows(IOException.class,
+ () -> provider.downloadSnapshot(leaderId, downloadTarget.toFile()));
+
assertTrue(ex.getMessage().contains(OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER),
+ ex::getMessage);
+ }
+ }
+
private void populateInodesOfFilesInDirectory(DBStore dbStore, Path
dbLocation,
Set<String> inodesFromOmDbCheckpoint, Map<String, List<String>>
hardlinkMap) throws IOException {
try (Stream<Path> filesInOmDb = Files.list(dbLocation)) {
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOMDBCheckpointUtils.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOMDBCheckpointUtils.java
index 249e9285d7d..cda781d0570 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOMDBCheckpointUtils.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOMDBCheckpointUtils.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.ozone.om.snapshot;
import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_INCLUDE_SNAPSHOT_DATA;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
@@ -89,6 +90,22 @@ private static String getExpectedLogLine(String
expectedDataSize, int expectedSS
return String.format("%s%d, snapshots: %d", baseMessage, expectedSSTFiles,
expectedSnapshots);
}
+ @Test
+ public void testEstimateCheckpointTarballSstDetails() throws IOException {
+ writeSstFilesToDirectory(dbDir, 10, 10 * 1024);
+ Set<Path> snapshotDirs = new HashSet<>();
+ OMDBCheckpointUtils.SstSizeEstimate withoutSnapshots =
+ OMDBCheckpointUtils.estimateCheckpointTarballSstDetails(dbDir,
snapshotDirs);
+ assertEquals(10 * 10 * 1024L, withoutSnapshots.getTotalBytes());
+ assertEquals(10L, withoutSnapshots.getFileCount());
+
+ snapshotDirs.add(dbDir);
+ OMDBCheckpointUtils.SstSizeEstimate withSnapshots =
+ OMDBCheckpointUtils.estimateCheckpointTarballSstDetails(dbDir,
snapshotDirs);
+ assertEquals(20 * 10 * 1024L, withSnapshots.getTotalBytes());
+ assertEquals(20L, withSnapshots.getFileCount());
+ }
+
@Test
public void testIncludeSnapshotData() {
HttpServletRequest httpServletRequest = mock(HttpServletRequest.class);
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMDBCheckpointServlet.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMDBCheckpointServlet.java
index b22bf5a6bee..fbb1b56ff16 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMDBCheckpointServlet.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMDBCheckpointServlet.java
@@ -24,6 +24,7 @@
import static org.apache.hadoop.ozone.OzoneConsts.OM_CHECKPOINT_DIR;
import static org.apache.hadoop.ozone.OzoneConsts.OM_SNAPSHOT_CHECKPOINT_DIR;
import static org.apache.hadoop.ozone.OzoneConsts.OM_SNAPSHOT_DIR;
+import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER;
import static org.apache.hadoop.ozone.OzoneConsts.ROCKSDB_SST_SUFFIX;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RATIS_SNAPSHOT_MAX_TOTAL_SST_SIZE_DEFAULT;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RATIS_SNAPSHOT_MAX_TOTAL_SST_SIZE_KEY;
@@ -37,7 +38,6 @@
import jakarta.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
-import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -157,7 +157,7 @@ public void
processMetadataSnapshotRequest(HttpServletRequest request, HttpServl
@Override
public void writeDbDataToStream(DBCheckpoint checkpoint,
HttpServletRequest request,
- OutputStream destination,
+ HttpServletResponse response,
Set<String> toExcludeList,
Path tmpdir)
throws IOException, InterruptedException {
@@ -175,18 +175,35 @@ public void writeDbDataToStream(DBCheckpoint checkpoint,
// Map of link to path.
Map<Path, Path> hardLinkFiles = new HashMap<>();
- try (ArchiveOutputStream<TarArchiveEntry> archiveOutputStream =
tar(destination)) {
- RocksDBCheckpointDiffer differ =
- getDbStore().getRocksDBCheckpointDiffer();
- DirectoryData sstBackupDir = new DirectoryData(tmpdir,
- differ.getSSTBackupDir());
- DirectoryData compactionLogDir = new DirectoryData(tmpdir,
- differ.getCompactionLogDir());
+ RocksDBCheckpointDiffer differ =
+ getDbStore().getRocksDBCheckpointDiffer();
+ DirectoryData sstBackupDir = new DirectoryData(tmpdir,
+ differ.getSSTBackupDir());
+ DirectoryData compactionLogDir = new DirectoryData(tmpdir,
+ differ.getCompactionLogDir());
- // Files to be excluded from tarball
- Map<String, Map<Path, Path>> sstFilesToExclude =
normalizeExcludeList(toExcludeList,
- checkpoint.getCheckpointLocation(), sstBackupDir);
+ // Files to be excluded from tarball
+ Map<String, Map<Path, Path>> sstFilesToExclude =
normalizeExcludeList(toExcludeList,
+ checkpoint.getCheckpointLocation(), sstBackupDir);
+ if (sstFilesToExclude.isEmpty()) {
+ try {
+ Set<Path> snapshotPaths =
+ snapshotPathsForCheckpointEstimate(checkpoint,
includeSnapshotData(request));
+ OMDBCheckpointUtils.SstSizeEstimate estimate =
+ OMDBCheckpointUtils.estimateCheckpointTarballSstDetails(
+ checkpoint.getCheckpointLocation(), snapshotPaths);
+ response.setHeader(OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER,
+ Long.toString(estimate.getTotalBytes()));
+ OMDBCheckpointUtils.logEstimatedTarballSize(estimate,
snapshotPaths.size());
+ } catch (IOException e) {
+ LOG.warn("Could not estimate checkpoint tarball SST size for response
header: {}",
+ e.getMessage());
+ }
+ }
+
+ try (ArchiveOutputStream<TarArchiveEntry> archiveOutputStream =
+ tar(response.getOutputStream())) {
boolean completed = getFilesForArchive(checkpoint, copyFiles,
hardLinkFiles, sstFilesToExclude, includeSnapshotData(request),
sstBackupDir, compactionLogDir);
@@ -200,6 +217,15 @@ hardLinkFiles, sstFilesToExclude,
includeSnapshotData(request),
}
}
+ private Set<Path> snapshotPathsForCheckpointEstimate(DBCheckpoint checkpoint,
+ boolean includeSnapshotData) throws IOException {
+ Set<Path> snapshotPaths = new HashSet<>();
+ if (includeSnapshotData) {
+ snapshotPaths = getSnapshotDirs(checkpoint, false);
+ }
+ return snapshotPaths;
+ }
+
/**
* Format the list of excluded sst files from follower to match data
* on leader.
@@ -310,11 +336,6 @@ private boolean getFilesForArchive(DBCheckpoint checkpoint,
AtomicLong copySize = new AtomicLong(0L);
- // Log estimated total data transferred on first request.
- if (sstFilesToExclude.isEmpty()) {
- logEstimatedTarballSize(checkpoint, includeSnapshotData);
- }
-
// Get the active fs files.
Path dir = checkpoint.getCheckpointLocation();
if (!processDir(dir, copyFiles, hardLinkFiles, sstFilesToExclude,
@@ -347,16 +368,6 @@ private boolean getFilesForArchive(DBCheckpoint checkpoint,
compactionLogDir.getOriginalDir().toPath());
}
- private void logEstimatedTarballSize(DBCheckpoint checkpoint, boolean
includeSnapshotData)
- throws IOException {
- Set<Path> snapshotPaths = new HashSet<>();
- if (includeSnapshotData) {
- // since this is an estimate we can avoid waiting for dir to exist.
- snapshotPaths = getSnapshotDirs(checkpoint, false);
- }
-
OMDBCheckpointUtils.logEstimatedTarballSize(checkpoint.getCheckpointLocation(),
snapshotPaths);
- }
-
/**
* The snapshotInfo table may contain a snapshot that
* doesn't yet exist on the fs, so wait a few seconds for it.
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMDBCheckpointServletInodeBasedXfer.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMDBCheckpointServletInodeBasedXfer.java
index dfe610b0b50..9cea3d5a0ff 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMDBCheckpointServletInodeBasedXfer.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMDBCheckpointServletInodeBasedXfer.java
@@ -20,12 +20,12 @@
import static org.apache.hadoop.hdds.utils.Archiver.includeFile;
import static org.apache.hadoop.ozone.OzoneConsts.OM_CHECKPOINT_DIR;
import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_REQUEST_TO_EXCLUDE_SST;
+import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER;
import static org.apache.hadoop.ozone.OzoneConsts.ROCKSDB_SST_SUFFIX;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RATIS_SNAPSHOT_MAX_TOTAL_SST_SIZE_DEFAULT;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RATIS_SNAPSHOT_MAX_TOTAL_SST_SIZE_KEY;
import static org.apache.hadoop.ozone.om.OmSnapshotManager.getSnapshotPath;
import static
org.apache.hadoop.ozone.om.snapshot.OMDBCheckpointUtils.includeSnapshotData;
-import static
org.apache.hadoop.ozone.om.snapshot.OMDBCheckpointUtils.logEstimatedTarballSize;
import static org.apache.hadoop.ozone.om.snapshot.OmSnapshotUtils.DATA_PREFIX;
import static org.apache.hadoop.ozone.om.snapshot.OmSnapshotUtils.DATA_SUFFIX;
@@ -49,6 +49,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.OptionalLong;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@@ -71,6 +72,7 @@
import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
import
org.apache.hadoop.ozone.om.lock.HierarchicalResourceLockManager.HierarchicalResourceLock;
import org.apache.hadoop.ozone.om.lock.OMLockDetails;
+import org.apache.hadoop.ozone.om.snapshot.OMDBCheckpointUtils;
import org.apache.hadoop.ozone.om.snapshot.OmSnapshotLocalDataManager;
import org.apache.hadoop.ozone.om.snapshot.OmSnapshotUtils;
import org.apache.hadoop.ozone.om.snapshot.SnapshotCache;
@@ -175,7 +177,10 @@ public void
processMetadataSnapshotRequest(HttpServletRequest request, HttpServl
response.setContentType("application/x-tar");
response.setHeader("Content-Disposition", "attachment; filename=\"" +
tarName + "\"");
Instant start = Instant.now();
- collectDbDataToTransfer(request, receivedSstFiles, omdbArchiver);
+ OptionalLong estimatedSstBytes =
+ collectDbDataToTransfer(request, receivedSstFiles, omdbArchiver);
+ estimatedSstBytes.ifPresent(bytes -> response.setHeader(
+ OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER,
Long.toString(bytes)));
Instant end = Instant.now();
long duration = Duration.between(start, end).toMillis();
LOG.info("Time taken to collect the DB data : {} milliseconds",
duration);
@@ -234,9 +239,10 @@ Path getCompactionLogDir() {
*
* @param request The HTTP servlet request containing parameters
for the snapshot.
* @param sstFilesToExclude Set of SST file identifiers to exclude from the
archive.
- * @throws IOException if an I/O error occurs during processing or streaming.
+ * @return estimated total uncompressed SST bytes for a full checkpoint
+ * (no SST exclusions), or empty if not computed
*/
- public void collectDbDataToTransfer(HttpServletRequest request,
+ public OptionalLong collectDbDataToTransfer(HttpServletRequest request,
Set<String> sstFilesToExclude, OMDBArchiver omdbArchiver) throws
IOException {
DBCheckpoint checkpoint = null;
OzoneManager om = (OzoneManager)
getServletContext().getAttribute(OzoneConsts.OM_CONTEXT_ATTRIBUTE);
@@ -254,8 +260,17 @@ public void collectDbDataToTransfer(HttpServletRequest
request,
snapshotPaths = getSnapshotDirsFromDB(omMetadataManager,
omMetadataManager, snapshotLocalDataManager).values();
}
+ OptionalLong estimateForHeader = OptionalLong.empty();
if (sstFilesToExclude.isEmpty()) {
- logEstimatedTarballSize(getDbStore().getDbLocation().toPath(),
snapshotPaths);
+ try {
+ OMDBCheckpointUtils.SstSizeEstimate estimate = OMDBCheckpointUtils
+
.estimateCheckpointTarballSstDetails(getDbStore().getDbLocation().toPath(),
snapshotPaths);
+ OMDBCheckpointUtils.logEstimatedTarballSize(estimate,
snapshotPaths.size());
+ estimateForHeader = OptionalLong.of(estimate.getTotalBytes());
+ } catch (IOException e) {
+ LOG.warn("Could not estimate checkpoint tarball SST size for response
header: {}",
+ e.getMessage());
+ }
}
boolean shouldContinue = true;
@@ -339,6 +354,7 @@ public void collectDbDataToTransfer(HttpServletRequest
request,
} finally {
cleanupCheckpoint(checkpoint);
}
+ return estimateForHeader;
}
/**
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 1910d92e969..5d1e33f7cd8 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -50,6 +50,7 @@
import static org.apache.hadoop.ozone.OzoneConsts.OM_KEY_PREFIX;
import static org.apache.hadoop.ozone.OzoneConsts.OM_METRICS_FILE;
import static org.apache.hadoop.ozone.OzoneConsts.OM_METRICS_TEMP_FILE;
+import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER;
import static org.apache.hadoop.ozone.OzoneConsts.OZONE_RATIS_SNAPSHOT_DIR;
import static org.apache.hadoop.ozone.OzoneConsts.PREPARE_MARKER_KEY;
import static org.apache.hadoop.ozone.OzoneConsts.RPC_PORT;
@@ -60,6 +61,8 @@
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_DIR_DELETING_SERVICE_INTERVAL;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_KEY_DELETING_LIMIT_PER_TASK;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_KEY;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_EDEKCACHELOADER_INITIAL_DELAY_MS_DEFAULT;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_EDEKCACHELOADER_INITIAL_DELAY_MS_KEY;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_EDEKCACHELOADER_INTERVAL_MS_DEFAULT;
@@ -4115,7 +4118,18 @@ public synchronized TermIndex
installSnapshotFromLeader(String leaderId) throws
omDBCheckpoint = omRatisSnapshotProvider.
downloadDBSnapshotFromLeader(leaderId);
} catch (IOException ex) {
- LOG.error("Failed to download snapshot from Leader {}.", leaderId, ex);
+ if (OmRatisSnapshotProvider.isDiskFullOrQuotaIOException(ex)) {
+ LOG.error(
+ "Failed to download snapshot from leader {}: local disk appears
full or over quota "
+ + "on the OM ratis snapshot volume (see previous ERROR for
path/usable space). "
+ + "Free disk or adjust {}, {}, or {} before bootstrap can
succeed.",
+ leaderId,
+ OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY,
+ OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_KEY,
+ OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER);
+ } else {
+ LOG.error("Failed to download snapshot from Leader {}.", leaderId, ex);
+ }
cleanupCheckpoint(omDBCheckpoint);
return null;
}
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis_snapshot/OmRatisSnapshotProvider.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis_snapshot/OmRatisSnapshotProvider.java
index 9de1b692c5c..e087a617d01 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis_snapshot/OmRatisSnapshotProvider.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis_snapshot/OmRatisSnapshotProvider.java
@@ -22,6 +22,11 @@
import static org.apache.hadoop.ozone.OzoneConsts.MULTIPART_FORM_DATA_BOUNDARY;
import static org.apache.hadoop.ozone.OzoneConsts.OM_DB_NAME;
import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_REQUEST_TO_EXCLUDE_SST;
+import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_DEFAULT;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_KEY;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_BOOTSTRAP_MIN_SPACE_DEFAULT;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_DEFAULT;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_KEY;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_HTTP_AUTH_TYPE;
@@ -30,6 +35,7 @@
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_SNAPSHOT_PROVIDER_REQUEST_TIMEOUT_DEFAULT;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_SNAPSHOT_PROVIDER_REQUEST_TIMEOUT_KEY;
+import com.google.common.annotations.VisibleForTesting;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
@@ -37,14 +43,17 @@
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FileUtils;
import org.apache.hadoop.hdds.conf.MutableConfigurationSource;
+import org.apache.hadoop.hdds.conf.StorageUnit;
import org.apache.hadoop.hdds.server.http.HttpConfig;
import org.apache.hadoop.hdds.utils.HAUtils;
import org.apache.hadoop.hdds.utils.LegacyHadoopConfigurationSource;
@@ -54,6 +63,8 @@
import org.apache.hadoop.hdfs.web.URLConnectionFactory;
import org.apache.hadoop.ozone.om.helpers.OMNodeDetails;
import org.apache.hadoop.security.SecurityUtil;
+import org.apache.hadoop.util.DiskChecker.DiskOutOfSpaceException;
+import org.apache.hadoop.util.StringUtils;
import org.apache.hadoop.util.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -86,6 +97,103 @@ public class OmRatisSnapshotProvider extends
RDBSnapshotProvider {
private final boolean spnegoEnabled;
private final URLConnectionFactory connectionFactory;
private final boolean useV2CheckpointApi;
+ /** Minimum usable bytes on snapshot volume before download; 0 = disabled. */
+ private final long bootstrapMinSpaceBytes;
+ /** Applied to leader-reported SST byte estimate to reserve tar/unpack
headroom. */
+ private final double bootstrapCheckpointHeadroomRatio;
+
+ private static final class BootstrapSpaceRequirement {
+ private final long requiredBytes;
+ private final boolean usedLeaderEstimateHeader;
+
+ private BootstrapSpaceRequirement(long requiredBytes, boolean
usedLeaderEstimateHeader) {
+ this.requiredBytes = requiredBytes;
+ this.usedLeaderEstimateHeader = usedLeaderEstimateHeader;
+ }
+ }
+
+ /**
+ * Whether this {@link IOException} (or its causes) typically means the
+ * local filesystem ran out of space or hit a quota while writing.
+ */
+ public static boolean isDiskFullOrQuotaIOException(IOException ioe) {
+ for (Throwable t = ioe; t != null; t = t.getCause()) {
+ if (t instanceof DiskOutOfSpaceException) {
+ return true;
+ }
+ if (matchesDiskFullOrQuotaMessage(t)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Best-effort supplement for JDK {@link FileSystemException} ENOSPC and
+ * quota wording on Linux OM deployments (typically English from libc/JVM).
+ * {@link DiskOutOfSpaceException} in the cause chain is handled by type
+ * in {@link #isDiskFullOrQuotaIOException(IOException)} and does not depend
+ * on message text. Localized OS messages without matching substrings are not
+ * detected here.
+ */
+ private static boolean matchesDiskFullOrQuotaMessage(Throwable throwable) {
+ if (throwable instanceof FileSystemException) {
+ String reason = ((FileSystemException) throwable).getReason();
+ if (reason != null && containsDiskFullOrQuotaText(reason)) {
+ return true;
+ }
+ }
+ String msg = throwable.getMessage();
+ return msg != null && containsDiskFullOrQuotaText(msg);
+ }
+
+ private static boolean containsDiskFullOrQuotaText(String text) {
+ String m = text.toLowerCase(Locale.ROOT);
+ return m.contains("no space left on device")
+ || m.contains("no space")
+ || m.contains("space left")
+ || m.contains("enospc")
+ || m.contains("disk quota exceeded")
+ || m.contains("quota exceeded")
+ || m.contains("quota");
+ }
+
+ private static String formatSnapshotVolumeUsableSpace(File pathOnVolume) {
+ try {
+ Path storePath =
+ pathOnVolume.isDirectory() ? pathOnVolume.toPath() :
pathOnVolume.toPath().getParent();
+ if (storePath == null) {
+ return "unknown";
+ }
+ long usable = Files.getFileStore(storePath).getUsableSpace();
+ return String.format("%s (%d bytes)", StringUtils.byteDesc(usable),
usable);
+ } catch (Exception e) {
+ return "unknown (" + e.getMessage() + ")";
+ }
+ }
+
+ /**
+ * Logs at ERROR when the failure is likely due to disk full / quota, so
+ * operators can distinguish it from network or leader-side errors.
+ */
+ private static void logDiskFullOrQuotaDuringDownload(
+ IOException ioe, File targetFile, String leaderNodeId, URL
checkpointUrl) {
+ if (!isDiskFullOrQuotaIOException(ioe)) {
+ return;
+ }
+ LOG.error(
+ "OM ratis snapshot download from leader {} failed: disk full or
filesystem quota while "
+ + "writing checkpoint file {} (checkpoint URL {}). Usable space on
this volume: {}. "
+ + "Free disk on this OM node or raise {} or adjust {}. Underlying
message: {}",
+ leaderNodeId,
+ targetFile.getAbsolutePath(),
+ checkpointUrl,
+ formatSnapshotVolumeUsableSpace(targetFile),
+ OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY,
+ OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_KEY,
+ ioe.getMessage(),
+ ioe);
+ }
public OmRatisSnapshotProvider(File snapshotDir,
Map<String, OMNodeDetails> peerNodesMap, HttpConfig.Policy httpPolicy,
@@ -96,38 +204,64 @@ public OmRatisSnapshotProvider(File snapshotDir,
this.spnegoEnabled = spnegoEnabled;
this.connectionFactory = connectionFactory;
this.useV2CheckpointApi = OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_DEFAULT;
+ this.bootstrapMinSpaceBytes = 0L;
+ this.bootstrapCheckpointHeadroomRatio =
OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_DEFAULT;
}
public OmRatisSnapshotProvider(MutableConfigurationSource conf,
File omRatisSnapshotDir, Map<String, OMNodeDetails> peerNodeDetails) {
+ this(conf, omRatisSnapshotDir, peerNodeDetails, null);
+ }
+
+ /**
+ * Same as {@link #OmRatisSnapshotProvider(MutableConfigurationSource, File,
Map)} but allows
+ * tests to inject a {@link URLConnectionFactory} (for example a factory
that returns a mock
+ * {@link HttpURLConnection}).
+ */
+ @VisibleForTesting
+ public OmRatisSnapshotProvider(MutableConfigurationSource conf,
+ File omRatisSnapshotDir,
+ Map<String, OMNodeDetails> peerNodeDetails,
+ URLConnectionFactory connectionFactoryOverride) {
super(omRatisSnapshotDir, OM_DB_NAME);
LOG.info("Initializing OM Snapshot Provider");
this.peerNodesMap = new ConcurrentHashMap<>();
peerNodesMap.putAll(peerNodeDetails);
this.useV2CheckpointApi =
conf.getBoolean(OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_KEY,
OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_DEFAULT);
+ this.bootstrapMinSpaceBytes = (long) conf.getStorageSize(
+ OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY,
+ OZONE_OM_BOOTSTRAP_MIN_SPACE_DEFAULT,
+ StorageUnit.BYTES);
+ this.bootstrapCheckpointHeadroomRatio = conf.getDouble(
+ OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_KEY,
+ OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_DEFAULT);
this.httpPolicy = HttpConfig.getHttpPolicy(conf);
this.spnegoEnabled = conf.get(OZONE_OM_HTTP_AUTH_TYPE, "simple")
.equals("kerberos");
- TimeUnit connectionTimeoutUnit =
- OZONE_OM_SNAPSHOT_PROVIDER_CONNECTION_TIMEOUT_DEFAULT.getUnit();
- int connectionTimeoutMS = (int) conf.getTimeDuration(
- OZONE_OM_SNAPSHOT_PROVIDER_CONNECTION_TIMEOUT_KEY,
- OZONE_OM_SNAPSHOT_PROVIDER_CONNECTION_TIMEOUT_DEFAULT.getDuration(),
- connectionTimeoutUnit);
-
- TimeUnit requestTimeoutUnit =
- OZONE_OM_SNAPSHOT_PROVIDER_REQUEST_TIMEOUT_DEFAULT.getUnit();
- int requestTimeoutMS = (int) conf.getTimeDuration(
- OZONE_OM_SNAPSHOT_PROVIDER_REQUEST_TIMEOUT_KEY,
- OZONE_OM_SNAPSHOT_PROVIDER_REQUEST_TIMEOUT_DEFAULT.getDuration(),
- requestTimeoutUnit);
-
- connectionFactory = URLConnectionFactory
- .newDefaultURLConnectionFactory(connectionTimeoutMS, requestTimeoutMS,
- LegacyHadoopConfigurationSource.asHadoopConfiguration(conf));
+ if (connectionFactoryOverride != null) {
+ this.connectionFactory = connectionFactoryOverride;
+ } else {
+ TimeUnit connectionTimeoutUnit =
+ OZONE_OM_SNAPSHOT_PROVIDER_CONNECTION_TIMEOUT_DEFAULT.getUnit();
+ int connectionTimeoutMS = (int) conf.getTimeDuration(
+ OZONE_OM_SNAPSHOT_PROVIDER_CONNECTION_TIMEOUT_KEY,
+ OZONE_OM_SNAPSHOT_PROVIDER_CONNECTION_TIMEOUT_DEFAULT.getDuration(),
+ connectionTimeoutUnit);
+
+ TimeUnit requestTimeoutUnit =
+ OZONE_OM_SNAPSHOT_PROVIDER_REQUEST_TIMEOUT_DEFAULT.getUnit();
+ int requestTimeoutMS = (int) conf.getTimeDuration(
+ OZONE_OM_SNAPSHOT_PROVIDER_REQUEST_TIMEOUT_KEY,
+ OZONE_OM_SNAPSHOT_PROVIDER_REQUEST_TIMEOUT_DEFAULT.getDuration(),
+ requestTimeoutUnit);
+
+ this.connectionFactory = URLConnectionFactory
+ .newDefaultURLConnectionFactory(connectionTimeoutMS,
requestTimeoutMS,
+ LegacyHadoopConfigurationSource.asHadoopConfiguration(conf));
+ }
}
/**
@@ -144,6 +278,88 @@ public void removeDecommissionedPeerNode(String
decommNodeId) {
peerNodesMap.remove(decommNodeId);
}
+ /**
+ * Ensures the filesystem that holds {@link #getSnapshotDir()} has enough
+ * free space for OM bootstrap / install snapshot download and unpack.
+ *
+ * @throws IOException if {@link #bootstrapMinSpaceBytes} is > 0 and
+ * usable space is below the configured minimum
+ */
+ void ensureBootstrapDiskSpace() throws IOException {
+ ensureBootstrapDiskSpaceForRequiredBytes(
+ new BootstrapSpaceRequirement(bootstrapMinSpaceBytes, false));
+ }
+
+ private BootstrapSpaceRequirement resolveBootstrapSpaceRequirement(
+ HttpURLConnection connection) {
+ String headerValue =
+
connection.getHeaderField(OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER);
+ if (headerValue != null) {
+ String trimmed = headerValue.trim();
+ if (!trimmed.isEmpty()) {
+ try {
+ long estimatedSstBytes = Long.parseLong(trimmed);
+ if (estimatedSstBytes > 0) {
+ long required = (long) Math.ceil(estimatedSstBytes *
bootstrapCheckpointHeadroomRatio);
+ return new BootstrapSpaceRequirement(required, true);
+ }
+ } catch (NumberFormatException e) {
+ LOG.warn("Ignoring invalid {} response header: {}",
+ OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER, headerValue);
+ }
+ }
+ }
+ return new BootstrapSpaceRequirement(bootstrapMinSpaceBytes, false);
+ }
+
+ private void
ensureBootstrapDiskSpaceForRequiredBytes(BootstrapSpaceRequirement requirement)
+ throws IOException {
+ if (requirement.requiredBytes <= 0) {
+ if (requirement.usedLeaderEstimateHeader) {
+ LOG.debug("Leader returned a non-positive SST size estimate; skipping
disk space check.");
+ } else {
+ LOG.debug("{} is 0 or negative; skipping bootstrap disk space check.",
+ OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY);
+ }
+ return;
+ }
+ File snapshotRoot = getSnapshotDir();
+ if (!snapshotRoot.exists()) {
+ throw new IOException(String.format(
+ "OM ratis snapshot directory %s does not exist; cannot verify
required free space (%s)",
+ snapshotRoot.getAbsolutePath(),
+ StringUtils.byteDesc(requirement.requiredBytes)));
+ }
+ final long usable =
Files.getFileStore(snapshotRoot.toPath()).getUsableSpace();
+ if (usable < requirement.requiredBytes) {
+ String source = requirement.usedLeaderEstimateHeader
+ ? OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER + " with "
+ + OZONE_OM_BOOTSTRAP_CHECKPOINT_HEADROOM_RATIO_KEY
+ : OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY;
+ String message = String.format(
+ "OM bootstrap / install snapshot aborted: volume containing ratis
snapshot dir "
+ + "%s has usable space %s (%d bytes) but at least %s (%d bytes)
is required "
+ + "(from %s). Free disk on this OM host or adjust
configuration.",
+ snapshotRoot.getAbsolutePath(),
+ StringUtils.byteDesc(usable),
+ usable,
+ StringUtils.byteDesc(requirement.requiredBytes),
+ requirement.requiredBytes,
+ source);
+ LOG.error(message);
+ throw new IOException(message);
+ }
+ LOG.info(
+ "Bootstrap disk space check passed for OM ratis snapshot dir {}:
usable {} >= "
+ + "required {} (from {})",
+ snapshotRoot.getAbsolutePath(),
+ StringUtils.byteDesc(usable),
+ StringUtils.byteDesc(requirement.requiredBytes),
+ requirement.usedLeaderEstimateHeader
+ ? OZONE_OM_CHECKPOINT_ESTIMATED_SST_BYTES_HEADER
+ : OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY);
+ }
+
@Override
public void downloadSnapshot(String leaderNodeID, File targetFile)
throws IOException {
@@ -156,35 +372,43 @@ public void downloadSnapshot(String leaderNodeID, File
targetFile)
HttpURLConnection connection = (HttpURLConnection)
connectionFactory.openConnection(omCheckpointUrl, spnegoEnabled);
- connection.setRequestMethod("POST");
- String contentTypeValue = "multipart/form-data; boundary=" +
- MULTIPART_FORM_DATA_BOUNDARY;
- connection.setRequestProperty("Content-Type", contentTypeValue);
- connection.setDoOutput(true);
-
- List<String> existingFiles = useV2CheckpointApi ?
HAUtils.getExistingFiles(getCandidateDir())
- : HAUtils.getExistingSstFilesRelativeToDbDir(getCandidateDir());
- writeFormData(connection, existingFiles);
-
- connection.connect();
- int errorCode = connection.getResponseCode();
- if ((errorCode != HTTP_OK) && (errorCode != HTTP_CREATED)) {
- throw new IOException("Unexpected exception when trying to reach " +
- "OM to download latest checkpoint. Checkpoint URL: " +
- omCheckpointUrl + ". ErrorCode: " + errorCode);
- }
+ try {
+ connection.setRequestMethod("POST");
+ String contentTypeValue = "multipart/form-data; boundary=" +
+ MULTIPART_FORM_DATA_BOUNDARY;
+ connection.setRequestProperty("Content-Type", contentTypeValue);
+ connection.setDoOutput(true);
+
+ List<String> existingFiles = useV2CheckpointApi ?
HAUtils.getExistingFiles(getCandidateDir())
+ : HAUtils.getExistingSstFilesRelativeToDbDir(getCandidateDir());
+ writeFormData(connection, existingFiles);
+
+ connection.connect();
+ int errorCode = connection.getResponseCode();
+ if ((errorCode != HTTP_OK) && (errorCode != HTTP_CREATED)) {
+ throw new IOException("Unexpected exception when trying to reach " +
+ "OM to download latest checkpoint. Checkpoint URL: " +
+ omCheckpointUrl + ". ErrorCode: " + errorCode);
+ }
- try (InputStream inputStream = connection.getInputStream()) {
- downloadFileWithProgress(inputStream, targetFile);
+ ensureBootstrapDiskSpaceForRequiredBytes(
+ resolveBootstrapSpaceRequirement(connection));
+
+ try (InputStream inputStream = connection.getInputStream()) {
+ downloadFileWithProgress(inputStream, targetFile);
+ }
} catch (IOException ex) {
+ logDiskFullOrQuotaDuringDownload(ex, targetFile, leaderNodeID,
omCheckpointUrl);
boolean deleted = FileUtils.deleteQuietly(targetFile);
- if (!deleted) {
+ if (!deleted && targetFile.exists()) {
LOG.error("OM snapshot which failed to download {} cannot be
deleted",
targetFile);
}
throw ex;
} finally {
- connection.disconnect();
+ if (connection != null) {
+ connection.disconnect();
+ }
}
return null;
});
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/OMDBCheckpointUtils.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/OMDBCheckpointUtils.java
index e264709bd16..558ff596cbc 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/OMDBCheckpointUtils.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/OMDBCheckpointUtils.java
@@ -21,6 +21,7 @@
import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_INCLUDE_SNAPSHOT_DATA;
import static org.apache.hadoop.ozone.OzoneConsts.ROCKSDB_SST_SUFFIX;
+import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
@@ -49,32 +50,75 @@ public final class OMDBCheckpointUtils {
private OMDBCheckpointUtils() {
}
+ /**
+ * Uncompressed total size of SST files under the DB checkpoint and optional
+ * snapshot paths (used for logging and follower disk checks).
+ */
+ public static final class SstSizeEstimate {
+ private final long totalBytes;
+ private final long fileCount;
+
+ public SstSizeEstimate(long totalBytes, long fileCount) {
+ this.totalBytes = totalBytes;
+ this.fileCount = fileCount;
+ }
+
+ public long getTotalBytes() {
+ return totalBytes;
+ }
+
+ public long getFileCount() {
+ return fileCount;
+ }
+ }
+
public static boolean includeSnapshotData(HttpServletRequest request) {
String includeParam =
request.getParameter(OZONE_DB_CHECKPOINT_INCLUDE_SNAPSHOT_DATA);
return Boolean.parseBoolean(includeParam);
}
+ /**
+ * Walks the given paths and sums logical size of SST files only.
+ *
+ * @throws IOException if the file tree walk fails
+ */
+ public static SstSizeEstimate estimateCheckpointTarballSstDetails(
+ Path dbLocation, Collection<Path> snapshotPaths) throws IOException {
+ Counters.PathCounters counters = Counters.longPathCounters();
+ CountingPathVisitor visitor = new CountingPathVisitor(
+ counters, SST_FILE_FILTER, TRUE);
+ Files.walkFileTree(dbLocation, visitor);
+ boolean includeSnapshotData = !snapshotPaths.isEmpty();
+ if (includeSnapshotData) {
+ for (Path snapshotDir : snapshotPaths) {
+ Files.walkFileTree(snapshotDir, visitor);
+ }
+ }
+ return new SstSizeEstimate(
+ counters.getByteCounter().get(),
+ counters.getFileCounter().get());
+ }
+
public static void logEstimatedTarballSize(Path dbLocation, Collection<Path>
snapshotPaths) {
try {
- Counters.PathCounters counters = Counters.longPathCounters();
- CountingPathVisitor visitor = new CountingPathVisitor(
- counters, SST_FILE_FILTER, TRUE);
- Files.walkFileTree(dbLocation, visitor);
- boolean includeSnapshotData = !snapshotPaths.isEmpty();
- long totalSnapshots = snapshotPaths.size();
- if (includeSnapshotData) {
- for (Path snapshotDir: snapshotPaths) {
- Files.walkFileTree(snapshotDir, visitor);
- }
- }
- LOG.info("Estimates for Checkpoint Tarball Stream - Data size: {} KB,
SST files: {}{}",
- counters.getByteCounter().get() / (1024),
- counters.getFileCounter().get(),
- (includeSnapshotData ? ", snapshots: " + totalSnapshots : ""));
+ SstSizeEstimate estimate =
+ estimateCheckpointTarballSstDetails(dbLocation, snapshotPaths);
+ logEstimatedTarballSize(estimate, snapshotPaths.size());
} catch (Exception e) {
LOG.error("Could not estimate size of transfer to Checkpoint Tarball
Stream for dbLocation:{} snapshotPaths:{}",
dbLocation, snapshotPaths, e);
}
}
+
+ /**
+ * Logs the result of a prior {@link #estimateCheckpointTarballSstDetails}
call.
+ */
+ public static void logEstimatedTarballSize(SstSizeEstimate estimate, int
snapshotDirCount) {
+ boolean includeSnapshotData = snapshotDirCount > 0;
+ LOG.info("Estimates for Checkpoint Tarball Stream - Data size: {} KB, SST
files: {}{}",
+ estimate.getTotalBytes() / (1024),
+ estimate.getFileCount(),
+ (includeSnapshotData ? ", snapshots: " + snapshotDirCount : ""));
+ }
}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis_snapshot/TestOmRatisSnapshotProvider.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis_snapshot/TestOmRatisSnapshotProvider.java
index 2fb0f56ae89..13c6b2355d4 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis_snapshot/TestOmRatisSnapshotProvider.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis_snapshot/TestOmRatisSnapshotProvider.java
@@ -19,7 +19,10 @@
import static java.net.HttpURLConnection.HTTP_OK;
import static org.apache.hadoop.ozone.OzoneConsts.MULTIPART_FORM_DATA_BOUNDARY;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.mock;
@@ -33,15 +36,19 @@
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystemException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.server.http.HttpConfig;
import org.apache.hadoop.hdfs.web.URLConnectionFactory;
import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
import org.apache.hadoop.ozone.om.helpers.OMNodeDetails;
import
org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.hadoop.util.DiskChecker.DiskOutOfSpaceException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -80,6 +87,58 @@ public void setup(@TempDir File snapshotDir,
false, connectionFactory);
}
+ @Test
+ public void testIsDiskFullOrQuotaIOExceptionDetectsNoSpaceMessage() {
+ assertThat(OmRatisSnapshotProvider.isDiskFullOrQuotaIOException(
+ new IOException("No space left on device"))).isTrue();
+ }
+
+ @Test
+ public void
testIsDiskFullOrQuotaIOExceptionDetectsFileSystemExceptionReason() {
+ IOException wrapped = new IOException("write failed",
+ new FileSystemException("p", null, "No space left on device"));
+
assertThat(OmRatisSnapshotProvider.isDiskFullOrQuotaIOException(wrapped)).isTrue();
+ }
+
+ @Test
+ public void
testIsDiskFullOrQuotaIOExceptionDetectsDiskOutOfSpaceExceptionInCauseChain() {
+ IOException wrapped = new IOException("write failed", new
DiskOutOfSpaceException("full"));
+
assertThat(OmRatisSnapshotProvider.isDiskFullOrQuotaIOException(wrapped)).isTrue();
+ }
+
+ @Test
+ public void
testIsDiskFullOrQuotaIOExceptionReturnsFalseForNonEnglishFileSystemException() {
+ IOException wrapped = new IOException("write failed",
+ new FileSystemException("p", null, "Kein Speicherplatz mehr auf dem
Gerät"));
+
assertThat(OmRatisSnapshotProvider.isDiskFullOrQuotaIOException(wrapped)).isFalse();
+ }
+
+ @Test
+ public void testIsDiskFullOrQuotaIOExceptionReturnsFalseForOtherErrors() {
+ assertThat(OmRatisSnapshotProvider.isDiskFullOrQuotaIOException(
+ new IOException("Connection reset"))).isFalse();
+ }
+
+ @Test
+ public void testBootstrapDiskSpaceCheckSkippedWhenZero(@TempDir File
snapshotDir) {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(OMConfigKeys.OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY, "0GB");
+ OmRatisSnapshotProvider provider =
+ new OmRatisSnapshotProvider(conf, snapshotDir, new HashMap<>());
+ assertDoesNotThrow(provider::ensureBootstrapDiskSpace);
+ }
+
+ @Test
+ public void testBootstrapDiskSpaceCheckFailsWhenBelowMinimum(@TempDir File
snapshotDir) {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(OMConfigKeys.OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY, "1024EB");
+ OmRatisSnapshotProvider provider =
+ new OmRatisSnapshotProvider(conf, snapshotDir, new HashMap<>());
+ IOException ex =
+ assertThrows(IOException.class, provider::ensureBootstrapDiskSpace);
+
assertThat(ex.getMessage()).contains(OMConfigKeys.OZONE_OM_BOOTSTRAP_MIN_SPACE_KEY);
+ }
+
@Test
public void testDownloadSnapshot() throws IOException,
AuthenticationException {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]