Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.marklogic.developer.corb.DefaultContentSourcePool Maven / Gradle / Ivy
/*
* * Copyright (c) 2004-2019 MarkLogic Corporation
* *
* * 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.
* *
* * The use of the Apache License does not indicate that this project is
* * affiliated with the Apache Software Foundation.
*/
package com.marklogic.developer.corb;
import static com.marklogic.developer.corb.Options.CONNECTION_POLICY;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
import com.marklogic.xcc.AdhocQuery;
import com.marklogic.xcc.Content;
import com.marklogic.xcc.ContentSource;
import com.marklogic.xcc.ModuleInvoke;
import com.marklogic.xcc.Request;
import com.marklogic.xcc.Session;
import com.marklogic.xcc.exceptions.RequestException;
import com.marklogic.xcc.exceptions.ServerConnectionException;
import com.marklogic.xcc.types.XdmVariable;
import java.util.List;
/**
* @since 2.4.0
*/
public class DefaultContentSourcePool extends AbstractContentSourcePool {
protected static final String CONNECTION_POLICY_ROUND_ROBIN = "ROUND-ROBIN";
protected static final String CONNECTION_POLICY_RANDOM = "RANDOM";
protected static final String CONNECTION_POLICY_LOAD = "LOAD";
protected String connectionPolicy = CONNECTION_POLICY_ROUND_ROBIN;
protected List contentSourceList = new ArrayList<>();
protected Map errorCountsMap = new HashMap<>();
protected Map connectionCountsMap = new HashMap<>();
protected Map errorTimeMap = new HashMap<>();
protected int retryInterval = 0;
protected int hostRetryLimit = 0;
protected int retryLimit = 0;
protected int roundRobinIndex = -1;
protected boolean isLoadPolicy = false;
protected boolean isRandomPolicy = false;
private static final Logger LOG = Logger.getLogger(DefaultContentSourcePool.class.getName());
public DefaultContentSourcePool(){
super();
}
@Override
public void init(Properties properties, SSLConfig sslConfig, String... connectionStrings) {
super.init(properties, sslConfig);
if (connectionStrings == null || connectionStrings.length == 0) {
throw new NullPointerException("XCC connection strings cannot be null or empty");
}
retryInterval = getConnectRetryInterval();
retryLimit = getConnectRetryLimit();
hostRetryLimit = getConnectHostRetryLimit();
String policy = getProperty(CONNECTION_POLICY);
if (CONNECTION_POLICY_RANDOM.equals(policy) || CONNECTION_POLICY_LOAD.equals(policy)) {
this.connectionPolicy = policy;
}
LOG.log(INFO, "Using the connection policy {0}", connectionPolicy);
for (String connectionString : connectionStrings) {
initContentSource(connectionString);
}
//for better performance avoid to many string equals later for every get and submit
isRandomPolicy = CONNECTION_POLICY_RANDOM.equals(connectionPolicy);
isLoadPolicy = CONNECTION_POLICY_LOAD.equals(connectionPolicy);
}
protected void initContentSource(String connectionString){
ContentSource contentSource = super.createContentSource(connectionString);
if (contentSource != null) {
contentSourceList.add(contentSource);
LOG.log(INFO, "Initialized ContentSource {0}", new Object[]{asString(contentSource)});
}
}
/**
* Note: Do not make this synchronized, it will affect performance significantly due to sleep.
*/
@Override
public ContentSource get() throws CorbException{
ContentSource contentSource = nextContentSource();
if (contentSource == null) {
throw new CorbException("ContentSource not available.");
}
//if the nextContentSource() returns the connection with existing errors, then it means it could not find
//any clean connections, so we need to wait.
//even if errored, but wait expired, then no need to wait.
Integer failedCount = errorCountsMap.get(contentSource);
if (failedCount != null && failedCount > 0 && errorTimeMap.containsKey(contentSource)) {
LOG.log(WARNING, "Connection failed for ContentSource {0}. Waiting for {1} seconds before retry attempt {2}",
new Object[]{asString(contentSource), retryInterval, failedCount + 1});
try {
Thread.sleep(retryInterval * 1000L);
} catch (InterruptedException ex) {
LOG.log(WARNING, "Interrupted!", ex);
Thread.currentThread().interrupt();
}
}
return createContentSourceProxy(contentSource);
}
protected synchronized ContentSource nextContentSource() {
List availableList = getAvailableContentSources();
if (availableList.isEmpty()){
return null;
}
ContentSource contentSource = null;
if (availableList.size() == 1) {
contentSource = availableList.get(0);
} else if (isRandomPolicy) {
contentSource = availableList.get((int)(Math.random() * availableList.size()));
} else if (isLoadPolicy) {
for (ContentSource next: availableList){
Integer count = connectionCountsMap.get(next);
if (count == null || count == 0) {
contentSource = next;
break;
} else if (contentSource == null || count < connectionCountsMap.get(contentSource)) {
contentSource = next;
}
}
} else {
roundRobinIndex++;
if (roundRobinIndex >= availableList.size()) {
roundRobinIndex = 0;
}
contentSource = availableList.get(roundRobinIndex);
}
return contentSource;
}
protected synchronized List getAvailableContentSources() {
//check if any errored connections are eligible for retries with out further wait
if (!errorTimeMap.isEmpty()) {
long current = System.currentTimeMillis();
errorTimeMap.entrySet().removeIf(next -> (current - next.getValue()) >= (retryInterval * 1000L));
}
if (!errorTimeMap.isEmpty()) {
List availableList = new ArrayList<>(contentSourceList.size());
for (ContentSource cs: contentSourceList) {
if (!errorTimeMap.containsKey(cs)) {
availableList.add(cs);
}
}
//if nothing available, then return the whole list as we have to wait anyway
return !availableList.isEmpty() ? availableList : contentSourceList;
} else {
return contentSourceList;
}
}
@Override
public void remove(ContentSource contentSource){
removeInternal(getContentSourceFromProxy(contentSource));
}
@Override
public boolean available() {
return !contentSourceList.isEmpty();
}
@Override
public ContentSource[] getAllContentSources() {
return contentSourceList.toArray(new ContentSource[contentSourceList.size()]);
}
@Override
public void close() {
connectionCountsMap.clear();
errorCountsMap.clear();
errorTimeMap.clear();
contentSourceList.clear();
}
protected boolean isRandomPolicy() {
return this.isRandomPolicy;
}
protected boolean isLoadPolicy() {
return this.isLoadPolicy;
}
protected synchronized void hold(ContentSource cs) {
if (contentSourceList.contains(cs)) {
Integer count = connectionCountsMap.get(cs);
count = count == null ? 1 : count + 1;
connectionCountsMap.put(cs, count);
}
}
protected synchronized void release(ContentSource cs) {
Integer count = connectionCountsMap.get(cs);
if (count != null && count > 0) {
connectionCountsMap.put(cs, count-1);
}
}
protected synchronized void success(ContentSource cs) {
errorCountsMap.remove(cs);
errorTimeMap.remove(cs);
}
protected void error(ContentSource cs) {
error(cs,-1);
}
protected synchronized void error(ContentSource cs, long allocTime) {
if (contentSourceList.contains(cs)) {
Long lastErrorTime = errorTimeMap.get(cs);
if (lastErrorTime == null || allocTime <= 0 || allocTime > lastErrorTime) {
Integer count = errorCountsMap.get(cs);
count = count == null ? 1 : count + 1;
errorCountsMap.put(cs, count);
errorTimeMap.put(cs, System.currentTimeMillis());
LOG.log(WARNING, "Connection error count for ContentSource {0} is {1}. Max limit is {2}.", new Object[]{asString(cs),count,hostRetryLimit});
if (count > hostRetryLimit) {
removeInternal(cs);
}
} else {
LOG.log(WARNING, "Connection error for ContentSource {0} is not counted towards the limit as it was allocated before last error.", new Object[]{asString(cs)});
}
}
}
protected int errorCount(ContentSource cs) {
Integer count = errorCountsMap.get(cs);
return count != null ? count : 0;
}
//this is not a proxy
protected synchronized void removeInternal(ContentSource cs) {
if (contentSourceList.contains(cs)) {
LOG.log(WARNING, "Removing the ContentSource {0} from the content source pool.", new Object[]{asString(cs)});
contentSourceList.remove(cs);
connectionCountsMap.remove(cs);
errorCountsMap.remove(cs);
errorTimeMap.remove(cs);
}
}
//TODO: handle redaction if necessary?
protected String asString(ContentSource cs) {
return cs == null ? "null" : cs.toString();
}
//methods to create dynamic proxy instances.
protected ContentSource createContentSourceProxy(ContentSource cs) {
return (ContentSource) Proxy.newProxyInstance(
DefaultContentSourcePool.class.getClassLoader(), new Class[] { ContentSource.class },
new ContentSourceInvocationHandler(this, cs));
}
public static ContentSource getContentSourceFromProxy(ContentSource proxy) {
ContentSource target = proxy;
if (proxy != null && Proxy.isProxyClass(proxy.getClass())) {
InvocationHandler handler = Proxy.getInvocationHandler(proxy);
if (handler instanceof ContentSourceInvocationHandler) {
target = ((ContentSourceInvocationHandler)handler).target;
}
}
return target;
}
public static Session getSessionFromProxy(Session proxy) {
Session target = proxy;
if (proxy != null && Proxy.isProxyClass(proxy.getClass())) {
InvocationHandler handler = Proxy.getInvocationHandler(proxy);
if (handler instanceof SessionInvocationHandler) {
target = ((SessionInvocationHandler)handler).target;
}
}
return target;
}
//invocation handlers
protected static class ContentSourceInvocationHandler implements InvocationHandler{
static final String NEW_SESSION = "newSession";
DefaultContentSourcePool csp;
ContentSource target;
long allocTime;
protected ContentSourceInvocationHandler(DefaultContentSourcePool csp, ContentSource target) {
this.csp = csp;
this.target = target;
this.allocTime = System.currentTimeMillis();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object obj = method.invoke(target, args);
if (obj != null && isNewSession(method) && obj instanceof Session) {
obj = createSessionProxy((Session)obj);
}
return obj;
}
protected Session createSessionProxy(Session session) {
return (Session)Proxy.newProxyInstance(
DefaultContentSourcePool.class.getClassLoader(), new Class[] { Session.class },
new SessionInvocationHandler(csp, target, session, allocTime));
}
private boolean isNewSession(Method method) {
return NEW_SESSION.equals(method.getName());
}
}
//TODO: This code does not handle explicit commits and rollbacks.
protected static class SessionInvocationHandler implements InvocationHandler {
static final String SUBMIT_REQUEST = "submitRequest";
static final String INSERT_CONTENT = "insertContent";
static final String COMMIT = "commit";
static final String ROLLBACK = "rollback";
static final String CLOSE = "close";
static final String EMPTY_SEQ = "()";
private DefaultContentSourcePool csp;
private ContentSource cs;
private Session target;
private long allocTime;
private int attempts = 0;
private Session retryProxy;
protected SessionInvocationHandler(DefaultContentSourcePool csp, ContentSource cs, Session target, long allocTime) {
this.csp = csp;
this.cs = cs;
this.target = target;
this.allocTime = allocTime;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
checkUnsupported(method);
//NOTE: We only need to track connection counts for LOAD policy
if (isSubmitRequest(method) || isInsertContent(method)) {
if (isSubmitRequest(method)) {
validRequest(args);
}
if (csp.isLoadPolicy()) {
csp.hold(cs);
}
attempts++;
}
try {
if (retryProxy != null && isClose(method)) {
retryProxy.close(); //Use proxy only as there can be multiple retry attempts in a chain.
}
Object obj = method.invoke(target, args);
//TODO: connection is held longer for streaming result sequence even after request is submitted.
//We are ok now as we only use streaming results for query uris loader.
if (isSubmitRequest(method) || isInsertContent(method)) {
csp.success(cs);
if (csp.isLoadPolicy()) {
csp.release(cs);
}
}
return obj;
} catch (Exception exc) {
if (csp.isLoadPolicy() && (isSubmitRequest(method) || isInsertContent(method))) {
csp.release(cs); //we should do this before the recursion.. not finally.
}
if (exc instanceof InvocationTargetException) {
if (exc.getCause() instanceof ServerConnectionException) {
csp.error(cs,allocTime); //we should do this before the recursion.. not finally.
String name = exc.getCause().getClass().getSimpleName();
if (isSubmitRequest(method) && attempts <= csp.retryLimit) {
LOG.log(WARNING, "Submit request failed {0} times with {1}. Max Limit is {2}. Retrying..", new Object[]{attempts, name, csp.retryLimit});
return submitAsNewRequest(args);
} else if (isInsertContent(method) && attempts <= csp.retryLimit) {
LOG.log(WARNING, "Insert content failed {0} times {1}. Max Limit is {2}. Retrying..", new Object[]{attempts, name, csp.retryLimit});
return insertAsNewRequest(args);
} else {
throw exc.getCause();
}
} else {
throw exc.getCause();
}
} else {
throw exc;
}
}
}
protected void validRequest(Object... args) {
Request request = (Request)args[0];
if (!(request instanceof AdhocQuery || request instanceof ModuleInvoke)) {
throw new IllegalArgumentException("Only moduleInvoke or adhocQuery requests are supported by corb");
}
}
protected Object submitAsNewRequest(Object... args) throws RequestException{
Request request = (Request)args[0];
try {
retryProxy = csp.get().newSession();
setAttemptsToNewSession(retryProxy);
Request newRequest;
if (request instanceof AdhocQuery) {
newRequest = retryProxy.newAdhocQuery(((AdhocQuery)request).getQuery());
} else {
newRequest = retryProxy.newModuleInvoke(((ModuleInvoke)request).getModuleUri());
}
newRequest.setOptions(request.getOptions());
XdmVariable[] vars = request.getVariables();
for (int i = 0; vars != null && i < vars.length; i++) {
newRequest.setVariable(vars[i]);
}
return retryProxy.submitRequest(newRequest);
} catch (CorbException exc) {
throw new RequestException(exc.getMessage(),request,exc);
}
}
protected Object insertAsNewRequest(Object... args) throws RequestException{
try {
retryProxy = csp.get().newSession();
setAttemptsToNewSession(retryProxy);
if (args[0] instanceof Content) {
retryProxy.insertContent((Content)args[0]);
} else if (args[0] instanceof Content[]) {
retryProxy.insertContent((Content[])args[0]);
}
return null;
} catch (CorbException exc) {
throw new RequestException(exc.getMessage(),target.newAdhocQuery(EMPTY_SEQ),exc);
}
}
private boolean isSubmitRequest(Method method) {
return SUBMIT_REQUEST.equals(method.getName());
}
private boolean isInsertContent(Method method) {
return INSERT_CONTENT.equals(method.getName());
}
private boolean isClose(Method method) {
return CLOSE.equals(method.getName());
}
private void checkUnsupported(Method method) {
if (COMMIT.equals(method.getName()) || ROLLBACK.equals(method.getName())) {
throw new UnsupportedOperationException(method.getName()+" is not supported by "+getClass().getName());
}
}
protected void setAttemptsToNewSession(Session newProxy) {
if (Proxy.isProxyClass(newProxy.getClass())) {
InvocationHandler handler = Proxy.getInvocationHandler(newProxy);
if (handler instanceof SessionInvocationHandler) {
((SessionInvocationHandler)handler).attempts = this.attempts;
}
}
}
}
}