com.arcadedb.remote.RemoteHttpComponent Maven / Gradle / Ivy
The newest version!
/*
* Copyright © 2021-present Arcade Data Ltd ([email protected])
*
* 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.
*
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected])
* SPDX-License-Identifier: Apache-2.0
*/
package com.arcadedb.remote;
import com.arcadedb.ContextConfiguration;
import com.arcadedb.GlobalConfiguration;
import com.arcadedb.database.DatabaseFactory;
import com.arcadedb.database.DatabaseStats;
import com.arcadedb.database.RID;
import com.arcadedb.exception.ConcurrentModificationException;
import com.arcadedb.exception.DatabaseOperationException;
import com.arcadedb.exception.DuplicatedKeyException;
import com.arcadedb.exception.NeedRetryException;
import com.arcadedb.exception.RecordNotFoundException;
import com.arcadedb.exception.SchemaException;
import com.arcadedb.exception.TimeoutException;
import com.arcadedb.exception.TransactionException;
import com.arcadedb.log.LogManager;
import com.arcadedb.network.HostUtil;
import com.arcadedb.network.binary.QuorumNotReachedException;
import com.arcadedb.network.binary.ServerIsNotTheLeaderException;
import com.arcadedb.serializer.json.JSONObject;
import com.arcadedb.utility.FileUtils;
import com.arcadedb.utility.Pair;
import com.arcadedb.utility.RWLockContext;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;
import java.util.logging.*;
import java.util.stream.*;
/**
* Remote Database implementation. It's not thread safe. For multi-thread usage create one instance of RemoteDatabase per thread.
*
* @author Luca Garulli ([email protected])
*/
public class RemoteHttpComponent extends RWLockContext {
public static final int DEFAULT_PORT = 2480;
private final String originalServer;
private final int originalPort;
private int apiVersion = 1;
protected final ContextConfiguration configuration;
private final String userName;
private final String userPassword;
private final List> replicaServerList = new ArrayList<>();
protected String currentServer;
protected int currentPort;
private CONNECTION_STRATEGY connectionStrategy = CONNECTION_STRATEGY.ROUND_ROBIN;
private Pair leaderServer;
private int currentReplicaServerIndex = -1;
private int timeout;
protected static final String protocol = "http";
private static final String charset = "UTF-8";
protected final DatabaseStats stats = new DatabaseStats();
public enum CONNECTION_STRATEGY {
STICKY, ROUND_ROBIN
}
public interface Callback {
Object call(HttpURLConnection iArgument, JSONObject response) throws Exception;
}
public RemoteHttpComponent(final String server, final int port, final String userName, final String userPassword) {
this(server, port, userName, userPassword, new ContextConfiguration());
}
public RemoteHttpComponent(final String server, final int port, final String userName, final String userPassword,
final ContextConfiguration configuration) {
this.originalServer = server;
this.originalPort = port;
this.currentServer = originalServer;
this.currentPort = originalPort;
this.userName = userName;
this.userPassword = userPassword;
this.configuration = configuration;
this.timeout = this.configuration.getValueAsInteger(GlobalConfiguration.NETWORK_SOCKET_TIMEOUT);
requestClusterConfiguration();
}
public int getTimeout() {
return timeout;
}
public void setTimeout(final int timeout) {
this.timeout = timeout;
}
public String getUserName() {
return userName;
}
public String getUserPassword() {
return userPassword;
}
List> getReplicaServerList() {
return replicaServerList;
}
public Map getStats() {
return stats.toMap();
}
Object httpCommand(final String method, final String extendedURL, final String operation, final String language,
final String payloadCommand, final Map params, final boolean leaderIsPreferable, final boolean autoReconnect,
final Callback callback) {
Exception lastException = null;
final int maxRetry = leaderIsPreferable ? 3 : getReplicaServerList().size() + 1;
Pair connectToServer =
leaderIsPreferable && leaderServer != null ? leaderServer : new Pair<>(currentServer, currentPort);
String server = null;
for (int retry = 0; retry < maxRetry && connectToServer != null; ++retry) {
server = connectToServer.getFirst() + ":" + connectToServer.getSecond();
String url = protocol + "://" + server + "/api/v" + apiVersion + "/" + operation;
if (extendedURL != null)
url += "/" + extendedURL;
try {
final HttpURLConnection connection = createConnection(method, url);
connection.setDoOutput(true);
try {
if (payloadCommand != null) {
if ("GET".equalsIgnoreCase(method))
throw new IllegalArgumentException("Cannot execute a HTTP GET request with a payload");
final JSONObject jsonRequest = new JSONObject();
if (language != null)
jsonRequest.put("language", language);
jsonRequest.put("command", payloadCommand);
jsonRequest.put("serializer", "record");
if (params != null)
jsonRequest.put("params", new JSONObject(params));
setRequestPayload(connection, jsonRequest);
}
connection.connect();
if (connection.getResponseCode() != 200) {
lastException = manageException(connection, payloadCommand != null ? payloadCommand : operation);
if (lastException instanceof RuntimeException && lastException.getMessage().equals("Empty payload received"))
LogManager.instance()
.log(this, Level.FINE, "Empty payload received, retrying (retry=%d/%d)...", null, retry, maxRetry);
continue;
}
final JSONObject response = new JSONObject(FileUtils.readStreamAsString(connection.getInputStream(), charset));
if (callback == null)
return null;
return callback.call(connection, response);
} finally {
connection.disconnect();
}
} catch (final IOException | ServerIsNotTheLeaderException e) {
lastException = e;
if (!autoReconnect)
break;
if (!reloadClusterConfiguration())
throw new RemoteException("Error on executing remote operation " + operation + ", no server available", e);
final Pair currentConnectToServer = connectToServer;
if (leaderIsPreferable && !currentConnectToServer.equals(leaderServer)) {
connectToServer = leaderServer;
} else
connectToServer = getNextReplicaAddress();
if (connectToServer != null)
LogManager.instance()
.log(this, Level.WARNING, "Remote server (%s:%d) seems unreachable, switching to server %s:%d...", null,
currentConnectToServer.getFirst(), currentConnectToServer.getSecond(), connectToServer.getFirst(),
connectToServer.getSecond());
} catch (final RemoteException | NeedRetryException | DuplicatedKeyException | TransactionException | TimeoutException |
SecurityException e) {
throw e;
} catch (final Exception e) {
throw new RemoteException("Error on executing remote operation " + operation + " (cause: " + e.getMessage() + ")", e);
}
}
if (lastException instanceof RuntimeException)
throw (RuntimeException) lastException;
throw new RemoteException(
"Error on executing remote operation '" + operation + "' (server=" + server + " retry=" + maxRetry + ")", lastException);
}
public int getApiVersion() {
return apiVersion;
}
public void setApiVersion(final int apiVersion) {
this.apiVersion = apiVersion;
}
public String getLeaderAddress() {
return leaderServer.getFirst() + ":" + leaderServer.getSecond();
}
public List getReplicaAddresses() {
return replicaServerList.stream().map((e) -> e.getFirst() + ":" + e.getSecond()).collect(Collectors.toList());
}
HttpURLConnection createConnection(final String httpMethod, final String url) throws IOException {
final HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestProperty("charset", "utf-8");
connection.setRequestMethod(httpMethod);
final String authorization = userName + ":" + userPassword;
connection.setRequestProperty("Authorization",
"Basic " + Base64.getEncoder().encodeToString(authorization.getBytes(DatabaseFactory.getDefaultCharset())));
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
return connection;
}
void requestClusterConfiguration() {
final JSONObject response;
try {
final HttpURLConnection connection = createConnection("GET", getUrl("server?mode=cluster"));
connection.connect();
if (connection.getResponseCode() != 200) {
final Exception detail = manageException(connection, "cluster configuration");
if (detail instanceof SecurityException)
throw detail;
throw new RemoteException("Error on requesting cluster configuration: " + connection.getResponseMessage(), detail);
}
response = new JSONObject(FileUtils.readStreamAsString(connection.getInputStream(), charset));
LogManager.instance().log(this, Level.FINE, "Configuring remote database: %s", null, response);
} catch (final SecurityException e) {
throw e;
} catch (final Exception e) {
throw new DatabaseOperationException("Error on connecting to the server", e);
}
try {
if (!response.has("ha")) {
leaderServer = new Pair<>(originalServer, originalPort);
replicaServerList.clear();
return;
}
final JSONObject ha = response.getJSONObject("ha");
final String cfgLeaderServer = (String) ha.get("leaderAddress");
final String[] leaderServerParts = HostUtil.parseHostAddress(cfgLeaderServer, HostUtil.HA_DEFAULT_PORT);
leaderServer = new Pair<>(leaderServerParts[0], Integer.parseInt(leaderServerParts[1]));
final String cfgReplicaServers = (String) ha.get("replicaAddresses");
// PARSE SERVER LISTS
replicaServerList.clear();
if (cfgReplicaServers != null && !cfgReplicaServers.isEmpty()) {
final String[] serverEntries = cfgReplicaServers.split(",");
for (final String serverEntry : serverEntries) {
try {
final String[] serverParts = HostUtil.parseHostAddress(serverEntry, HostUtil.CLIENT_DEFAULT_PORT);
final String sHost = serverParts[0];
final int sPort = Integer.parseInt(serverParts[1]);
replicaServerList.add(new Pair(sHost, sPort));
} catch (Exception e) {
LogManager.instance().log(this, Level.SEVERE, "Invalid replica server address '%s'", null, serverEntry);
}
}
}
LogManager.instance().log(this, Level.FINE, "Remote Database configured with leader=%s and replicas=%s", null, leaderServer,
replicaServerList);
} catch (final SecurityException e) {
throw e;
} catch (final Exception e) {
throw new DatabaseOperationException("Error on requesting cluster configuration", e);
}
}
private Pair getNextReplicaAddress() {
if (replicaServerList.isEmpty())
return leaderServer;
++currentReplicaServerIndex;
if (currentReplicaServerIndex > replicaServerList.size() - 1)
currentReplicaServerIndex = 0;
return replicaServerList.get(currentReplicaServerIndex);
}
boolean reloadClusterConfiguration() {
final Pair oldLeader = leaderServer;
// ASK REPLICA FIRST
for (int replicaIdx = 0; replicaIdx < replicaServerList.size(); ++replicaIdx) {
final Pair connectToServer = replicaServerList.get(replicaIdx);
currentServer = connectToServer.getFirst();
currentPort = connectToServer.getSecond();
try {
requestClusterConfiguration();
} catch (final Exception e) {
// IGNORE: TRY NEXT
continue;
}
if (leaderServer != null)
return true;
}
if (oldLeader != null) {
// RESET LEADER SERVER TO AVOID LOOP
leaderServer = null;
// ASK TO THE OLD LEADER
currentServer = oldLeader.getFirst();
currentPort = oldLeader.getSecond();
try {
requestClusterConfiguration();
} catch (final Exception e) {
// IGNORE
}
}
return leaderServer != null;
}
protected String getUrl(final String command) {
return protocol + "://" + currentServer + ":" + currentPort + "/api/v" + apiVersion + "/" + command;
}
void setRequestPayload(final HttpURLConnection connection, final JSONObject jsonRequest) throws IOException {
connection.setDoOutput(true);
final byte[] postData = jsonRequest.toString().getBytes(StandardCharsets.UTF_8);
connection.setRequestProperty("Content-Length", Integer.toString(postData.length));
try (final DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.write(postData);
}
}
protected Exception manageException(final HttpURLConnection connection, final String operation) throws IOException {
String detail = null;
String reason = null;
String exception = null;
String exceptionArgs = null;
String responsePayload = null;
if (connection.getErrorStream() != null) {
try {
responsePayload = FileUtils.readStreamAsString(connection.getErrorStream(), charset);
final JSONObject response = new JSONObject(responsePayload);
reason = response.getString("error");
detail = response.has("detail") ? response.getString("detail") : null;
exception = response.has("exception") ? response.getString("exception") : null;
exceptionArgs = response.has("exceptionArgs") ? response.getString("exceptionArgs") : null;
} catch (final Exception e) {
// TODO CHECK IF THE COMMAND NEEDS TO BE RE-EXECUTED OR NOT
LogManager.instance()
.log(this, Level.WARNING, "Error on executing command, retrying... (payload=%s, error=%s)", null, responsePayload,
e.toString());
return e;
}
}
if (exception != null) {
if (detail == null)
detail = "Unknown";
if (exception.equals(ServerIsNotTheLeaderException.class.getName())) {
final int sep = detail.lastIndexOf('.');
return new ServerIsNotTheLeaderException(sep > -1 ? detail.substring(0, sep) : detail, exceptionArgs);
} else if (exception.equals(RecordNotFoundException.class.getName())) {
final int begin = detail.indexOf("#");
final int end = detail.indexOf(" ", begin);
return new RecordNotFoundException(detail, new RID(detail.substring(begin, end)));
} else if (exception.equals(QuorumNotReachedException.class.getName())) {
return new QuorumNotReachedException(detail);
} else if (exception.equals(DuplicatedKeyException.class.getName()) && exceptionArgs != null) {
final String[] exceptionArgsParts = exceptionArgs.split("\\|");
return new DuplicatedKeyException(exceptionArgsParts[0], exceptionArgsParts[1], new RID(exceptionArgsParts[2]));
} else if (exception.equals(ConcurrentModificationException.class.getName())) {
return new ConcurrentModificationException(detail);
} else if (exception.equals(TransactionException.class.getName())) {
return new TransactionException(detail);
} else if (exception.equals(TimeoutException.class.getName())) {
return new TimeoutException(detail);
} else if (exception.equals(SchemaException.class.getName())) {
return new SchemaException(detail);
} else if (exception.equals(NoSuchElementException.class.getName())) {
return new NoSuchElementException(detail);
} else if (exception.equals(SecurityException.class.getName())) {
return new SecurityException(detail);
} else if (exception.equals("com.arcadedb.server.security.ServerSecurityException")) {
return new SecurityException(detail);
} else if (exception.equals(ConnectException.class.getName())) {
return new NeedRetryException(detail);
} else if (exception.equals("com.arcadedb.server.ha.ReplicationException")) {
return new NeedRetryException(detail);
} else
// ELSE
return new RemoteException(
"Error on executing remote operation " + operation + " (cause:" + exception + " detail:" + detail + ")");
}
final String httpErrorDescription = connection.getResponseMessage();
// TEMPORARY FIX FOR AN ISSUE WITH THE CLIENT/SERVER COMMUNICATION WHERE THE PAYLOAD ARRIVES AS EMPTY
if (connection.getResponseCode() == 400 && "Bad Request".equals(httpErrorDescription) && "Command text is null".equals(
reason)) {
// RETRY
return new RemoteException("Empty payload received");
}
return new RemoteException(
"Error on executing remote command '" + operation + "' (httpErrorCode=" + connection.getResponseCode()
+ " httpErrorDescription=" + httpErrorDescription + " reason=" + reason + " detail=" + detail + " exception="
+ exception + ")");
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy