/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.module.extension.internal.runtime.operation;
import static java.lang.String.format;
import static java.util.Optional.empty;
import static org.mule.runtime.core.api.rx.Exceptions.wrapFatal;
import static org.mule.runtime.core.execution.TransactionalExecutionTemplate.createTransactionalExecutionTemplate;
import static org.mule.runtime.core.util.ExceptionUtils.extractConnectionException;
import static reactor.core.publisher.Mono.error;
import static reactor.core.publisher.Mono.from;
import org.mule.runtime.api.connection.ConnectionException;
import org.mule.runtime.api.connection.ConnectionProvider;
import org.mule.runtime.api.meta.model.ComponentModel;
import org.mule.runtime.api.meta.model.ExtensionModel;
import org.mule.runtime.api.meta.model.declaration.fluent.ConfigurationDeclaration;
import org.mule.runtime.api.meta.model.operation.OperationModel;
import org.mule.runtime.api.util.Reference;
import org.mule.runtime.core.api.execution.ExecutionTemplate;
import org.mule.runtime.core.api.retry.RetryPolicyTemplate;
import org.mule.runtime.core.exception.ErrorTypeRepository;
import org.mule.runtime.core.internal.connection.ConnectionManagerAdapter;
import org.mule.runtime.core.internal.connection.ConnectionProviderWrapper;
import org.mule.runtime.extension.api.runtime.ConfigurationInstance;
import org.mule.runtime.extension.api.runtime.ConfigurationStats;
import org.mule.runtime.extension.api.runtime.Interceptable;
import org.mule.runtime.extension.api.runtime.operation.ExecutionContext;
import org.mule.runtime.extension.api.runtime.operation.Interceptor;
import org.mule.runtime.extension.api.runtime.operation.OperationExecutor;
import org.mule.runtime.module.extension.internal.runtime.ExecutionContextAdapter;
import org.mule.runtime.module.extension.internal.runtime.config.MutableConfigurationStats;
import org.mule.runtime.module.extension.internal.runtime.exception.ExceptionHandlerManager;
import org.mule.runtime.module.extension.internal.runtime.exception.ModuleExceptionHandler;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
/**
* Default implementation of {@link ExecutionMediator}.
* <p>
* If the given {@code context} implements the {@link Interceptable}, then its defined {@link Interceptor}s are properly executed
* as well.
* <p>
* It also inspects the {@link ConfigurationStats} obtained from the {@link ConfigurationDeclaration} in the {@code context}. If
* the stats class implements the {@link MutableConfigurationStats} interface, then
* {@link MutableConfigurationStats#addInflightOperation()} and {@link MutableConfigurationStats#discountInflightOperation()} are
* guaranteed to be called, whatever the operation's outcome.
* <p>
* In case of operation failure, it will execute the {@link Interceptor#onError(ExecutionContext, Throwable)} method of all the
* available interceptors. If the operation fails with {@link ConnectionException}, then a retry might be attempted depending on
* the configured {@link RetryPolicyTemplate}. Notice that if a retry is attempted, the entire cycle of interception (before,
* onSuccess/onError, after) will be fired again.
*
* @since 4.0
*/
public final class DefaultExecutionMediator implements ExecutionMediator {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultExecutionMediator.class);
private final ExceptionHandlerManager exceptionEnricherManager;
private final ConnectionManagerAdapter connectionManager;
private final ExecutionTemplate<?> defaultExecutionTemplate = callback -> callback.process();
private final ModuleExceptionHandler moduleExceptionHandler;
public DefaultExecutionMediator(ExtensionModel extensionModel, OperationModel operationModel,
ConnectionManagerAdapter connectionManager, ErrorTypeRepository typeRepository) {
this.connectionManager = connectionManager;
this.exceptionEnricherManager = new ExceptionHandlerManager(extensionModel, operationModel);
this.moduleExceptionHandler = new ModuleExceptionHandler(operationModel, extensionModel, typeRepository);
}
/**
* Executes the operation per the specification in this classes' javadoc
*
* @param executor an {@link OperationExecutor}
* @param context the {@link ExecutionContextAdapter} for the {@code executor} to use
* @return the operation's result
* @throws Exception if the operation or a {@link Interceptor#before(ExecutionContext)} invokation fails
*/
@Override
public Publisher<Object> execute(OperationExecutor executor, ExecutionContextAdapter context) {
final List<Interceptor> interceptors = collectInterceptors(context.getConfiguration(), executor);
final Optional<MutableConfigurationStats> stats = getMutableConfigurationStats(context);
stats.ifPresent(s -> s.addInflightOperation());
try {
return (Mono<Object>) getExecutionTemplate(context)
.execute(() -> executeWithInterceptors(executor, context, interceptors, stats));
} catch (Exception e) {
return error(e);
} catch (Throwable t) {
return error(wrapFatal(t));
}
}
private Mono<Object> executeWithInterceptors(OperationExecutor executor,
ExecutionContextAdapter context,
final List<Interceptor> interceptors,
Optional<MutableConfigurationStats> stats) {
List<Interceptor> executedInterceptors = new ArrayList<>(interceptors.size());
// If the operation is retried, then the interceptors need to be executed again,
// so we wrap the mono which executes the operation into another which sets up
// the context and is the one configured with the retry logic
Mono<Object> publisher = Mono.create(sink -> {
Mono<Object> result;
InterceptorsExecutionResult beforeExecutionResult = before(context, interceptors);
if (beforeExecutionResult.isOk()) {
result = from(executor.execute(context));
executedInterceptors.addAll(interceptors);
} else {
result = error(beforeExecutionResult.getThrowable());
executedInterceptors.addAll(beforeExecutionResult.getExecutedInterceptors());
}
result.doOnSuccess(value -> {
onSuccess(context, value, interceptors);
stats.ifPresent(s -> s.discountInflightOperation());
sink.success(value);
})
.onErrorMap(e -> {
e = exceptionEnricherManager.processException(e);
e = moduleExceptionHandler.processException(e);
e = onError(context, e, interceptors);
return e;
}).subscribe(value -> {
}, sink::error);
}).doOnTerminate((value, e) -> {
try {
after(context, value, executedInterceptors);
} finally {
executedInterceptors.clear();
}
});
return from(getRetryPolicyTemplate(context.getConfiguration()).applyPolicy(publisher,
e -> extractConnectionException(e)
.isPresent(),
e -> stats.ifPresent(s -> s
.discountInflightOperation())));
}
private InterceptorsExecutionResult before(ExecutionContext executionContext, List<Interceptor> interceptors) {
List<Interceptor> interceptorList = new ArrayList<>();
try {
for (Interceptor interceptor : interceptors) {
interceptorList.add(interceptor);
interceptor.before(executionContext);
}
} catch (Exception e) {
return new InterceptorsExecutionResult(exceptionEnricherManager.handleException(e), interceptorList);
}
return new InterceptorsExecutionResult(null, interceptorList);
}
private void onSuccess(ExecutionContext executionContext, Object result, List<Interceptor> interceptors) {
intercept(interceptors,
interceptor -> interceptor.onSuccess(executionContext, result), interceptor -> format(
"Interceptor %s threw exception executing 'onSuccess' phase. Exception will be ignored. Next interceptors (if any)"
+ "will be executed and the operation's result will be returned",
interceptor));
}
private Throwable onError(ExecutionContext executionContext, Throwable e, List<Interceptor> interceptors) {
Reference<Throwable> exceptionHolder = new Reference<>(e);
intercept(interceptors, interceptor -> {
Throwable decoratedException = interceptor.onError(executionContext, exceptionHolder.get());
if (decoratedException != null) {
exceptionHolder.set(decoratedException);
}
}, interceptor -> format(
"Interceptor %s threw exception executing 'onError' phase. Exception will be ignored. Next interceptors (if any)"
+ "will be executed and the operation's exception will be returned",
interceptor));
return exceptionHolder.get();
}
private void after(ExecutionContext executionContext, Object result, List<Interceptor> interceptors) {
{
intercept(interceptors,
interceptor -> interceptor.after(executionContext, result), interceptor -> format(
"Interceptor %s threw exception executing 'after' phase. Exception will be ignored. Next interceptors (if any)"
+ "will be executed and the operation's result be returned",
interceptor));
}
}
private void intercept(List<Interceptor> interceptors, Consumer<Interceptor> closure,
Function<Interceptor, String> exceptionMessageFunction) {
interceptors.forEach(interceptor -> {
try {
closure.accept(interceptor);
} catch (Exception e) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(exceptionMessageFunction.apply(interceptor), e);
}
}
});
}
private <T> ExecutionTemplate<T> getExecutionTemplate(ExecutionContextAdapter<OperationModel> context) {
return context.getTransactionConfig()
.map(txConfig -> ((ExecutionTemplate<T>) createTransactionalExecutionTemplate(context.getMuleContext(), txConfig)))
.orElse((ExecutionTemplate<T>) defaultExecutionTemplate);
}
// TODO: MULE-10580 - Operation reconnection should be decoupled from config reconnection
private RetryPolicyTemplate getRetryPolicyTemplate(Optional<ConfigurationInstance> configurationInstance) {
Optional<ConnectionProvider> connectionProviderOptional = configurationInstance.map(
ConfigurationInstance::getConnectionProvider)
.orElse(empty());
if (connectionProviderOptional.isPresent()) {
final ConnectionProvider connectionProvider = connectionProviderOptional.get();
if (ConnectionProviderWrapper.class.isAssignableFrom(connectionProvider.getClass())) {
return ((ConnectionProviderWrapper) connectionProvider).getRetryPolicyTemplate();
}
}
return connectionManager.getDefaultRetryPolicyTemplate();
}
private Optional<MutableConfigurationStats> getMutableConfigurationStats(ExecutionContext<ComponentModel> context) {
return context.getConfiguration()
.map(ConfigurationInstance::getStatistics)
.filter(s -> s instanceof MutableConfigurationStats)
.map(s -> (MutableConfigurationStats) s);
}
private List<Interceptor> collectInterceptors(Optional<ConfigurationInstance> configurationInstance,
OperationExecutor executor) {
List<Interceptor> accumulator = new LinkedList<>();
configurationInstance.ifPresent(config -> collectInterceptors(accumulator, config));
collectInterceptors(accumulator, executor);
return accumulator;
}
private void collectInterceptors(List<Interceptor> accumulator, Object subject) {
if (subject instanceof Interceptable) {
accumulator.addAll(((Interceptable) subject).getInterceptors());
}
}
}