package com.redhat.lightblue.migrator.facade; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.redhat.lightblue.migrator.facade.methodcallstringifier.LazyMethodCallStringifier; import com.redhat.lightblue.migrator.facade.methodcallstringifier.MethodCallStringifier; import com.redhat.lightblue.migrator.facade.sharedstore.SharedStore; import com.redhat.lightblue.migrator.facade.sharedstore.SharedStoreException; import com.redhat.lightblue.migrator.facade.sharedstore.SharedStoreImpl; import com.redhat.lightblue.migrator.facade.sharedstore.SharedStoreSetter; import com.redhat.lightblue.migrator.features.LightblueMigration; import com.redhat.lightblue.migrator.features.TogglzRandomUsername; /** * A helper base class for migrating services from legacy datastore to * lightblue. It lets you call any service/dao method, using togglz switches to * choose which service/dao to use and verifying returned data. * * @author mpatercz * */ @SuppressWarnings("all") public class ServiceFacade<D extends SharedStoreSetter> implements SharedStoreSetter { private static final Logger log = LoggerFactory.getLogger(ServiceFacade.class); protected final D legacySvc, lightblueSvc; private SharedStore sharedStore = null; private Map<Class<?>, ModelMixIn> modelMixIns; private Properties properties = new Properties(); private TimeoutConfiguration timeoutConfiguration; // default timeout is 2 seconds public static final long DEFAULT_TIMEOUT_MS = 2000; private ConsistencyChecker consistencyChecker; // used to associate inconsistencies with the service in the logs private final String implementationName; private ExceptionSwallowedListener exceptionSwallowedListener = null; /** * Facade swallows Lightblue implementation failures and returns from legacy implementation. */ public static interface ExceptionSwallowedListener { public void onLightblueSvcExceptionSwallowed(Throwable t, String implementationName); } public SharedStore getSharedStore() { return sharedStore; } public void setSharedStore(SharedStore shareStore) { this.sharedStore = shareStore; legacySvc.setSharedStore(shareStore); lightblueSvc.setSharedStore(shareStore); } public ConsistencyChecker getConsistencyChecker() { if (consistencyChecker == null) { consistencyChecker = new ConsistencyChecker(implementationName, null, getMaxJsonStrLengthForJsonCompare()); } return consistencyChecker; } public void setConsistencyChecker(ConsistencyChecker consistencyChecker) { this.consistencyChecker = consistencyChecker; } public ServiceFacade(D legacySvc, D lightblueSvc, String implementationName) { this(legacySvc, lightblueSvc, implementationName, null); } public ServiceFacade(D legacySvc, D lightblueSvc, String implementationName, Properties properties) { super(); this.legacySvc = legacySvc; this.lightblueSvc = lightblueSvc; setSharedStore(new SharedStoreImpl(implementationName)); this.implementationName = implementationName; if (properties != null) { this.properties = properties; } timeoutConfiguration = new TimeoutConfiguration(DEFAULT_TIMEOUT_MS, implementationName, this.properties); log.info("Initialized facade for "+implementationName); } private long getLightblueExecutionTimeout(String methodName) { String timeout = properties.getProperty("com.redhat.lightblue.migrator.facade.timeout." + implementationName + "." + methodName); if (timeout == null) { timeout = properties.getProperty("com.redhat.lightblue.migrator.facade.timeout." + implementationName, "" + DEFAULT_TIMEOUT_MS); } return Long.parseLong(timeout); } private Integer getMaxJsonStrLengthForJsonCompare() { String value = properties.getProperty("com.redhat.lightblue.migrator.facade.timeout." + implementationName + ".maxJsonStrLengthForJsonCompare"); if (value == null) { return null; } return Integer.parseInt(value); } private ListeningExecutorService createExecutor() { return MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)); } private Class[] toClasses(Object[] objects) { List<Class> classes = new ArrayList<>(); for (Object o : objects) { classes.add(o.getClass()); } return classes.toArray(new Class[]{}); } private <T> ListenableFuture<T> callLightblueSvc(final Method method, final Object[] values, final FacadeOperation op, final MethodCallStringifier callStringifier) { ListeningExecutorService executor = createExecutor(); try { // fetch from lightblue using future (asynchronously) final long parentThreadId = Thread.currentThread().getId(); return executor.submit(new Callable<T>(){ @Override public T call() throws Exception { Timer dest = new Timer("destination."+method.getName()); if (sharedStore != null) { sharedStore.copyFromThread(parentThreadId); } try { return (T) method.invoke(lightblueSvc, values); } finally { long callTook = dest.complete(); long slowWarning = timeoutConfiguration.getSlowWarningMS(method.getName(), op); if (callTook >= slowWarning) { // call is slow; this will log even if source fails to respond log.warn("Slow call warning: {}.{} took {}ms",implementationName, callStringifier.toString(), callTook); } } } }); } finally { executor.shutdown(); } } /** * Call destination (lightblue) using a timeout in dual read/write phases. * Do not use facade timeout during lightblue proxy and kinda proxy phases (when * reading from source is disabled). * * @param listenableFuture * @param methodName method name is used to read method specific timeout * configuration * @param destinationCallTimeout Set future timeout to this amount * @return * @throws InterruptedException * @throws ExecutionException * @throws TimeoutException */ private <T> T getWithTimeout(ListenableFuture<T> listenableFuture, String methodName, FacadeOperation facadeOperation, int destinationCallTimeout) throws InterruptedException, ExecutionException, TimeoutException { // not reading from source/legacy means this is either proxy or kinda proxy phase // in that case, ignore timeout settings for Lightblue call if (!shouldSource(FacadeOperation.READ) || timeoutConfiguration.getTimeoutMS(methodName, facadeOperation) <= 0) { return listenableFuture.get(); } else { return listenableFuture.get(destinationCallTimeout, TimeUnit.MILLISECONDS); } } private Throwable extractUnderlyingException(Throwable t) { if ((t instanceof ExecutionException || t instanceof InvocationTargetException) && t.getCause() != null) { return extractUnderlyingException(t.getCause()); } else { return t; } } // which togglz flags to use public enum FacadeOperation { READ, WRITE; } private boolean shouldDestination(FacadeOperation facadeOperation) { switch (facadeOperation) { case READ: return LightblueMigration.shouldReadDestinationEntity(); case WRITE: return LightblueMigration.shouldWriteDestinationEntity(); default: throw new IllegalArgumentException(facadeOperation.toString()); } } private boolean shouldSource(FacadeOperation facadeOperation) { switch (facadeOperation) { case READ: return LightblueMigration.shouldReadSourceEntity(); case WRITE: return LightblueMigration.shouldWriteSourceEntity(); default: throw new IllegalArgumentException(facadeOperation.toString()); } } private boolean shouldCheckConsistency(FacadeOperation facadeOperation) { switch (facadeOperation) { case READ: return LightblueMigration.shouldCheckReadConsistency(); case WRITE: return LightblueMigration.shouldCheckWriteConsistency(); default: throw new IllegalArgumentException(facadeOperation.toString()); } } private void cancel(ListenableFuture listenableFuture) { if (listenableFuture != null) { try { listenableFuture.cancel(true); } catch (Exception e) { log.error("Failed to cancel lightblue call", e); } } } /** * Call service method according to settings specified in togglz. * * @param facadeOperation READ/WRITE operation, used by togglz flags * @param callInParallel true means both oracle and lightblue will be called * in parallel and state sharing between services will not be possible * @param returnedType type of the returned object * @param methodName method name to call * @param types List of parameter types * @param values List of parameters * @return Object returned by dao * @throws Exception */ public <T> T callSvcMethod(final FacadeOperation facadeOperation, final boolean callInParallel, final Method methodCalled, final Object... values) throws Throwable { final Class<T> returnedType = (Class<T>) methodCalled.getReturnType(); final String methodName = methodCalled.getName(); final Class[] types = methodCalled.getParameterTypes(); LazyMethodCallStringifier callStringifier = new LazyMethodCallStringifier(methodCalled, values); if (log.isDebugEnabled()) { log.debug("Calling {}.{} ({} {})", implementationName, callStringifier, callInParallel ? "parallel" : "serial", facadeOperation); } TogglzRandomUsername.init(); if (sharedStore != null) { sharedStore.clear(); // make sure no data is left from previous calls } if (sharedStore != null && shouldSource(facadeOperation) && shouldDestination(facadeOperation)) { sharedStore.setDualMigrationPhase(true); } else if (sharedStore != null) { sharedStore.setDualMigrationPhase(false); } T legacyEntity = null, lightblueEntity = null; ListenableFuture<T> listenableFuture = null; if (callInParallel) { if (shouldDestination(facadeOperation)) { Method method = lightblueSvc.getClass().getMethod(methodName, types); listenableFuture = callLightblueSvc(method, values, facadeOperation, callStringifier); } } int legacyCallTookMS = 0; if (shouldSource(facadeOperation)) { // perform operation in oracle, synchronously log.debug("Calling legacy {}.{}", implementationName, methodName); Method method = legacySvc.getClass().getMethod(methodName, types); Timer source = new Timer("source." + methodName); try { legacyEntity = (T) method.invoke(legacySvc, values); } catch (InvocationTargetException e) { if (shouldDestination(facadeOperation) && !shouldCheckConsistency(facadeOperation)) { // Lightblue is going to be called and consistency checker is disabled // swallow the exception from legacy log.warn("Legacy call " + implementationName + "." + callStringifier + " threw an exception. Returning data from Lightblue.", e.getCause()); } else { if (timeoutConfiguration.isInterruptOnTimeout() && facadeOperation == FacadeOperation.READ) { cancel(listenableFuture); } throw e.getCause(); } } finally { legacyCallTookMS = (int) source.complete(); } } if (shouldDestination(facadeOperation)) { log.debug("Calling lightblue {}.{}", implementationName, methodName); int destinationCallTimeout = (int) Math.max(timeoutConfiguration.getTimeoutMS(methodName, facadeOperation), legacyCallTookMS); try { if (callInParallel) { // lightblue was called before source/legacy, now just getting results lightblueEntity = getWithTimeout(listenableFuture, methodName, facadeOperation, destinationCallTimeout); } else { // call lightblue after source/legacy finished Method method = lightblueSvc.getClass().getMethod(methodName, types); listenableFuture = callLightblueSvc(method, values, facadeOperation, callStringifier); lightblueEntity = getWithTimeout(listenableFuture, methodName, facadeOperation, destinationCallTimeout); } } catch (TimeoutException te) { if (timeoutConfiguration.isInterruptOnTimeout() && facadeOperation == FacadeOperation.READ) { // try to interrupt the thread cancel(listenableFuture); } if (shouldSource(facadeOperation)) { log.warn("Lightblue call {}.{} is taking too long (longer than {}ms). Returning data from legacy.", implementationName, callStringifier, destinationCallTimeout); return legacyEntity; } else { throw te; } } catch (Throwable e) { if (shouldSource(facadeOperation) && shouldCheckConsistency(facadeOperation)) { // swallow lightblue exception if legacy was called and consistency checker is on log.warn("Lightblue call " + implementationName + "." + callStringifier + " threw an exception. Returning data from legacy.", e); onExceptionSwallowed(e); return legacyEntity; } else { // throw lightblue exception if legacy was not called or consistency checker is disabled throw extractUnderlyingException(e); } } } if (shouldCheckConsistency(facadeOperation) && shouldSource(facadeOperation) && shouldDestination(facadeOperation)) { // dual phase, consistency check enabled // make sure that response from lightblue and oracle are the same if (log.isDebugEnabled()) log.debug("." + methodName + " checking returned entity's consistency"); // check if entities match if (getConsistencyChecker().checkConsistency(legacyEntity, lightblueEntity, methodName, callStringifier)) { // return lightblue data if they are return lightblueEntity; } else { // return oracle data if they aren't and log data inconsistency return legacyEntity; } } else if (!shouldCheckConsistency(facadeOperation) && shouldSource(facadeOperation) && shouldDestination(facadeOperation)) { if (log.isDebugEnabled()) log.debug("dual phase, no consistency check (disabled), returning data from Lightblue"); return lightblueEntity; } else if (!shouldSource(facadeOperation) && shouldDestination(facadeOperation)) { if (log.isDebugEnabled()) log.debug("proxy phase, returning data from Lightblue"); return lightblueEntity; } else { if (log.isDebugEnabled()) log.debug("initial phase, returning data from legacy"); return legacyEntity; } } private SharedStoreException extractSharedStoreExceptionIfExists(ExecutionException ee) { try { if (ee.getCause().getCause() instanceof SharedStoreException) { return (SharedStoreException) ee.getCause().getCause(); } else { return null; } } catch (NullPointerException e) { return null; } } public void setLogResponseDataEnabled(boolean logResponsesEnabled) { getConsistencyChecker().setLogResponseDataEnabled(logResponsesEnabled); } public void setMaxInconsistencyLogLength(int length) { getConsistencyChecker().setMaxInconsistencyLogLength(length); } public D getLegacySvc() { return legacySvc; } public D getLightblueSvc() { return lightblueSvc; } public TimeoutConfiguration getTimeoutConfiguration() { return timeoutConfiguration; } public void setTimeoutConfiguration(TimeoutConfiguration timeoutConfiguration) { this.timeoutConfiguration = timeoutConfiguration; } public ExceptionSwallowedListener getExceptionSwallowedListener() { return exceptionSwallowedListener; } public void registerExceptionSwallowedListener(ExceptionSwallowedListener exceptionSwallowedEvent) { this.exceptionSwallowedListener = exceptionSwallowedEvent; } private void onExceptionSwallowed(Throwable t) { if (exceptionSwallowedListener != null) exceptionSwallowedListener.onLightblueSvcExceptionSwallowed(t, implementationName); } }