/*
* Copyright 2013, The Sporting Exchange Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.betfair.cougar.core.impl.ev;
import com.betfair.cougar.api.ExecutionContext;
import com.betfair.cougar.api.ExecutionContextWithTokens;
import com.betfair.cougar.api.security.*;
import com.betfair.cougar.core.api.ev.*;
import com.betfair.cougar.core.api.exception.CougarException;
import com.betfair.cougar.core.api.exception.CougarFrameworkException;
import com.betfair.cougar.core.api.exception.CougarServiceException;
import com.betfair.cougar.core.api.exception.ServerFaultCode;
import com.betfair.cougar.core.impl.DefaultTimeConstraints;
import com.betfair.cougar.core.impl.security.IdentityChainImpl;
import com.betfair.cougar.logging.CougarLogger;
import com.betfair.cougar.logging.CougarLoggingUtils;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import java.util.*;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
/**
* Provides common ExecutionVenue functionality.
*/
public class BaseExecutionVenue implements ExecutionVenue {
private final static CougarLogger logger = CougarLoggingUtils.getLogger(ExecutionVenue.class);
private List<ExecutionPostProcessor> postProcessorList = new ArrayList<ExecutionPostProcessor>();
private List<ExecutionPreProcessor> preProcessorList = new ArrayList<ExecutionPreProcessor>();
private IdentityResolver identityResolver;
private DelayQueue<ExpiringObserver> expiringObservers = new DelayQueue<>();
protected Map<OperationKey, DefinedExecutable> registry = new HashMap<>();
@Override
public void registerOperation(String namespace, OperationDefinition def, Executable executable, ExecutionTimingRecorder recorder, long maxExecutionTime) {
if (isInterceptingSupported()) {
executable = new InterceptingExecutableWrapper(executable, preProcessorList, postProcessorList);
}
OperationKey key = def.getOperationKey();
if (namespace != null) {
key = new OperationKey(key, namespace);
}
if (registry.containsKey(key)) {
throw new IllegalArgumentException("The Operation key "+key+" is already defined in the execution venue");
}
registry.put(key,
new DefinedExecutable(
def,
executable,
recorder,
maxExecutionTime));
logger.log(Level.INFO, "Registered operation: %s", key);
}
private boolean isInterceptingSupported() {
return !preProcessorList.isEmpty() || !postProcessorList.isEmpty();
}
@Override
public OperationDefinition getOperationDefinition(OperationKey key) {
DefinedExecutable de = registry.get(key);
if (de != null) {
return de.def;
} else {
return null;
}
}
@Override
@ManagedAttribute
public Set<OperationKey> getOperationKeys() {
return registry.keySet();
}
private ExecutionContext resolveIdentitiesIfRequired(final ExecutionContext ctx) {
if (ctx instanceof ExecutionContextWithTokens) {
ExecutionContextWithTokens contextWithTokens = (ExecutionContextWithTokens) ctx;
if (identityResolver == null) {
contextWithTokens.setIdentityChain(new IdentityChainImpl(new ArrayList<Identity>()));
// if there's no identity resolver then it can't tokenise any tokens back to the transport..
contextWithTokens.getIdentityTokens().clear();
return contextWithTokens;
}
else {
IdentityChain chain = new IdentityChainImpl();
try {
identityResolver.resolve(chain, contextWithTokens);
}
catch (InvalidCredentialsException e) {
if (e.getCredentialFaultCode() != null) { // Check if a custom error code should be used
ServerFaultCode sfc = ServerFaultCode.getByCredentialFaultCode(e.getCredentialFaultCode());
throw new CougarFrameworkException(sfc, "Credentials supplied were invalid", e);
}
throw new CougarFrameworkException(ServerFaultCode.SecurityException, "Credentials supplied were invalid", e);
}
// ensure the identity chain set in the context is immutable
contextWithTokens.setIdentityChain(new IdentityChainImpl(chain.getIdentities()));
contextWithTokens.getIdentityTokens().clear();
List<IdentityToken> tokens = identityResolver.tokenise(contextWithTokens.getIdentity());
if (tokens != null) {
contextWithTokens.getIdentityTokens().addAll(tokens);
}
return contextWithTokens;
}
}
// might not be in the case of a client, or a batched transport which will have executed a seperate command to resolve identities for e.g.
return ctx;
}
@Override
public void execute(final ExecutionContext ctx, final OperationKey key, final Object[] args, ExecutionObserver observer, TimeConstraints timeConstraints) {
final DefinedExecutable de = registry.get(key);
if (de == null) {
logger.log(Level.FINE, "Not request logging request to URI: %s as no operation was found", key.toString());
observer.onResult(new ExecutionResult(new CougarFrameworkException(ServerFaultCode.NoSuchOperation, "Operation not found: "+key.toString())));
} else {
long serverExpiryTime = de.maxExecutionTime == 0 ? Long.MAX_VALUE : System.currentTimeMillis() + de.maxExecutionTime;
long clientExpiryTime = timeConstraints.getExpiryTime() == null ? Long.MAX_VALUE : timeConstraints.getExpiryTime();
long expiryTime = Math.min(clientExpiryTime, serverExpiryTime);
if (expiryTime == Long.MAX_VALUE) {
expiryTime = 0;
}
if (!(observer instanceof ExpiringObserver)) {
final ExpiringObserver expiringObserver = new ExpiringObserver(observer, expiryTime);
if (expiringObserver.expires()) {
registerExpiringObserver(expiringObserver);
}
observer = expiringObserver;
}
observer = new ExecutionObserverWrapper(observer, de.recorder, key);
try {
ExecutionContext contextToUse = resolveIdentitiesIfRequired(ctx);
de.exec.execute(contextToUse, key, args, observer, this, timeConstraints);
} catch (CougarException e) {
observer.onResult(new ExecutionResult(e));
} catch (Exception e) {
observer.onResult(new ExecutionResult(
new CougarFrameworkException(ServerFaultCode.ServiceRuntimeException,
"Exception thrown by service method",
e)));
}
}
if (observer instanceof ExpiringObserver) {
deregisterExpiringObserver((ExpiringObserver) observer);
}
}
@Override
public void execute(final ExecutionContext ctx, final OperationKey key, final Object[] args, final ExecutionObserver observer, final Executor executor, final TimeConstraints timeConstraints) {
if (timeConstraints == null) {
throw new IllegalArgumentException("Time constraints may not be null");
}
final DefinedExecutable de = registry.get(key);
final InterceptingExecutableWrapper interceptingExecutableWrapper = ExecutableWrapperUtils.findChild(InterceptingExecutableWrapper.class, de.exec);
// this has to be a new list since the InterceptionUtils.execute call below is allowed to (expected to even) mutate the second list passed in
final List<ExecutionPreProcessor> remainingProcessors = new ArrayList<>(preProcessorList);
final Runnable execution = new Runnable() {
@Override
public void run() {
long serverExpiryTime = de.maxExecutionTime == 0 ? Long.MAX_VALUE : System.currentTimeMillis() + de.maxExecutionTime;
long clientExpiryTimeCopy = timeConstraints.getExpiryTime() == null ? Long.MAX_VALUE : timeConstraints.getExpiryTime();
long expiryTime = Math.min(clientExpiryTimeCopy, serverExpiryTime);
if (expiryTime == Long.MAX_VALUE) {
expiryTime = 0;
}
final ExpiringObserver expiringObserver = new ExpiringObserver(observer, expiryTime);
if (expiringObserver.expires()) {
registerExpiringObserver(expiringObserver);
}
final TimeConstraints timeConstraints = expiryTime == 0 ? DefaultTimeConstraints.NO_CONSTRAINTS : DefaultTimeConstraints.fromExpiryTime(expiryTime);
executor.execute(new Runnable() {
@Override
public void run() {
// this gets run on the executor thread, so will be visible in the wrapper when executed
if (interceptingExecutableWrapper != null) {
interceptingExecutableWrapper.setUnexecutedPreProcessorsForThisThread(remainingProcessors);
}
execute(ctx, key, args, expiringObserver, timeConstraints);
}
});
}
};
InterceptionUtils.execute(preProcessorList, remainingProcessors, ExecutionRequirement.PRE_QUEUE, execution, ctx, key, args, observer);
}
protected void start() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
processExpiredObservers();
}
}, "EV-ExecutableExpiryDetection");
t.setDaemon(true);
t.start();
}
private void processExpiredObservers() {
// this executes on a daemon thread so we can happily loop forever
while (true) {
try {
ExpiringObserver expired = expiringObservers.take();
expired.expire();
} catch (InterruptedException e) {
// ignore, just carry on round
}
}
}
private void registerExpiringObserver(ExpiringObserver expiringObserver) {
expiringObservers.add(expiringObserver);
}
private void deregisterExpiringObserver(ExpiringObserver expiringObserver) {
expiringObservers.remove(expiringObserver);
}
private class ExpiringObserver implements ExecutionObserver, Delayed {
private AtomicBoolean onResultCalled = new AtomicBoolean(false);
private final ExecutionObserver observer;
private final long expiryTime;
private ExpiringObserver(final ExecutionObserver observer, final long expiryTime) {
this.observer = observer;
this.expiryTime = expiryTime;
}
public boolean expires() {
return expiryTime != 0;
}
@Override
public void onResult(ExecutionResult executionResult) {
if (onResultCalled.compareAndSet(false, true)) {
observer.onResult(executionResult);
}
}
public int compareTo(ExpiringObserver o) {
long diff = expiryTime - o.expiryTime;
if (diff == 0) {
return 0;
}
return diff < 0 ? -1 : 1;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expiryTime-System.currentTimeMillis(),TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return compareTo((ExpiringObserver)o);
}
public void expire() {
if (onResultCalled.compareAndSet(false, true)) {
observer.onResult(new ExecutionResult(new CougarFrameworkException(ServerFaultCode.Timeout, "Executable did not complete in time")));
}
}
}
// package private for testing
static class DefinedExecutable {
private final OperationDefinition def;
private final Executable exec;
private final ExecutionTimingRecorder recorder;
private final long maxExecutionTime;
public DefinedExecutable(final OperationDefinition def, final Executable exec, final ExecutionTimingRecorder recorder, final long maxExecutionTime) {
this.def = def;
this.exec = exec;
if (recorder != null) {
this.recorder = recorder;
} else {
throw new IllegalArgumentException("recorder must be defined");
}
this.maxExecutionTime = maxExecutionTime;
}
long getMaxExecutionTime() {
return maxExecutionTime;
}
}
/**
* Wrapper class to ensure that all timings are recorded with the ExecutionManager
*/
private class ExecutionObserverWrapper implements ExecutionObserver {
private final ExecutionObserver observer;
private final ExecutionTimingRecorder recorder;
private final OperationKey operationKey;
private final long startTime;
public ExecutionObserverWrapper(final ExecutionObserver observer, final ExecutionTimingRecorder recorder, OperationKey key) {
this.observer = observer;
this.recorder = recorder;
this.operationKey = key;
startTime = System.nanoTime();
}
@Override
public void onResult(ExecutionResult result) {
if (operationKey.getType() == OperationKey.Type.Request) {
switch (result.getResultType()) {
case Fault:
recorder.recordFailure((System.nanoTime() - startTime)/1000000.0);
break;
case Success:
recorder.recordCall((System.nanoTime() - startTime)/1000000.0);
break;
}
}
observer.onResult(result);
}
}
@Override
public void setPreProcessors(List<ExecutionPreProcessor> preProcessorList) {
this.preProcessorList = preProcessorList;
}
@Override
public void setPostProcessors(List<ExecutionPostProcessor> postProcessorList) {
this.postProcessorList = postProcessorList;
}
public void setIdentityResolver(IdentityResolver identityResolver) {
if (identityResolver != null) {
this.identityResolver = identityResolver;
}
}
// for testing
DefinedExecutable getDefinedExecutable(final OperationKey key) {
return registry.get(key);
}
}