diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /chain |
Publish
Diffstat (limited to 'chain')
33 files changed, 2071 insertions, 0 deletions
diff --git a/chain/.gitignore b/chain/.gitignore new file mode 100644 index 00000000000..c0dd71fac68 --- /dev/null +++ b/chain/.gitignore @@ -0,0 +1,4 @@ +chain.iml +target + +/pom.xml.build diff --git a/chain/OWNERS b/chain/OWNERS new file mode 100644 index 00000000000..34deebf8e0b --- /dev/null +++ b/chain/OWNERS @@ -0,0 +1,2 @@ +gjoranv +bratseth diff --git a/chain/pom.xml b/chain/pom.xml new file mode 100755 index 00000000000..fe83d2bfe74 --- /dev/null +++ b/chain/pom.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.yahoo.vespa</groupId> + <artifactId>parent</artifactId> + <version>6-SNAPSHOT</version> + <relativePath>../parent/pom.xml</relativePath> + </parent> + <artifactId>chain</artifactId> + <packaging>jar</packaging> + <version>6-SNAPSHOT</version> + <dependencies> + <dependency> + <groupId>com.google.code.findbugs</groupId> + <artifactId>annotations</artifactId> + </dependency> + <dependency> + <groupId>com.google.code.findbugs</groupId> + <artifactId>jsr305</artifactId> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>annotations</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>provided-dependencies</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>container-di</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>component</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>config-bundle</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>vespajlib</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>net.jcip</groupId> + <artifactId>jcip-annotations</artifactId> + <version>1.0</version> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>com.yahoo.vespa</groupId> + <artifactId>config-class-plugin</artifactId> + <version>${project.version}</version> + <executions> + <execution> + <goals> + <goal>config-gen</goal> + </goals> + </execution> + <execution> + <id>configgen-test-defs</id> + <phase>generate-test-sources</phase> + <goals> + <goal>config-gen</goal> + </goals> + <configuration> + <defFilesDirectories>src/test/vespa-configdef</defFilesDirectories> + <outputDirectory>target/generated-test-sources/vespa-configgen-plugin</outputDirectory> + <testConfig>true</testConfig> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/chain/src/main/java/com/yahoo/component/chain/Chain.java b/chain/src/main/java/com/yahoo/component/chain/Chain.java new file mode 100644 index 00000000000..e067b934f03 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/Chain.java @@ -0,0 +1,131 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain; + +import com.google.common.collect.ImmutableList; +import com.yahoo.component.ComponentId; +import com.yahoo.component.chain.dependencies.ordering.ChainBuilder; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * An immutable ordered list of components + * + * @author tonytv + */ +public class Chain<COMPONENT extends ChainedComponent> { + final private List<COMPONENT> componentList; + private final ComponentId id; + + /** Create a chain directly. This will NOT order the chain by the ordering constraints. */ + public Chain(String id, List<COMPONENT> componentList) { + this(new ComponentId(id), componentList); + } + + /** Create a chain directly. This will NOT order the chain by the ordering constraints. */ + public Chain(ComponentId id, List<COMPONENT> componentList) { + this.id = id; + this.componentList = ImmutableList.copyOf(componentList); + } + + /** Create a chain directly. This will NOT order the chain by the ordering constraints. */ + public Chain(List<COMPONENT> componentList) { + this(new ComponentId("anonymous chain"), componentList); + } + + /** Create a chain directly. This will NOT order the chain by the ordering constraints. */ + @SafeVarargs + public Chain(COMPONENT... components) { + this("anonymous chain", components); + } + + /** Create a chain directly. This will NOT order the chain by the ordering constraints. */ + @SafeVarargs + public Chain(String id, COMPONENT... components) { + this(new ComponentId(id), components); + } + + /** Create a chain directly. This will NOT order the chain by the ordering constraints. */ + @SafeVarargs + public Chain(ComponentId id, COMPONENT... components) { + this(id, Arrays.<COMPONENT>asList(components)); + } + + /** Create a chain by using a builder. This will order the chain by the ordering constraints. */ + public Chain(ComponentId id, Collection<COMPONENT> components, Collection<Phase> phases) { + this(id, buildChain( + emptyListIfNull(components), + emptyListIfNull(phases)).components()); + + } + + public ComponentId getId() { + return id; + } + + private static <T> Collection<T> emptyListIfNull(Collection<T> collection) { + return collection == null ? Collections.<T>emptyList() : collection; + } + + private static <T extends ChainedComponent> Chain<T> buildChain(Collection<T> components, Collection<Phase> phases) { + ChainBuilder<T> builder = new ChainBuilder<>(new ComponentId("temp")); + for (Phase phase : phases) { + builder.addPhase(phase); + } + + for (T component : components) { + builder.addComponent(component); + } + + return builder.orderNodes(); + } + + public List<COMPONENT> components() { + return componentList; + } + + public + @Override + String toString() { + StringBuilder b = new StringBuilder("chain '"); + b.append(getId().stringValue()); + b.append("' ["); + appendComponent(0, b); + appendComponent(1, b); + if (components().size() > 3) + b.append("... -> "); + if (components().size() > 2) + appendComponent(components().size() - 1, b); + b.append("]"); + return b.toString(); + } + + private void appendComponent(int i, StringBuilder b) { + if (i >= components().size()) return; + b.append(components().get(i).getId().stringValue()); + if (i < components().size() - 1) + b.append(" -> "); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Chain chain = (Chain) o; + + if (!componentList.equals(chain.componentList)) return false; + if (!id.equals(chain.id)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = componentList.hashCode(); + result = 31 * result + id.hashCode(); + return result; + } +} diff --git a/chain/src/main/java/com/yahoo/component/chain/ChainedComponent.java b/chain/src/main/java/com/yahoo/component/chain/ChainedComponent.java new file mode 100644 index 00000000000..3a31b07adb7 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/ChainedComponent.java @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain; + +import com.yahoo.component.AbstractComponent; +import com.yahoo.component.ComponentId; +import com.yahoo.component.chain.dependencies.After; +import com.yahoo.component.chain.dependencies.Before; +import com.yahoo.component.chain.dependencies.Dependencies; +import com.yahoo.component.chain.dependencies.Provides; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Component with dependencies. + * + * @author tonytv + */ +public abstract class ChainedComponent extends AbstractComponent { + + /** The immutable set of dependencies of this. NOTE: the default is only for unit testing. */ + private Dependencies dependencies = getDefaultAnnotatedDependencies(); + + public ChainedComponent(ComponentId id) { + super(id); + } + + protected ChainedComponent() {} + + /** + * Called by the container to assign the full set of dependencies to this class (configured and declared). + * This is called once before this is started. + * @param dependencies The configured dependencies, that this method will merge with annotated dependencies. + */ + public void initDependencies(Dependencies dependencies) { + this.dependencies = dependencies.union(getDefaultAnnotatedDependencies()); + } + + /** Returns the configured and declared dependencies of this chainedcomponent */ + public Dependencies getDependencies() { return dependencies; } + + /** This method is here only for legacy reasons, do not override. */ + protected Dependencies getDefaultAnnotatedDependencies() { + Dependencies dependencies = getAnnotatedDependencies(com.yahoo.yolean.chain.Provides.class, com.yahoo.yolean.chain.Before.class, com.yahoo.yolean.chain.After.class); + Dependencies legacyDependencies = getAnnotatedDependencies(Provides.class, Before.class, After.class); + + return dependencies.union(legacyDependencies); + } + + /** + * @param providesClass The annotation class representing 'provides'. + * @param beforeClass The annotation class representing 'before'. + * @param afterClass The annotation class representing 'after'. + * @return a new {@link Dependencies} created from the annotations given in this component's class. + */ + protected Dependencies getAnnotatedDependencies(Class<? extends Annotation> providesClass, + Class<? extends Annotation> beforeClass, + Class<? extends Annotation> afterClass) { + return new Dependencies( + allOf(getSymbols(this, providesClass), this.getClass().getSimpleName(), this.getClass().getName()), + getSymbols(this, beforeClass), + getSymbols(this, afterClass)); + } + + // TODO: move to vespajlib. + private static List<String> allOf(List<String> symbols, String... otherSymbols) { + List<String> result = new ArrayList<>(symbols); + result.addAll(Arrays.asList(otherSymbols)); + return result; + } + + + private static List<String> getSymbols(ChainedComponent component, Class<? extends Annotation> annotationClass) { + List<String> result = new ArrayList<>(); + + result.addAll(annotationSymbols(component, annotationClass)); + return result; + } + + private static Collection<String> annotationSymbols(ChainedComponent component, Class<? extends Annotation> annotationClass) { + + try { + Annotation annotation = component.getClass().getAnnotation(annotationClass); + if (annotation != null) { + Object values = annotationClass.getMethod("value").invoke(annotation); + return Arrays.asList((String[])values); + } + return Collections.emptyList(); + + } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/ChainsConfigurer.java b/chain/src/main/java/com/yahoo/component/chain/ChainsConfigurer.java new file mode 100644 index 00000000000..4c55c061f18 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/ChainsConfigurer.java @@ -0,0 +1,84 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain; + +import com.yahoo.component.AbstractComponent; +import com.yahoo.component.ComponentSpecification; +import com.yahoo.component.chain.model.ChainSpecification; +import com.yahoo.component.chain.model.ChainedComponentModel; +import com.yahoo.component.chain.model.ChainsModel; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.config.ConfigurationRuntimeException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Configures a registry of chains. + * + * @author bratseth + * @author gjoranv + */ +public class ChainsConfigurer { + + public static <COMPONENT extends ChainedComponent> void prepareChainRegistry( + ComponentRegistry<Chain<COMPONENT>> registry, + ChainsModel model, + ComponentRegistry<COMPONENT> allComponents) { + + initDependencies(model, allComponents); + instantiateChains(registry, model, allComponents); + } + + private static <COMPONENT extends ChainedComponent> void initDependencies( + ChainsModel model, + ComponentRegistry<COMPONENT> allComponents) { + + for (ChainedComponentModel componentModel : model.allComponents()) { + COMPONENT component = getComponentOrThrow(allComponents, componentModel.getComponentId().toSpecification()); + component.initDependencies(componentModel.dependencies); + } + } + + private static <COMPONENT extends ChainedComponent> COMPONENT getComponentOrThrow( + ComponentRegistry<COMPONENT> registry, + ComponentSpecification specification) { + + COMPONENT component = registry.getComponent(specification); + if (component == null) { + throw new ConfigurationRuntimeException("No such component '" + specification + "'"); + } + + return component; + } + + private static <COMPONENT extends ChainedComponent> void instantiateChains( + ComponentRegistry<Chain<COMPONENT>> chainRegistry, + ChainsModel model, + ComponentRegistry<COMPONENT> allComponents) { + + for (ChainSpecification chain : model.allChainsFlattened()) { + try { + Chain<COMPONENT> componentChain = new Chain<>(chain.componentId, + resolveComponents(chain.componentReferences, allComponents), + chain.phases()); + chainRegistry.register(chain.componentId, componentChain); + } catch (Exception e) { + throw new ConfigurationRuntimeException("Invalid chain '" + chain.componentId + "'", e); + } + } + } + + private static <T extends ChainedComponent> List<T> resolveComponents( + Set<ComponentSpecification> componentSpecifications, + ComponentRegistry<T> allComponents) { + + List<T> components = new ArrayList<>(componentSpecifications.size()); + for (ComponentSpecification componentSpec : componentSpecifications) { + T component = getComponentOrThrow(allComponents, componentSpec); + components.add(component); + } + return components; + } + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/Phase.java b/chain/src/main/java/com/yahoo/component/chain/Phase.java new file mode 100644 index 00000000000..fcc1255a5cc --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/Phase.java @@ -0,0 +1,52 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain; + +import com.yahoo.component.chain.dependencies.Dependencies; +import net.jcip.annotations.Immutable; + +import java.util.Set; +import java.util.TreeSet; + +/** + * Used for many to many constraints on searcher ordering. + * + * @author tonytv + */ +@Immutable +public class Phase { + public final Dependencies dependencies; + + public Phase(String name, Set<String> before, Set<String> after) { + dependencies = new Dependencies(provides(name), before, after); + } + + public Phase(String name, Dependencies dependencies) { + this(name, dependencies.before(), dependencies.after()); + assert(dependencies.provides().isEmpty()); + } + + private Set<String> provides(String name) { + Set<String> provides = new TreeSet<>(); + provides.add(name); + return provides; + } + + public String getName() { + return dependencies.provides().iterator().next(); + } + + public Set<String> before() { + return dependencies.before(); + } + + public Set<String> after() { + return dependencies.after(); + } + + public Phase union(Phase phase) { + assert(getName().equals(phase.getName())); + + Dependencies union = dependencies.union(phase.dependencies); + return new Phase(getName(), union.before(), union.after()); + } +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/After.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/After.java new file mode 100644 index 00000000000..6b3c1b43585 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/After.java @@ -0,0 +1,20 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies; + +import java.lang.annotation.*; + +/** + * Components or phases providing names contained in this list must be + * placed earlier in the chain than the component that is annotated. + * <p> + * See {@link com.yahoo.component.chain.dependencies.ordering.ChainBuilder} + * for dependency handling information. + * + * @author tonytv + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface After { + public abstract String[] value() default {}; +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/Before.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/Before.java new file mode 100644 index 00000000000..ebf0c2832f7 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/Before.java @@ -0,0 +1,20 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies; + +import java.lang.annotation.*; + +/** + * Components or phases providing names contained in this list must be + * placed later in the chain than the component that is annotated. + * <p> + * See {@link com.yahoo.component.chain.dependencies.ordering.ChainBuilder} + * for dependency handling information. + * + * @author tonytv + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Before { + public abstract String[] value() default {}; +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/Dependencies.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/Dependencies.java new file mode 100644 index 00000000000..e39d7a5c56e --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/Dependencies.java @@ -0,0 +1,73 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies; + +import java.util.*; + +import com.google.common.collect.ImmutableSet; +import net.jcip.annotations.Immutable; + +/** + * Constraints for ordering ChainedComponents in chains. + * + * @author tonytv + */ +@Immutable +public class Dependencies { + + private final Set<String> provides; + private final Set<String> before; + private final Set<String> after; + + /** + * Create from collections of strings, typically from config. + */ + public Dependencies(Collection<String> provides, Collection<String> before, Collection<String> after) { + this.provides = immutableSet(provides); + this.before = immutableSet(before); + this.after = immutableSet(after); + } + + public static Dependencies emptyDependencies() { + return new Dependencies(null, null, null); + } + + public Dependencies union(Dependencies dependencies) { + return new Dependencies( + union(provides, dependencies.provides), + union(before, dependencies.before), + union(after, dependencies.after)); + } + + private Set<String> immutableSet(Collection<String> set) { + if (set == null) return ImmutableSet.of(); + return ImmutableSet.copyOf(set); + } + + private Set<String> union(Set<String> s1, Set<String> s2) { + Set<String> result = new LinkedHashSet<>(s1); + result.addAll(s2); + return result; + } + + @Override + public String toString() { + return "Dependencies{" + + "provides=" + provides + + ", before=" + before + + ", after=" + after + + '}'; + } + + public Set<String> provides() { + return provides; + } + + public Set<String> before() { + return before; + } + + public Set<String> after() { + return after; + } + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/Provides.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/Provides.java new file mode 100644 index 00000000000..deb3096862e --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/Provides.java @@ -0,0 +1,18 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies; + +import java.lang.annotation.*; + +/** + * Mark this component as providing some named functionality. + * Other components can then mark themselves as "before" and "after" the string provided here, + * to impose constraints on ordering. + * + * @author tonytv + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Provides { + public abstract String[] value() default {}; +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilder.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilder.java new file mode 100644 index 00000000000..4d36dc53ca9 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilder.java @@ -0,0 +1,169 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.yahoo.component.ComponentId; +import com.yahoo.component.chain.Chain; +import com.yahoo.component.chain.ChainedComponent; +import com.yahoo.component.chain.Phase; + + +/** + * Given a set of phases and a set of components, + * a ordered list of components satisfying the dependencies is given if possible. + * <p> + * The phase list implicitly defines the ordering: + * {@literal if i < j : p_i before p_j where i,j are valid indexes of the phrase list p.} + * <p> + * If multiple components provide the same name, ALL the components providing + * the same name must be placed earlier/later than an entity depending on + * that name. + * <p> + * A warning will be logged if multiple components of different types provides the + * same name. A component can not provide the same name as a phase. + * + * @author tonytv + */ +public class ChainBuilder<T extends ChainedComponent> { + private final ComponentId id; + private int numComponents = 0; + private int priority = 1; + + private Map<String, NameProvider> nameProviders = + new LinkedHashMap<>(); + + private Node allPhase; + + public ChainBuilder(ComponentId id) { + this.id = id; + allPhase = addPhase(new Phase("*", set("*"), Collections.<String>emptySet())); + } + + private Set<String> set(String... s) { + return new HashSet<>(Arrays.asList(s)); + } + + public PhaseNameProvider addPhase(Phase phase) { + NameProvider nameProvider = nameProviders.get(phase.getName()); + if (nameProvider instanceof ComponentNameProvider) { + throw new ConflictingNodeTypeException("Cannot add phase '" + phase.getName() + "' as it is already provided by " + nameProvider); + } + PhaseNameProvider phaseNameProvider; + if(nameProvider == null) { + phaseNameProvider = new PhaseNameProvider(phase.getName(), priority++); + } else { + phaseNameProvider = (PhaseNameProvider) nameProvider; + } + nameProviders.put(phase.getName(), phaseNameProvider); + for(String before : phase.before()) { + phaseNameProvider.before(getPhaseNameProvider(before)); + } + for(String after : phase.after()) { + getPhaseNameProvider(after).before(phaseNameProvider); + } + + return phaseNameProvider; + } + + public void addComponent(ChainedComponent component) { + ComponentNode<ChainedComponent> componentNode = new ComponentNode<>(component, priority++); + + ensureProvidesNotEmpty(component); + for (String name : component.getDependencies().provides()) { + NameProvider nameProvider = getNameProvider(name); + + nameProvider.addNode(componentNode); + } + + for (String before : component.getDependencies().before()) { + componentNode.before(getNameProvider(before)); + } + + for (String after : component.getDependencies().after()) { + getNameProvider(after).before(componentNode); + } + + ++numComponents; + } + + //destroys this dependency handler in the process + @SuppressWarnings("unchecked") + public Chain<T> orderNodes() { + List<T> chain = new ArrayList<>(); + OrderedReadyNodes readyNodes = getReadyNodes(); + + while (!readyNodes.isEmpty() || popAllPhase(readyNodes) ) { + Node candidate = readyNodes.pop(); + + candidate.removed(readyNodes); + + if ( candidate instanceof ComponentNode) + chain.add(((ComponentNode<T>)candidate).getComponent()); + } + + if ( chain.size() != numComponents) + throw new CycleDependenciesException(nameProviders); + + //prevent accidental reuse + nameProviders = null; + + return new Chain<>(id, chain); + } + + private void ensureProvidesNotEmpty(ChainedComponent component) { + if (component.getDependencies().provides().isEmpty()) { + throw new RuntimeException("The component " + component.getId() + " did not provide anything."); + } + } + + private Node getPhaseNameProvider(String name) { + NameProvider nameProvider = nameProviders.get(name); + if (nameProvider != null) + return nameProvider; + else { + nameProvider = new PhaseNameProvider(name, priority++); + nameProviders.put(name, nameProvider); + return nameProvider; + } + } + + private boolean popAllPhase(OrderedReadyNodes readyNodes) { + if (allPhase == null) { + return false; + } else { + Node phase = allPhase; + allPhase = null; + phase.removed(readyNodes); + return !readyNodes.isEmpty(); + } + } + + private NameProvider getNameProvider(String name) { + NameProvider nameProvider = nameProviders.get(name); + if (nameProvider != null) + return nameProvider; + else { + nameProvider = new ComponentNameProvider(name); + nameProviders.put(name, nameProvider); + return nameProvider; + } + } + + private OrderedReadyNodes getReadyNodes() { + OrderedReadyNodes readyNodes = new OrderedReadyNodes(); + for (Node node : nameProviders.values() ) { + if (node.ready()) + readyNodes.add(node); + } + return readyNodes; + } +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNameProvider.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNameProvider.java new file mode 100644 index 00000000000..77e492395ce --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNameProvider.java @@ -0,0 +1,63 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.logging.Logger; + +import com.yahoo.component.chain.ChainedComponent; + +/** + * A set of components providing a given name. + * + * @author tonytv + */ +class ComponentNameProvider extends NameProvider { + + @SuppressWarnings("rawtypes") + private Set<ComponentNode> nodes = new LinkedHashSet<>(); + private Logger logger = Logger.getLogger(getClass().getName()); + + ComponentNameProvider(String name) { + super(name, 0); + } + + protected void addNode(@SuppressWarnings("rawtypes") ComponentNode componentNode) { + if (nodes.add(componentNode)) + componentNode.notifyAfter(); + } + + @Override + protected void handleRemoved(OrderedReadyNodes readyNodes) { + for (Node node: nodes) { + /* + All providers must be run before dependencies are run. + Adding these dependencies just in time improves dot output + for the purpose of finding cycles manually. + */ + for (Node afterThis : nodesAfterThis) { + node.before(afterThis); + } + node.beforeRemoved(readyNodes); + } + } + + @Override + int classPriority() { + return 1; + } + + public @Override String toString() { + StringBuilder b=new StringBuilder("components ["); + for (@SuppressWarnings("rawtypes") + Iterator<ComponentNode> i=nodes.iterator(); i.hasNext(); ) { + b.append(i.next().getComponent().getId()); + if (i.hasNext()) + b.append(", "); + } + b.append("]"); + return b.toString(); + } + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNode.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNode.java new file mode 100644 index 00000000000..f6b62aec741 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNode.java @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + +import com.yahoo.component.chain.ChainedComponent; + +/** + * A node representing a given component. + * + * @see Node + * @author tonytv + */ +class ComponentNode<T extends ChainedComponent> extends Node { + private T component; + + public ComponentNode(T component, int priority) { + super(priority); + this.component = component; + } + + T getComponent() { + return component; + } + + @Override + protected String dotName() { + //TODO: config dependent name + return component.getClass().getSimpleName(); + } + + @Override + int classPriority() { + return 2; + } +} + diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ConflictingNodeTypeException.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ConflictingNodeTypeException.java new file mode 100644 index 00000000000..75b4025eef0 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ConflictingNodeTypeException.java @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + +/** + * Thrown if a searcher provides the same name as a phase. + * + * @author tonytv + */ +@SuppressWarnings("serial") +public class ConflictingNodeTypeException extends RuntimeException { + + public ConflictingNodeTypeException(String message) { + super(message); + } + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/CycleDependenciesException.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/CycleDependenciesException.java new file mode 100644 index 00000000000..09f8d7eb914 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/CycleDependenciesException.java @@ -0,0 +1,45 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Signals that the dependency graph contains cycles. A DOT language + * representation of the cycle is available to help solve the problem (<a + * href="http://graphviz.org/">GraphViz</a>). + * + * @author tonytv + */ +@SuppressWarnings("serial") +public class CycleDependenciesException extends RuntimeException { + public Map<String, NameProvider> cycleNodes; + + CycleDependenciesException(Map<String, NameProvider> cycleNodes) { + super("The following set of dependencies lead to a cycle:\n" + + createDotString(cycleNodes)); + this.cycleNodes = cycleNodes; + } + + private static String createDotString(Map<String, NameProvider> cycleNodes) { + StringBuilder res = new StringBuilder(); + res.append("digraph dependencyGraph {\n"); + + Set<Node> used = new HashSet<>(); + for (Node node: cycleNodes.values()) { + if (!node.ready()) + node.dotDependenciesString(res, used); + + } + res.append("}"); + return res.toString(); + } + + + public String dotString() { + return createDotString(cycleNodes); + } + + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/NameProvider.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/NameProvider.java new file mode 100644 index 00000000000..d914fa489e9 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/NameProvider.java @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + +/** + * A node containing nodes providing a given name. + * + * @author tonytv + */ +abstract class NameProvider extends Node { + final String name; + + public NameProvider(String name, int priority) { + super(priority); + this.name = name; + } + + protected abstract void addNode(ComponentNode<?> node); + + protected String name() { + return name; + } + + @Override + protected String dotName() { + return name; + } +} + + diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/Node.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/Node.java new file mode 100644 index 00000000000..7d2a7e112e4 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/Node.java @@ -0,0 +1,85 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + +import java.util.HashSet; +import java.util.Set; + +/** + * A node in a dependency graph. + * + * Dependencies must declared as follows: + * a.before(b) , where a,b are nodes. + * + * The following dependencies are currently allowed: + * searcher.before(name) + * name.before(searcher) + * searcher1.before(searcher2) + * + * Where name designates a NameProvider( either a phase or a set of searchers). + * + * @author tonytv +*/ +abstract class Node { + //How this node should be prioritized if its compared with a node of the same class, see class priority. + final int priority; + + private int numNodesBeforeThis = 0; + Set<Node> nodesAfterThis = new HashSet<>(); + + public Node(int priority) { + this.priority = priority; + } + + protected void before(Node node) { + if (nodesAfterThis.add(node)) { + node.notifyAfter(); + } + } + + void notifyAfter() { + ++numNodesBeforeThis; + } + + void removed(OrderedReadyNodes readyNodes) { + handleRemoved(readyNodes); + for (Node node: nodesAfterThis) { + node.beforeRemoved(readyNodes); + } + } + + void beforeRemoved(OrderedReadyNodes readyNodes) { + --numNodesBeforeThis; + + if (ready()) { + readyNodes.add(this); + } + } + + boolean ready() { + return numNodesBeforeThis == 0; + } + + protected void handleRemoved(OrderedReadyNodes readyNodes) {} + + void dotDependenciesString(StringBuilder s, Set<Node> used) { + if (used.contains(this)) + return; + used.add(this); + + for (Node afterNode : nodesAfterThis) { + String indent = " "; + s.append(indent); + s.append(dotName()).append(" -> ").append(afterNode.dotName()) + .append('\n'); + afterNode.dotDependenciesString(s, used); + } + } + + abstract protected String dotName(); + + /* + * Ensures that PhaseNameProviders < ComponentNameProviders < ComponentNodes + * The regular priority is only considered if the class priorities are equal. + */ + abstract int classPriority(); +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodes.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodes.java new file mode 100644 index 00000000000..f0b12e26a1b --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodes.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + + +import java.util.Comparator; +import java.util.PriorityQueue; + +/** + * Ensures that Searchers are ordered deterministically. + * + * @author tonytv + */ +class OrderedReadyNodes { + private class PriorityComparator implements Comparator<Node> { + @Override + public int compare(Node lhs, Node rhs) { + int result = new Integer(lhs.classPriority()).compareTo(rhs.classPriority()); + + return result != 0 ? + result : + new Integer(lhs.priority).compareTo(rhs.priority); + } + } + + final private PriorityQueue<Node> nodes = + new PriorityQueue<>(10, new PriorityComparator()); + + public void add(Node node) { + nodes.add(node); + } + + public Node pop() { + return nodes.poll(); + } + + public boolean isEmpty() { + return nodes.isEmpty(); + } +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/PhaseNameProvider.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/PhaseNameProvider.java new file mode 100644 index 00000000000..f72a18d5ce6 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/PhaseNameProvider.java @@ -0,0 +1,28 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + +/** + * A phase providing a given name. + * + * @author tonytv + */ +class PhaseNameProvider extends NameProvider { + public PhaseNameProvider(String name, int priority) { + super(name,priority); + } + + protected void addNode(ComponentNode<?> newNode) { + throw new ConflictingNodeTypeException("Both a phase and a searcher provides the name '" + name + "'"); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[name = " + name + "]"; + } + + + @Override + int classPriority() { + return 0; + } +} diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/package-info.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/package-info.java new file mode 100644 index 00000000000..ce64ef8ffab --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.component.chain.dependencies; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ChainSpecification.java b/chain/src/main/java/com/yahoo/component/chain/model/ChainSpecification.java new file mode 100644 index 00000000000..8a5b907abdd --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/model/ChainSpecification.java @@ -0,0 +1,221 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.model; + +import com.google.common.collect.ImmutableSet; +import com.yahoo.component.ComponentId; +import com.yahoo.component.ComponentSpecification; +import com.yahoo.component.chain.Phase; +import net.jcip.annotations.Immutable; + +import java.util.*; + +/** + * Specifies how the components should be selected to create a chain. + * + * @author tonytv + */ +@Immutable +public class ChainSpecification { + public static class Inheritance { + public final Set<ComponentSpecification> chainSpecifications; + public final Set<ComponentSpecification> excludedComponents; + + Inheritance flattened() { + return new Inheritance(Collections.<ComponentSpecification>emptySet(), excludedComponents); + } + + public Inheritance(Set<ComponentSpecification> inheritedChains, Set<ComponentSpecification> excludedComponents) { + this.chainSpecifications = immutableCopy(inheritedChains); + this.excludedComponents = immutableCopy(excludedComponents); + } + + public Inheritance addInherits(Collection<ComponentSpecification> inheritedChains) { + Set<ComponentSpecification> newChainSpecifications = + new LinkedHashSet<>(chainSpecifications); + newChainSpecifications.addAll(inheritedChains); + return new Inheritance(newChainSpecifications, excludedComponents); + } + } + + public final ComponentId componentId; + public final Inheritance inheritance; + final Map<String, Phase> phases; + public final Set<ComponentSpecification> componentReferences; + + public ChainSpecification(ComponentId componentId, Inheritance inheritance, + Collection<Phase> phases, + Set<ComponentSpecification> componentReferences) { + assertNotNull(componentId, inheritance, phases, componentReferences); + + if (componentsByName(componentReferences).size() != componentReferences.size()) + throw new RuntimeException("Two components with the same name are specified in '" + componentId + + "', but name must be unique inside a given chain."); + + this.componentId = componentId; + this.inheritance = inheritance; + this.phases = copyPhasesImmutable(phases); + this.componentReferences = ImmutableSet.copyOf( + filterByComponentSpecification(componentReferences, inheritance.excludedComponents)); + } + + public ChainSpecification addComponents(Collection<ComponentSpecification> componentSpecifications) { + Set<ComponentSpecification> newComponentReferences = new LinkedHashSet<>(componentReferences); + newComponentReferences.addAll(componentSpecifications); + + return new ChainSpecification(componentId, inheritance, phases(), newComponentReferences); + } + + public ChainSpecification addInherits(Collection<ComponentSpecification> inheritedChains) { + return new ChainSpecification(componentId, inheritance.addInherits(inheritedChains), phases(), componentReferences); + } + + public ChainSpecification setComponentId(ComponentId newComponentId) { + return new ChainSpecification(newComponentId, inheritance, phases(), componentReferences); + } + + public ChainSpecification flatten(Resolver<ChainSpecification> allChainSpecifications) { + Deque<ComponentId> path = new ArrayDeque<>(); + return flatten(allChainSpecifications, path); + } + + /** + * @param allChainSpecifications resolves ChainSpecifications from ComponentSpecifications + * as given in the inheritance fields. + * @param path tracks which chains are used in each recursive invocation of flatten, used for detecting cycles. + * @return ChainSpecification directly containing all the component references and phases of the inherited chains. + */ + private ChainSpecification flatten(Resolver<ChainSpecification> allChainSpecifications, + Deque<ComponentId> path) { + path.push(componentId); + + //if this turns out to be a bottleneck(which I seriously doubt), please add memoization + Map<String, ComponentSpecification> resultingComponents = componentsByName(componentReferences); + Map<String, Phase> resultingPhases = new LinkedHashMap<>(phases); + + + for (ComponentSpecification inheritedChainSpecification : inheritance.chainSpecifications) { + ChainSpecification inheritedChain = + resolveChain(path, allChainSpecifications, inheritedChainSpecification). + flatten(allChainSpecifications, path); + + mergeInto(resultingComponents, + filterByComponentSpecification( + filterByName(inheritedChain.componentReferences, names(componentReferences)), + inheritance.excludedComponents)); + mergeInto(resultingPhases, inheritedChain.phases); + } + + path.pop(); + return new ChainSpecification(componentId, inheritance.flattened(), resultingPhases.values(), + new LinkedHashSet<>(resultingComponents.values())); + } + + public Collection<Phase> phases() { + return phases.values(); + } + + private static <T> Set<T> immutableCopy(Set<T> set) { + if (set == null) return ImmutableSet.of(); + return ImmutableSet.copyOf(set); + } + + private static Map<String, Phase> copyPhasesImmutable(Collection<Phase> phases) { + Map<String, Phase> result = new LinkedHashMap<>(); + for (Phase phase : phases) { + Phase oldValue = result.put(phase.getName(), phase); + if (oldValue != null) + throw new RuntimeException("Two phases with the same name " + phase.getName() + " present in the same scope."); + } + return Collections.unmodifiableMap(result); + } + + private static void assertNotNull(Object... objects) { + for (Object o : objects) { + assert(o != null); + } + } + + static Map<String, ComponentSpecification> componentsByName(Set<ComponentSpecification> componentSpecifications) { + Map<String, ComponentSpecification> componentsByName = new LinkedHashMap<>(); + + for (ComponentSpecification component : componentSpecifications) + componentsByName.put(component.getName(), component); + + return componentsByName; + } + + private static void mergeInto(Map<String, ComponentSpecification> resultingComponents, + Set<ComponentSpecification> components) { + for (ComponentSpecification component : components) { + String name = component.getName(); + if (resultingComponents.containsKey(name)) { + resultingComponents.put(name, component.intersect(resultingComponents.get(name))); + } else { + resultingComponents.put(name, component); + } + } + } + + + private static void mergeInto(Map<String, Phase> resultingPhases, Map<String, Phase> phases) { + for (Phase phase : phases.values()) { + String name = phase.getName(); + if (resultingPhases.containsKey(name)) { + phase = phase.union(resultingPhases.get(name)); + } + resultingPhases.put(name, phase); + } + } + + private static Set<String> names(Set<ComponentSpecification> components) { + Set<String> names = new LinkedHashSet<>(); + for (ComponentSpecification component : components) { + names.add(component.getName()); + } + return names; + } + + private static Set<ComponentSpecification> filterByComponentSpecification(Set<ComponentSpecification> components, Set<ComponentSpecification> excludes) { + Set<ComponentSpecification> result = new LinkedHashSet<>(); + for (ComponentSpecification component : components) { + if (!matches(component, excludes)) + result.add(component); + } + + return result; + } + + private static Set<ComponentSpecification> filterByName(Set<ComponentSpecification> components, Set<String> names) { + Set<ComponentSpecification> result = new LinkedHashSet<>(); + for (ComponentSpecification component : components) { + if (!names.contains(component.getName())) + result.add(component); + } + return result; + } + + private static boolean matches(ComponentSpecification component, Set<ComponentSpecification> excludes) { + ComponentId id = component.toId().withoutNamespace(); + for (ComponentSpecification exclude : excludes) { + if (exclude.matches(id)) { + return true; + } + } + return false; + } + + private ChainSpecification resolveChain(Deque<ComponentId> path, + Resolver<ChainSpecification> allChainSpecifications, + ComponentSpecification chainSpecification) { + + ChainSpecification chain = allChainSpecifications.resolve(chainSpecification); + if (chain == null) { + throw new RuntimeException("Missing chain '" + chainSpecification + "'."); + } else if (path.contains(chain.componentId)) { + throw new RuntimeException("The chain " + chain.componentId + " inherits(possibly indirectly) from itself."); + } else { + return chain; + } + } + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ChainedComponentModel.java b/chain/src/main/java/com/yahoo/component/chain/model/ChainedComponentModel.java new file mode 100644 index 00000000000..be94bd4973d --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/model/ChainedComponentModel.java @@ -0,0 +1,31 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.model; + +import com.yahoo.container.bundle.BundleInstantiationSpecification; +import com.yahoo.component.chain.dependencies.Dependencies; +import com.yahoo.osgi.provider.model.ComponentModel; +import net.jcip.annotations.Immutable; + +/** + * Describes how a chained component should be created. + * + * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a> + * @author tonytv + */ +@Immutable +public class ChainedComponentModel extends ComponentModel { + public final Dependencies dependencies; + + public ChainedComponentModel(BundleInstantiationSpecification bundleInstantiationSpec, Dependencies dependencies, + String configId) { + super(bundleInstantiationSpec, configId); + assert(dependencies != null); + + this.dependencies = dependencies; + } + + public ChainedComponentModel(BundleInstantiationSpecification bundleInstantiationSpec, Dependencies dependencies) { + this(bundleInstantiationSpec, dependencies, null); + } + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ChainsModel.java b/chain/src/main/java/com/yahoo/component/chain/model/ChainsModel.java new file mode 100644 index 00000000000..6b92c8899e5 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/model/ChainsModel.java @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +import com.yahoo.component.ComponentId; +import com.yahoo.component.ComponentSpecification; +import com.yahoo.component.provider.ComponentRegistry; + +/** + * A model of how the chains and components should be created. + * + * @author tonytv + */ +public class ChainsModel { + + private final ComponentRegistry<ComponentAdaptor<ChainSpecification>> chainSpecifications = new ComponentRegistry<>(); + private final ComponentRegistry<ComponentAdaptor<ChainedComponentModel>> componentModels = new ComponentRegistry<>(); + + public void register(ChainSpecification chainSpecification) { + chainSpecifications.register(chainSpecification.componentId, + ComponentAdaptor.create(chainSpecification.componentId, chainSpecification)); + } + + public void register(ComponentId globalComponentId, ChainedComponentModel componentModel) { + assert (componentModel.getComponentId().withoutNamespace().equals( + globalComponentId.withoutNamespace())); + + componentModels.register(globalComponentId, ComponentAdaptor.create(globalComponentId, componentModel)); + } + + public Collection<ChainedComponentModel> allComponents() { + Collection<ChainedComponentModel> components = new ArrayList<>(); + for (ComponentAdaptor<ChainedComponentModel> component : componentModels.allComponents()) { + components.add(component.model); + } + return components; + } + + public Collection<ChainSpecification> allChainsFlattened() { + Resolver<ChainSpecification> resolver = new Resolver<ChainSpecification>() { + @Override + public ChainSpecification resolve(ComponentSpecification componentSpecification) { + ComponentAdaptor<ChainSpecification> spec = chainSpecifications.getComponent(componentSpecification); + return (spec==null) ? null : spec.model; + } + }; + + Collection<ChainSpecification> chains = new ArrayList<>(); + for (ComponentAdaptor<ChainSpecification> chain : chainSpecifications.allComponents()) { + chains.add(chain.model.flatten(resolver)); + } + return chains; + } + + public void validate() { + allChainsFlattened(); + for (ComponentAdaptor<ChainSpecification> chain : chainSpecifications.allComponents()) { + validate(chain.model); + } + } + + private void validate(ChainSpecification model) { + for (ComponentSpecification componentSpec : model.componentReferences) { + if (componentModels.getComponent(componentSpec) == null) { + throw new RuntimeException("No component matching the component specification " + componentSpec); + } + } + } + + // For testing + Map<ComponentId, ComponentAdaptor<ChainSpecification>> chainSpecifications() { + return chainSpecifications.allComponentsById(); + } + + // For testing + Map<ComponentId, ComponentAdaptor<ChainedComponentModel>> componentModels() { + return componentModels.allComponentsById(); + } + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ChainsModelBuilder.java b/chain/src/main/java/com/yahoo/component/chain/model/ChainsModelBuilder.java new file mode 100644 index 00000000000..b656829ffbd --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/model/ChainsModelBuilder.java @@ -0,0 +1,84 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.model; + +import java.util.*; + +import com.yahoo.container.bundle.BundleInstantiationSpecification; +import com.yahoo.component.ComponentId; +import com.yahoo.component.ComponentSpecification; + +import com.yahoo.component.chain.dependencies.Dependencies; +import com.yahoo.component.chain.Phase; +import com.yahoo.container.core.ChainsConfig; + +/** + * Builds a chains model from config. + * + * @author tonytv + */ +public class ChainsModelBuilder { + + public static ChainsModel buildFromConfig(ChainsConfig chainsConfig) { + ChainsModel model = createChainsModel(chainsConfig); + + for (ChainsConfig.Components component : chainsConfig.components()) { + ChainedComponentModel componentModel = createChainedComponentModel(component); + model.register(componentModel.getComponentId(), componentModel); + } + return model; + } + + private static ChainedComponentModel createChainedComponentModel(ChainsConfig.Components component) { + return new ChainedComponentModel( + new BundleInstantiationSpecification(new ComponentSpecification(component.id()), null, null), + createDependencies( + component.dependencies().provides(), + component.dependencies().before(), + component.dependencies().after()), + null); + } + + private static ChainsModel createChainsModel(ChainsConfig chainsConfig) { + ChainsModel model = new ChainsModel(); + for (ChainsConfig.Chains chainConfig : chainsConfig.chains()) { + model.register( + createChainSpecification(chainConfig)); + } + return model; + } + + private static ChainSpecification createChainSpecification(ChainsConfig.Chains config) { + return new ChainSpecification(new ComponentId(config.id()), + createInheritance(config.inherits(), config.excludes()), + createPhases(config.phases()), + createComponentSpecifications(config.components())); + } + + private static Collection<Phase> createPhases(List<ChainsConfig.Chains.Phases> phases) { + Collection<Phase> result = new ArrayList<>(); + for (ChainsConfig.Chains.Phases phase : phases) { + result.add( + new Phase(phase.id(), createDependencies(null, phase.before(), phase.after()))); + } + return result; + } + + private static Dependencies createDependencies(List<String> provides, + List<String> before, List<String> after) { + return new Dependencies(provides, before, after); + } + + private static Set<ComponentSpecification> createComponentSpecifications(List<String> stringSpecs) { + Set<ComponentSpecification> specifications = new LinkedHashSet<>(); + for (String stringSpec : stringSpecs) { + specifications.add(new ComponentSpecification(stringSpec)); + } + return specifications; + } + + private static ChainSpecification.Inheritance createInheritance(List<String> inherit, List<String> exclude) { + return new ChainSpecification.Inheritance( + createComponentSpecifications(inherit), + createComponentSpecifications(exclude)); + } +} diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ComponentAdaptor.java b/chain/src/main/java/com/yahoo/component/chain/model/ComponentAdaptor.java new file mode 100644 index 00000000000..1aa96f1fcf3 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/model/ComponentAdaptor.java @@ -0,0 +1,31 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.model; + +import com.yahoo.component.AbstractComponent; +import com.yahoo.component.ComponentId; + +/** + * For using non-component model classes with ComponentRegistry. + * + * @author tonytv + */ +public final class ComponentAdaptor<T> extends AbstractComponent { + + public final T model; + + @SuppressWarnings("deprecation") + public ComponentAdaptor(ComponentId globalComponentId, T model) { + super(globalComponentId); + this.model = model; + } + + public static <T> ComponentAdaptor<T> create(ComponentId globalComponentId, T model) { + return new ComponentAdaptor<>(globalComponentId, model); + } + + // For testing + T model() { + return model; + } + +} diff --git a/chain/src/main/java/com/yahoo/component/chain/model/Resolver.java b/chain/src/main/java/com/yahoo/component/chain/model/Resolver.java new file mode 100644 index 00000000000..0f848cafea3 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/model/Resolver.java @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.model; + +import com.yahoo.component.ComponentSpecification; + +/** + * Maps component specifications to matching instances. + * + * @author tonytv + */ +public interface Resolver<T> { + T resolve(ComponentSpecification componentSpecification); +} diff --git a/chain/src/main/java/com/yahoo/component/chain/model/package-info.java b/chain/src/main/java/com/yahoo/component/chain/model/package-info.java new file mode 100644 index 00000000000..d1c5d5f0d76 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/model/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.component.chain.model; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/chain/src/main/java/com/yahoo/component/chain/package-info.java b/chain/src/main/java/com/yahoo/component/chain/package-info.java new file mode 100644 index 00000000000..a8b1981ef98 --- /dev/null +++ b/chain/src/main/java/com/yahoo/component/chain/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.component.chain; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/chain/src/main/resources/configdefinitions/chains.def b/chain/src/main/resources/configdefinitions/chains.def new file mode 100644 index 00000000000..c0ea1ef7d85 --- /dev/null +++ b/chain/src/main/resources/configdefinitions/chains.def @@ -0,0 +1,43 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +# Chains configuration +version=13 +namespace=container.core + +components[].id string + +# Configured functionality provided by this - comes in addition to those set in the code +components[].dependencies.provides[] string + +# Configured "before" dependencies provided by this - comes in addition to those set in the code +components[].dependencies.before[] string + +# Configured "after" dependencies provided by this - comes in addition to those set in the code +components[].dependencies.after[] string + +# The id of this chain. The id has the form name(:version)? +# where the version has the form 1(.2(.3(.identifier)?)?)?. +# The default chain must be called "default". +chains[].id string + +#The type of this chain +chains[].type enum {DOCPROC, SEARCH} default=SEARCH + +# The id of a component to include in this chain. +# The id has the form fullclassname(:version)? +# where the version has the form 1(.2(.3(.identifier)?)?)?. +chains[].components[] string + +# The optional list of chain ids this inherits. +# The ids has the form name(:version)? +# where the version has the form 1(.2(.3(.identifier)?)?)?. +# If the version is not specified the newest version is used. +chains[].inherits[] string + +# The optional list of component ids to exclude from this chain even if they exists in inherited chains +# If versions are specified in these ids, they are ignored. +chains[].excludes[] string + +# The phases for a chain +chains[].phases[].id string +chains[].phases[].before[] string +chains[].phases[].after[] string diff --git a/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilderTest.java b/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilderTest.java new file mode 100644 index 00000000000..0695dcd7cff --- /dev/null +++ b/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilderTest.java @@ -0,0 +1,248 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + +import com.yahoo.component.ComponentId; +import com.yahoo.component.chain.Chain; +import com.yahoo.component.chain.ChainedComponent; +import com.yahoo.component.chain.Phase; +import com.yahoo.component.chain.dependencies.After; +import com.yahoo.component.chain.dependencies.Before; +import com.yahoo.component.chain.dependencies.Provides; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author tonytv + * @since 5.1.10 + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class ChainBuilderTest { + + private void addAtoG(ChainBuilder chainBuilder) + throws IllegalAccessException, InstantiationException { + + List<Class<? extends ChainedComponent>> componentTypes = new ArrayList<>(); + + componentTypes.add(A.class); + componentTypes.add(B.class); + componentTypes.add(C.class); + componentTypes.add(D.class); + componentTypes.add(E.class); + componentTypes.add(F.class); + componentTypes.add(G.class); + + permute(componentTypes); + + for (Class<? extends ChainedComponent> searcherClass : componentTypes) { + chainBuilder.addComponent(searcherClass.newInstance()); + } + } + + + private void permute(List<Class<? extends ChainedComponent>> searcherTypes) { + for (int i=0; i<searcherTypes.size(); ++i) { + int j = (int) (Math.random() * searcherTypes.size()); + Class<? extends ChainedComponent> tmp = searcherTypes.get(i); + searcherTypes.set(i,searcherTypes.get(j)); + searcherTypes.set(j, tmp); + } + } + + @Test + public void testRegular() throws InstantiationException, IllegalAccessException { + ChainBuilder chainBuilder = createDependencyHandler(); + + addAtoG(chainBuilder); + + Chain<ChainedComponent> res = chainBuilder.orderNodes(); + + Iterator<ChainedComponent> i = res.components().iterator(); + for (char j=0; j< 'G' - 'A'; ++j) { + assertEquals(String.valueOf((char)('A' + j)), name(i.next())); + } + } + + @Test + public void testCycle() + throws InstantiationException, IllegalAccessException { + + ChainBuilder chainBuilder = createDependencyHandler(); + + addAtoG(chainBuilder); + chainBuilder.addComponent(new H()); + + boolean cycle = false; + try { + chainBuilder.orderNodes(); + } catch (CycleDependenciesException e) { + cycle = true; + } + assertTrue(cycle); + } + + + @Test + public void testPhaseAndSearcher() { + ChainBuilder depHandler = newChainBuilder(); + depHandler.addPhase(new Phase("phase1", set("phase2"), Collections.<String>emptySet())); + depHandler.addPhase(new Phase("phase2", set("phase3"), set("phase1"))); + depHandler.addPhase(new Phase("phase3", Collections.<String>emptySet(), set("phase2", "phase1"))); + ChainedComponent first = new First(); + ChainedComponent second = new Second(); + + depHandler.addComponent(first); + depHandler.addComponent(second); + assertEquals(depHandler.orderNodes().components(), Arrays.asList(first, second)); + + } + + @Test + public void testInputOrderPreservedWhenProvidesOverlap() { + ChainBuilder chainBuilder = newChainBuilder(); + + A a1 = new A(); + C c = new C(); + A a2 = new A(); + + chainBuilder.addComponent(a1); + chainBuilder.addComponent(c); + chainBuilder.addComponent(a2); + + assertEquals(Arrays.asList(a1, c, a2), chainBuilder.orderNodes().components()); + } + + private ChainBuilder newChainBuilder() { + return new ChainBuilder(new ComponentId("test")); + } + + private Set<String> set(String... strings) { + return new HashSet<>(Arrays.asList(strings)); + } + + @Before("phase1") + static class First extends NoopComponent { + + } + + @After("phase3") + static class Second extends NoopComponent { + + } + + @Test + public void testAfterAll1() + throws InstantiationException, IllegalAccessException { + ChainBuilder chainBuilder = createDependencyHandler(); + ChainedComponent afterAll1 = new AfterAll(); + chainBuilder.addComponent(afterAll1); + addAtoG(chainBuilder); + + List<ChainedComponent> resolution= chainBuilder.orderNodes().components(); + assertEquals(afterAll1,resolution.get(resolution.size()-1)); + } + + @Test + public void testAfterAll2() + throws InstantiationException, IllegalAccessException { + ChainBuilder chainBuilder = createDependencyHandler(); + addAtoG(chainBuilder); + ChainedComponent afterAll1 = new AfterAll(); + chainBuilder.addComponent(afterAll1); + + List<ChainedComponent> resolution = chainBuilder.orderNodes().components(); + assertEquals(afterAll1,resolution.get(resolution.size()-1)); + } + + @Test + public void testAfterImplicitProvides() + throws InstantiationException, IllegalAccessException { + ChainBuilder chainBuilder = createDependencyHandler(); + ChainedComponent afterProvidesNothing=new AfterProvidesNothing(); + ChainedComponent providesNothing=new ProvidesNothing(); + chainBuilder.addComponent(afterProvidesNothing); + chainBuilder.addComponent(providesNothing); + List<ChainedComponent> resolution = chainBuilder.orderNodes().components(); + assertEquals(providesNothing,resolution.get(0)); + assertEquals(afterProvidesNothing,resolution.get(1)); + } + + private ChainBuilder createDependencyHandler() { + ChainBuilder chainBuilder = newChainBuilder(); + chainBuilder.addPhase(new Phase("phase1", Collections.<String>emptySet(), Collections.<String>emptySet())); + chainBuilder.addPhase(new Phase("phase2", Collections.<String>emptySet(), Collections.<String>emptySet())); + chainBuilder.addPhase(new Phase("phase3", Collections.<String>emptySet(), Collections.<String>emptySet())); + return chainBuilder; + } + + private String name(ChainedComponent searcher) { + return searcher.getClass().getSimpleName(); + } + + @Provides("A") + static class A extends NoopComponent { + } + + @Provides("B") + @After("A") + @Before({"D", "phase1"}) + static class B extends NoopComponent { + } + + @Provides("C") + @After("phase1") + static class C extends NoopComponent { + } + + @Provides("D") + @After({"C","A"}) + static class D extends NoopComponent { + } + + @Provides("E") + @After({"B","D"}) + @Before("phase2") + static class E extends NoopComponent { + } + + @Provides("F") + @After("phase2") + static class F extends NoopComponent { + } + + @Provides("G") + @After("F") + static class G extends NoopComponent { + } + + @Provides("H") + @Before("A") + @After("F") + static class H extends NoopComponent { + } + + @Provides("AfterAll") + @After("*") + static class AfterAll extends NoopComponent { + } + + static class ProvidesNothing extends NoopComponent { + } + + @After("ProvidesNothing") + static class AfterProvidesNothing extends NoopComponent { + } + + public static class NoopComponent extends ChainedComponent { + } + +} diff --git a/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodesTest.java b/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodesTest.java new file mode 100644 index 00000000000..9cb974a128e --- /dev/null +++ b/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodesTest.java @@ -0,0 +1,104 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.dependencies.ordering; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import com.yahoo.component.chain.ChainedComponent; +import com.yahoo.component.chain.dependencies.Dependencies; +import org.junit.Before; +import org.junit.Test; + +import com.yahoo.component.ComponentId; + + + +/** + * Test for OrderedReadyNodes. + * @author tonytv + */ +@SuppressWarnings("rawtypes") +public class OrderedReadyNodesTest { + class ComponentA extends ChainedComponent { + public ComponentA(ComponentId id) { + super(id); + } + + @Override + public Dependencies getDependencies() { + return new Dependencies(Arrays.asList(getId().getName()), null, null); + } + } + + class ComponentB extends ComponentA { + public ComponentB(ComponentId id) { + super(id); + } + } + + private OrderedReadyNodes readyNodes; + + @Before + public void setup() { + readyNodes = new OrderedReadyNodes(); + } + + @Test + public void require_NameProviders_before_SearcherNodes() { + NameProvider nameProvider = createDummyNameProvider(100); + ComponentNode componentNode = new ComponentNode<>(createFakeComponentA("a"), 1); + + addNodes(nameProvider, componentNode); + + assertEquals(nameProvider, pop()); + assertEquals(componentNode, pop()); + } + + private NameProvider createDummyNameProvider(int priority) { + return new NameProvider("anonymous", priority) { + @Override + protected void addNode(ComponentNode node) { + throw new UnsupportedOperationException(); + } + + @Override + int classPriority() { + return 0; + } + }; + } + + @Test + public void require_SearcherNodes_ordered_by_insertion_order() { + int priority = 0; + ComponentNode a = new ComponentNode<>(createFakeComponentB("1"), priority++); + ComponentNode b = new ComponentNode<>(createFakeComponentA("2"), priority++); + ComponentNode c = new ComponentNode<>(createFakeComponentA("03"), priority++); + + addNodes(a, b, c); + + assertEquals(a, pop()); + assertEquals(b, pop()); + assertEquals(c, pop()); + } + + ChainedComponent createFakeComponentA(String id) { + return new ComponentA(ComponentId.fromString(id)); + } + + ChainedComponent createFakeComponentB(String id) { + return new ComponentB(ComponentId.fromString(id)); + } + + + private void addNodes(Node... nodes) { + for (Node node : nodes) { + readyNodes.add(node); + } + } + + private Node pop() { + return readyNodes.pop(); + } +} diff --git a/chain/src/test/java/com/yahoo/component/chain/model/ChainsModelBuilderTest.java b/chain/src/test/java/com/yahoo/component/chain/model/ChainsModelBuilderTest.java new file mode 100644 index 00000000000..a4e09dc90b7 --- /dev/null +++ b/chain/src/test/java/com/yahoo/component/chain/model/ChainsModelBuilderTest.java @@ -0,0 +1,72 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component.chain.model; + +import com.yahoo.component.ComponentId; +import com.yahoo.component.ComponentSpecification; +import com.yahoo.container.core.ChainsConfig; +import org.junit.Test; + +import java.util.Map; +import java.util.Set; + +import static com.yahoo.container.core.ChainsConfig.Components; +import static com.yahoo.container.core.ChainsConfig.Chains; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author gjoranv + * @since 5.1.10 + */ +public class ChainsModelBuilderTest { + + @Test + public void components_are_added_to_componentModels() throws Exception { + ChainsModel model = chainsModel(); + assertThat(model.allComponents().size(), is(2)); + assertTrue(model.componentModels().containsKey(new ComponentId("componentA"))); + } + + @Test + public void components_are_added_to_chainSpecification() throws Exception { + ChainsModel model = chainsModel(); + ChainSpecification chainSpec = model.chainSpecifications().get(new ComponentId("chain1")).model(); + assertTrue(getComponentsByName(chainSpec.componentReferences).containsKey("componentA")); + } + + @Test + public void inherited_chains_are_added_to_chainSpecification() throws Exception { + ChainsModel model = chainsModel(); + ChainSpecification inheritsChain1 = model.chainSpecifications().get(new ComponentId("inheritsChain1")).model(); + assertThat(model.allChainsFlattened().size(), is(2)); + assertTrue(getComponentsByName(inheritsChain1.inheritance.chainSpecifications).containsKey("chain1")); + assertTrue(getComponentsByName(inheritsChain1.inheritance.excludedComponents).containsKey("componentA")); + } + + private ChainsModel chainsModel() { + ChainsConfig.Builder builder = new ChainsConfig.Builder() + .components(new Components.Builder() + .id("componentA")) + .components(new Components.Builder() + .id("componentB")) + .chains(new Chains.Builder() + .id("chain1") + .components("componentA") + .components("componentB")) + .chains(new Chains.Builder() + .id("inheritsChain1") + .inherits("chain1") + .excludes("componentA")); + ChainsConfig config = new ChainsConfig(builder); + + ChainsModel model = ChainsModelBuilder.buildFromConfig(config); + model.validate(); + return model; + } + + private static Map<String, ComponentSpecification> + getComponentsByName(Set<ComponentSpecification> componentSpecifications) { + return ChainSpecification.componentsByName(componentSpecifications); + } +} |