/*
*
* * Copyright 2014 Orient Technologies LTD (info(at)orientechnologies.com)
* *
* * 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.
* *
* * For more information: http://www.orientechnologies.com
*
*/
package com.orientechnologies.orient.server.distributed;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.exception.OConfigurationException;
import com.orientechnologies.orient.core.record.impl.ODocument;
import java.util.*;
/**
* Immutable Distributed configuration. It uses an ODocument object to store the configuration. Every changes must be done by
* obtaining a modifiable verson of the object through the method `modify()`.
*
* @author Luca Garulli (l.garulli--at--orientechnologies.com)
*/
public class ODistributedConfiguration {
public static final String NEW_NODE_TAG = "<NEW_NODE>";
public static final String ALL_WILDCARD = "*";
protected static final String SERVERS = "servers";
protected static final String DCS = "dataCenters";
protected static final String OWNER = "owner";
protected static final String CLUSTERS = "clusters";
protected static final String VERSION = "version";
protected static final String READ_QUORUM = "readQuorum";
protected static final String WRITE_QUORUM = "writeQuorum";
public static final String QUORUM_MAJORITY = "majority";
public static final String QUORUM_ALL = "all";
public static final String QUORUM_LOCAL_DC = "localDataCenter";
public static final Integer DEFAULT_READ_QUORUM = 1;
public static final String DEFAULT_WRITE_QUORUM = QUORUM_MAJORITY;
protected static final String NEW_NODE_STRATEGY = "newNodeStrategy";
protected static final String READ_YOUR_WRITES = "readYourWrites";
protected static final String EXECUTION_MODE = "executionMode";
protected static final String EXECUTION_MODE_SYNCHRONOUS = "synchronous";
protected final ODocument configuration;
protected static final List<String> DEFAULT_CLUSTER_NAME = Collections.singletonList(ALL_WILDCARD);
public enum ROLES {
MASTER, REPLICA
}
public enum NEW_NODE_STRATEGIES {
DYNAMIC, STATIC
}
public ODistributedConfiguration(final ODocument iConfiguration) {
configuration = iConfiguration;
configuration.setTrackingChanges(false);
}
public OModifiableDistributedConfiguration modify() {
return new OModifiableDistributedConfiguration(configuration.copy());
}
/**
* Returns true if the replication is active, otherwise false.
*
* @param iClusterName Cluster name, or null for *
*/
public boolean isReplicationActive(final String iClusterName, final String iLocalNode) {
final Collection<String> servers = getClusterConfiguration(iClusterName).field(SERVERS);
if (servers != null && !servers.isEmpty()) {
return true;
}
return false;
}
/**
* Returns true if the configuration per data centers is specified.
*/
public boolean hasDataCenterConfiguration() {
return configuration.field(DCS) != null;
}
/**
* Returns the new node strategy between "dynamic" and "static". If static, the node is registered under the "server" tag.
*
* @return NEW_NODE_STRATEGIES enum
*/
public NEW_NODE_STRATEGIES getNewNodeStrategy() {
final String value = configuration.field(NEW_NODE_STRATEGY);
if (value != null)
return NEW_NODE_STRATEGIES.valueOf(value.toUpperCase());
return NEW_NODE_STRATEGIES.STATIC;
}
/**
* Returns the execution mode if synchronous.
*
* @param iClusterName Cluster name, or null for *
*
* @return true = synchronous, false = asynchronous, null = undefined
*/
public Boolean isExecutionModeSynchronous(final String iClusterName) {
Object value = getClusterConfiguration(iClusterName).field(EXECUTION_MODE);
if (value == null) {
value = configuration.field(EXECUTION_MODE);
if (value == null)
return null;
}
if (value.toString().equalsIgnoreCase("undefined"))
return null;
return value.toString().equalsIgnoreCase(EXECUTION_MODE_SYNCHRONOUS);
}
/**
* Reads your writes.
*
* @param iClusterName Cluster name, or null for *
*/
public Boolean isReadYourWrites(final String iClusterName) {
Object value = getClusterConfiguration(iClusterName).field(READ_YOUR_WRITES);
if (value == null) {
value = configuration.field(READ_YOUR_WRITES);
if (value == null) {
OLogManager.instance()
.warn(this, "%s setting not found for cluster=%s in distributed-config.json", READ_YOUR_WRITES, iClusterName);
return true;
}
}
return (Boolean) value;
}
/**
* Returns the list of servers that can manage a list of clusters. The algorithm makes its best to involve the less servers as it
* can.
*
* @param iClusterNames Set of cluster names to find
* @param iLocalNode Local node name
*/
public Map<String, Collection<String>> getServerClusterMap(Collection<String> iClusterNames, final String iLocalNode,
final boolean optimizeForLocalOnly) {
if (iClusterNames == null || iClusterNames.isEmpty())
iClusterNames = DEFAULT_CLUSTER_NAME;
final Map<String, Collection<String>> servers = new HashMap<String, Collection<String>>(iClusterNames.size());
// TRY TO SEE IF IT CAN BE EXECUTED ON LOCAL NODE ONLY
boolean canUseLocalNode = true;
for (String p : iClusterNames) {
final List<String> serverList = getClusterConfiguration(p).field(SERVERS);
if (serverList != null && !serverList.contains(iLocalNode)) {
canUseLocalNode = false;
break;
}
}
if (optimizeForLocalOnly && canUseLocalNode) {
// USE LOCAL NODE ONLY (MUCH FASTER)
servers.put(iLocalNode, iClusterNames);
return servers;
}
// GROUP BY SERVER WITH THE NUMBER OF CLUSTERS
final Map<String, Collection<String>> serverMap = new HashMap<String, Collection<String>>();
for (String p : iClusterNames) {
final List<String> serverList = getClusterConfiguration(p).field(SERVERS);
for (String s : serverList) {
if (NEW_NODE_TAG.equalsIgnoreCase(s))
continue;
Collection<String> clustersInServer = serverMap.get(s);
if (clustersInServer == null) {
clustersInServer = new HashSet<String>();
serverMap.put(s, clustersInServer);
}
clustersInServer.add(p);
}
}
if (serverMap.size() == 1)
// RETURN THE ONLY SERVER INVOLVED
return serverMap;
if (!optimizeForLocalOnly)
return serverMap;
// ORDER BY NUMBER OF CLUSTERS
final List<String> orderedServers = new ArrayList<String>(serverMap.keySet());
Collections.sort(orderedServers, new Comparator<String>() {
@Override
public int compare(final String o1, final String o2) {
return ((Integer) serverMap.get(o2).size()).compareTo((Integer) serverMap.get(o1).size());
}
});
// BROWSER ORDERED SERVER MAP PUTTING THE MINIMUM SERVER TO COVER ALL THE CLUSTERS
final Set<String> remainingClusters = new HashSet<String>(iClusterNames); // KEEPS THE REMAINING CLUSTER TO ADD IN FINAL
// RESULT
final Set<String> includedClusters = new HashSet<String>(iClusterNames.size()); // KEEPS THE COLLECTION OF ALREADY INCLUDED
// CLUSTERS
for (String s : orderedServers) {
final Collection<String> clusters = serverMap.get(s);
if (!servers.isEmpty()) {
// FILTER CLUSTER LIST AVOIDING TO REPEAT CLUSTERS ALREADY INCLUDED ON PREVIOUS NODES
clusters.removeAll(includedClusters);
}
servers.put(s, clusters);
remainingClusters.removeAll(clusters);
includedClusters.addAll(clusters);
if (remainingClusters.isEmpty())
// FOUND ALL CLUSTERS
break;
}
return servers;
}
/**
* Returns the clusters where a server is owner. This is used when a cluster must be selected: locality is always the best choice.
*
* @param iClusterNames Set of cluster names
* @param iNode Node
*/
public List<String> getOwnedClustersByServer(Collection<String> iClusterNames, final String iNode) {
if (iClusterNames == null || iClusterNames.isEmpty())
iClusterNames = DEFAULT_CLUSTER_NAME;
final List<String> notDefinedClusters = new ArrayList<String>(5);
final List<String> candidates = new ArrayList<String>(5);
for (String p : iClusterNames) {
if (p == null)
continue;
final String ownerServer = getClusterOwner(p);
if (ownerServer == null)
notDefinedClusters.add(p);
else if (iNode.equals(ownerServer)) {
// COLLECT AS CANDIDATE
candidates.add(p);
}
}
if (!candidates.isEmpty())
// RETURN THE FIRST ONE
return candidates;
final String owner = getClusterOwner(ALL_WILDCARD);
if (iNode.equals(owner))
// CURRENT SERVER IS MASTER OF DEFAULT: RETURN ALL THE NON CONFIGURED CLUSTERS
return notDefinedClusters;
// NO MASTER FOUND, RETURN EMPTY LIST
return candidates;
}
/**
* Returns the set of server names involved on the passed cluster collection.
*
* @param iClusterNames Collection of cluster names to find
*/
public Set<String> getServers(Collection<String> iClusterNames) {
if (iClusterNames == null || iClusterNames.isEmpty())
return getAllConfiguredServers();
final Set<String> partitions = new HashSet<String>(iClusterNames.size());
for (String p : iClusterNames) {
final List<String> serverList = getClusterConfiguration(p).field(SERVERS);
if (serverList != null) {
for (String s : serverList)
if (!s.equals(NEW_NODE_TAG))
partitions.add(s);
}
}
return partitions;
}
/**
* Returns true if the local server has all the requested clusters.
*
* @param server Server name
* @param clusters Collection of cluster names to find
*/
public boolean isServerContainingAllClusters(final String server, Collection<String> clusters) {
if (clusters == null || clusters.isEmpty())
clusters = DEFAULT_CLUSTER_NAME;
for (String cluster : clusters) {
final List<String> serverList = getClusterConfiguration(cluster).field(SERVERS);
if (serverList != null) {
if (!serverList.contains(server))
return false;
}
}
return true;
}
/**
* Returns true if the local server has the requested cluster.
*
* @param server Server name
* @param cluster cluster names to find
*/
public boolean isServerContainingCluster(final String server, String cluster) {
if (cluster == null)
cluster = ALL_WILDCARD;
final List<String> serverList = getClusterConfiguration(cluster).field(SERVERS);
if (serverList != null) {
return serverList.contains(server);
}
return true;
}
/**
* Returns the server list for the requested cluster cluster excluding any tags like <NEW_NODES> and iExclude if any.
*
* @param iClusterName Cluster name, or null for *
* @param iExclude Node to exclude
*/
public List<String> getServers(final String iClusterName, final String iExclude) {
final List<String> serverList = getClusterConfiguration(iClusterName).field(SERVERS);
if (serverList != null) {
// COPY AND REMOVE ANY NEW_NODE_TAG
List<String> filteredServerList = new ArrayList<String>(serverList.size());
for (String s : serverList) {
if (!s.equals(NEW_NODE_TAG) && (iExclude == null || !iExclude.equals(s)))
filteredServerList.add(s);
}
return filteredServerList;
}
return Collections.EMPTY_LIST;
}
/**
* Returns an ordered list of master server. The first in the list is the first found in configuration. This is used to determine
* the cluster leader.
*/
public List<String> getMasterServers() {
final List<String> result = new ArrayList<String>();
final List<String> serverList = getClusterConfiguration(null).field(SERVERS);
if (serverList != null) {
// COPY AND REMOVE ANY NEW_NODE_TAG
List<String> masters = new ArrayList<String>(serverList.size());
for (String s : serverList) {
if (!s.equals(NEW_NODE_TAG))
masters.add(s);
}
final ROLES defRole = getDefaultServerRole();
final ODocument servers = configuration.field(SERVERS);
if (servers != null) {
for (Iterator<String> it = masters.iterator(); it.hasNext(); ) {
final String server = it.next();
final String roleAsString = servers.field(server);
final ROLES role = roleAsString != null ? ROLES.valueOf(roleAsString.toUpperCase()) : defRole;
if (role != ROLES.MASTER)
it.remove();
}
}
return masters;
}
return Collections.EMPTY_LIST;
}
/**
* Returns the complete list of servers found in configuration.
*/
public Set<String> getAllConfiguredServers() {
final Set<String> servers = new HashSet<String>();
for (String p : getClusterNames()) {
final List<String> serverList = getClusterConfiguration(p).field(SERVERS);
if (serverList != null) {
for (String s : serverList)
if (!s.equals(NEW_NODE_TAG))
servers.add(s);
}
}
return servers;
}
/**
* Returns the set of clusters managed by a server.
*
* @param iNodeName Server name
*/
public Set<String> getClustersOnServer(final String iNodeName) {
final Set<String> clusters = new HashSet<String>();
for (String cl : getClusterNames()) {
final List<String> servers = getServers(cl, null);
if (servers.contains(iNodeName))
clusters.add(cl);
}
return clusters;
}
/**
* Returns the set of clusters where server is the owner.
*
* @param iNodeName Server name
*/
public Set<String> getClustersOwnedByServer(final String iNodeName) {
final Set<String> clusters = new HashSet<String>();
for (String cl : getClusterNames()) {
if (iNodeName.equals(getClusterOwner(cl)))
clusters.add(cl);
}
return clusters;
}
/**
* Returns the owner server for the given cluster excluding the passed node. The Owner server is the first in server list.
*
* @param iClusterName Cluster name, or null for *
*/
public String getClusterOwner(final String iClusterName) {
String owner;
final ODocument clusters = getConfiguredClusters();
// GET THE CLUSTER CFG
final ODocument cfg = iClusterName != null ? (ODocument) clusters.field(iClusterName) : null;
if (cfg != null) {
owner = cfg.field(OWNER);
if (owner != null)
return owner;
final List<String> serverList = cfg.field(SERVERS);
if (serverList != null && !serverList.isEmpty()) {
// RETURN THE FIRST ONE
owner = serverList.get(0);
if (NEW_NODE_TAG.equals(owner) && serverList.size() > 1)
// DON'T RETURN <NEW_NODE>
owner = serverList.get(1);
}
} else
// RETURN THE OWNER OF *
return getClusterOwner(ALL_WILDCARD);
return owner;
}
/**
* Returns the static owner server for the given cluster.
*
* @param iClusterName Cluster name, or null for *
*/
public String getConfiguredClusterOwner(final String iClusterName) {
String owner = null;
final ODocument clusters = getConfiguredClusters();
// GET THE CLUSTER CFG
final ODocument cfg = clusters.field(iClusterName);
if (cfg != null)
owner = cfg.field(OWNER);
return owner;
}
/**
* Returns the configured server list for the requested cluster.
*
* @param iClusterName Cluster name, or null for *
*/
public List<String> getConfiguredServers(final String iClusterName) {
final Collection<? extends String> list = (Collection<? extends String>) getClusterConfiguration(iClusterName).field(SERVERS);
return list != null ? new ArrayList<String>(list) : null;
}
/**
* Returns the array of configured clusters
*/
public String[] getClusterNames() {
final ODocument clusters = configuration.field(CLUSTERS);
return clusters.fieldNames();
}
/**
* Returns the default server role between MASTER (default) and REPLICA.
*/
public ROLES getDefaultServerRole() {
final ODocument servers = configuration.field(SERVERS);
if (servers == null)
// DEFAULT: MASTER
return ROLES.MASTER;
final String role = servers.field(ALL_WILDCARD);
if (role == null)
// DEFAULT: MASTER
return ROLES.MASTER;
return ROLES.valueOf(role.toUpperCase());
}
/**
* Returns the server role between MASTER (default) and REPLICA.
*/
public ROLES getServerRole(final String iServerName) {
final ODocument servers = configuration.field(SERVERS);
if (servers == null)
// DEFAULT: MASTER
return ROLES.MASTER;
String role = servers.field(iServerName);
if (role == null) {
// DEFAULT: MASTER
role = servers.field(ALL_WILDCARD);
if (role == null)
// DEFAULT: MASTER
return ROLES.MASTER;
}
return ROLES.valueOf(role.toUpperCase());
}
/**
* Returns the registered servers.
*/
public Set<String> getRegisteredServers() {
final ODocument servers = configuration.field(SERVERS);
final Set<String> result = new HashSet<String>();
if (servers != null)
for (String s : servers.fieldNames())
result.add(s);
return result;
}
public ODocument getDocument() {
return configuration;
}
/**
* Returns all the configured data centers' names, if any.
*/
public Set<String> getDataCenters() {
final ODocument dcs = configuration.field(DCS);
if (dcs == null)
return Collections.EMPTY_SET;
final Set<String> result = new HashSet<String>();
for (String dc : dcs.fieldNames()) {
result.add(dc);
}
return result;
}
/**
* Returns the data center write quorum.
*
* @param dataCenter Data center name
*/
public int getDataCenterWriteQuorum(final String dataCenter) {
final ODocument dc = getDataCenterConfiguration(dataCenter);
Object wq = dc.field(WRITE_QUORUM);
if (wq instanceof String) {
if (wq.toString().equalsIgnoreCase(ODistributedConfiguration.QUORUM_MAJORITY)) {
final List<String> servers = dc.field(SERVERS);
wq = servers.size() / 2 + 1;
} else if (wq.toString().equalsIgnoreCase(ODistributedConfiguration.QUORUM_ALL)) {
final List<String> servers = dc.field(SERVERS);
wq = servers.size();
}
}
return (Integer) wq;
}
/**
* Returns true if the database is sharded across servers. False if it's completely replicated.
*/
public boolean isSharded() {
final ODocument allCluster = getClusterConfiguration(ALL_WILDCARD);
if (allCluster != null) {
final List<String> allServers = allCluster.field(SERVERS);
if (allServers != null && !allServers.isEmpty()) {
for (String cl : getClusterNames()) {
final List<String> servers = getServers(cl, null);
if (servers != null && !servers.isEmpty() && !allServers.containsAll(servers))
return false;
}
}
}
return false;
}
/**
* Returns the list of servers in a data center.
*
* @param dataCenter Data center name
*
* @throws OConfigurationException if the list of servers is not found in data center configuration
*/
public List<String> getDataCenterServers(final String dataCenter) {
final ODocument dc = getDataCenterConfiguration(dataCenter);
final List<String> servers = dc.field(SERVERS);
if (servers == null || servers.isEmpty())
throw new OConfigurationException(
"Data center '" + dataCenter + "' does not contain any server in distributed database configuration");
return new ArrayList<String>(servers);
}
/**
* Returns the data center where the server belongs.
*
* @param server Server name
*/
public String getDataCenterOfServer(final String server) {
final ODocument dcs = configuration.field(DCS);
if (dcs != null) {
for (String dc : dcs.fieldNames()) {
final ODocument dcConfig = dcs.field(dc);
if (dcConfig != null) {
final List<String> dcServers = dcConfig.field("servers");
if (dcServers != null && !dcServers.isEmpty()) {
if (dcServers.contains(server))
// FOUND
return dc;
}
}
}
}
// NOT FOUND
return null;
}
public int getVersion() {
final Integer v = configuration.field(VERSION);
if (v == null)
return 1;
return v;
}
/**
* Returns true if the global write quorum is "localDataCenter".
*/
public boolean isLocalDataCenterWriteQuorum() {
return QUORUM_LOCAL_DC.equals(configuration.field(WRITE_QUORUM));
}
/**
* Returns the global read quorum.
*
* @param iClusterName Cluster name, or null for *
*/
public Object getGlobalReadQuorum(final String iClusterName) {
Object value = getClusterConfiguration(iClusterName).field(READ_QUORUM);
if (value == null)
value = configuration.field(READ_QUORUM);
return value;
}
/**
* Returns the read quorum.
*
* @param clusterName Cluster name, or null for *
* @param availableNodes Total node available
*/
public int getReadQuorum(final String clusterName, final int availableNodes, final String server) {
return getQuorum("readQuorum", clusterName, availableNodes, DEFAULT_READ_QUORUM, server);
}
/**
* Returns the write quorum.
*
* @param clusterName Cluster name, or null for *
* @param availableNodes Total node available
*/
public int getWriteQuorum(final String clusterName, final int availableNodes, final String server) {
return getQuorum("writeQuorum", clusterName, availableNodes, DEFAULT_WRITE_QUORUM, server);
}
private ODocument getConfiguredClusters() {
final ODocument clusters = configuration.field(CLUSTERS);
if (clusters == null)
throw new OConfigurationException("Cannot find '" + CLUSTERS + "' in distributed database configuration");
return clusters;
}
@Override
public String toString() {
return configuration.toString();
}
/**
* Gets the document representing the cluster configuration.
*
* @param iClusterName Cluster name, or null for *
*
* @return Always a ODocument
*
* @throws OConfigurationException in case "clusters" field is not found in configuration
*/
protected ODocument getClusterConfiguration(String iClusterName) {
final ODocument clusters = getConfiguredClusters();
if (iClusterName == null)
iClusterName = ALL_WILDCARD;
final ODocument cfg;
if (!clusters.containsField(iClusterName))
// NO CLUSTER IN CFG: GET THE DEFAULT ONE
cfg = clusters.field(ALL_WILDCARD);
else
// GET THE CLUSTER CFG
cfg = clusters.field(iClusterName);
if (cfg == null)
return new ODocument();
return cfg;
}
/**
* Gets the document representing the dc configuration.
*
* @param dataCenter Data center name
*
* @return Always a ODocument
*
* @throws OConfigurationException if the data center configuration is not found
*/
private ODocument getDataCenterConfiguration(final String dataCenter) {
final ODocument dcs = configuration.field(DCS);
if (dcs != null)
return dcs.field(dataCenter);
throw new OConfigurationException("Cannot find the data center '" + dataCenter + "' in distributed database configuration");
}
/**
* Returns the read quorum.
*
* @param iClusterName Cluster name, or null for *
* @param iAvailableNodes Total nodes available
*/
private int getQuorum(final String quorumSetting, final String iClusterName, final int iAvailableNodes, final Object defaultValue,
final String server) {
Object value = getClusterConfiguration(iClusterName).field(quorumSetting);
if (value == null) {
value = configuration.field(quorumSetting);
if (value == null) {
OLogManager.instance()
.warn(this, "%s setting not found for cluster=%s in distributed-config.json", quorumSetting, iClusterName);
value = defaultValue;
}
}
if (value instanceof String) {
if (value.toString().equalsIgnoreCase(QUORUM_MAJORITY))
value = iAvailableNodes / 2 + 1;
else if (value.toString().equalsIgnoreCase(QUORUM_ALL))
value = iAvailableNodes;
else if (value.toString().equalsIgnoreCase(QUORUM_LOCAL_DC)) {
final String dc = getDataCenterOfServer(server);
if (dc == null)
throw new OConfigurationException("Data center not specified for server '" + server + "' in distributed configuration");
value = getDataCenterWriteQuorum(dc);
} else
throw new OConfigurationException(
"The value '" + value + "' is not supported for " + quorumSetting + " in distributed configuration");
}
return (Integer) value;
}
}