Opened x-pack ccr code
This commit is contained in:
commit
cbd38983f4
|
@ -0,0 +1,52 @@
|
||||||
|
import com.carrotsearch.gradle.junit4.RandomizedTestingTask
|
||||||
|
import org.elasticsearch.gradle.BuildPlugin
|
||||||
|
|
||||||
|
evaluationDependsOn(':x-pack-elasticsearch:plugin:core')
|
||||||
|
|
||||||
|
apply plugin: 'elasticsearch.esplugin'
|
||||||
|
esplugin {
|
||||||
|
name 'x-pack-ccr'
|
||||||
|
description 'Elasticsearch Expanded Pack Plugin - CCR'
|
||||||
|
classname 'org.elasticsearch.xpack.ccr.Ccr'
|
||||||
|
hasNativeController false
|
||||||
|
requiresKeystore true
|
||||||
|
extendedPlugins = ['x-pack-core']
|
||||||
|
licenseFile project(':x-pack-elasticsearch').file('LICENSE.txt')
|
||||||
|
noticeFile project(':x-pack-elasticsearch').file('NOTICE.txt')
|
||||||
|
}
|
||||||
|
archivesBaseName = 'x-pack-ccr'
|
||||||
|
|
||||||
|
integTest.enabled = false
|
||||||
|
|
||||||
|
// Instead we create a separate task to run the
|
||||||
|
// tests based on ESIntegTestCase
|
||||||
|
task internalClusterTest(type: RandomizedTestingTask,
|
||||||
|
group: JavaBasePlugin.VERIFICATION_GROUP,
|
||||||
|
description: 'Java fantasy integration tests',
|
||||||
|
dependsOn: test.dependsOn) {
|
||||||
|
configure(BuildPlugin.commonTestConfig(project))
|
||||||
|
classpath = project.test.classpath
|
||||||
|
testClassesDir = project.test.testClassesDir
|
||||||
|
include '**/*IT.class'
|
||||||
|
systemProperty 'es.set.netty.runtime.available.processors', 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
check {
|
||||||
|
dependsOn = [internalClusterTest, 'qa:multi-cluster:followClusterTest']
|
||||||
|
}
|
||||||
|
|
||||||
|
internalClusterTest.mustRunAfter test
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly "org.elasticsearch:elasticsearch:${version}"
|
||||||
|
compileOnly "org.elasticsearch.plugin:x-pack-core:${version}"
|
||||||
|
testCompile project(path: xpackModule('core'), configuration: 'testArtifacts')
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyLicenses {
|
||||||
|
ignoreSha 'x-pack-core'
|
||||||
|
}
|
||||||
|
|
||||||
|
run {
|
||||||
|
plugin ':x-pack-elasticsearch:plugin:core'
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
/* Remove assemble on all qa projects because we don't need to publish
|
||||||
|
* artifacts for them. */
|
||||||
|
gradle.projectsEvaluated {
|
||||||
|
subprojects {
|
||||||
|
Task assemble = project.tasks.findByName('assemble')
|
||||||
|
if (assemble) {
|
||||||
|
project.tasks.remove(assemble)
|
||||||
|
project.build.dependsOn.remove('assemble')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import org.elasticsearch.gradle.test.RestIntegTestTask
|
||||||
|
|
||||||
|
apply plugin: 'elasticsearch.standalone-test'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testCompile project(path: xpackModule('core'), configuration: 'runtime')
|
||||||
|
testCompile project(path: xpackModule('ccr'), configuration: 'runtime')
|
||||||
|
}
|
||||||
|
|
||||||
|
task leaderClusterTest(type: RestIntegTestTask) {
|
||||||
|
mustRunAfter(precommit)
|
||||||
|
}
|
||||||
|
|
||||||
|
leaderClusterTestCluster {
|
||||||
|
numNodes = 1
|
||||||
|
clusterName = 'leader-cluster'
|
||||||
|
plugin xpackProject('plugin').path
|
||||||
|
}
|
||||||
|
|
||||||
|
leaderClusterTestRunner {
|
||||||
|
systemProperty 'tests.is_leader_cluster', 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
task followClusterTest(type: RestIntegTestTask) {}
|
||||||
|
|
||||||
|
followClusterTestCluster {
|
||||||
|
dependsOn leaderClusterTestRunner
|
||||||
|
numNodes = 1
|
||||||
|
clusterName = 'follow-cluster'
|
||||||
|
plugin xpackProject('plugin').path
|
||||||
|
setting 'search.remote.leader_cluster.seeds', "\"${-> leaderClusterTest.nodes.get(0).transportUri()}\""
|
||||||
|
}
|
||||||
|
|
||||||
|
followClusterTestRunner {
|
||||||
|
systemProperty 'tests.is_leader_cluster', 'false'
|
||||||
|
systemProperty 'tests.leader_host', "${-> leaderClusterTest.nodes.get(0).httpUri()}"
|
||||||
|
finalizedBy 'leaderClusterTestCluster#stop'
|
||||||
|
}
|
||||||
|
|
||||||
|
test.enabled = false // no unit tests for multi-cluster-search, only the rest integration test
|
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr;
|
||||||
|
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
import org.apache.http.entity.ContentType;
|
||||||
|
import org.apache.http.entity.StringEntity;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.elasticsearch.client.Response;
|
||||||
|
import org.elasticsearch.client.RestClient;
|
||||||
|
import org.elasticsearch.common.Booleans;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentHelper;
|
||||||
|
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||||
|
import org.elasticsearch.common.xcontent.support.XContentMapValues;
|
||||||
|
import org.elasticsearch.test.rest.ESRestTestCase;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyMap;
|
||||||
|
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
|
||||||
|
public class FollowIndexIT extends ESRestTestCase {
|
||||||
|
|
||||||
|
private final boolean runningAgainstLeaderCluster = Booleans.parseBoolean(System.getProperty("tests.is_leader_cluster"));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean preserveClusterUponCompletion() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testFollowIndex() throws Exception {
|
||||||
|
final int numDocs = 128;
|
||||||
|
final String leaderIndexName = "test_index1";
|
||||||
|
if (runningAgainstLeaderCluster) {
|
||||||
|
logger.info("Running against leader cluster");
|
||||||
|
for (int i = 0; i < numDocs; i++) {
|
||||||
|
logger.info("Indexing doc [{}]", i);
|
||||||
|
index(client(), leaderIndexName, Integer.toString(i), "field", i);
|
||||||
|
}
|
||||||
|
refresh(leaderIndexName);
|
||||||
|
verifyDocuments(leaderIndexName, numDocs);
|
||||||
|
} else {
|
||||||
|
logger.info("Running against follow cluster");
|
||||||
|
final String followIndexName = "test_index2";
|
||||||
|
Settings indexSettings = Settings.builder()
|
||||||
|
.put("index.xpack.ccr.following_index", true)
|
||||||
|
.build();
|
||||||
|
// TODO: remove mapping here when ccr syncs mappings too
|
||||||
|
createIndex(followIndexName, indexSettings, "\"doc\": { \"properties\": { \"field\": { \"type\": \"integer\" }}}");
|
||||||
|
ensureYellow(followIndexName);
|
||||||
|
|
||||||
|
followIndex("leader_cluster:" + leaderIndexName, followIndexName);
|
||||||
|
assertBusy(() -> verifyDocuments(followIndexName, numDocs));
|
||||||
|
|
||||||
|
try (RestClient leaderClient = buildLeaderClient()) {
|
||||||
|
int id = numDocs;
|
||||||
|
index(leaderClient, leaderIndexName, Integer.toString(id), "field", id);
|
||||||
|
index(leaderClient, leaderIndexName, Integer.toString(id + 1), "field", id + 1);
|
||||||
|
index(leaderClient, leaderIndexName, Integer.toString(id + 2), "field", id + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertBusy(() -> verifyDocuments(followIndexName, numDocs + 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void index(RestClient client, String index, String id, Object... fields) throws IOException {
|
||||||
|
XContentBuilder document = jsonBuilder().startObject();
|
||||||
|
for (int i = 0; i < fields.length; i += 2) {
|
||||||
|
document.field((String) fields[i], fields[i + 1]);
|
||||||
|
}
|
||||||
|
document.endObject();
|
||||||
|
assertOK(client.performRequest("POST", "/" + index + "/doc/" + id, emptyMap(),
|
||||||
|
new StringEntity(Strings.toString(document), ContentType.APPLICATION_JSON)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void refresh(String index) throws IOException {
|
||||||
|
assertOK(client().performRequest("POST", "/" + index + "/_refresh"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void followIndex(String leaderIndex, String followIndex) throws IOException {
|
||||||
|
Map<String, String> params = Collections.singletonMap("leader_index", leaderIndex);
|
||||||
|
assertOK(client().performRequest("POST", "/_xpack/ccr/" + followIndex + "/_follow", params));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyDocuments(String index, int expectedNumDocs) throws IOException {
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
params.put("size", Integer.toString(expectedNumDocs));
|
||||||
|
params.put("sort", "field:asc");
|
||||||
|
Map<String, ?> response = toMap(client().performRequest("GET", "/" + index + "/_search", params));
|
||||||
|
|
||||||
|
int numDocs = (int) XContentMapValues.extractValue("hits.total", response);
|
||||||
|
assertThat(numDocs, equalTo(expectedNumDocs));
|
||||||
|
|
||||||
|
List<?> hits = (List<?>) XContentMapValues.extractValue("hits.hits", response);
|
||||||
|
assertThat(hits.size(), equalTo(expectedNumDocs));
|
||||||
|
for (int i = 0; i < expectedNumDocs; i++) {
|
||||||
|
int value = (int) XContentMapValues.extractValue("_source.field", (Map<?, ?>) hits.get(i));
|
||||||
|
assertThat(i, equalTo(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> toMap(Response response) throws IOException {
|
||||||
|
return toMap(EntityUtils.toString(response.getEntity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> toMap(String response) {
|
||||||
|
return XContentHelper.convertToMap(JsonXContent.jsonXContent, response, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureYellow(String index) throws IOException {
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
params.put("wait_for_status", "yellow");
|
||||||
|
params.put("wait_for_no_relocating_shards", "true");
|
||||||
|
params.put("timeout", "30s");
|
||||||
|
params.put("level", "shards");
|
||||||
|
assertOK(client().performRequest("GET", "_cluster/health/" + index, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RestClient buildLeaderClient() throws IOException {
|
||||||
|
assert runningAgainstLeaderCluster == false;
|
||||||
|
String leaderUrl = System.getProperty("tests.leader_host");
|
||||||
|
int portSeparator = leaderUrl.lastIndexOf(':');
|
||||||
|
HttpHost httpHost = new HttpHost(leaderUrl.substring(0, portSeparator),
|
||||||
|
Integer.parseInt(leaderUrl.substring(portSeparator + 1)), getProtocol());
|
||||||
|
return buildClient(Settings.EMPTY, new HttpHost[]{httpHost});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.ActionRequest;
|
||||||
|
import org.elasticsearch.action.ActionResponse;
|
||||||
|
import org.elasticsearch.client.Client;
|
||||||
|
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
||||||
|
import org.elasticsearch.cluster.node.DiscoveryNodes;
|
||||||
|
import org.elasticsearch.cluster.service.ClusterService;
|
||||||
|
import org.elasticsearch.common.ParseField;
|
||||||
|
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
|
||||||
|
import org.elasticsearch.common.settings.ClusterSettings;
|
||||||
|
import org.elasticsearch.common.settings.IndexScopedSettings;
|
||||||
|
import org.elasticsearch.common.settings.Setting;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.settings.SettingsFilter;
|
||||||
|
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
||||||
|
import org.elasticsearch.index.IndexSettings;
|
||||||
|
import org.elasticsearch.index.engine.EngineFactory;
|
||||||
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
|
import org.elasticsearch.persistent.PersistentTaskParams;
|
||||||
|
import org.elasticsearch.persistent.PersistentTasksExecutor;
|
||||||
|
import org.elasticsearch.plugins.ActionPlugin;
|
||||||
|
import org.elasticsearch.plugins.EnginePlugin;
|
||||||
|
import org.elasticsearch.plugins.PersistentTaskPlugin;
|
||||||
|
import org.elasticsearch.plugins.Plugin;
|
||||||
|
import org.elasticsearch.rest.RestController;
|
||||||
|
import org.elasticsearch.rest.RestHandler;
|
||||||
|
import org.elasticsearch.tasks.Task;
|
||||||
|
import org.elasticsearch.threadpool.ExecutorBuilder;
|
||||||
|
import org.elasticsearch.threadpool.FixedExecutorBuilder;
|
||||||
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.FollowExistingIndexAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardChangesAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardFollowNodeTask;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardFollowTask;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardFollowTasksExecutor;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.UnfollowIndexAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.bulk.TransportBulkShardOperationsAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.index.engine.FollowingEngineFactory;
|
||||||
|
import org.elasticsearch.xpack.ccr.rest.RestFollowExistingIndexAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.rest.RestUnfollowIndexAction;
|
||||||
|
import org.elasticsearch.xpack.core.XPackPlugin;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static org.elasticsearch.xpack.ccr.CcrSettings.CCR_ENABLED_SETTING;
|
||||||
|
import static org.elasticsearch.xpack.ccr.CcrSettings.CCR_FOLLOWING_INDEX_SETTING;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container class for CCR functionality.
|
||||||
|
*/
|
||||||
|
public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, EnginePlugin {
|
||||||
|
|
||||||
|
public static final String CCR_THREAD_POOL_NAME = "ccr";
|
||||||
|
|
||||||
|
private final boolean enabled;
|
||||||
|
private final Settings settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an instance of the CCR container with the specified settings.
|
||||||
|
*
|
||||||
|
* @param settings the settings
|
||||||
|
*/
|
||||||
|
public Ccr(final Settings settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
this.enabled = CCR_ENABLED_SETTING.get(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PersistentTasksExecutor<?>> getPersistentTasksExecutor(ClusterService clusterService,
|
||||||
|
ThreadPool threadPool, Client client) {
|
||||||
|
return Collections.singletonList(new ShardFollowTasksExecutor(settings, client, threadPool));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {
|
||||||
|
if (enabled == false) {
|
||||||
|
return emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(
|
||||||
|
new ActionHandler<>(ShardChangesAction.INSTANCE, ShardChangesAction.TransportAction.class),
|
||||||
|
new ActionHandler<>(FollowExistingIndexAction.INSTANCE, FollowExistingIndexAction.TransportAction.class),
|
||||||
|
new ActionHandler<>(UnfollowIndexAction.INSTANCE, UnfollowIndexAction.TransportAction.class),
|
||||||
|
new ActionHandler<>(BulkShardOperationsAction.INSTANCE, TransportBulkShardOperationsAction.class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RestHandler> getRestHandlers(Settings settings, RestController restController, ClusterSettings clusterSettings,
|
||||||
|
IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter,
|
||||||
|
IndexNameExpressionResolver indexNameExpressionResolver,
|
||||||
|
Supplier<DiscoveryNodes> nodesInCluster) {
|
||||||
|
return Arrays.asList(
|
||||||
|
new RestUnfollowIndexAction(settings, restController),
|
||||||
|
new RestFollowExistingIndexAction(settings, restController)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NamedWriteableRegistry.Entry> getNamedWriteables() {
|
||||||
|
return Arrays.asList(
|
||||||
|
// Persistent action requests
|
||||||
|
new NamedWriteableRegistry.Entry(PersistentTaskParams.class, ShardFollowTask.NAME,
|
||||||
|
ShardFollowTask::new),
|
||||||
|
|
||||||
|
// Task statuses
|
||||||
|
new NamedWriteableRegistry.Entry(Task.Status.class, ShardFollowNodeTask.Status.NAME,
|
||||||
|
ShardFollowNodeTask.Status::new)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NamedXContentRegistry.Entry> getNamedXContent() {
|
||||||
|
return Arrays.asList(
|
||||||
|
// Persistent action requests
|
||||||
|
new NamedXContentRegistry.Entry(PersistentTaskParams.class, new ParseField(ShardFollowTask.NAME),
|
||||||
|
ShardFollowTask::fromXContent),
|
||||||
|
|
||||||
|
// Task statuses
|
||||||
|
new NamedXContentRegistry.Entry(ShardFollowNodeTask.Status.class, new ParseField(ShardFollowNodeTask.Status.NAME),
|
||||||
|
ShardFollowNodeTask.Status::fromXContent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The settings defined by CCR.
|
||||||
|
*
|
||||||
|
* @return the settings
|
||||||
|
*/
|
||||||
|
public List<Setting<?>> getSettings() {
|
||||||
|
return CcrSettings.getSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional engine factory for CCR. This method inspects the index settings for the {@link CcrSettings#CCR_FOLLOWING_INDEX_SETTING}
|
||||||
|
* setting to determine whether or not the engine implementation should be a following engine.
|
||||||
|
*
|
||||||
|
* @return the optional engine factory
|
||||||
|
*/
|
||||||
|
public Optional<EngineFactory> getEngineFactory(final IndexSettings indexSettings) {
|
||||||
|
if (CCR_FOLLOWING_INDEX_SETTING.get(indexSettings.getSettings())) {
|
||||||
|
return Optional.of(new FollowingEngineFactory());
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ExecutorBuilder<?>> getExecutorBuilders(Settings settings) {
|
||||||
|
if (enabled == false) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
FixedExecutorBuilder ccrTp = new FixedExecutorBuilder(settings, CCR_THREAD_POOL_NAME,
|
||||||
|
32, 100, "xpack.ccr.ccr_thread_pool");
|
||||||
|
|
||||||
|
return Collections.singletonList(ccrTp);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected XPackLicenseState getLicenseState() { return XPackPlugin.getSharedLicenseState(); }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.settings.Setting;
|
||||||
|
import org.elasticsearch.common.settings.Setting.Property;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container class for CCR settings.
|
||||||
|
*/
|
||||||
|
public final class CcrSettings {
|
||||||
|
|
||||||
|
// prevent construction
|
||||||
|
private CcrSettings() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setting for controlling whether or not CCR is enabled.
|
||||||
|
*/
|
||||||
|
static final Setting<Boolean> CCR_ENABLED_SETTING = Setting.boolSetting("xpack.ccr.enabled", true, Property.NodeScope);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index setting for a following index.
|
||||||
|
*/
|
||||||
|
public static final Setting<Boolean> CCR_FOLLOWING_INDEX_SETTING =
|
||||||
|
Setting.boolSetting("index.xpack.ccr.following_index", false, Setting.Property.IndexScope);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The settings defined by CCR.
|
||||||
|
*
|
||||||
|
* @return the settings
|
||||||
|
*/
|
||||||
|
static List<Setting<?>> getSettings() {
|
||||||
|
return Arrays.asList(
|
||||||
|
CCR_ENABLED_SETTING,
|
||||||
|
CCR_FOLLOWING_INDEX_SETTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,277 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.Action;
|
||||||
|
import org.elasticsearch.action.ActionListener;
|
||||||
|
import org.elasticsearch.action.ActionRequest;
|
||||||
|
import org.elasticsearch.action.ActionRequestBuilder;
|
||||||
|
import org.elasticsearch.action.ActionRequestValidationException;
|
||||||
|
import org.elasticsearch.action.ActionResponse;
|
||||||
|
import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest;
|
||||||
|
import org.elasticsearch.action.support.ActionFilters;
|
||||||
|
import org.elasticsearch.action.support.HandledTransportAction;
|
||||||
|
import org.elasticsearch.client.Client;
|
||||||
|
import org.elasticsearch.client.ElasticsearchClient;
|
||||||
|
import org.elasticsearch.cluster.ClusterState;
|
||||||
|
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
||||||
|
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
||||||
|
import org.elasticsearch.cluster.service.ClusterService;
|
||||||
|
import org.elasticsearch.common.inject.Inject;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamInput;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.persistent.PersistentTasksCustomMetaData;
|
||||||
|
import org.elasticsearch.persistent.PersistentTasksService;
|
||||||
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
import org.elasticsearch.transport.RemoteClusterAware;
|
||||||
|
import org.elasticsearch.transport.RemoteClusterService;
|
||||||
|
import org.elasticsearch.transport.TransportService;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||||
|
|
||||||
|
public class FollowExistingIndexAction extends Action<FollowExistingIndexAction.Request,
|
||||||
|
FollowExistingIndexAction.Response, FollowExistingIndexAction.RequestBuilder> {
|
||||||
|
|
||||||
|
public static final FollowExistingIndexAction INSTANCE = new FollowExistingIndexAction();
|
||||||
|
public static final String NAME = "cluster:admin/xpack/ccr/follow_existing_index";
|
||||||
|
|
||||||
|
private FollowExistingIndexAction() {
|
||||||
|
super(NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestBuilder newRequestBuilder(ElasticsearchClient client) {
|
||||||
|
return new RequestBuilder(client, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response newResponse() {
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Request extends ActionRequest {
|
||||||
|
|
||||||
|
private String leaderIndex;
|
||||||
|
private String followIndex;
|
||||||
|
private long batchSize = ShardFollowTasksExecutor.DEFAULT_BATCH_SIZE;
|
||||||
|
private int concurrentProcessors = ShardFollowTasksExecutor.DEFAULT_CONCURRENT_PROCESSORS;
|
||||||
|
private long processorMaxTranslogBytes = ShardFollowTasksExecutor.DEFAULT_MAX_TRANSLOG_BYTES;
|
||||||
|
|
||||||
|
public String getLeaderIndex() {
|
||||||
|
return leaderIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLeaderIndex(String leaderIndex) {
|
||||||
|
this.leaderIndex = leaderIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFollowIndex() {
|
||||||
|
return followIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFollowIndex(String followIndex) {
|
||||||
|
this.followIndex = followIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getBatchSize() {
|
||||||
|
return batchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBatchSize(long batchSize) {
|
||||||
|
if (batchSize < 1) {
|
||||||
|
throw new IllegalArgumentException("Illegal batch_size [" + batchSize + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.batchSize = batchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConcurrentProcessors(int concurrentProcessors) {
|
||||||
|
if (concurrentProcessors < 1) {
|
||||||
|
throw new IllegalArgumentException("concurrent_processors must be larger than 0");
|
||||||
|
}
|
||||||
|
this.concurrentProcessors = concurrentProcessors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProcessorMaxTranslogBytes(long processorMaxTranslogBytes) {
|
||||||
|
if (processorMaxTranslogBytes <= 0) {
|
||||||
|
throw new IllegalArgumentException("processor_max_translog_bytes must be larger than 0");
|
||||||
|
}
|
||||||
|
this.processorMaxTranslogBytes = processorMaxTranslogBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActionRequestValidationException validate() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(StreamInput in) throws IOException {
|
||||||
|
super.readFrom(in);
|
||||||
|
leaderIndex = in.readString();
|
||||||
|
followIndex = in.readString();
|
||||||
|
batchSize = in.readVLong();
|
||||||
|
processorMaxTranslogBytes = in.readVLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(StreamOutput out) throws IOException {
|
||||||
|
super.writeTo(out);
|
||||||
|
out.writeString(leaderIndex);
|
||||||
|
out.writeString(followIndex);
|
||||||
|
out.writeVLong(batchSize);
|
||||||
|
out.writeVLong(processorMaxTranslogBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RequestBuilder extends ActionRequestBuilder<Request, Response, FollowExistingIndexAction.RequestBuilder> {
|
||||||
|
|
||||||
|
RequestBuilder(ElasticsearchClient client, Action<Request, Response, RequestBuilder> action) {
|
||||||
|
super(client, action, new Request());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response extends ActionResponse {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TransportAction extends HandledTransportAction<Request, Response> {
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final ClusterService clusterService;
|
||||||
|
private final RemoteClusterService remoteClusterService;
|
||||||
|
private final PersistentTasksService persistentTasksService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TransportAction(Settings settings, ThreadPool threadPool, TransportService transportService, ActionFilters actionFilters,
|
||||||
|
IndexNameExpressionResolver indexNameExpressionResolver, Client client, ClusterService clusterService,
|
||||||
|
PersistentTasksService persistentTasksService) {
|
||||||
|
super(settings, NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, Request::new);
|
||||||
|
this.client = client;
|
||||||
|
this.clusterService = clusterService;
|
||||||
|
this.remoteClusterService = transportService.getRemoteClusterService();
|
||||||
|
this.persistentTasksService = persistentTasksService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doExecute(Request request, ActionListener<Response> listener) {
|
||||||
|
ClusterState localClusterState = clusterService.state();
|
||||||
|
IndexMetaData followIndexMetadata = localClusterState.getMetaData().index(request.followIndex);
|
||||||
|
|
||||||
|
String[] indices = new String[]{request.getLeaderIndex()};
|
||||||
|
Map<String, List<String>> remoteClusterIndices = remoteClusterService.groupClusterIndices(indices, s -> false);
|
||||||
|
if (remoteClusterIndices.containsKey(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) {
|
||||||
|
// Following an index in local cluster, so use local cluster state to fetch leader IndexMetaData:
|
||||||
|
IndexMetaData leaderIndexMetadata = localClusterState.getMetaData().index(request.leaderIndex);
|
||||||
|
start(request, null, leaderIndexMetadata, followIndexMetadata, listener);
|
||||||
|
} else {
|
||||||
|
// Following an index in remote cluster, so use remote client to fetch leader IndexMetaData:
|
||||||
|
assert remoteClusterIndices.size() == 1;
|
||||||
|
Map.Entry<String, List<String>> entry = remoteClusterIndices.entrySet().iterator().next();
|
||||||
|
assert entry.getValue().size() == 1;
|
||||||
|
String clusterNameAlias = entry.getKey();
|
||||||
|
String leaderIndex = entry.getValue().get(0);
|
||||||
|
|
||||||
|
Client remoteClient = client.getRemoteClusterClient(clusterNameAlias);
|
||||||
|
ClusterStateRequest clusterStateRequest = new ClusterStateRequest();
|
||||||
|
clusterStateRequest.clear();
|
||||||
|
clusterStateRequest.metaData(true);
|
||||||
|
clusterStateRequest.indices(leaderIndex);
|
||||||
|
remoteClient.admin().cluster().state(clusterStateRequest, ActionListener.wrap(r -> {
|
||||||
|
ClusterState remoteClusterState = r.getState();
|
||||||
|
IndexMetaData leaderIndexMetadata = remoteClusterState.getMetaData().index(leaderIndex);
|
||||||
|
start(request, clusterNameAlias, leaderIndexMetadata, followIndexMetadata, listener);
|
||||||
|
}, listener::onFailure));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs validation on the provided leader and follow {@link IndexMetaData} instances and then
|
||||||
|
* creates a persistent task for each leader primary shard. This persistent tasks track changes in the leader
|
||||||
|
* shard and replicate these changes to a follower shard.
|
||||||
|
*
|
||||||
|
* Currently the following validation is performed:
|
||||||
|
* <ul>
|
||||||
|
* <li>The leader index and follow index need to have the same number of primary shards</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
void start(Request request, String clusterNameAlias, IndexMetaData leaderIndexMetadata, IndexMetaData followIndexMetadata,
|
||||||
|
ActionListener<Response> handler) {
|
||||||
|
if (leaderIndexMetadata == null) {
|
||||||
|
handler.onFailure(new IllegalArgumentException("leader index [" + request.leaderIndex + "] does not exist"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (followIndexMetadata == null) {
|
||||||
|
handler.onFailure(new IllegalArgumentException("follow index [" + request.followIndex + "] does not exist"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leaderIndexMetadata.getNumberOfShards() != followIndexMetadata.getNumberOfShards()) {
|
||||||
|
handler.onFailure(new IllegalArgumentException("leader index primary shards [" +
|
||||||
|
leaderIndexMetadata.getNumberOfShards() + "] does not match with the number of " +
|
||||||
|
"shards of the follow index [" + followIndexMetadata.getNumberOfShards() + "]"));
|
||||||
|
// TODO: other validation checks
|
||||||
|
} else {
|
||||||
|
final int numShards = followIndexMetadata.getNumberOfShards();
|
||||||
|
final AtomicInteger counter = new AtomicInteger(numShards);
|
||||||
|
final AtomicReferenceArray<Object> responses = new AtomicReferenceArray<>(followIndexMetadata.getNumberOfShards());
|
||||||
|
for (int i = 0; i < numShards; i++) {
|
||||||
|
final int shardId = i;
|
||||||
|
String taskId = followIndexMetadata.getIndexUUID() + "-" + shardId;
|
||||||
|
ShardFollowTask shardFollowTask = new ShardFollowTask(clusterNameAlias,
|
||||||
|
new ShardId(followIndexMetadata.getIndex(), shardId),
|
||||||
|
new ShardId(leaderIndexMetadata.getIndex(), shardId),
|
||||||
|
request.batchSize, request.concurrentProcessors, request.processorMaxTranslogBytes);
|
||||||
|
persistentTasksService.startPersistentTask(taskId, ShardFollowTask.NAME, shardFollowTask,
|
||||||
|
new ActionListener<PersistentTasksCustomMetaData.PersistentTask<ShardFollowTask>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(PersistentTasksCustomMetaData.PersistentTask<ShardFollowTask> task) {
|
||||||
|
responses.set(shardId, task);
|
||||||
|
finalizeResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Exception e) {
|
||||||
|
responses.set(shardId, e);
|
||||||
|
finalizeResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
void finalizeResponse() {
|
||||||
|
Exception error = null;
|
||||||
|
if (counter.decrementAndGet() == 0) {
|
||||||
|
for (int j = 0; j < responses.length(); j++) {
|
||||||
|
Object response = responses.get(j);
|
||||||
|
if (response instanceof Exception) {
|
||||||
|
if (error == null) {
|
||||||
|
error = (Exception) response;
|
||||||
|
} else {
|
||||||
|
error.addSuppressed((Throwable) response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error == null) {
|
||||||
|
// include task ids?
|
||||||
|
handler.onResponse(new Response());
|
||||||
|
} else {
|
||||||
|
// TODO: cancel all started tasks
|
||||||
|
handler.onFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,297 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.Action;
|
||||||
|
import org.elasticsearch.action.ActionRequestValidationException;
|
||||||
|
import org.elasticsearch.action.ActionResponse;
|
||||||
|
import org.elasticsearch.action.support.ActionFilters;
|
||||||
|
import org.elasticsearch.action.support.single.shard.SingleShardOperationRequestBuilder;
|
||||||
|
import org.elasticsearch.action.support.single.shard.SingleShardRequest;
|
||||||
|
import org.elasticsearch.action.support.single.shard.TransportSingleShardAction;
|
||||||
|
import org.elasticsearch.client.ElasticsearchClient;
|
||||||
|
import org.elasticsearch.cluster.ClusterState;
|
||||||
|
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
||||||
|
import org.elasticsearch.cluster.routing.ShardsIterator;
|
||||||
|
import org.elasticsearch.cluster.service.ClusterService;
|
||||||
|
import org.elasticsearch.common.inject.Inject;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamInput;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.index.IndexService;
|
||||||
|
import org.elasticsearch.index.shard.IndexShard;
|
||||||
|
import org.elasticsearch.index.shard.IndexShardNotStartedException;
|
||||||
|
import org.elasticsearch.index.shard.IndexShardState;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.index.translog.Translog;
|
||||||
|
import org.elasticsearch.indices.IndicesService;
|
||||||
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
import org.elasticsearch.transport.TransportService;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.PriorityQueue;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
|
import static org.elasticsearch.action.ValidateActions.addValidationError;
|
||||||
|
|
||||||
|
public class ShardChangesAction extends Action<ShardChangesAction.Request, ShardChangesAction.Response, ShardChangesAction.RequestBuilder> {
|
||||||
|
|
||||||
|
public static final ShardChangesAction INSTANCE = new ShardChangesAction();
|
||||||
|
public static final String NAME = "cluster:admin/xpack/ccr/shard_changes";
|
||||||
|
|
||||||
|
private ShardChangesAction() {
|
||||||
|
super(NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestBuilder newRequestBuilder(ElasticsearchClient client) {
|
||||||
|
return new RequestBuilder(client, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response newResponse() {
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Request extends SingleShardRequest<Request> {
|
||||||
|
|
||||||
|
private long minSeqNo;
|
||||||
|
private long maxSeqNo;
|
||||||
|
private ShardId shardId;
|
||||||
|
private long maxTranslogsBytes = ShardFollowTasksExecutor.DEFAULT_MAX_TRANSLOG_BYTES;
|
||||||
|
|
||||||
|
public Request(ShardId shardId) {
|
||||||
|
super(shardId.getIndexName());
|
||||||
|
this.shardId = shardId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Request() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShardId getShard() {
|
||||||
|
return shardId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMinSeqNo() {
|
||||||
|
return minSeqNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMinSeqNo(long minSeqNo) {
|
||||||
|
this.minSeqNo = minSeqNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxSeqNo() {
|
||||||
|
return maxSeqNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxSeqNo(long maxSeqNo) {
|
||||||
|
this.maxSeqNo = maxSeqNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxTranslogsBytes() {
|
||||||
|
return maxTranslogsBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxTranslogsBytes(long maxTranslogsBytes) {
|
||||||
|
this.maxTranslogsBytes = maxTranslogsBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActionRequestValidationException validate() {
|
||||||
|
ActionRequestValidationException validationException = null;
|
||||||
|
if (minSeqNo < 0) {
|
||||||
|
validationException = addValidationError("minSeqNo [" + minSeqNo + "] cannot be lower than 0", validationException);
|
||||||
|
}
|
||||||
|
if (maxSeqNo < minSeqNo) {
|
||||||
|
validationException = addValidationError("minSeqNo [" + minSeqNo + "] cannot be larger than maxSeqNo ["
|
||||||
|
+ maxSeqNo + "]", validationException);
|
||||||
|
}
|
||||||
|
if (maxTranslogsBytes <= 0) {
|
||||||
|
validationException = addValidationError("maxTranslogsBytes [" + maxTranslogsBytes + "] must be larger than 0",
|
||||||
|
validationException);
|
||||||
|
}
|
||||||
|
return validationException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(StreamInput in) throws IOException {
|
||||||
|
super.readFrom(in);
|
||||||
|
minSeqNo = in.readVLong();
|
||||||
|
maxSeqNo = in.readVLong();
|
||||||
|
shardId = ShardId.readShardId(in);
|
||||||
|
maxTranslogsBytes = in.readVLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(StreamOutput out) throws IOException {
|
||||||
|
super.writeTo(out);
|
||||||
|
out.writeVLong(minSeqNo);
|
||||||
|
out.writeVLong(maxSeqNo);
|
||||||
|
shardId.writeTo(out);
|
||||||
|
out.writeVLong(maxTranslogsBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
final Request request = (Request) o;
|
||||||
|
return minSeqNo == request.minSeqNo &&
|
||||||
|
maxSeqNo == request.maxSeqNo &&
|
||||||
|
Objects.equals(shardId, request.shardId) &&
|
||||||
|
maxTranslogsBytes == request.maxTranslogsBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(minSeqNo, maxSeqNo, shardId, maxTranslogsBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Response extends ActionResponse {
|
||||||
|
|
||||||
|
private Translog.Operation[] operations;
|
||||||
|
|
||||||
|
Response() {
|
||||||
|
}
|
||||||
|
|
||||||
|
Response(final Translog.Operation[] operations) {
|
||||||
|
this.operations = operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Translog.Operation[] getOperations() {
|
||||||
|
return operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(final StreamInput in) throws IOException {
|
||||||
|
super.readFrom(in);
|
||||||
|
operations = in.readArray(Translog.Operation::readOperation, Translog.Operation[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(final StreamOutput out) throws IOException {
|
||||||
|
super.writeTo(out);
|
||||||
|
out.writeArray(Translog.Operation::writeOperation, operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
final Response response = (Response) o;
|
||||||
|
return Arrays.equals(operations, response.operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Arrays.hashCode(operations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class RequestBuilder extends SingleShardOperationRequestBuilder<Request, Response, RequestBuilder> {
|
||||||
|
|
||||||
|
RequestBuilder(ElasticsearchClient client, Action<Request, Response, RequestBuilder> action) {
|
||||||
|
super(client, action, new Request());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TransportAction extends TransportSingleShardAction<Request, Response> {
|
||||||
|
|
||||||
|
private final IndicesService indicesService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TransportAction(Settings settings,
|
||||||
|
ThreadPool threadPool,
|
||||||
|
ClusterService clusterService,
|
||||||
|
TransportService transportService,
|
||||||
|
ActionFilters actionFilters,
|
||||||
|
IndexNameExpressionResolver indexNameExpressionResolver,
|
||||||
|
IndicesService indicesService) {
|
||||||
|
super(settings, NAME, threadPool, clusterService, transportService, actionFilters,
|
||||||
|
indexNameExpressionResolver, Request::new, ThreadPool.Names.GET);
|
||||||
|
this.indicesService = indicesService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response shardOperation(Request request, ShardId shardId) throws IOException {
|
||||||
|
IndexService indexService = indicesService.indexServiceSafe(request.getShard().getIndex());
|
||||||
|
IndexShard indexShard = indexService.getShard(request.getShard().id());
|
||||||
|
|
||||||
|
return getOperationsBetween(indexShard, request.minSeqNo, request.maxSeqNo, request.maxTranslogsBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean resolveIndex(Request request) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShardsIterator shards(ClusterState state, InternalRequest request) {
|
||||||
|
return state.routingTable()
|
||||||
|
.index(request.concreteIndex())
|
||||||
|
.shard(request.request().getShard().id())
|
||||||
|
.activeInitializingShardsRandomIt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response newResponse() {
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Translog.Operation[] EMPTY_OPERATIONS_ARRAY = new Translog.Operation[0];
|
||||||
|
|
||||||
|
static Response getOperationsBetween(IndexShard indexShard, long minSeqNo, long maxSeqNo, long byteLimit) throws IOException {
|
||||||
|
if (indexShard.state() != IndexShardState.STARTED) {
|
||||||
|
throw new IndexShardNotStartedException(indexShard.shardId(), indexShard.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
long seenBytes = 0;
|
||||||
|
long nextExpectedSeqNo = minSeqNo;
|
||||||
|
final Queue<Translog.Operation> orderedOps = new PriorityQueue<>(Comparator.comparingLong(Translog.Operation::seqNo));
|
||||||
|
|
||||||
|
final List<Translog.Operation> operations = new ArrayList<>();
|
||||||
|
try (Translog.Snapshot snapshot = indexShard.newTranslogSnapshotBetween(minSeqNo, maxSeqNo)) {
|
||||||
|
for (Translog.Operation unorderedOp = snapshot.next(); unorderedOp != null; unorderedOp = snapshot.next()) {
|
||||||
|
if (unorderedOp.seqNo() < minSeqNo || unorderedOp.seqNo() > maxSeqNo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedOps.add(unorderedOp);
|
||||||
|
while (orderedOps.peek() != null && orderedOps.peek().seqNo() == nextExpectedSeqNo) {
|
||||||
|
Translog.Operation orderedOp = orderedOps.poll();
|
||||||
|
if (seenBytes < byteLimit) {
|
||||||
|
nextExpectedSeqNo++;
|
||||||
|
seenBytes += orderedOp.estimateSize();
|
||||||
|
operations.add(orderedOp);
|
||||||
|
if (nextExpectedSeqNo > maxSeqNo) {
|
||||||
|
return new Response(operations.toArray(EMPTY_OPERATIONS_ARRAY));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new Response(operations.toArray(EMPTY_OPERATIONS_ARRAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextExpectedSeqNo >= maxSeqNo) {
|
||||||
|
return new Response(operations.toArray(EMPTY_OPERATIONS_ARRAY));
|
||||||
|
} else {
|
||||||
|
String message = "Not all operations between min_seq_no [" + minSeqNo + "] and max_seq_no [" + maxSeqNo +
|
||||||
|
"] found, tracker checkpoint [" + nextExpectedSeqNo + "]";
|
||||||
|
throw new IllegalStateException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.ParseField;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamInput;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||||
|
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.persistent.AllocatedPersistentTask;
|
||||||
|
import org.elasticsearch.tasks.Task;
|
||||||
|
import org.elasticsearch.tasks.TaskId;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
public class ShardFollowNodeTask extends AllocatedPersistentTask {
|
||||||
|
|
||||||
|
private final AtomicLong processedGlobalCheckpoint = new AtomicLong();
|
||||||
|
|
||||||
|
ShardFollowNodeTask(long id, String type, String action, String description, TaskId parentTask, Map<String, String> headers) {
|
||||||
|
super(id, type, action, description, parentTask, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateProcessedGlobalCheckpoint(long processedGlobalCheckpoint) {
|
||||||
|
this.processedGlobalCheckpoint.set(processedGlobalCheckpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Task.Status getStatus() {
|
||||||
|
return new Status(processedGlobalCheckpoint.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Status implements Task.Status {
|
||||||
|
|
||||||
|
public static final String NAME = "shard-follow-node-task-status";
|
||||||
|
|
||||||
|
static final ParseField PROCESSED_GLOBAL_CHECKPOINT_FIELD = new ParseField("processed_global_checkpoint");
|
||||||
|
|
||||||
|
static final ConstructingObjectParser<Status, Void> PARSER =
|
||||||
|
new ConstructingObjectParser<>(NAME, args -> new Status((Long) args[0]));
|
||||||
|
|
||||||
|
static {
|
||||||
|
PARSER.declareLong(ConstructingObjectParser.constructorArg(), PROCESSED_GLOBAL_CHECKPOINT_FIELD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final long processedGlobalCheckpoint;
|
||||||
|
|
||||||
|
Status(long processedGlobalCheckpoint) {
|
||||||
|
this.processedGlobalCheckpoint = processedGlobalCheckpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Status(StreamInput in) throws IOException {
|
||||||
|
this.processedGlobalCheckpoint = in.readVLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getProcessedGlobalCheckpoint() {
|
||||||
|
return processedGlobalCheckpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getWriteableName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(StreamOutput out) throws IOException {
|
||||||
|
out.writeVLong(processedGlobalCheckpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
builder.startObject();
|
||||||
|
{
|
||||||
|
builder.field(PROCESSED_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), processedGlobalCheckpoint);
|
||||||
|
}
|
||||||
|
builder.endObject();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Status fromXContent(XContentParser parser) {
|
||||||
|
return PARSER.apply(parser, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Status status = (Status) o;
|
||||||
|
return processedGlobalCheckpoint == status.processedGlobalCheckpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(processedGlobalCheckpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return Strings.toString(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.ParseField;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamInput;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||||
|
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
|
||||||
|
import org.elasticsearch.common.xcontent.ObjectParser;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.tasks.Task;
|
||||||
|
import org.elasticsearch.persistent.PersistentTaskParams;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ShardFollowTask implements PersistentTaskParams {
|
||||||
|
|
||||||
|
public static final String NAME = "shard_follow";
|
||||||
|
|
||||||
|
static final ParseField LEADER_CLUSTER_ALIAS_FIELD = new ParseField("leader_cluster_alias");
|
||||||
|
static final ParseField FOLLOW_SHARD_INDEX_FIELD = new ParseField("follow_shard_index");
|
||||||
|
static final ParseField FOLLOW_SHARD_INDEX_UUID_FIELD = new ParseField("follow_shard_index_uuid");
|
||||||
|
static final ParseField FOLLOW_SHARD_SHARDID_FIELD = new ParseField("follow_shard_shard");
|
||||||
|
static final ParseField LEADER_SHARD_INDEX_FIELD = new ParseField("leader_shard_index");
|
||||||
|
static final ParseField LEADER_SHARD_INDEX_UUID_FIELD = new ParseField("leader_shard_index_uuid");
|
||||||
|
static final ParseField LEADER_SHARD_SHARDID_FIELD = new ParseField("leader_shard_shard");
|
||||||
|
public static final ParseField MAX_CHUNK_SIZE = new ParseField("max_chunk_size");
|
||||||
|
public static final ParseField NUM_CONCURRENT_CHUNKS = new ParseField("max_concurrent_chunks");
|
||||||
|
public static final ParseField PROCESSOR_MAX_TRANSLOG_BYTES_PER_REQUEST = new ParseField("processor_max_translog_bytes");
|
||||||
|
|
||||||
|
public static ConstructingObjectParser<ShardFollowTask, Void> PARSER = new ConstructingObjectParser<>(NAME,
|
||||||
|
(a) -> new ShardFollowTask((String) a[0], new ShardId((String) a[1], (String) a[2], (int) a[3]),
|
||||||
|
new ShardId((String) a[4], (String) a[5], (int) a[6]), (long) a[7], (int) a[8], (long) a[9]));
|
||||||
|
|
||||||
|
static {
|
||||||
|
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), LEADER_CLUSTER_ALIAS_FIELD);
|
||||||
|
PARSER.declareString(ConstructingObjectParser.constructorArg(), FOLLOW_SHARD_INDEX_FIELD);
|
||||||
|
PARSER.declareString(ConstructingObjectParser.constructorArg(), FOLLOW_SHARD_INDEX_UUID_FIELD);
|
||||||
|
PARSER.declareInt(ConstructingObjectParser.constructorArg(), FOLLOW_SHARD_SHARDID_FIELD);
|
||||||
|
PARSER.declareString(ConstructingObjectParser.constructorArg(), LEADER_SHARD_INDEX_FIELD);
|
||||||
|
PARSER.declareString(ConstructingObjectParser.constructorArg(), LEADER_SHARD_INDEX_UUID_FIELD);
|
||||||
|
PARSER.declareInt(ConstructingObjectParser.constructorArg(), LEADER_SHARD_SHARDID_FIELD);
|
||||||
|
PARSER.declareLong(ConstructingObjectParser.constructorArg(), MAX_CHUNK_SIZE);
|
||||||
|
PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUM_CONCURRENT_CHUNKS);
|
||||||
|
PARSER.declareLong(ConstructingObjectParser.constructorArg(), PROCESSOR_MAX_TRANSLOG_BYTES_PER_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String leaderClusterAlias;
|
||||||
|
private final ShardId followShardId;
|
||||||
|
private final ShardId leaderShardId;
|
||||||
|
private final long maxChunkSize;
|
||||||
|
private final int numConcurrentChunks;
|
||||||
|
private final long processorMaxTranslogBytes;
|
||||||
|
|
||||||
|
ShardFollowTask(String leaderClusterAlias, ShardId followShardId, ShardId leaderShardId, long maxChunkSize,
|
||||||
|
int numConcurrentChunks, long processorMaxTranslogBytes) {
|
||||||
|
this.leaderClusterAlias = leaderClusterAlias;
|
||||||
|
this.followShardId = followShardId;
|
||||||
|
this.leaderShardId = leaderShardId;
|
||||||
|
this.maxChunkSize = maxChunkSize;
|
||||||
|
this.numConcurrentChunks = numConcurrentChunks;
|
||||||
|
this.processorMaxTranslogBytes = processorMaxTranslogBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShardFollowTask(StreamInput in) throws IOException {
|
||||||
|
this.leaderClusterAlias = in.readOptionalString();
|
||||||
|
this.followShardId = ShardId.readShardId(in);
|
||||||
|
this.leaderShardId = ShardId.readShardId(in);
|
||||||
|
this.maxChunkSize = in.readVLong();
|
||||||
|
this.numConcurrentChunks = in.readVInt();
|
||||||
|
this.processorMaxTranslogBytes = in.readVLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLeaderClusterAlias() {
|
||||||
|
return leaderClusterAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShardId getFollowShardId() {
|
||||||
|
return followShardId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShardId getLeaderShardId() {
|
||||||
|
return leaderShardId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxChunkSize() {
|
||||||
|
return maxChunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNumConcurrentChunks() {
|
||||||
|
return numConcurrentChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getProcessorMaxTranslogBytes() {
|
||||||
|
return processorMaxTranslogBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getWriteableName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(StreamOutput out) throws IOException {
|
||||||
|
out.writeOptionalString(leaderClusterAlias);
|
||||||
|
followShardId.writeTo(out);
|
||||||
|
leaderShardId.writeTo(out);
|
||||||
|
out.writeVLong(maxChunkSize);
|
||||||
|
out.writeVInt(numConcurrentChunks);
|
||||||
|
out.writeVLong(processorMaxTranslogBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShardFollowTask fromXContent(XContentParser parser) {
|
||||||
|
return PARSER.apply(parser, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
builder.startObject();
|
||||||
|
if (leaderClusterAlias != null) {
|
||||||
|
builder.field(LEADER_CLUSTER_ALIAS_FIELD.getPreferredName(), leaderClusterAlias);
|
||||||
|
}
|
||||||
|
builder.field(FOLLOW_SHARD_INDEX_FIELD.getPreferredName(), followShardId.getIndex().getName());
|
||||||
|
builder.field(FOLLOW_SHARD_INDEX_UUID_FIELD.getPreferredName(), followShardId.getIndex().getUUID());
|
||||||
|
builder.field(FOLLOW_SHARD_SHARDID_FIELD.getPreferredName(), followShardId.id());
|
||||||
|
builder.field(LEADER_SHARD_INDEX_FIELD.getPreferredName(), leaderShardId.getIndex().getName());
|
||||||
|
builder.field(LEADER_SHARD_INDEX_UUID_FIELD.getPreferredName(), leaderShardId.getIndex().getUUID());
|
||||||
|
builder.field(LEADER_SHARD_SHARDID_FIELD.getPreferredName(), leaderShardId.id());
|
||||||
|
builder.field(MAX_CHUNK_SIZE.getPreferredName(), maxChunkSize);
|
||||||
|
builder.field(NUM_CONCURRENT_CHUNKS.getPreferredName(), numConcurrentChunks);
|
||||||
|
builder.field(PROCESSOR_MAX_TRANSLOG_BYTES_PER_REQUEST.getPreferredName(), processorMaxTranslogBytes);
|
||||||
|
return builder.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
ShardFollowTask that = (ShardFollowTask) o;
|
||||||
|
return Objects.equals(leaderClusterAlias, that.leaderClusterAlias) &&
|
||||||
|
Objects.equals(followShardId, that.followShardId) &&
|
||||||
|
Objects.equals(leaderShardId, that.leaderShardId) &&
|
||||||
|
maxChunkSize == that.maxChunkSize &&
|
||||||
|
numConcurrentChunks == that.numConcurrentChunks &&
|
||||||
|
processorMaxTranslogBytes == that.processorMaxTranslogBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(leaderClusterAlias, followShardId, leaderShardId, maxChunkSize, numConcurrentChunks,
|
||||||
|
processorMaxTranslogBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return Strings.toString(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,358 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||||
|
import org.elasticsearch.ElasticsearchException;
|
||||||
|
import org.elasticsearch.action.ActionListener;
|
||||||
|
import org.elasticsearch.action.admin.indices.stats.IndexStats;
|
||||||
|
import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest;
|
||||||
|
import org.elasticsearch.action.admin.indices.stats.ShardStats;
|
||||||
|
import org.elasticsearch.client.Client;
|
||||||
|
import org.elasticsearch.cluster.ClusterState;
|
||||||
|
import org.elasticsearch.cluster.routing.IndexRoutingTable;
|
||||||
|
import org.elasticsearch.common.logging.Loggers;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.transport.NetworkExceptionHelper;
|
||||||
|
import org.elasticsearch.common.unit.TimeValue;
|
||||||
|
import org.elasticsearch.common.util.concurrent.AbstractRunnable;
|
||||||
|
import org.elasticsearch.common.util.concurrent.CountDown;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.index.translog.Translog;
|
||||||
|
import org.elasticsearch.persistent.PersistentTasksCustomMetaData;
|
||||||
|
import org.elasticsearch.tasks.Task;
|
||||||
|
import org.elasticsearch.tasks.TaskId;
|
||||||
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
import org.elasticsearch.xpack.ccr.Ccr;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsRequest;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsResponse;
|
||||||
|
import org.elasticsearch.persistent.AllocatedPersistentTask;
|
||||||
|
import org.elasticsearch.persistent.PersistentTasksExecutor;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.LongConsumer;
|
||||||
|
|
||||||
|
public class ShardFollowTasksExecutor extends PersistentTasksExecutor<ShardFollowTask> {
|
||||||
|
|
||||||
|
static final long DEFAULT_BATCH_SIZE = 1024;
|
||||||
|
static final int PROCESSOR_RETRY_LIMIT = 16;
|
||||||
|
static final int DEFAULT_CONCURRENT_PROCESSORS = 1;
|
||||||
|
static final long DEFAULT_MAX_TRANSLOG_BYTES= Long.MAX_VALUE;
|
||||||
|
private static final TimeValue RETRY_TIMEOUT = TimeValue.timeValueMillis(500);
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final ThreadPool threadPool;
|
||||||
|
|
||||||
|
public ShardFollowTasksExecutor(Settings settings, Client client, ThreadPool threadPool) {
|
||||||
|
super(settings, ShardFollowTask.NAME, Ccr.CCR_THREAD_POOL_NAME);
|
||||||
|
this.client = client;
|
||||||
|
this.threadPool = threadPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(ShardFollowTask params, ClusterState clusterState) {
|
||||||
|
if (params.getLeaderClusterAlias() == null) {
|
||||||
|
// We can only validate IndexRoutingTable in local cluster,
|
||||||
|
// for remote cluster we would need to make a remote call and we cannot do this here.
|
||||||
|
IndexRoutingTable routingTable = clusterState.getRoutingTable().index(params.getLeaderShardId().getIndex());
|
||||||
|
if (routingTable.shard(params.getLeaderShardId().id()).primaryShard().started() == false) {
|
||||||
|
throw new IllegalArgumentException("Not all copies of leader shard are started");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexRoutingTable routingTable = clusterState.getRoutingTable().index(params.getFollowShardId().getIndex());
|
||||||
|
if (routingTable.shard(params.getFollowShardId().id()).primaryShard().started() == false) {
|
||||||
|
throw new IllegalArgumentException("Not all copies of follow shard are started");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AllocatedPersistentTask createTask(long id, String type, String action, TaskId parentTaskId,
|
||||||
|
PersistentTasksCustomMetaData.PersistentTask<ShardFollowTask> taskInProgress,
|
||||||
|
Map<String, String> headers) {
|
||||||
|
return new ShardFollowNodeTask(id, type, action, getDescription(taskInProgress), parentTaskId, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void nodeOperation(AllocatedPersistentTask task, ShardFollowTask params, Task.Status status) {
|
||||||
|
ShardFollowNodeTask shardFollowNodeTask = (ShardFollowNodeTask) task;
|
||||||
|
Client leaderClient = params.getLeaderClusterAlias() != null ?
|
||||||
|
this.client.getRemoteClusterClient(params.getLeaderClusterAlias()) : this.client;
|
||||||
|
logger.info("Starting shard following [{}]", params);
|
||||||
|
fetchGlobalCheckpoint(client, params.getFollowShardId(),
|
||||||
|
followGlobalCheckPoint -> prepare(leaderClient, shardFollowNodeTask, params, followGlobalCheckPoint), task::markAsFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepare(Client leaderClient, ShardFollowNodeTask task, ShardFollowTask params, long followGlobalCheckPoint) {
|
||||||
|
if (task.getState() != AllocatedPersistentTask.State.STARTED) {
|
||||||
|
// TODO: need better cancellation control
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ShardId leaderShard = params.getLeaderShardId();
|
||||||
|
final ShardId followerShard = params.getFollowShardId();
|
||||||
|
fetchGlobalCheckpoint(leaderClient, leaderShard, leaderGlobalCheckPoint -> {
|
||||||
|
// TODO: check if both indices have the same history uuid
|
||||||
|
if (leaderGlobalCheckPoint == followGlobalCheckPoint) {
|
||||||
|
retry(leaderClient, task, params, followGlobalCheckPoint);
|
||||||
|
} else {
|
||||||
|
assert followGlobalCheckPoint < leaderGlobalCheckPoint : "followGlobalCheckPoint [" + followGlobalCheckPoint +
|
||||||
|
"] is not below leaderGlobalCheckPoint [" + leaderGlobalCheckPoint + "]";
|
||||||
|
Executor ccrExecutor = threadPool.executor(Ccr.CCR_THREAD_POOL_NAME);
|
||||||
|
Consumer<Exception> handler = e -> {
|
||||||
|
if (e == null) {
|
||||||
|
task.updateProcessedGlobalCheckpoint(leaderGlobalCheckPoint);
|
||||||
|
prepare(leaderClient, task, params, leaderGlobalCheckPoint);
|
||||||
|
} else {
|
||||||
|
task.markAsFailed(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ChunksCoordinator coordinator = new ChunksCoordinator(client, leaderClient, ccrExecutor, params.getMaxChunkSize(),
|
||||||
|
params.getNumConcurrentChunks(), params.getProcessorMaxTranslogBytes(), leaderShard, followerShard, handler);
|
||||||
|
coordinator.createChucks(followGlobalCheckPoint, leaderGlobalCheckPoint);
|
||||||
|
coordinator.start();
|
||||||
|
}
|
||||||
|
}, task::markAsFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void retry(Client leaderClient, ShardFollowNodeTask task, ShardFollowTask params, long followGlobalCheckPoint) {
|
||||||
|
threadPool.schedule(RETRY_TIMEOUT, Ccr.CCR_THREAD_POOL_NAME, new AbstractRunnable() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Exception e) {
|
||||||
|
task.markAsFailed(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doRun() throws Exception {
|
||||||
|
prepare(leaderClient, task, params, followGlobalCheckPoint);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchGlobalCheckpoint(Client client, ShardId shardId, LongConsumer handler, Consumer<Exception> errorHandler) {
|
||||||
|
client.admin().indices().stats(new IndicesStatsRequest().indices(shardId.getIndexName()), ActionListener.wrap(r -> {
|
||||||
|
IndexStats indexStats = r.getIndex(shardId.getIndexName());
|
||||||
|
Optional<ShardStats> filteredShardStats = Arrays.stream(indexStats.getShards())
|
||||||
|
.filter(shardStats -> shardStats.getShardRouting().shardId().equals(shardId))
|
||||||
|
.filter(shardStats -> shardStats.getShardRouting().primary())
|
||||||
|
.findAny();
|
||||||
|
|
||||||
|
if (filteredShardStats.isPresent()) {
|
||||||
|
// Treat -1 as 0. If no indexing has happened in leader shard then global checkpoint is -1.
|
||||||
|
final long globalCheckPoint = Math.max(0, filteredShardStats.get().getSeqNoStats().getGlobalCheckpoint());
|
||||||
|
handler.accept(globalCheckPoint);
|
||||||
|
} else {
|
||||||
|
errorHandler.accept(new IllegalArgumentException("Cannot find shard stats for shard " + shardId));
|
||||||
|
}
|
||||||
|
}, errorHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ChunksCoordinator {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Loggers.getLogger(ChunksCoordinator.class);
|
||||||
|
|
||||||
|
private final Client followerClient;
|
||||||
|
private final Client leaderClient;
|
||||||
|
private final Executor ccrExecutor;
|
||||||
|
|
||||||
|
private final long batchSize;
|
||||||
|
private final int concurrentProcessors;
|
||||||
|
private final long processorMaxTranslogBytes;
|
||||||
|
private final ShardId leaderShard;
|
||||||
|
private final ShardId followerShard;
|
||||||
|
private final Consumer<Exception> handler;
|
||||||
|
|
||||||
|
private final CountDown countDown;
|
||||||
|
private final Queue<long[]> chunks = new ConcurrentLinkedQueue<>();
|
||||||
|
private final AtomicReference<Exception> failureHolder = new AtomicReference<>();
|
||||||
|
|
||||||
|
ChunksCoordinator(Client followerClient, Client leaderClient, Executor ccrExecutor, long batchSize, int concurrentProcessors,
|
||||||
|
long processorMaxTranslogBytes, ShardId leaderShard, ShardId followerShard, Consumer<Exception> handler) {
|
||||||
|
this.followerClient = followerClient;
|
||||||
|
this.leaderClient = leaderClient;
|
||||||
|
this.ccrExecutor = ccrExecutor;
|
||||||
|
this.batchSize = batchSize;
|
||||||
|
this.concurrentProcessors = concurrentProcessors;
|
||||||
|
this.processorMaxTranslogBytes = processorMaxTranslogBytes;
|
||||||
|
this.leaderShard = leaderShard;
|
||||||
|
this.followerShard = followerShard;
|
||||||
|
this.handler = handler;
|
||||||
|
this.countDown = new CountDown(concurrentProcessors);
|
||||||
|
}
|
||||||
|
|
||||||
|
void createChucks(long from, long to) {
|
||||||
|
LOGGER.debug("{} Creating chunks for operation range [{}] to [{}]", leaderShard, from, to);
|
||||||
|
for (long i = from; i < to; i += batchSize) {
|
||||||
|
long v2 = i + batchSize < to ? i + batchSize : to;
|
||||||
|
chunks.add(new long[]{i == from ? i : i + 1, v2});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
LOGGER.debug("{} Start coordination of [{}] chunks with [{}] concurrent processors",
|
||||||
|
leaderShard, chunks.size(), concurrentProcessors);
|
||||||
|
for (int i = 0; i < concurrentProcessors; i++) {
|
||||||
|
ccrExecutor.execute(new AbstractRunnable() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Exception e) {
|
||||||
|
assert e != null;
|
||||||
|
LOGGER.error(() -> new ParameterizedMessage("{} Failure starting processor", leaderShard), e);
|
||||||
|
postProcessChuck(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doRun() throws Exception {
|
||||||
|
processNextChunk();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void processNextChunk() {
|
||||||
|
long[] chunk = chunks.poll();
|
||||||
|
if (chunk == null) {
|
||||||
|
postProcessChuck(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOGGER.debug("{} Processing chunk [{}/{}]", leaderShard, chunk[0], chunk[1]);
|
||||||
|
Consumer<Exception> processorHandler = e -> {
|
||||||
|
if (e == null) {
|
||||||
|
LOGGER.debug("{} Successfully processed chunk [{}/{}]", leaderShard, chunk[0], chunk[1]);
|
||||||
|
processNextChunk();
|
||||||
|
} else {
|
||||||
|
LOGGER.error(() -> new ParameterizedMessage("{} Failure processing chunk [{}/{}]",
|
||||||
|
leaderShard, chunk[0], chunk[1]), e);
|
||||||
|
postProcessChuck(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ChunkProcessor processor = new ChunkProcessor(leaderClient, followerClient, chunks, ccrExecutor, leaderShard,
|
||||||
|
followerShard, processorHandler);
|
||||||
|
processor.start(chunk[0], chunk[1], processorMaxTranslogBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void postProcessChuck(Exception e) {
|
||||||
|
if (failureHolder.compareAndSet(null, e) == false) {
|
||||||
|
Exception firstFailure = failureHolder.get();
|
||||||
|
firstFailure.addSuppressed(e);
|
||||||
|
}
|
||||||
|
if (countDown.countDown()) {
|
||||||
|
handler.accept(failureHolder.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Queue<long[]> getChunks() {
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ChunkProcessor {
|
||||||
|
|
||||||
|
private final Client leaderClient;
|
||||||
|
private final Client followerClient;
|
||||||
|
private final Queue<long[]> chunks;
|
||||||
|
private final Executor ccrExecutor;
|
||||||
|
|
||||||
|
private final ShardId leaderShard;
|
||||||
|
private final ShardId followerShard;
|
||||||
|
private final Consumer<Exception> handler;
|
||||||
|
final AtomicInteger retryCounter = new AtomicInteger(0);
|
||||||
|
|
||||||
|
ChunkProcessor(Client leaderClient, Client followerClient, Queue<long[]> chunks, Executor ccrExecutor, ShardId leaderShard,
|
||||||
|
ShardId followerShard, Consumer<Exception> handler) {
|
||||||
|
this.leaderClient = leaderClient;
|
||||||
|
this.followerClient = followerClient;
|
||||||
|
this.chunks = chunks;
|
||||||
|
this.ccrExecutor = ccrExecutor;
|
||||||
|
this.leaderShard = leaderShard;
|
||||||
|
this.followerShard = followerShard;
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
void start(final long from, final long to, final long maxTranslogsBytes) {
|
||||||
|
ShardChangesAction.Request request = new ShardChangesAction.Request(leaderShard);
|
||||||
|
request.setMinSeqNo(from);
|
||||||
|
request.setMaxSeqNo(to);
|
||||||
|
request.setMaxTranslogsBytes(maxTranslogsBytes);
|
||||||
|
leaderClient.execute(ShardChangesAction.INSTANCE, request, new ActionListener<ShardChangesAction.Response>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(ShardChangesAction.Response response) {
|
||||||
|
handleResponse(to, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Exception e) {
|
||||||
|
assert e != null;
|
||||||
|
if (shouldRetry(e)) {
|
||||||
|
if (retryCounter.incrementAndGet() <= PROCESSOR_RETRY_LIMIT) {
|
||||||
|
start(from, to, maxTranslogsBytes);
|
||||||
|
} else {
|
||||||
|
handler.accept(new ElasticsearchException("retrying failed [" + retryCounter.get() +
|
||||||
|
"] times, aborting...", e));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handler.accept(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleResponse(final long to, final ShardChangesAction.Response response) {
|
||||||
|
if (response.getOperations().length != 0) {
|
||||||
|
Translog.Operation lastOp = response.getOperations()[response.getOperations().length - 1];
|
||||||
|
boolean maxByteLimitReached = lastOp.seqNo() < to;
|
||||||
|
if (maxByteLimitReached) {
|
||||||
|
// add a new entry to the queue for the operations that couldn't be fetched in the current shard changes api call:
|
||||||
|
chunks.add(new long[]{lastOp.seqNo() + 1, to});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ccrExecutor.execute(new AbstractRunnable() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Exception e) {
|
||||||
|
assert e != null;
|
||||||
|
handler.accept(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doRun() throws Exception {
|
||||||
|
final BulkShardOperationsRequest request = new BulkShardOperationsRequest(followerShard, response.getOperations());
|
||||||
|
followerClient.execute(BulkShardOperationsAction.INSTANCE, request, new ActionListener<BulkShardOperationsResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(final BulkShardOperationsResponse bulkShardOperationsResponse) {
|
||||||
|
handler.accept(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(final Exception e) {
|
||||||
|
// No retry mechanism here, because if a failure is being redirected to this place it is considered
|
||||||
|
// non recoverable.
|
||||||
|
assert e != null;
|
||||||
|
handler.accept(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean shouldRetry(Exception e) {
|
||||||
|
// TODO: What other exceptions should be retried?
|
||||||
|
return NetworkExceptionHelper.isConnectException(e) ||
|
||||||
|
NetworkExceptionHelper.isCloseConnectionException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.Action;
|
||||||
|
import org.elasticsearch.action.ActionListener;
|
||||||
|
import org.elasticsearch.action.ActionRequest;
|
||||||
|
import org.elasticsearch.action.ActionRequestBuilder;
|
||||||
|
import org.elasticsearch.action.ActionRequestValidationException;
|
||||||
|
import org.elasticsearch.action.ActionResponse;
|
||||||
|
import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest;
|
||||||
|
import org.elasticsearch.action.support.ActionFilters;
|
||||||
|
import org.elasticsearch.action.support.HandledTransportAction;
|
||||||
|
import org.elasticsearch.client.Client;
|
||||||
|
import org.elasticsearch.client.ElasticsearchClient;
|
||||||
|
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
||||||
|
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
||||||
|
import org.elasticsearch.common.inject.Inject;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamInput;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
import org.elasticsearch.transport.TransportService;
|
||||||
|
import org.elasticsearch.persistent.PersistentTasksCustomMetaData;
|
||||||
|
import org.elasticsearch.persistent.PersistentTasksService;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||||
|
|
||||||
|
public class UnfollowIndexAction extends Action<UnfollowIndexAction.Request, UnfollowIndexAction.Response,
|
||||||
|
UnfollowIndexAction.RequestBuilder> {
|
||||||
|
|
||||||
|
public static final UnfollowIndexAction INSTANCE = new UnfollowIndexAction();
|
||||||
|
public static final String NAME = "cluster:admin/xpack/ccr/unfollow_index";
|
||||||
|
|
||||||
|
private UnfollowIndexAction() {
|
||||||
|
super(NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestBuilder newRequestBuilder(ElasticsearchClient client) {
|
||||||
|
return new RequestBuilder(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response newResponse() {
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Request extends ActionRequest {
|
||||||
|
|
||||||
|
private String followIndex;
|
||||||
|
|
||||||
|
public String getFollowIndex() {
|
||||||
|
return followIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFollowIndex(String followIndex) {
|
||||||
|
this.followIndex = followIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActionRequestValidationException validate() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(StreamInput in) throws IOException {
|
||||||
|
super.readFrom(in);
|
||||||
|
followIndex = in.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(StreamOutput out) throws IOException {
|
||||||
|
super.writeTo(out);
|
||||||
|
out.writeString(followIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response extends ActionResponse {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder> {
|
||||||
|
|
||||||
|
public RequestBuilder(ElasticsearchClient client) {
|
||||||
|
super(client, INSTANCE, new Request());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class TransportAction extends HandledTransportAction<Request, Response> {
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final PersistentTasksService persistentTasksService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TransportAction(Settings settings, ThreadPool threadPool, TransportService transportService, ActionFilters actionFilters,
|
||||||
|
IndexNameExpressionResolver indexNameExpressionResolver, Client client,
|
||||||
|
PersistentTasksService persistentTasksService) {
|
||||||
|
super(settings, NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, Request::new);
|
||||||
|
this.client = client;
|
||||||
|
this.persistentTasksService = persistentTasksService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doExecute(Request request, ActionListener<Response> listener) {
|
||||||
|
client.admin().cluster().state(new ClusterStateRequest(), ActionListener.wrap(r -> {
|
||||||
|
IndexMetaData followIndexMetadata = r.getState().getMetaData().index(request.followIndex);
|
||||||
|
final int numShards = followIndexMetadata.getNumberOfShards();
|
||||||
|
final AtomicInteger counter = new AtomicInteger(numShards);
|
||||||
|
final AtomicReferenceArray<Object> responses = new AtomicReferenceArray<>(followIndexMetadata.getNumberOfShards());
|
||||||
|
for (int i = 0; i < numShards; i++) {
|
||||||
|
final int shardId = i;
|
||||||
|
String taskId = followIndexMetadata.getIndexUUID() + "-" + shardId;
|
||||||
|
persistentTasksService.cancelPersistentTask(taskId,
|
||||||
|
new ActionListener<PersistentTasksCustomMetaData.PersistentTask<?>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(PersistentTasksCustomMetaData.PersistentTask<?> task) {
|
||||||
|
responses.set(shardId, task);
|
||||||
|
finalizeResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Exception e) {
|
||||||
|
responses.set(shardId, e);
|
||||||
|
finalizeResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
void finalizeResponse() {
|
||||||
|
Exception error = null;
|
||||||
|
if (counter.decrementAndGet() == 0) {
|
||||||
|
for (int j = 0; j < responses.length(); j++) {
|
||||||
|
Object response = responses.get(j);
|
||||||
|
if (response instanceof Exception) {
|
||||||
|
if (error == null) {
|
||||||
|
error = (Exception) response;
|
||||||
|
} else {
|
||||||
|
error.addSuppressed((Throwable) response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error == null) {
|
||||||
|
// include task ids?
|
||||||
|
listener.onResponse(new Response());
|
||||||
|
} else {
|
||||||
|
// TODO: cancel all started tasks
|
||||||
|
listener.onFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, listener::onFailure));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action.bulk;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.Action;
|
||||||
|
import org.elasticsearch.client.ElasticsearchClient;
|
||||||
|
|
||||||
|
public class BulkShardOperationsAction
|
||||||
|
extends Action<BulkShardOperationsRequest, BulkShardOperationsResponse, BulkShardOperationsRequestBuilder> {
|
||||||
|
|
||||||
|
public static final BulkShardOperationsAction INSTANCE = new BulkShardOperationsAction();
|
||||||
|
public static final String NAME = "indices:data/write/bulk_shard_operations[s]";
|
||||||
|
|
||||||
|
private BulkShardOperationsAction() {
|
||||||
|
super(NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BulkShardOperationsRequestBuilder newRequestBuilder(ElasticsearchClient client) {
|
||||||
|
return new BulkShardOperationsRequestBuilder(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BulkShardOperationsResponse newResponse() {
|
||||||
|
return new BulkShardOperationsResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action.bulk;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.support.replication.ReplicatedWriteRequest;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamInput;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.index.translog.Translog;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public final class BulkShardOperationsRequest extends ReplicatedWriteRequest<BulkShardOperationsRequest> {
|
||||||
|
|
||||||
|
private Translog.Operation[] operations;
|
||||||
|
|
||||||
|
public BulkShardOperationsRequest() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulkShardOperationsRequest(final ShardId shardId, final Translog.Operation[] operations) {
|
||||||
|
super(shardId);
|
||||||
|
setRefreshPolicy(RefreshPolicy.NONE);
|
||||||
|
this.operations = operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Translog.Operation[] getOperations() {
|
||||||
|
return operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(final StreamInput in) throws IOException {
|
||||||
|
super.readFrom(in);
|
||||||
|
operations = in.readArray(Translog.Operation::readOperation, Translog.Operation[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(final StreamOutput out) throws IOException {
|
||||||
|
super.writeTo(out);
|
||||||
|
out.writeArray(Translog.Operation::writeOperation, operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "BulkShardOperationsRequest{" +
|
||||||
|
"operations=" + operations.length+
|
||||||
|
", shardId=" + shardId +
|
||||||
|
", timeout=" + timeout +
|
||||||
|
", index='" + index + '\'' +
|
||||||
|
", waitForActiveShards=" + waitForActiveShards +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action.bulk;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.ActionRequestBuilder;
|
||||||
|
import org.elasticsearch.client.ElasticsearchClient;
|
||||||
|
|
||||||
|
public class BulkShardOperationsRequestBuilder
|
||||||
|
extends ActionRequestBuilder<BulkShardOperationsRequest, BulkShardOperationsResponse, BulkShardOperationsRequestBuilder> {
|
||||||
|
|
||||||
|
public BulkShardOperationsRequestBuilder(final ElasticsearchClient client) {
|
||||||
|
super(client, BulkShardOperationsAction.INSTANCE, new BulkShardOperationsRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action.bulk;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.support.WriteResponse;
|
||||||
|
import org.elasticsearch.action.support.replication.ReplicationResponse;
|
||||||
|
|
||||||
|
public final class BulkShardOperationsResponse extends ReplicationResponse implements WriteResponse {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setForcedRefresh(final boolean forcedRefresh) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action.bulk;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.support.ActionFilters;
|
||||||
|
import org.elasticsearch.action.support.replication.TransportWriteAction;
|
||||||
|
import org.elasticsearch.cluster.action.shard.ShardStateAction;
|
||||||
|
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
||||||
|
import org.elasticsearch.cluster.service.ClusterService;
|
||||||
|
import org.elasticsearch.common.inject.Inject;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.index.engine.Engine;
|
||||||
|
import org.elasticsearch.index.mapper.MapperException;
|
||||||
|
import org.elasticsearch.index.shard.IndexShard;
|
||||||
|
import org.elasticsearch.index.translog.Translog;
|
||||||
|
import org.elasticsearch.indices.IndicesService;
|
||||||
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
import org.elasticsearch.transport.TransportService;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class TransportBulkShardOperationsAction
|
||||||
|
extends TransportWriteAction<BulkShardOperationsRequest, BulkShardOperationsRequest, BulkShardOperationsResponse> {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TransportBulkShardOperationsAction(
|
||||||
|
final Settings settings,
|
||||||
|
final TransportService transportService,
|
||||||
|
final ClusterService clusterService,
|
||||||
|
final IndicesService indicesService,
|
||||||
|
final ThreadPool threadPool,
|
||||||
|
final ShardStateAction shardStateAction,
|
||||||
|
final ActionFilters actionFilters,
|
||||||
|
final IndexNameExpressionResolver indexNameExpressionResolver) {
|
||||||
|
super(
|
||||||
|
settings,
|
||||||
|
BulkShardOperationsAction.NAME,
|
||||||
|
transportService,
|
||||||
|
clusterService,
|
||||||
|
indicesService,
|
||||||
|
threadPool,
|
||||||
|
shardStateAction,
|
||||||
|
actionFilters,
|
||||||
|
indexNameExpressionResolver,
|
||||||
|
BulkShardOperationsRequest::new,
|
||||||
|
BulkShardOperationsRequest::new,
|
||||||
|
ThreadPool.Names.WRITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected WritePrimaryResult<BulkShardOperationsRequest, BulkShardOperationsResponse> shardOperationOnPrimary(
|
||||||
|
final BulkShardOperationsRequest request, final IndexShard primary) throws Exception {
|
||||||
|
final Translog.Location location = applyTranslogOperations(request, primary, Engine.Operation.Origin.PRIMARY);
|
||||||
|
return new WritePrimaryResult<>(request, new BulkShardOperationsResponse(), location, null, primary, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected WriteReplicaResult<BulkShardOperationsRequest> shardOperationOnReplica(
|
||||||
|
final BulkShardOperationsRequest request, final IndexShard replica) throws Exception {
|
||||||
|
final Translog.Location location = applyTranslogOperations(request, replica, Engine.Operation.Origin.REPLICA);
|
||||||
|
return new WriteReplicaResult<>(request, location, null, replica, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Translog.Location applyTranslogOperations(
|
||||||
|
final BulkShardOperationsRequest request, final IndexShard shard, final Engine.Operation.Origin origin) throws IOException {
|
||||||
|
Translog.Location location = null;
|
||||||
|
for (final Translog.Operation operation : request.getOperations()) {
|
||||||
|
final Engine.Result result = shard.applyTranslogOperation(operation, origin, m -> {
|
||||||
|
// TODO: Figure out how to deal best with dynamic mapping updates from the leader side:
|
||||||
|
throw new MapperException("dynamic mapping updates are not allowed in follow shards [" + operation + "]");
|
||||||
|
});
|
||||||
|
assert result.getSeqNo() == operation.seqNo();
|
||||||
|
assert result.hasFailure() == false;
|
||||||
|
location = locationToSync(location, result.getTranslogLocation());
|
||||||
|
}
|
||||||
|
assert request.getOperations().length == 0 || location != null;
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BulkShardOperationsResponse newResponseInstance() {
|
||||||
|
return new BulkShardOperationsResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.index.engine;
|
||||||
|
|
||||||
|
import org.elasticsearch.index.VersionType;
|
||||||
|
import org.elasticsearch.index.engine.EngineConfig;
|
||||||
|
import org.elasticsearch.index.engine.InternalEngine;
|
||||||
|
import org.elasticsearch.index.seqno.SequenceNumbers;
|
||||||
|
import org.elasticsearch.xpack.ccr.CcrSettings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An engine implementation for following shards.
|
||||||
|
*/
|
||||||
|
public final class FollowingEngine extends InternalEngine {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new following engine with the specified engine configuration.
|
||||||
|
*
|
||||||
|
* @param engineConfig the engine configuration
|
||||||
|
*/
|
||||||
|
FollowingEngine(final EngineConfig engineConfig) {
|
||||||
|
super(validateEngineConfig(engineConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EngineConfig validateEngineConfig(final EngineConfig engineConfig) {
|
||||||
|
if (CcrSettings.CCR_FOLLOWING_INDEX_SETTING.get(engineConfig.getIndexSettings().getSettings()) == false) {
|
||||||
|
throw new IllegalArgumentException("a following engine can not be constructed for a non-following index");
|
||||||
|
}
|
||||||
|
return engineConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void preFlight(final Operation operation) {
|
||||||
|
/*
|
||||||
|
* We assert here so that this goes uncaught in unit tests and fails nodes in standalone tests (we want a harsh failure so that we
|
||||||
|
* do not have a situation where a shard fails and is recovered elsewhere and a test subsequently passes). We throw an exception so
|
||||||
|
* that we also prevent issues in production code.
|
||||||
|
*/
|
||||||
|
assert operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO;
|
||||||
|
if (operation.seqNo() == SequenceNumbers.UNASSIGNED_SEQ_NO) {
|
||||||
|
throw new IllegalStateException("a following engine does not accept operations without an assigned sequence number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected InternalEngine.IndexingStrategy indexingStrategyForOperation(final Index index) throws IOException {
|
||||||
|
preFlight(index);
|
||||||
|
return planIndexingAsNonPrimary(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected InternalEngine.DeletionStrategy deletionStrategyForOperation(final Delete delete) throws IOException {
|
||||||
|
preFlight(delete);
|
||||||
|
return planDeletionAsNonPrimary(delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean assertPrimaryIncomingSequenceNumber(final Operation.Origin origin, final long seqNo) {
|
||||||
|
// sequence number should be set when operation origin is primary
|
||||||
|
assert seqNo != SequenceNumbers.UNASSIGNED_SEQ_NO : "primary operations on a following index must have an assigned sequence number";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean assertNonPrimaryOrigin(final Operation operation) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean assertPrimaryCanOptimizeAddDocument(final Index index) {
|
||||||
|
assert index.version() == 1 && index.versionType() == VersionType.EXTERNAL
|
||||||
|
: "version [" + index.version() + "], type [" + index.versionType() + "]";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.index.engine;
|
||||||
|
|
||||||
|
import org.elasticsearch.index.engine.Engine;
|
||||||
|
import org.elasticsearch.index.engine.EngineConfig;
|
||||||
|
import org.elasticsearch.index.engine.EngineFactory;
|
||||||
|
import org.elasticsearch.index.engine.InternalEngine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An engine factory for following engines.
|
||||||
|
*/
|
||||||
|
public final class FollowingEngineFactory implements EngineFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine newReadWriteEngine(final EngineConfig config) {
|
||||||
|
return new FollowingEngine(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.rest;
|
||||||
|
|
||||||
|
import org.elasticsearch.client.node.NodeClient;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.rest.BaseRestHandler;
|
||||||
|
import org.elasticsearch.rest.BytesRestResponse;
|
||||||
|
import org.elasticsearch.rest.RestController;
|
||||||
|
import org.elasticsearch.rest.RestRequest;
|
||||||
|
import org.elasticsearch.rest.RestResponse;
|
||||||
|
import org.elasticsearch.rest.RestStatus;
|
||||||
|
import org.elasticsearch.rest.action.RestBuilderListener;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardFollowTask;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.elasticsearch.xpack.ccr.action.FollowExistingIndexAction.INSTANCE;
|
||||||
|
import static org.elasticsearch.xpack.ccr.action.FollowExistingIndexAction.Request;
|
||||||
|
import static org.elasticsearch.xpack.ccr.action.FollowExistingIndexAction.Response;
|
||||||
|
|
||||||
|
// TODO: change to confirm with API design
|
||||||
|
public class RestFollowExistingIndexAction extends BaseRestHandler {
|
||||||
|
|
||||||
|
public RestFollowExistingIndexAction(Settings settings, RestController controller) {
|
||||||
|
super(settings);
|
||||||
|
// TODO: figure out why: '/{follow_index}/_xpack/ccr/_follow' path clashes with create index api.
|
||||||
|
controller.registerHandler(RestRequest.Method.POST, "/_xpack/ccr/{follow_index}/_follow", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "xpack_ccr_follow_index_action";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
|
||||||
|
Request request = new Request();
|
||||||
|
request.setLeaderIndex(restRequest.param("leader_index"));
|
||||||
|
request.setFollowIndex(restRequest.param("follow_index"));
|
||||||
|
if (restRequest.hasParam(ShardFollowTask.MAX_CHUNK_SIZE.getPreferredName())) {
|
||||||
|
request.setBatchSize(Long.valueOf(restRequest.param(ShardFollowTask.MAX_CHUNK_SIZE.getPreferredName())));
|
||||||
|
}
|
||||||
|
if (restRequest.hasParam(ShardFollowTask.NUM_CONCURRENT_CHUNKS.getPreferredName())) {
|
||||||
|
request.setConcurrentProcessors(Integer.valueOf(restRequest.param(ShardFollowTask.NUM_CONCURRENT_CHUNKS.getPreferredName())));
|
||||||
|
}
|
||||||
|
if (restRequest.hasParam(ShardFollowTask.PROCESSOR_MAX_TRANSLOG_BYTES_PER_REQUEST.getPreferredName())) {
|
||||||
|
long value = Long.valueOf(restRequest.param(ShardFollowTask.PROCESSOR_MAX_TRANSLOG_BYTES_PER_REQUEST.getPreferredName()));
|
||||||
|
request.setProcessorMaxTranslogBytes(value);
|
||||||
|
}
|
||||||
|
return channel -> client.execute(INSTANCE, request, new RestBuilderListener<Response>(channel) {
|
||||||
|
@Override
|
||||||
|
public RestResponse buildResponse(Response response, XContentBuilder builder) throws Exception {
|
||||||
|
return new BytesRestResponse(RestStatus.OK, builder.startObject()
|
||||||
|
.endObject());
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.rest;
|
||||||
|
|
||||||
|
import org.elasticsearch.client.node.NodeClient;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.rest.BaseRestHandler;
|
||||||
|
import org.elasticsearch.rest.BytesRestResponse;
|
||||||
|
import org.elasticsearch.rest.RestController;
|
||||||
|
import org.elasticsearch.rest.RestRequest;
|
||||||
|
import org.elasticsearch.rest.RestResponse;
|
||||||
|
import org.elasticsearch.rest.RestStatus;
|
||||||
|
import org.elasticsearch.rest.action.RestBuilderListener;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.elasticsearch.xpack.ccr.action.UnfollowIndexAction.INSTANCE;
|
||||||
|
import static org.elasticsearch.xpack.ccr.action.UnfollowIndexAction.Request;
|
||||||
|
import static org.elasticsearch.xpack.ccr.action.UnfollowIndexAction.Response;
|
||||||
|
|
||||||
|
// TODO: change to confirm with API design
|
||||||
|
public class RestUnfollowIndexAction extends BaseRestHandler {
|
||||||
|
|
||||||
|
public RestUnfollowIndexAction(Settings settings, RestController controller) {
|
||||||
|
super(settings);
|
||||||
|
// TODO: figure out why: '/{follow_index}/_xpack/ccr/_unfollow' path clashes with create index api.
|
||||||
|
controller.registerHandler(RestRequest.Method.POST, "/_xpack/ccr/{follow_index}/_unfollow", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "xpack_ccr_unfollow_index_action";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
|
||||||
|
Request request = new Request();
|
||||||
|
request.setFollowIndex(restRequest.param("follow_index"));
|
||||||
|
return channel -> client.execute(INSTANCE, request, new RestBuilderListener<Response>(channel) {
|
||||||
|
@Override
|
||||||
|
public RestResponse buildResponse(Response response, XContentBuilder builder) throws Exception {
|
||||||
|
return new BytesRestResponse(RestStatus.OK, builder.startObject()
|
||||||
|
.endObject());
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
grant {
|
||||||
|
// needed because of problems in unbound LDAP library
|
||||||
|
permission java.util.PropertyPermission "*", "read,write";
|
||||||
|
|
||||||
|
// required to configure the custom mailcap for watcher
|
||||||
|
permission java.lang.RuntimePermission "setFactory";
|
||||||
|
|
||||||
|
// needed when sending emails for javax.activation
|
||||||
|
// otherwise a classnotfound exception is thrown due to trying
|
||||||
|
// to load the class with the application class loader
|
||||||
|
permission java.lang.RuntimePermission "setContextClassLoader";
|
||||||
|
permission java.lang.RuntimePermission "getClassLoader";
|
||||||
|
// TODO: remove use of this jar as soon as possible!!!!
|
||||||
|
permission java.lang.RuntimePermission "accessClassInPackage.com.sun.activation.registries";
|
||||||
|
|
||||||
|
// bouncy castle
|
||||||
|
permission java.security.SecurityPermission "putProviderProperty.BC";
|
||||||
|
|
||||||
|
// needed for x-pack security extension
|
||||||
|
permission java.security.SecurityPermission "createPolicy.JavaPolicy";
|
||||||
|
permission java.security.SecurityPermission "getPolicy";
|
||||||
|
permission java.security.SecurityPermission "setPolicy";
|
||||||
|
|
||||||
|
// needed for multiple server implementations used in tests
|
||||||
|
permission java.net.SocketPermission "*", "accept,connect";
|
||||||
|
|
||||||
|
// needed for Windows named pipes in machine learning
|
||||||
|
permission java.io.FilePermission "\\\\.\\pipe\\*", "read,write";
|
||||||
|
};
|
||||||
|
|
||||||
|
grant codeBase "${codebase.netty-common}" {
|
||||||
|
// for reading the system-wide configuration for the backlog of established sockets
|
||||||
|
permission java.io.FilePermission "/proc/sys/net/core/somaxconn", "read";
|
||||||
|
};
|
||||||
|
|
||||||
|
grant codeBase "${codebase.netty-transport}" {
|
||||||
|
// Netty NioEventLoop wants to change this, because of https://bugs.openjdk.java.net/browse/JDK-6427854
|
||||||
|
// the bug says it only happened rarely, and that its fixed, but apparently it still happens rarely!
|
||||||
|
permission java.util.PropertyPermission "sun.nio.ch.bugLevel", "write";
|
||||||
|
};
|
||||||
|
|
||||||
|
grant codeBase "${codebase.elasticsearch-rest-client}" {
|
||||||
|
// rest client uses system properties which gets the default proxy
|
||||||
|
permission java.net.NetPermission "getProxySelector";
|
||||||
|
};
|
||||||
|
|
||||||
|
grant codeBase "${codebase.httpasyncclient}" {
|
||||||
|
// rest client uses system properties which gets the default proxy
|
||||||
|
permission java.net.NetPermission "getProxySelector";
|
||||||
|
};
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr;
|
||||||
|
|
||||||
|
import org.elasticsearch.Version;
|
||||||
|
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
||||||
|
import org.elasticsearch.common.UUIDs;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.index.Index;
|
||||||
|
import org.elasticsearch.index.IndexSettings;
|
||||||
|
import org.elasticsearch.index.engine.EngineFactory;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.elasticsearch.xpack.ccr.Ccr;
|
||||||
|
import org.elasticsearch.xpack.ccr.index.engine.FollowingEngineFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
|
|
||||||
|
public class CcrTests extends ESTestCase {
|
||||||
|
|
||||||
|
public void testGetEngineFactory() throws IOException {
|
||||||
|
final Boolean[] values = new Boolean[] { true, false, null };
|
||||||
|
for (final Boolean value : values) {
|
||||||
|
final String indexName = "following-" + value;
|
||||||
|
final Index index = new Index(indexName, UUIDs.randomBase64UUID());
|
||||||
|
final Settings.Builder builder = Settings.builder()
|
||||||
|
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
|
||||||
|
.put(IndexMetaData.SETTING_INDEX_UUID, index.getUUID());
|
||||||
|
if (value != null) {
|
||||||
|
builder.put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
final IndexMetaData indexMetaData = new IndexMetaData.Builder(index.getName())
|
||||||
|
.settings(builder.build())
|
||||||
|
.numberOfShards(1)
|
||||||
|
.numberOfReplicas(0)
|
||||||
|
.build();
|
||||||
|
final Ccr ccr = new Ccr(Settings.EMPTY);
|
||||||
|
final Optional<EngineFactory> engineFactory =
|
||||||
|
ccr.getEngineFactory(new IndexSettings(indexMetaData, Settings.EMPTY));
|
||||||
|
if (value != null && value) {
|
||||||
|
assertTrue(engineFactory.isPresent());
|
||||||
|
assertThat(engineFactory.get(), instanceOf(FollowingEngineFactory.class));
|
||||||
|
} else {
|
||||||
|
assertFalse(engineFactory.isPresent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
|
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class LocalStateCcr extends LocalStateCompositeXPackPlugin {
|
||||||
|
|
||||||
|
public LocalStateCcr(final Settings settings, final Path configPath) throws Exception {
|
||||||
|
super(settings, configPath);
|
||||||
|
LocalStateCcr thisVar = this;
|
||||||
|
|
||||||
|
plugins.add(new Ccr(settings){
|
||||||
|
@Override
|
||||||
|
protected XPackLicenseState getLicenseState() {
|
||||||
|
return thisVar.getLicenseState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,327 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ELASTICSEARCH CONFIDENTIAL
|
||||||
|
* __________________
|
||||||
|
*
|
||||||
|
* [2017] Elasticsearch Incorporated. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* NOTICE: All information contained herein is, and remains
|
||||||
|
* the property of Elasticsearch Incorporated and its suppliers,
|
||||||
|
* if any. The intellectual and technical concepts contained
|
||||||
|
* herein are proprietary to Elasticsearch Incorporated
|
||||||
|
* and its suppliers and may be covered by U.S. and Foreign Patents,
|
||||||
|
* patents in process, and are protected by trade secret or copyright law.
|
||||||
|
* Dissemination of this information or reproduction of this material
|
||||||
|
* is strictly forbidden unless prior written permission is obtained
|
||||||
|
* from Elasticsearch Incorporated.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
|
||||||
|
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse;
|
||||||
|
import org.elasticsearch.action.admin.indices.stats.ShardStats;
|
||||||
|
import org.elasticsearch.action.get.GetResponse;
|
||||||
|
import org.elasticsearch.analysis.common.CommonAnalysisPlugin;
|
||||||
|
import org.elasticsearch.cluster.ClusterState;
|
||||||
|
import org.elasticsearch.common.CheckedRunnable;
|
||||||
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.index.translog.Translog;
|
||||||
|
import org.elasticsearch.persistent.PersistentTasksCustomMetaData;
|
||||||
|
import org.elasticsearch.plugins.Plugin;
|
||||||
|
import org.elasticsearch.tasks.TaskInfo;
|
||||||
|
import org.elasticsearch.test.ESIntegTestCase;
|
||||||
|
import org.elasticsearch.test.discovery.TestZenDiscovery;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.FollowExistingIndexAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardChangesAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardFollowNodeTask;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardFollowTask;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.UnfollowIndexAction;
|
||||||
|
import org.elasticsearch.xpack.core.XPackSettings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||||
|
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
|
||||||
|
@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, transportClientRatio = 0)
|
||||||
|
public class ShardChangesIT extends ESIntegTestCase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Settings nodeSettings(int nodeOrdinal) {
|
||||||
|
Settings.Builder newSettings = Settings.builder();
|
||||||
|
newSettings.put(super.nodeSettings(nodeOrdinal));
|
||||||
|
newSettings.put(XPackSettings.SECURITY_ENABLED.getKey(), false);
|
||||||
|
newSettings.put(XPackSettings.MONITORING_ENABLED.getKey(), false);
|
||||||
|
newSettings.put(XPackSettings.WATCHER_ENABLED.getKey(), false);
|
||||||
|
newSettings.put(XPackSettings.MACHINE_LEARNING_ENABLED.getKey(), false);
|
||||||
|
newSettings.put(XPackSettings.LOGSTASH_ENABLED.getKey(), false);
|
||||||
|
return newSettings.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Collection<Class<? extends Plugin>> getMockPlugins() {
|
||||||
|
return Arrays.asList(TestSeedPlugin.class, TestZenDiscovery.TestPlugin.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Collection<Class<? extends Plugin>> nodePlugins() {
|
||||||
|
return Arrays.asList(LocalStateCcr.class, CommonAnalysisPlugin.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean ignoreExternalCluster() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Collection<Class<? extends Plugin>> transportClientPlugins() {
|
||||||
|
return nodePlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
// this emulates what the CCR persistent task will do for pulling
|
||||||
|
public void testGetOperationsBasedOnGlobalSequenceId() throws Exception {
|
||||||
|
client().admin().indices().prepareCreate("index")
|
||||||
|
.setSettings(Settings.builder().put("index.number_of_shards", 1))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
client().prepareIndex("index", "doc", "1").setSource("{}", XContentType.JSON).get();
|
||||||
|
client().prepareIndex("index", "doc", "2").setSource("{}", XContentType.JSON).get();
|
||||||
|
client().prepareIndex("index", "doc", "3").setSource("{}", XContentType.JSON).get();
|
||||||
|
|
||||||
|
ShardStats shardStats = client().admin().indices().prepareStats("index").get().getIndex("index").getShards()[0];
|
||||||
|
long globalCheckPoint = shardStats.getSeqNoStats().getGlobalCheckpoint();
|
||||||
|
assertThat(globalCheckPoint, equalTo(2L));
|
||||||
|
|
||||||
|
ShardChangesAction.Request request = new ShardChangesAction.Request(shardStats.getShardRouting().shardId());
|
||||||
|
request.setMinSeqNo(0L);
|
||||||
|
request.setMaxSeqNo(globalCheckPoint);
|
||||||
|
ShardChangesAction.Response response = client().execute(ShardChangesAction.INSTANCE, request).get();
|
||||||
|
assertThat(response.getOperations().length, equalTo(3));
|
||||||
|
Translog.Index operation = (Translog.Index) response.getOperations()[0];
|
||||||
|
assertThat(operation.seqNo(), equalTo(0L));
|
||||||
|
assertThat(operation.id(), equalTo("1"));
|
||||||
|
|
||||||
|
operation = (Translog.Index) response.getOperations()[1];
|
||||||
|
assertThat(operation.seqNo(), equalTo(1L));
|
||||||
|
assertThat(operation.id(), equalTo("2"));
|
||||||
|
|
||||||
|
operation = (Translog.Index) response.getOperations()[2];
|
||||||
|
assertThat(operation.seqNo(), equalTo(2L));
|
||||||
|
assertThat(operation.id(), equalTo("3"));
|
||||||
|
|
||||||
|
client().prepareIndex("index", "doc", "3").setSource("{}", XContentType.JSON).get();
|
||||||
|
client().prepareIndex("index", "doc", "4").setSource("{}", XContentType.JSON).get();
|
||||||
|
client().prepareIndex("index", "doc", "5").setSource("{}", XContentType.JSON).get();
|
||||||
|
|
||||||
|
shardStats = client().admin().indices().prepareStats("index").get().getIndex("index").getShards()[0];
|
||||||
|
globalCheckPoint = shardStats.getSeqNoStats().getGlobalCheckpoint();
|
||||||
|
assertThat(globalCheckPoint, equalTo(5L));
|
||||||
|
|
||||||
|
request = new ShardChangesAction.Request(shardStats.getShardRouting().shardId());
|
||||||
|
request.setMinSeqNo(3L);
|
||||||
|
request.setMaxSeqNo(globalCheckPoint);
|
||||||
|
response = client().execute(ShardChangesAction.INSTANCE, request).get();
|
||||||
|
assertThat(response.getOperations().length, equalTo(3));
|
||||||
|
operation = (Translog.Index) response.getOperations()[0];
|
||||||
|
assertThat(operation.seqNo(), equalTo(3L));
|
||||||
|
assertThat(operation.id(), equalTo("3"));
|
||||||
|
|
||||||
|
operation = (Translog.Index) response.getOperations()[1];
|
||||||
|
assertThat(operation.seqNo(), equalTo(4L));
|
||||||
|
assertThat(operation.id(), equalTo("4"));
|
||||||
|
|
||||||
|
operation = (Translog.Index) response.getOperations()[2];
|
||||||
|
assertThat(operation.seqNo(), equalTo(5L));
|
||||||
|
assertThat(operation.id(), equalTo("5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testFollowIndex() throws Exception {
|
||||||
|
final int numberOfPrimaryShards = randomIntBetween(1, 3);
|
||||||
|
|
||||||
|
final String leaderIndexSettings = getIndexSettings(numberOfPrimaryShards, Collections.emptyMap());
|
||||||
|
assertAcked(client().admin().indices().prepareCreate("index1").setSource(leaderIndexSettings, XContentType.JSON));
|
||||||
|
|
||||||
|
final String followerIndexSettings =
|
||||||
|
getIndexSettings(numberOfPrimaryShards, Collections.singletonMap(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), "true"));
|
||||||
|
assertAcked(client().admin().indices().prepareCreate("index2").setSource(followerIndexSettings, XContentType.JSON));
|
||||||
|
|
||||||
|
ensureGreen("index1", "index2");
|
||||||
|
|
||||||
|
final FollowExistingIndexAction.Request followRequest = new FollowExistingIndexAction.Request();
|
||||||
|
followRequest.setLeaderIndex("index1");
|
||||||
|
followRequest.setFollowIndex("index2");
|
||||||
|
client().execute(FollowExistingIndexAction.INSTANCE, followRequest).get();
|
||||||
|
|
||||||
|
final int firstBatchNumDocs = randomIntBetween(2, 64);
|
||||||
|
for (int i = 0; i < firstBatchNumDocs; i++) {
|
||||||
|
final String source = String.format(Locale.ROOT, "{\"f\":%d}", i);
|
||||||
|
client().prepareIndex("index1", "doc", Integer.toString(i)).setSource(source, XContentType.JSON).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<ShardId, Long> firstBatchNumDocsPerShard = new HashMap<>();
|
||||||
|
final ShardStats[] firstBatchShardStats = client().admin().indices().prepareStats("index1").get().getIndex("index1").getShards();
|
||||||
|
for (final ShardStats shardStats : firstBatchShardStats) {
|
||||||
|
if (shardStats.getShardRouting().primary()) {
|
||||||
|
long value = shardStats.getStats().getIndexing().getTotal().getIndexCount() - 1;
|
||||||
|
firstBatchNumDocsPerShard.put(shardStats.getShardRouting().shardId(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertBusy(assertTask(numberOfPrimaryShards, firstBatchNumDocsPerShard));
|
||||||
|
|
||||||
|
for (int i = 0; i < firstBatchNumDocs; i++) {
|
||||||
|
assertBusy(assertExpectedDocumentRunnable(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
final int secondBatchNumDocs = randomIntBetween(2, 64);
|
||||||
|
for (int i = firstBatchNumDocs; i < firstBatchNumDocs + secondBatchNumDocs; i++) {
|
||||||
|
final String source = String.format(Locale.ROOT, "{\"f\":%d}", i);
|
||||||
|
client().prepareIndex("index1", "doc", Integer.toString(i)).setSource(source, XContentType.JSON).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<ShardId, Long> secondBatchNumDocsPerShard = new HashMap<>();
|
||||||
|
final ShardStats[] secondBatchShardStats = client().admin().indices().prepareStats("index1").get().getIndex("index1").getShards();
|
||||||
|
for (final ShardStats shardStats : secondBatchShardStats) {
|
||||||
|
if (shardStats.getShardRouting().primary()) {
|
||||||
|
final long value = shardStats.getStats().getIndexing().getTotal().getIndexCount() - 1;
|
||||||
|
secondBatchNumDocsPerShard.put(shardStats.getShardRouting().shardId(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertBusy(assertTask(numberOfPrimaryShards, secondBatchNumDocsPerShard));
|
||||||
|
|
||||||
|
for (int i = firstBatchNumDocs; i < firstBatchNumDocs + secondBatchNumDocs; i++) {
|
||||||
|
assertBusy(assertExpectedDocumentRunnable(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
final UnfollowIndexAction.Request unfollowRequest = new UnfollowIndexAction.Request();
|
||||||
|
unfollowRequest.setFollowIndex("index2");
|
||||||
|
client().execute(UnfollowIndexAction.INSTANCE, unfollowRequest).get();
|
||||||
|
|
||||||
|
assertBusy(() -> {
|
||||||
|
final ClusterState clusterState = client().admin().cluster().prepareState().get().getState();
|
||||||
|
final PersistentTasksCustomMetaData tasks = clusterState.getMetaData().custom(PersistentTasksCustomMetaData.TYPE);
|
||||||
|
assertThat(tasks.tasks().size(), equalTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testFollowNonExistentIndex() throws Exception {
|
||||||
|
assertAcked(client().admin().indices().prepareCreate("test-leader").get());
|
||||||
|
assertAcked(client().admin().indices().prepareCreate("test-follower").get());
|
||||||
|
final FollowExistingIndexAction.Request followRequest = new FollowExistingIndexAction.Request();
|
||||||
|
// Leader index does not exist.
|
||||||
|
followRequest.setLeaderIndex("non-existent-leader");
|
||||||
|
followRequest.setFollowIndex("test-follower");
|
||||||
|
expectThrows(IllegalArgumentException.class, () -> client().execute(FollowExistingIndexAction.INSTANCE, followRequest).actionGet());
|
||||||
|
// Follower index does not exist.
|
||||||
|
followRequest.setLeaderIndex("test-leader");
|
||||||
|
followRequest.setFollowIndex("non-existent-follower");
|
||||||
|
expectThrows(IllegalArgumentException.class, () -> client().execute(FollowExistingIndexAction.INSTANCE, followRequest).actionGet());
|
||||||
|
// Both indices do not exist.
|
||||||
|
followRequest.setLeaderIndex("non-existent-leader");
|
||||||
|
followRequest.setFollowIndex("non-existent-follower");
|
||||||
|
expectThrows(IllegalArgumentException.class, () -> client().execute(FollowExistingIndexAction.INSTANCE, followRequest).actionGet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CheckedRunnable<Exception> assertTask(final int numberOfPrimaryShards, final Map<ShardId, Long> numDocsPerShard) {
|
||||||
|
return () -> {
|
||||||
|
final ClusterState clusterState = client().admin().cluster().prepareState().get().getState();
|
||||||
|
final PersistentTasksCustomMetaData tasks = clusterState.getMetaData().custom(PersistentTasksCustomMetaData.TYPE);
|
||||||
|
assertThat(tasks.tasks().size(), equalTo(numberOfPrimaryShards));
|
||||||
|
|
||||||
|
ListTasksRequest listTasksRequest = new ListTasksRequest();
|
||||||
|
listTasksRequest.setDetailed(true);
|
||||||
|
listTasksRequest.setActions(ShardFollowTask.NAME + "[c]");
|
||||||
|
ListTasksResponse listTasksResponse = client().admin().cluster().listTasks(listTasksRequest).actionGet();
|
||||||
|
assertThat(listTasksResponse.getNodeFailures().size(), equalTo(0));
|
||||||
|
assertThat(listTasksResponse.getTaskFailures().size(), equalTo(0));
|
||||||
|
|
||||||
|
List<TaskInfo> taskInfos = listTasksResponse.getTasks();
|
||||||
|
assertThat(taskInfos.size(), equalTo(numberOfPrimaryShards));
|
||||||
|
for (PersistentTasksCustomMetaData.PersistentTask<?> task : tasks.tasks()) {
|
||||||
|
final ShardFollowTask shardFollowTask = (ShardFollowTask) task.getParams();
|
||||||
|
|
||||||
|
TaskInfo taskInfo = null;
|
||||||
|
String expectedId = "id=" + task.getId();
|
||||||
|
for (TaskInfo info : taskInfos) {
|
||||||
|
if (expectedId.equals(info.getDescription())) {
|
||||||
|
taskInfo = info;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(taskInfo, notNullValue());
|
||||||
|
ShardFollowNodeTask.Status status = (ShardFollowNodeTask.Status) taskInfo.getStatus();
|
||||||
|
assertThat(status, notNullValue());
|
||||||
|
assertThat(
|
||||||
|
status.getProcessedGlobalCheckpoint(),
|
||||||
|
equalTo(numDocsPerShard.get(shardFollowTask.getLeaderShardId())));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private CheckedRunnable<Exception> assertExpectedDocumentRunnable(final int value) {
|
||||||
|
return () -> {
|
||||||
|
final GetResponse getResponse = client().prepareGet("index2", "doc", Integer.toString(value)).get();
|
||||||
|
assertTrue(getResponse.isExists());
|
||||||
|
assertTrue((getResponse.getSource().containsKey("f")));
|
||||||
|
assertThat(getResponse.getSource().get("f"), equalTo(value));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getIndexSettings(final int numberOfPrimaryShards, final Map<String, String> additionalIndexSettings) throws IOException {
|
||||||
|
final String settings;
|
||||||
|
try (XContentBuilder builder = jsonBuilder()) {
|
||||||
|
builder.startObject();
|
||||||
|
{
|
||||||
|
builder.startObject("settings");
|
||||||
|
{
|
||||||
|
builder.field("index.number_of_shards", numberOfPrimaryShards);
|
||||||
|
for (final Map.Entry<String, String> additionalSetting : additionalIndexSettings.entrySet()) {
|
||||||
|
builder.field(additionalSetting.getKey(), additionalSetting.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.endObject();
|
||||||
|
builder.startObject("mappings");
|
||||||
|
{
|
||||||
|
builder.startObject("doc");
|
||||||
|
{
|
||||||
|
builder.startObject("properties");
|
||||||
|
{
|
||||||
|
builder.startObject("f");
|
||||||
|
{
|
||||||
|
builder.field("type", "integer");
|
||||||
|
}
|
||||||
|
builder.endObject();
|
||||||
|
}
|
||||||
|
builder.endObject();
|
||||||
|
}
|
||||||
|
builder.endObject();
|
||||||
|
}
|
||||||
|
builder.endObject();
|
||||||
|
}
|
||||||
|
builder.endObject();
|
||||||
|
settings = BytesReference.bytes(builder).utf8ToString();
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,367 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.ActionListener;
|
||||||
|
import org.elasticsearch.client.Client;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.index.translog.Translog;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardFollowTasksExecutor.ChunkProcessor;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.ShardFollowTasksExecutor.ChunksCoordinator;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsAction;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsRequest;
|
||||||
|
import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsResponse;
|
||||||
|
|
||||||
|
import java.net.ConnectException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||||
|
import static org.hamcrest.CoreMatchers.nullValue;
|
||||||
|
import static org.hamcrest.CoreMatchers.sameInstance;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Matchers.same;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
public class ChunksCoordinatorTests extends ESTestCase {
|
||||||
|
|
||||||
|
public void testCreateChunks() {
|
||||||
|
Client client = mock(Client.class);
|
||||||
|
Executor ccrExecutor = Runnable::run;
|
||||||
|
ShardId leaderShardId = new ShardId("index1", "index1", 0);
|
||||||
|
ShardId followShardId = new ShardId("index2", "index1", 0);
|
||||||
|
|
||||||
|
ChunksCoordinator coordinator =
|
||||||
|
new ChunksCoordinator(client, client, ccrExecutor, 1024, 1, Long.MAX_VALUE, leaderShardId, followShardId, e -> {});
|
||||||
|
coordinator.createChucks(0, 1024);
|
||||||
|
List<long[]> result = new ArrayList<>(coordinator.getChunks());
|
||||||
|
assertThat(result.size(), equalTo(1));
|
||||||
|
assertThat(result.get(0)[0], equalTo(0L));
|
||||||
|
assertThat(result.get(0)[1], equalTo(1024L));
|
||||||
|
|
||||||
|
coordinator.getChunks().clear();
|
||||||
|
coordinator.createChucks(0, 2048);
|
||||||
|
result = new ArrayList<>(coordinator.getChunks());
|
||||||
|
assertThat(result.size(), equalTo(2));
|
||||||
|
assertThat(result.get(0)[0], equalTo(0L));
|
||||||
|
assertThat(result.get(0)[1], equalTo(1024L));
|
||||||
|
assertThat(result.get(1)[0], equalTo(1025L));
|
||||||
|
assertThat(result.get(1)[1], equalTo(2048L));
|
||||||
|
|
||||||
|
coordinator.getChunks().clear();
|
||||||
|
coordinator.createChucks(0, 4096);
|
||||||
|
result = new ArrayList<>(coordinator.getChunks());
|
||||||
|
assertThat(result.size(), equalTo(4));
|
||||||
|
assertThat(result.get(0)[0], equalTo(0L));
|
||||||
|
assertThat(result.get(0)[1], equalTo(1024L));
|
||||||
|
assertThat(result.get(1)[0], equalTo(1025L));
|
||||||
|
assertThat(result.get(1)[1], equalTo(2048L));
|
||||||
|
assertThat(result.get(2)[0], equalTo(2049L));
|
||||||
|
assertThat(result.get(2)[1], equalTo(3072L));
|
||||||
|
assertThat(result.get(3)[0], equalTo(3073L));
|
||||||
|
assertThat(result.get(3)[1], equalTo(4096L));
|
||||||
|
|
||||||
|
coordinator.getChunks().clear();
|
||||||
|
coordinator.createChucks(4096, 8196);
|
||||||
|
result = new ArrayList<>(coordinator.getChunks());
|
||||||
|
assertThat(result.size(), equalTo(5));
|
||||||
|
assertThat(result.get(0)[0], equalTo(4096L));
|
||||||
|
assertThat(result.get(0)[1], equalTo(5120L));
|
||||||
|
assertThat(result.get(1)[0], equalTo(5121L));
|
||||||
|
assertThat(result.get(1)[1], equalTo(6144L));
|
||||||
|
assertThat(result.get(2)[0], equalTo(6145L));
|
||||||
|
assertThat(result.get(2)[1], equalTo(7168L));
|
||||||
|
assertThat(result.get(3)[0], equalTo(7169L));
|
||||||
|
assertThat(result.get(3)[1], equalTo(8192L));
|
||||||
|
assertThat(result.get(4)[0], equalTo(8193L));
|
||||||
|
assertThat(result.get(4)[1], equalTo(8196L));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCoordinator() throws Exception {
|
||||||
|
Client client = mock(Client.class);
|
||||||
|
mockShardChangesApiCall(client);
|
||||||
|
mockBulkShardOperationsApiCall(client);
|
||||||
|
Executor ccrExecutor = Runnable::run;
|
||||||
|
ShardId leaderShardId = new ShardId("index1", "index1", 0);
|
||||||
|
ShardId followShardId = new ShardId("index2", "index1", 0);
|
||||||
|
|
||||||
|
Consumer<Exception> handler = e -> assertThat(e, nullValue());
|
||||||
|
int concurrentProcessors = randomIntBetween(1, 4);
|
||||||
|
int batchSize = randomIntBetween(1, 1000);
|
||||||
|
ChunksCoordinator coordinator = new ChunksCoordinator(client, client, ccrExecutor, batchSize, concurrentProcessors,
|
||||||
|
Long.MAX_VALUE, leaderShardId, followShardId, handler);
|
||||||
|
|
||||||
|
int numberOfOps = randomIntBetween(batchSize, batchSize * 20);
|
||||||
|
long from = randomInt(1000);
|
||||||
|
long to = from + numberOfOps;
|
||||||
|
coordinator.createChucks(from, to);
|
||||||
|
int expectedNumberOfChunks = numberOfOps / batchSize;
|
||||||
|
if (numberOfOps % batchSize > 0) {
|
||||||
|
expectedNumberOfChunks++;
|
||||||
|
}
|
||||||
|
assertThat(coordinator.getChunks().size(), equalTo(expectedNumberOfChunks));
|
||||||
|
|
||||||
|
coordinator.start();
|
||||||
|
assertThat(coordinator.getChunks().size(), equalTo(0));
|
||||||
|
verify(client, times(expectedNumberOfChunks)).execute(same(ShardChangesAction.INSTANCE),
|
||||||
|
any(ShardChangesAction.Request.class), any());
|
||||||
|
verify(client, times(expectedNumberOfChunks)).execute(same(BulkShardOperationsAction.INSTANCE),
|
||||||
|
any(BulkShardOperationsRequest.class), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCoordinator_failure() throws Exception {
|
||||||
|
Exception expectedException = new RuntimeException("throw me");
|
||||||
|
Client client = mock(Client.class);
|
||||||
|
boolean shardChangesActionApiCallFailed;
|
||||||
|
if (randomBoolean()) {
|
||||||
|
shardChangesActionApiCallFailed = true;
|
||||||
|
doThrow(expectedException).when(client).execute(same(ShardChangesAction.INSTANCE),
|
||||||
|
any(ShardChangesAction.Request.class), any());
|
||||||
|
} else {
|
||||||
|
shardChangesActionApiCallFailed = false;
|
||||||
|
mockShardChangesApiCall(client);
|
||||||
|
doThrow(expectedException).when(client).execute(same(BulkShardOperationsAction.INSTANCE),
|
||||||
|
any(BulkShardOperationsRequest.class), any());
|
||||||
|
}
|
||||||
|
Executor ccrExecutor = Runnable::run;
|
||||||
|
ShardId leaderShardId = new ShardId("index1", "index1", 0);
|
||||||
|
ShardId followShardId = new ShardId("index2", "index1", 0);
|
||||||
|
|
||||||
|
Consumer<Exception> handler = e -> {
|
||||||
|
assertThat(e, notNullValue());
|
||||||
|
assertThat(e, sameInstance(expectedException));
|
||||||
|
};
|
||||||
|
ChunksCoordinator coordinator =
|
||||||
|
new ChunksCoordinator(client, client, ccrExecutor, 10, 1, Long.MAX_VALUE, leaderShardId, followShardId, handler);
|
||||||
|
coordinator.createChucks(0, 20);
|
||||||
|
assertThat(coordinator.getChunks().size(), equalTo(2));
|
||||||
|
|
||||||
|
coordinator.start();
|
||||||
|
assertThat(coordinator.getChunks().size(), equalTo(1));
|
||||||
|
verify(client, times(1)).execute(same(ShardChangesAction.INSTANCE), any(ShardChangesAction.Request.class),
|
||||||
|
any());
|
||||||
|
verify(client, times(shardChangesActionApiCallFailed ? 0 : 1)).execute(same(BulkShardOperationsAction.INSTANCE),
|
||||||
|
any(BulkShardOperationsRequest.class), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCoordinator_concurrent() throws Exception {
|
||||||
|
Client client = mock(Client.class);
|
||||||
|
mockShardChangesApiCall(client);
|
||||||
|
mockBulkShardOperationsApiCall(client);
|
||||||
|
Executor ccrExecutor = command -> new Thread(command).start();
|
||||||
|
ShardId leaderShardId = new ShardId("index1", "index1", 0);
|
||||||
|
ShardId followShardId = new ShardId("index2", "index1", 0);
|
||||||
|
|
||||||
|
AtomicBoolean calledOnceChecker = new AtomicBoolean(false);
|
||||||
|
AtomicReference<Exception> failureHolder = new AtomicReference<>();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
Consumer<Exception> handler = e -> {
|
||||||
|
if (failureHolder.compareAndSet(null, e) == false) {
|
||||||
|
// This handler should only be called once, irregardless of the number of concurrent processors
|
||||||
|
calledOnceChecker.set(true);
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
};
|
||||||
|
ChunksCoordinator coordinator =
|
||||||
|
new ChunksCoordinator(client, client, ccrExecutor, 1000, 4, Long.MAX_VALUE, leaderShardId, followShardId, handler);
|
||||||
|
coordinator.createChucks(0, 1000000);
|
||||||
|
assertThat(coordinator.getChunks().size(), equalTo(1000));
|
||||||
|
|
||||||
|
coordinator.start();
|
||||||
|
latch.await();
|
||||||
|
assertThat(coordinator.getChunks().size(), equalTo(0));
|
||||||
|
verify(client, times(1000)).execute(same(ShardChangesAction.INSTANCE), any(ShardChangesAction.Request.class), any());
|
||||||
|
verify(client, times(1000)).execute(same(BulkShardOperationsAction.INSTANCE), any(BulkShardOperationsRequest.class), any());
|
||||||
|
assertThat(calledOnceChecker.get(), is(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testChunkProcessor() {
|
||||||
|
Client client = mock(Client.class);
|
||||||
|
Queue<long[]> chunks = new LinkedList<>();
|
||||||
|
mockShardChangesApiCall(client);
|
||||||
|
mockBulkShardOperationsApiCall(client);
|
||||||
|
Executor ccrExecutor = Runnable::run;
|
||||||
|
ShardId leaderShardId = new ShardId("index1", "index1", 0);
|
||||||
|
ShardId followShardId = new ShardId("index2", "index1", 0);
|
||||||
|
|
||||||
|
boolean[] invoked = new boolean[1];
|
||||||
|
Exception[] exception = new Exception[1];
|
||||||
|
Consumer<Exception> handler = e -> {invoked[0] = true;exception[0] = e;};
|
||||||
|
ChunkProcessor chunkProcessor = new ChunkProcessor(client, client, chunks, ccrExecutor, leaderShardId, followShardId, handler);
|
||||||
|
chunkProcessor.start(0, 10, Long.MAX_VALUE);
|
||||||
|
assertThat(invoked[0], is(true));
|
||||||
|
assertThat(exception[0], nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testChunkProcessorRetry() {
|
||||||
|
Client client = mock(Client.class);
|
||||||
|
Queue<long[]> chunks = new LinkedList<>();
|
||||||
|
mockBulkShardOperationsApiCall(client);
|
||||||
|
int testRetryLimit = randomIntBetween(1, ShardFollowTasksExecutor.PROCESSOR_RETRY_LIMIT - 1);
|
||||||
|
mockShardCangesApiCallWithRetry(client, testRetryLimit, new ConnectException("connection exception"));
|
||||||
|
|
||||||
|
Executor ccrExecutor = Runnable::run;
|
||||||
|
ShardId leaderShardId = new ShardId("index1", "index1", 0);
|
||||||
|
ShardId followShardId = new ShardId("index2", "index1", 0);
|
||||||
|
|
||||||
|
boolean[] invoked = new boolean[1];
|
||||||
|
Exception[] exception = new Exception[1];
|
||||||
|
Consumer<Exception> handler = e -> {invoked[0] = true;exception[0] = e;};
|
||||||
|
ChunkProcessor chunkProcessor = new ChunkProcessor(client, client, chunks, ccrExecutor, leaderShardId, followShardId, handler);
|
||||||
|
chunkProcessor.start(0, 10, Long.MAX_VALUE);
|
||||||
|
assertThat(invoked[0], is(true));
|
||||||
|
assertThat(exception[0], nullValue());
|
||||||
|
assertThat(chunkProcessor.retryCounter.get(), equalTo(testRetryLimit + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testChunkProcessorRetryTooManyTimes() {
|
||||||
|
Client client = mock(Client.class);
|
||||||
|
Queue<long[]> chunks = new LinkedList<>();
|
||||||
|
mockBulkShardOperationsApiCall(client);
|
||||||
|
int testRetryLimit = ShardFollowTasksExecutor.PROCESSOR_RETRY_LIMIT + 1;
|
||||||
|
mockShardCangesApiCallWithRetry(client, testRetryLimit, new ConnectException("connection exception"));
|
||||||
|
|
||||||
|
Executor ccrExecutor = Runnable::run;
|
||||||
|
ShardId leaderShardId = new ShardId("index1", "index1", 0);
|
||||||
|
ShardId followShardId = new ShardId("index2", "index1", 0);
|
||||||
|
|
||||||
|
boolean[] invoked = new boolean[1];
|
||||||
|
Exception[] exception = new Exception[1];
|
||||||
|
Consumer<Exception> handler = e -> {invoked[0] = true;exception[0] = e;};
|
||||||
|
ChunkProcessor chunkProcessor = new ChunkProcessor(client, client, chunks, ccrExecutor, leaderShardId, followShardId, handler);
|
||||||
|
chunkProcessor.start(0, 10, Long.MAX_VALUE);
|
||||||
|
assertThat(invoked[0], is(true));
|
||||||
|
assertThat(exception[0], notNullValue());
|
||||||
|
assertThat(exception[0].getMessage(), equalTo("retrying failed [17] times, aborting..."));
|
||||||
|
assertThat(exception[0].getCause().getMessage(), equalTo("connection exception"));
|
||||||
|
assertThat(chunkProcessor.retryCounter.get(), equalTo(testRetryLimit));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testChunkProcessorNoneRetryableError() {
|
||||||
|
Client client = mock(Client.class);
|
||||||
|
Queue<long[]> chunks = new LinkedList<>();
|
||||||
|
mockBulkShardOperationsApiCall(client);
|
||||||
|
mockShardCangesApiCallWithRetry(client, 3, new RuntimeException("unexpected"));
|
||||||
|
|
||||||
|
Executor ccrExecutor = Runnable::run;
|
||||||
|
ShardId leaderShardId = new ShardId("index1", "index1", 0);
|
||||||
|
ShardId followShardId = new ShardId("index2", "index1", 0);
|
||||||
|
|
||||||
|
boolean[] invoked = new boolean[1];
|
||||||
|
Exception[] exception = new Exception[1];
|
||||||
|
Consumer<Exception> handler = e -> {invoked[0] = true;exception[0] = e;};
|
||||||
|
ChunkProcessor chunkProcessor = new ChunkProcessor(client, client, chunks, ccrExecutor, leaderShardId, followShardId, handler);
|
||||||
|
chunkProcessor.start(0, 10, Long.MAX_VALUE);
|
||||||
|
assertThat(invoked[0], is(true));
|
||||||
|
assertThat(exception[0], notNullValue());
|
||||||
|
assertThat(exception[0].getMessage(), equalTo("unexpected"));
|
||||||
|
assertThat(chunkProcessor.retryCounter.get(), equalTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testChunkProcessorExceedMaxTranslogsBytes() {
|
||||||
|
long from = 0;
|
||||||
|
long to = 20;
|
||||||
|
long actualTo = 10;
|
||||||
|
Client client = mock(Client.class);
|
||||||
|
Queue<long[]> chunks = new LinkedList<>();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
Object[] args = invocation.getArguments();
|
||||||
|
assert args.length == 3;
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ActionListener<ShardChangesAction.Response> listener = (ActionListener) args[2];
|
||||||
|
|
||||||
|
List<Translog.Operation> operations = new ArrayList<>();
|
||||||
|
for (int i = 0; i <= actualTo; i++) {
|
||||||
|
operations.add(new Translog.NoOp(i, 1, "test"));
|
||||||
|
}
|
||||||
|
listener.onResponse(new ShardChangesAction.Response(operations.toArray(new Translog.Operation[0])));
|
||||||
|
return null;
|
||||||
|
}).when(client).execute(same(ShardChangesAction.INSTANCE), any(ShardChangesAction.Request.class), any());
|
||||||
|
|
||||||
|
mockBulkShardOperationsApiCall(client);
|
||||||
|
Executor ccrExecutor = Runnable::run;
|
||||||
|
ShardId leaderShardId = new ShardId("index1", "index1", 0);
|
||||||
|
ShardId followShardId = new ShardId("index2", "index1", 0);
|
||||||
|
|
||||||
|
boolean[] invoked = new boolean[1];
|
||||||
|
Exception[] exception = new Exception[1];
|
||||||
|
Consumer<Exception> handler = e -> {invoked[0] = true;exception[0] = e;};
|
||||||
|
ChunkProcessor chunkProcessor = new ChunkProcessor(client, client, chunks, ccrExecutor, leaderShardId, followShardId, handler);
|
||||||
|
chunkProcessor.start(from, to, Long.MAX_VALUE);
|
||||||
|
assertThat(invoked[0], is(true));
|
||||||
|
assertThat(exception[0], nullValue());
|
||||||
|
assertThat(chunks.size(), equalTo(1));
|
||||||
|
assertThat(chunks.peek()[0], equalTo(11L));
|
||||||
|
assertThat(chunks.peek()[1], equalTo(20L));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockShardCangesApiCallWithRetry(Client client, int testRetryLimit, Exception e) {
|
||||||
|
int[] retryCounter = new int[1];
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
Object[] args = invocation.getArguments();
|
||||||
|
assert args.length == 3;
|
||||||
|
ShardChangesAction.Request request = (ShardChangesAction.Request) args[1];
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ActionListener<ShardChangesAction.Response> listener = (ActionListener) args[2];
|
||||||
|
if (retryCounter[0]++ <= testRetryLimit) {
|
||||||
|
listener.onFailure(e);
|
||||||
|
} else {
|
||||||
|
long delta = request.getMaxSeqNo() - request.getMinSeqNo();
|
||||||
|
Translog.Operation[] operations = new Translog.Operation[(int) delta];
|
||||||
|
for (int i = 0; i < operations.length; i++) {
|
||||||
|
operations[i] = new Translog.NoOp(request.getMinSeqNo() + i, 1, "test");
|
||||||
|
}
|
||||||
|
ShardChangesAction.Response response = new ShardChangesAction.Response(operations);
|
||||||
|
listener.onResponse(response);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).when(client).execute(same(ShardChangesAction.INSTANCE), any(ShardChangesAction.Request.class), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockShardChangesApiCall(Client client) {
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
Object[] args = invocation.getArguments();
|
||||||
|
assert args.length == 3;
|
||||||
|
ShardChangesAction.Request request = (ShardChangesAction.Request) args[1];
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ActionListener<ShardChangesAction.Response> listener = (ActionListener) args[2];
|
||||||
|
|
||||||
|
List<Translog.Operation> operations = new ArrayList<>();
|
||||||
|
for (long i = request.getMinSeqNo(); i <= request.getMaxSeqNo(); i++) {
|
||||||
|
operations.add(new Translog.NoOp(request.getMinSeqNo() + i, 1, "test"));
|
||||||
|
}
|
||||||
|
ShardChangesAction.Response response = new ShardChangesAction.Response(operations.toArray(new Translog.Operation[0]));
|
||||||
|
listener.onResponse(response);
|
||||||
|
return null;
|
||||||
|
}).when(client).execute(same(ShardChangesAction.INSTANCE), any(ShardChangesAction.Request.class), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockBulkShardOperationsApiCall(Client client) {
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
Object[] args = invocation.getArguments();
|
||||||
|
assert args.length == 3;
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ActionListener<BulkShardOperationsResponse> listener = (ActionListener) args[2];
|
||||||
|
listener.onResponse(new BulkShardOperationsResponse());
|
||||||
|
return null;
|
||||||
|
}).when(client).execute(same(BulkShardOperationsAction.INSTANCE), any(BulkShardOperationsRequest.class), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.cluster.routing.ShardRouting;
|
||||||
|
import org.elasticsearch.cluster.routing.ShardRoutingState;
|
||||||
|
import org.elasticsearch.cluster.routing.TestShardRouting;
|
||||||
|
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.shard.IndexShard;
|
||||||
|
import org.elasticsearch.index.shard.IndexShardNotStartedException;
|
||||||
|
import org.elasticsearch.index.translog.Translog;
|
||||||
|
import org.elasticsearch.test.ESSingleNodeTestCase;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.LongStream;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
|
||||||
|
public class ShardChangesActionTests extends ESSingleNodeTestCase {
|
||||||
|
|
||||||
|
public void testGetOperationsBetween() throws Exception {
|
||||||
|
final Settings settings = Settings.builder()
|
||||||
|
.put("index.number_of_shards", 1)
|
||||||
|
.put("index.number_of_replicas", 0)
|
||||||
|
.put("index.translog.generation_threshold_size", new ByteSizeValue(randomIntBetween(8, 64), ByteSizeUnit.KB))
|
||||||
|
.build();
|
||||||
|
final IndexService indexService = createIndex("index", settings);
|
||||||
|
|
||||||
|
final int numWrites = randomIntBetween(2, 8192);
|
||||||
|
for (int i = 0; i < numWrites; i++) {
|
||||||
|
client().prepareIndex("index", "doc", Integer.toString(i)).setSource("{}", XContentType.JSON).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A number of times, get operations within a range that exists:
|
||||||
|
int iters = randomIntBetween(8, 32);
|
||||||
|
IndexShard indexShard = indexService.getShard(0);
|
||||||
|
for (int iter = 0; iter < iters; iter++) {
|
||||||
|
int min = randomIntBetween(0, numWrites - 1);
|
||||||
|
int max = randomIntBetween(min, numWrites - 1);
|
||||||
|
|
||||||
|
final ShardChangesAction.Response r = ShardChangesAction.getOperationsBetween(indexShard, min, max, Long.MAX_VALUE);
|
||||||
|
/*
|
||||||
|
* We are not guaranteed that operations are returned to us in order they are in the translog (if our read crosses multiple
|
||||||
|
* generations) so the best we can assert is that we see the expected operations.
|
||||||
|
*/
|
||||||
|
final Set<Long> seenSeqNos = Arrays.stream(r.getOperations()).map(Translog.Operation::seqNo).collect(Collectors.toSet());
|
||||||
|
final Set<Long> expectedSeqNos = LongStream.range(min, max + 1).boxed().collect(Collectors.toSet());
|
||||||
|
assertThat(seenSeqNos, equalTo(expectedSeqNos));
|
||||||
|
}
|
||||||
|
|
||||||
|
// get operations for a range no operations exists:
|
||||||
|
Exception e = expectThrows(IllegalStateException.class,
|
||||||
|
() -> ShardChangesAction.getOperationsBetween(indexShard, numWrites, numWrites + 1, Long.MAX_VALUE));
|
||||||
|
assertThat(e.getMessage(), containsString("Not all operations between min_seq_no [" + numWrites + "] and max_seq_no [" +
|
||||||
|
(numWrites + 1) +"] found, tracker checkpoint ["));
|
||||||
|
|
||||||
|
// get operations for a range some operations do not exist:
|
||||||
|
e = expectThrows(IllegalStateException.class,
|
||||||
|
() -> ShardChangesAction.getOperationsBetween(indexShard, numWrites - 10, numWrites + 10, Long.MAX_VALUE));
|
||||||
|
assertThat(e.getMessage(), containsString("Not all operations between min_seq_no [" + (numWrites - 10) + "] and max_seq_no [" +
|
||||||
|
(numWrites + 10) +"] found, tracker checkpoint ["));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGetOperationsBetweenWhenShardNotStarted() throws Exception {
|
||||||
|
IndexShard indexShard = Mockito.mock(IndexShard.class);
|
||||||
|
|
||||||
|
ShardRouting shardRouting = TestShardRouting.newShardRouting("index", 0, "_node_id", true, ShardRoutingState.INITIALIZING);
|
||||||
|
Mockito.when(indexShard.routingEntry()).thenReturn(shardRouting);
|
||||||
|
expectThrows(IndexShardNotStartedException.class, () -> ShardChangesAction.getOperationsBetween(indexShard, 0, 1, Long.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGetOperationsBetweenExceedByteLimit() throws Exception {
|
||||||
|
final Settings settings = Settings.builder()
|
||||||
|
.put("index.number_of_shards", 1)
|
||||||
|
.put("index.number_of_replicas", 0)
|
||||||
|
.build();
|
||||||
|
final IndexService indexService = createIndex("index", settings);
|
||||||
|
|
||||||
|
final long numWrites = 32;
|
||||||
|
for (int i = 0; i < numWrites; i++) {
|
||||||
|
client().prepareIndex("index", "doc", Integer.toString(i)).setSource("{}", XContentType.JSON).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
final IndexShard indexShard = indexService.getShard(0);
|
||||||
|
final ShardChangesAction.Response r = ShardChangesAction.getOperationsBetween(indexShard, 0, numWrites - 1, 256);
|
||||||
|
assertThat(r.getOperations().length, equalTo(12));
|
||||||
|
assertThat(r.getOperations()[0].seqNo(), equalTo(0L));
|
||||||
|
assertThat(r.getOperations()[1].seqNo(), equalTo(1L));
|
||||||
|
assertThat(r.getOperations()[2].seqNo(), equalTo(2L));
|
||||||
|
assertThat(r.getOperations()[3].seqNo(), equalTo(3L));
|
||||||
|
assertThat(r.getOperations()[4].seqNo(), equalTo(4L));
|
||||||
|
assertThat(r.getOperations()[5].seqNo(), equalTo(5L));
|
||||||
|
assertThat(r.getOperations()[6].seqNo(), equalTo(6L));
|
||||||
|
assertThat(r.getOperations()[7].seqNo(), equalTo(7L));
|
||||||
|
assertThat(r.getOperations()[8].seqNo(), equalTo(8L));
|
||||||
|
assertThat(r.getOperations()[9].seqNo(), equalTo(9L));
|
||||||
|
assertThat(r.getOperations()[10].seqNo(), equalTo(10L));
|
||||||
|
assertThat(r.getOperations()[11].seqNo(), equalTo(11L));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.index.seqno.SequenceNumbers;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.test.AbstractStreamableTestCase;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
|
public class ShardChangesRequestTests extends AbstractStreamableTestCase<ShardChangesAction.Request> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShardChangesAction.Request createTestInstance() {
|
||||||
|
ShardChangesAction.Request request = new ShardChangesAction.Request(new ShardId("_index", "_indexUUID", 0));
|
||||||
|
request.setMaxSeqNo(randomNonNegativeLong());
|
||||||
|
request.setMinSeqNo(randomNonNegativeLong());
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShardChangesAction.Request createBlankInstance() {
|
||||||
|
return new ShardChangesAction.Request();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testValidate() {
|
||||||
|
ShardChangesAction.Request request = new ShardChangesAction.Request(new ShardId("_index", "_indexUUID", 0));
|
||||||
|
request.setMinSeqNo(-1);
|
||||||
|
assertThat(request.validate().getMessage(), containsString("minSeqNo [-1] cannot be lower than 0"));
|
||||||
|
|
||||||
|
request.setMinSeqNo(4);
|
||||||
|
assertThat(request.validate().getMessage(), containsString("minSeqNo [4] cannot be larger than maxSeqNo [0]"));
|
||||||
|
|
||||||
|
request.setMaxSeqNo(8);
|
||||||
|
assertThat(request.validate(), nullValue());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.index.translog.Translog;
|
||||||
|
import org.elasticsearch.test.AbstractStreamableTestCase;
|
||||||
|
|
||||||
|
public class ShardChangesResponseTests extends AbstractStreamableTestCase<ShardChangesAction.Response> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShardChangesAction.Response createTestInstance() {
|
||||||
|
final int numOps = randomInt(8);
|
||||||
|
final Translog.Operation[] operations = new Translog.Operation[numOps];
|
||||||
|
for (int i = 0; i < numOps; i++) {
|
||||||
|
operations[i] = new Translog.NoOp(i, 0, "test");
|
||||||
|
}
|
||||||
|
return new ShardChangesAction.Response(operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShardChangesAction.Response createBlankInstance() {
|
||||||
|
return new ShardChangesAction.Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.io.stream.Writeable;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.test.AbstractSerializingTestCase;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase<ShardFollowNodeTask.Status> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShardFollowNodeTask.Status doParseInstance(XContentParser parser) throws IOException {
|
||||||
|
return ShardFollowNodeTask.Status.fromXContent(parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShardFollowNodeTask.Status createTestInstance() {
|
||||||
|
return new ShardFollowNodeTask.Status(randomNonNegativeLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Writeable.Reader<ShardFollowNodeTask.Status> instanceReader() {
|
||||||
|
return ShardFollowNodeTask.Status::new;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.io.stream.Writeable;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.test.AbstractSerializingTestCase;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ShardFollowTaskTests extends AbstractSerializingTestCase<ShardFollowTask> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShardFollowTask doParseInstance(XContentParser parser) throws IOException {
|
||||||
|
return ShardFollowTask.fromXContent(parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShardFollowTask createTestInstance() {
|
||||||
|
return new ShardFollowTask(
|
||||||
|
randomAlphaOfLength(4),
|
||||||
|
new ShardId(randomAlphaOfLength(4), randomAlphaOfLength(4), randomInt(5)),
|
||||||
|
new ShardId(randomAlphaOfLength(4), randomAlphaOfLength(4), randomInt(5)),
|
||||||
|
randomIntBetween(1, Integer.MAX_VALUE), randomIntBetween(1, Integer.MAX_VALUE),
|
||||||
|
randomIntBetween(1, Integer.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Writeable.Reader<ShardFollowTask> instanceReader() {
|
||||||
|
return ShardFollowTask::new;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,310 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.ccr.index.engine;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.apache.lucene.document.Field;
|
||||||
|
import org.apache.lucene.document.NumericDocValuesField;
|
||||||
|
import org.apache.lucene.index.IndexWriterConfig;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.apache.lucene.search.IndexSearcher;
|
||||||
|
import org.apache.lucene.store.Directory;
|
||||||
|
import org.elasticsearch.Version;
|
||||||
|
import org.elasticsearch.action.index.IndexRequest;
|
||||||
|
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
||||||
|
import org.elasticsearch.common.CheckedBiConsumer;
|
||||||
|
import org.elasticsearch.common.bytes.BytesArray;
|
||||||
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.unit.TimeValue;
|
||||||
|
import org.elasticsearch.common.util.BigArrays;
|
||||||
|
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
|
import org.elasticsearch.index.Index;
|
||||||
|
import org.elasticsearch.index.IndexSettings;
|
||||||
|
import org.elasticsearch.index.VersionType;
|
||||||
|
import org.elasticsearch.index.codec.CodecService;
|
||||||
|
import org.elasticsearch.index.engine.Engine;
|
||||||
|
import org.elasticsearch.index.engine.EngineConfig;
|
||||||
|
import org.elasticsearch.index.engine.EngineTestCase;
|
||||||
|
import org.elasticsearch.index.engine.TranslogHandler;
|
||||||
|
import org.elasticsearch.index.mapper.IdFieldMapper;
|
||||||
|
import org.elasticsearch.index.mapper.ParseContext;
|
||||||
|
import org.elasticsearch.index.mapper.ParsedDocument;
|
||||||
|
import org.elasticsearch.index.mapper.SeqNoFieldMapper;
|
||||||
|
import org.elasticsearch.index.seqno.SequenceNumbers;
|
||||||
|
import org.elasticsearch.index.shard.ShardId;
|
||||||
|
import org.elasticsearch.index.store.DirectoryService;
|
||||||
|
import org.elasticsearch.index.store.Store;
|
||||||
|
import org.elasticsearch.index.translog.Translog;
|
||||||
|
import org.elasticsearch.index.translog.TranslogConfig;
|
||||||
|
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
|
||||||
|
import org.elasticsearch.test.DummyShardLock;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.elasticsearch.test.IndexSettingsModule;
|
||||||
|
import org.elasticsearch.threadpool.TestThreadPool;
|
||||||
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.hasToString;
|
||||||
|
|
||||||
|
public class FollowingEngineTests extends ESTestCase {
|
||||||
|
|
||||||
|
private ThreadPool threadPool;
|
||||||
|
private Index index;
|
||||||
|
private ShardId shardId;
|
||||||
|
private AtomicLong primaryTerm = new AtomicLong();
|
||||||
|
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
super.setUp();
|
||||||
|
threadPool = new TestThreadPool("following-engine-tests");
|
||||||
|
index = new Index("index", "uuid");
|
||||||
|
shardId = new ShardId(index, 0);
|
||||||
|
primaryTerm.set(randomLongBetween(1, Long.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tearDown() throws Exception {
|
||||||
|
terminate(threadPool);
|
||||||
|
super.tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testFollowingEngineRejectsNonFollowingIndex() throws IOException {
|
||||||
|
final Settings.Builder builder =
|
||||||
|
Settings.builder()
|
||||||
|
.put("index.number_of_shards", 1)
|
||||||
|
.put("index.number_of_replicas", 0)
|
||||||
|
.put("index.version.created", Version.CURRENT);
|
||||||
|
if (randomBoolean()) {
|
||||||
|
builder.put("index.xpack.ccr.following_index", false);
|
||||||
|
}
|
||||||
|
final Settings settings = builder.build();
|
||||||
|
final IndexMetaData indexMetaData = IndexMetaData.builder(index.getName()).settings(settings).build();
|
||||||
|
final IndexSettings indexSettings = new IndexSettings(indexMetaData, settings);
|
||||||
|
try (Store store = createStore(shardId, indexSettings, newDirectory())) {
|
||||||
|
final EngineConfig engineConfig = engineConfig(shardId, indexSettings, threadPool, store, logger, xContentRegistry());
|
||||||
|
final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new FollowingEngine(engineConfig));
|
||||||
|
assertThat(e, hasToString(containsString("a following engine can not be constructed for a non-following index")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIndexSeqNoIsMaintained() throws IOException {
|
||||||
|
final long seqNo = randomIntBetween(0, Integer.MAX_VALUE);
|
||||||
|
runIndexTest(
|
||||||
|
seqNo,
|
||||||
|
Engine.Operation.Origin.PRIMARY,
|
||||||
|
(followingEngine, index) -> {
|
||||||
|
final Engine.IndexResult result = followingEngine.index(index);
|
||||||
|
assertThat(result.getSeqNo(), equalTo(seqNo));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A following engine (whether or not it is an engine for a primary or replica shard) needs to maintain ordering semantics as the
|
||||||
|
* operations presented to it can arrive out of order (while a leader engine that is for a primary shard dictates the order). This test
|
||||||
|
* ensures that these semantics are maintained.
|
||||||
|
*/
|
||||||
|
public void testOutOfOrderDocuments() throws IOException {
|
||||||
|
final Settings settings =
|
||||||
|
Settings.builder()
|
||||||
|
.put("index.number_of_shards", 1)
|
||||||
|
.put("index.number_of_replicas", 0)
|
||||||
|
.put("index.version.created", Version.CURRENT)
|
||||||
|
.put("index.xpack.ccr.following_index", true)
|
||||||
|
.build();
|
||||||
|
final IndexMetaData indexMetaData = IndexMetaData.builder(index.getName()).settings(settings).build();
|
||||||
|
final IndexSettings indexSettings = new IndexSettings(indexMetaData, settings);
|
||||||
|
try (Store store = createStore(shardId, indexSettings, newDirectory())) {
|
||||||
|
final EngineConfig engineConfig = engineConfig(shardId, indexSettings, threadPool, store, logger, xContentRegistry());
|
||||||
|
try (FollowingEngine followingEngine = createEngine(store, engineConfig)) {
|
||||||
|
final VersionType versionType =
|
||||||
|
randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL, VersionType.EXTERNAL_GTE, VersionType.FORCE);
|
||||||
|
final List<Engine.Operation> ops = EngineTestCase.generateSingleDocHistory(true, versionType, false, 2, 2, 20);
|
||||||
|
EngineTestCase.assertOpsOnReplica(ops, followingEngine, true, logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void runIndexTest(
|
||||||
|
final long seqNo,
|
||||||
|
final Engine.Operation.Origin origin,
|
||||||
|
final CheckedBiConsumer<FollowingEngine, Engine.Index, IOException> consumer) throws IOException {
|
||||||
|
final Settings settings =
|
||||||
|
Settings.builder()
|
||||||
|
.put("index.number_of_shards", 1)
|
||||||
|
.put("index.number_of_replicas", 0)
|
||||||
|
.put("index.version.created", Version.CURRENT)
|
||||||
|
.put("index.xpack.ccr.following_index", true)
|
||||||
|
.build();
|
||||||
|
final IndexMetaData indexMetaData = IndexMetaData.builder(index.getName()).settings(settings).build();
|
||||||
|
final IndexSettings indexSettings = new IndexSettings(indexMetaData, settings);
|
||||||
|
try (Store store = createStore(shardId, indexSettings, newDirectory())) {
|
||||||
|
final EngineConfig engineConfig = engineConfig(shardId, indexSettings, threadPool, store, logger, xContentRegistry());
|
||||||
|
try (FollowingEngine followingEngine = createEngine(store, engineConfig)) {
|
||||||
|
final String id = "id";
|
||||||
|
final Field uidField = new Field("_id", id, IdFieldMapper.Defaults.FIELD_TYPE);
|
||||||
|
final String type = "type";
|
||||||
|
final Field versionField = new NumericDocValuesField("_version", 0);
|
||||||
|
final SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID();
|
||||||
|
final ParseContext.Document document = new ParseContext.Document();
|
||||||
|
document.add(uidField);
|
||||||
|
document.add(versionField);
|
||||||
|
document.add(seqID.seqNo);
|
||||||
|
document.add(seqID.seqNoDocValue);
|
||||||
|
document.add(seqID.primaryTerm);
|
||||||
|
final BytesReference source = new BytesArray(new byte[]{1});
|
||||||
|
final ParsedDocument parsedDocument = new ParsedDocument(
|
||||||
|
versionField,
|
||||||
|
seqID,
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
"routing",
|
||||||
|
Collections.singletonList(document),
|
||||||
|
source,
|
||||||
|
XContentType.JSON,
|
||||||
|
null);
|
||||||
|
|
||||||
|
final long version;
|
||||||
|
final long autoGeneratedIdTimestamp;
|
||||||
|
if (randomBoolean()) {
|
||||||
|
version = 1;
|
||||||
|
autoGeneratedIdTimestamp = System.currentTimeMillis();
|
||||||
|
} else {
|
||||||
|
version = randomNonNegativeLong();
|
||||||
|
autoGeneratedIdTimestamp = IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP;
|
||||||
|
}
|
||||||
|
final Engine.Index index = new Engine.Index(
|
||||||
|
new Term("_id", parsedDocument.id()),
|
||||||
|
parsedDocument,
|
||||||
|
seqNo,
|
||||||
|
primaryTerm.get(),
|
||||||
|
version,
|
||||||
|
VersionType.EXTERNAL,
|
||||||
|
origin,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
autoGeneratedIdTimestamp,
|
||||||
|
randomBoolean());
|
||||||
|
|
||||||
|
consumer.accept(followingEngine, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDeleteSeqNoIsMaintained() throws IOException {
|
||||||
|
final long seqNo = randomIntBetween(0, Integer.MAX_VALUE);
|
||||||
|
runDeleteTest(
|
||||||
|
seqNo,
|
||||||
|
Engine.Operation.Origin.PRIMARY,
|
||||||
|
(followingEngine, delete) -> {
|
||||||
|
final Engine.DeleteResult result = followingEngine.delete(delete);
|
||||||
|
assertThat(result.getSeqNo(), equalTo(seqNo));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void runDeleteTest(
|
||||||
|
final long seqNo,
|
||||||
|
final Engine.Operation.Origin origin,
|
||||||
|
final CheckedBiConsumer<FollowingEngine, Engine.Delete, IOException> consumer) throws IOException {
|
||||||
|
final Settings settings =
|
||||||
|
Settings.builder()
|
||||||
|
.put("index.number_of_shards", 1)
|
||||||
|
.put("index.number_of_replicas", 0)
|
||||||
|
.put("index.version.created", Version.CURRENT)
|
||||||
|
.put("index.xpack.ccr.following_index", true)
|
||||||
|
.build();
|
||||||
|
final IndexMetaData indexMetaData = IndexMetaData.builder(index.getName()).settings(settings).build();
|
||||||
|
final IndexSettings indexSettings = new IndexSettings(indexMetaData, settings);
|
||||||
|
try (Store store = createStore(shardId, indexSettings, newDirectory())) {
|
||||||
|
final EngineConfig engineConfig = engineConfig(shardId, indexSettings, threadPool, store, logger, xContentRegistry());
|
||||||
|
try (FollowingEngine followingEngine = createEngine(store, engineConfig)) {
|
||||||
|
final String id = "id";
|
||||||
|
final Engine.Delete delete = new Engine.Delete(
|
||||||
|
"type",
|
||||||
|
id,
|
||||||
|
new Term("_id", id),
|
||||||
|
seqNo,
|
||||||
|
primaryTerm.get(),
|
||||||
|
randomNonNegativeLong(),
|
||||||
|
VersionType.EXTERNAL,
|
||||||
|
origin,
|
||||||
|
System.currentTimeMillis());
|
||||||
|
|
||||||
|
consumer.accept(followingEngine, delete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EngineConfig engineConfig(
|
||||||
|
final ShardId shardId,
|
||||||
|
final IndexSettings indexSettings,
|
||||||
|
final ThreadPool threadPool,
|
||||||
|
final Store store,
|
||||||
|
final Logger logger,
|
||||||
|
final NamedXContentRegistry xContentRegistry) throws IOException {
|
||||||
|
final IndexWriterConfig indexWriterConfig = newIndexWriterConfig();
|
||||||
|
final Path translogPath = createTempDir("translog");
|
||||||
|
final TranslogConfig translogConfig = new TranslogConfig(shardId, translogPath, indexSettings, BigArrays.NON_RECYCLING_INSTANCE);
|
||||||
|
return new EngineConfig(
|
||||||
|
shardId,
|
||||||
|
"allocation-id",
|
||||||
|
threadPool,
|
||||||
|
indexSettings,
|
||||||
|
null,
|
||||||
|
store,
|
||||||
|
newMergePolicy(),
|
||||||
|
indexWriterConfig.getAnalyzer(),
|
||||||
|
indexWriterConfig.getSimilarity(),
|
||||||
|
new CodecService(null, logger),
|
||||||
|
new Engine.EventListener() {
|
||||||
|
@Override
|
||||||
|
public void onFailedEngine(String reason, Exception e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IndexSearcher.getDefaultQueryCache(),
|
||||||
|
IndexSearcher.getDefaultQueryCachingPolicy(),
|
||||||
|
translogConfig,
|
||||||
|
TimeValue.timeValueMinutes(5),
|
||||||
|
Collections.emptyList(),
|
||||||
|
Collections.emptyList(),
|
||||||
|
null,
|
||||||
|
new TranslogHandler(
|
||||||
|
xContentRegistry, IndexSettingsModule.newIndexSettings(shardId.getIndexName(), indexSettings.getSettings())),
|
||||||
|
new NoneCircuitBreakerService(),
|
||||||
|
() -> SequenceNumbers.NO_OPS_PERFORMED,
|
||||||
|
() -> primaryTerm.get(),
|
||||||
|
EngineTestCase::createTombstoneDoc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Store createStore(
|
||||||
|
final ShardId shardId, final IndexSettings indexSettings, final Directory directory) throws IOException {
|
||||||
|
final DirectoryService directoryService = new DirectoryService(shardId, indexSettings) {
|
||||||
|
@Override
|
||||||
|
public Directory newDirectory() throws IOException {
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new Store(shardId, indexSettings, directoryService, new DummyShardLock(shardId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private FollowingEngine createEngine(Store store, EngineConfig config) throws IOException {
|
||||||
|
store.createEmpty();
|
||||||
|
final String translogUuid = Translog.createEmptyTranslog(config.getTranslogConfig().getTranslogPath(),
|
||||||
|
SequenceNumbers.NO_OPS_PERFORMED, shardId, 1L);
|
||||||
|
store.associateIndexWithNewTranslog(translogUuid);
|
||||||
|
FollowingEngine followingEngine = new FollowingEngine(config);
|
||||||
|
followingEngine.recoverFromTranslog();
|
||||||
|
return followingEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue