This is an automated email from the ASF dual-hosted git repository.
sumitagrawal 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 153659032b HDDS-8782. Improve Volume Scanner Health checks. (#4867)
153659032b is described below
commit 153659032bcdc177f7a0e0afc6ff97c06fc2a76a
Author: Ethan Rose <[email protected]>
AuthorDate: Wed Jun 28 01:30:56 2023 -0700
HDDS-8782. Improve Volume Scanner Health checks. (#4867)
---
.../common/statemachine/DatanodeConfiguration.java | 119 +++++++++++-
.../container/common/utils/DiskCheckUtil.java | 201 +++++++++++++++++++++
.../ozone/container/common/volume/HddsVolume.java | 30 ++-
.../container/common/volume/StorageVolume.java | 151 +++++++++++++++-
.../common/volume/StorageVolumeChecker.java | 9 +-
.../container/common/utils/TestDiskCheckUtil.java | 88 +++++++++
.../container/common/volume/TestHddsVolume.java | 66 ++++++-
.../container/common/volume/TestStorageVolume.java | 185 +++++++++++++++++++
8 files changed, 826 insertions(+), 23 deletions(-)
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/DatanodeConfiguration.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/DatanodeConfiguration.java
index a52f941358..164af7f31c 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/DatanodeConfiguration.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/DatanodeConfiguration.java
@@ -42,6 +42,12 @@ public class DatanodeConfiguration {
"hdds.datanode.container.delete.threads.max";
static final String PERIODIC_DISK_CHECK_INTERVAL_MINUTES_KEY =
"hdds.datanode.periodic.disk.check.interval.minutes";
+ public static final String DISK_CHECK_FILE_SIZE_KEY =
+ "hdds.datanode.disk.check.file.size";
+ public static final String DISK_CHECK_IO_TEST_COUNT_KEY =
+ "hdds.datanode.disk.check.io.test.count";
+ public static final String DISK_CHECK_IO_FAILURES_TOLERATED_KEY =
+ "hdds.datanode.disk.check.io.failures.tolerated";
public static final String FAILED_DATA_VOLUMES_TOLERATED_KEY =
"hdds.datanode.failed.data.volumes.tolerated";
public static final String FAILED_METADATA_VOLUMES_TOLERATED_KEY =
@@ -64,10 +70,16 @@ public class DatanodeConfiguration {
static final int FAILED_VOLUMES_TOLERATED_DEFAULT = -1;
+ public static final int DISK_CHECK_IO_TEST_COUNT_DEFAULT = 3;
+
+ public static final int DISK_CHECK_IO_FAILURES_TOLERATED_DEFAULT = 1;
+
+ public static final int DISK_CHECK_FILE_SIZE_DEFAULT = 100;
+
static final boolean WAIT_ON_ALL_FOLLOWERS_DEFAULT = false;
static final long DISK_CHECK_MIN_GAP_DEFAULT =
- Duration.ofMinutes(15).toMillis();
+ Duration.ofMinutes(10).toMillis();
static final long DISK_CHECK_TIMEOUT_DEFAULT =
Duration.ofMinutes(10).toMillis();
@@ -265,8 +277,44 @@ public class DatanodeConfiguration {
)
private int failedDbVolumesTolerated = FAILED_VOLUMES_TOLERATED_DEFAULT;
+ @Config(key = "disk.check.io.test.count",
+ defaultValue = "3",
+ type = ConfigType.INT,
+ tags = { DATANODE },
+ description = "The number of IO tests required to determine if a disk " +
+ " has failed. Each disk check does one IO test. The volume will be "
+
+ "failed if more than " +
+ "hdds.datanode.disk.check.io.failures.tolerated out of the last " +
+ "hdds.datanode.disk.check.io.test.count runs failed. Set to 0 " +
+ "to disable disk IO checks."
+ )
+ private int volumeIOTestCount = DISK_CHECK_IO_TEST_COUNT_DEFAULT;
+
+ @Config(key = "disk.check.io.failures.tolerated",
+ defaultValue = "1",
+ type = ConfigType.INT,
+ tags = { DATANODE },
+ description = "The number of IO tests out of the last hdds.datanode" +
+ ".disk.check.io.test.count test run that are allowed to fail before"
+
+ " the volume is marked as failed."
+ )
+ private int volumeIOFailureTolerance =
+ DISK_CHECK_IO_FAILURES_TOLERATED_DEFAULT;
+
+ @Config(key = "disk.check.file.size",
+ defaultValue = "100B",
+ type = ConfigType.SIZE,
+ tags = { DATANODE },
+ description = "The size of the temporary file that will be synced to " +
+ "the disk and " +
+ "read back to assess its health. The contents of the " +
+ "file will be stored in memory during the duration of the check."
+ )
+ private int volumeHealthCheckFileSize =
+ DISK_CHECK_FILE_SIZE_DEFAULT;
+
@Config(key = "disk.check.min.gap",
- defaultValue = "15m",
+ defaultValue = "10m",
type = ConfigType.TIME,
tags = { DATANODE },
description = "The minimum gap between two successive checks of the same"
@@ -461,6 +509,48 @@ public class DatanodeConfiguration {
failedDbVolumesTolerated = FAILED_VOLUMES_TOLERATED_DEFAULT;
}
+ if (volumeIOTestCount == 0) {
+ LOG.info("{} set to {}. Disk IO health tests have been disabled.",
+ DISK_CHECK_IO_TEST_COUNT_KEY, volumeIOTestCount);
+ } else {
+ if (volumeIOTestCount < 0) {
+ LOG.warn("{} must be greater than 0 but was set to {}." +
+ "Defaulting to {}",
+ DISK_CHECK_IO_TEST_COUNT_KEY, volumeIOTestCount,
+ DISK_CHECK_IO_TEST_COUNT_DEFAULT);
+ volumeIOTestCount = DISK_CHECK_IO_TEST_COUNT_DEFAULT;
+ }
+
+ if (volumeIOFailureTolerance < 0) {
+ LOG.warn("{} must be greater than or equal to 0 but was set to {}. " +
+ "Defaulting to {}",
+ DISK_CHECK_IO_FAILURES_TOLERATED_KEY, volumeIOFailureTolerance,
+ DISK_CHECK_IO_FAILURES_TOLERATED_DEFAULT);
+ volumeIOFailureTolerance = DISK_CHECK_IO_FAILURES_TOLERATED_DEFAULT;
+ }
+
+ if (volumeIOFailureTolerance >= volumeIOTestCount) {
+ LOG.warn("{} was set to {} but cannot be greater or equals to {} " +
+ "set to {}. Defaulting {} to {} and {} to {}",
+ DISK_CHECK_IO_FAILURES_TOLERATED_KEY, volumeIOFailureTolerance,
+ DISK_CHECK_IO_TEST_COUNT_KEY, volumeIOTestCount,
+ DISK_CHECK_IO_FAILURES_TOLERATED_KEY,
+ DISK_CHECK_IO_FAILURES_TOLERATED_DEFAULT,
+ DISK_CHECK_IO_TEST_COUNT_KEY, DISK_CHECK_IO_TEST_COUNT_DEFAULT);
+ volumeIOTestCount = DISK_CHECK_IO_TEST_COUNT_DEFAULT;
+ volumeIOFailureTolerance = DISK_CHECK_IO_FAILURES_TOLERATED_DEFAULT;
+ }
+
+ if (volumeHealthCheckFileSize < 1) {
+ LOG.warn(DISK_CHECK_FILE_SIZE_KEY +
+ "must be at least 1 byte and was set to {}. Defaulting to {}",
+ volumeHealthCheckFileSize,
+ DISK_CHECK_FILE_SIZE_DEFAULT);
+ volumeHealthCheckFileSize =
+ DISK_CHECK_FILE_SIZE_DEFAULT;
+ }
+ }
+
if (diskCheckMinGap < 0) {
LOG.warn(DISK_CHECK_MIN_GAP_KEY +
" must be greater than zero and was set to {}. Defaulting to {}",
@@ -497,7 +587,6 @@ public class DatanodeConfiguration {
rocksdbDeleteObsoleteFilesPeriod =
ROCKSDB_DELETE_OBSOLETE_FILES_PERIOD_MICRO_SECONDS_DEFAULT;
}
-
}
public void setContainerDeleteThreads(int containerDeleteThreads) {
@@ -541,6 +630,30 @@ public class DatanodeConfiguration {
this.failedDbVolumesTolerated = failedVolumesTolerated;
}
+ public int getVolumeIOTestCount() {
+ return volumeIOTestCount;
+ }
+
+ public void setVolumeIOTestCount(int testCount) {
+ this.volumeIOTestCount = testCount;
+ }
+
+ public int getVolumeIOFailureTolerance() {
+ return volumeIOFailureTolerance;
+ }
+
+ public void setVolumeIOFailureTolerance(int failureTolerance) {
+ volumeIOFailureTolerance = failureTolerance;
+ }
+
+ public int getVolumeHealthCheckFileSize() {
+ return volumeHealthCheckFileSize;
+ }
+
+ public void getVolumeHealthCheckFileSize(int fileSizeBytes) {
+ this.volumeHealthCheckFileSize = fileSizeBytes;
+ }
+
public boolean getCheckEmptyContainerDir() {
return bCheckEmptyContainerDir;
}
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/utils/DiskCheckUtil.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/utils/DiskCheckUtil.java
new file mode 100644
index 0000000000..b267b1d479
--- /dev/null
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/utils/DiskCheckUtil.java
@@ -0,0 +1,201 @@
+/*
+ * 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.ozone.container.common.utils;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.SyncFailedException;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.UUID;
+
+/**
+ * Utility class that supports checking disk health when provided a directory
+ * where the disk is mounted.
+ */
+public final class DiskCheckUtil {
+ private DiskCheckUtil() { }
+
+ // For testing purposes, an alternate check implementation can be provided
+ // to inject failures.
+ private static DiskChecks impl = new DiskChecksImpl();
+
+ @VisibleForTesting
+ public static void setTestImpl(DiskChecks diskChecks) {
+ impl = diskChecks;
+ }
+
+ @VisibleForTesting
+ public static void clearTestImpl() {
+ impl = new DiskChecksImpl();
+ }
+
+ public static boolean checkExistence(File storageDir) {
+ return impl.checkExistence(storageDir);
+ }
+
+ public static boolean checkPermissions(File storageDir) {
+ return impl.checkPermissions(storageDir);
+ }
+
+ public static boolean checkReadWrite(File storageDir, File testFileDir,
+ int numBytesToWrite) {
+ return impl.checkReadWrite(storageDir, testFileDir, numBytesToWrite);
+ }
+
+ /**
+ * Defines operations that must be implemented by a class injecting
+ * failures into this class. Default implementations return true so that
+ * tests only need to override methods for the failures they want to test.
+ */
+ public interface DiskChecks {
+ default boolean checkExistence(File storageDir) {
+ return true;
+ }
+ default boolean checkPermissions(File storageDir) {
+ return true;
+ }
+ default boolean checkReadWrite(File storageDir, File testFileDir,
+ int numBytesToWrite) {
+ return true;
+ }
+ }
+
+ /**
+ * The default implementation of DiskCheck that production code will use
+ * for disk checking.
+ */
+ private static class DiskChecksImpl implements DiskChecks {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(DiskCheckUtil.class);
+
+ private static final Random RANDOM = new Random();
+
+ @Override
+ public boolean checkExistence(File diskDir) {
+ if (!diskDir.exists()) {
+ logError(diskDir, "Directory does not exist.");
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean checkPermissions(File storageDir) {
+ // Check all permissions on the volume. If there are multiple permission
+ // errors, count it as one failure so the admin can fix them all at once.
+ boolean permissionsCorrect = true;
+ if (!storageDir.canRead()) {
+ logError(storageDir,
+ "Datanode does not have read permission on volume.");
+ permissionsCorrect = false;
+ }
+ if (!storageDir.canWrite()) {
+ logError(storageDir,
+ "Datanode does not have write permission on volume.");
+ permissionsCorrect = false;
+ }
+ if (!storageDir.canExecute()) {
+ logError(storageDir, "Datanode does not have execute" +
+ "permission on volume.");
+ permissionsCorrect = false;
+ }
+
+ return permissionsCorrect;
+ }
+
+ @Override
+ public boolean checkReadWrite(File storageDir,
+ File testFileDir, int numBytesToWrite) {
+ File testFile = new File(testFileDir, "disk-check-" + UUID.randomUUID());
+ byte[] writtenBytes = new byte[numBytesToWrite];
+ RANDOM.nextBytes(writtenBytes);
+ try (FileOutputStream fos = new FileOutputStream(testFile)) {
+ fos.write(writtenBytes);
+ fos.getFD().sync();
+ } catch (FileNotFoundException notFoundEx) {
+ logError(storageDir, String.format("Could not find file %s for " +
+ "volume check.", testFile), notFoundEx);
+ return false;
+ } catch (SyncFailedException syncEx) {
+ logError(storageDir, String.format("Could sync file %s to disk.",
+ testFile), syncEx);
+ return false;
+ } catch (IOException ioEx) {
+ logError(storageDir, String.format("Could not write file %s " +
+ "for volume check.", testFile), ioEx);
+ return false;
+ }
+
+ // Read data back from the test file.
+ byte[] readBytes = new byte[numBytesToWrite];
+ try (FileInputStream fis = new FileInputStream(testFile)) {
+ int numBytesRead = fis.read(readBytes);
+ if (numBytesRead != numBytesToWrite) {
+ logError(storageDir, String.format("%d bytes written to file %s " +
+ "but %d bytes were read back.", numBytesToWrite, testFile,
+ numBytesRead));
+ return false;
+ }
+ } catch (FileNotFoundException notFoundEx) {
+ logError(storageDir, String.format("Could not find file %s " +
+ "for volume check.", testFile), notFoundEx);
+ return false;
+ } catch (IOException ioEx) {
+ logError(storageDir, String.format("Could not read file %s " +
+ "for volume check.", testFile), ioEx);
+ return false;
+ }
+
+ // Check that test file has the expected content.
+ if (!Arrays.equals(writtenBytes, readBytes)) {
+ logError(storageDir, String.format("%d Bytes read from file " +
+ "%s do not match the %d bytes that were written.",
+ writtenBytes.length, testFile, readBytes.length));
+ return false;
+ }
+
+ // Delete the file.
+ if (!testFile.delete()) {
+ logError(storageDir, String.format("Could not delete file %s " +
+ "for volume check.", testFile));
+ return false;
+ }
+
+ // If all checks passed, the volume is healthy.
+ return true;
+ }
+
+ private void logError(File storageDir, String message) {
+ LOG.error("Volume {} failed health check. {}", storageDir, message);
+ }
+
+ private void logError(File storageDir, String message, Exception ex) {
+ LOG.error("Volume {} failed health check. {}", storageDir, message, ex);
+ }
+ }
+}
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/HddsVolume.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/HddsVolume.java
index bcbfe37554..2a2de08cb2 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/HddsVolume.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/HddsVolume.java
@@ -310,20 +310,32 @@ public class HddsVolume extends StorageVolume {
}
@Override
- public VolumeCheckResult check(@Nullable Boolean unused) throws Exception {
+ public synchronized VolumeCheckResult check(@Nullable Boolean unused)
+ throws Exception {
VolumeCheckResult result = super.check(unused);
- if (!isDbLoaded()) {
- return result;
- }
+
DatanodeConfiguration df =
getConf().getObject(DatanodeConfiguration.class);
if (result != VolumeCheckResult.HEALTHY ||
- !df.getContainerSchemaV3Enabled() || !df.autoCompactionSmallSstFile())
{
+ !df.getContainerSchemaV3Enabled() || !isDbLoaded()) {
return result;
}
- // Calculate number of files per level and size per level
- RawDB rawDB = DatanodeStoreCache.getInstance().getDB(
- new File(dbParentDir, CONTAINER_DB_NAME).getAbsolutePath(), getConf());
- rawDB.getStore().compactionIfNeeded();
+
+ // Check that per-volume RocksDB is present.
+ File dbFile = new File(dbParentDir, CONTAINER_DB_NAME);
+ if (!dbFile.exists() || !dbFile.canRead()) {
+ LOG.warn("Volume {} failed health check. Could not access RocksDB at " +
+ "{}", getStorageDir(), dbFile);
+ return VolumeCheckResult.FAILED;
+ }
+
+ // TODO HDDS-8784 trigger compaction outside of volume check. Then the
+ // exception can be removed.
+ if (df.autoCompactionSmallSstFile()) {
+ // Calculate number of files per level and size per level
+ RawDB rawDB = DatanodeStoreCache.getInstance().getDB(
+ dbFile.getAbsolutePath(), getConf());
+ rawDB.getStore().compactionIfNeeded();
+ }
return VolumeCheckResult.HEALTHY;
}
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/StorageVolume.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/StorageVolume.java
index 8141c57055..95d1b2c2de 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/StorageVolume.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/StorageVolume.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.ozone.container.common.volume;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.hadoop.fs.StorageType;
import org.apache.hadoop.hdds.conf.ConfigurationSource;
@@ -27,8 +28,9 @@ import
org.apache.hadoop.hdfs.server.datanode.checker.Checkable;
import org.apache.hadoop.hdfs.server.datanode.checker.VolumeCheckResult;
import org.apache.hadoop.ozone.common.InconsistentStorageStateException;
import org.apache.hadoop.ozone.container.common.helpers.DatanodeVersionFile;
+import
org.apache.hadoop.ozone.container.common.statemachine.DatanodeConfiguration;
+import org.apache.hadoop.ozone.container.common.utils.DiskCheckUtil;
import org.apache.hadoop.ozone.container.common.utils.StorageVolumeUtil;
-import org.apache.hadoop.util.DiskChecker;
import org.apache.hadoop.util.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,10 +39,15 @@ import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedList;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
+import java.util.Queue;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
import static
org.apache.hadoop.ozone.container.common.HDDSVolumeLayoutVersion.getLatestVersion;
@@ -66,6 +73,9 @@ public abstract class StorageVolume
// The name of the directory used for temporary files on the volume.
public static final String TMP_DIR_NAME = "tmp";
+ // The name of the directory where temporary files used to check disk
+ // health are written to. This will go inside the tmp directory.
+ public static final String TMP_DISK_CHECK_DIR_NAME = "disk-check";
/**
* Type for StorageVolume.
@@ -111,11 +121,22 @@ public abstract class StorageVolume
private final File storageDir;
private String workingDirName;
private File tmpDir;
+ private File diskCheckDir;
private final Optional<VolumeInfo> volumeInfo;
private final VolumeSet volumeSet;
+ /*
+ Fields used to implement IO based disk health checks.
+ If more than ioFailureTolerance IO checks fail out of the last ioTestCount
+ tests run, then the volume is considered failed.
+ */
+ private final int ioTestCount;
+ private final int ioFailureTolerance;
+ private AtomicInteger currentIOFailureCount;
+ private Queue<Boolean> ioTestSlidingWindow;
+ private int healthCheckFileSize;
protected StorageVolume(Builder<?> b) throws IOException {
if (!b.failedVolume) {
@@ -131,12 +152,22 @@ public abstract class StorageVolume
this.clusterID = b.clusterID;
this.datanodeUuid = b.datanodeUuid;
this.conf = b.conf;
+
+ DatanodeConfiguration dnConf =
+ conf.getObject(DatanodeConfiguration.class);
+ this.ioTestCount = dnConf.getVolumeIOTestCount();
+ this.ioFailureTolerance = dnConf.getVolumeIOFailureTolerance();
+ this.ioTestSlidingWindow = new LinkedList<>();
+ this.currentIOFailureCount = new AtomicInteger(0);
+ this.healthCheckFileSize = dnConf.getVolumeHealthCheckFileSize();
} else {
storageDir = new File(b.volumeRootStr);
this.volumeInfo = Optional.empty();
this.volumeSet = null;
this.storageID = UUID.randomUUID().toString();
this.state = VolumeState.FAILED;
+ this.ioTestCount = 0;
+ this.ioFailureTolerance = 0;
}
}
@@ -220,6 +251,8 @@ public abstract class StorageVolume
this.tmpDir =
new File(new File(getStorageDir(), workDirName), TMP_DIR_NAME);
Files.createDirectories(tmpDir.toPath());
+ diskCheckDir = createTmpSubdirIfNeeded(TMP_DISK_CHECK_DIR_NAME);
+ cleanTmpDiskCheckDir();
}
/**
@@ -438,6 +471,11 @@ public abstract class StorageVolume
return this.tmpDir;
}
+ @VisibleForTesting
+ public File getDiskCheckDir() {
+ return this.diskCheckDir;
+ }
+
public void refreshVolumeInfo() {
volumeInfo.ifPresent(VolumeInfo::refreshNow);
}
@@ -509,21 +547,122 @@ public abstract class StorageVolume
public void shutdown() {
setState(VolumeState.NON_EXISTENT);
volumeInfo.ifPresent(VolumeInfo::shutdownUsageThread);
+ cleanTmpDiskCheckDir();
+ }
+
+ /**
+ * Delete all temporary files in the directory used ot check disk health.
+ */
+ private void cleanTmpDiskCheckDir() {
+ // If the volume was shut down before initialization completed, skip
+ // emptying the directory.
+ if (diskCheckDir == null) {
+ return;
+ }
+
+ if (!diskCheckDir.exists()) {
+ LOG.warn("Unable to clear disk check files from {}. Directory does " +
+ "not exist.", diskCheckDir);
+ return;
+ }
+
+ if (!diskCheckDir.isDirectory()) {
+ LOG.warn("Unable to clear disk check files from {}. Location is not a" +
+ " directory", diskCheckDir);
+ return;
+ }
+
+ try (Stream<Path> files = Files.list(diskCheckDir.toPath())) {
+ files.map(Path::toFile).filter(File::isFile).forEach(file -> {
+ try {
+ Files.delete(file.toPath());
+ } catch (IOException ex) {
+ LOG.warn("Failed to delete temporary volume health check file {}",
+ file);
+ }
+ });
+ } catch (IOException ex) {
+ LOG.warn("Failed to list contents of volume health check directory {} " +
+ "for deleting.", diskCheckDir);
+ }
}
/**
- * Run a check on the current volume to determine if it is healthy.
+ * Run a check on the current volume to determine if it is healthy. The
+ * check consists of a directory check and an IO check.
+ *
+ * If the directory check fails, the volume check fails immediately.
+ * The IO check is allows to fail up to {@code ioFailureTolerance} times
+ * out of the last {@code ioTestCount} IO checks before this volume check is
+ * failed. Each call to this method runs one IO check.
+ *
* @param unused context for the check, ignored.
* @return result of checking the volume.
+ * @throws InterruptedException if there was an error during the volume
+ * check because the thread was interrupted.
* @throws Exception if an exception was encountered while running
- * the volume check.
+ * the volume check and the thread was not interrupted.
*/
@Override
- public VolumeCheckResult check(@Nullable Boolean unused) throws Exception {
- if (!storageDir.exists()) {
+ public synchronized VolumeCheckResult check(@Nullable Boolean unused)
+ throws Exception {
+ boolean directoryChecksPassed =
+ DiskCheckUtil.checkExistence(storageDir) &&
+ DiskCheckUtil.checkPermissions(storageDir);
+ // If the directory is not present or has incorrect permissions, fail the
+ // volume immediately. This is not an intermittent error.
+ if (!directoryChecksPassed) {
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedException("Directory check of volume " + this +
+ " interrupted.");
+ }
+ return VolumeCheckResult.FAILED;
+ }
+
+ // If IO test count is set to 0, IO tests for disk health are disabled.
+ if (ioTestCount == 0) {
+ return VolumeCheckResult.HEALTHY;
+ }
+
+ // Since IO errors may be intermittent, volume remains healthy until the
+ // threshold of failures is crossed.
+ boolean diskChecksPassed = DiskCheckUtil.checkReadWrite(storageDir,
+ diskCheckDir, healthCheckFileSize);
+ if (Thread.currentThread().isInterrupted()) {
+ // Thread interrupt may have caused IO operations to abort. Do not
+ // consider this a failure.
+ throw new InterruptedException("IO check of volume " + this +
+ " interrupted.");
+ }
+
+ // Move the sliding window of IO test results forward 1 by adding the
+ // latest entry and removing the oldest entry from the window.
+ // Update the failure counter for the new window.
+ ioTestSlidingWindow.add(diskChecksPassed);
+ if (!diskChecksPassed) {
+ currentIOFailureCount.incrementAndGet();
+ }
+ if (ioTestSlidingWindow.size() > ioTestCount &&
+ Objects.equals(ioTestSlidingWindow.poll(), Boolean.FALSE)) {
+ currentIOFailureCount.decrementAndGet();
+ }
+
+ // If the failure threshold has been crossed, fail the volume without
+ // further scans.
+ // Once the volume is failed, it will not be checked anymore.
+ // The failure counts can be left as is.
+ if (currentIOFailureCount.get() > ioFailureTolerance) {
+ LOG.info("Failed IO test for volume {}: the last {} runs " +
+ "encountered {}/{} tolerated failures.", this,
+ ioTestSlidingWindow.size(), currentIOFailureCount,
+ ioFailureTolerance);
return VolumeCheckResult.FAILED;
+ } else if (LOG.isDebugEnabled()) {
+ LOG.debug("IO test results for volume {}: the last {} runs encountered "
+
+ "{}/{} tolerated failures", this, ioTestSlidingWindow.size(),
+ currentIOFailureCount, ioFailureTolerance);
}
- DiskChecker.checkDir(storageDir);
+
return VolumeCheckResult.HEALTHY;
}
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/StorageVolumeChecker.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/StorageVolumeChecker.java
index fe61a10d59..d9869894b2 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/StorageVolumeChecker.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/StorageVolumeChecker.java
@@ -318,6 +318,7 @@ public class StorageVolumeChecker {
switch (result) {
case HEALTHY:
case DEGRADED:
+ // Ozone does not currently use this state.
if (LOG.isDebugEnabled()) {
LOG.debug("Volume {} is {}.", volume, result);
}
@@ -343,8 +344,12 @@ public class StorageVolumeChecker {
t.getCause() : t;
LOG.warn("Exception running disk checks against volume {}",
volume, exception);
- markFailed();
- cleanup();
+ // If the scan was interrupted, do not count it as a volume failure.
+ // This should only happen if the volume checker is being shut down.
+ if (!(t instanceof InterruptedException)) {
+ markFailed();
+ cleanup();
+ }
}
private void markHealthy() {
diff --git
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/utils/TestDiskCheckUtil.java
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/utils/TestDiskCheckUtil.java
new file mode 100644
index 0000000000..701d13d81f
--- /dev/null
+++
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/utils/TestDiskCheckUtil.java
@@ -0,0 +1,88 @@
+/*
+ * 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.ozone.container.common.utils;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+/**
+ * Tests {@link DiskCheckUtil} does not incorrectly identify an unhealthy
+ * disk or mount point.
+ * Tests that it identifies an improperly configured directory mount point.
+ *
+ */
+public class TestDiskCheckUtil {
+ @Rule
+ public TemporaryFolder tempTestDir = new TemporaryFolder();
+
+ private File testDir;
+
+ @Before
+ public void setup() {
+ testDir = tempTestDir.getRoot();
+ }
+
+ @Test
+ public void testPermissions() {
+ // Ensure correct test setup before testing the disk check.
+ Assert.assertTrue(testDir.canRead());
+ Assert.assertTrue(testDir.canWrite());
+ Assert.assertTrue(testDir.canExecute());
+ Assert.assertTrue(DiskCheckUtil.checkPermissions(testDir));
+
+ // Test failure without read permissiosns.
+ Assert.assertTrue(testDir.setReadable(false));
+ Assert.assertFalse(DiskCheckUtil.checkPermissions(testDir));
+ Assert.assertTrue(testDir.setReadable(true));
+
+ // Test failure without write permissiosns.
+ Assert.assertTrue(testDir.setWritable(false));
+ Assert.assertFalse(DiskCheckUtil.checkPermissions(testDir));
+ Assert.assertTrue(testDir.setWritable(true));
+
+ // Test failure without execute permissiosns.
+ Assert.assertTrue(testDir.setExecutable(false));
+ Assert.assertFalse(DiskCheckUtil.checkPermissions(testDir));
+ Assert.assertTrue(testDir.setExecutable(true));
+ }
+
+ @Test
+ public void testExistence() {
+ // Ensure correct test setup before testing the disk check.
+ Assert.assertTrue(testDir.exists());
+ Assert.assertTrue(DiskCheckUtil.checkExistence(testDir));
+
+ Assert.assertTrue(testDir.delete());
+ Assert.assertFalse(DiskCheckUtil.checkExistence(testDir));
+ }
+
+ @Test
+ public void testReadWrite() {
+ Assert.assertTrue(DiskCheckUtil.checkReadWrite(testDir, testDir, 10));
+
+ // Test file should have been deleted.
+ File[] children = testDir.listFiles();
+ Assert.assertNotNull(children);
+ Assert.assertEquals(0, children.length);
+ }
+}
diff --git
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestHddsVolume.java
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestHddsVolume.java
index 53da489a3a..d02b5733d5 100644
---
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestHddsVolume.java
+++
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestHddsVolume.java
@@ -24,6 +24,7 @@ import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
+import org.apache.commons.io.FileUtils;
import org.apache.hadoop.fs.StorageType;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.conf.StorageSize;
@@ -32,6 +33,7 @@ import org.apache.hadoop.hdds.fs.SpaceUsageCheckFactory;
import org.apache.hadoop.hdds.fs.SpaceUsagePersistence;
import org.apache.hadoop.hdds.fs.SpaceUsageSource;
import org.apache.hadoop.hdds.scm.ScmConfigKeys;
+import org.apache.hadoop.hdfs.server.datanode.checker.VolumeCheckResult;
import org.apache.hadoop.ozone.OzoneConfigKeys;
import static org.apache.hadoop.hdds.fs.MockSpaceUsagePersistence.inMemory;
@@ -119,26 +121,29 @@ public class TestHddsVolume {
// All temp directories should have been created.
assertTrue(volume.getTmpDir().exists());
assertTrue(volume.getDeletedContainerDir().exists());
+ assertTrue(volume.getDiskCheckDir().exists());
volume.shutdown();
// tmp directories should still exist after shutdown. This is not
// checking their contents.
assertTrue(volume.getTmpDir().exists());
assertTrue(volume.getDeletedContainerDir().exists());
+ assertTrue(volume.getDiskCheckDir().exists());
}
@Test
- public void testClearVolumeTmpDirs() throws Exception {
+ public void testClearDeletedContainersDir() throws Exception {
// Set up volume.
HddsVolume volume = volumeBuilder.build();
volume.format(CLUSTER_ID);
File tmpDir = volume.getHddsRootDir().toPath()
.resolve(Paths.get(CLUSTER_ID, StorageVolume.TMP_DIR_NAME)).toFile();
- File tmpDeleteDir = new File(tmpDir,
- HddsVolume.TMP_CONTAINER_DELETE_DIR_NAME);
+
// Simulate a container that failed to delete fully from the deleted
// containers directory.
+ File tmpDeleteDir = new File(tmpDir,
+ HddsVolume.TMP_CONTAINER_DELETE_DIR_NAME);
File leftoverContainer = new File(tmpDeleteDir, "1");
assertTrue(leftoverContainer.mkdirs());
@@ -162,6 +167,42 @@ public class TestHddsVolume {
assertTrue(tmpDeleteDir.exists());
}
+ @Test
+ public void testClearVolumeHealthCheckDir() throws Exception {
+ // Set up volume.
+ HddsVolume volume = volumeBuilder.build();
+ volume.format(CLUSTER_ID);
+
+ File tmpDir = volume.getHddsRootDir().toPath()
+ .resolve(Paths.get(CLUSTER_ID, StorageVolume.TMP_DIR_NAME)).toFile();
+
+ // Simulate a leftover disk check file that failed to delete.
+ File tmpDiskCheckDir = new File(tmpDir,
+ StorageVolume.TMP_DISK_CHECK_DIR_NAME);
+ assertTrue(tmpDiskCheckDir.mkdirs());
+ File leftoverDiskCheckFile = new File(tmpDiskCheckDir, "diskcheck");
+ assertTrue(leftoverDiskCheckFile.createNewFile());
+
+ // Check that tmp dirs are created with expected names.
+ volume.createWorkingDir(CLUSTER_ID, null);
+ volume.createTmpDirs(CLUSTER_ID);
+ assertEquals(tmpDir, volume.getTmpDir());
+ assertEquals(tmpDiskCheckDir, volume.getDiskCheckDir());
+
+ // Cleanup should have removed the leftover disk check file without
+ // removing the directory itself.
+ assertFalse(leftoverDiskCheckFile.exists());
+ assertTrue(tmpDiskCheckDir.exists());
+
+ // Re-create the disk check file
+ assertTrue(leftoverDiskCheckFile.createNewFile());
+
+ volume.shutdown();
+ // It should be cleared again on shutdown.
+ assertFalse(leftoverDiskCheckFile.exists());
+ assertTrue(tmpDiskCheckDir.exists());
+ }
+
@Test
public void testShutdown() throws Exception {
long initialUsedSpace = 250;
@@ -446,6 +487,25 @@ public class TestHddsVolume {
}
}
+ @Test
+ public void testDBDirFailureDetected() throws Exception {
+ HddsVolume volume = volumeBuilder.build();
+ volume.format(CLUSTER_ID);
+ volume.createWorkingDir(CLUSTER_ID, null);
+ volume.createTmpDirs(CLUSTER_ID);
+
+ VolumeCheckResult result = volume.check(false);
+ assertEquals(VolumeCheckResult.HEALTHY, result);
+
+ File dbFile = new File(volume.getDbParentDir(), CONTAINER_DB_NAME);
+ FileUtils.deleteDirectory(dbFile);
+
+ result = volume.check(false);
+ assertEquals(VolumeCheckResult.FAILED, result);
+
+ volume.shutdown();
+ }
+
private MutableVolumeSet createDbVolumeSet() throws IOException {
File dbVolumeDir = folder.newFolder();
CONF.set(OzoneConfigKeys.HDDS_DATANODE_CONTAINER_DB_DIR,
diff --git
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestStorageVolume.java
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestStorageVolume.java
index 5f015204fa..74469c78b5 100644
---
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestStorageVolume.java
+++
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestStorageVolume.java
@@ -16,9 +16,12 @@
*/
package org.apache.hadoop.ozone.container.common.volume;
+import org.apache.hadoop.hdfs.server.datanode.checker.VolumeCheckResult;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.fs.MockSpaceUsageCheckFactory;
import org.apache.hadoop.ozone.container.common.helpers.DatanodeVersionFile;
+import
org.apache.hadoop.ozone.container.common.statemachine.DatanodeConfiguration;
+import org.apache.hadoop.ozone.container.common.utils.DiskCheckUtil;
import org.apache.hadoop.ozone.container.common.utils.StorageVolumeUtil;
import org.junit.Before;
import org.junit.Rule;
@@ -30,6 +33,7 @@ import java.util.Properties;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
/**
* Test for StorageVolume.
@@ -46,6 +50,15 @@ public class TestStorageVolume {
private HddsVolume.Builder volumeBuilder;
private File versionFile;
+ private static final DiskCheckUtil.DiskChecks IO_FAILURE =
+ new DiskCheckUtil.DiskChecks() {
+ @Override
+ public boolean checkReadWrite(File storageDir, File testFileDir,
+ int numBytesToWrite) {
+ return false;
+ }
+ };
+
@Before
public void setup() throws Exception {
File rootDir = new File(folder.getRoot(), HddsVolume.HDDS_VOLUME_DIR);
@@ -54,6 +67,7 @@ public class TestStorageVolume {
.conf(CONF)
.usageCheckFactory(MockSpaceUsageCheckFactory.NONE);
versionFile = StorageVolumeUtil.getVersionFile(rootDir);
+ DiskCheckUtil.clearTestImpl();
}
@Test
@@ -80,4 +94,175 @@ public class TestStorageVolume {
assertEquals(volume.getCTime(), cTime);
assertEquals(volume.getLayoutVersion(), layoutVersion);
}
+
+ @Test
+ public void testCheckExistence() throws Exception {
+ HddsVolume volume = volumeBuilder.build();
+ volume.format(CLUSTER_ID);
+
+ VolumeCheckResult result = volume.check(false);
+ assertEquals(VolumeCheckResult.HEALTHY, result);
+
+ final DiskCheckUtil.DiskChecks doesNotExist =
+ new DiskCheckUtil.DiskChecks() {
+ @Override
+ public boolean checkExistence(File storageDir) {
+ return false;
+ }
+ };
+
+ DiskCheckUtil.setTestImpl(doesNotExist);
+ result = volume.check(false);
+ assertEquals(VolumeCheckResult.FAILED, result);
+ }
+
+ @Test
+ public void testCheckPermissions() throws Exception {
+ HddsVolume volume = volumeBuilder.build();
+ volume.format(CLUSTER_ID);
+
+ VolumeCheckResult result = volume.check(false);
+ assertEquals(VolumeCheckResult.HEALTHY, result);
+
+ final DiskCheckUtil.DiskChecks noPermissions =
+ new DiskCheckUtil.DiskChecks() {
+ @Override
+ public boolean checkPermissions(File storageDir) {
+ return false;
+ }
+ };
+
+ DiskCheckUtil.setTestImpl(noPermissions);
+ result = volume.check(false);
+ assertEquals(VolumeCheckResult.FAILED, result);
+ }
+
+ /**
+ * Setting test count to 0 should disable IO tests.
+ */
+ @Test
+ public void testCheckIODisabled() throws Exception {
+ DatanodeConfiguration dnConf = CONF.getObject(DatanodeConfiguration.class);
+ dnConf.setVolumeIOTestCount(0);
+ CONF.setFromObject(dnConf);
+ volumeBuilder.conf(CONF);
+ HddsVolume volume = volumeBuilder.build();
+ volume.format(CLUSTER_ID);
+
+ DiskCheckUtil.setTestImpl(IO_FAILURE);
+ assertEquals(VolumeCheckResult.HEALTHY, volume.check(false));
+ }
+
+ @Test
+ public void testCheckIODefaultConfigs() {
+ CONF.clear();
+ DatanodeConfiguration dnConf = CONF.getObject(DatanodeConfiguration.class);
+ // Make sure default values are not invalid.
+ assertTrue(dnConf.getVolumeIOFailureTolerance() <
+ dnConf.getVolumeIOTestCount());
+ }
+
+ @Test
+ public void testCheckIOInvalidConfig() throws Exception {
+ HddsVolume volume = volumeBuilder.build();
+ volume.format(CLUSTER_ID);
+ DatanodeConfiguration dnConf = CONF.getObject(DatanodeConfiguration.class);
+
+ // When failure tolerance is above test count, default values should be
+ // used.
+ dnConf.setVolumeIOTestCount(3);
+ dnConf.setVolumeIOFailureTolerance(4);
+ CONF.setFromObject(dnConf);
+ dnConf = CONF.getObject(DatanodeConfiguration.class);
+ assertEquals(dnConf.getVolumeIOTestCount(),
+ DatanodeConfiguration.DISK_CHECK_IO_TEST_COUNT_DEFAULT);
+ assertEquals(dnConf.getVolumeIOFailureTolerance(),
+ DatanodeConfiguration.DISK_CHECK_IO_FAILURES_TOLERATED_DEFAULT);
+
+ // When test count and failure tolerance are set to the same value,
+ // Default values should be used.
+ dnConf.setVolumeIOTestCount(2);
+ dnConf.setVolumeIOFailureTolerance(2);
+ CONF.setFromObject(dnConf);
+ dnConf = CONF.getObject(DatanodeConfiguration.class);
+ assertEquals(DatanodeConfiguration.DISK_CHECK_IO_TEST_COUNT_DEFAULT,
+ dnConf.getVolumeIOTestCount());
+
assertEquals(DatanodeConfiguration.DISK_CHECK_IO_FAILURES_TOLERATED_DEFAULT,
+ dnConf.getVolumeIOFailureTolerance());
+
+ // Negative test count should reset to default value.
+ dnConf.setVolumeIOTestCount(-1);
+ CONF.setFromObject(dnConf);
+ dnConf = CONF.getObject(DatanodeConfiguration .class);
+ assertEquals(DatanodeConfiguration.DISK_CHECK_IO_TEST_COUNT_DEFAULT,
+ dnConf.getVolumeIOTestCount());
+
+ // Negative failure tolerance should reset to default value.
+ dnConf.setVolumeIOFailureTolerance(-1);
+ CONF.setFromObject(dnConf);
+ dnConf = CONF.getObject(DatanodeConfiguration .class);
+
assertEquals(DatanodeConfiguration.DISK_CHECK_IO_FAILURES_TOLERATED_DEFAULT,
+ dnConf.getVolumeIOFailureTolerance());
+ }
+
+ @Test
+ public void testCheckIOInitiallyPassing() throws Exception {
+ testCheckIOUntilFailure(3, 1, true, true, true, false, true, false);
+ }
+
+ @Test
+ public void testCheckIOEarlyFailure() throws Exception {
+ testCheckIOUntilFailure(3, 1, false, false);
+ }
+
+ @Test
+ public void testCheckIOFailuresDiscarded() throws Exception {
+ testCheckIOUntilFailure(3, 1, false, true, true, true, false, false);
+ }
+
+ @Test
+ public void testCheckIOAlternatingFailures() throws Exception {
+ testCheckIOUntilFailure(3, 1, true, false, true, false);
+ }
+
+ /**
+ * Helper method to test the sliding window of IO checks before volume
+ * failure.
+ *
+ * @param ioTestCount The number of most recent tests whose results should
+ * be considered.
+ * @param ioFailureTolerance The number of IO failures tolerated out of the
+ * last {@param ioTestCount} tests.
+ * @param checkResults The result of the IO check for each run. Volume
+ * should fail after the last IO check is completed.
+ */
+ private void testCheckIOUntilFailure(int ioTestCount, int ioFailureTolerance,
+ boolean... checkResults) throws Exception {
+ DatanodeConfiguration dnConf = CONF.getObject(DatanodeConfiguration.class);
+ dnConf.setVolumeIOTestCount(ioTestCount);
+ dnConf.setVolumeIOFailureTolerance(ioFailureTolerance);
+ CONF.setFromObject(dnConf);
+ volumeBuilder.conf(CONF);
+ HddsVolume volume = volumeBuilder.build();
+ volume.format(CLUSTER_ID);
+
+ for (int i = 0; i < checkResults.length; i++) {
+ final boolean result = checkResults[i];
+ final DiskCheckUtil.DiskChecks ioResult = new DiskCheckUtil.DiskChecks()
{
+ @Override
+ public boolean checkReadWrite(File storageDir, File testDir,
+ int numBytesToWrite) {
+ return result;
+ }
+ };
+ DiskCheckUtil.setTestImpl(ioResult);
+ if (i < checkResults.length - 1) {
+ assertEquals("Unexpected IO failure in run " + i,
+ VolumeCheckResult.HEALTHY, volume.check(false));
+ } else {
+ assertEquals("Unexpected IO success in run " + i,
+ VolumeCheckResult.FAILED, volume.check(false));
+ }
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]