/* * 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.index.shard; import org.apache.lucene.mockfile.FilterFileSystemProvider; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.io.PathUtilsForTesting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.env.NodeEnvironment.NodePath; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.IndexSettingsModule; import org.junit.AfterClass; import org.junit.BeforeClass; import java.io.IOException; import java.nio.file.FileStore; import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.attribute.FileAttributeView; import java.nio.file.attribute.FileStoreAttributeView; import java.nio.file.spi.FileSystemProvider; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** Separate test class from ShardPathTests because we need static (BeforeClass) setup to install mock filesystems... */ public class NewPathForShardTests extends ESTestCase { private static final IndexSettings INDEX_SETTINGS = IndexSettingsModule.newIndexSettings("index", Settings.EMPTY); // Sneakiness to install mock file stores so we can pretend how much free space we have on each path.data: private static MockFileStore aFileStore = new MockFileStore("mocka"); private static MockFileStore bFileStore = new MockFileStore("mockb"); private static String aPathPart; private static String bPathPart; @BeforeClass public static void installMockUsableSpaceFS() throws Exception { FileSystem current = PathUtils.getDefaultFileSystem(); aPathPart = current.getSeparator() + 'a' + current.getSeparator(); bPathPart = current.getSeparator() + 'b' + current.getSeparator(); FileSystemProvider mock = new MockUsableSpaceFileSystemProvider(current); PathUtilsForTesting.installMock(mock.getFileSystem(null)); } @AfterClass public static void removeMockUsableSpaceFS() throws Exception { PathUtilsForTesting.teardown(); aFileStore = null; bFileStore = null; } /** Mock file system that fakes usable space for each FileStore */ static class MockUsableSpaceFileSystemProvider extends FilterFileSystemProvider { MockUsableSpaceFileSystemProvider(FileSystem inner) { super("mockusablespace://", inner); final List<FileStore> fileStores = new ArrayList<>(); fileStores.add(aFileStore); fileStores.add(bFileStore); } @Override public FileStore getFileStore(Path path) throws IOException { if (path.toString().contains(aPathPart)) { return aFileStore; } else { return bFileStore; } } } static class MockFileStore extends FileStore { public long usableSpace; private final String desc; MockFileStore(String desc) { this.desc = desc; } @Override public String type() { return "mock"; } @Override public String name() { return desc; } @Override public String toString() { return desc; } @Override public boolean isReadOnly() { return false; } @Override public long getTotalSpace() throws IOException { return usableSpace*3; } @Override public long getUsableSpace() throws IOException { return usableSpace; } @Override public long getUnallocatedSpace() throws IOException { return usableSpace*2; } @Override public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) { return false; } @Override public boolean supportsFileAttributeView(String name) { return false; } @Override public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) { return null; } @Override public Object getAttribute(String attribute) throws IOException { return null; } } public void testSelectNewPathForShard() throws Exception { Path path = PathUtils.get(createTempDir().toString()); // Use 2 data paths: String[] paths = new String[] {path.resolve("a").toString(), path.resolve("b").toString()}; Settings settings = Settings.builder() .put(Environment.PATH_HOME_SETTING.getKey(), path) .putArray(Environment.PATH_DATA_SETTING.getKey(), paths).build(); NodeEnvironment nodeEnv = new NodeEnvironment(settings, new Environment(settings)); // Make sure all our mocking above actually worked: NodePath[] nodePaths = nodeEnv.nodePaths(); assertEquals(2, nodePaths.length); assertEquals("mocka", nodePaths[0].fileStore.name()); assertEquals("mockb", nodePaths[1].fileStore.name()); // Path a has lots of free space, but b has little, so new shard should go to a: aFileStore.usableSpace = 100000; bFileStore.usableSpace = 1000; ShardId shardId = new ShardId("index", "_na_", 0); ShardPath result = ShardPath.selectNewPathForShard(nodeEnv, shardId, INDEX_SETTINGS, 100, Collections.<Path,Integer>emptyMap()); assertTrue(result.getDataPath().toString().contains(aPathPart)); // Test the reverse: b has lots of free space, but a has little, so new shard should go to b: aFileStore.usableSpace = 1000; bFileStore.usableSpace = 100000; shardId = new ShardId("index", "_na_", 0); result = ShardPath.selectNewPathForShard(nodeEnv, shardId, INDEX_SETTINGS, 100, Collections.<Path,Integer>emptyMap()); assertTrue(result.getDataPath().toString().contains(bPathPart)); // Now a and be have equal usable space; we allocate two shards to the node, and each should go to different paths: aFileStore.usableSpace = 100000; bFileStore.usableSpace = 100000; Map<Path,Integer> dataPathToShardCount = new HashMap<>(); ShardPath result1 = ShardPath.selectNewPathForShard(nodeEnv, shardId, INDEX_SETTINGS, 100, dataPathToShardCount); dataPathToShardCount.put(NodeEnvironment.shardStatePathToDataPath(result1.getDataPath()), 1); ShardPath result2 = ShardPath.selectNewPathForShard(nodeEnv, shardId, INDEX_SETTINGS, 100, dataPathToShardCount); // #11122: this was the original failure: on a node with 2 disks that have nearly equal // free space, we would always allocate all N incoming shards to the one path that // had the most free space, never using the other drive unless new shards arrive // after the first shards started using storage: assertNotEquals(result1.getDataPath(), result2.getDataPath()); nodeEnv.close(); } }