/* * 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.config; import static java.util.Optional.ofNullable; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.mule.runtime.api.i18n.I18nMessageFactory.createStaticMessage; import static org.mule.runtime.core.api.lifecycle.LifecycleUtils.disposeIfNeeded; import static org.mule.runtime.core.api.lifecycle.LifecycleUtils.initialiseIfNeeded; import static org.mule.runtime.core.api.lifecycle.LifecycleUtils.startIfNeeded; import static org.mule.runtime.core.api.lifecycle.LifecycleUtils.stopIfNeeded; import static org.mule.runtime.core.util.ClassUtils.withContextClassLoader; import static org.mule.runtime.module.extension.internal.runtime.resolver.ValueResolvingContext.from; import static org.slf4j.LoggerFactory.getLogger; import org.mule.runtime.api.connection.ConnectionProvider; import org.mule.runtime.api.exception.MuleException; import org.mule.runtime.api.exception.MuleRuntimeException; import org.mule.runtime.api.lifecycle.Initialisable; import org.mule.runtime.api.lifecycle.InitialisationException; import org.mule.runtime.api.lifecycle.Startable; import org.mule.runtime.api.meta.model.ExtensionModel; import org.mule.runtime.api.meta.model.config.ConfigurationModel; import org.mule.runtime.core.api.Event; import org.mule.runtime.core.api.MuleContext; import org.mule.runtime.core.api.util.Pair; import org.mule.runtime.core.util.collection.ImmutableListCollector; import org.mule.runtime.extension.api.runtime.ConfigurationInstance; import org.mule.runtime.extension.api.runtime.ConfigurationProvider; import org.mule.runtime.extension.api.runtime.ConfigurationStats; import org.mule.runtime.extension.api.runtime.ExpirableConfigurationProvider; import org.mule.runtime.extension.api.runtime.ExpirationPolicy; import org.mule.runtime.module.extension.internal.runtime.resolver.ConnectionProviderValueResolver; import org.mule.runtime.module.extension.internal.runtime.resolver.ResolverSet; import org.mule.runtime.module.extension.internal.runtime.resolver.ResolverSetResult; import org.mule.runtime.module.extension.internal.runtime.resolver.ValueResolver; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.slf4j.Logger; /** * A {@link ConfigurationProvider} which continuously evaluates the same {@link ResolverSet} and then uses the resulting * {@link ResolverSetResult} to build an instance of type {@code T} * <p> * Although each invocation to {@link #get(Object)} is guaranteed to end up in an invocation to * {@link #resolverSet#resolve(Object)}, the resulting {@link ResolverSetResult} might not end up generating a new instance. This * is so because {@link ResolverSetResult} instances are put in a cache to guarantee that equivalent evaluations of the * {@code resolverSet} return the same instance. * * @since 4.0.0 */ public final class DynamicConfigurationProvider extends LifecycleAwareConfigurationProvider implements ExpirableConfigurationProvider { private static final Logger LOGGER = getLogger(DynamicConfigurationProvider.class); private final ConfigurationInstanceFactory configurationInstanceFactory; private final ResolverSet resolverSet; private final ConnectionProviderValueResolver connectionProviderResolver; private final ExpirationPolicy expirationPolicy; private final Map<Pair<ResolverSetResult, ResolverSetResult>, ConfigurationInstance> cache = new ConcurrentHashMap<>(); private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); private final Lock cacheReadLock = cacheLock.readLock(); private final Lock cacheWriteLock = cacheLock.writeLock(); /** * Creates a new instance * * @param name this provider's name * @param extensionModel the model that owns the {@code configurationModel} * @param configurationModel the model for the returned configurations * @param resolverSet the {@link ResolverSet} that provides the configuration's parameter values * @param connectionProviderResolver a {@link ValueResolver} used to obtain a {@link ConnectionProvider} * @param expirationPolicy the {@link ExpirationPolicy} for the unused instances */ public DynamicConfigurationProvider(String name, ExtensionModel extensionModel, ConfigurationModel configurationModel, ResolverSet resolverSet, ConnectionProviderValueResolver connectionProviderResolver, ExpirationPolicy expirationPolicy, MuleContext muleContext) { super(name, extensionModel, configurationModel, muleContext); configurationInstanceFactory = new ConfigurationInstanceFactory<>(extensionModel, configurationModel, resolverSet, muleContext); this.resolverSet = resolverSet; this.connectionProviderResolver = connectionProviderResolver; this.expirationPolicy = expirationPolicy; } /** * Evaluates {@link #resolverSet} using the given {@code event} and returns an instance produced with the result. For equivalent * {@link ResolverSetResult}s it will return the same instance. * * @param event the current {@code event} * @return the resolved {@link ConfigurationInstance} */ @Override public ConfigurationInstance get(Object event) { return withContextClassLoader(getExtensionClassLoader(), () -> { ResolverSetResult result = resolverSet.resolve(from((Event) event)); ResolverSetResult providerResult = null; if (connectionProviderResolver.getResolverSet().isPresent()) { providerResult = ((ResolverSet) connectionProviderResolver.getResolverSet().get()).resolve(from((Event) event)); } return getConfiguration(new Pair<>(result, providerResult), (Event) event); }); } private ConfigurationInstance getConfiguration(Pair<ResolverSetResult, ResolverSetResult> resolverSetResult, Event event) throws Exception { ConfigurationInstance configuration; cacheReadLock.lock(); try { configuration = cache.get(resolverSetResult); if (configuration != null) { // important to account between the boundaries of the lock to prevent race condition updateUsageStatistic(configuration); return configuration; } } finally { cacheReadLock.unlock(); } cacheWriteLock.lock(); try { // re-check in case some other thread beat us to it... configuration = cache.get(resolverSetResult); if (configuration == null) { configuration = createConfiguration(resolverSetResult.getFirst(), event); cache.put(resolverSetResult, configuration); } // accounting here for the same reasons as above updateUsageStatistic(configuration); return configuration; } finally { cacheWriteLock.unlock(); } } private void updateUsageStatistic(ConfigurationInstance configuration) { MutableConfigurationStats stats = (MutableConfigurationStats) configuration.getStatistics(); stats.updateLastUsed(); } private ConfigurationInstance createConfiguration(ResolverSetResult result, Event event) throws MuleException { ConfigurationInstance configuration = configurationInstanceFactory .createConfiguration(getName(), result, ofNullable(connectionProviderResolver.resolve(from(event)))); registerConfiguration(configuration); return configuration; } @Override protected void registerConfiguration(ConfigurationInstance configuration) { try { withContextClassLoader(getExtensionClassLoader(), () -> { if (lifecycleManager.isPhaseComplete(Initialisable.PHASE_NAME)) { try { initialiseIfNeeded(configuration, true, muleContext); } catch (Exception e) { disposeIfNeeded(configuration, LOGGER); throw e; } } if (lifecycleManager.isPhaseComplete(Startable.PHASE_NAME)) { try { startConfig(configuration); } catch (Exception e) { try { stopIfNeeded(configuration); } catch (Exception ex) { // Ignore and continue with the disposal LOGGER.warn("Exception while stopping " + configuration.toString(), e); } disposeIfNeeded(configuration, LOGGER); throw e; } } return null; }); } catch (Exception e) { throw new MuleRuntimeException(createStaticMessage("Could not register configuration of key " + getName()), e); } super.registerConfiguration(configuration); } @Override public List<ConfigurationInstance> getExpired() { cacheWriteLock.lock(); try { return cache.entrySet().stream().filter(entry -> isExpired(entry.getValue())).map(entry -> { cache.remove(entry.getKey()); return entry.getValue(); }).collect(new ImmutableListCollector<>()); } finally { cacheWriteLock.unlock(); } } private boolean isExpired(ConfigurationInstance configuration) { ConfigurationStats stats = configuration.getStatistics(); return stats.getInflightOperations() == 0 && expirationPolicy.isExpired(stats.getLastUsedMillis(), MILLISECONDS); } @Override protected void doInitialise() { try { initialiseIfNeeded(resolverSet, muleContext); initialiseIfNeeded(connectionProviderResolver, muleContext); } catch (InitialisationException e) { throw new MuleRuntimeException(e); } } @Override public void start() throws MuleException { super.start(); startIfNeeded(connectionProviderResolver); } }