org.carrot2.core.PoolingProcessingComponentManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of carrot2-mini Show documentation
Show all versions of carrot2-mini Show documentation
Carrot2 search results clustering framework. Minimal functional subset
(core algorithms and infrastructure, no document sources).
/*
* Carrot2 project.
*
* Copyright (C) 2002-2019, Dawid Weiss, Stanisław Osiński.
* All rights reserved.
*
* Refer to the full license file "carrot2.LICENSE"
* in the root folder of the repository checkout or at:
* http://www.carrot2.org/carrot2.LICENSE
*/
package org.carrot2.core;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.carrot2.core.attribute.Init;
import org.carrot2.core.attribute.Processing;
import org.carrot2.util.ExceptionUtils;
import org.carrot2.util.Pair;
import org.carrot2.util.annotations.ThreadSafe;
import org.carrot2.util.attribute.AttributeBinder;
import org.carrot2.util.attribute.AttributeBinder.AllAnnotationsPresentPredicate;
import org.carrot2.util.attribute.AttributeBinder.BindingTracker;
import org.carrot2.util.attribute.AttributeBinder.IAttributeBinderAction;
import org.carrot2.util.attribute.AttributeBindingException;
import org.carrot2.util.attribute.BindableUtils;
import org.carrot2.util.attribute.Input;
import org.carrot2.util.attribute.Output;
import org.carrot2.util.pool.IActivationListener;
import org.carrot2.util.pool.IDisposalListener;
import org.carrot2.util.pool.IInstantiationListener;
import org.carrot2.util.pool.IParameterizedPool;
import org.carrot2.util.pool.IPassivationListener;
import org.carrot2.util.pool.SoftUnboundedPool;
import org.carrot2.util.resource.IResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.carrot2.shaded.guava.common.base.Predicate;
import org.carrot2.shaded.guava.common.collect.ImmutableSet;
import org.carrot2.shaded.guava.common.collect.Maps;
/**
* An {@link IProcessingComponentManager} that pools instances of processing components.
*/
public class PoolingProcessingComponentManager implements IProcessingComponentManager
{
/** Controller context */
private IControllerContext context;
/** Pool for component instances. */
private volatile IParameterizedPool componentPool;
/** A copy of init attributes */
private Map initAttributes;
/** Component configurations, may be empty. */
private Map componentIdToConfiguration;
/**
* Values of {@link Init} {@link Output} attributes collected during initialization.
* As the {@link #prepare(Class, String, Map, Map)} method is expected to return these
* and we are borrowing instances from the pool, we need to keep track of
* initialization results in the ComponentInstantiationListener and pass them back in
* the create() method.
*/
private ConcurrentHashMap, String>, Map> initOutputAttributes = new ConcurrentHashMap, String>, Map>();
/**
* Creates a new {@link PoolingProcessingComponentManager} using the default pool
* implementation {@link SoftUnboundedPool}).
*/
public PoolingProcessingComponentManager()
{
this(new SoftUnboundedPool());
}
/**
* Creates a new {@link PoolingProcessingComponentManager} with a custom pool
* implementation.
*
* @param componentPool the pool to be used by this manager
*/
public PoolingProcessingComponentManager(
IParameterizedPool componentPool)
{
this.componentPool = componentPool;
final ComponentResetListener componentResetListener = new ComponentResetListener();
componentPool.init(new ComponentInstantiationListener(), componentResetListener,
componentResetListener, ComponentDisposalListener.INSTANCE);
}
@Override
public void init(IControllerContext context, Map attributes,
ProcessingComponentConfiguration... configurations)
{
assert context != null;
// This will ensure that one manager is used with only one controller
if (this.context != null)
{
throw new IllegalStateException("This manager has already been initialized.");
}
this.context = context;
this.initAttributes = Maps.newHashMap(attributes);
this.componentIdToConfiguration = ProcessingComponentConfiguration
.indexByComponentId(configurations);
}
@Override
public IProcessingComponent prepare(Class extends IProcessingComponent> clazz,
String id, Map inputAttributes,
Map outputAttributes)
{
try
{
final IProcessingComponent component = componentPool.borrowObject(clazz, id);
// Pass @Init @Output attributes back to the caller.
final Map initOutputAttrs = initOutputAttributes
.get(new Pair, String>(component
.getClass(), id));
if (initOutputAttrs != null)
{
outputAttributes.putAll(initOutputAttrs);
}
return component;
}
catch (final InstantiationException e)
{
throw new ComponentInitializationException(
"Could not instantiate component class: " + clazz.getName(), e);
}
catch (final IllegalAccessException e)
{
throw new ComponentInitializationException(
"Could not instantiate component class: " + clazz.getName(), e);
}
}
@Override
public void recycle(IProcessingComponent component, String id)
{
componentPool.returnObject(component, id);
}
@Override
public void dispose()
{
componentPool.dispose();
}
/**
* Initializes newly created component instances, remembers attribute values so that
* they can be reset after the component gets returned to the pool.
*/
private final class ComponentInstantiationListener implements
IInstantiationListener
{
public void objectInstantiated(IProcessingComponent component, String parameter)
{
try
{
final Map initAttrs = Maps.newHashMap(initAttributes);
if (parameter != null)
{
initAttrs
.putAll(componentIdToConfiguration.get(parameter).attributes);
}
final Map initOutputAttrs = Maps.newHashMap();
checkNonPrimitiveInstances(component, initAttrs,
new AllAnnotationsPresentPredicate(Input.class, Init.class));
ControllerUtils.init(component, initAttrs, initOutputAttrs, false,
context);
// Capture @Init @Output attributes
initOutputAttributes.putIfAbsent(
new Pair, String>(component
.getClass(), parameter),
Collections.unmodifiableMap(Maps.newHashMap(initOutputAttrs)));
// To support a very natural scenario where processing attributes are
// provided/overridden during initialization, we'll also bind processing
// attributes here. We need to switch off checking for required attributes
// as the required processing attributes will be most likely provided at
// request time. We explicitly skip @Init @Processing attributes here as
// @Init attributes have already been bound above.
try
{
final Predicate predicate = new Predicate()
{
public boolean apply(Field field)
{
return field.getAnnotation(Input.class) != null
&& (field.getAnnotation(Processing.class) != null && field
.getAnnotation(Init.class) == null);
}
};
checkNonPrimitiveInstances(component, initAttrs, predicate);
AttributeBinder.set(component, initAttrs, false, predicate);
}
catch (AttributeBindingException e)
{
throw new ComponentInitializationException(
"Could not initialize component", e);
}
catch (InstantiationException e)
{
throw new ComponentInitializationException(
"Could not initialize component", e);
}
}
catch (Exception e)
{
// If init() throws any exception, this exception will
// be propagated to the borrowObject() call.
component.dispose();
throw ExceptionUtils.wrapAs(ComponentInitializationException.class, e);
}
}
/**
* Performs safety checks aimed at reporting attempts to set non-primitive
* non-thread-safe primitive instances during initialization. These may lead to
* hard-to-trace bugs.
*
* @see "https://issues.apache.org/jira/browse/SOLR-2282"
*/
void checkNonPrimitiveInstances(IProcessingComponent processingComponent,
Map inputAttributes, Predicate predicate)
throws InstantiationException
{
AttributeBinder.bind(processingComponent, new IAttributeBinderAction []
{
new NonPrimitiveInputAttributesCheck(inputAttributes)
}, predicate);
}
}
/**
* An {@link IAttributeBinderAction} that checks for non-primitive instances passed
* for binding at init time. If they are not declared {@link ThreadSafe}, an info is
* logged.
*/
static final class NonPrimitiveInputAttributesCheck implements IAttributeBinderAction
{
static boolean makeAssertion = false;
private static final Logger log = LoggerFactory
.getLogger(NonPrimitiveInputAttributesCheck.class);
static final Set> ALLOWED_PLAIN_TYPES = ImmutableSet.> of(
Boolean.class, Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class, Character.class, File.class, String.class,
Calendar.class, Date.class);
static final Set> ALLOWED_ASSIGNABLE_TYPES = ImmutableSet.> of(
Enum.class, IResource.class, Collection.class, Map.class);
// A number of safe typically used classes
static final Set ALLOWED_PLAIN_TYPES_BY_NAME = ImmutableSet.of(
"org.apache.lucene.store.FSDirectory",
"org.apache.lucene.store.RAMDirectory",
"org.apache.lucene.store.MMapDirectory",
"org.apache.lucene.store.SimpleFSDirectory",
"org.apache.lucene.store.SimpleFSDirectory");
private final Map values;
NonPrimitiveInputAttributesCheck(Map values)
{
this.values = values;
}
@Override
public void performAction(BindingTracker bindingTracker, int level,
Object object, Field field, Object fieldValue, Predicate predicate)
throws InstantiationException
{
final String key = BindableUtils.getKey(field);
final Object value = values.get(key);
if (value == null || Class.class.isInstance(value)
|| Proxy.isProxyClass(value.getClass())
|| value.getClass().getAnnotation(ThreadSafe.class) != null)
{
return;
}
// If there's an @Init @Input non-primitive attribute whose type
// is not declared as @ThreadSafe, log a warning.
final Class> valueType = value.getClass();
if (!ALLOWED_PLAIN_TYPES.contains(valueType)
&& !ALLOWED_PLAIN_TYPES_BY_NAME.contains(valueType.getName())
&& !isAllowedAssignableType(valueType))
{
log.info("An object of a non-@ThreadSafe class " + valueType.getName()
+ " bound at initialization-time to attribute " + key
+ ". Make sure this is intended.");
if (makeAssertion)
{
assert false : "An object of a non-@ThreadSafe class "
+ valueType.getName()
+ " bound at initialization-time to attribute " + key
+ ". Make sure this intended.";
}
}
}
private static boolean isAllowedAssignableType(Class> attributeType)
{
for (Class> clazz : ALLOWED_ASSIGNABLE_TYPES)
{
if (clazz.isAssignableFrom(attributeType))
{
return true;
}
}
return false;
}
}
/**
* Disposes of components on shut down.
*/
private final static class ComponentDisposalListener implements
IDisposalListener
{
final static ComponentDisposalListener INSTANCE = new ComponentDisposalListener();
public void dispose(IProcessingComponent component, String parameter)
{
component.dispose();
}
}
/**
* Resets {@link Processing} attribute values before the component is returned to the
* pool.
*/
private static final class ComponentResetListener implements
IPassivationListener,
IActivationListener
{
/**
* Stores values of {@link Processing} attributes for the duration of processing.
*/
private ConcurrentHashMap> resetValues = new ConcurrentHashMap>();
public void activate(IProcessingComponent processingComponent, String parameter)
{
// Remember values of @Input @Processing attributes
final Map originalValues = Maps.newHashMap();
try
{
AttributeBinder.get(processingComponent, originalValues, Input.class,
Processing.class);
resetValues.put(new ReferenceEquality(processingComponent),
originalValues);
}
catch (Exception e)
{
throw new ProcessingException("Could not unbind attribute values", e);
}
}
public void passivate(IProcessingComponent processingComponent, String parameter)
{
// Reset values of @Input @Processing attributes back to original values
try
{
// Here's a little hack: we need to disable checking
// for required attributes, otherwise, we won't be able
// to reset @Required input attributes to null
final Map originalAttributes = resetValues
.get(new ReferenceEquality(processingComponent));
if (originalAttributes != null) {
AttributeBinder.set(processingComponent, originalAttributes, false,
Input.class, Processing.class);
}
}
catch (Exception e)
{
throw new ProcessingException("Could not reset attribute values", e);
}
}
}
}