| /* |
| * 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)); |
| } |
| })); |
| } |
| } |