/* * Copyright 2016 The Simple File Server Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sfs.elasticsearch; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.collect.FluentIterable; import com.google.common.io.CharStreams; import com.google.common.net.HostAndPort; import io.vertx.core.Context; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ActionWriteResponse; import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.Client; import org.elasticsearch.client.transport.TransportClient; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.logging.slf4j.Slf4jESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.engine.DocumentAlreadyExistsException; import org.elasticsearch.index.engine.VersionConflictEngineException; import org.sfs.Server; import org.sfs.SfsVertx; import org.sfs.VertxContext; import org.sfs.rx.Defer; import org.sfs.rx.ObservableFuture; import org.sfs.rx.RxHelper; import org.sfs.rx.ToVoid; import org.sfs.util.ConfigHelper; import org.sfs.util.ExceptionHelper; import org.sfs.util.Limits; import rx.Observable; import rx.Subscriber; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; public class Elasticsearch { enum Status { STARTING, STOPPING, STARTED, STOPPED } private static final Logger LOGGER = LoggerFactory.getLogger(Elasticsearch.class); private Client elasticSearchClient; private long defaultIndexTimeout; private long defaultSearchTimeout; private long defaultDeleteTimeout; private long defaultAdminTimeout; private long defaultGetTimeout; private long defaultScrollTimeout; private int shards; private int replicas; private AtomicReference<Status> status = new AtomicReference<>(Status.STOPPED); public Elasticsearch() { } public Observable<Void> start(final VertxContext<Server> vertxContext, final JsonObject config, boolean isMasterNode) { SfsVertx sfsVertx = vertxContext.vertx(); Context context = sfsVertx.getOrCreateContext(); return Defer.aVoid() .filter(aVoid -> status.compareAndSet(Status.STOPPED, Status.STARTING)) .flatMap(aVoid -> RxHelper.executeBlocking(context, sfsVertx.getBackgroundPool(), () -> { if (elasticSearchClient == null) { LOGGER.debug("Starting Elasticsearch"); try { ESLoggerFactory.setDefaultFactory(new Slf4jESLoggerFactory()); defaultScrollTimeout = Long.parseLong(ConfigHelper.getFieldOrEnv(config, "elasticsearch.defaultscrolltimeout", String.valueOf(TimeUnit.MINUTES.toMillis(2)))); defaultIndexTimeout = Long.parseLong(ConfigHelper.getFieldOrEnv(config, "elasticsearch.defaultindextimeout", "500")); defaultGetTimeout = Long.parseLong(ConfigHelper.getFieldOrEnv(config, "elasticsearch.defaultgettimeout", "500")); defaultSearchTimeout = Long.parseLong(ConfigHelper.getFieldOrEnv(config, "elasticsearch.defaultsearchtimeout", String.valueOf(TimeUnit.SECONDS.toMillis(5)))); defaultDeleteTimeout = Long.parseLong(ConfigHelper.getFieldOrEnv(config, "elasticsearch.defaultdeletetimeout", "500")); defaultAdminTimeout = Long.parseLong(ConfigHelper.getFieldOrEnv(config, "elasticsearch.defaultadmintimeout", String.valueOf(TimeUnit.SECONDS.toMillis(30)))); shards = Integer.parseInt(ConfigHelper.getFieldOrEnv(config, "elasticsearch.shards", String.valueOf(1))); replicas = Integer.parseInt(ConfigHelper.getFieldOrEnv(config, "elasticsearch.replicas", String.valueOf(0))); Settings.Builder settings = Settings.settingsBuilder(); settings.put("node.client", true); String clusterName = ConfigHelper.getFieldOrEnv(config, "elasticsearch.cluster.name"); if (clusterName != null) { settings.put("cluster.name", clusterName); } String nodeName = ConfigHelper.getFieldOrEnv(config, "elasticsearch.node.name"); if (nodeName != null) { settings.put("node.name", nodeName); } Iterable<String> unicastHosts = ConfigHelper.getArrayFieldOrEnv(config, "elasticsearch.discovery.zen.ping.unicast.hosts", new String[]{}); settings.put("discovery.zen.ping.multicast.enabled", ConfigHelper.getFieldOrEnv(config, "elasticsearch.discovery.zen.ping.multicast.enabled", "true")); settings.put("discovery.zen.ping.unicast.enabled", ConfigHelper.getFieldOrEnv(config, "elasticsearch.discovery.zen.ping.unicast.enabled", "false")); settings.put("discovery.zen.ping.unicast.hosts", Joiner.on(',').join(unicastHosts)); settings.put("client.transport.sniff", "true"); Iterable<InetSocketTransportAddress> transports = FluentIterable.from(unicastHosts) .filter(Predicates.notNull()) .transform(HostAndPort::fromString) .transform(input -> { try { return new InetSocketTransportAddress(InetAddress.getByName(input.getHostText()), input.getPortOrDefault(9300)); } catch (UnknownHostException e) { throw new RuntimeException(e); } }); TransportClient transportClient = TransportClient.builder().settings(settings).build(); for (InetSocketTransportAddress transportAddress : transports) { transportClient.addTransportAddress(transportAddress); } elasticSearchClient = transportClient; } catch (Exception e) { throw new RuntimeException(e); } } return null; })) .flatMap(aVoid -> waitForGreen(vertxContext)) .flatMap(aVoid -> prepareCommonIndex(vertxContext, isMasterNode)) .doOnNext(aVoid -> Preconditions.checkState(status.compareAndSet(Status.STARTING, Status.STARTED))) .doOnNext(aVoid -> LOGGER.debug("Started Elasticsearch")); } public long getDefaultScrollTimeout() { return defaultScrollTimeout; } public long getDefaultGetTimeout() { return defaultGetTimeout; } public long getDefaultAdminTimeout() { return defaultAdminTimeout; } public long getDefaultDeleteTimeout() { return defaultDeleteTimeout; } public long getDefaultIndexTimeout() { return defaultIndexTimeout; } public long getDefaultSearchTimeout() { return defaultSearchTimeout; } public String defaultType() { return "default"; } public String indexPrefix() { return "sfs_v0_"; } private Observable<Void> waitForGreen(VertxContext<Server> vertxContext) { int maxRetries = 10; ObservableFuture<Void> handler = RxHelper.observableFuture(); waitForGreen0(vertxContext, 0, maxRetries, handler); return handler; } private void waitForGreen0(VertxContext<Server> vertxContext, int retryCount, int maxRetries, ObservableFuture<Void> handler) { String indexPrefix = indexPrefix(); ClusterHealthRequestBuilder request = elasticSearchClient .admin() .cluster() .prepareHealth(indexPrefix) .setWaitForStatus(ClusterHealthStatus.GREEN) .setTimeout(timeValueSeconds(2)); execute(vertxContext, request, getDefaultAdminTimeout()) .map(Optional::get) .map(new ToVoid<>()) .subscribe(new Subscriber<Void>() { @Override public void onCompleted() { handler.complete(null); } @Override public void onError(Throwable e) { int nextRetryCount = retryCount + 1; long delayMs = ((long) Math.pow(2, nextRetryCount) * 100L); if (retryCount < maxRetries) { LOGGER.warn("Handling connect error. Retrying after " + delayMs + "ms", e); vertxContext.vertx().setTimer(delayMs, event -> waitForGreen0(vertxContext, nextRetryCount, maxRetries, handler)); } else { handler.fail(e); } } @Override public void onNext(Void aVoid) { } }); } public Observable<Void> prepareCommonIndex(VertxContext<Server> vertxContext, boolean isMasterNode) { if (isMasterNode) { return Defer.aVoid() .flatMap(aVoid -> createUpdateIndex(vertxContext, accountIndex(), "es-account-mapping.json", Limits.NOT_SET, Limits.NOT_SET)) .flatMap(aVoid -> createUpdateIndex(vertxContext, containerIndex(), "es-container-mapping.json", Limits.NOT_SET, Limits.NOT_SET)) .flatMap(aVoid -> createUpdateIndex(vertxContext, containerKeyIndex(), "es-container-key-mapping.json", Limits.NOT_SET, Limits.NOT_SET)) .flatMap(aVoid -> createUpdateIndex(vertxContext, masterKeyTypeIndex(), "es-master-key-mapping.json", Limits.NOT_SET, Limits.NOT_SET)); } else { return Defer.aVoid(); } } public Observable<Void> prepareObjectIndex(VertxContext<Server> vertxContext, String containerName, int shards, int replicas) { Preconditions.checkState(shards == Limits.NOT_SET || shards >= 1, "Shards must be >= 1"); Preconditions.checkState(replicas == Limits.NOT_SET || replicas >= 0, "Replicas must be >= 0"); String objectIndexName = objectIndex(containerName); return createUpdateIndex(vertxContext, objectIndexName, "es-object-mapping.json", shards, replicas); } public Observable<Void> deleteObjectIndex(VertxContext<Server> vertxContext, String containerName) { String objectIndexName = objectIndex(containerName); return deleteIndex(vertxContext, objectIndexName); } protected Observable<Void> deleteIndex(VertxContext<Server> vertxContext, String index) { return Defer.just(index) .flatMap(new IndexDelete(vertxContext)) .doOnNext(success -> Preconditions.checkState(success, "Failed to delete index %s", index)) .map(new ToVoid<>()) .onErrorResumeNext(throwable -> { if (ExceptionHelper.containsException(IndexNotFoundException.class, throwable)) { return Defer.aVoid(); } else { return Observable.error(throwable); } }); } protected Observable<Void> createUpdateIndex(VertxContext<Server> vertxContext, String index, String mapping, int shards, int replicas) { Elasticsearch _this = this; return getMapping(vertxContext, mapping) .flatMap(mappingData -> Defer.just(index) .flatMap(new IndexExists(vertxContext)) .flatMap(exists -> { if (Boolean.TRUE.equals(exists)) { return Defer.just(index) .flatMap(new IndexUpdateMapping(vertxContext, defaultType(), mappingData)) .doOnNext(success -> Preconditions.checkState(success, "Failed to updated index mapping %s", index)) .map(new ToVoid<>()) .flatMap(aVoid -> { if (replicas >= 0) { Settings settings = Settings.settingsBuilder() .put("index.number_of_replicas", replicas) .build(); IndexUpdateSettings indexUpdateSettings = new IndexUpdateSettings(vertxContext, settings); return indexUpdateSettings.call(index); } else { return Defer.just(true); } }) .doOnNext(success -> Preconditions.checkState(success, "Failed to updated index settings %s", index)) .map(success -> index); } else { return Defer.just(index) .flatMap(new IndexCreate(vertxContext) .withMapping(defaultType(), mappingData) .setSettings(Settings.settingsBuilder() .put("index.refresh_interval", "1s") .put("index.number_of_replicas", replicas >= 0 ? replicas : _this.replicas) .put("index.number_of_shards", shards >= 1 ? shards : _this.shards) .build())) .doOnNext(success -> Preconditions.checkState(success, "Failed to create index %s", index)) .map(success -> index); } })) .map(new ToVoid<>()) .flatMap(new IndexWaitForStatus(vertxContext, index, ClusterHealthStatus.GREEN)); } protected Observable<String> getMapping(VertxContext<Server> vertxContext, final String name) { SfsVertx sfsVertx = vertxContext.vertx(); Context context = sfsVertx.getOrCreateContext(); return RxHelper.executeBlocking(context, sfsVertx.getBackgroundPool(), () -> { try (Reader reader = new InputStreamReader(Thread.currentThread().getContextClassLoader().getResourceAsStream(name), Charsets.UTF_8)) { return CharStreams.toString(reader); } catch (IOException e) { throw new RuntimeException(e); } }); } public String accountIndex() { return indexPrefix() + "account"; } public String containerIndex() { return indexPrefix() + "container"; } public String objectIndex(String containerName) { return String.format("%s%s_objects", indexPrefix(), containerName); } public String serviceDefTypeIndex() { return indexPrefix() + "service_def"; } public String containerKeyIndex() { return indexPrefix() + "container_key"; } public String masterKeyTypeIndex() { return indexPrefix() + "master_key"; } public boolean isObjectIndex(String indexName) { return indexName != null && indexName.startsWith(indexPrefix()) && indexName.endsWith("_objects"); } public Client get() { return elasticSearchClient; } public Observable<Void> stop(VertxContext<Server> vertxContext) { SfsVertx vertx = vertxContext.vertx(); Context context = vertx.getOrCreateContext(); return Defer.aVoid() .filter(aVoid -> status.compareAndSet(Status.STARTED, Status.STOPPING) || status.compareAndSet(Status.STARTING, Status.STOPPING)) .flatMap(aVoid -> RxHelper.executeBlocking(context, vertx.getBackgroundPool(), (() -> { LOGGER.debug("Stopping Elasticsearch"); if (elasticSearchClient != null) { try { elasticSearchClient.close(); } catch (Throwable e) { LOGGER.warn(e.getLocalizedMessage(), e); } elasticSearchClient = null; } LOGGER.debug("Stopped Elasticsearch"); return (Void) null; })) .doOnNext(aVoid1 -> Preconditions.checkState(status.compareAndSet(Status.STOPPING, Status.STOPPED)))); } public <Request extends ActionRequest, Response extends ActionResponse, RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder>> Observable<Optional<Response>> execute(VertxContext<Server> vertxContext, final RequestBuilder actionRequestBuilder, long timeoutMs) { return execute(vertxContext.vertx(), actionRequestBuilder, timeoutMs); } public <Request extends ActionRequest, Response extends ActionResponse, RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder>> Observable<Optional<Response>> execute(Vertx vertx, final RequestBuilder actionRequestBuilder, long timeoutMs) { Context context = vertx.getOrCreateContext(); return Defer.aVoid() .flatMap(aVoid -> { ObservableFuture<Response> observableFuture = RxHelper.observableFuture(); actionRequestBuilder.execute(new ActionListener<Response>() { @Override public void onResponse(Response response) { context.runOnContext(event -> observableFuture.complete(response)); } @Override public void onFailure(Throwable e) { context.runOnContext(event -> observableFuture.fail(e)); } }); return observableFuture; }) .doOnNext(response -> { if (response instanceof SearchResponse) { SearchResponse searchResponse = (SearchResponse) response; int totalShards = searchResponse.getTotalShards(); int successfulShards = searchResponse.getSuccessfulShards(); Preconditions.checkState(totalShards == successfulShards, "%s shards succeeded, expected %s", successfulShards, totalShards); } else if (response instanceof ActionWriteResponse) { ActionWriteResponse actionWriteResponse = (ActionWriteResponse) response; ActionWriteResponse.ShardInfo shardInfo = actionWriteResponse.getShardInfo(); int totalShards = shardInfo.getTotal(); int successfulShards = shardInfo.getSuccessful(); Preconditions.checkState(totalShards == successfulShards, "%s shards succeeded, expected %s", successfulShards, totalShards); } else if (response instanceof AcknowledgedResponse) { AcknowledgedResponse acknowledgedResponse = (AcknowledgedResponse) response; Preconditions.checkState(acknowledgedResponse.isAcknowledged(), "request not ack'd"); } }) .map(Optional::of) .onErrorResumeNext(throwable -> { if (ExceptionHelper.containsException(DocumentAlreadyExistsException.class, throwable) || ExceptionHelper.containsException(VersionConflictEngineException.class, throwable)) { return Observable.just(Optional.absent()); } else { return Observable.error(throwable); } }); } }