/*
* 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.indices;
import org.elasticsearch.Version;
import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.routing.ShardRouting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.ByteSizeUnit;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.engine.Engine;
import org.elasticsearch.index.shard.IndexSearcherWrapper;
import org.elasticsearch.index.shard.IndexShard;
import org.elasticsearch.index.shard.IndexShardIT;
import org.elasticsearch.indices.recovery.RecoveryState;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.threadpool.ThreadPool.Cancellable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
import static org.hamcrest.Matchers.equalTo;
public class IndexingMemoryControllerTests extends ESSingleNodeTestCase {
static class MockController extends IndexingMemoryController {
// Size of each shard's indexing buffer
final Map<IndexShard, Long> indexBufferRAMBytesUsed = new HashMap<>();
// How many bytes this shard is currently moving to disk
final Map<IndexShard, Long> writingBytes = new HashMap<>();
// Shards that are currently throttled
final Set<IndexShard> throttled = new HashSet<>();
MockController(Settings settings) {
super(Settings.builder()
.put("indices.memory.interval", "200h") // disable it
.put(settings)
.build(), null, null);
}
public void deleteShard(IndexShard shard) {
indexBufferRAMBytesUsed.remove(shard);
writingBytes.remove(shard);
}
@Override
protected List<IndexShard> availableShards() {
return new ArrayList<>(indexBufferRAMBytesUsed.keySet());
}
@Override
protected long getIndexBufferRAMBytesUsed(IndexShard shard) {
return indexBufferRAMBytesUsed.get(shard) + writingBytes.get(shard);
}
@Override
protected long getShardWritingBytes(IndexShard shard) {
Long bytes = writingBytes.get(shard);
if (bytes == null) {
return 0;
} else {
return bytes;
}
}
@Override
protected void checkIdle(IndexShard shard, long inactiveTimeNS) {
}
@Override
public void writeIndexingBufferAsync(IndexShard shard) {
long bytes = indexBufferRAMBytesUsed.put(shard, 0L);
writingBytes.put(shard, writingBytes.get(shard) + bytes);
indexBufferRAMBytesUsed.put(shard, 0L);
}
@Override
public void activateThrottling(IndexShard shard) {
assertTrue(throttled.add(shard));
}
@Override
public void deactivateThrottling(IndexShard shard) {
assertTrue(throttled.remove(shard));
}
public void doneWriting(IndexShard shard) {
writingBytes.put(shard, 0L);
}
public void assertBuffer(IndexShard shard, int expectedMB) {
Long actual = indexBufferRAMBytesUsed.get(shard);
if (actual == null) {
actual = 0L;
}
assertEquals(expectedMB * 1024 * 1024, actual.longValue());
}
public void assertThrottled(IndexShard shard) {
assertTrue(throttled.contains(shard));
}
public void assertNotThrottled(IndexShard shard) {
assertFalse(throttled.contains(shard));
}
public void assertWriting(IndexShard shard, int expectedMB) {
Long actual = writingBytes.get(shard);
if (actual == null) {
actual = 0L;
}
assertEquals(expectedMB * 1024 * 1024, actual.longValue());
}
public void simulateIndexing(IndexShard shard) {
Long bytes = indexBufferRAMBytesUsed.get(shard);
if (bytes == null) {
bytes = 0L;
// First time we are seeing this shard:
writingBytes.put(shard, 0L);
}
// Each doc we index takes up a megabyte!
bytes += 1024*1024;
indexBufferRAMBytesUsed.put(shard, bytes);
forceCheck();
}
@Override
protected Cancellable scheduleTask(ThreadPool threadPool) {
return null;
}
}
public void testShardAdditionAndRemoval() {
createIndex("test", Settings.builder().put("index.number_of_shards", 3).put("index.number_of_replicas", 0).build());
IndicesService indicesService = getInstanceFromNode(IndicesService.class);
IndexService test = indicesService.indexService(resolveIndex("test"));
MockController controller = new MockController(Settings.builder()
.put("indices.memory.index_buffer_size", "4mb").build());
IndexShard shard0 = test.getShard(0);
controller.simulateIndexing(shard0);
controller.assertBuffer(shard0, 1);
// add another shard
IndexShard shard1 = test.getShard(1);
controller.simulateIndexing(shard1);
controller.assertBuffer(shard0, 1);
controller.assertBuffer(shard1, 1);
// remove first shard
controller.deleteShard(shard0);
controller.forceCheck();
controller.assertBuffer(shard1, 1);
// remove second shard
controller.deleteShard(shard1);
controller.forceCheck();
// add a new one
IndexShard shard2 = test.getShard(2);
controller.simulateIndexing(shard2);
controller.assertBuffer(shard2, 1);
}
public void testActiveInactive() {
createIndex("test", Settings.builder().put("index.number_of_shards", 2).put("index.number_of_replicas", 0).build());
IndicesService indicesService = getInstanceFromNode(IndicesService.class);
IndexService test = indicesService.indexService(resolveIndex("test"));
MockController controller = new MockController(Settings.builder()
.put("indices.memory.index_buffer_size", "5mb")
.build());
IndexShard shard0 = test.getShard(0);
controller.simulateIndexing(shard0);
IndexShard shard1 = test.getShard(1);
controller.simulateIndexing(shard1);
controller.assertBuffer(shard0, 1);
controller.assertBuffer(shard1, 1);
controller.simulateIndexing(shard0);
controller.simulateIndexing(shard1);
controller.assertBuffer(shard0, 2);
controller.assertBuffer(shard1, 2);
// index into one shard only, crosses the 5mb limit, so shard1 is refreshed
controller.simulateIndexing(shard0);
controller.simulateIndexing(shard0);
controller.assertBuffer(shard0, 0);
controller.assertBuffer(shard1, 2);
controller.simulateIndexing(shard1);
controller.simulateIndexing(shard1);
controller.assertBuffer(shard1, 4);
controller.simulateIndexing(shard1);
controller.simulateIndexing(shard1);
// shard1 crossed 5 mb and is now cleared:
controller.assertBuffer(shard1, 0);
}
public void testMinBufferSizes() {
MockController controller = new MockController(Settings.builder()
.put("indices.memory.index_buffer_size", "0.001%")
.put("indices.memory.min_index_buffer_size", "6mb").build());
assertThat(controller.indexingBufferSize(), equalTo(new ByteSizeValue(6, ByteSizeUnit.MB)));
}
public void testNegativeMinIndexBufferSize() {
Exception e = expectThrows(IllegalArgumentException.class,
() -> new MockController(Settings.builder()
.put("indices.memory.min_index_buffer_size", "-6mb").build()));
assertEquals("Failed to parse value [-6mb] for setting [indices.memory.min_index_buffer_size] must be >= 0b", e.getMessage());
}
public void testNegativeInterval() {
Exception e = expectThrows(IllegalArgumentException.class,
() -> new MockController(Settings.builder()
.put("indices.memory.interval", "-42s").build()));
assertEquals("Failed to parse value [-42s] for setting [indices.memory.interval] must be >= 0s", e.getMessage());
}
public void testNegativeShardInactiveTime() {
Exception e = expectThrows(IllegalArgumentException.class,
() -> new MockController(Settings.builder()
.put("indices.memory.shard_inactive_time", "-42s").build()));
assertEquals("Failed to parse value [-42s] for setting [indices.memory.shard_inactive_time] must be >= 0s", e.getMessage());
}
public void testNegativeMaxIndexBufferSize() {
Exception e = expectThrows(IllegalArgumentException.class,
() -> new MockController(Settings.builder()
.put("indices.memory.max_index_buffer_size", "-6mb").build()));
assertEquals("Failed to parse value [-6mb] for setting [indices.memory.max_index_buffer_size] must be >= -1b", e.getMessage());
}
public void testMaxBufferSizes() {
MockController controller = new MockController(Settings.builder()
.put("indices.memory.index_buffer_size", "90%")
.put("indices.memory.max_index_buffer_size", "6mb").build());
assertThat(controller.indexingBufferSize(), equalTo(new ByteSizeValue(6, ByteSizeUnit.MB)));
}
public void testThrottling() throws Exception {
createIndex("test", Settings.builder().put("index.number_of_shards", 3).put("index.number_of_replicas", 0).build());
IndicesService indicesService = getInstanceFromNode(IndicesService.class);
IndexService test = indicesService.indexService(resolveIndex("test"));
MockController controller = new MockController(Settings.builder()
.put("indices.memory.index_buffer_size", "4mb").build());
IndexShard shard0 = test.getShard(0);
IndexShard shard1 = test.getShard(1);
IndexShard shard2 = test.getShard(2);
controller.simulateIndexing(shard0);
controller.simulateIndexing(shard0);
controller.simulateIndexing(shard0);
controller.assertBuffer(shard0, 3);
controller.simulateIndexing(shard1);
controller.simulateIndexing(shard1);
// We are now using 5 MB, so we should be writing shard0 since it's using the most heap:
controller.assertWriting(shard0, 3);
controller.assertWriting(shard1, 0);
controller.assertBuffer(shard0, 0);
controller.assertBuffer(shard1, 2);
controller.simulateIndexing(shard0);
controller.simulateIndexing(shard1);
controller.simulateIndexing(shard1);
// Now we are still writing 3 MB (shard0), and using 5 MB index buffers, so we should now 1) be writing shard1, and 2) be throttling shard1:
controller.assertWriting(shard0, 3);
controller.assertWriting(shard1, 4);
controller.assertBuffer(shard0, 1);
controller.assertBuffer(shard1, 0);
controller.assertNotThrottled(shard0);
controller.assertThrottled(shard1);
logger.info("--> Indexing more data");
// More indexing to shard0
controller.simulateIndexing(shard0);
controller.simulateIndexing(shard0);
controller.simulateIndexing(shard0);
controller.simulateIndexing(shard0);
// Now we are using 5 MB again, so shard0 should also be writing and now also be throttled:
controller.assertWriting(shard0, 8);
controller.assertWriting(shard1, 4);
controller.assertBuffer(shard0, 0);
controller.assertBuffer(shard1, 0);
controller.assertThrottled(shard0);
controller.assertThrottled(shard1);
// Both shards finally finish writing, and throttling should stop:
controller.doneWriting(shard0);
controller.doneWriting(shard1);
controller.forceCheck();
controller.assertNotThrottled(shard0);
controller.assertNotThrottled(shard1);
}
// #10312
public void testDeletesAloneCanTriggerRefresh() throws Exception {
createIndex("index",
Settings.builder().put("index.number_of_shards", 1)
.put("index.number_of_replicas", 0)
.put("index.refresh_interval", -1)
.build());
ensureGreen();
IndicesService indicesService = getInstanceFromNode(IndicesService.class);
IndexService indexService = indicesService.indexService(resolveIndex("index"));
IndexShard shard = indexService.getShardOrNull(0);
assertNotNull(shard);
for (int i = 0; i < 100; i++) {
String id = Integer.toString(i);
client().prepareIndex("index", "type", id).setSource("field", "value").get();
}
// Force merge so we know all merges are done before we start deleting:
ForceMergeResponse r = client().admin().indices().prepareForceMerge().setMaxNumSegments(1).execute().actionGet();
assertNoFailures(r);
// Make a shell of an IMC to check up on indexing buffer usage:
Settings settings = Settings.builder().put("indices.memory.index_buffer_size", "1kb").build();
// TODO: would be cleaner if I could pass this 1kb setting to the single node this test created....
IndexingMemoryController imc = new IndexingMemoryController(settings, null, null) {
@Override
protected List<IndexShard> availableShards() {
return Collections.singletonList(shard);
}
@Override
protected long getIndexBufferRAMBytesUsed(IndexShard shard) {
return shard.getIndexBufferRAMBytesUsed();
}
@Override
protected void writeIndexingBufferAsync(IndexShard shard) {
// just do it sync'd for this test
shard.writeIndexingBuffer();
}
@Override
protected Cancellable scheduleTask(ThreadPool threadPool) {
return null;
}
};
for (int i = 0; i < 100; i++) {
String id = Integer.toString(i);
client().prepareDelete("index", "type", id).get();
}
final long indexingBufferBytes1 = shard.getIndexBufferRAMBytesUsed();
imc.forceCheck();
// We must assertBusy because the writeIndexingBufferAsync is done in background (REFRESH) thread pool:
assertBusy(new Runnable() {
@Override
public void run() {
try (Engine.Searcher s2 = shard.acquireSearcher("index")) {
// 100 buffered deletes will easily exceed our 1 KB indexing buffer so it should trigger a write:
final long indexingBufferBytes2 = shard.getIndexBufferRAMBytesUsed();
assertTrue(indexingBufferBytes2 < indexingBufferBytes1);
}
}
});
}
public void testTranslogRecoveryWorksWithIMC() throws IOException {
createIndex("test");
ensureGreen();
IndicesService indicesService = getInstanceFromNode(IndicesService.class);
IndexService indexService = indicesService.indexService(resolveIndex("test"));
IndexShard shard = indexService.getShardOrNull(0);
for (int i = 0; i < 100; i++) {
client().prepareIndex("test", "test", Integer.toString(i)).setSource("{\"foo\" : \"bar\"}", XContentType.JSON).get();
}
IndexSearcherWrapper wrapper = new IndexSearcherWrapper() {};
shard.close("simon says", false);
AtomicReference<IndexShard> shardRef = new AtomicReference<>();
Settings settings = Settings.builder().put("indices.memory.index_buffer_size", "50kb").build();
Iterable<IndexShard> iterable = () -> (shardRef.get() == null) ? Collections.<IndexShard>emptyList().iterator()
: Collections.singleton(shardRef.get()).iterator();
AtomicInteger flushes = new AtomicInteger();
IndexingMemoryController imc = new IndexingMemoryController(settings, client().threadPool(), iterable) {
@Override
protected void writeIndexingBufferAsync(IndexShard shard) {
assertEquals(shard, shardRef.get());
flushes.incrementAndGet();
shard.writeIndexingBuffer();
}
};
final IndexShard newShard = IndexShardIT.newIndexShard(indexService, shard, wrapper, imc);
shardRef.set(newShard);
try {
assertEquals(0, imc.availableShards().size());
ShardRouting routing = newShard.routingEntry();
DiscoveryNode localNode = new DiscoveryNode("foo", buildNewFakeTransportAddress(), emptyMap(), emptySet(), Version.CURRENT);
newShard.markAsRecovering("store", new RecoveryState(routing, localNode, null));
assertEquals(1, imc.availableShards().size());
assertTrue(newShard.recoverFromStore());
assertTrue("we should have flushed in IMC at least once but did: " + flushes.get(), flushes.get() >= 1);
newShard.updateRoutingEntry(routing.moveToStarted());
} finally {
newShard.close("simon says", false);
}
}
}