
oracle.kv.impl.tif.ElasticsearchHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of oracle-nosql-server Show documentation
Show all versions of oracle-nosql-server Show documentation
NoSQL Database Server - supplies build and runtime support for the server (store) side of the Oracle NoSQL Database.
The newest version!
/*-
* Copyright (C) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This file was distributed by Oracle as part of a version of Oracle NoSQL
* Database made available at:
*
* http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
*
* Please see the LICENSE file included in the top-level directory of the
* appropriate version of Oracle NoSQL Database for a copy of the license and
* additional information.
*/
package oracle.kv.impl.tif;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicReference;
import java.util.Set;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import oracle.kv.impl.admin.Admin;
import oracle.kv.impl.admin.IllegalCommandException;
import oracle.kv.impl.admin.param.SecurityParams;
import oracle.kv.impl.api.table.FieldDefImpl;
import oracle.kv.impl.api.table.IndexImpl;
import oracle.kv.impl.api.table.IndexImpl.IndexField;
import oracle.kv.impl.api.table.RowImpl;
import oracle.kv.impl.api.table.TableImpl;
import oracle.kv.impl.api.table.TableKey;
import oracle.kv.impl.param.ParameterUtils;
import oracle.kv.impl.tif.TransactionAgenda.Commit;
import oracle.kv.impl.tif.esclient.esRequest.BulkRequest;
import oracle.kv.impl.tif.esclient.esRequest.CATRequest;
import oracle.kv.impl.tif.esclient.esRequest.ClusterHealthRequest;
import oracle.kv.impl.tif.esclient.esRequest.CreateIndexRequest;
import oracle.kv.impl.tif.esclient.esRequest.DeleteIndexRequest;
import oracle.kv.impl.tif.esclient.esRequest.DeleteRequest;
import oracle.kv.impl.tif.esclient.esRequest.GetHttpNodesRequest;
import oracle.kv.impl.tif.esclient.esRequest.GetMappingRequest;
import oracle.kv.impl.tif.esclient.esRequest.GetRequest;
import oracle.kv.impl.tif.esclient.esRequest.IndexDocumentRequest;
import oracle.kv.impl.tif.esclient.esRequest.IndexExistRequest;
import oracle.kv.impl.tif.esclient.esRequest.MappingExistRequest;
import oracle.kv.impl.tif.esclient.esRequest.PutMappingRequest;
import oracle.kv.impl.tif.esclient.esResponse.BulkResponse;
import oracle.kv.impl.tif.esclient.esResponse.CATResponse;
import oracle.kv.impl.tif.esclient.esResponse.ClusterHealthResponse;
import oracle.kv.impl.tif.esclient.esResponse.ClusterHealthResponse.ClusterHealthStatus;
import oracle.kv.impl.tif.esclient.esResponse.CreateIndexResponse;
import oracle.kv.impl.tif.esclient.esResponse.DeleteIndexResponse;
import oracle.kv.impl.tif.esclient.esResponse.DeleteResponse;
import oracle.kv.impl.tif.esclient.esResponse.GetHttpNodesResponse;
import oracle.kv.impl.tif.esclient.esResponse.GetMappingResponse;
import oracle.kv.impl.tif.esclient.esResponse.GetResponse;
import oracle.kv.impl.tif.esclient.esResponse.IndexAlreadyExistsException;
import oracle.kv.impl.tif.esclient.esResponse.IndexDocumentResponse;
import oracle.kv.impl.tif.esclient.esResponse.IndexExistResponse;
import oracle.kv.impl.tif.esclient.esResponse.MappingExistResponse;
import oracle.kv.impl.tif.esclient.esResponse.PutMappingResponse;
import oracle.kv.impl.tif.esclient.httpClient.ESHttpClient;
import oracle.kv.impl.tif.esclient.httpClient.ESHttpClientBuilder;
import oracle.kv.impl.tif.esclient.httpClient.ESHttpClientBuilder.SecurityConfigCallback;
import oracle.kv.impl.tif.esclient.httpClient.SSLContextException;
import oracle.kv.impl.tif.esclient.jsonContent.ESJsonBuilder;
import oracle.kv.impl.tif.esclient.jsonContent.ESJsonUtil;
import oracle.kv.impl.tif.esclient.restClient.ESAdminClient;
import oracle.kv.impl.tif.esclient.restClient.ESDMLClient;
import oracle.kv.impl.tif.esclient.restClient.ESRestClient;
import oracle.kv.impl.tif.esclient.restClient.RestResponse;
import oracle.kv.impl.tif.esclient.restClient.RestStatus;
import oracle.kv.impl.tif.esclient.restClient.monitoring.ESNodeMonitor;
import oracle.kv.impl.tif.esclient.restClient.monitoring.MonitorClient;
import oracle.kv.impl.tif.esclient.restClient.utils.ESLatestResponse;
import oracle.kv.impl.tif.esclient.restClient.utils.ESRestClientUtil;
import oracle.kv.impl.tif.esclient.security.TIFSSLContext;
import oracle.kv.impl.util.HostPort;
import oracle.kv.impl.util.JsonUtils;
import oracle.kv.impl.util.server.LoggerUtils;
import oracle.kv.table.ArrayDef;
import oracle.kv.table.ArrayValue;
import oracle.kv.table.FieldDef;
import oracle.kv.table.FieldValue;
import oracle.kv.table.MapDef;
import oracle.kv.table.MapValue;
import oracle.kv.table.RecordValue;
import oracle.kv.table.Table;
import oracle.kv.table.TimestampValue;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import org.apache.http.HttpHost;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
/**
* Object representing an Elastic Search (ES) handler with all ES related
* operations including ES index and mapping management and data operations.
*/
public class ElasticsearchHandler {
/*
* Property names of interest to index creation.
*/
final static String SHARDS_PROPERTY = "ES_SHARDS";
final static String REPLICAS_PROPERTY = "ES_REPLICAS";
/* The name of fields used in mapping type for TIMESTAMP */
private static final String DATE = "date";
private static final String NANOS = "nanos";
/* The JSONs of mapping type for TIMESTAMP */
private static final String SIMPLE_TIMSTAMP_TYPE_JSON =
"{\"type\":\"date\"}";
private static final String TIMESTAMP_TYPE_JSON =
"{\"" + DATE + "\": " + SIMPLE_TIMSTAMP_TYPE_JSON +
", " + "\"" + NANOS + "\":{\"type\":\"integer\"}}";
private final Logger logger;
private final ESRestClient esRestClient;
private final ESAdminClient adminClient;
private final ESDMLClient client;
private final MonitorClient esMonitoringClient;
private final ESNodeMonitor esNodeMonitor;
/* Timeout values for the http client this ElasticsearchHandler uses
* This timeout is used by ESSyncResponseListener and is based on
* the expected bulk request processing time.
*/
private static final int maxRetryTimeoutMillis = 120000;
/*
* These variables are set at Construction time. AdminClient instantiation
* does a call to ES to get the values for this.
*
* Note that one JVM process can connect to only one ES Cluster.
*/
public static String ES_VERSION;
public static String ES_CLUSTER_NAME;
/*
* This variable is set at the time of Register-ES plan.
* ElasticsearchHandler is per store.
*
* This is the store name which is prefixed to all FTS index.
*
* One effect of STORE_NAME being null is that ensureCommit() will refresh
* all indices on the ES Cluster.
*/
public static String STORE_NAME = null;
private boolean doEnsureCommit;
ElasticsearchHandler(ESRestClient esRestClient,
MonitorClient esMonitoringClient,
ESNodeMonitor esNodeMonitor,
Logger logger) {
this.esRestClient = esRestClient;
this.client = esRestClient.dml();
this.adminClient = esRestClient.admin();
this.esMonitoringClient = esMonitoringClient;
this.esNodeMonitor = esNodeMonitor;
this.logger = logger;
ES_VERSION = this.adminClient.getESVersion();
ES_CLUSTER_NAME = this.adminClient.getClusterName();
}
/**
* Closes the ES handler
*/
public void close() {
if (esNodeMonitor != null) {
esNodeMonitor.close();
}
if (esMonitoringClient != null) {
esMonitoringClient.close();
}
if (esRestClient != null) {
esRestClient.close();
}
}
/*
* Typically FTS would use only one instance of ElasticsearchHandler.
* However, this single instance restriction, if required should be at the
* TextIndexFeeder Layer and not here.
*/
public static ElasticsearchHandler newInstance(
final String clusterName,
final String esMembers,
final boolean isSecure,
SecurityParams esSecurityParams,
final int monitoringFixedDelay,
Logger logger)
throws IOException {
ESRestClient newEsRestClient = null;
MonitorClient newEsMonitoringClient = null;
ESNodeMonitor newEsNodeMonitor = null;
ESHttpClient baseRestClient = null;
ESHttpClient baseMonitoringClient = null;
if (!isSecure) {
esSecurityParams = null;
}
/*
* Create ES Http Clients for restClient and monitoring Client. No need
* to check connections as that is done during register-es-plan before
* setting the SN parameters.
*
* For now, using same logger for low level httpclient and higher level
* clients.
*/
baseRestClient =
ElasticsearchHandler.createESHttpClient(clusterName, esMembers,
esSecurityParams, logger);
baseMonitoringClient =
ElasticsearchHandler.createESHttpClient(clusterName, esMembers,
esSecurityParams, logger);
newEsRestClient = new ESRestClient(baseRestClient, logger);
newEsMonitoringClient = new MonitorClient(baseMonitoringClient,
new ESLatestResponse(), logger);
List registeredESHttpClients =
new ArrayList();
registeredESHttpClients.add(newEsMonitoringClient.getESHttpClient());
registeredESHttpClients.add(newEsRestClient.getESHttpClient());
newEsNodeMonitor =
new ESNodeMonitor(newEsMonitoringClient, monitoringFixedDelay,
registeredESHttpClients, isSecure, logger);
newEsRestClient.getESHttpClient().setFailureListener(newEsNodeMonitor);
newEsNodeMonitor.start();
return new ElasticsearchHandler(newEsRestClient, newEsMonitoringClient,
newEsNodeMonitor, logger);
}
public ESRestClient getEsRestClient() {
return esRestClient;
}
public ESAdminClient getAdminClient() {
return adminClient;
}
public ESDMLClient getClient() {
return client;
}
public MonitorClient getEsMonitoringClient() {
return esMonitoringClient;
}
/**
* Enables ensure commit
*/
void enableEnsureCommit() {
doEnsureCommit = true;
}
/**
* Checks if an ES index exists
*
* @param indexName name of ES index
*
* @return true if an ES index exists
* @throws IOException
*/
boolean existESIndex(String indexName) throws IOException {
return (existESIndex(indexName, adminClient));
}
/**
* Checks if an ES index mapping exists
*
* @param esIndexName name of ES index
* @param esIndexType type of mapping in ES index
*
* @return true if a mapping exits
* @throws IOException
*/
boolean existESIndexMapping(String esIndexName,
String esIndexType)
throws IOException {
/* if no index, no mapping */
if (!existESIndex(esIndexName)) {
return false;
}
MappingExistRequest request =
new MappingExistRequest(esIndexName, esIndexType,
adminClient.getESVersion());
MappingExistResponse response = adminClient.mappingExists(request);
/*
* Create Index API may create an empty mapping.
*/
if (response.exists()) {
GetMappingRequest getMappingReq =
new GetMappingRequest(esIndexName, esIndexType);
GetMappingResponse getMappingResp =
adminClient.getMapping(getMappingReq);
if (getMappingResp.mapping() != null) {
return true;
}
return false;
}
/* check if mapping exists in index */
return response.exists();
}
/**
*
* Gets a json string representation of a mapping.
*
* @param esIndexName name of ES index
* @param esIndexType type of mapping in ES index
*
* @return json string
* @throws IOException
*/
String getESIndexMapping(String esIndexName, String esIndexType)
throws IOException {
/* if no index, no mapping */
if (!existESIndex(esIndexName)) {
return null;
}
GetMappingRequest request = new GetMappingRequest(esIndexName,
esIndexType);
GetMappingResponse response = adminClient.getMapping(request);
return response.mapping();
}
/**
* Creates an ES index with default property, the default number of shards
* and replicas would be applied by ES.
*
* @param esIndexName name of ES index
* @throws Exception
* @throws IllegalStateException
*/
void createESIndex(String esIndexName) throws IOException {
createESIndex(esIndexName, (Map) null);
}
/**
* Creates an ES index
*
* @param esIndexName name of ES index
* @param properties Map of index properties, can be null
* @throws Exception
*/
void createESIndex(String esIndexName, Map properties)
throws IOException {
Map indexSettings = new LinkedHashMap();
if (properties != null) {
final String shards = properties.get(SHARDS_PROPERTY);
final String replicas = properties.get(REPLICAS_PROPERTY);
if (shards != null) {
if (Integer.parseInt(shards) < 1) {
throw new IllegalStateException
("The " + SHARDS_PROPERTY + " value of " + shards +
" is not allowed.");
}
indexSettings.put("number_of_shards", shards);
}
if (replicas != null) {
if (Integer.parseInt(replicas) < 0) {
throw new IllegalStateException
("The " + REPLICAS_PROPERTY + " value of " + replicas +
" is not allowed.");
}
indexSettings.put("number_of_replicas", replicas);
}
}
CreateIndexResponse createResponse = null;
try {
CreateIndexRequest createIndex =
new CreateIndexRequest(esIndexName).settings(indexSettings);
createResponse = adminClient.createIndex(createIndex);
} catch (IndexAlreadyExistsException iae) {
/*
* That is OK; multiple repnodes will all try to create the index
* at the same time, only one of them can win.
*
* Or this could be a restart of TIF case, so index could already be
* existing.
*/
logger.fine("ES index " + esIndexName + " has already been" +
"created");
return;
} catch (IOException e) {
logger.log(java.util.logging.Level.SEVERE,
" index could not be created due to:" + e);
throw e;
}
if (!createResponse.isAcknowledged()) {
throw new IllegalStateException("Fail to create ES index "
+ esIndexName);
}
logger.info("ES index " + esIndexName + " created");
}
/**
* Deletes an ES index
*
* @param esIndexName name of ES index
*
* @throws IllegalStateException
*/
void deleteESIndex(String esIndexName) throws IllegalStateException {
if (!deleteESIndex(esIndexName, adminClient, logger)) {
logger.info("nothing to delete, ES index " + esIndexName +
" does not exist.");
}
logger.info("ES index " + esIndexName + " deleted");
}
/**
* Returns all ES indices corresponding to text indices in the kvstore
*
* @param storeName name of kv store
*
* @return list of all ES index names
* @throws IOException
*/
Set getAllESIndexNamesInStore(final String storeName)
throws IOException {
return getAllESIndexInStoreInternal(storeName, adminClient);
}
/**
* Creates an ES index mapping
*
* @param esIndexName name of ES index
* @param esIndexType ES index type
* @param mappingSpec mapping specification
*
* @throws IllegalStateException
*/
void createESIndexMapping(String esIndexName,
String esIndexType,
JsonGenerator mappingSpec)
throws IllegalStateException {
PutMappingResponse mresp = null;
try {
/* ensure the ES index exists */
if (!existESIndex(esIndexName)) {
throw new IllegalStateException("ES Index " + esIndexName
+ " " + "does not exist");
}
/* ensure no pre-existing conflicting mapping */
if (existESIndexMapping(esIndexName, esIndexType)) {
/*
* It is not entirely create how create index API behaves.
* Create Index API can add empty mapping
*/
String existingMapping = getESIndexMapping(esIndexName,
esIndexType);
if (!JsonUtils.getMapFromJsonStr(existingMapping).isEmpty()) {
if (ESRestClientUtil.isMappingResponseEqual
(existingMapping,
mappingSpec,
esIndexName,
esIndexType)) {
return;
}
throw new IllegalStateException
("Mapping " + esIndexType + " already exists in index " +
esIndexName + ", but differs from new mapping." +
"\nexisting mapping: " + existingMapping +
"\nnew mapping: " + mappingSpec);
}
}
PutMappingRequest mappingReq = new PutMappingRequest(esIndexName,
esIndexType,
mappingSpec);
mresp = adminClient.createMapping(mappingReq);
} catch (IOException ioe) {
logger.severe("Exception occured while trying to create mapping:"
+ ioe);
}
if (mresp == null || !mresp.isAcknowledged()) {
String msg = "Cannot install ES mapping for ES index "
+ esIndexName + ", type " + esIndexType;
logger.info(msg);
throw new IllegalStateException(msg);
}
logger.info("Mapping created for ES index: " + esIndexName +
", index type: " + esIndexType +
", mapping spec: " + mappingSpec);
}
/**
* Fetches an entry from ES index
*
* @param esIndexName name of ES index
* @param esIndexType type of index mapping
* @param key key of entry to get
*
* @return response from ES index
* @throws IOException
*/
GetResponse get(String esIndexName, String esIndexType, String key)
throws IOException {
GetRequest req = new GetRequest(esIndexName, esIndexType, key);
return client.get(req);
}
/**
* Sends a document to ES for indexing
*
* @param document document to index
* @throws IOException
*/
IndexDocumentResponse index(IndexOperation document) throws IOException {
IndexDocumentRequest req =
new IndexDocumentRequest(document.getESIndexName(),
document.getESIndexType(),
document.getPkPath()).source(document.getDocument());
IndexDocumentResponse response = client.index(req);
ensureCommit();
return response;
}
/**
* Deletes an entry from ES index
*
* @param esIndexName name of ES index
* @param esIndexType type of index mapping
* @param key key of entry to delete
* @throws IOException
*/
DeleteResponse del(String esIndexName, String esIndexType, String key)
throws IllegalStateException,
IOException {
DeleteRequest delReq = new DeleteRequest(esIndexName, esIndexType, key);
DeleteResponse response = null;
try {
response = client.delete(delReq);
} catch (IOException e) {
throw new IllegalStateException("Could not delete document:" +
esIndexName + ":" + esIndexType + ":" + key + " due to:" +
e.getCause());
}
ensureCommit();
return response;
}
/**
* Send a bulk operation to Elasticsearch
*
* @param batch a batch of operations
* @return response from ES cluster, or null if the batch is empty.
* @throws IOException
*/
BulkResponse doBulkOperations(List batch)
throws IOException {
if (batch.size() == 0) {
return null;
}
BulkRequest bulkRequest = new BulkRequest();
/* If operations were purged by an index deletion, the batch will
* contain empty transactions. If every transaction in the batch is
* empty, then the bulkRequest will be empty. We don't want to send an
* empty bulkRequest to ES -- it will throw
* ActionRequestValidationException. So we keep track of the actual
* number of operations here, and skip this request, declaring it
* successful so that the empty transactions will be cleaned up.
*/
int numberOfOperations = 0;
for (Commit commit : batch) {
/* apply each operation to ES index */
for (IndexOperation op : commit.getOps()) {
IndexOperation.OperationType type = op.getOperation();
if (type == IndexOperation.OperationType.PUT) {
bulkRequest.add(new IndexDocumentRequest(op.getESIndexName(),
op.getESIndexType(),
op.getPkPath())
.source
(op.getDocument()));
} else if (type == IndexOperation.OperationType.DEL) {
bulkRequest.add(new DeleteRequest(op.getESIndexName(),
op.getESIndexType(),
op.getPkPath()));
} else {
throw new IllegalStateException("illegal op to ES index "
+ op.getOperation());
}
numberOfOperations++;
}
}
if (numberOfOperations == 0) {
return null;
}
/* Default timeout is one minute, which seems proper. */
return client.bulk(bulkRequest);
}
/* sync ES to ensure commit */
private void ensureCommit() throws IOException {
if (doEnsureCommit) {
/*
* First get all indices in the ES for this store. Refresh them all.
*/
Set indices = getAllESIndexNamesInStore(STORE_NAME);
client.refresh(indices.toArray(new String[0]));
}
}
/*------------------------------------------------------*/
/* static functions, start with public static functions */
/*------------------------------------------------------*/
/**
* Verify that the given Elasticsearch node exists by connecting to it.
* This is a transient connection used only during configuration. This
* method is called in the context of the Admin during plan construction.
*
* If the node doesn't exist/the connection fails, throw
* IllegalCommandException, because the user provided an incorrect address.
*
* If the node exists/the connection succeeds, ask the node for a list of
* its peers, and return that list as a String of hostname:port[,...].
*
* If storeName is not null, then we expect that an ES Index corresponding
* to the store name should NOT exist. If such does exist, then
* IllegalCommandException is thrown, unless the forceClear flag is set. If
* forceClear is true, then the offending ES index will be summarily
* removed.
*
* This method will check the cluster state of ES and verify the cluster
* name matches the provided cluster name.
*
* @param clusterName name of ES cluster
* @param transportHp host and port of ES node to connect
* @param storeName name of the NoSQL store, or null as described above
* @param secure user configuration requirement.
* This value comes from the Plan command.
* Based on this value SSLContext
* can be null or not null.
* @param secParams SecurityParams configured on SN.
* This will be used to create the
* SSLContext used in SSLEngine.
* @param forceClear if true, allows deletion of the existing ES index
* @param logger caller's logger, if any. Null is allowed in tests.
*
* @return list of discovered ES node and port
*/
public static String getAllTransports(String clusterName,
HostPort transportHp,
String storeName,
boolean secure,
SecurityParams secParams,
boolean forceClear,
Logger logger) {
final String errorMsg =
"Can't connect to an Elasticsearch cluster at ";
/*
* Create a Monitoring Client. Get All nodes in the cluster. Make sure
* that clusterName given in the argument matches. Create a string out
* of HttpHosts in the format: hostname:port[,...] Close the monitoring
* client, as this is a one time connection for configuration.
*/
StringBuilder sb = new StringBuilder();
MonitorClient monitorClient = null;
ESRestClient restClient = null;
List availableNodes = null;
GetHttpNodesResponse resp = null;
if (!secure) {
secParams = null;
}
try {
ESHttpClient baseHttpClient =
createESHttpClient(clusterName, transportHp, secParams,
logger);
monitorClient = new MonitorClient(baseHttpClient,
new ESLatestResponse(), logger);
resp =
monitorClient.getHttpNodesResponse(new GetHttpNodesRequest());
if (resp != null &&
!resp.getClusterName().equals(clusterName.trim())) {
throw new IllegalCommandException(errorMsg + transportHp +
" Given Cluster Name does not match the" +
" cluster name on ES side.");
}
if (resp != null) {
if (!ESRestClientUtil.isEmpty(resp.getClusterName()) &&
resp.getClusterName().equals(clusterName)) {
availableNodes = resp.getHttpHosts(
secure ? ESHttpClient.Scheme.HTTPS
: ESHttpClient.Scheme.HTTP,
logger);
}
}
if (availableNodes == null || availableNodes.isEmpty()) {
throw new IllegalCommandException(errorMsg + transportHp +
" {" + clusterName + "}");
}
for (HttpHost node : availableNodes) {
if (sb.length() != 0) {
sb.append(ParameterUtils.HELPER_HOST_SEPARATOR);
}
sb.append(node.getHostName()).append(":")
.append(node.getPort());
}
/*
* since each es index corresponds to a text index, we do not
* know exactly the es index name, but we know all es indices
* starts with a prefix derived from store name, which can
* be used to check if there are pre-existing es indices for
* the particular store
*/
if (storeName != null) {
/* fetch list of all indices in ES under the store */
restClient = new ESRestClient(baseHttpClient, logger);
Set allIndices =
getAllESIndexInStoreInternal(storeName,
restClient.admin());
/* delete each existing ES index if force clear */
String offendingIndexes = "";
for (String indexName : allIndices) {
if (forceClear) {
deleteESIndex(indexName, restClient.admin(), logger);
} else {
offendingIndexes += " " + indexName + "\n";
}
}
if (! "".equals(offendingIndexes)) {
throw new IllegalCommandException
("The Elasticsearch cluster \"" + clusterName +
"\" already contains indexes\n" +
"corresponding to the NoSQL Database " +
"store \"" + storeName + "\".\n" +
"Here is a list of them:\n" +
offendingIndexes +
"This situation might occur if you " +
"register an ES cluster simultaneously with\n" +
"two NoSQL Database stores that have the same " +
"store name, which is not allowed;\n" +
"or if you have removed a NoSQL store " +
"to which the ES cluster was registered\n" +
"(which makes the ES indexes orphans), " +
"and then created the store again with \n" +
"the same name. If the offending indexes " +
"are no longer needed, you can remove\n" +
"them by re-issuing the plan register-es " +
"command with the -force option.");
}
}
} catch (IOException e) {
if (e.getCause() instanceof SSLContextException) {
throw new IllegalCommandException("Could not set up Security" +
"Context based on the current security configurations" +
"for ESNode:" + transportHp);
}
// TODO: more granularity in exceptions required..especially
// connection refused.
throw new IllegalCommandException(errorMsg + transportHp, e);
} catch (Exception e) {
throw new IllegalCommandException(errorMsg + transportHp, e);
} finally {
/*
* Both restClient and monitorClient will end up closing same
* httpClient.
*/
if (monitorClient != null) {
monitorClient.close();
}
if (restClient != null) {
restClient.close();
}
}
return sb.toString();
}
/**
* Return an indication of whether the ES cluster is considered "healthy".
*
* @param esMembers
*/
public static boolean isClusterHealthy(String esMembers,
ESAdminClient esAdminClient) {
ClusterHealthRequest req = new ClusterHealthRequest();
ClusterHealthResponse resp = null;
try {
resp = esAdminClient.clusterHealth(req);
} catch (IOException e) {
/*
* Could not execute an API call after few retries means cluster is
* not healthy.
*/
return false;
}
ClusterHealthStatus status = resp.getClusterHealthStatus();
/*
* We want to insist on a GREEN status for the cluster when operations
* such as creating or deleting an index are performed. This is
* because there are weird cases where a deletion would be undone
* later, if some nodes are down when the deletion took place.
* cf. https://github.com/elastic/elasticsearch/issues/13298
*
* The problem with requiring GREEN status is that ES's out-of-the-box
* defaults result in a single-node cluster's status always being
* YELLOW. Requiring GREEN for index deletion would cause problems for
* tire-kickers, and we don't want that.
*
* The compromise is to allow YELLOW status for single-node clusters,
* but insist on GREEN for every other situation.
*/
if (ClusterHealthStatus.GREEN.equals(status)) {
return true;
}
/*
* Turns out it's not so easy to distinguish between a "single-node
* cluster" and a multi-node cluster that has only one node available.
* I am not finding a reliable way to do it.
*
* It seems that ES lacks a notion of a persistent topology. If a node
* is not present, it's as if it never existed!
*
* Things I tried:
* - NodesInfoRequest will not reliably return information
* about nodes that aren't running.
*
* - Looking at the number of unassigned shards - this only tells
* you that there aren't enough nodes to satisfy the number of
* replicas specified.
*
* - for an unassigned shard there's a "reason" it is unassigned,
* which can be any of the values of the enum
* org.elasticsearch.cluster.routing.Reason. This looked promising,
* as newly created indexes that can't satisfy their replica
* requirements give the reason value of INDEX_CREATED; but
* after a restart of the single ES node, this value changed to
* CLUSTER_RECOVERED, which is the same as for shards that were
* previously assigned to a missing node. Dang it.
* The reason value NODE_LEFT seems promising, but it's also
* transient.
*
* So, for now we will user kvstore's knowledge of the number of nodes
* in the cluster at register-es time. This isn't 100% reliable because
* the number of nodes might have changed since register-es, but that
* should be uncommon. We do advise users to issue register-es after
* changing the ES cluster's topology, to update kvstore's list of
* nodes.
*
*/
final int nRegisteredNodes = esMembers.split(",").length;
if (ClusterHealthStatus.YELLOW.equals(status) &&
resp.getNumberOfDataNodes() == 1 &&
nRegisteredNodes == 1) {
return true;
}
return false;
}
/**
* Static version of existESIndex for use by getAllTransports, when no
* ElasticsearchHandler object exists. Called during es registration.
*
* @param indexName The name of the ES index to check
* @param esAdminClient The ES Admin client handle
* @return True if the index exists
* @throws IOException
*/
public static boolean existESIndex(String indexName,
ESAdminClient esAdminClient)
throws IOException {
IndexExistRequest existsRequest =
new IndexExistRequest(indexName);
IndexExistResponse existResponse = esAdminClient.indexExists(existsRequest);
return existResponse.exists();
}
/**
* Static version of addingESIndex
*
* @param esIndexName name of ES index
* @param esAdminClient ES Admin client handle
*
* @throws IllegalStateException, IndexAlreadyExistsException
*/
public static void createESIndex(String esIndexName,
ESAdminClient esAdminClient)
throws IndexAlreadyExistsException,
IllegalStateException {
CreateIndexResponse createResponse = null;
try {
createResponse =
esAdminClient.createIndex(new CreateIndexRequest(esIndexName));
} catch (IOException e) {
throw new IllegalStateException("Fail to create ES index " +
esIndexName + " due to:" + e);
}
if (createResponse == null || !createResponse.isAcknowledged()) {
throw new IllegalStateException("Fail to create ES index " +
esIndexName);
}
}
/**
* Static version of deleteEsIndex
*
* @param indexName name of the ES index to remove
* @param esAdminClient ES Admin client handle
* @return True if the index existed and was deleted
*/
public static boolean deleteESIndex(String indexName,
ESAdminClient esAdminClient,
Logger logger) {
DeleteIndexResponse deleteIndexResponse = null;
try {
if (!existESIndex(indexName, esAdminClient)) {
return false;
}
DeleteIndexRequest deleteIndexRequest =
new DeleteIndexRequest(indexName);
deleteIndexResponse =
esAdminClient.deleteIndex(deleteIndexRequest);
if (!deleteIndexResponse.exists()) {
logger.warning("Index:" + indexName +
" could not get deleted because it did not exist.");
}
} catch (IOException e) {
logger.severe("Delete Request Failed due to:" + e);
throw new IllegalStateException("Fail to delete ES index " +
indexName + " due to:" + e);
}
if (!deleteIndexResponse.isAcknowledged()) {
throw new IllegalStateException("Fail to delete ES index " +
indexName);
}
return true;
}
/**
* Returns all ES indices corresponding to text indices in the kvstore
*
* @param storeName name of kv store
* @param adminClient es client
* @return list of all ES index names
* @throws IOException
*/
static Set getAllESIndexInStoreInternal
(final String storeName,
final ESAdminClient adminClient)
throws IOException {
final Set ret;
String prefix = null;
if (storeName != null) {
prefix = TextIndexFeeder.deriveESIndexPrefix(storeName);
}
/* fetch list of all indices in ES with prefix */
CATResponse resp = adminClient.catInfo(CATRequest.API.INDICES, null,
prefix, null, null);
ret = resp.getIndices();
return ret;
}
static String constructMapping(IndexImpl index) {
try {
JsonGenerator jsonGen = generateMapping(index);
jsonGen.flush();
return new String(
((ByteArrayOutputStream) jsonGen
.getOutputTarget()).toByteArray(),
"UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(
"Unable to serialize ES mapping" +
" for text index due to UTF-8" +
" enconding not supported " +
index.getName(), e);
} catch (Exception e) {
throw new IllegalStateException(
"Unable to serialize ES mapping for text index due to" +
" json generation issues " +
index.getName(), e);
}
}
/*
* Creates the JSON to describe an ES type mapping for this index.
*/
static JsonGenerator generateMapping(IndexImpl index) {
final Table table = index.getTable();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ESJsonBuilder jsonBuilder = new ESJsonBuilder(baos);
JsonGenerator jsonGen = jsonBuilder.jsonGenarator();
jsonGen.writeStartObject(); // Mapping Start
jsonGen.writeBooleanField("dynamic", false);
jsonBuilder.startStructure("properties").startStructure("_pkey");
jsonGen.writeBooleanField("enabled", false);
jsonBuilder.startStructure("properties").startStructure("_table");
/*
* For unit tests, this static method is used directly without
* any esHandler instantiation.
* ES_VERSION will be null in that case, as there is no
* ES Cluster started in these tests. Default ES_VERSION.
*/
if (ES_VERSION == null) {
ES_VERSION = "2.4.4";
}
if (ES_VERSION.compareTo("5") > 0) {
jsonGen.writeStringField("type", "keyword");
} else {
jsonGen.writeStringField("type", "string");
}
jsonGen.writeEndObject(); // end _table structure
for (String keyField : table.getPrimaryKey()) {
String type = defaultESTypeFor(table.getField(keyField));
if ("string".equals(type)) {
if (ES_VERSION.compareTo("5") > 0) {
jsonBuilder.field(keyField,
getMappingTypeInfo("keyword"));
} else {
jsonBuilder.field(keyField,
getMappingTypeInfo("string"));
}
} else {
jsonBuilder.field(keyField, getMappingTypeInfo(type));
}
}
jsonGen.writeEndObject(); // end pkey properties
jsonGen.writeEndObject(); // end pkey
/*
* We want to preserve the letter case of field names in the ES
* document type, but the name of the path in IndexField is
* lower-cased. The field names in IndexImpl have their original
* case intact. So we iterate over the list of String field names
* and the list of IndexFields in parallel, so that we have the
* unmolested name in hand when it's needed.
*/
List indexFields = index.getIndexFields();
int indexFieldCounter = 0;
for (String field : index.getFields()) {
IndexField indexField = indexFields.get(indexFieldCounter++);
/*
* We have to parse the mappingSpec string so that it is copied
* correctly into the builder. The mappingSpec cannot be treated
* as a string, or it will be quoted in its entirety in the
* resulting JSON string.
*/
String annotation = index.getAnnotationForField(field);
annotation = (annotation == null ? "{}" : annotation);
try (JsonParser parser = ESJsonUtil.createParser(annotation)) {
Map m = ESJsonUtil.parseAsMap(parser);
String mappingFieldName = getMappingFieldName(field);
if (null == m.get("type")) {
String type = getMappingFieldType(indexField);
if ("string".equals(type)) {
if (ES_VERSION.compareTo("5") > 0) {
m.putAll(getMappingTypeInfo("text"));
} else {
m.putAll(getMappingTypeInfo("string"));
}
}
m.putAll(getMappingTypeInfo(type));
}
jsonBuilder.field(mappingFieldName, m);
}
}
jsonGen.writeEndObject(); // top leve properties end
jsonGen.writeEndObject(); // mapping structure end
return jsonGen;
} catch (IOException e) {
throw new IllegalStateException
("Unable to serialize ES mapping"+"" + " for text index " +
index.getName(), e);
}
}
/**
* Returns a map that contains the type information
*
* For non-TIMESTAMP type, its type information is as below: {
* "type": }
*
* For TIMESTAMP type, it is basically mapped to ES "date" field with an
* additional parameter "format", but since ES "date" field supports up to
* millisecond precision and TIMESTAMP in NoSQL supports up to nanosecond
* precision, so based on its precision, it will be mapped to 2 kinds of
* types:
*
* For TIMESTAMP with precision <= 3, it is mapped to a single "date" field:
* { "type":"date", "format":"yyyy-MM-dd'T'HH:mm:ss[.SSS]G" }
*
* For TIMESTAMP with precision > 3, it is mapped it to a object of "date"
* and "integer", the "date" field represents a Timestamp without fractional
* second, the "integer" field represents the nanosecond: { "properties":{
* "date": { "type":"date", "format":"yyyy-MM-dd'T'HH:mm:ssG" }, "nanos":{
* "type":"integer" } } }
*
* @throws IOException
*/
private static Map getMappingTypeInfo(String type)
throws IOException {
if (type.startsWith(DATE)) {
int precision = Integer.parseInt(type.substring(DATE.length()));
return getTimestampTypeProps(precision);
}
Map map = new HashMap(1);
map.put("type", type);
return map;
}
/**
* Returns a map that contains mapping type information for TIMESTAMP type.
*/
private static Map getTimestampTypeProps(int precision)
throws IOException {
if (simpleDate(precision)) {
try (JsonParser parser = ESJsonUtil.createParser(SIMPLE_TIMSTAMP_TYPE_JSON)) {
return ESJsonUtil.parseAsMap(parser);
}
}
Map map = new HashMap();
try (JsonParser parser = ESJsonUtil.createParser(TIMESTAMP_TYPE_JSON)) {
Map props = ESJsonUtil.parseAsMap(parser);
map.put("properties", props);
}
return map;
}
/**
* Checks if using simple "date" according to the given precision or
* composite one.
*/
private static boolean simpleDate(int precision) {
return precision <= 3;
}
/*
* Mangle a table field's name so that it works as an ES mapping field
* name. In particular, the '.' character is not allowed in mappings,
* so we substitute '/' for '.'. Otherwise, the name is used as is,
* including the perverse coding [] that marks a map value.
*/
private static String getMappingFieldName(String field) {
return field.replace('.', '/');
}
/*
* Return the default type for the field represented by the iField.
*/
private static String getMappingFieldType(IndexField ifield) {
/* The possibilities are as follows:
*
* 1. ifield represents a scalar type.
*
* 2. ifield represents an Array
* 2a. The array contains a scalar type.
* 2b. The array contains a record and ifield refers to a
* scalar type field in the record.
*
* 3. ifield represents a Record and refers to a scalar field
* in the Record.
*
* 4. ifield represents a Map
* 4a. ifield refers to the Map's string key.
* 4b. ifield refers to the Map's value.
* 4c. ifield refers to a specific key name.
*/
FieldDef fdef = ifield.getFirstDef();
int stepIdx = 0;
String fieldName = ifield.getStep(stepIdx++);
switch (fdef.getType()) {
case STRING:
case INTEGER:
case LONG:
case BOOLEAN:
case FLOAT:
case DOUBLE:
case TIMESTAMP:
/* case 1 */
return defaultESTypeFor(fdef);
case ARRAY:
final ArrayDef adef = fdef.asArray();
fdef = adef.getElement();
if (!fdef.isComplex()) {
/* case 2a. */
return defaultESTypeFor(fdef);
}
if (!fdef.isRecord()) {
throw new IllegalStateException
("Array type " + fdef + " not allowed as an index field.");
}
/* case 2b. */
stepIdx++; /* Skip over the ifield placeholder "[]" */
//$FALL-THROUGH$
case RECORD:
/* case 3. */
fieldName = ifield.getStep(stepIdx++);
fdef = fdef.asRecord().getFieldDef(fieldName);
return defaultESTypeFor(fdef);
case MAP:
final MapDef mdef = fdef.asMap();
fieldName = ifield.getStep(stepIdx++);
if (TableImpl.KEYS.equalsIgnoreCase(fieldName)) {
/* case 4a. Keys are always strings. */
return defaultESTypeFor(FieldDefImpl.stringDef);
}
/* case 4b and 4c are the same from a schema standpoint. */
fdef = mdef.getElement();
return defaultESTypeFor(fdef);
default:
throw new IllegalStateException
("Fields of type " + fdef + " aren't allowed as index fields.");
}
}
/*
* Returns a put operation containing a JSON document suitable for
* indexing at an ES index, based on the given RowImpl.
*
* @param esIndexName name of es index to which the put operation is
* created
* @param esIndexType es index mapping to which the put operation is
* created
* @param row row from which to create a put operation
* @return a put index operation to an es index; if null, it means
* that no significant content was found.
*/
static IndexOperation makePutOperation(IndexImpl index,
String esIndexName,
String esIndexType,
RowImpl row) {
final Table table = index.getTable();
assert (table == row.getTable());
/* The encoded string form of the row's primary key. */
String pkPath = TableKey.createKey(table, row, false).getKey()
.toString();
try {
/* root object */
ESJsonBuilder document = ESJsonBuilder.builder()
.startStructure()
.startStructure("_pkey")
. /* nested primary key object */
field("_table",
table.getFullName());
for (String keyField : table.getPrimaryKey()) {
new DocEmitter(keyField, document).putValue(row.get(keyField));
}
document.endStructure(); /* end of primary key object */
List indexFields = index.getIndexFields();
int indexFieldCounter = 0;
boolean contentToIndex = false;
for (String field : index.getFields()) {
IndexField indexField = indexFields.get(indexFieldCounter++);
if (addValue(indexField, row, getMappingFieldName(field),
document)) {
contentToIndex = true;
}
}
if (!contentToIndex) {
return null;
}
document.endStructure(); /* end of root object */
return new IndexOperation(esIndexName, esIndexType, pkPath,
document.byteArray(),
IndexOperation.OperationType.PUT);
} catch (IOException e) {
throw new IllegalStateException
("Unable to serialize ES" + " document for text index " +
index.getName(), e);
}
}
/*
* Add a field to the JSON document using the value implied by indexField
* and row. A return value of false indicates that no indexable content
* was found.
*/
private static boolean addValue(IndexField indexField,
RowImpl row,
String mappingFieldName,
ESJsonBuilder document) throws IOException {
FieldDef fdef = indexField.getFirstDef();
int stepIdx = 0;
String fieldName = indexField.getStep(stepIdx++);
/*
* Emit the field name lazily; if there is nothing to index,
* don't bother indexing the field at all.
*/
final DocEmitter emitter = new DocEmitter(mappingFieldName, document);
switch (fdef.getType()) {
/* Scalar types are easy. */
case STRING:
case INTEGER:
case LONG:
case BOOLEAN:
case FLOAT:
case DOUBLE:
case TIMESTAMP:
emitter.putValue(row.get(fieldName));
break;
/* An array can contain either scalars or records.
* If it's an array of records, one field of the record will
* be indicated by IndexField.
*/
case ARRAY:
final ArrayValue aValue = row.get(fieldName).asArray();
final ArrayDef adef = fdef.asArray();
fdef = adef.getElement();
if (fdef.isComplex()) {
if (!fdef.isRecord()) {
throw new IllegalStateException
("Array type " + fdef +
" not allowed as an index field.");
}
stepIdx++; /* Skip over the ifield placeholder "[]" */
fieldName = indexField.getStep(stepIdx++);
}
for (FieldValue element : aValue.toList()) {
if (element.isRecord()) {
emitter.putArrayValue(element.asRecord()
.get(fieldName));
} else {
emitter.putArrayValue(element);
}
}
break;
/*
* A record will have one field indicated for indexing.
*/
case RECORD:
RecordValue rValue = row.get(fieldName).asRecord();
fieldName = indexField.getStep(stepIdx++);
emitter.putValue(rValue.get(fieldName));
break;
/*
* An index field can specify that all keys, all values, or one
* value corresponding to a given key be included in the index.
*/
case MAP:
final MapValue mValue = row.get(fieldName).asMap();
final Map mFields = mValue.getFields();
fieldName = indexField.getStep(stepIdx++);
if (TableImpl.KEYS.equalsIgnoreCase(fieldName)) {
/* add all the keys in the map */
for (String key : mFields.keySet()) {
emitter.putArrayString(key);
}
} else if (TableImpl.VALUES.equalsIgnoreCase(fieldName)) {
/* add all the values in the map */
for (Entry entry : mFields.entrySet()) {
emitter.putArrayValue(entry.getValue());
}
} else {
emitter.putValue(mFields.get(fieldName));
}
break;
default:
throw new IllegalStateException
("Unexpected type in addValue" + fdef);
}
emitter.end();
return emitter.emitted();
}
/*
* DocEmitter is a helper class for writing fields into an XContentBuilder.
* It delays writing the field name, so that if it is discovered that there
* is no content of interest, it can avoid writing anything at all.
*/
private static class DocEmitter {
private final String fieldName;
private final ESJsonBuilder document;
private boolean emitted;
private boolean emittingArray;
DocEmitter(String fieldName, ESJsonBuilder document) {
this.fieldName = fieldName;
this.document = document;
this.emitted = false;
this.emittingArray = false;
}
private void startEmittingMaybe() throws IOException {
if (!emitted) {
document.field(fieldName);
emitted = true;
}
}
private void startEmittingArrayMaybe() throws IOException {
startEmittingMaybe();
if (!emittingArray) {
document.startArray();
emittingArray = true;
}
}
void putString(String val) throws IOException {
if (val == null) {
return;
}
startEmittingMaybe();
document.value(val);
}
void putValue(FieldValue val) throws IOException {
if (val == null || val.isNull()) {
return;
}
if (val.isTimestamp()) {
putTimestamp(val.asTimestamp());
} else {
putString(val.toString());
}
}
private void putTimestamp(TimestampValue tsv) throws IOException {
final int precision = tsv.getDefinition().asTimestamp()
.getPrecision();
if (simpleDate(precision)) {
putString(tsv.toString());
} else {
startEmittingMaybe();
document.startStructure();
putTimestampObject(tsv);
document.endStructure();
}
}
private void putTimestampObject(TimestampValue tsv) throws IOException {
document.field(DATE);
String str = tsv.toString();
putString(str.substring(0, str.indexOf(".")));
document.field(NANOS);
putString(String.valueOf(tsv.get().getNanos()));
}
void putArrayString(String val) throws IOException {
if (val == null || "".equals(val)) {
return;
}
startEmittingArrayMaybe();
putString(val);
}
void putArrayValue(FieldValue val) throws IOException {
if (val == null || val.isNull()) {
return;
}
startEmittingArrayMaybe();
if (val.isTimestamp()) {
putTimestamp(val.asTimestamp());
} else {
putString(val.toString());
}
}
void end() throws IOException {
if (emittingArray) {
document.endArray();
emittingArray = false;
}
}
boolean emitted() {
return emitted;
}
}
/*
* Returns a delete operation containing a JSON document suitable for
* indexing at an ES index, based on the given RowImpl.
*
* @param esIndexName name of es index to which the delete operation is
* created
* @param esIndexType es index mapping to which the delete operation is
* created
* @param row row from which to create a delete operation
*
* @return a delete operation to an es index
*/
static IndexOperation makeDeleteOperation(IndexImpl index,
String esIndexName,
String esIndexType,
RowImpl row) {
final Table table = index.getTable();
assert table == row.getTable();
/* The encoded string form of the row's primary key. */
String pkPath =
TableKey.createKey(table, row, false).getKey().toString();
return new IndexOperation(esIndexName,
esIndexType,
pkPath,
null,
IndexOperation.OperationType.DEL);
}
/*
* Provides a default translation between NoSQL types and ES types.
*
* The TIMESTAMP type is translated to "date" e.g. "date3" for
* TIMESTAMP(3)
*
* @param fdef field definition in NoSQL DB
*
* @return ES type translated from field type
*/
static String defaultESTypeFor(FieldDef fdef) {
FieldDef.Type t = fdef.getType();
switch (t) {
case STRING:
case INTEGER:
case LONG:
case BOOLEAN:
case FLOAT:
case DOUBLE:
return t.toString().toLowerCase();
case TIMESTAMP:
return "date" + fdef.asTimestamp().getPrecision();
case ARRAY:
case BINARY:
case FIXED_BINARY:
case MAP:
case RECORD:
case ENUM:
default:
throw new IllegalStateException
("Unexpected default type mapping requested for " + t);
}
}
/*
* Returns true if f represents a retriable failure. Some ES errors
* indicate that a there is a problem with the document that was sent to
* ES. Such documents will never succeed in being indexed and so should
* not be retried. The status code is intended for REST request statuses,
* and not all of the possible values are relevant to the bulk request. I
* have chosen to list all possible values in the switch statement anyway;
* any that seem irrelevant are simply relegated to the "not retriable"
* category.
*
* @param f A Failure object from a BulkItemResponse.
*
* @return Boolean indication of whether the failure should be re-tried.
*/
static boolean isRetriable(RestStatus status) {
switch (status) {
case BAD_GATEWAY:
case CONFLICT:
case GATEWAY_TIMEOUT:
case INSUFFICIENT_STORAGE:
case INTERNAL_SERVER_ERROR:
case TOO_MANY_REQUESTS: /*
* Returned if we try to use a client node that
* has been shut down; which would be a bug
*/
case SERVICE_UNAVAILABLE: /*
* Returned if the shard has insufficient
* replicas - this is the significant one
*/
return true;
case ACCEPTED:
case BAD_REQUEST:
case CONTINUE:
case CREATED:
case EXPECTATION_FAILED:
case FAILED_DEPENDENCY:
case FOUND:
case FORBIDDEN:
case GONE:
case HTTP_VERSION_NOT_SUPPORTED:
case LENGTH_REQUIRED:
case LOCKED:
case METHOD_NOT_ALLOWED:
case MOVED_PERMANENTLY:
case MULTIPLE_CHOICES:
case MULTI_STATUS:
case NON_AUTHORITATIVE_INFORMATION:
case NOT_ACCEPTABLE:
case NOT_FOUND:
case NOT_IMPLEMENTED:
case NOT_MODIFIED:
case NO_CONTENT:
case OK:
case PARTIAL_CONTENT:
case PAYMENT_REQUIRED:
case PRECONDITION_FAILED:
case PROXY_AUTHENTICATION:
case REQUESTED_RANGE_NOT_SATISFIED:
case REQUEST_ENTITY_TOO_LARGE:
case REQUEST_TIMEOUT:
case REQUEST_URI_TOO_LONG:
case RESET_CONTENT:
case SEE_OTHER:
case SWITCHING_PROTOCOLS:
case TEMPORARY_REDIRECT:
case UNAUTHORIZED:
case UNPROCESSABLE_ENTITY:
case UNSUPPORTED_MEDIA_TYPE:
case USE_PROXY:
default:
return false;
}
}
/* Convenience Static Utility Methods */
/*
* For use in the context of KV AdminService
*/
/**
* For use in the context of KV AdminService
*
* The parameter secure can be checked by the StorageNode Parameter
* ES_CLUSTER_SECURE.
*
* @param clusterName - ES Cluster name.
* @param esMembers - hostport pair of registered ES Node.
* @param secure - true means ES Cluster is available on https.(TLS)
* @param admin - The admin instance
* @return - ESRestClient
* @throws IOException - Exception thrown if Client could not be created.
*/
public static ESRestClient createESRestClient(String clusterName,
String esMembers,
boolean secure,
Admin admin)
throws IOException {
ESHttpClient baseHttpClient = null;
if (!secure) {
baseHttpClient = createESHttpClient(clusterName, esMembers,
admin.getLogger());
} else {
baseHttpClient = createESHttpClient(clusterName, esMembers,
admin.getParams()
.getSecurityParams(),
admin.getLogger());
}
return new ESRestClient(baseHttpClient, admin.getLogger());
}
/*
* Non secure ES Client.
*/
public static ESRestClient createESRestClient(String clusterName,
String esMembers,
Logger logger)
throws IOException {
ESHttpClient baseHttpClient = null;
baseHttpClient = createESHttpClient(clusterName, esMembers, logger);
return new ESRestClient(baseHttpClient, logger);
}
/*
* The caller has to make sure that ES Cluster is set up in a secure
* fashion.
*
* This method does not check that because register-es plan makes sure that
* if KVStore is running in secured mode, that ES Cluster has to be
* registered for HTTPS.
*
* Whether ESCluster is secured or not, can be checked by StorageNode
* Parameter, SEARCH_CLUSTER_SECURE.
*/
public static ESHttpClient createESHttpClient(String clusterName,
String esMembers,
SecurityParams secParams,
Logger logger)
throws IOException {
if (ESRestClientUtil.isEmpty(esMembers)) {
throw new IllegalArgumentException();
}
final HostPort[] hps = HostPort.parse(esMembers.split(","));
return createESHttpClient(clusterName, hps, secParams, logger);
}
/*
* The caller has to make sure that ES Cluster is set up in a secure
* fashion.
*
* This method does not check that because register-es plan makes sure that
* if KVStore is running in secured mode, that ES Cluster has to be
* registered for HTTPS.
*
* Whether ESCluster is secured or not, can be checked by StorageNode
* Parameter, SEARCH_CLUSTER_SECURE.
*/
public static ESHttpClient createESHttpClient(String clusterName,
HostPort hostPort,
SecurityParams secParams,
Logger logger)
throws IOException {
HostPort[] hostPorts = new HostPort[1];
hostPorts[0] = hostPort;
return createESHttpClient(clusterName, hostPorts, secParams, logger);
}
public static ESHttpClient createESHttpClient(String clusterName,
HostPort[] hostPorts,
SecurityParams secParams,
Logger logger)
throws IOException {
return createESHttpClient(clusterName, hostPorts, secParams, logger,
maxRetryTimeoutMillis);
}
public static ESHttpClient createESHttpClient(String clusterName,
HostPort[] hostPorts,
SecurityParams secParams,
Logger logger,
int retryTimeout)
throws IOException {
if (logger == null) {
logger = LoggerUtils.getLogger(ElasticsearchHandler.class,
"[es]");
}
if (secParams == null || !secParams.isSecure()) {
return createESHttpClient(clusterName, hostPorts, logger);
}
AtomicReference ksPwdAR = new AtomicReference();
SSLContext sslContext = null;
try {
sslContext = TIFSSLContext.makeSSLContext(secParams, ksPwdAR, logger);
/*
* No need to check connections as that is done during
* register-es-plan before setting the SN parameters.
*
* For now, using same logger for low level httpclient and higher
* level clients.
*/
ESHttpClient client = createESHttpClient(hostPorts, true,
sslContext, retryTimeout,
logger);
if (verifyClusterName(clusterName, client)) {
return client;
}
throw new IOException("ClusterName does not match on ES Cluster");
} catch (SSLContextException e) {
throw new IOException(e);
} finally {
if (ksPwdAR.get() != null)
Arrays.fill(ksPwdAR.get(), ' ');
}
}
/*
* Currently setting up ES Client does not check if the given ES Node
* Members is hosting the cluster given by the clusterName parameter.
*/
private static ESHttpClient createESHttpClient(String clusterName,
String esMembers,
Logger logger)
throws IOException {
if (ESRestClientUtil.isEmpty(esMembers)) {
throw new IllegalArgumentException();
}
final HostPort[] hps = HostPort.parse(esMembers.split(","));
return createESHttpClient(clusterName, hps, logger);
}
private static ESHttpClient createESHttpClient(String clusterName,
HostPort[] hps,
Logger logger)
throws IOException {
if (hps == null || hps.length == 0) {
throw new IllegalArgumentException();
}
ESHttpClient client = createESHttpClient(hps, false, null,
maxRetryTimeoutMillis,
logger);
if (verifyClusterName(clusterName, client)) {
return client;
}
throw new IOException("ClusterName does not match on ES Cluster");
}
/*
* FOR TEST PURPOSES ONLY - SSLContext Creation is private to this class. *
*/
public static ESHttpClient createESHttpClient(String clusterName,
HostPort[] hostPorts,
boolean secure,
SSLContext sslContext,
int retryTimeout,
Logger logger)
throws IOException {
ESHttpClient client = createESHttpClient(hostPorts, secure,
sslContext, retryTimeout,
logger);
if (verifyClusterName(clusterName, client)) {
return client;
}
throw new IOException("ClusterName does not match on ES Cluster");
}
/**
*
* @param hostPorts ES Node HostPorts.
* @param secure A boolean variable coming from the user end.
* @param sslContext An SSLContext containing keystore info. Note that
* the keystore password is filled with null after the method is done. if
* secure is true, needs SSLContext. Caller method should create this in
* case, user configures a secure connection.
* @param retryTimeout timeout before retries give up.
* @return ESHttpClient instance.
*/
private static ESHttpClient createESHttpClient(
HostPort[] hostPorts,
boolean secure,
SSLContext sslContext,
int retryTimeout,
Logger logger) {
if (hostPorts == null || hostPorts.length == 0) {
throw new IllegalArgumentException("hostPorts is required");
}
ESHttpClient baseHttpClient = null;
HttpHost[] httpHosts = new HttpHost[hostPorts.length];
int i = 0;
for (HostPort hostPort : hostPorts) {
httpHosts[i++] = new HttpHost(hostPort.hostname(), hostPort.port(),
secure ? "https" : "http");
}
ESHttpClientBuilder builder =
new ESHttpClientBuilder(httpHosts).setMaxRetryTimeoutMillis
(retryTimeout)
.setLogger(logger);
if (secure) {
if (sslContext == null) {
throw new IllegalArgumentException();
}
builder.setSecurityConfigCallback(new SecurityConfigCallback() {
@Override
public HttpAsyncClientBuilder addSecurityConfig
(HttpAsyncClientBuilder
httpClientBuilder) {
return httpClientBuilder.setSSLContext(sslContext);
}
});
}
baseHttpClient = builder.build();
return baseHttpClient;
}
public static boolean verifyClusterName(String clusterName,
ESHttpClient httpClient)
throws IOException {
if (ESRestClientUtil.isEmpty(clusterName)) {
return false;
}
JsonParser parser = null;
try {
String esClusterName;
RestResponse resp =
httpClient.executeSync(HttpGet.METHOD_NAME, "");
parser = ESRestClientUtil.initParser(resp);
JsonToken token;
String currentFieldName;
while ((token = parser.nextToken()) != JsonToken.END_OBJECT) {
if (token.isScalarValue()) {
currentFieldName = parser.getCurrentName();
if ("cluster_name".equals(currentFieldName)) {
esClusterName = parser.getText();
if (clusterName.equals(esClusterName)) {
return true;
}
}
}
}
} catch (Exception e) {
throw new IOException(e);
} finally {
if (parser != null) {
parser.close();
}
}
return false;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy