/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch 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.elasticsearch.common.util; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import org.apache.lucene.util.CollectionUtil; import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.TestUtil; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.gateway.MetaDataStateFormat; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardPath; import org.elasticsearch.index.shard.ShardStateMetaData; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.OldIndexUtils; import com.google.common.base.Charsets; import com.google.common.collect.Sets; /** */ @LuceneTestCase.SuppressFileSystems("ExtrasFS") public class MultiDataPathUpgraderTests extends ESTestCase { public void testUpgradeRandomPaths() throws IOException { try (NodeEnvironment nodeEnvironment = newNodeEnvironment()) { final String uuid = Strings.base64UUID(); final ShardId shardId = new ShardId("foo", 0); final Path[] shardDataPaths = nodeEnvironment.availableShardPaths(shardId); if (nodeEnvironment.nodeDataPaths().length == 1) { MultiDataPathUpgrader helper = new MultiDataPathUpgrader(nodeEnvironment); assertFalse(helper.needsUpgrading(shardId)); return; } int numIdxFiles = 0; int numTranslogFiles = 0; int metaStateVersion = 0; for (Path shardPath : shardDataPaths) { final Path translog = shardPath.resolve(ShardPath.TRANSLOG_FOLDER_NAME); final Path idx = shardPath.resolve(ShardPath.INDEX_FOLDER_NAME); Files.createDirectories(translog); Files.createDirectories(idx); int numFiles = randomIntBetween(1, 10); for (int i = 0; i < numFiles; i++, numIdxFiles++) { String filename = Integer.toString(numIdxFiles); try (BufferedWriter w = Files.newBufferedWriter(idx.resolve(filename + ".tst"), Charsets.UTF_8)) { w.write(filename); } } numFiles = randomIntBetween(1, 10); for (int i = 0; i < numFiles; i++, numTranslogFiles++) { String filename = Integer.toString(numTranslogFiles); try (BufferedWriter w = Files.newBufferedWriter(translog.resolve(filename + ".translog"), Charsets.UTF_8)) { w.write(filename); } } ++metaStateVersion; ShardStateMetaData.FORMAT.write(new ShardStateMetaData(metaStateVersion, true, uuid), metaStateVersion, shardDataPaths); } final Path path = randomFrom(shardDataPaths); ShardPath targetPath = new ShardPath(false, path, path, uuid, new ShardId("foo", 0)); MultiDataPathUpgrader helper = new MultiDataPathUpgrader(nodeEnvironment); helper.upgrade(shardId, targetPath); assertFalse(helper.needsUpgrading(shardId)); if (shardDataPaths.length > 1) { for (Path shardPath : shardDataPaths) { if (shardPath.equals(targetPath.getDataPath())) { continue; } final Path translog = shardPath.resolve(ShardPath.TRANSLOG_FOLDER_NAME); final Path idx = shardPath.resolve(ShardPath.INDEX_FOLDER_NAME); final Path state = shardPath.resolve(MetaDataStateFormat.STATE_DIR_NAME); assertFalse(Files.exists(translog)); assertFalse(Files.exists(idx)); assertFalse(Files.exists(state)); assertFalse(Files.exists(shardPath)); } } final ShardStateMetaData stateMetaData = ShardStateMetaData.FORMAT.loadLatestState(logger, targetPath.getShardStatePath()); assertEquals(metaStateVersion, stateMetaData.version); assertTrue(stateMetaData.primary); assertEquals(uuid, stateMetaData.indexUUID); final Path translog = targetPath.getDataPath().resolve(ShardPath.TRANSLOG_FOLDER_NAME); final Path idx = targetPath.getDataPath().resolve(ShardPath.INDEX_FOLDER_NAME); Files.deleteIfExists(idx.resolve("write.lock")); assertEquals(numTranslogFiles, FileSystemUtils.files(translog).length); assertEquals(numIdxFiles, FileSystemUtils.files(idx).length); final HashSet<Path> translogFiles = Sets.newHashSet(FileSystemUtils.files(translog)); for (int i = 0; i < numTranslogFiles; i++) { final String name = Integer.toString(i); translogFiles.contains(translog.resolve(name + ".translog")); byte[] content = Files.readAllBytes(translog.resolve(name + ".translog")); assertEquals(name , new String(content, Charsets.UTF_8)); } final HashSet<Path> idxFiles = Sets.newHashSet(FileSystemUtils.files(idx)); for (int i = 0; i < numIdxFiles; i++) { final String name = Integer.toString(i); idxFiles.contains(idx.resolve(name + ".tst")); byte[] content = Files.readAllBytes(idx.resolve(name + ".tst")); assertEquals(name , new String(content, Charsets.UTF_8)); } } } /** * Run upgrade on a real bwc index */ public void testUpgradeRealIndex() throws IOException, URISyntaxException { List<Path> indexes = new ArrayList<>(); try (DirectoryStream<Path> stream = Files.newDirectoryStream(getBwcIndicesPath(), "index-*.zip")) { for (Path path : stream) { indexes.add(path); } } CollectionUtil.introSort(indexes, new Comparator<Path>() { @Override public int compare(Path o1, Path o2) { return o1.getFileName().compareTo(o2.getFileName()); } }); final ShardId shardId = new ShardId("test", 0); final Path path = randomFrom(indexes); final Path indexFile = path; final String indexName = indexFile.getFileName().toString().replace(".zip", "").toLowerCase(Locale.ROOT); try (NodeEnvironment nodeEnvironment = newNodeEnvironment()) { if (nodeEnvironment.nodeDataPaths().length == 1) { MultiDataPathUpgrader helper = new MultiDataPathUpgrader(nodeEnvironment); assertFalse(helper.needsUpgrading(shardId)); return; } Path unzipDir = createTempDir(); Path unzipDataDir = unzipDir.resolve("data"); // decompress the index try (InputStream stream = Files.newInputStream(indexFile)) { TestUtil.unzip(stream, unzipDir); } // check it is unique assertTrue(Files.exists(unzipDataDir)); Path[] list = FileSystemUtils.files(unzipDataDir); if (list.length != 1) { throw new IllegalStateException("Backwards index must contain exactly one cluster but was " + list.length); } // the bwc scripts packs the indices under this path Path src = list[0].resolve("nodes/0/indices/" + indexName); assertTrue("[" + indexFile + "] missing index dir: " + src.toString(), Files.exists(src)); Path[] multiDataPath = new Path[nodeEnvironment.nodeDataPaths().length]; int i = 0; for (NodeEnvironment.NodePath nodePath : nodeEnvironment.nodePaths()) { multiDataPath[i++] = nodePath.indicesPath; } logger.info("--> injecting index [{}] into multiple data paths", indexName); OldIndexUtils.copyIndex(logger, src, indexName, multiDataPath); final ShardPath shardPath = new ShardPath(false, nodeEnvironment.availableShardPaths(new ShardId(indexName, 0))[0], nodeEnvironment.availableShardPaths(new ShardId(indexName, 0))[0], IndexMetaData.INDEX_UUID_NA_VALUE, new ShardId(indexName, 0)); logger.info("{}", FileSystemUtils.files(shardPath.resolveIndex())); MultiDataPathUpgrader helper = new MultiDataPathUpgrader(nodeEnvironment); helper.upgrade(new ShardId(indexName, 0), shardPath); helper.checkIndex(shardPath); assertFalse(helper.needsUpgrading(new ShardId(indexName, 0))); } } public void testNeedsUpgrade() throws IOException { try (NodeEnvironment nodeEnvironment = newNodeEnvironment()) { String uuid = Strings.randomBase64UUID(); final ShardId shardId = new ShardId("foo", 0); ShardStateMetaData.FORMAT.write(new ShardStateMetaData(1, true, uuid), 1, nodeEnvironment.availableShardPaths(shardId)); MultiDataPathUpgrader helper = new MultiDataPathUpgrader(nodeEnvironment); boolean multiDataPaths = nodeEnvironment.nodeDataPaths().length > 1; boolean needsUpgrading = helper.needsUpgrading(shardId); if (multiDataPaths) { assertTrue(needsUpgrading); } else { assertFalse(needsUpgrading); } } } public void testPickTargetShardPath() throws IOException { try (NodeEnvironment nodeEnvironment = newNodeEnvironment()) { final ShardId shard = new ShardId("foo", 0); final Path[] paths = nodeEnvironment.availableShardPaths(shard); if (paths.length == 1) { MultiDataPathUpgrader helper = new MultiDataPathUpgrader(nodeEnvironment); try { helper.pickShardPath(new ShardId("foo", 0)); fail("one path needs no upgrading"); } catch (IllegalStateException ex) { // only one path } } else { final Map<Path, Tuple<Long, Long>> pathToSpace = new HashMap<>(); final Path expectedPath; if (randomBoolean()) { // path with most of the file bytes expectedPath = randomFrom(paths); long[] used = new long[paths.length]; long sumSpaceUsed = 0; for (int i = 0; i < used.length; i++) { long spaceUsed = paths[i] == expectedPath ? randomIntBetween(101, 200) : randomIntBetween(10, 100); sumSpaceUsed += spaceUsed; used[i] = spaceUsed; } for (int i = 0; i < used.length; i++) { long availalbe = randomIntBetween((int)(2*sumSpaceUsed-used[i]), 4 * (int)sumSpaceUsed); pathToSpace.put(paths[i], new Tuple<>(availalbe, used[i])); } } else { // path with largest available space expectedPath = randomFrom(paths); long[] used = new long[paths.length]; long sumSpaceUsed = 0; for (int i = 0; i < used.length; i++) { long spaceUsed = randomIntBetween(10, 100); sumSpaceUsed += spaceUsed; used[i] = spaceUsed; } for (int i = 0; i < used.length; i++) { long availalbe = paths[i] == expectedPath ? randomIntBetween((int)(sumSpaceUsed), (int)(2*sumSpaceUsed)) : randomIntBetween(0, (int)(sumSpaceUsed) - 1) ; pathToSpace.put(paths[i], new Tuple<>(availalbe, used[i])); } } MultiDataPathUpgrader helper = new MultiDataPathUpgrader(nodeEnvironment) { @Override protected long getUsabelSpace(NodeEnvironment.NodePath path) throws IOException { return pathToSpace.get(path.resolve(shard)).v1(); } @Override protected long getSpaceUsedByShard(Path path) throws IOException { return pathToSpace.get(path).v2(); } }; String uuid = Strings.randomBase64UUID(); ShardStateMetaData.FORMAT.write(new ShardStateMetaData(1, true, uuid), 1, paths); final ShardPath shardPath = helper.pickShardPath(new ShardId("foo", 0)); assertEquals(expectedPath, shardPath.getDataPath()); assertEquals(expectedPath, shardPath.getShardStatePath()); } MultiDataPathUpgrader helper = new MultiDataPathUpgrader(nodeEnvironment) { @Override protected long getUsabelSpace(NodeEnvironment.NodePath path) throws IOException { return randomIntBetween(0, 10); } @Override protected long getSpaceUsedByShard(Path path) throws IOException { return randomIntBetween(11, 20); } }; try { helper.pickShardPath(new ShardId("foo", 0)); fail("not enough space"); } catch (IllegalStateException ex) { // not enough space } } } }