Copilot commented on code in PR #10496:
URL: https://github.com/apache/ozone/pull/10496#discussion_r3401451079
##########
hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java:
##########
@@ -391,6 +392,11 @@ public ContainerChecksumTreeManager getChecksumManager() {
return this.checksumManager;
}
+ @VisibleForTesting
+ public void setBlockInputStreamFactory(BlockInputStreamFactoryImpl factory) {
+ this.blockInputStreamFactory = factory;
+ }
Review Comment:
The new test-only setter accepts null and is declared public, which expands
the production API surface unnecessarily and can lead to a delayed NPE when
reconciliation runs. Since it is newly added and only intended for tests,
prefer package-private visibility and eagerly null-check the argument.
##########
hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestReconcileChunksPerBlockHoleBcsId.java:
##########
@@ -0,0 +1,260 @@
+/*
+ * 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.keyvalue;
+
+import static org.apache.hadoop.hdds.HddsConfigKeys.OZONE_METADATA_DIRS;
+import static
org.apache.hadoop.hdds.protocol.MockDatanodeDetails.randomDatanodeDetails;
+import static org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_DATANODE_DIR_KEY;
+import static
org.apache.hadoop.ozone.container.common.ContainerTestUtils.WRITE_STAGE;
+import static
org.apache.hadoop.ozone.container.common.ContainerTestUtils.createDbInstancesForTestIfNeeded;
+import static
org.apache.hadoop.ozone.container.common.impl.ContainerImplTestUtils.newContainerSet;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import org.apache.hadoop.hdds.client.BlockID;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.scm.storage.BlockInputStream;
+import org.apache.hadoop.hdds.scm.storage.ChunkInputStream;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.client.io.BlockInputStreamFactoryImpl;
+import org.apache.hadoop.ozone.common.Checksum;
+import org.apache.hadoop.ozone.common.ChecksumData;
+import org.apache.hadoop.ozone.container.checksum.ContainerMerkleTreeWriter;
+import org.apache.hadoop.ozone.container.checksum.DNContainerOperationClient;
+import org.apache.hadoop.ozone.container.common.ContainerTestUtils;
+import org.apache.hadoop.ozone.container.common.helpers.BlockData;
+import org.apache.hadoop.ozone.container.common.helpers.ChunkInfo;
+import org.apache.hadoop.ozone.container.common.impl.ContainerSet;
+import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
+import org.apache.hadoop.ozone.container.common.volume.StorageVolume;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Regression test for the BCSID high-water bug on the holed-block
reconciliation path
+ * (KeyValueHandler.reconcileChunksPerBlock).
+ *
+ * <p>Scenario reproduced here:
+ * <ul>
+ * <li>A closed local replica holds block L with only the offset-0 chunk;
its block and container
+ * blockCommitSequenceId (BCSID) are both 1.</li>
+ * <li>A peer is ahead at BCSID 99 and advertises a chunk merkle list
{CHUNK_LEN, 3*CHUNK_LEN}.
+ * The chunk at 2*CHUNK_LEN is absent, so 3*CHUNK_LEN sits past a hole.
(A peer's scanner
+ * legitimately omits missing chunks from its merkle tree, so a healthy
peer can advertise a
+ * gapped list.)</li>
+ * </ul>
+ *
+ * <p>Reconciliation ingests the chunk at CHUNK_LEN (its predecessor, offset
0, is present locally),
+ * then reaches 3*CHUNK_LEN whose predecessor 2*CHUNK_LEN is missing and stops
at the hole break.
+ * The block is therefore incomplete. The method contract states the BCSID is
advanced to the peer's
+ * value only when the entire block is read and written successfully, so on a
holed repair the
+ * BCSID must stay at the local value.
+ *
+ * <p>This test mocks only the peer side (the BlockInputStream and its single
served chunk) and
+ * exercises the real reconcileChunksPerBlock against a real closed container.
Before the fix the
+ * BCSID is advanced to 99 and the assertions below fail; after the fix the
BCSID stays at 1.
+ */
+public class TestReconcileChunksPerBlockHoleBcsId {
+
+ @TempDir
+ private Path tempDir;
+
+ private static final String CLUSTER_ID = UUID.randomUUID().toString();
+ private static final long CONTAINER_ID = 100L;
+ private static final long LOCAL_ID = 0L;
+ // 2 KiB chunks so the offsets line up with the description: ingested chunk
at 2048,
+ // hole at 4096, skipped chunk past the hole at 6144.
+ private static final int CHUNK_LEN = 2 * (int) OzoneConsts.KB;
+ private static final int BYTES_PER_CHECKSUM = 2 * (int) OzoneConsts.KB;
+ private static final long LOCAL_BCSID = 1L;
+ private static final long PEER_BCSID = 99L;
+
+ private OzoneConfiguration conf;
+ private ContainerSet containerSet;
+ private KeyValueHandler handler;
+ private KeyValueContainer container;
+ private DNContainerOperationClient dnClient;
+ private Pipeline peerPipeline;
+
+ @BeforeEach
+ public void setup() throws Exception {
+ conf = new OzoneConfiguration();
+ Path dataVolume = Paths.get(tempDir.toString(), "data");
+ Path metadataVolume = Paths.get(tempDir.toString(), "metadata");
+ conf.set(HDDS_DATANODE_DIR_KEY, dataVolume.toString());
+ conf.set(OZONE_METADATA_DIRS, metadataVolume.toString());
+
+ containerSet = newContainerSet();
+ DatanodeDetails localDn = randomDatanodeDetails();
+ MutableVolumeSet volumeSet = new MutableVolumeSet(localDn.getUuidString(),
conf, null,
+ StorageVolume.VolumeType.DATA_VOLUME, null);
+ createDbInstancesForTestIfNeeded(volumeSet, CLUSTER_ID, CLUSTER_ID, conf);
+
+ handler = ContainerTestUtils.getKeyValueHandler(conf,
localDn.getUuidString(), containerSet, volumeSet,
+ new
org.apache.hadoop.ozone.container.checksum.ContainerChecksumTreeManager(conf));
+ handler.setClusterID(CLUSTER_ID);
+
+ container = createClosedContainerWithOffsetZeroChunk();
+
+ dnClient = new DNContainerOperationClient(conf, null, null);
+ peerPipeline = singleNodePipeline(randomDatanodeDetails());
+ }
+
+ /**
+ * Builds a real closed container holding block L with a single chunk at
offset 0, BCSID 1.
+ */
+ private KeyValueContainer createClosedContainerWithOffsetZeroChunk() throws
Exception {
+ ContainerProtos.ContainerCommandRequestProto createRequest =
+ ContainerProtos.ContainerCommandRequestProto.newBuilder()
+ .setCmdType(ContainerProtos.Type.CreateContainer)
+ .setContainerID(CONTAINER_ID)
+ .setDatanodeUuid(UUID.randomUUID().toString())
+
.setCreateContainer(ContainerProtos.CreateContainerRequestProto.newBuilder()
+
.setContainerType(ContainerProtos.ContainerType.KeyValueContainer)
+ .build())
+ .build();
+ handler.handleCreateContainer(createRequest, null);
+ KeyValueContainer kvContainer =
+ (KeyValueContainer) containerSet.getContainer(CONTAINER_ID);
+
+ BlockID blockID = new BlockID(CONTAINER_ID, LOCAL_ID);
+ byte[] chunkData = new byte[CHUNK_LEN];
+ Arrays.fill(chunkData, (byte) 'a');
+
+ ChunkInfo chunkAtZero = new ChunkInfo("chunk0", 0, CHUNK_LEN);
+ chunkAtZero.setChecksumData(checksumOf(chunkData));
+ handler.getChunkManager().writeChunk(kvContainer, blockID, chunkAtZero,
+ ByteBuffer.wrap(chunkData), WRITE_STAGE);
+ handler.getChunkManager().finishWriteChunks(kvContainer, new
BlockData(blockID));
+
+ BlockData blockData = new BlockData(blockID);
+
blockData.setChunks(Collections.singletonList(chunkAtZero.getProtoBufMessage()));
+ blockData.setBlockCommitSequenceId(LOCAL_BCSID);
+ handler.getBlockManager().putBlock(kvContainer, blockData);
+
+ kvContainer.markContainerForClose();
+ handler.closeContainer(kvContainer);
+ return kvContainer;
+ }
+
+ @Test
+ public void holeExitMustNotAdvanceBcsIdToPeerValue() throws Exception {
+ // Precondition: local replica is at BCSID 1, well below the peer's 99.
+ BlockData localBefore = handler.getBlockManager().getBlock(container, new
BlockID(CONTAINER_ID, LOCAL_ID));
+ assertEquals(LOCAL_BCSID, localBefore.getBlockCommitSequenceId());
+ assertEquals(LOCAL_BCSID,
container.getContainerData().getBlockCommitSequenceId());
+
+ // The peer advertises a merkle list with a hole: {CHUNK_LEN,
3*CHUNK_LEN}. 2*CHUNK_LEN is omitted,
+ // so 3*CHUNK_LEN sits past a hole relative to what the local replica can
place contiguously.
+ List<ContainerProtos.ChunkMerkleTree> peerChunkList = Arrays.asList(
+ chunkMerkleTree(CHUNK_LEN),
+ chunkMerkleTree(3L * CHUNK_LEN));
+
+ // Mock only the peer side. getStreamBlockData advertises BCSID 99; the
single chunk stream serves the
+ // contiguous chunk at CHUNK_LEN that reconciliation ingests before it
reaches the hole.
+ installMockedPeerStream();
+
+ ByteBuffer chunkByteBuffer = ByteBuffer.allocate(CHUNK_LEN);
+ handler.reconcileChunksPerBlock(container, peerPipeline, dnClient,
LOCAL_ID, peerChunkList,
+ new ContainerMerkleTreeWriter(), chunkByteBuffer);
+
+ // A hole remains, so the block is incomplete: the BCSID must not be
advanced to the peer's value.
+ BlockData localAfter = handler.getBlockManager().getBlock(container, new
BlockID(CONTAINER_ID, LOCAL_ID));
+ assertNotEquals(PEER_BCSID, localAfter.getBlockCommitSequenceId(),
+ "block BCSID was advanced to the peer value (" + PEER_BCSID + ") even
though the chunk at offset "
+ + (3L * CHUNK_LEN) + " past the hole at offset " + (2L *
CHUNK_LEN) + " was never ingested");
+ assertNotEquals(PEER_BCSID,
container.getContainerData().getBlockCommitSequenceId(),
+ "container BCSID was advanced to the peer value (" + PEER_BCSID + ")
on a holed, incomplete block");
Review Comment:
Same as above: this should assert the container BCSID remains at the local
value after a holed reconciliation, rather than only asserting it isn't equal
to the peer value.
##########
hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestReconcileChunksPerBlockHoleBcsId.java:
##########
@@ -0,0 +1,260 @@
+/*
+ * 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.keyvalue;
+
+import static org.apache.hadoop.hdds.HddsConfigKeys.OZONE_METADATA_DIRS;
+import static
org.apache.hadoop.hdds.protocol.MockDatanodeDetails.randomDatanodeDetails;
+import static org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_DATANODE_DIR_KEY;
+import static
org.apache.hadoop.ozone.container.common.ContainerTestUtils.WRITE_STAGE;
+import static
org.apache.hadoop.ozone.container.common.ContainerTestUtils.createDbInstancesForTestIfNeeded;
+import static
org.apache.hadoop.ozone.container.common.impl.ContainerImplTestUtils.newContainerSet;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import org.apache.hadoop.hdds.client.BlockID;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.scm.storage.BlockInputStream;
+import org.apache.hadoop.hdds.scm.storage.ChunkInputStream;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.client.io.BlockInputStreamFactoryImpl;
+import org.apache.hadoop.ozone.common.Checksum;
+import org.apache.hadoop.ozone.common.ChecksumData;
+import org.apache.hadoop.ozone.container.checksum.ContainerMerkleTreeWriter;
+import org.apache.hadoop.ozone.container.checksum.DNContainerOperationClient;
+import org.apache.hadoop.ozone.container.common.ContainerTestUtils;
+import org.apache.hadoop.ozone.container.common.helpers.BlockData;
+import org.apache.hadoop.ozone.container.common.helpers.ChunkInfo;
+import org.apache.hadoop.ozone.container.common.impl.ContainerSet;
+import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
+import org.apache.hadoop.ozone.container.common.volume.StorageVolume;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Regression test for the BCSID high-water bug on the holed-block
reconciliation path
+ * (KeyValueHandler.reconcileChunksPerBlock).
+ *
+ * <p>Scenario reproduced here:
+ * <ul>
+ * <li>A closed local replica holds block L with only the offset-0 chunk;
its block and container
+ * blockCommitSequenceId (BCSID) are both 1.</li>
+ * <li>A peer is ahead at BCSID 99 and advertises a chunk merkle list
{CHUNK_LEN, 3*CHUNK_LEN}.
+ * The chunk at 2*CHUNK_LEN is absent, so 3*CHUNK_LEN sits past a hole.
(A peer's scanner
+ * legitimately omits missing chunks from its merkle tree, so a healthy
peer can advertise a
+ * gapped list.)</li>
+ * </ul>
+ *
+ * <p>Reconciliation ingests the chunk at CHUNK_LEN (its predecessor, offset
0, is present locally),
+ * then reaches 3*CHUNK_LEN whose predecessor 2*CHUNK_LEN is missing and stops
at the hole break.
+ * The block is therefore incomplete. The method contract states the BCSID is
advanced to the peer's
+ * value only when the entire block is read and written successfully, so on a
holed repair the
+ * BCSID must stay at the local value.
+ *
+ * <p>This test mocks only the peer side (the BlockInputStream and its single
served chunk) and
+ * exercises the real reconcileChunksPerBlock against a real closed container.
Before the fix the
+ * BCSID is advanced to 99 and the assertions below fail; after the fix the
BCSID stays at 1.
+ */
+public class TestReconcileChunksPerBlockHoleBcsId {
+
+ @TempDir
+ private Path tempDir;
+
+ private static final String CLUSTER_ID = UUID.randomUUID().toString();
+ private static final long CONTAINER_ID = 100L;
+ private static final long LOCAL_ID = 0L;
+ // 2 KiB chunks so the offsets line up with the description: ingested chunk
at 2048,
+ // hole at 4096, skipped chunk past the hole at 6144.
+ private static final int CHUNK_LEN = 2 * (int) OzoneConsts.KB;
+ private static final int BYTES_PER_CHECKSUM = 2 * (int) OzoneConsts.KB;
+ private static final long LOCAL_BCSID = 1L;
+ private static final long PEER_BCSID = 99L;
+
+ private OzoneConfiguration conf;
+ private ContainerSet containerSet;
+ private KeyValueHandler handler;
+ private KeyValueContainer container;
+ private DNContainerOperationClient dnClient;
+ private Pipeline peerPipeline;
+
+ @BeforeEach
+ public void setup() throws Exception {
+ conf = new OzoneConfiguration();
+ Path dataVolume = Paths.get(tempDir.toString(), "data");
+ Path metadataVolume = Paths.get(tempDir.toString(), "metadata");
+ conf.set(HDDS_DATANODE_DIR_KEY, dataVolume.toString());
+ conf.set(OZONE_METADATA_DIRS, metadataVolume.toString());
+
+ containerSet = newContainerSet();
+ DatanodeDetails localDn = randomDatanodeDetails();
+ MutableVolumeSet volumeSet = new MutableVolumeSet(localDn.getUuidString(),
conf, null,
+ StorageVolume.VolumeType.DATA_VOLUME, null);
+ createDbInstancesForTestIfNeeded(volumeSet, CLUSTER_ID, CLUSTER_ID, conf);
+
+ handler = ContainerTestUtils.getKeyValueHandler(conf,
localDn.getUuidString(), containerSet, volumeSet,
+ new
org.apache.hadoop.ozone.container.checksum.ContainerChecksumTreeManager(conf));
+ handler.setClusterID(CLUSTER_ID);
+
+ container = createClosedContainerWithOffsetZeroChunk();
+
+ dnClient = new DNContainerOperationClient(conf, null, null);
+ peerPipeline = singleNodePipeline(randomDatanodeDetails());
+ }
+
+ /**
+ * Builds a real closed container holding block L with a single chunk at
offset 0, BCSID 1.
+ */
+ private KeyValueContainer createClosedContainerWithOffsetZeroChunk() throws
Exception {
+ ContainerProtos.ContainerCommandRequestProto createRequest =
+ ContainerProtos.ContainerCommandRequestProto.newBuilder()
+ .setCmdType(ContainerProtos.Type.CreateContainer)
+ .setContainerID(CONTAINER_ID)
+ .setDatanodeUuid(UUID.randomUUID().toString())
+
.setCreateContainer(ContainerProtos.CreateContainerRequestProto.newBuilder()
+
.setContainerType(ContainerProtos.ContainerType.KeyValueContainer)
+ .build())
+ .build();
+ handler.handleCreateContainer(createRequest, null);
+ KeyValueContainer kvContainer =
+ (KeyValueContainer) containerSet.getContainer(CONTAINER_ID);
+
+ BlockID blockID = new BlockID(CONTAINER_ID, LOCAL_ID);
+ byte[] chunkData = new byte[CHUNK_LEN];
+ Arrays.fill(chunkData, (byte) 'a');
+
+ ChunkInfo chunkAtZero = new ChunkInfo("chunk0", 0, CHUNK_LEN);
+ chunkAtZero.setChecksumData(checksumOf(chunkData));
+ handler.getChunkManager().writeChunk(kvContainer, blockID, chunkAtZero,
+ ByteBuffer.wrap(chunkData), WRITE_STAGE);
+ handler.getChunkManager().finishWriteChunks(kvContainer, new
BlockData(blockID));
+
+ BlockData blockData = new BlockData(blockID);
+
blockData.setChunks(Collections.singletonList(chunkAtZero.getProtoBufMessage()));
+ blockData.setBlockCommitSequenceId(LOCAL_BCSID);
+ handler.getBlockManager().putBlock(kvContainer, blockData);
+
+ kvContainer.markContainerForClose();
+ handler.closeContainer(kvContainer);
+ return kvContainer;
+ }
+
+ @Test
+ public void holeExitMustNotAdvanceBcsIdToPeerValue() throws Exception {
+ // Precondition: local replica is at BCSID 1, well below the peer's 99.
+ BlockData localBefore = handler.getBlockManager().getBlock(container, new
BlockID(CONTAINER_ID, LOCAL_ID));
+ assertEquals(LOCAL_BCSID, localBefore.getBlockCommitSequenceId());
+ assertEquals(LOCAL_BCSID,
container.getContainerData().getBlockCommitSequenceId());
+
+ // The peer advertises a merkle list with a hole: {CHUNK_LEN,
3*CHUNK_LEN}. 2*CHUNK_LEN is omitted,
+ // so 3*CHUNK_LEN sits past a hole relative to what the local replica can
place contiguously.
+ List<ContainerProtos.ChunkMerkleTree> peerChunkList = Arrays.asList(
+ chunkMerkleTree(CHUNK_LEN),
+ chunkMerkleTree(3L * CHUNK_LEN));
+
+ // Mock only the peer side. getStreamBlockData advertises BCSID 99; the
single chunk stream serves the
+ // contiguous chunk at CHUNK_LEN that reconciliation ingests before it
reaches the hole.
+ installMockedPeerStream();
+
+ ByteBuffer chunkByteBuffer = ByteBuffer.allocate(CHUNK_LEN);
+ handler.reconcileChunksPerBlock(container, peerPipeline, dnClient,
LOCAL_ID, peerChunkList,
+ new ContainerMerkleTreeWriter(), chunkByteBuffer);
+
+ // A hole remains, so the block is incomplete: the BCSID must not be
advanced to the peer's value.
+ BlockData localAfter = handler.getBlockManager().getBlock(container, new
BlockID(CONTAINER_ID, LOCAL_ID));
+ assertNotEquals(PEER_BCSID, localAfter.getBlockCommitSequenceId(),
+ "block BCSID was advanced to the peer value (" + PEER_BCSID + ") even
though the chunk at offset "
+ + (3L * CHUNK_LEN) + " past the hole at offset " + (2L *
CHUNK_LEN) + " was never ingested");
Review Comment:
These assertions only check that the BCSID did not become the peer value,
which could still pass if the BCSID changes to some other incorrect value (eg
0). Since the expected behavior is that the block remains at the local BCSID
when reconciliation stops at a hole, assert the exact expected value.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]