blob: 22f8e05b5a59d3877cb441fe2632d64789b63100 [file] [log] [blame]
/*
* 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.jackrabbit.oak.run;
import static com.google.common.base.Charsets.UTF_8;
import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import joptsimple.internal.Strings;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.felix.cm.file.ConfigurationHandler;
import org.apache.jackrabbit.core.data.DataStore;
import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzureConstants;
import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzureDataStoreUtils;
import org.apache.jackrabbit.oak.blob.cloud.s3.S3Constants;
import org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStoreUtils;
import org.apache.jackrabbit.oak.commons.FileIOUtils;
import org.apache.jackrabbit.oak.plugins.blob.datastore.DataStoreBlobStore;
import org.apache.jackrabbit.oak.plugins.blob.datastore.OakFileDataStore;
import org.apache.jackrabbit.oak.segment.SegmentBlob;
import org.apache.jackrabbit.oak.segment.SegmentNodeStoreBuilders;
import org.apache.jackrabbit.oak.segment.file.FileStore;
import org.apache.jackrabbit.oak.segment.file.FileStoreBuilder;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.jetbrains.annotations.Nullable;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tests for {@link DataStoreCheckCommand}
*/
public class DataStoreCheckTest {
private static final Logger log = LoggerFactory.getLogger(DataStoreCheckTest.class);
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder(new File("target"));
private String storePath;
private Set<String> blobsAdded;
private Map<String, String> blobsAddedWithNodes;
private String cfgFilePath;
private String dsPath;
private DataStoreBlobStore setupDataStore;
private String dsOption;
private String container;
@Before
public void setup() throws Exception {
if (S3DataStoreUtils.isS3Configured()) {
Properties props = S3DataStoreUtils.getS3Config();
props.setProperty("cacheSize", "0");
container = props.getProperty(S3Constants.S3_BUCKET);
container = container + "-" + String.valueOf(System.currentTimeMillis());
props.setProperty(S3Constants.S3_BUCKET, container);
DataStore ds = S3DataStoreUtils.getS3DataStore(S3DataStoreUtils.getFixtures().get(0),
props,
temporaryFolder.newFolder().getAbsolutePath());
setupDataStore = new DataStoreBlobStore(ds);
cfgFilePath = createTempConfig(temporaryFolder.newFile(), props);
dsOption = "s3ds";
} else if (AzureDataStoreUtils.isAzureConfigured()) {
Properties props = AzureDataStoreUtils.getAzureConfig();
props.setProperty("cacheSize", "0");
container = props.getProperty(AzureConstants.AZURE_BLOB_CONTAINER_NAME);
DataStore ds = AzureDataStoreUtils.getAzureDataStore(props,
temporaryFolder.newFolder().getAbsolutePath());
setupDataStore = new DataStoreBlobStore(ds);
cfgFilePath = createTempConfig(temporaryFolder.newFile(), props);
dsOption = "azureblobds";
}
else {
OakFileDataStore delegate = new OakFileDataStore();
dsPath = temporaryFolder.newFolder().getAbsolutePath();
delegate.setPath(dsPath);
delegate.init(null);
setupDataStore = new DataStoreBlobStore(delegate);
File cfgFile = temporaryFolder.newFile();
Properties props = new Properties();
props.put("path", dsPath);
props.put("minRecordLength", new Long(4096));
cfgFilePath = createTempConfig(cfgFile, props);
dsOption = "fds";
}
File storeFile = temporaryFolder.newFolder();
storePath = storeFile.getAbsolutePath();
FileStore fileStore = FileStoreBuilder.fileStoreBuilder(storeFile)
.withBlobStore(setupDataStore)
.withMaxFileSize(256)
.withSegmentCacheSize(64)
.build();
NodeStore store = SegmentNodeStoreBuilders.builder(fileStore).build();
/* Create nodes with blobs stored in DS*/
NodeBuilder a = store.getRoot().builder();
int numBlobs = 10;
blobsAdded = Sets.newHashSet();
blobsAddedWithNodes = Maps.newHashMap();
for (int i = 0; i < numBlobs; i++) {
SegmentBlob b = (SegmentBlob) store.createBlob(randomStream(i, 18342));
Iterator<String> idIter = setupDataStore.resolveChunks(b.getBlobId());
while (idIter.hasNext()) {
String chunk = idIter.next();
blobsAdded.add(chunk);
blobsAddedWithNodes.put(chunk, "/c"+i+"/x");
}
a.child("c" + i).setProperty("x", b);
}
store.merge(a, EmptyHook.INSTANCE, CommitInfo.EMPTY);
log.info("Created blobs : {}", blobsAdded);
fileStore.close();
}
@After
public void tearDown() {
System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err)));
if (!Strings.isNullOrEmpty(container)) {
try {
if (dsOption.equals("s3ds")) {
S3DataStoreUtils.deleteBucket(container, new Date());
} else {
AzureDataStoreUtils.deleteContainer(container);
}
} catch (Exception e) {
log.error("Error in cleaning container", e);
}
}
}
@Test
public void testCorrect() throws Exception {
File dump = temporaryFolder.newFolder();
File repoHome = temporaryFolder.newFolder();
setupDataStore.close();
testAllParams(dump, repoHome);
}
@Test
public void testConsistency() throws Exception {
File dump = temporaryFolder.newFolder();
File repoHome = temporaryFolder.newFolder();
Random rand = new Random();
String deletedBlobId = Iterables.get(blobsAdded, rand.nextInt(blobsAdded.size()));
blobsAdded.remove(deletedBlobId);
long count = setupDataStore.countDeleteChunks(ImmutableList.of(deletedBlobId), 0);
assertEquals(1, count);
setupDataStore.close();
testAllParams(dump, repoHome);
assertFileEquals(dump, "[id]", blobsAdded);
assertFileEquals(dump, "[ref]", Sets.union(blobsAdded, Sets.newHashSet(deletedBlobId)));
assertFileEquals(dump, "[consistency]", Sets.newHashSet(deletedBlobId));
}
@Test
public void testConsistencyVerbose() throws Exception {
File dump = temporaryFolder.newFolder();
File repoHome = temporaryFolder.newFolder();
Random rand = new Random();
String deletedBlobId = Iterables.get(blobsAdded, rand.nextInt(blobsAdded.size()));
blobsAdded.remove(deletedBlobId);
long count = setupDataStore
.countDeleteChunks(ImmutableList.of(deletedBlobId),
0);
assertEquals(1, count);
setupDataStore.close();
testAllParamsVerbose(dump, repoHome);
assertFileEquals(dump, "[id]", encodedIds(blobsAdded, dsOption));
assertFileEquals(dump, "[ref]",
encodedIdsAndPath(Sets.union(blobsAdded, Sets.newHashSet(deletedBlobId)), dsOption, blobsAddedWithNodes));
assertFileEquals(dump, "[consistency]",
encodedIdsAndPath(Sets.newHashSet(deletedBlobId), dsOption, blobsAddedWithNodes));
}
@Test
public void testConsistencyWithDeleteTracker() throws Exception {
File dump = temporaryFolder.newFolder();
File repoHome = temporaryFolder.newFolder();
File trackerFolder = new File(repoHome, "blobids");
FileUtils.forceMkdir(trackerFolder);
File delTracker = new File(trackerFolder, "activedeletions.del");
Random rand = new Random();
String deletedBlobId = Iterables.get(blobsAdded, rand.nextInt(blobsAdded.size()));
blobsAdded.remove(deletedBlobId);
long count = setupDataStore.countDeleteChunks(ImmutableList.of(deletedBlobId), 0);
String activeDeletedBlobId = Iterables.get(blobsAdded, rand.nextInt(blobsAdded.size()));
blobsAdded.remove(activeDeletedBlobId);
count += setupDataStore.countDeleteChunks(ImmutableList.of(activeDeletedBlobId), 0);
assertEquals(2, count);
// artificially put the deleted id in the tracked .del file
FileIOUtils.writeStrings(Iterators.singletonIterator(activeDeletedBlobId), delTracker, false);
setupDataStore.close();
testAllParams(dump, repoHome);
assertFileEquals(dump, "[id]", blobsAdded);
assertFileEquals(dump, "[ref]", Sets.union(blobsAdded, Sets.newHashSet(deletedBlobId, activeDeletedBlobId)));
assertFileEquals(dump, "[consistency]", Sets.newHashSet(deletedBlobId));
}
@Test
public void testConsistencyVerboseWithDeleteTracker() throws Exception {
File dump = temporaryFolder.newFolder();
File repoHome = temporaryFolder.newFolder();
File trackerFolder = new File(repoHome, "blobids");
FileUtils.forceMkdir(trackerFolder);
File delTracker = new File(trackerFolder, "activedeletions.del");
Random rand = new Random();
String deletedBlobId = Iterables.get(blobsAdded, rand.nextInt(blobsAdded.size()));
blobsAdded.remove(deletedBlobId);
long count = setupDataStore.countDeleteChunks(ImmutableList.of(deletedBlobId), 0);
String activeDeletedBlobId = Iterables.get(blobsAdded, rand.nextInt(blobsAdded.size()));
blobsAdded.remove(activeDeletedBlobId);
count += setupDataStore.countDeleteChunks(ImmutableList.of(activeDeletedBlobId), 0);
assertEquals(2, count);
// artificially put the deleted id in the tracked .del file
FileIOUtils.writeStrings(Iterators.singletonIterator(activeDeletedBlobId), delTracker, false);
setupDataStore.close();
testAllParamsVerbose(dump, repoHome);
assertFileEquals(dump, "[id]", encodedIds(blobsAdded, dsOption));
assertFileEquals(dump, "[ref]",
encodedIdsAndPath(Sets.union(blobsAdded, Sets.newHashSet(deletedBlobId, activeDeletedBlobId)), dsOption,
blobsAddedWithNodes));
assertFileEquals(dump, "[consistency]",
encodedIdsAndPath(Sets.newHashSet(deletedBlobId), dsOption, blobsAddedWithNodes));
}
@Test
public void testConsistencyNoDS() throws Exception {
File dump = temporaryFolder.newFolder();
testTarNoDSOption(dump);
assertFileEquals(dump, "[ref]", blobsAdded);
assertFileEquals(dump, "[consistency]", blobsAdded);
}
private void testAllParams(File dump, File repoHome) throws Exception {
List<String> argsList = Lists
.newArrayList("--id", "--ref", "--consistency", "--" + dsOption, cfgFilePath, "--store", storePath,
"--dump", dump.getAbsolutePath(), "--repoHome", repoHome.getAbsolutePath());
DataStoreCheckCommand.checkDataStore(argsList.toArray(new String[0]));
}
private void testAllParamsVerbose(File dump, File repoHome) throws Exception {
List<String> argsList = Lists
.newArrayList("--id", "--ref", "--consistency", "--" + dsOption, cfgFilePath, "--store", storePath,
"--dump", dump.getAbsolutePath(), "--repoHome", repoHome.getAbsolutePath(), "--verbose");
DataStoreCheckCommand.checkDataStore(argsList.toArray(new String[0]));
}
@Test
public void testMissingOpParams() throws Exception {
setupDataStore.close();
File dump = temporaryFolder.newFolder();
List<String> argsList = Lists
.newArrayList("--" + dsOption, cfgFilePath, "--store", storePath,
"--dump", dump.getAbsolutePath());
log.info("Running testMissinOpParams: {}", argsList);
testIncorrectParams(argsList, Lists.newArrayList("Missing required option(s)", "id", "ref", "consistency"));
}
public void testTarNoDSOption(File dump) throws Exception {
List<String> argsList = Lists
.newArrayList("--id", "--ref", "--consistency", "--nods", "--store", storePath,
"--dump", dump.getAbsolutePath(), "--repoHome", temporaryFolder.newFolder().getAbsolutePath());
DataStoreCheckCommand.checkDataStore(argsList.toArray(new String[0]));
}
@Test
public void testTarNoDS() throws Exception {
setupDataStore.close();
File dump = temporaryFolder.newFolder();
List<String> argsList = Lists
.newArrayList("--id", "--ref", "--consistency", "--store", storePath,
"--dump", dump.getAbsolutePath(), "--repoHome", temporaryFolder.newFolder().getAbsolutePath());
testIncorrectParams(argsList, Lists.newArrayList("Operation not defined for SegmentNodeStore without external datastore"));
}
@Test
public void testOpNoStore() throws Exception {
setupDataStore.close();
File dump = temporaryFolder.newFolder();
List<String> argsList = Lists
.newArrayList("--consistency", "--" + dsOption, cfgFilePath,
"--dump", dump.getAbsolutePath(), "--repoHome", temporaryFolder.newFolder().getAbsolutePath());
testIncorrectParams(argsList, Lists.newArrayList("Missing required option(s) [store]"));
argsList = Lists
.newArrayList("--ref", "--" + dsOption, cfgFilePath,
"--dump", dump.getAbsolutePath(), "--repoHome", temporaryFolder.newFolder().getAbsolutePath());
testIncorrectParams(argsList, Lists.newArrayList("Missing required option(s) [store]"));
}
@Test
public void testTrackWithRefs() throws Exception {
setupDataStore.close();
File dump = temporaryFolder.newFolder();
List<String> argsList = Lists
.newArrayList("--ref", "--store", storePath,
"--dump", dump.getAbsolutePath(), "--track", "--repoHome", temporaryFolder.newFolder().getAbsolutePath());
testIncorrectParams(argsList,
Lists.newArrayList("Option(s) [track] are unavailable given other options on the command line"));
}
@Test
public void testConsistencyNoRepo() throws Exception {
setupDataStore.close();
File dump = temporaryFolder.newFolder();
List<String> argsList = Lists
.newArrayList("--id", "--ref", "--consistency", "--store", storePath,
"--dump", dump.getAbsolutePath());
testIncorrectParams(argsList, Lists.newArrayList("Missing required option(s) [repoHome]"));
}
@Test
public void testTrackNoRepo() throws Exception {
setupDataStore.close();
File dump = temporaryFolder.newFolder();
List<String> argsList = Lists
.newArrayList("--id", "--ref", "--consistency", "--store", storePath,
"--dump", dump.getAbsolutePath(), "--track");
testIncorrectParams(argsList, Lists.newArrayList("Missing required option(s) [repoHome]"));
}
public static void testIncorrectParams(List<String> argList, ArrayList<String> assertMsg) throws Exception {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setErr(new PrintStream(buffer, true, UTF_8.toString()));
DataStoreCheckCommand.checkDataStore(argList.toArray(new String[0]));
String message = buffer.toString(UTF_8.toString());
log.info("Assert message: {}", assertMsg);
log.info("Message logged in System.err: {}", message);
for (String msg : assertMsg) {
Assert.assertTrue(message.contains(msg));
}
System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err)));
}
private static void assertFileEquals(File dump, String prefix, Set<String> blobsAdded)
throws IOException {
File files[] =
FileFilterUtils.filter(FileFilterUtils.prefixFileFilter(prefix), dump.listFiles());
Assert.assertNotNull(files);
Assert.assertTrue(files.length == 1);
Assert.assertTrue(files[0].exists());
assertEquals(blobsAdded,
FileIOUtils.readStringsAsSet(new FileInputStream(files[0]), false));
}
static InputStream randomStream(int seed, int size) {
Random r = new Random(seed);
byte[] data = new byte[size];
r.nextBytes(data);
return new ByteArrayInputStream(data);
}
private static String createTempConfig(File cfgFile, Properties props) throws IOException {
FileOutputStream fos = FileUtils.openOutputStream(cfgFile);
ConfigurationHandler.write(fos, props);
return cfgFile.getAbsolutePath();
}
private static Set<String> encodedIds(Set<String> ids, String dsOption) {
return Sets.newHashSet(Iterators.transform(ids.iterator(), new Function<String, String>() {
@Nullable @Override public String apply(@Nullable String input) {
return DataStoreCheckCommand.encodeId(input, "--"+dsOption);
}
}));
}
private static Set<String> encodedIdsAndPath(Set<String> ids, String dsOption, Map<String, String> blobsAddedWithNodes) {
return Sets.newHashSet(Iterators.transform(ids.iterator(), new Function<String, String>() {
@Nullable @Override public String apply(@Nullable String input) {
return Joiner.on(",").join(
DataStoreCheckCommand.encodeId(input, "--"+dsOption),
blobsAddedWithNodes.get(input));
}
}));
}
}