/*
* Copyright 2014 Avanza Bank AB
*
* 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.avanza.astrix.beans.service;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.avanza.astrix.beans.config.BeanConfiguration;
import com.avanza.astrix.beans.core.AstrixBeanKey;
import com.avanza.astrix.beans.core.AstrixBeanSettings;
import com.avanza.astrix.beans.core.AstrixSettings;
import com.avanza.astrix.beans.core.BeanInvocationDispatcher;
import com.avanza.astrix.beans.core.BeanProxy;
import com.avanza.astrix.beans.core.BeanProxyFilter;
import com.avanza.astrix.beans.core.ReactiveTypeConverter;
import com.avanza.astrix.config.DynamicBooleanProperty;
import com.avanza.astrix.core.IllegalServiceMetadataException;
import com.avanza.astrix.core.ServiceUnavailableException;
/**
*
* @author Elias Lindholm (elilin)
*
* @param <T>
*/
public class ServiceBeanInstance<T> implements StatefulAstrixBean, InvocationHandler {
private static final Logger log = LoggerFactory.getLogger(ServiceBeanInstance.class);
private static final AtomicInteger nextId = new AtomicInteger(0);
private final String id = Integer.toString(nextId.incrementAndGet()); // Used for debugging to distinguish between many context's started within same jvm.
private final AstrixBeanKey<T> beanKey;
private final ServiceComponentRegistry serviceComponents;
private final ServiceDefinition<T> serviceDefinition;
/*
* Monitor used to signal state changes. (waitForBean)
*/
private final Lock boundStateLock = new ReentrantLock();
private final Condition boundCondition = boundStateLock.newCondition();
private final ServiceDiscovery serviceDiscovery;
private final DynamicBooleanProperty available;
/*
* Guards the state of this service bean instance.
*/
private final Lock beanStateLock = new ReentrantLock();
private volatile ServiceProperties currentProperties;
private volatile BeanState currentState;
private final List<BeanProxy> beanProxies;
private final ReactiveTypeConverter reactiveTypeConverter;
private ServiceBeanInstance(ServiceDefinition<T> serviceDefinition,
AstrixBeanKey<T> beanKey,
ServiceDiscovery serviceDiscovery,
ServiceComponentRegistry serviceComponents,
ServiceBeanProxies beanProxies,
ReactiveTypeConverter reactiveTypeConverter, DynamicBooleanProperty available) {
this.serviceDiscovery = serviceDiscovery;
this.reactiveTypeConverter = reactiveTypeConverter;
this.beanProxies = beanProxies.create(beanKey);
this.available = available;
this.serviceDefinition = Objects.requireNonNull(serviceDefinition);
this.beanKey = Objects.requireNonNull(beanKey);
this.serviceComponents = Objects.requireNonNull(serviceComponents);
this.currentState = new Unbound(ServiceUnavailableException.class, "No bind attempt run yet");
}
public static <T> ServiceBeanInstance<T> create(ServiceDefinition<T> serviceDefinition,
AstrixBeanKey<T> beanKey,
ServiceDiscovery serviceDiscovery,
ServiceBeanContext serviceBeanContext) {
BeanConfiguration beanConfiguration = serviceBeanContext.getConfig().getBeanConfiguration(beanKey);
return new ServiceBeanInstance<T>(serviceDefinition,
beanKey,
serviceDiscovery,
serviceBeanContext.getServiceComponents(),
serviceBeanContext.getServiceBeanProxies(),
serviceBeanContext.getReactiveTypeConverter(),
beanConfiguration.get(AstrixBeanSettings.AVAILABLE));
}
public void renewLease() {
beanStateLock.lock();
try {
ServiceDiscoveryResult serviceDiscoveryResult = runServiceDiscovery();
if (!serviceDiscoveryResult.isSuccessful()) {
log.warn(String.format("Failed to renew lease, service discovery failure. bean=%s astrixBeanId=%s", getBeanKey(), id), serviceDiscoveryResult.getError());
return;
}
if (serviceHasChanged(serviceDiscoveryResult.getResult())) {
bind(serviceDiscoveryResult.getResult());
} else {
log.debug("Service properties have not changed. No need to bind bean=" + getBeanKey());
}
} catch (Exception e) {
log.warn(String.format("Failed to renew lease for service bean. bean=%s astrixBeanId=%s", getBeanKey(), id), e);
} finally {
beanStateLock.unlock();
}
}
private boolean serviceHasChanged(ServiceProperties serviceProperties) {
return !Objects.equals(currentProperties, serviceProperties);
}
public void bind() {
beanStateLock.lock();
try {
if (isBound()) {
return;
}
ServiceDiscoveryResult serviceDiscoveryResult = runServiceDiscovery();
if (!serviceDiscoveryResult.isSuccessful()) {
log.warn(String.format("Service discovery failure. bean=%s astrixBeanId=%s", getBeanKey(), id), serviceDiscoveryResult.getError());
currentState.setState(new Unbound(ServiceDiscoveryError.class, "An error occured during last service discovery attempt, see cause for details.", serviceDiscoveryResult.getError()));
return;
}
if (serviceDiscoveryResult.getResult() == null) {
log.info(String.format(
"Did not discover a service provider using %s. bean=%s astrixBeanId=%s",
serviceDiscovery.description(), getBeanKey(), id));
currentState.setState(new Unbound(NoServiceProviderFound.class, "Did not discover a service provider for " + getBeanKey().getBeanType().getSimpleName() + " on last service discovery attempt. discoveryStrategy=" + serviceDiscovery.description()));
return;
}
bind(serviceDiscoveryResult.getResult());
} catch (Exception e) {
log.warn(String.format("Failed to bind service bean. bean=%s astrixBeanId=%s", getBeanKey(), id), e);
} finally {
beanStateLock.unlock();
}
}
private ServiceDiscoveryResult runServiceDiscovery() {
try {
return ServiceDiscoveryResult.successful(serviceDiscovery.run());
} catch (Exception e) {
return ServiceDiscoveryResult.failure(e);
}
}
static class ServiceDiscoveryResult {
final ServiceProperties serviceProperties;
final Exception discoveryError;
ServiceDiscoveryResult(ServiceProperties serviceProperties, Exception discoveryError) {
this.serviceProperties = serviceProperties;
this.discoveryError = discoveryError;
}
static ServiceDiscoveryResult failure(Exception e) {
return new ServiceDiscoveryResult(null, e);
}
static ServiceDiscoveryResult successful(ServiceProperties serviceProperties) {
return new ServiceDiscoveryResult(serviceProperties, null);
}
boolean isSuccessful() {
return discoveryError == null;
}
public ServiceProperties getResult() {
return serviceProperties;
}
public Exception getError() {
return discoveryError;
}
}
/**
* Attempts to bind this bean with the latest serviceProperties, or null if serviceLookup
* returned null indicating that service is not available.
*
* Throws exception if bind attempt fails.
*/
private void bind(ServiceProperties serviceProperties) {
this.currentState.bindTo(serviceProperties);
}
void destroy() {
log.info("Destroying service bean. bean={} astrixBeanId={}", getBeanKey(), id);
beanStateLock.lock();
try {
this.currentState.releaseInstance();
} finally {
beanStateLock.unlock();
}
}
private void notifyBound() {
boundStateLock.lock();
try {
boundCondition.signalAll();
} finally {
boundStateLock.unlock();
}
}
private ServiceComponent getServiceComponent(ServiceProperties serviceProperties) {
String componentName = serviceProperties.getComponent();
if (componentName == null) {
throw new IllegalArgumentException("Expected a componentName to be set on serviceProperties: " + serviceProperties);
}
return serviceComponents.getComponent(componentName);
}
@Override
public void waitUntilBound(long timeoutMillis) throws InterruptedException {
boundStateLock.lock();
try {
if (!isBound()) {
if (!boundCondition.await(timeoutMillis, TimeUnit.MILLISECONDS)) {
this.currentState.verifyBound(); // Let current state throw exception describing cause
}
}
} finally {
boundStateLock.unlock();
}
}
/**
* A bean is considered bound if it is in any state except Unbound.
*
* @return
*/
public boolean isBound() {
return !this.currentState.getClass().equals(Unbound.class);
}
public AstrixBeanKey<T> getBeanKey() {
return beanKey;
}
String getBeanId() {
return this.id;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getDeclaringClass().equals(StatefulAstrixBean.class)) {
try {
return method.invoke(this, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
if (method.getDeclaringClass().equals(Object.class)) {
return method.invoke(this, args);
}
if (!this.available.get()) {
throw new ServiceUnavailableException("Service is explicitly set in unavailable state");
}
return this.currentState.invoke(proxy, method, args);
}
private abstract class BeanState implements InvocationHandler {
protected void bindTo(ServiceProperties serviceProperties) {
if (serviceProperties == null) {
setState(new Unbound(NoServiceProviderFound.class, "No service provider found"));
return;
}
String providerSubsystem = serviceProperties.getProperty(ServiceProperties.SUBSYSTEM);
if (providerSubsystem == null) {
providerSubsystem = AstrixSettings.SUBSYSTEM_NAME.defaultValue();
}
try {
ServiceComponent serviceComponent = getServiceComponent(serviceProperties);
if (!serviceComponent.canBindType(beanKey.getBeanType())) {
throw new UnsupportedTargetTypeException(serviceComponent.getName(), beanKey.getBeanType());
}
BoundServiceBeanInstance<T> boundInstance = serviceComponent.bind(serviceDefinition, serviceProperties);
BeanInvocationDispatcher beanInvocationDispatcher = new BeanInvocationDispatcher(getBeanProxies(serviceComponent)
,reactiveTypeConverter,
boundInstance.get());
setState(new Bound(boundInstance, beanInvocationDispatcher));
currentProperties = serviceProperties;
} catch (IllegalServiceMetadataException e) {
setState(new IllegalServiceMetadataState(e.getMessage()));
} catch (Exception e) {
log.warn(String.format("Failed to bind service bean: %s", getBeanKey()), e);
setState(new Unbound(ServiceBindError.class, "Failed to bind " + getBeanKey().getBeanType().getSimpleName() + " using serviceProperties=" + serviceProperties + ", see cause for details.", e));
}
}
private List<BeanProxy> getBeanProxies(ServiceComponent serviceComponent) {
if (!(serviceComponent instanceof BeanProxyFilter)) {
return beanProxies;
}
BeanProxyFilter filter = BeanProxyFilter.class.cast(serviceComponent);
return beanProxies.stream()
.filter(beanProxy -> {
boolean applyBeanProxy = filter.applyBeanProxy(beanProxy);
if (!applyBeanProxy) {
log.info("BeanProxy is disabled by ServiceComponent. beanProxy={} componentName={} beanKey={}",
beanProxy.name(), serviceComponent.getName(), serviceDefinition.getBeanKey().toString());
}
return applyBeanProxy;
})
.collect(Collectors.toList());
}
protected abstract void verifyBound();
protected final void setState(BeanState newState) {
if (!currentState.getClass().equals(newState.getClass())) {
log.info(String.format("Service bean entering new state. newState=%s bean=%s id=%s", newState.name(), beanKey, id));
}
currentState = newState;
if (isBoundState(newState)) {
notifyBound();
}
releaseInstance();
}
private boolean isBoundState(BeanState newState) {
return !newState.getClass().equals(Unbound.class);
}
protected abstract String name();
protected abstract void releaseInstance();
}
private class Bound extends BeanState {
private final BoundServiceBeanInstance<T> serviceBeanInstance;
private final BeanInvocationDispatcher serviceBeanInvocationDispatcher;
public Bound(BoundServiceBeanInstance<T> bean, BeanInvocationDispatcher serviceBeanInvocationDispatcher) {
this.serviceBeanInstance = bean;
this.serviceBeanInvocationDispatcher = serviceBeanInvocationDispatcher;
}
@Override
public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
return serviceBeanInvocationDispatcher.invoke(proxy, method, args);
}
@Override
protected void releaseInstance() {
serviceBeanInstance.release();
}
@Override
protected String name() {
return "Bound";
}
@Override
protected void verifyBound() {
}
}
private class Unbound extends BeanState {
private final Class<? extends ServiceUnavailableException> exceptionFactory;
private final String message;
private final Exception discoveryFailure;
public Unbound(Class<? extends ServiceUnavailableException> exceptionFactory, String message) {
this(exceptionFactory, message, null);
}
public Unbound(Class<? extends ServiceUnavailableException> exceptionFactory, String message, Exception discoveryFailureCause) {
this.exceptionFactory = exceptionFactory;
this.discoveryFailure = discoveryFailureCause;
this.message = message;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
throw createServiceUnavailableException();
}
private ServiceUnavailableException createServiceUnavailableException() {
ServiceUnavailableException exception = initException();
if (discoveryFailure != null) {
exception.initCause(discoveryFailure);
}
return exception;
}
private ServiceUnavailableException initException() {
try {
return exceptionFactory.getConstructor(String.class).newInstance(message + " astrixBeanId=" + id + " bean=" + beanKey);
} catch (Exception e) {
return new ServiceUnavailableException(message);
}
}
@Override
protected void verifyBound() {
throw createServiceUnavailableException();
}
@Override
protected void releaseInstance() {
}
@Override
protected String name() {
return "Unbound";
}
}
private class IllegalServiceMetadataState extends BeanState {
private String message;
public IllegalServiceMetadataState(String message) {
this.message = message;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
throw new IllegalServiceMetadataException(String.format("bean=%s astrixBeanId=%s message=%s", beanKey, id, message));
}
@Override
protected void releaseInstance() {
}
@Override
protected void verifyBound() {
throw new IllegalServiceMetadataException(String.format("bean=%s astrixBeanId=%s message=%s", beanKey, id, message));
}
@Override
protected String name() {
return "IllegalServiceMetadata";
}
}
String getState() {
return this.currentState.name();
}
ServiceProperties getCurrentProperties() {
return currentProperties;
}
}