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 /jdisc_core |
Publish
Diffstat (limited to 'jdisc_core')
223 files changed, 27318 insertions, 0 deletions
diff --git a/jdisc_core/.gitignore b/jdisc_core/.gitignore new file mode 100644 index 00000000000..579f9d16789 --- /dev/null +++ b/jdisc_core/.gitignore @@ -0,0 +1,3 @@ +/pom.xml.build +/target +/tmp diff --git a/jdisc_core/OWNERS b/jdisc_core/OWNERS new file mode 100644 index 00000000000..90fdb511ae3 --- /dev/null +++ b/jdisc_core/OWNERS @@ -0,0 +1 @@ +bakksjo diff --git a/jdisc_core/pom.xml b/jdisc_core/pom.xml new file mode 100644 index 00000000000..315a46a65c4 --- /dev/null +++ b/jdisc_core/pom.xml @@ -0,0 +1,300 @@ +<?xml version="1.0"?> +<!-- 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>jdisc_core</artifactId> + <version>6-SNAPSHOT</version> + <packaging>jar</packaging> + <name>${project.artifactId}</name> + <dependencies> + <dependency> + <groupId>xml-apis</groupId> + <artifactId>xml-apis</artifactId> + <version>1.4.01</version> + </dependency> + <dependency> + <groupId>javax.annotation</groupId> + <artifactId>javax.annotation-api</artifactId> + <version>1.2</version> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>aopalliance</groupId> + <artifactId>aopalliance</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>defaults</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>com.google.inject</groupId> + <artifactId>guice</artifactId> + <scope>compile</scope> + <classifier>no_aop</classifier> + </dependency> + <dependency> + <groupId>com.google.inject.extensions</groupId> + <artifactId>guice-assistedinject</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>com.google.inject.extensions</groupId> + <artifactId>guice-multibindings</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>commons-daemon</groupId> + <artifactId>commons-daemon</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.framework</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.log</artifactId> + <scope>compile</scope> + <exclusions> + <exclusion> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.compendium</artifactId> + </exclusion> + <exclusion> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>jcl-over-slf4j</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>log4j-over-slf4j</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-jdk14</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>config-lib</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>yolean</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + </dependency> + <dependency> + <!-- This seems odd. Used for export-package parsing. Lazy stuff. Should be separated out. --> + <groupId>com.yahoo.vespa</groupId> + <artifactId>bundle-plugin</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <compilerArgs> + <arg>-Xlint:rawtypes</arg> + <arg>-Xlint:unchecked</arg> + <arg>-Xlint:deprecation</arg> + </compilerArgs> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <forkMode>once</forkMode> + <redirectTestOutputToFile>${test.hide}</redirectTestOutputToFile> + <systemPropertyVariables> + <jdisc.logger.level>ALL</jdisc.logger.level> + </systemPropertyVariables> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-install-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <execution> + <id>copy-dependencies</id> + <phase>compile</phase> + <goals> + <goal>copy-dependencies</goal> + </goals> + <configuration> + <excludeTransitive>true</excludeTransitive> + <stripVersion>true</stripVersion> + <outputDirectory>${project.build.directory}/dependency</outputDirectory> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <executions> + <execution> + <id>export-packages</id> + <phase>compile</phase> + <goals> + <goal>java</goal> + </goals> + <configuration> + <mainClass>com.yahoo.jdisc.core.ExportPackages</mainClass> + <classpathScope>test</classpathScope> + <arguments> + <argument>${exportPackagesFile}</argument> + <argument>${project.build.directory}/dependency/commons-daemon.jar</argument> + <argument>__REPLACE_VERSION__${project.build.directory}/dependency/guava.jar</argument> + <argument>${project.build.directory}/dependency/guice-no_aop.jar</argument> + <argument>${project.build.directory}/dependency/guice-assistedinject.jar</argument> + <argument>${project.build.directory}/dependency/guice-multibindings.jar</argument> + <argument>${project.build.directory}/dependency/javax.annotation-api.jar</argument> + <argument>${project.build.directory}/dependency/org.apache.felix.log.jar</argument> + <argument>${project.build.directory}/dependency/slf4j-api.jar</argument> + <argument>${project.build.directory}/dependency/slf4j-jdk14.jar</argument> + <argument>${project.build.directory}/dependency/jcl-over-slf4j.jar</argument> + <argument>${project.build.directory}/dependency/log4j-over-slf4j.jar</argument> + <argument>${project.build.directory}/dependency/config-lib.jar</argument> + <argument>${project.build.directory}/dependency/yolean.jar</argument> + </arguments> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>properties-maven-plugin</artifactId> + <executions> + <execution> + <phase>prepare-package</phase> + <goals> + <goal>read-project-properties</goal> + </goals> + <configuration> + <files> + <file>${exportPackagesFile}</file> + </files> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <archive> + <manifestEntries> + <Bundle-ManifestVersion>2</Bundle-ManifestVersion> + <Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName> + <Export-Package>${exportPackages}</Export-Package> + </manifestEntries> + </archive> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <configuration> + <descriptorRefs> + <descriptorRef>jar-with-dependencies</descriptorRef> + </descriptorRefs> + </configuration> + <executions> + <execution> + <id>make-assembly</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + <profiles> + <profile> + <id>test-logfmt</id> + <activation> + <property> + <name>!skipTests</name> + </property> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <executions> + <execution> + <id>test-logfmt</id> + <phase>test</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <executable>${project.basedir}/src/test/perl/jdisc_logfmt_test.sh</executable> + <arguments> + <argument>${project.basedir}/src/main/perl/jdisc_logfmt</argument> + </arguments> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> + <properties> + <exportPackagesFile>${project.build.directory}/classes/exportPackages.properties</exportPackagesFile> + </properties> +</project> diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/AbstractResource.java b/jdisc_core/src/main/java/com/yahoo/jdisc/AbstractResource.java new file mode 100644 index 00000000000..9862e574009 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/AbstractResource.java @@ -0,0 +1,205 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.service.ClientProvider; +import com.yahoo.jdisc.service.ServerProvider; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * <p>This class provides a thread-safe implementation of the {@link SharedResource} interface, and should be used for + * all subclasses of {@link RequestHandler}, {@link ClientProvider} and {@link ServerProvider}. Once the reference count + * of this resource reaches zero, the {@link #destroy()} method is called.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class AbstractResource implements SharedResource { + + private static final Logger log = Logger.getLogger(AbstractResource.class.getName()); + + private final boolean debug = SharedResource.DEBUG; + private final AtomicInteger refCount; + private final Object monitor; + private final Set<Throwable> activeReferences; + private final ResourceReference initialCreationReference; + + protected AbstractResource() { + if (!debug) { + this.refCount = new AtomicInteger(1); + this.monitor = null; + this.activeReferences = null; + this.initialCreationReference = new NoDebugResourceReference(this); + } else { + this.refCount = null; + this.monitor = new Object(); + this.activeReferences = new HashSet<>(); + final Throwable referenceStack = new Throwable(); + this.activeReferences.add(referenceStack); + this.initialCreationReference = new DebugResourceReference(this, referenceStack); + } + } + + @Override + public final ResourceReference refer() { + if (!debug) { + addRef(1); + return new NoDebugResourceReference(this); + } + + final Throwable referenceStack = new Throwable(); + final String state; + synchronized (monitor) { + if (activeReferences.isEmpty()) { + throw new IllegalStateException("Object is already destroyed, no more new references may be created." + + " State={ " + currentStateDebugWithLock() + " }"); + } + activeReferences.add(referenceStack); + state = currentStateDebugWithLock(); + } + log.log(Level.WARNING, + getClass().getName() + "@" + System.identityHashCode(this) + ".refer(): state={ " + state + " }", + referenceStack); + return new DebugResourceReference(this, referenceStack); + } + + public void release() { + initialCreationReference.close(); + } + + private void removeReferenceStack(final Throwable referenceStack, final Throwable releaseStack) { + final boolean doDestroy; + final String state; + synchronized (monitor) { + final boolean wasThere = activeReferences.remove(referenceStack); + state = currentStateDebugWithLock(); + if (!wasThere) { + throw new IllegalStateException("Reference is already released and can only be released once." + + " reference=" + Arrays.toString(referenceStack.getStackTrace()) + + ". State={ " + state + "}"); + } + doDestroy = activeReferences.isEmpty(); + } + log.log(Level.WARNING, + getClass().getName() + "@" + System.identityHashCode(this) + " release: state={ " + state + " }", + releaseStack); + if (doDestroy) { + destroy(); + } + } + + /** + * <p>Returns the reference count of this resource. This typically has no value for other than single-threaded unit- + * tests, as it is merely a snapshot of the counter.</p> + * + * @return The current value of the reference counter. + */ + public final int retainCount() { + if (!debug) { + return refCount.get(); + } + + synchronized (monitor) { + return activeReferences.size(); + } + } + + /** + * <p>This method signals that this AbstractResource can dispose of any internal resources, and commence with shut + * down of any internal threads. This will be called once the reference count of this resource reaches zero.</p> + */ + protected void destroy() { + + } + + private int addRef(int value) { + while (true) { + int prev = refCount.get(); + if (prev == 0) { + throw new IllegalStateException(getClass().getName() + ".addRef(" + value + "):" + + " Object is already destroyed." + + " Consider toggling the " + SharedResource.SYSTEM_PROPERTY_NAME_DEBUG + + " system property to get debugging assistance with reference tracking."); + } + int next = prev + value; + if (refCount.compareAndSet(prev, next)) { + return next; + } + } + } + + /** + * Returns a string describing the current state of references in human-friendly terms. May be used for debugging. + */ + public String currentState() { + if (!debug) { + return "Active references: " + refCount.get() + "." + + " Resource reference debugging is turned off. Consider toggling the " + + SharedResource.SYSTEM_PROPERTY_NAME_DEBUG + + " system property to get debugging assistance with reference tracking."; + } + synchronized (monitor) { + return currentStateDebugWithLock(); + } + } + + private String currentStateDebugWithLock() { + return "Active references: " + makeListOfActiveReferences(); + } + + private String makeListOfActiveReferences() { + final StringBuilder builder = new StringBuilder(); + builder.append("["); + for (final Throwable activeReference : activeReferences) { + builder.append(" "); + builder.append(Arrays.toString(activeReference.getStackTrace())); + } + builder.append(" ]"); + return builder.toString(); + } + + private static class NoDebugResourceReference implements ResourceReference { + private final AbstractResource resource; + private final AtomicBoolean isReleased = new AtomicBoolean(false); + + public NoDebugResourceReference(final AbstractResource resource) { + this.resource = resource; + } + + @Override + public final void close() { + final boolean wasReleasedBefore = isReleased.getAndSet(true); + if (wasReleasedBefore) { + final String message = "Reference is already released and can only be released once." + + " State={ " + resource.currentState() + " }"; + throw new IllegalStateException(message); + } + int refCount = resource.addRef(-1); + if (refCount == 0) { + resource.destroy(); + } + } + } + + private static class DebugResourceReference implements ResourceReference { + private final AbstractResource resource; + private final Throwable referenceStack; + + public DebugResourceReference(final AbstractResource resource, final Throwable referenceStack) { + this.resource = resource; + this.referenceStack = referenceStack; + } + + @Override + public final void close() { + final Throwable releaseStack = new Throwable(); + resource.removeReferenceStack(referenceStack, releaseStack); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Container.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Container.java new file mode 100644 index 00000000000..53e9c76eb61 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Container.java @@ -0,0 +1,66 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.google.inject.ConfigurationException; +import com.google.inject.Key; +import com.google.inject.ProvisionException; +import com.yahoo.jdisc.application.Application; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.service.ServerProvider; + +import java.net.URI; + +/** + * <p>This is the immutable Container. An instance of this class is attached to every {@link Request}, and as long as + * the {@link Request#release()} method has not been called, that Container instance is actively kept alive to prevent + * any race conditions during reconfiguration or shutdown. At any time there is only a single active Container in the + * running {@link Application}, and the only way to retrieve a reference to that Container is by calling {@link + * CurrentContainer#newReference(URI)}. Instead of holding a local Container object inside a {@link ServerProvider} + * (which will eventually become stale), use the {@link Request#Request(CurrentContainer, URI) appropriate Request + * constructor} instead.</p> + * + * <p>The only way to <u>create</u> a new instance of this class is to 1) create and configure a {@link + * ContainerBuilder}, and 2) pass that to the {@link ContainerActivator#activateContainer(ContainerBuilder)} method.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface Container extends SharedResource, Timer { + + /** + * <p>Attempts to find a {@link RequestHandler} in the current server- (if {@link Request#isServerRequest()} is + * <em>true</em>) or client- (if {@link Request#isServerRequest()} is <em>false</em>) {@link BindingSet} that + * matches the given {@link URI}. If no match can be found, this method returns null.</p> + * + * @param request The Request to match against the bound {@link RequestHandler}s. + * @return The matching RequestHandler, or null if there is no match. + */ + public RequestHandler resolveHandler(Request request); + + /** + * <p>Returns the appropriate instance for the given injection key. When feasible, avoid using this method in favor + * of having Guice inject your dependencies ahead of time.</p> + * + * @param key The key of the instance to return. + * @param <T> The class of the instance to return. + * @return The appropriate instance of the given class. + * @throws ConfigurationException If this injector cannot find or create the provider. + * @throws ProvisionException If there was a runtime failure while providing an instance. + */ + public <T> T getInstance(Key<T> key); + + /** + * <p>Returns the appropriate instance for the given injection type. When feasible, avoid using this method in + * favor of having Guice inject your dependencies ahead of time.</p> + * + * @param type The class object of the instance to return. + * @param <T> The class of the instance to return. + * @return The appropriate instance of the given class. + * @throws ConfigurationException If this injector cannot find or create the provider. + * @throws ProvisionException If there was a runtime failure while providing an instance. + */ + public <T> T getInstance(Class<T> type); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java b/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java new file mode 100644 index 00000000000..a81fb3ff152 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java @@ -0,0 +1,305 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.google.common.collect.ImmutableList; + +import java.util.*; + +/** + * <p>This is an encapsulation of the header fields that belong to either a {@link Request} or a {@link Response}. It is + * a multimap from String to String, with some additional methods for convenience. The keys of this map are compared by + * ignoring their case, so that <tt>get("foo")</tt> returns the same entry as <tt>get("FOO")</tt>.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class HeaderFields implements Map<String, List<String>> { + + private final TreeMap<String, List<String>> content = new TreeMap<>(new Comparator<String>() { + + @Override + public int compare(String lhs, String rhs) { + return lhs.compareToIgnoreCase(rhs); + } + }); + + @Override + public int size() { + return content.size(); + } + + @Override + public boolean isEmpty() { + return content.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return content.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return content.containsValue(value); + } + + /** + * <p>Convenience method for checking whether or not a named header contains a specific value. If the named header + * is not set, or if the given value is not contained within that header's value list, this method returns + * <em>false</em>.</p> + * + * <p><em>NOTE:</em> This method is case-SENSITIVE.</p> + * + * @param key The key whose values to search in. + * @param value The values to search for. + * @return True if the given value was found in the named header. + * @see #containsIgnoreCase + */ + public boolean contains(String key, String value) { + List<String> lst = content.get(key); + if (lst == null) { + return false; + } + return lst.contains(value); + } + + /** + * <p>Convenience method for checking whether or not a named header contains a specific value, regardless of case. + * If the named header is not set, or if the given value is not contained within that header's value list, this + * method returns <em>false</em>.</p> + * + * <p><em>NOTE:</em> This method is case-INSENSITIVE.</p> + * + * @param key The key whose values to search in. + * @param value The values to search for, ignoring case. + * @return True if the given value was found in the named header. + * @see #contains + */ + public boolean containsIgnoreCase(String key, String value) { + List<String> lst = content.get(key); + if (lst == null) { + return false; + } + for (String val : lst) { + if (value.equalsIgnoreCase(val)) { + return true; + } + } + return false; + } + + /** + * <p>Adds the given value to the entry of the specified key. If no entry exists for the given key, a new one is + * created containing only the given value.</p> + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be added to the list associated with the specified key. + */ + public void add(String key, String value) { + List<String> lst = content.get(key); + if (lst != null) { + lst.add(value); + } else { + put(key, value); + } + } + + /** + * <p>Adds the given values to the entry of the specified key. If no entry exists for the given key, a new one is + * created containing only the given values.</p> + * + * @param key The key with which the specified value is to be associated. + * @param values The values to be added to the list associated with the specified key. + */ + public void add(String key, List<String> values) { + List<String> lst = content.get(key); + if (lst != null) { + lst.addAll(values); + } else { + put(key, values); + } + } + + /** + * <p>Adds all the entries of the given map to this. This is the same as calling {@link #add(String, List)} for each + * entry in <tt>values</tt>.</p> + * + * @param values The values to be added to this. + */ + public void addAll(Map<? extends String, ? extends List<String>> values) { + for (Entry<? extends String, ? extends List<String>> entry : values.entrySet()) { + add(entry.getKey(), entry.getValue()); + } + } + + /** + * <p>Convenience method to call {@link #put(String, List)} with a singleton list that contains the specified + * value.</p> + * + * @param key The key of the entry to put. + * @param value The value to put. + * @return The previous value associated with <tt>key</tt>, or <tt>null</tt> if there was no mapping for + * <tt>key</tt>. + */ + public List<String> put(String key, String value) { + ArrayList<String> list = new ArrayList<String>(1); + list.add(value); + return content.put(key, list); + } + + @Override + public List<String> put(String key, List<String> value) { + return content.put(key, new ArrayList<>(value)); + } + + @Override + public void putAll(Map<? extends String, ? extends List<String>> values) { + for (Entry<? extends String, ? extends List<String>> entry : values.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public List<String> remove(Object key) { + return content.remove(key); + } + + /** + * <p>Removes the given value from the entry of the specified key.</p> + * + * @param key The key of the entry to remove from. + * @param value The value to remove from the entry. + * @return True if the value was removed. + */ + public boolean remove(String key, String value) { + List<String> lst = content.get(key); + if (lst == null) { + return false; + } + if (!lst.remove(value)) { + return false; + } + if (lst.isEmpty()) { + content.remove(key); + } + return true; + } + + @Override + public void clear() { + content.clear(); + } + + @Override + public List<String> get(Object key) { + return content.get(key); + } + + /** + * <p>Convenience method for retrieving the first value of a named header field. If the header is not set, or if the + * value list is empty, this method returns null.</p> + * + * @param key The key whose first value to return. + * @return The first value of the named header, or null. + */ + public String getFirst(String key) { + List<String> lst = get(key); + if (lst == null || lst.isEmpty()) { + return null; + } + return lst.get(0); + } + + /** + * <p>Convenience method for checking whether or not a named header field is <em>true</em>. To satisfy this, the + * header field needs to have at least 1 entry, and Boolean.valueOf() of all its values must parse as + * <em>true</em>.</p> + * + * @param key The key whose values to parse as a boolean. + * @return The boolean value of the named header. + */ + public boolean isTrue(String key) { + List<String> lst = content.get(key); + if (lst == null) { + return false; + } + for (String value : lst) { + if (!Boolean.valueOf(value)) { + return false; + } + } + return true; + } + + @Override + public Set<String> keySet() { + return content.keySet(); + } + + @Override + public Collection<List<String>> values() { + return content.values(); + } + + @Override + public Set<Entry<String, List<String>>> entrySet() { + return content.entrySet(); + } + + @Override + public String toString() { + return content.toString(); + } + + /** + * <p>Returns an unmodifiable list of all key-value pairs of this. This provides a flattened view on the content of + * this map.</p> + * + * @return The collection of entries. + */ + public List<Entry<String, String>> entries() { + List<Entry<String, String>> list = new ArrayList<>(content.size()); + for (Entry<String, List<String>> entry : content.entrySet()) { + String key = entry.getKey(); + for (String value : entry.getValue()) { + list.add(new MyEntry(key, value)); + } + } + return ImmutableList.copyOf(list); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof HeaderFields && content.equals(((HeaderFields)obj).content); + } + + @Override + public int hashCode() { + return content.hashCode(); + } + + private static class MyEntry implements Map.Entry<String, String> { + + final String key; + final String value; + + private MyEntry(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getValue() { + return value; + } + + @Override + public String setValue(String value) { + throw new UnsupportedOperationException(); + } + } +}
\ No newline at end of file diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Metric.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Metric.java new file mode 100644 index 00000000000..50b25dbbdf0 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Metric.java @@ -0,0 +1,61 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.google.inject.ProvidedBy; +import com.google.inject.Provider; +import com.yahoo.jdisc.application.MetricConsumer; +import com.yahoo.jdisc.application.MetricProvider; + +import java.util.Map; + +/** + * <p>This interface provides an API for writing metric data to the configured {@link MetricConsumer}. If no {@link + * Provider} for the MetricConsumer class has been bound by the application, all calls to this interface are no-ops. The + * implementation of this interface uses thread local consumer instances, so as long as the {@link MetricConsumer} is + * thread-safe, so is this.</p> + * + * <p>An instance of this class can be injected anywhere.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +@ProvidedBy(MetricProvider.class) +public interface Metric { + + /** + * <p>Set a metric value. This is typically used with histogram-type metrics.</p> + * + * @param key The name of the metric to modify. + * @param val The value to assign to the named metric. + * @param ctx The context to further describe this entry. + */ + void set(String key, Number val, Context ctx); + + /** + * <p>Add to a metric value. This is typically used with counter-type metrics.</p> + * + * @param key The name of the metric to modify. + * @param val The value to add to the named metric. + * @param ctx The context to further describe this entry. + */ + void add(String key, Number val, Context ctx); + + /** + * <p>Creates a {@link MetricConsumer}-specific {@link Context} object that encapsulates the given properties. The + * returned Context object should be passed along every future call to {@link #set(String, Number, Context)} and + * {@link #add(String, Number, Context)} where the properties match those given here.</p> + * + * @param properties The properties to incorporate in the context. + * @return The created context. + */ + Context createContext(Map<String, ?> properties); + + /** + * <p>Declares the interface for the arbitrary context object to pass to both the {@link + * #set(String, Number, Context)} and {@link #add(String, Number, Context)} methods. This is intentionally empty so + * that implementations can vary.</p> + */ + interface Context { + + } + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/NoopSharedResource.java b/jdisc_core/src/main/java/com/yahoo/jdisc/NoopSharedResource.java new file mode 100644 index 00000000000..fe4990dbd60 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/NoopSharedResource.java @@ -0,0 +1,19 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +/** + * An implementation of {@link SharedResource} that does not do anything. + * Useful base class for e.g. mocks of SharedResource sub-interfaces, where reference counting is not the focus. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class NoopSharedResource implements SharedResource { + @Override + public final ResourceReference refer() { + return References.NOOP_REFERENCE; + } + + @Override + public final void release() { + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/ProxyRequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/ProxyRequestHandler.java new file mode 100644 index 00000000000..6f3d342705c --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/ProxyRequestHandler.java @@ -0,0 +1,239 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.NullContent; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; + +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** +* @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> +*/ +class ProxyRequestHandler implements RequestHandler { + + private static final CompletionHandler IGNORED_COMPLETION = new IgnoredCompletion(); + private static final Logger log = Logger.getLogger(ProxyRequestHandler.class.getName()); + + final RequestHandler delegate; + + ProxyRequestHandler(RequestHandler delegate) { + Objects.requireNonNull(delegate, "delegate"); + this.delegate = delegate; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) { + try (final ResourceReference requestReference = request.refer()) { + ContentChannel contentChannel; + final ResponseHandler proxyResponseHandler = new ProxyResponseHandler( + request, new NullContentResponseHandler(responseHandler)); + try { + contentChannel = delegate.handleRequest(request, proxyResponseHandler); + Objects.requireNonNull(contentChannel, "contentChannel"); + } catch (Throwable t) { + try { + proxyResponseHandler + .handleResponse(new Response(Response.Status.INTERNAL_SERVER_ERROR, t)) + .close(IGNORED_COMPLETION); + } catch (Throwable ignored) { + // empty + } + throw t; + } + return new ProxyContentChannel(request, contentChannel); + } + } + + @Override + public void handleTimeout(Request request, ResponseHandler responseHandler) { + delegate.handleTimeout(request, new NullContentResponseHandler(responseHandler)); + } + + @Override + public ResourceReference refer() { + return delegate.refer(); + } + + @Override + public void release() { + delegate.release(); + } + + @Override + public String toString() { + return delegate.toString(); + } + + private static class ProxyResponseHandler implements ResponseHandler { + + final SharedResource request; + final ResourceReference requestReference; + final ResponseHandler delegate; + final AtomicBoolean closed = new AtomicBoolean(false); + + ProxyResponseHandler(SharedResource request, ResponseHandler delegate) { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(delegate, "delegate"); + this.request = request; + this.delegate = delegate; + this.requestReference = request.refer(); + } + + @Override + public ContentChannel handleResponse(Response response) { + if (closed.getAndSet(true)) { + throw new IllegalStateException(delegate + " is already called."); + } + try (final ResourceReference ref = requestReference) { + ContentChannel contentChannel = delegate.handleResponse(response); + Objects.requireNonNull(contentChannel, "contentChannel"); + return new ProxyContentChannel(request, contentChannel); + } + } + + @Override + public String toString() { + return delegate.toString(); + } + } + + private static class ProxyContentChannel implements ContentChannel { + + final SharedResource request; + final ResourceReference requestReference; + final ContentChannel delegate; + + ProxyContentChannel(SharedResource request, ContentChannel delegate) { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(delegate, "delegate"); + this.request = request; + this.delegate = delegate; + this.requestReference = request.refer(); + } + + @Override + public void write(ByteBuffer buf, CompletionHandler completionHandler) { + ProxyCompletionHandler proxyCompletionHandler = new ProxyCompletionHandler(request, completionHandler); + try { + delegate.write(buf, proxyCompletionHandler); + } catch (Throwable t) { + try { + proxyCompletionHandler.failed(t); + } catch (Throwable ignored) { + // empty + } + throw t; + } + } + + @Override + public void close(CompletionHandler completionHandler) { + final ProxyCompletionHandler proxyCompletionHandler + = new ProxyCompletionHandler(request, completionHandler); + try (final ResourceReference ref = requestReference) { + delegate.close(proxyCompletionHandler); + } catch (Throwable t) { + try { + proxyCompletionHandler.failed(t); + } catch (Throwable ignored) { + // empty + } + throw t; + } + } + + @Override + public String toString() { + return delegate.toString(); + } + } + + private static class ProxyCompletionHandler implements CompletionHandler { + + final ResourceReference requestReference; + final CompletionHandler delegate; + final AtomicBoolean closed = new AtomicBoolean(false); + + public ProxyCompletionHandler(SharedResource request, CompletionHandler delegate) { + this.delegate = delegate; + this.requestReference = request.refer(); + } + + @Override + public void completed() { + if (closed.getAndSet(true)) { + throw new IllegalStateException(delegate + " is already called."); + } + try { + if (delegate != null) { + delegate.completed(); + } + } finally { + requestReference.close(); + } + } + + @Override + public void failed(Throwable t) { + if (closed.getAndSet(true)) { + throw new IllegalStateException(delegate + " is already called."); + } + try (final ResourceReference ref = requestReference) { + if (delegate != null) { + delegate.failed(t); + } else { + log.log(Level.WARNING, "Uncaught completion failure.", t); + } + } + } + + @Override + public String toString() { + return String.valueOf(delegate); + } + } + + private static class NullContentResponseHandler implements ResponseHandler { + + final ResponseHandler delegate; + + NullContentResponseHandler(ResponseHandler delegate) { + Objects.requireNonNull(delegate, "delegate"); + this.delegate = delegate; + } + + @Override + public ContentChannel handleResponse(Response response) { + ContentChannel contentChannel = delegate.handleResponse(response); + if (contentChannel == null) { + contentChannel = NullContent.INSTANCE; + } + return contentChannel; + } + + @Override + public String toString() { + return delegate.toString(); + } + } + + private static class IgnoredCompletion implements CompletionHandler { + + @Override + public void completed() { + // ignore + } + + @Override + public void failed(Throwable t) { + // ignore + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/ReferencedResource.java b/jdisc_core/src/main/java/com/yahoo/jdisc/ReferencedResource.java new file mode 100644 index 00000000000..f55a46f1a05 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/ReferencedResource.java @@ -0,0 +1,58 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +/** + * <p>Utility class for working with reference-counted {@link SharedResource}s.</p> + * + * <p>Sometimes, you may want a method to return <i>both</i> a resource object <i>and</i> + * a {@link ResourceReference} that refers the resource object (for later release of the resource). + * Java methods cannot return multiple objects, so this class provides Pair-like functionality + * for returning both.</p> + * + * <p>Example usage:</p> + * <pre> + * ReferencedResource<MyResource> getResource() { + * final ResourceReference ref = resource.refer(); + * return new ReferencedResource(resource, ref); + * } + * + * void useResource() { + * final ReferencedResource<MyResource> referencedResource = getResource(); + * referencedResource.getResource().use(); + * referencedResource.getReference().close(); + * } + * </pre> + * + * <p>This class implements AutoCloseable, so the latter method may also be written as follows:</p> + * <pre> + * void useResource() { + * for (final ReferencedResource<MyResource> referencedResource = getResource()) { + * referencedResource.getResource().use(); + * } + * } + * </pre> + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class ReferencedResource<T extends SharedResource> implements AutoCloseable { + private final T resource; + private final ResourceReference reference; + + public ReferencedResource(final T resource, final ResourceReference reference) { + this.resource = resource; + this.reference = reference; + } + + public T getResource() { + return resource; + } + + public ResourceReference getReference() { + return reference; + } + + @Override + public void close() { + reference.close(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/References.java b/jdisc_core/src/main/java/com/yahoo/jdisc/References.java new file mode 100644 index 00000000000..868ae6ac720 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/References.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +/** + * Utility class for working with {@link SharedResource}s and {@link ResourceReference}s. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class References { + // Prevents instantiation. + private References() { + } + + /** + * A {@link ResourceReference} that does nothing. + * Useful for e.g. testing of resource types when reference counting is not the focus. + */ + public static final ResourceReference NOOP_REFERENCE = new ResourceReference() { + @Override + public void close() { + } + }; + + /** + * <p>Returns a {@link ResourceReference} that invokes {@link SharedResource#release()} on + * {@link ResourceReference#close() close}. Useful for treating the "main" reference of a {@link SharedResource} + * just as any other reference obtained by calling {@link SharedResource#refer()}. Example:</p> + * <pre> + * final Request request = new Request(...); + * try (final ResourceReference ref = References.fromResource(request)) { + * .... + * } + * // The request will be released on exit from the try block. + * </pre> + * + * @param resource The resource to create a ResourceReference for. + * @return a ResourceReference whose close() method will call release() on the given resource. + */ + public static ResourceReference fromResource(final SharedResource resource) { + return new ResourceReference() { + @Override + public void close() { + resource.release(); + } + }; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Request.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Request.java new file mode 100644 index 00000000000..a210660aae5 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Request.java @@ -0,0 +1,411 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.jdisc.application.UriPattern; +import com.yahoo.jdisc.handler.BindingNotFoundException; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestDeniedException; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.service.ServerProvider; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * <p>This class represents a single request (which may have any content model that a {@link ServerProvider} chooses to + * implement). The {@link #uri URI} is used by the {@link Container} to route it to the appropriate {@link + * RequestHandler}, which in turn will provide a {@link ContentChannel} to write content to.</p> + * + * <p>To ensure application consistency throughout the lifetime of a Request, the Request itself holds an active + * reference to the Container for which it was created. This has the unfortunate side-effect of requiring the creator of + * a Request to do explicit reference counting during the setup of a content stream.</p> + * + * <p>For every successfully dispatched Request (i.e. a non-null ContentChannel has been retrieved), there will be + * exactly one {@link Response} returned to the provided {@link ResponseHandler}.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @see Container + * @see Response + */ +public class Request extends AbstractResource { + + private final Map<String, Object> context = new HashMap<>(); + private final HeaderFields headers = new HeaderFields(); + private final Container container; + private final Request parent; + private final ResourceReference parentReference; + private final long creationTime; + private volatile boolean cancel = false; + private BindingMatch<RequestHandler> bindingMatch; + private TimeoutManager timeoutManager; + private boolean serverRequest; + private Long timeout; + private URI uri; + + /** + * <p>Creates a new instance of this class. As a {@link ServerProvider} you need to inject a {@link + * CurrentContainer} instance at construction time and use that as argument to this method. As a {@link + * RequestHandler} that needs to spawn child Requests, use the {@link #Request(Request, URI) other + * constructor}.</p> + * + * <p>Because a Request holds an active reference to the owning {@link Container}, it is necessary to call {@link + * #release()} once a {@link ContentChannel} has been established. Suggested usage:</p> + * + * <pre> + * Request request = null; + * ContentChannel content = null; + * try { + * request = new Request(currentContainer, uri); + * (...) + * content = request.connect(responseHandler); + * } finally { + * if (request != null) request.release(); + * } + * content.write(...); + * </pre> + * + * @param current The CurrentContainer for which this Request is created. + * @param uri The identifier of this request. + */ + public Request(CurrentContainer current, URI uri) { + container = current.newReference(uri); + parent = null; + parentReference = null; + creationTime = container.currentTimeMillis(); + serverRequest = true; + setUri(uri); + } + + /** + * <p>Creates a new instance of this class. As a {@link RequestHandler} you should use this method to spawn child + * Requests of another. As a {@link ServerProvider} that needs to spawn new Requests, us the {@link + * #Request(CurrentContainer, URI) other constructor}.</p> + * + * <p>Because a Request holds an active reference to the owning {@link Container}, it is necessary to call {@link + * #release()} once a {@link ContentChannel} has been established. Suggested usage:</p> + * + * <pre> + * Request request = null; + * ContentChannel content = null; + * try { + * request = new Request(parentRequest, uri); + * (...) + * content = request.connect(responseHandler); + * } finally { + * if (request != null) request.release(); + * } + * content.write(...); + * </pre> + * + * @param parent The parent Request of this. + * @param uri The identifier of this request. + */ + public Request(Request parent, URI uri) { + this.parent = parent; + this.parentReference = this.parent.refer(); + container = null; + creationTime = parent.container().currentTimeMillis(); + serverRequest = false; + setUri(uri); + } + + /** + * <p>Returns the {@link Container} for which this Request was created.</p> + * + * @return The container instance. + */ + public Container container() { + return parent != null ? parent.container() : container; + } + + /** + * <p>Returns the Uniform Resource Identifier used by the {@link Container} to resolve the appropriate {@link + * RequestHandler} for this Request.</p> + * + * @return The resource identifier. + * @see #setUri(URI) + */ + public URI getUri() { + return uri; + } + + /** + * <p>Sets the Uniform Resource Identifier used by the {@link Container} to resolve the appropriate {@link + * RequestHandler} for this Request. Because access to the URI is not guarded by any lock, any changes made after + * calling {@link #connect(ResponseHandler)} might never become visible to other threads.</p> + * + * @param uri The URI to set. + * @return This, to allow chaining. + * @see #getUri() + */ + public Request setUri(URI uri) { + this.uri = uri.normalize(); + return this; + } + + /** + * <p>Returns whether or not this Request was created by a {@link ServerProvider}. The value of this is used by + * {@link Container#resolveHandler(Request)} to decide whether to match against server- or client-bindings.</p> + * + * @return True, if this is a server request. + */ + public boolean isServerRequest() { + return serverRequest; + } + + /** + * <p>Sets whether or not this Request was created by a {@link ServerProvider}. The constructor that accepts a + * {@link CurrentContainer} sets this to <em>true</em>, whereas the constructor that accepts a parent Request sets + * this to <em>false</em>.</p> + * + * @param serverRequest Whether or not this is a server request. + * @return This, to allow chaining. + * @see #isServerRequest() + */ + public Request setServerRequest(boolean serverRequest) { + this.serverRequest = serverRequest; + return this; + } + + /** + * <p>Returns the last resolved {@link BindingMatch}, or null if none has been resolved yet. This is set + * automatically when calling the {@link Container#resolveHandler(Request)} method. The BindingMatch object holds + * information about the match of this Request's {@link #getUri() URI} to the {@link UriPattern} of the resolved + * {@link RequestHandler}. It allows you to reflect on the parts of the URI that were matched by wildcards in the + * UriPattern.</p> + * + * @return The last resolved BindingMatch, or null. + * @see #setBindingMatch(BindingMatch) + * @see Container#resolveHandler(Request) + */ + public BindingMatch<RequestHandler> getBindingMatch() { + return bindingMatch; + } + + /** + * <p>Sets the last resolved {@link BindingMatch} of this Request. This is called by the {@link + * Container#resolveHandler(Request)} method.</p> + * + * @param bindingMatch The BindingMatch to set. + * @return This, to allow chaining. + * @see #getBindingMatch() + */ + public Request setBindingMatch(BindingMatch<RequestHandler> bindingMatch) { + this.bindingMatch = bindingMatch; + return this; + } + + /** + * <p>Returns the named application context objects. This data is not intended for network transport, rather they + * are intended for passing shared data between components of an Application.</p> + * + * <p>Modifying the context map is a thread-unsafe operation -- any changes made after calling {@link + * #connect(ResponseHandler)} might never become visible to other threads, and might throw + * ConcurrentModificationExceptions in other threads.</p> + * + * @return The context map. + */ + public Map<String, Object> context() { + return context; + } + + /** + * <p>Returns the set of header fields of this Request. These are the meta-data of the Request, and are not applied + * to any internal {@link Container} logic. As opposed to the {@link #context()}, the headers ARE intended for + * network transport. Modifying headers is a thread-unsafe operation -- any changes made after calling {@link + * #connect(ResponseHandler)} might never become visible to other threads, and might throw + * ConcurrentModificationExceptions in other threads.</p> + * + * @return The header fields. + */ + public HeaderFields headers() { + return headers; + } + + /** + * <p>Sets a {@link TimeoutManager} to be called when {@link #setTimeout(long, TimeUnit)} is invoked. If a timeout + * has already been set for this Request, the TimeoutManager is called before returning. This method will throw an + * IllegalStateException if it has already been called.</p> + * + * <p><b>NOTE:</b> This is used by the default timeout management implementation, so unless you are replacing that + * mechanism you should avoid calling this method. If you <em>do</em> want to replace that mechanism, you need to + * call this method prior to calling the target {@link RequestHandler} (since that injects the default manager).</p> + * + * @param timeoutManager The manager to set. + * @throws NullPointerException If the TimeoutManager is null. + * @throws IllegalStateException If another TimeoutManager has already been set. + * @see #getTimeoutManager() + * @see #setTimeout(long, TimeUnit) + */ + public void setTimeoutManager(TimeoutManager timeoutManager) { + Objects.requireNonNull(timeoutManager, "timeoutManager"); + if (this.timeoutManager != null) { + throw new IllegalStateException("Timeout manager already set."); + } + this.timeoutManager = timeoutManager; + if (timeout != null) { + timeoutManager.scheduleTimeout(this); + } + } + + /** + * <p>Returns the {@link TimeoutManager} of this request, or null if none has been assigned.</p> + * + * @return The TimeoutManager of this Request. + * @see #setTimeoutManager(TimeoutManager) + */ + public TimeoutManager getTimeoutManager() { + return timeoutManager; + } + + /** + * <p>Sets the allocated time that this Request is allowed to exist before the corresponding call to {@link + * ResponseHandler#handleResponse(Response)} must have been made. If no timeout value is assigned to a Request, + * there will be no timeout.</p> + * + * <p>Once the allocated time has expired, unless the {@link ResponseHandler} has already been called, the {@link + * RequestHandler#handleTimeout(Request, ResponseHandler)} method is invoked.</p> + * + * <p>Calls to {@link #isCancelled()} return <em>true</em> if timeout has been exceeded.</p> + * + * @param timeout The allocated amount of time. + * @param unit The time unit of the <em>timeout</em> argument. + * @see #getTimeout(TimeUnit) + * @see #timeRemaining(TimeUnit) + */ + public void setTimeout(long timeout, TimeUnit unit) { + this.timeout = unit.toMillis(timeout); + if (timeoutManager != null) { + timeoutManager.scheduleTimeout(this); + } + } + + /** + * <p>Returns the allocated number of milliseconds that this Request is allowed to exist. If no timeout has been set + * for this Request, this method returns <em>null</em>.</p> + * + * @param unit The unit to return the timeout in. + * @return The timeout of this Request. + * @see #setTimeout(long, TimeUnit) + */ + public Long getTimeout(TimeUnit unit) { + if (timeout == null) { + return null; + } + return unit.convert(timeout, TimeUnit.MILLISECONDS); + } + + /** + * <p>Returns the time that this Request is allowed to exist. If no timeout has been set, this method will return + * <em>null</em>.</p> + * + * @param unit The unit to return the time in. + * @return The number of milliseconds left until this Request times out, or <em>null</em>. + */ + public Long timeRemaining(TimeUnit unit) { + if (timeout == null) { + return null; + } + return unit.convert(timeout - (container().currentTimeMillis() - creationTime), TimeUnit.MILLISECONDS); + } + + /** + * <p>Returns the time at which this Request was created. This is whatever value was returned by {@link + * Timer#currentTimeMillis()} when constructing this.</p> + * + * @param unit The unit to return the time in. + * @return The creation time of this Request. + */ + public long creationTime(TimeUnit unit) { + return unit.convert(creationTime, TimeUnit.MILLISECONDS); + } + + /** + * <p>Returns whether or not this Request has been cancelled. This can be thought of as the {@link + * Thread#isInterrupted()} of Requests - it does not enforce anything in ways of blocking the Request, it is simply + * a signal to allow the developer to break early if the Request has already been dropped.</p> + * + * <p>This method will also return <em>true</em> if the Request has a non-null timeout, and that timeout has + * expired.</p> + * + * <p>Finally, this method will also return <em>true</em> if this Request has a parent Request that has been + * cancelled.</p> + * + * @return True if this Request has timed out or been cancelled. + * @see #cancel() + * @see #setTimeout(long, TimeUnit) + */ + public boolean isCancelled() { + if (cancel) { + return true; + } + if (timeout != null && timeRemaining(TimeUnit.MILLISECONDS) <= 0) { + return true; + } + if (parent != null && parent.isCancelled()) { + return true; + } + return false; + } + + /** + * <p>Mark this request as cancelled and frees any resources held by the request if possible. + * All subsequent calls to {@link #isCancelled()} on this Request return <em>true</em>.</p> + * + * @see #isCancelled() + */ + public void cancel() { + if (cancel) return; + + if (timeoutManager != null && timeout != null) + timeoutManager.unscheduleTimeout(this); + cancel = true; + } + + /** + * <p>Attempts to resolve and connect to the {@link RequestHandler} appropriate for the {@link URI} of this Request. + * An exception is thrown if this operation fails at any point. This method is exception-safe.</p> + * + * @param responseHandler The handler to pass the corresponding {@link Response} to. + * @return The {@link ContentChannel} to write the Request content to. + * @throws NullPointerException If the {@link ResponseHandler} is null. + * @throws BindingNotFoundException If the corresponding call to {@link Container#resolveHandler(Request)} returns + * null. + */ + public ContentChannel connect(final ResponseHandler responseHandler) { + try { + Objects.requireNonNull(responseHandler, "responseHandler"); + RequestHandler requestHandler = container().resolveHandler(this); + if (requestHandler == null) { + throw new BindingNotFoundException(uri); + } + requestHandler = new ProxyRequestHandler(requestHandler); + ContentChannel content = requestHandler.handleRequest(this, responseHandler); + if (content == null) { + throw new RequestDeniedException(this); + } + return content; + } + catch (Throwable t) { + cancel(); + throw t; + } + } + + @Override + protected void destroy() { + if (parentReference != null) { + parentReference.close(); + } + if (container != null) { + container.release(); + } + } + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/ResourceReference.java b/jdisc_core/src/main/java/com/yahoo/jdisc/ResourceReference.java new file mode 100644 index 00000000000..d004846d5b8 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/ResourceReference.java @@ -0,0 +1,33 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +/** + * <p>Represents a live reference to a {@link SharedResource}. Only provides the ability to release the reference.</p> + * + * <p>Implements {@link AutoCloseable} so that it can be used in try-with-resources statements. Example</p> + * <pre> + * void doSomethingWithRequest(final Request request) { + * try (final ResourceReference ref = request.refer()) { + * // Do something with request + * } + * // ref.close() will be called automatically on exit from the try block, releasing the reference on 'request'. + * } + * </pre> + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public interface ResourceReference extends AutoCloseable { + + /** + * <p>Decrements the reference count of the referenced resource. + * You call this method once you are done using an object + * that you have previously {@link SharedResource#refer() referred}.</p> + * + * <p>Note that this method is NOT idempotent; you must call it exactly once.</p> + * + * @see SharedResource#refer() + */ + @Override + void close(); + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Response.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Response.java new file mode 100644 index 00000000000..809805fdcc4 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Response.java @@ -0,0 +1,220 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; + +import java.util.HashMap; +import java.util.Map; + +/** + * <p>This class represents the single response (which may have any content model that a {@link RequestHandler} chooses + * to implement) of some single request. Contrary to the {@link Request} class, this has no active reference to the + * parent {@link Container} (this is tracked internally by counting the number of requests vs the number of responses + * seen). The {@link ResponseHandler} of a Response is implicit in the invocation of {@link + * RequestHandler#handleRequest(Request, ResponseHandler)}.</p> + * + * <p>The usage pattern of the Response is similar to that of the Request in that the {@link ResponseHandler} returns a + * {@link ContentChannel} into which to write the Response content.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @see Request + * @see ResponseHandler + */ +public class Response { + + /** + * <p>This interface acts as a namespace for the built-in status codes of the jDISC core. These are identical to the + * common HTTP status codes (see <a href="http://www.rfc-editor.org/rfc/rfc2616.txt">RFC2616</a>).</p> + */ + public interface Status { + + /** + * <p>1xx: Informational - Request received, continuing process.</p> + */ + int CONTINUE = 100; + int SWITCHING_PROTOCOLS = 101; + int PROCESSING = 102; + + /** + * <p>2xx: Success - The action was successfully received, understood, and accepted.</p> + */ + int OK = 200; + int CREATED = 201; + int ACCEPTED = 202; + int NON_AUTHORITATIVE_INFORMATION = 203; + int NO_CONTENT = 204; + int RESET_CONTENT = 205; + int PARTIAL_CONTENT = 206; + int MULTI_STATUS = 207; + + /** + * <p>3xx: Redirection - Further action must be taken in order to complete the request.</p> + */ + int MULTIPLE_CHOICES = 300; + int MOVED_PERMANENTLY = 301; + int FOUND = 302; + int SEE_OTHER = 303; + int NOT_MODIFIED = 304; + int USE_PROXY = 305; + int TEMPORARY_REDIRECT = 307; + + /** + * <p>4xx: Client Error - The request contains bad syntax or cannot be fulfilled.</p> + */ + int BAD_REQUEST = 400; + int UNAUTHORIZED = 401; + int PAYMENT_REQUIRED = 402; + int FORBIDDEN = 403; + int NOT_FOUND = 404; + int METHOD_NOT_ALLOWED = 405; + int NOT_ACCEPTABLE = 406; + int PROXY_AUTHENTICATION_REQUIRED = 407; + int REQUEST_TIMEOUT = 408; + int CONFLICT = 409; + int GONE = 410; + int LENGTH_REQUIRED = 411; + int PRECONDITION_FAILED = 412; + int REQUEST_TOO_LONG = 413; + int REQUEST_URI_TOO_LONG = 414; + int UNSUPPORTED_MEDIA_TYPE = 415; + int REQUESTED_RANGE_NOT_SATISFIABLE = 416; + int EXPECTATION_FAILED = 417; + int INSUFFICIENT_SPACE_ON_RESOURCE = 419; + int METHOD_FAILURE = 420; + int UNPROCESSABLE_ENTITY = 422; + int LOCKED = 423; + int FAILED_DEPENDENCY = 424; + + /** + * <p>5xx: Server Error - The server failed to fulfill an apparently valid request.</p> + */ + int INTERNAL_SERVER_ERROR = 500; + int NOT_IMPLEMENTED = 501; + int BAD_GATEWAY = 502; + int SERVICE_UNAVAILABLE = 503; + int GATEWAY_TIMEOUT = 504; + int VERSION_NOT_SUPPORTED = 505; + int INSUFFICIENT_STORAGE = 507; + } + + private final Map<String, Object> context = new HashMap<>(); + private final HeaderFields headers = new HeaderFields(); + private Throwable error; + private int status; + + /** + * <p>Creates a new instance of this class.</p> + * + * @param status The status code to assign to this. + */ + public Response(int status) { + this(status, null); + } + + /** + * <p>Creates a new instance of this class.</p> + * + * @param status The status code to assign to this. + * @param error The error to assign to this. + */ + public Response(int status, Throwable error) { + this.status = status; + this.error = error; + } + + /** + * <p>Returns the named application context objects. This data is not intended for network transport, rather they + * are intended for passing shared data between components of an Application.</p> + * + * <p>Modifying the context map is a thread-unsafe operation -- any changes made after calling {@link + * ResponseHandler#handleResponse(Response)} might never become visible to other threads, and might throw + * ConcurrentModificationExceptions in other threads.</p> + * + * @return The context map. + */ + public Map<String, Object> context() { + return context; + } + + /** + * <p>Returns the set of header fields of this Request. These are the meta-data of the Request, and are not applied + * to any internal {@link Container} logic. Modifying headers is a thread-unsafe operation -- any changes made after + * calling {@link ResponseHandler#handleResponse(Response)} might never become visible to other threads, and might + * throw ConcurrentModificationExceptions in other threads.</p> + * + * @return The header fields. + */ + public HeaderFields headers() { + return headers; + } + + /** + * <p>Returns the status code of this response. This is an integer result code of the attempt to understand and + * satisfy the corresponding {@link Request}. It is encouraged, although not enforced, to use the built-in {@link + * Status} codes whenever possible.</p> + * + * @return The status code. + * @see #setStatus(int) + */ + public int getStatus() { + return status; + } + + /** + * <p>Sets the status code of this response. This is an integer result code of the attempt to understand and + * satisfy the corresponding {@link Request}. It is encouraged, although not enforced, to use the built-in {@link + * Status} codes whenever possible. </p> + * + * <p>Because access to this field is not guarded by any lock, any changes made after calling {@link + * ResponseHandler#handleResponse(Response)} might never become visible to other threads.</p> + * + * @param status The status code to set. + * @return This, to allow chaining. + * @see #getStatus() + */ + public Response setStatus(int status) { + this.status = status; + return this; + } + + /** + * <p>Returns the error of this response, or null if none has been set. This is typically non-null if the status + * indicates an unsuccessful response.</p> + * + * @return The error. + * @see #getError() + */ + public Throwable getError() { + return error; + } + + /** + * <p>Sets the error of this response. It is encouraged, although not enforced, to use this field to attach + * additional information to an unsuccessful response.</p> + * + * <p>Because access to this field is not guarded by any lock, any changes made after calling {@link + * ResponseHandler#handleResponse(Response)} might never become visible to other threads.</p> + * + * @param error The error to set. + * @return This, to allow chaining. + * @see #getError() + */ + public Response setError(Throwable error) { + this.error = error; + return this; + } + + /** + * <p>This is a convenience method for creating a Response with status {@link Status#REQUEST_TIMEOUT} and passing + * that to the given {@link ResponseHandler#handleResponse(Response)}. For trivial implementations of {@link + * RequestHandler#handleTimeout(Request, ResponseHandler)}, simply call this method.</p> + * + * @param handler The handler to pass the timeout {@link Response} to. + */ + public static void dispatchTimeout(ResponseHandler handler) { + ResponseDispatch.newInstance(Status.REQUEST_TIMEOUT).dispatch(handler); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/SharedResource.java b/jdisc_core/src/main/java/com/yahoo/jdisc/SharedResource.java new file mode 100644 index 00000000000..4552ba3fe3a --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/SharedResource.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.DeactivatedContainer; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.service.ClientProvider; +import com.yahoo.jdisc.service.ServerProvider; + +/** + * <p>This interface defines a reference counted resource. This is the parent interface of {@link RequestHandler}, + * {@link ClientProvider} and {@link ServerProvider}, and is used by jDISC to appropriately signal resources as they + * become candidates for deallocation. As a {@link ContainerBuilder} is {@link + * ContainerActivator#activateContainer(ContainerBuilder) activated}, all its components are {@link #refer() retained} + * by that {@link Container}. Once a {@link DeactivatedContainer} terminates, all of that Container's components are + * {@link ResourceReference#close() released}. This resource tracking allows an Application to implement a significantly + * simpler scheme for managing its resources than would otherwise be possible.</p> + * + * <p>Objects are created with an initial reference count of 1, representing the reference held by the object creator. + * + * <p>You should not really think about the management of resources in terms of reference counting, instead think of it + * in terms of resource ownership. You retain a resource to prevent it from being destroyed while you are using it, and + * you release a resource once you are done using it.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface SharedResource { + public static final String SYSTEM_PROPERTY_NAME_DEBUG = "jdisc.debug.resources"; + public static final boolean DEBUG = Boolean.valueOf(System.getProperty(SYSTEM_PROPERTY_NAME_DEBUG)); + + /** + * <p>Increments the reference count of this resource. You call this method to prevent an object from being + * destroyed until you have finished using it.</p> + * + * <p>You MUST keep the returned {@link ResourceReference} object and release the reference by calling + * {@link ResourceReference#close()} on it. A reference created by this method can NOT be released by calling + * {@link #release()}.</p> + * + * @see ResourceReference#close() + */ + ResourceReference refer(); + + /** + * <p>Releases the "main" reference to this resource (the implicit reference due to creation of the object).</p> + * + * <p>References obtained by calling {@link #refer()} must be released by calling {@link ResourceReference#close()} + * on the {@link ResourceReference} returned from {@link #refer()}, NOT by calling this method. You call this + * method once you are done using an object that you have previously caused instantiation of.</p> + * + * @see ResourceReference + */ + void release(); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/TimeoutManager.java b/jdisc_core/src/main/java/com/yahoo/jdisc/TimeoutManager.java new file mode 100644 index 00000000000..4bca8136b8f --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/TimeoutManager.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.yahoo.jdisc.handler.RequestHandler; + +import java.util.concurrent.TimeUnit; + +/** + * <p>This interface provides a callback for when the {@link Request#setTimeout(long, TimeUnit)} is invoked. If no such + * handler is registered at the time where the target {@link RequestHandler} is called, the default timeout manager will + * be injected.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public interface TimeoutManager { + + /** + * Schedule timeout management for a request. + * This is called by a request whenever {@link Request#setTimeout(long, TimeUnit)} is invoked; + * this may be called multiple times for the same {@link Request}. + * + * @param request the request whose timeout to schedule + */ + public void scheduleTimeout(Request request); + + /** + * Unschedule timeout management for a previously scheduled request. + * This is called whenever a request is cancelled, and the purpose is to free up + * resources taken by the implementation of this associated with the request. + * <p> + * This is only called once for a request, and only after at least one scheduleTimeout call. + * <p> + * The default implementation of this does nothing. + * + * @param request the previously scheduled timeout + */ + default public void unscheduleTimeout(Request request) { + } + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java new file mode 100644 index 00000000000..1c42221e735 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.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.jdisc; + +import com.google.inject.ImplementedBy; +import com.yahoo.jdisc.core.SystemTimer; + +/** + * <p>This class provides access to the current time in milliseconds, as viewed by the {@link Container}. Inject an + * instance of this class into any component that needs to access time, instead of using + * <code>System.currentTimeMillis()</code>.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +@ImplementedBy(SystemTimer.class) +public interface Timer { + + /** + * <p>Returns the current time in milliseconds. Note that while the unit of time of the return value is a + * millisecond, the granularity of the value depends on the underlying operating system and may be larger. For + * example, many operating systems measure time in units of tens of milliseconds.</p> + * + * <p> See the description of the class <code>Date</code> for a discussion of slight discrepancies that may arise + * between "computer time" and coordinated universal time (UTC).</p> + * + * @return The difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC. + * @see java.util.Date + */ + public long currentTimeMillis(); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/AbstractApplication.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/AbstractApplication.java new file mode 100644 index 00000000000..240ee605174 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/AbstractApplication.java @@ -0,0 +1,108 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.Inject; +import com.yahoo.jdisc.service.CurrentContainer; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * <p>This class is a convenient parent class for {@link Application} developers that require simple access to the most + * commonly used jDISC APIs.</p> + * + * <p>A simple hello world application could be implemented like this:</p> + * <pre> + * class HelloApplication extends AbstractApplication { + * + * @Inject + * public HelloApplication(BundleInstaller bundleInstaller, ContainerActivator activator, + * CurrentContainer container) { + * super(bundleInstaller, activator, container); + * } + * + * @Override + * public void start() { + * ContainerBuilder builder = newContainerBuilder(); + * ServerProvider myServer = new MyHttpServer(); + * builder.serverProviders().install(myServer); + * builder.serverBindings().bind("http://*/*", new MyHelloWorldHandler()); + * + * activateContainer(builder); + * myServer.start(); + * myServer.release(); + * } + * } + * </pre> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class AbstractApplication implements Application { + + private final CountDownLatch destroyed = new CountDownLatch(1); + private final BundleInstaller bundleInstaller; + private final ContainerActivator activator; + private final CurrentContainer container; + + @Inject + protected AbstractApplication(BundleInstaller bundleInstaller, ContainerActivator activator, + CurrentContainer container) { + this.bundleInstaller = bundleInstaller; + this.activator = activator; + this.container = container; + } + + @Override + public void stop() { + + } + + @Override + public final void destroy() { + destroyed.countDown(); + } + + public final List<Bundle> installAndStartBundle(String... locations) throws BundleException { + return installAndStartBundle(Arrays.asList(locations)); + } + + public final List<Bundle> installAndStartBundle(Iterable<String> locations) throws BundleException { + return bundleInstaller.installAndStart(locations); + } + + public final void stopAndUninstallBundle(Bundle... bundles) throws BundleException { + stopAndUninstallBundle(Arrays.asList(bundles)); + } + + public final void stopAndUninstallBundle(Iterable<Bundle> bundles) throws BundleException { + bundleInstaller.stopAndUninstall(bundles); + } + + public final ContainerBuilder newContainerBuilder() { + return activator.newContainerBuilder(); + } + + public final DeactivatedContainer activateContainer(ContainerBuilder builder) { + return activator.activateContainer(builder); + } + + public final CurrentContainer container() { + return container; + } + + public final boolean isTerminated() { + return destroyed.getCount() == 0; + } + + public final boolean awaitTermination(int timeout, TimeUnit unit) throws InterruptedException { + return destroyed.await(timeout, unit); + } + + public final void awaitTermination() throws InterruptedException { + destroyed.await(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/Application.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/Application.java new file mode 100644 index 00000000000..f70e3c90884 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/Application.java @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.core.ApplicationLoader; +import com.yahoo.jdisc.service.ClientProvider; +import com.yahoo.jdisc.service.ServerProvider; + +/** + * <p>This interface defines the API of the singleton Application that runs in a jDISC instance. An Application instance + * will always have its {@link #destroy()} method called, regardless of whether {@link #start()} or {@link #stop()} + * threw any exceptions.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface Application { + + /** + * <p>This method is called by the {@link ApplicationLoader} just after creating this Application instance. Use this + * method to start the Application's worker thread, and to activate a {@link Container}. If you attempt to call + * {@link ContainerActivator#activateContainer(ContainerBuilder)} before this method is invoked, that call will + * throw an {@link ApplicationNotReadyException}. If this method does not throw an exception, the {@link #stop()} + * method will be called at some time in the future.</p> + */ + void start(); + + /** + * <p>This method is called by the {@link ApplicationLoader} after the corresponding signal has been issued by the + * controlling start script. Once this method returns, all calls to {@link + * ContainerActivator#activateContainer(ContainerBuilder)} will throw {@link ApplicationNotReadyException}s. Use + * this method to prepare for termination (see {@link #destroy()}).</p> + */ + void stop(); + + /** + * <p>This method is called by the {@link ApplicationLoader} after first calling {@link #stop()}, and all previous + * {@link DeactivatedContainer}s have terminated. Use this method to shut down all Application components such as + * {@link ClientProvider}s and {@link ServerProvider}s.</p> + */ + void destroy(); + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ApplicationNotReadyException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ApplicationNotReadyException.java new file mode 100644 index 00000000000..fbd5f1b00c6 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ApplicationNotReadyException.java @@ -0,0 +1,19 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +/** + * This exception is used to signal that no {@link Application} has been configured. An instance of this class will be + * thrown by the {@link ContainerActivator#activateContainer(ContainerBuilder)} method if it is called before the call + * to {@link Application#start()} or after the call to {@link Application#stop()}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class ApplicationNotReadyException extends RuntimeException { + + /** + * Constructs a new instance of this class with a detail message. + */ + public ApplicationNotReadyException() { + super("Application not ready."); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingMatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingMatch.java new file mode 100644 index 00000000000..679fb52f0e7 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingMatch.java @@ -0,0 +1,64 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import java.net.URI; +import java.util.Objects; + +/** + * <p>This class holds the result of a {@link BindingSet#match(URI)} operation. It contains methods to inspect the + * groups captured during matching, where a <em>group</em> is defined as a sequence of characters matches by a wildcard + * in the {@link UriPattern}, and to retrieve the matched target.</p> + * + * @param <T> The class of the target. + */ +public class BindingMatch<T> { + + private final UriPattern.Match match; + private final T target; + + /** + * <p>Constructs a new instance of this class.</p> + * + * @param match The match information for this instance. + * @param target The target of this match. + * @throws NullPointerException If any argument is null. + */ + public BindingMatch(UriPattern.Match match, T target) { + Objects.requireNonNull(match, "match"); + Objects.requireNonNull(target, "target"); + this.match = match; + this.target = target; + } + + /** + * <p>Returns the number of captured groups of this match. Any non-negative integer smaller than the value returned + * by this method is a valid group index for this match.</p> + * + * @return The number of captured groups. + */ + public int groupCount() { + return match.groupCount(); + } + + /** + * <p>Returns the input subsequence captured by the given group by this match. Groups are indexed from left to + * right, starting at zero. Note that some groups may match an empty string, in which case this method returns the + * empty string. This method never returns null.</p> + * + * @param idx The index of the group to return. + * @return The (possibly empty) substring captured by the group during matching, never <tt>null</tt>. + * @throws IndexOutOfBoundsException If there is no group in the match with the given index. + */ + public String group(int idx) { + return match.group(idx); + } + + /** + * <p>Returns the matched target.</p> + * + * @return The matched target. + */ + public T target() { + return target; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingRepository.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingRepository.java new file mode 100644 index 00000000000..75d687eb619 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingRepository.java @@ -0,0 +1,110 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.handler.RequestHandler; + +import java.net.URI; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Logger; + +/** + * <p>This is a mutable repository of bindings from {@link UriPattern}s to some target type T. The {@link + * ContainerBuilder} has a mapping of named instances of this class for {@link RequestHandler}s, and is used to + * configure the set of {@link BindingSet}s that eventually become part of the active {@link Container}.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BindingRepository<T> implements Iterable<Map.Entry<UriPattern, T>> { + + private static final Logger log = Logger.getLogger(BindingRepository.class.getName()); + + private final Map<UriPattern, T> bindings = new HashMap<>(); + + /** + * <p>Creates a {@link UriPattern} from the given pattern string, and calls {@link #put(UriPattern, Object)}.</p> + * + * @param uriPattern The URI pattern to parse and bind to the target. + * @param target The target to assign to the URI pattern. + * @throws NullPointerException If any argument is null. + * @throws IllegalArgumentException If the URI pattern string could not be parsed. + */ + public void bind(String uriPattern, T target) { + put(new UriPattern(uriPattern), target); + } + + /** + * <p>Convenient method for calling {@link #bind(String, Object)} for all entries in a collection of bindings.</p> + * + * @param bindings The collection of bindings to copy to this. + * @throws NullPointerException If argument is null or contains null. + */ + public void bindAll(Map<String, T> bindings) { + for (Map.Entry<String, T> entry : bindings.entrySet()) { + bind(entry.getKey(), entry.getValue()); + } + } + + /** + * <p>Binds the given target to the given {@link UriPattern}. Although all bindings will eventually be evaluated by + * a call to {@link BindingSet#resolve(URI)}, where matching order is significant, the order in which bindings are + * added is NOT. Instead, the creation of the {@link BindingSet} in {@link #activate()} sorts the bindings in such a + * way that the more strict patterns are evaluated first. See class-level commentary on {@link UriPattern} for more + * on this. + * + * @param uriPattern The URI pattern to parse and bind to the target. + * @param target The target to assign to the URI pattern. + * @throws NullPointerException If any argument is null. + * @throws IllegalArgumentException If the pattern has already been bound to another target. + */ + public void put(UriPattern uriPattern, T target) { + Objects.requireNonNull(uriPattern, "uriPattern"); + Objects.requireNonNull(target, "target"); + if (bindings.containsKey(uriPattern)) { + T boundTarget = bindings.get(uriPattern); + log.info("Pattern '" + uriPattern + "' was already bound to target of class " + boundTarget.getClass().getName() + + ", and will NOT be bound to target of class " + target.getClass().getName()); + } else { + bindings.put(uriPattern, target); + } + } + + /** + * <p>Convenient method for calling {@link #put(UriPattern, Object)} for all entries in a collection of + * bindings.</p> + * + * @param bindings The collection of bindings to copy to this. + * @throws NullPointerException If argument is null or contains null. + */ + public void putAll(Iterable<Map.Entry<UriPattern, T>> bindings) { + for (Map.Entry<UriPattern, T> entry : bindings) { + put(entry.getKey(), entry.getValue()); + } + } + + /** + * <p>Creates and returns an immutable {@link BindingSet} that contains the bindings of this BindingRepository. + * Notice that the BindingSet uses a snapshot of the current bindings so that this repository remains mutable and + * reusable.</p> + * + * @return The created BindingSet instance. + */ + public BindingSet<T> activate() { + return new BindingSet<>(bindings.entrySet()); + } + + /** + * Removes all bindings from this repository. + */ + public void clear() { + bindings.clear(); + } + + @Override + public Iterator<Map.Entry<UriPattern, T>> iterator() { + return bindings.entrySet().iterator(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSet.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSet.java new file mode 100644 index 00000000000..b14a832b1d4 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSet.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.jdisc.application; + +import com.google.common.collect.ImmutableList; + +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * <p>This is an immutable set of ordered bindings from {@link UriPattern}s to some target type T. To create an instance + * of this class, you must 1) create a {@link BindingRepository}, 2) configure it using the {@link + * BindingRepository#bind(String, Object)} method, and finally 3) call {@link BindingRepository#activate()}.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BindingSet<T> implements Iterable<Map.Entry<UriPattern, T>> { + + public static final String DEFAULT = "default"; + private final Collection<Map.Entry<UriPattern, T>> bindings; + + BindingSet(Collection<Map.Entry<UriPattern, T>> bindings) { + this.bindings = sort(bindings); + } + + /** + * <p>Resolves the binding that best matches (see commentary on {@link BindingRepository#bind(String, Object)}) the + * given {@link URI}, and returns a {@link BindingMatch} object that describes the match and contains the + * matched target. If there is no binding that matches the given URI, this method returns null.</p> + * + * @param uri The URI to match against the bindings in this set. + * @return A {@link BindingMatch} object describing the match found, or null if not found. + */ + public BindingMatch<T> match(URI uri) { + for (Map.Entry<UriPattern, T> entry : bindings) { + UriPattern.Match match = entry.getKey().match(uri); + if (match != null) { + return new BindingMatch<>(match, entry.getValue()); + } + } + return null; + } + + /** + * <p>Resolves the binding that best matches (see commentary on {@link BindingRepository#bind(String, Object)}) the + * given {@link URI}, and returns that target. If there is no binding that matches the given URI, this method + * returns null.</p> + * + * <p>Apart from a <em>null</em>-guard, this is equal to <code>return match(uri).target()</code>.</p> + * + * @param uri The URI to match against the bindings in this set. + * @return The best matched target, or null. + * @see #match(URI) + */ + public T resolve(URI uri) { + BindingMatch<T> match = match(uri); + if (match == null) { + return null; + } + return match.target(); + } + + @Override + public Iterator<Map.Entry<UriPattern, T>> iterator() { + return bindings.iterator(); + } + + private static <T> Collection<Map.Entry<UriPattern, T>> sort(Collection<Map.Entry<UriPattern, T>> unsorted) { + List<Map.Entry<UriPattern, T>> ret = new LinkedList<>(unsorted); + Collections.sort(ret, new Comparator<Map.Entry<UriPattern, ?>>() { + + @Override + public int compare(Map.Entry<UriPattern, ?> lhs, Map.Entry<UriPattern, ?> rhs) { + return lhs.getKey().compareTo(rhs.getKey()); + } + }); + return ImmutableList.copyOf(ret); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSetSelector.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSetSelector.java new file mode 100644 index 00000000000..a480d3968c9 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSetSelector.java @@ -0,0 +1,33 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.ImplementedBy; +import com.google.inject.Module; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.core.DefaultBindingSelector; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.service.NoBindingSetSelectedException; + +import java.net.URI; + +/** + * This interface defines the component that is used by the {@link CurrentContainer} to assign a {@link BindingSet} to a + * newly created {@link Container} based on the given {@link URI}. The default implementation of this interface returns + * {@link BindingSet#DEFAULT} regardless of input. To specify your own selector you need to {@link + * GuiceRepository#install(Module) install} a Guice {@link Module} that provides a binding for this interface. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +@ImplementedBy(DefaultBindingSelector.class) +public interface BindingSetSelector { + + /** + * Returns the name of the {@link BindingSet} to assign to the {@link Container} for the given {@link URI}. If this + * method returns <em>null</em>, the corresponding call to {@link CurrentContainer#newReference(URI)} will throw a + * {@link NoBindingSetSelectedException}. + * + * @param uri The URI to select on. + * @return The name of selected BindingSet. + */ + public String select(URI uri); +}
\ No newline at end of file diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstallationException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstallationException.java new file mode 100644 index 00000000000..deb0f4554ff --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstallationException.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.common.collect.ImmutableList; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * <p>This exception is thrown by {@link OsgiFramework#installBundle(String)} if installation failed. Because </p> + * + * <p>Please see commentary on {@link OsgiFramework#installBundle(String)} and {@link + * OsgiFramework#startBundles(java.util.List, boolean)} for a description of exception-safety issues to consider when + * installing bundles that use the {@link OsgiHeader#PREINSTALL_BUNDLE} manifest instruction.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public final class BundleInstallationException extends BundleException { + + private final List<Bundle> installedBundles; + + public BundleInstallationException(Collection<Bundle> installedBundles, Throwable cause) { + super(cause.getMessage(), cause); + this.installedBundles = ImmutableList.copyOf(installedBundles); + } + + public List<Bundle> installedBundles() { + return installedBundles; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstaller.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstaller.java new file mode 100644 index 00000000000..273d29e8dfb --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstaller.java @@ -0,0 +1,86 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.Inject; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * <p>This is a utility class to help with installing, starting, stopping and uninstalling OSGi Bundles. You can choose + * to inject an instance of this class, or it can be created explicitly by reference to a {@link OsgiFramework}.</p> + * + * <p>Please see commentary on {@link OsgiFramework#installBundle(String)} for a description of exception-safety issues + * to consider when installing bundles that use the {@link OsgiHeader#PREINSTALL_BUNDLE} manifest instruction.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class BundleInstaller { + + private final OsgiFramework osgiFramework; + + @Inject + public BundleInstaller(OsgiFramework osgiFramework) { + this.osgiFramework = osgiFramework; + } + + public List<Bundle> installAndStart(String... locations) throws BundleException { + return installAndStart(Arrays.asList(locations)); + } + + public List<Bundle> installAndStart(Iterable<String> locations) throws BundleException { + List<Bundle> bundles = new LinkedList<>(); + try { + for (String location : locations) { + bundles.addAll(osgiFramework.installBundle(location)); + } + } catch (BundleInstallationException e) { + bundles.addAll(e.installedBundles()); + throw new BundleInstallationException(bundles, e); + } catch (Exception e) { + throw new BundleInstallationException(bundles, e); + } + try { + for (Bundle bundle : bundles) { + start(bundle); + } + } catch (Exception e) { + throw new BundleInstallationException(bundles, e); + } + return bundles; + } + + public void stopAndUninstall(Bundle... bundles) throws BundleException { + stopAndUninstall(Arrays.asList(bundles)); + } + + public void stopAndUninstall(Iterable<Bundle> bundles) throws BundleException { + for (Bundle bundle : bundles) { + stop(bundle); + } + for (Bundle bundle : bundles) { + bundle.uninstall(); + } + } + + private void start(Bundle bundle) throws BundleException { + if (bundle.getState() == Bundle.ACTIVE) { + throw new BundleException("OSGi bundle " + bundle.getSymbolicName() + " already started."); + } + if (!OsgiHeader.asList(bundle, OsgiHeader.APPLICATION).isEmpty()) { + throw new BundleException("OSGi header '" + OsgiHeader.APPLICATION + "' not allowed for " + + "non-application bundle " + bundle.getSymbolicName() + "."); + } + osgiFramework.startBundles(Arrays.asList(bundle), false); + } + + private void stop(Bundle bundle) throws BundleException { + if (bundle.getState() != Bundle.ACTIVE) { + throw new BundleException("OSGi bundle " + bundle.getSymbolicName() + " not started."); + } + bundle.stop(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerActivator.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerActivator.java new file mode 100644 index 00000000000..105ce5c8d0f --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerActivator.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.jdisc.application; + +import com.yahoo.jdisc.Container; + +/** + * <p>This interface defines the API for changing the active {@link Container} of a jDISC application. An instance of + * this class is typically injected into the {@link Application} constructor. If injection is unavailable due to an + * Application design, an instance of this class is also available as an OSGi service under the full ContainerActivator + * class name.</p> + * + * <p>This interface allows one to create and active a new Container. To do so, one has to 1) call {@link + * #newContainerBuilder()}, 2) configure the returned {@link ContainerBuilder}, and 3) pass the builder to the {@link + * #activateContainer(ContainerBuilder)} method.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ContainerActivator { + + /** + * <p>This method creates and returns a new {@link ContainerBuilder} object that has the necessary references to the + * application and its internal components.</p> + * + * @return The created builder. + */ + public ContainerBuilder newContainerBuilder(); + + /** + * <p>Creates and activates a {@link Container} based on the provided {@link ContainerBuilder}. By providing a + * <em>null</em> argument, this method can be used to deactivate the current Container. The returned object can be + * used to schedule a cleanup task that is executed once the the deactivated Container has terminated.</p> + * + * @param builder The builder to activate. + * @return The previous container, if any. + * @throws ApplicationNotReadyException If this method is called before {@link Application#start()} or after {@link + * Application#stop()}. + */ + public DeactivatedContainer activateContainer(ContainerBuilder builder); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerBuilder.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerBuilder.java new file mode 100644 index 00000000000..f3b1e03b30f --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerBuilder.java @@ -0,0 +1,133 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.Module; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.handler.RequestHandler; + +import java.util.*; +import java.util.concurrent.ThreadFactory; + +/** + * <p>This is the inactive, mutable {@link Container}. Because it requires references to the application internals, it + * should always be injected by guice or created by calling {@link ContainerActivator#newContainerBuilder()}. Once the + * builder has been configured, it is activated by calling {@link + * ContainerActivator#activateContainer(ContainerBuilder)}. You may use the {@link #setAppContext(Object)} method to + * attach an arbitrary object to a Container, which will be available in the corresponding {@link + * DeactivatedContainer}.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerBuilder { + + private final GuiceRepository guiceModules = new GuiceRepository(); + private final ServerRepository serverProviders = new ServerRepository(guiceModules); + private final Map<String, BindingRepository<RequestHandler>> serverBindings = new HashMap<>(); + private final Map<String, BindingRepository<RequestHandler>> clientBindings = new HashMap<>(); + private Object appContext = null; + + public ContainerBuilder(Iterable<Module> guiceModules) { + this.guiceModules.installAll(guiceModules); + this.guiceModules.install(new AbstractModule() { + + @Override + public void configure() { + bind(ContainerBuilder.class).toInstance(ContainerBuilder.this); + } + }); + this.serverBindings.put(BindingSet.DEFAULT, new BindingRepository<RequestHandler>()); + this.clientBindings.put(BindingSet.DEFAULT, new BindingRepository<RequestHandler>()); + } + + public void setAppContext(Object ctx) { + appContext = ctx; + } + + public Object appContext() { + return appContext; + } + + public GuiceRepository guiceModules() { + return guiceModules; + } + + public <T> T getInstance(Key<T> key) { + return guiceModules.getInstance(key); + } + + public <T> T getInstance(Class<T> type) { + return guiceModules.getInstance(type); + } + + public ServerRepository serverProviders() { + return serverProviders; + } + + public BindingRepository<RequestHandler> serverBindings() { + return serverBindings.get(BindingSet.DEFAULT); + } + + public BindingRepository<RequestHandler> serverBindings(String setName) { + BindingRepository<RequestHandler> ret = serverBindings.get(setName); + if (ret == null) { + ret = new BindingRepository<>(); + serverBindings.put(setName, ret); + } + return ret; + } + + public Map<String, BindingSet<RequestHandler>> activateServerBindings() { + Map<String, BindingSet<RequestHandler>> ret = new HashMap<>(); + for (Map.Entry<String, BindingRepository<RequestHandler>> entry : serverBindings.entrySet()) { + ret.put(entry.getKey(), entry.getValue().activate()); + } + return ImmutableMap.copyOf(ret); + } + + public BindingRepository<RequestHandler> clientBindings() { + return clientBindings.get(BindingSet.DEFAULT); + } + + public BindingRepository<RequestHandler> clientBindings(String setName) { + BindingRepository<RequestHandler> ret = clientBindings.get(setName); + if (ret == null) { + ret = new BindingRepository<>(); + clientBindings.put(setName, ret); + } + return ret; + } + + public Map<String, BindingSet<RequestHandler>> activateClientBindings() { + Map<String, BindingSet<RequestHandler>> ret = new HashMap<>(); + for (Map.Entry<String, BindingRepository<RequestHandler>> entry : clientBindings.entrySet()) { + ret.put(entry.getKey(), entry.getValue().activate()); + } + return ImmutableMap.copyOf(ret); + } + + @SuppressWarnings({ "unchecked" }) + public static <T> Class<T> safeClassCast(Class<T> baseClass, Class<?> someClass) { + if (!baseClass.isAssignableFrom(someClass)) { + throw new IllegalArgumentException("Expected " + baseClass.getName() + ", got " + + someClass.getName() + "."); + } + return (Class<T>)someClass; + } + + public static List<String> safeStringSplit(Object obj, String delim) { + if (!(obj instanceof String)) { + return Collections.emptyList(); + } + List<String> lst = new LinkedList<>(); + for (String str : ((String)obj).split(delim)) { + str = str.trim(); + if (!str.isEmpty()) { + lst.add(str); + } + } + return lst; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerThread.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerThread.java new file mode 100644 index 00000000000..38527acc099 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerThread.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * <p>This class decorates {@link Thread} to allow for internal jDISC optimizations. Whenever possible a jDISC + * application should use this class instead of Thread. The {@link ContainerThread.Factory} class is a helper-class for + * working with the {@link Executors} framework.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerThread extends Thread { + + private final MetricConsumer consumer; + + /** + * <p>Allocates a new ContainerThread object. This constructor calls the parent {@link Thread#Thread(Runnable)} + * constructor.</p> + * + * @param target The object whose <code>run</code> method is called. + * @param consumer The MetricConsumer of this thread. + */ + public ContainerThread(Runnable target, MetricConsumer consumer) { + super(target); + this.consumer = consumer; + } + + /** + * <p>Returns the {@link MetricConsumer} of this. Note that this may be null.</p> + * + * @return The MetricConsumer of this, or null. + */ + public MetricConsumer consumer() { + return consumer; + } + + /** + * <p>This class implements the {@link ThreadFactory} interface on top of a {@link Provider} for {@link + * MetricConsumer} instances.</p> + */ + public static class Factory implements ThreadFactory { + + private final Provider<MetricConsumer> provider; + + @Inject + public Factory(Provider<MetricConsumer> provider) { + this.provider = provider; + } + + @Override + public Thread newThread(Runnable target) { + return new ContainerThread(target, provider.get()); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/DeactivatedContainer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/DeactivatedContainer.java new file mode 100644 index 00000000000..5f43a8644e6 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/DeactivatedContainer.java @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.ContentChannel; + +/** + * <p>This interface represents a {@link Container} which has been deactivated. An instance of this class is returned by + * the {@link ContainerActivator#activateContainer(ContainerBuilder)} method, and is used to schedule a cleanup task + * that is executed once the the deactivated Container has terminated.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface DeactivatedContainer { + + /** + * <p>Returns the context object that was previously attached to the corresponding {@link ContainerBuilder} through + * the {@link ContainerBuilder#setAppContext(Object)} method. This is useful for tracking {@link Application} + * specific resources that are to be tracked alongside a {@link Container}.</p> + * + * @return The Application context. + */ + Object appContext(); + + /** + * <p>Schedules the given {@link Runnable} to execute once this DeactivatedContainer has terminated. A + * DeactivatedContainer is considered to have terminated once there are no more {@link Request}s, {@link Response}s + * or corresponding {@link ContentChannel}s being processed by components that belong to it.</p> + * + * <p>If termination has already occured, this method immediately runs the given Runnable in the current thread.</p> + * + * @param task The task to run once this DeactivatedContainer has terminated. + */ + void notifyTermination(Runnable task); + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/GlobPattern.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/GlobPattern.java new file mode 100644 index 00000000000..101825328b4 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/GlobPattern.java @@ -0,0 +1,191 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class GlobPattern implements Comparable<GlobPattern> { + + private static final GlobPattern WILDCARD = new WildcardPattern(); + protected final String[] parts; + + private GlobPattern(String... parts) { + this.parts = parts; + } + + public final Match match(String text) { + return match(text, 0); + } + + public Match match(String text, int offset) { + int[] pos = new int[parts.length - 1 << 1]; + if (!matches(text, offset, 0, pos)) { + return null; + } + return new Match(text, pos); + } + + private boolean matches(String text, int textIdx, int partIdx, int[] out) { + String part = parts[partIdx]; + if (partIdx == parts.length - 1 && part.isEmpty()) { + out[partIdx - 1 << 1 | 1] = text.length(); + return true; // optimize trailing wildcard + } + int partEnd = textIdx + part.length(); + if (partEnd > text.length()|| !text.startsWith(part, textIdx)) { + return false; + } + if (partIdx == parts.length - 1) { + return partEnd == text.length(); + } + out[partIdx << 1] = partEnd; + for (int i = partEnd; i <= text.length(); ++i) { + out[partIdx << 1 | 1] = i; + if (matches(text, i, partIdx + 1, out)) { + return true; + } + } + return false; + } + + @Override + public int compareTo(GlobPattern rhs) { + // wildcard pattern always orders last + if (parts.length == 0 || rhs.parts.length == 0) { + return rhs.parts.length - parts.length; + } + // next is trailing wildcard + int cmp = compare(parts[parts.length - 1], rhs.parts[rhs.parts.length - 1], false); + if (cmp != 0) { + return cmp; + } + // then comes part comparison + for (int i = 0; i < parts.length && i < rhs.parts.length; ++i) { + cmp = compare(parts[i], rhs.parts[i], true); + if (cmp != 0) { + return cmp; + } + } + // one starts with the other, sort longest first + return rhs.parts.length - parts.length; + } + + private static int compare(String lhs, String rhs, boolean compareNonEmpty) { + if ((lhs.isEmpty() || rhs.isEmpty()) && !lhs.equals(rhs)) { + return rhs.length() - lhs.length(); + } + if (!compareNonEmpty) { + return 0; + } + return rhs.compareTo(lhs); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof GlobPattern)) { + return false; + } + GlobPattern rhs = (GlobPattern)obj; + if (!Arrays.equals(parts, rhs.parts)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(parts); + } + + @Override + public String toString() { + StringBuilder ret = new StringBuilder(); + for (int i = 0; i < parts.length; ++i) { + ret.append(parts[i]); + if (i < parts.length - 1) { + ret.append("*"); + } + } + return ret.toString(); + } + + public static Match match(String glob, String text) { + return compile(glob).match(text); + } + + public static GlobPattern compile(String pattern) { + if (pattern.equals("*")) { + return WILDCARD; + } + if (pattern.indexOf('*') < 0) { + return new VerbatimPattern(pattern); + } + List<String> arr = new LinkedList<>(); + for (int prev = 0, next = 0; next <= pattern.length(); ++next) { + if (next == pattern.length() || pattern.charAt(next) == '*') { + arr.add(pattern.substring(prev, next)); + prev = next + 1; + } + } + return new GlobPattern(arr.toArray(new String[arr.size()])); + } + + public static class Match { + + private final String str; + private final int[] pos; + + private Match(String str, int[] pos) { + this.str = str; + this.pos = pos; + } + + public int groupCount() { + return pos.length >> 1; + } + + public String group(int idx) { + return str.substring(pos[idx << 1], pos[idx << 1 | 1]); + } + } + + private static class VerbatimPattern extends GlobPattern { + + VerbatimPattern(String value) { + super(value); + } + + @Override + public Match match(String text, int offset) { + int len = text.length() - offset; + if (len != parts[0].length()) { + return null; + } + if (!parts[0].regionMatches(0, text, offset, len)) { + return null; + } + return new Match(parts[0], new int[0]); + } + } + + private static class WildcardPattern extends GlobPattern { + + @Override + public Match match(String text, int offset) { + int len = text.length(); + if (len <= offset) { + return new Match(text, new int[] { 0, 0 }); + } + return new Match(text, new int[] { offset, len }); + } + + @Override + public String toString() { + return "*"; + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/GuiceRepository.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/GuiceRepository.java new file mode 100644 index 00000000000..9412d51bb49 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/GuiceRepository.java @@ -0,0 +1,127 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.*; +import com.google.inject.spi.DefaultElementVisitor; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; +import com.yahoo.jdisc.Container; +import org.osgi.framework.Bundle; + +import java.util.*; +import java.util.logging.Logger; + +/** + * This is a repository of {@link Module}s. An instance of this class is owned by the {@link ContainerBuilder}, and is + * used to configure the set of Modules that eventually form the {@link Injector} of the active {@link Container}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class GuiceRepository implements Iterable<Module> { + + private static final Logger log = Logger.getLogger(GuiceRepository.class.getName()); + private final Map<Module, List<Element>> modules = new LinkedHashMap<>(); + private Injector injector; + + public GuiceRepository(Module... modules) { + installAll(Arrays.asList(modules)); + } + + public Injector activate() { + return getInjector(); + } + + public List<Module> installAll(Bundle bundle, Iterable<String> moduleNames) throws ClassNotFoundException { + List<Module> lst = new LinkedList<>(); + for (String moduleName : moduleNames) { + lst.add(install(bundle, moduleName)); + } + return lst; + } + + public Module install(Bundle bundle, String moduleName) throws ClassNotFoundException { + log.finer("Installing Guice module '" + moduleName + "'."); + Class<?> namedClass = bundle.loadClass(moduleName); + Class<Module> moduleClass = ContainerBuilder.safeClassCast(Module.class, namedClass); + Module module = getInstance(moduleClass); + install(module); + return module; + } + + public void installAll(Iterable<? extends Module> modules) { + for (Module module : modules) { + install(module); + } + } + + public void install(Module module) { + modules.put(module, Elements.getElements(module)); + injector = null; + } + + public void uninstallAll(Iterable<? extends Module> modules) { + for (Module module : modules) { + uninstall(module); + } + } + + public void uninstall(Module module) { + modules.remove(module); + injector = null; + } + + public Injector getInjector() { + if (injector == null) { + injector = Guice.createInjector(createModule()); + } + return injector; + } + + public <T> T getInstance(Key<T> key) { + return getInjector().getInstance(key); + } + + public <T> T getInstance(Class<T> type) { + return getInjector().getInstance(type); + } + + public Collection<Module> collection() { return ImmutableSet.copyOf(modules.keySet()); } + + @Override + public Iterator<Module> iterator() { + return collection().iterator(); + } + + private Module createModule() { + List<Element> allElements = new LinkedList<>(); + for (List<Element> moduleElements : modules.values()) { + allElements.addAll(moduleElements); + } + ElementCollector collector = new ElementCollector(); + for (ListIterator<Element> it = allElements.listIterator(allElements.size()); it.hasPrevious(); ) { + it.previous().acceptVisitor(collector); + } + return Elements.getModule(collector.elements); + } + + private static class ElementCollector extends DefaultElementVisitor<Boolean> { + + final Set<Key<?>> seenKeys = new HashSet<>(); + final List<Element> elements = new LinkedList<>(); + + @Override + public <T> Boolean visit(Binding<T> binding) { + if (seenKeys.add(binding.getKey())) { + elements.add(binding); + } + return Boolean.TRUE; + } + + @Override + public Boolean visitOther(Element element) { + elements.add(element); + return Boolean.TRUE; + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricConsumer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricConsumer.java new file mode 100644 index 00000000000..d057321565c --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricConsumer.java @@ -0,0 +1,68 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.ProvidedBy; +import com.google.inject.Provider; +import com.yahoo.jdisc.Metric; + +import java.util.Map; + +/** + * <p>This interface defines the consumer counterpart of the {@link Metric} interface. All Metric objects contain their + * own thread local instance of this interface, so most implementations will require a registry of sorts to manage the + * aggregation of state across MetricConsumers.</p> + * + * <p>An {@link Application} needs to bind a {@link Provider} of this interface to an implementation, or else all calls + * to the Metric objects become no-ops. An implementation will look similar to:</p> + * + * <pre> + * private final MyMetricRegistry myMetricRegistry = new MyMetricRegistry(); + * void createContainer() { + * ContainerBuilder builder = containerActivator.newContainerBuilder(); + * builder.guice().install(new MyGuiceModule()); + * (...) + * } + * class MyGuiceModule extends com.google.inject.AbstractModule { + * void configure() { + * bind(MetricConsumer.class).toProvider(myMetricRegistry); + * (...) + * } + * } + * class MyMetricRegistry implements com.google.inject.Provider<MetricConsumer> { + * (...) + * } + * </pre> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +@ProvidedBy(MetricNullProvider.class) +public interface MetricConsumer { + + /** + * <p>Consume a call to <tt>Metric.set(String, Number, Metric.Context)</tt>.</p> + * + * @param key The name of the metric to modify. + * @param val The value to assign to the named metric. + * @param ctx The context to further describe this entry. + */ + public void set(String key, Number val, Metric.Context ctx); + + /** + * <p>Consume a call to <tt>Metric.add(String, Number, Metric.Context)</tt>.</p> + * + * @param key The name of the metric to modify. + * @param val The value to add to the named metric. + * @param ctx The context to further describe this entry. + */ + public void add(String key, Number val, Metric.Context ctx); + + /** + * <p>Creates a <tt>Metric.Context</tt> object that encapsulates the given properties. The returned Context object + * will be passed along every future call to <tt>set(String, Number, Metric.Context)</tt> and + * <tt>add(String, Number, Metric.Context)</tt> where the properties match those given here.</p> + * + * @param properties The properties to incorporate in the context. + * @return The created context. + */ + public Metric.Context createContext(Map<String, ?> properties); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricImpl.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricImpl.java new file mode 100644 index 00000000000..8fab60429d0 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricImpl.java @@ -0,0 +1,68 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.yahoo.jdisc.Metric; + +import java.util.Map; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +class MetricImpl implements Metric { + + private final LocalConsumer consumer; + + @Inject + public MetricImpl(Provider<MetricConsumer> provider) { + consumer = new LocalConsumer(provider); + } + + @Override + public void set(String key, Number val, Context ctx) { + MetricConsumer consumer = currentConsumer(); + if (consumer != null) { + consumer.set(key, val, ctx); + } + } + + @Override + public void add(String key, Number val, Context ctx) { + MetricConsumer consumer = currentConsumer(); + if (consumer != null) { + consumer.add(key, val, ctx); + } + } + + @Override + public Context createContext(Map<String, ?> keys) { + MetricConsumer consumer = currentConsumer(); + if (consumer == null) { + return null; + } + return consumer.createContext(keys); + } + + private MetricConsumer currentConsumer() { + Thread thread = Thread.currentThread(); + if (thread instanceof ContainerThread) { + return ((ContainerThread)thread).consumer(); + } + return consumer.get(); + } + + private static class LocalConsumer extends ThreadLocal<MetricConsumer> { + + final Provider<MetricConsumer> factory; + + LocalConsumer(Provider<MetricConsumer> factory) { + this.factory = factory; + } + + @Override + protected MetricConsumer initialValue() { + return factory.get(); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricNullProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricNullProvider.java new file mode 100644 index 00000000000..e23ccbc95c0 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricNullProvider.java @@ -0,0 +1,15 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.Provider; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +class MetricNullProvider implements Provider<MetricConsumer> { + + @Override + public MetricConsumer get() { + return null; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricProvider.java new file mode 100644 index 00000000000..51fdcdd1c87 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricProvider.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.yahoo.jdisc.Metric; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class MetricProvider implements Provider<Metric> { + + private final Provider<MetricConsumer> consumerProvider; + + @Inject + public MetricProvider(Provider<MetricConsumer> consumerProvider) { + this.consumerProvider = consumerProvider; + } + + @Override + public Metric get() { + return new MetricImpl(consumerProvider); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiFramework.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiFramework.java new file mode 100644 index 00000000000..615b36fef1f --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiFramework.java @@ -0,0 +1,99 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; + +import java.util.List; + +/** + * <p>This is an abstraction of the OSGi framework that hides the actual implementation details. If you need access to + * this interface, simply inject it into your Application. In most cases, however, you are better of injecting a + * {@link BundleInstaller} since that provides common convenience methods.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface OsgiFramework { + + /** + * <p>Installs a bundle from the specified location. The specified location identifier will be used as the identity + * of the bundle. If a bundle containing the same location identifier is already installed, the <tt>Bundle</tt> + * object for that bundle is returned. All bundles listed in the {@link OsgiHeader#PREINSTALL_BUNDLE} manifest + * header are also installed. The bundle at index 0 of the returned list matches the <tt>bundleLocation</tt> + * argument.</p> + * + * <p><b>NOTE:</b> When this method installs more than one bundle, <em>AND</em> one of those bundles throw an + * exception during installation, the bundles installed prior to throwing the expcetion will remain installed. To + * enable the caller to recover from such a situation, this method wraps any thrown exception within a {@link + * BundleInstallationException} that contains the list of successfully installed bundles.</p> + * + * <p>It would be preferable if this method was exception-safe (that it would roll-back all installed bundles in the + * case of an exception), but that can not be implemented thread-safely since an <tt>Application</tt> may choose to + * install bundles concurrently through any available <tt>BundleContext</tt>.</p> + * + * @param bundleLocation The location identifier of the bundle to install. + * @return The list of Bundle objects installed, the object at index 0 matches the given location. + * @throws BundleInstallationException If the input stream cannot be read, or the installation of a bundle failed, + * or the caller does not have the appropriate permissions, or the system {@link + * BundleContext} is no longer valid. + */ + public List<Bundle> installBundle(String bundleLocation) throws BundleException; + + /** + * <p>Starts the given {@link Bundle}s. The parameter <tt>privileged</tt> tells the framework whether or not + * privileges are available, and is checked against the {@link OsgiHeader#PRIVILEGED_ACTIVATOR} header of each + * Bundle being started. Any bundle that is a fragment is silently ignored.</p> + * + * @param bundles The bundles to start. + * @param privileged Whether or not privileges are available. + * @throws BundleException If a bundle could not be started. This could be because a code dependency could not + * be resolved or the specified BundleActivator could not be loaded or threw an + * exception. + * @throws SecurityException If the caller does not have the appropriate permissions. + * @throws IllegalStateException If this bundle has been uninstalled or this bundle tries to change its own state. + */ + public void startBundles(List<Bundle> bundles, boolean privileged) throws BundleException; + + /** + * <p>This method <em>synchronously</em> refreshes all bundles currently loaded. Once this method returns, the + * class loaders of all bundles will reflect on the current set of loaded bundles.</p> + */ + public void refreshPackages(); + + /** + * <p>Returns the BundleContext of this framework's system bundle. The returned BundleContext can be used by the + * caller to act on behalf of this bundle. This method may return <tt>null</tt> if it has no valid + * BundleContext.</p> + * + * @return A <tt>BundleContext</tt> for the system bundle, or <tt>null</tt>. + * @throws SecurityException If the caller does not have the appropriate permissions. + * @since 2.0 + */ + public BundleContext bundleContext(); + + /** + * <p>Returns an iterable collection of all installed bundles. This method returns a list of all bundles installed + * in the OSGi environment at the time of the call to this method. However, since the OsgiFramework is a very + * dynamic environment, bundles can be installed or uninstalled at anytime.</p> + * + * @return An iterable collection of Bundle objects, one object per installed bundle. + */ + public List<Bundle> bundles(); + + /** + * <p>This method starts the framework instance. Before this method is called, any call to {@link + * #installBundle(String)} or {@link #bundles()} will generate a {@link NullPointerException}.</p> + * + * @throws BundleException If any error occurs. + */ + public void start() throws BundleException; + + /** + * <p>This method <em>synchronously</em> shuts down the framework. It must be called at the end of a session in + * order to shutdown all active bundles.</p> + * + * @throws BundleException If any error occurs. + */ + public void stop() throws BundleException; +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiHeader.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiHeader.java new file mode 100644 index 00000000000..524b23808e0 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiHeader.java @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import org.osgi.framework.Bundle; + +import java.util.List; + +/** + * This interface acts as a namespace for the supported OSGi bundle headers. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class OsgiHeader { + + public static final String APPLICATION = "X-JDisc-Application"; + public static final String PREINSTALL_BUNDLE = "X-JDisc-Preinstall-Bundle"; + public static final String PRIVILEGED_ACTIVATOR = "X-JDisc-Privileged-Activator"; + + /** + * Returns true if the named header is present in the manifest of the given bundle. + * + * @param bundle The bundle whose manifest to check. + * @param headerName The name of the header to check for. + * @return True if header is present. + */ + public static boolean isSet(Bundle bundle, String headerName) { + return Boolean.valueOf(String.valueOf(bundle.getHeaders().get(headerName))); + } + + /** + * This method reads the named header from the manifest of the given bundle, and parses it as a comma-separated list + * of values. If the header is not set, this method returns an empty list. + * + * @param bundle The bundle whose manifest to parse the header from. + * @param headerName The name of the header to parse. + * @return A list of parsed header values, may be empty. + */ + public static List<String> asList(Bundle bundle, String headerName) { + return ContainerBuilder.safeStringSplit(bundle.getHeaders().get(headerName), ","); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ResourcePool.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ResourcePool.java new file mode 100644 index 00000000000..4d62377d461 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ResourcePool.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.jdisc.application; + +import com.google.inject.Key; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.SharedResource; +import com.yahoo.jdisc.References; + +import java.util.ArrayList; +import java.util.List; + +/** + * <p>This is a utility class to help manage {@link SharedResource}s while configuring a {@link ContainerBuilder}. This + * class can still be used without a ContainerBuilder, albeit with the injection APIs (i.e. {@link #get(Class)} and + * {@link #get(com.google.inject.Key)}) disabled.</p> + * <p>The core problem with SharedResources is that they need to be tracked carefully to ensure exception safety in the + * code that creates and registers them with a ContainerBuilder. The code for this typically looks like this:</p> + * <pre> + * MyServerProvider serverProvider = null; + * MyRequestHandler requestHandler = null; + * try { + * serverProvider = builder.getInstance(MyServerProvider.class); + * serverProvider.start(); + * containerBuilder.serverProviders().install(serverProvider); + * + * requestHandler = builder.getInstance(MyRequestHandler.class); + * containerBuilder.serverBindings().bind("http://host/path", requestHandler); + * + * containerActivator.activateContainer(containerBuilder); + * } finally { + * if (serverProvider != null) { + * serverProvider.release(); + * } + * if (requestHandler != null) { + * requestHandler.release(); + * } + * } + * </pre> + * + * <p>The ResourcePool helps remove the boiler-plate code used to track the resources from outside the try-finally + * block. Using the ResourcePool, the above snippet can be rewritten to the following:</p> + * <pre> + * try (ResourcePool resources = new ResourcePool(containerBuilder)) { + * ServerProvider serverProvider = resources.get(MyServerProvider.class); + * serverProvider.start(); + * containerBuilder.serverProviders().install(serverProvider); + * + * RequestHandler requestHandler = resources.get(MyRequestHandler.class); + * containerBuilder.serverBindings().bind("http://host/path", requestHandler); + * + * containerActivator.activateContainer(containerBuilder); + * } + * </pre> + * + * <p>This class is not thread-safe.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public final class ResourcePool extends AbstractResource implements AutoCloseable { + + private final List<ResourceReference> resources = new ArrayList<>(); + private final ContainerBuilder builder; + + /** + * <p>Creates a new instance of this class without a backing {@link ContainerBuilder}. A ResourcePool created with + * this constructor will throw a NullPointerException if either {@link #get(Class)} or {@link #get(Key)} is + * called.</p> + */ + public ResourcePool() { + this(null); + } + + /** + * <p>Creates a new instance of this class. All calls to {@link #get(Class)} and {@link #get(Key)} are forwarded to + * the {@link ContainerBuilder} given to this constructor.</p> + * + * @param builder The ContainerBuilder that provides the injection functionality for this ResourcePool. + */ + public ResourcePool(ContainerBuilder builder) { + this.builder = builder; + } + + /** + * <p>Adds the given {@link SharedResource} to this ResourcePool. Note that this DOES NOT call {@link + * SharedResource#refer()}, as opposed to {@link #retain(SharedResource)}. When this ResourcePool is + * destroyed, it will release the main reference to the resource (by calling {@link SharedResource#release()}).</p> + * + * @param t The SharedResource to add. + * @param <T> The class of parameter <tt>t</tt>. + * @return The parameter <tt>t</tt>, to allow inlined calls to this function. + */ + public <T extends SharedResource> T add(T t) { + try { + resources.add(References.fromResource(t)); + } catch (IllegalStateException e) { + // Ignore. TODO(bakksjo): Don't rely on ISE to detect duplicates; handle that in this class instead. + } + return t; + } + + /** + * <p>Returns the appropriate instance for the given injection key. Note that this DOES NOT call {@link + * SharedResource#refer()}. This is the equivalent of doing:</p> + * <pre> + * t = containerBuilder.getInstance(key); + * resourcePool.add(t); + * </pre> + * + * <p>When this ResourcePool is destroyed, it will release the main reference to the resource + * (by calling {@link SharedResource#release()}).</p> + * + * @param key The injection key to return. + * @param <T> The class of the injection type. + * @return The appropriate instance of T. + * @throws NullPointerException If this pool was constructed without a ContainerBuilder. + */ + public <T extends SharedResource> T get(Key<T> key) { + return add(builder.getInstance(key)); + } + + /** + * <p>Returns the appropriate instance for the given injection type. Note that this DOES NOT call {@link + * SharedResource#refer()}. This is the equivalent of doing:</p> + * <pre> + * t = containerBuilder.getInstance(type); + * resourcePool.add(t); + * </pre> + * + * <p>When this ResourcePool is destroyed, it will release the main reference to the resource + * (by calling {@link SharedResource#release()}).</p> + * + * @param type The injection type to return. + * @param <T> The class of the injection type. + * @return The appropriate instance of T. + * @throws NullPointerException If this pool was constructed without a ContainerBuilder. + */ + public <T extends SharedResource> T get(Class<T> type) { + return add(builder.getInstance(type)); + } + + /** + * <p>Retains and adds the given {@link SharedResource} to this ResourcePool. Note that this DOES call {@link + * SharedResource#refer()}, as opposed to {@link #add(SharedResource)}. + * + * <p>When this ResourcePool is destroyed, it will release the resource reference returned by the + * {@link SharedResource#refer()} call.</p> + * + * @param t The SharedResource to retain and add. + * @param <T> The class of parameter <tt>t</tt>. + * @return The parameter <tt>t</tt>, to allow inlined calls to this function. + */ + public <T extends SharedResource> T retain(T t) { + resources.add(t.refer()); + return t; + } + + @Override + protected void destroy() { + for (ResourceReference resource : resources) { + resource.close(); + } + } + + @Override + public void close() throws Exception { + release(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ServerRepository.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ServerRepository.java new file mode 100644 index 00000000000..83aa8e7d9d7 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ServerRepository.java @@ -0,0 +1,75 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.common.collect.ImmutableList; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.service.ServerProvider; +import org.osgi.framework.Bundle; + +import java.util.*; +import java.util.logging.Logger; + +/** + * This is a repository of {@link ServerProvider}s. An instance of this class is owned by the {@link ContainerBuilder}, + * and is used to configure the set of ServerProviders that eventually become part of the active {@link Container}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ServerRepository implements Iterable<ServerProvider> { + + private static final Logger log = Logger.getLogger(ServerRepository.class.getName()); + private final List<ServerProvider> servers = new LinkedList<>(); + private final GuiceRepository guice; + + public ServerRepository(GuiceRepository guice) { + this.guice = guice; + } + + public Iterable<ServerProvider> activate() { return ImmutableList.copyOf(servers); } + + public List<ServerProvider> installAll(Bundle bundle, Iterable<String> serverNames) throws ClassNotFoundException { + List<ServerProvider> lst = new LinkedList<>(); + for (String serverName : serverNames) { + lst.add(install(bundle, serverName)); + } + return lst; + } + + public ServerProvider install(Bundle bundle, String serverName) throws ClassNotFoundException { + log.finer("Installing server provider '" + serverName + "'."); + Class<?> namedClass = bundle.loadClass(serverName); + Class<ServerProvider> serverClass = ContainerBuilder.safeClassCast(ServerProvider.class, namedClass); + ServerProvider server = guice.getInstance(serverClass); + install(server); + return server; + } + + public void installAll(Iterable<? extends ServerProvider> servers) { + for (ServerProvider server : servers) { + install(server); + } + } + + public void install(ServerProvider server) { + servers.add(server); + } + + public void uninstallAll(Iterable<? extends ServerProvider> handlers) { + for (ServerProvider handler : handlers) { + uninstall(handler); + } + } + + public void uninstall(ServerProvider handler) { + servers.remove(handler); + } + + public Collection<ServerProvider> collection() { + return Collections.unmodifiableCollection(servers); + } + + @Override + public Iterator<ServerProvider> iterator() { + return collection().iterator(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java new file mode 100644 index 00000000000..6f587057c77 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java @@ -0,0 +1,217 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * <p>This class holds a regular expression designed so that it only matches certain {@link URI}s. The constructor of + * this class accepts a simplified pattern string, and turns that into something that can be used to quickly match + * against URIs. This class also implements {@link Comparable} in such a way that stricter patterns order before looser + * patterns.</p> + * + * <p>Here are some examples of ordering:</p> + * <ul> + * <li><code>http://host/path</code> evaluated before <code>*://host/path</code></li> + * <li><code>http://host/path</code> evaluated before <code>http://*/path</code></li> + * <li><code>http://a.host/path</code> evaluated before <code>http://*.host/path</code></li> + * <li><code>http://*.host/path</code> evaluated before <code>http://host/path</code></li> + * <li><code>http://host.a/path</code> evaluated before <code>http://host.*/path</code></li> + * <li><code>http://host.*/path</code> evaluated before <code>http://host/path</code></li> + * <li><code>http://host:80/path</code> evaluated before <code>http://host:*/path</code></li> + * <li><code>http://host/path</code> evaluated before <code>http://host/*</code></li> + * <li><code>http://host/path/*</code> evaluated before <code>http://host/path</code></li> + * </ul> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class UriPattern implements Comparable<UriPattern> { + + public static final int DEFAULT_PRIORITY = 0; + private static final Pattern PATTERN = Pattern.compile("([^:]+)://([^:/]+)(:((\\*)|([0-9]+)))?/(.*)", + Pattern.UNICODE_CASE | Pattern.CANON_EQ); + private final String pattern; + private final GlobPattern scheme; + private final GlobPattern host; + private final int port; + private final GlobPattern path; + private final int priority; + + /** + * <p>Creates a new instance of this class that represents the given pattern string, with a priority of <tt>0</tt>. + * The input string must be on the form <code><scheme>://<host>[:<port>]<path></code>, where + * '*' can be used as a wildcard character at any position.</p> + * + * @param uri The pattern to parse. + * @throws IllegalArgumentException If the pattern could not be parsed. + */ + public UriPattern(String uri) { + this(uri, DEFAULT_PRIORITY); + } + + /** + * <p>Creates a new instance of this class that represents the given pattern string, with the given priority. The + * input string must be on the form <code><scheme>://<host>[:<port>]<path></code>, where + * '*' can be used as a wildcard character at any position.</p> + * + * @param uri The pattern to parse. + * @param priority The priority of this pattern. + * @throws IllegalArgumentException If the pattern could not be parsed. + */ + public UriPattern(String uri, int priority) { + Matcher matcher = PATTERN.matcher(uri); + if (!matcher.find()) { + throw new IllegalArgumentException(uri); + } + scheme = GlobPattern.compile(resolvePatternComponent(matcher.group(1))); + host = GlobPattern.compile(resolvePatternComponent(matcher.group(2))); + port = resolvePortPattern(matcher.group(4)); + path = GlobPattern.compile(resolvePatternComponent(matcher.group(7))); + pattern = scheme + "://" + host + ":" + (port > 0 ? port : "*") + "/" + path; + this.priority = priority; + } + + /** + * <p>Attempts to match the given {@link URI} to this pattern. Note that only the scheme, host, port, and path + * components of the URI are used. Any query or fragment part is simply ignored.</p> + * + * @param uri The URI to match. + * @return A {@link Match} object describing the match found, or null if not found. + */ + public Match match(URI uri) { + // Performance optimization: Match path first since scheme and host are often the same in a given binding repository. + String uriPath = resolveUriComponent(uri.getPath()); + GlobPattern.Match pathMatch = path.match(uriPath, uriPath.startsWith("/") ? 1 : 0); + if (pathMatch == null) { + return null; + } + if (port > 0 && port != uri.getPort()) { + return null; + } + // Match scheme before host because it has a higher chance of differing (e.g. http versus https) + GlobPattern.Match schemeMatch = scheme.match(resolveUriComponent(uri.getScheme())); + if (schemeMatch == null) { + return null; + } + GlobPattern.Match hostMatch = host.match(resolveUriComponent(uri.getHost())); + if (hostMatch == null) { + return null; + } + return new Match(schemeMatch, hostMatch, port > 0 ? 0 : uri.getPort(), pathMatch); + } + + @Override + public int hashCode() { + return pattern.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof UriPattern && pattern.equals(((UriPattern)obj).pattern); + } + + @Override + public String toString() { + return pattern; + } + + @Override + public int compareTo(UriPattern rhs) { + int cmp; + cmp = rhs.priority - priority; + if (cmp != 0) { + return cmp; + } + cmp = scheme.compareTo(rhs.scheme); + if (cmp != 0) { + return cmp; + } + cmp = host.compareTo(rhs.host); + if (cmp != 0) { + return cmp; + } + cmp = path.compareTo(rhs.path); + if (cmp != 0) { + return cmp; + } + cmp = rhs.port - port; + if (cmp != 0) { + return cmp; + } + return 0; + } + + private static String resolveUriComponent(String str) { + return str != null ? str : ""; + } + + private static String resolvePatternComponent(String val) { + return val != null ? val : "*"; + } + + private static int resolvePortPattern(String str) { + if (str == null || str.equals("*")) { + return 0; + } + return Integer.parseInt(str); + } + + /** + * <p>This class holds the result of a {@link UriPattern#match(URI)} operation. It contains methods to inspect the + * groups captured during matching, where a <em>group</em> is defined as a sequence of characters matches by a + * wildcard in the {@link UriPattern}.</p> + */ + public static class Match { + + private final GlobPattern.Match scheme; + private final GlobPattern.Match host; + private final int port; + private final GlobPattern.Match path; + + private Match(GlobPattern.Match scheme, GlobPattern.Match host, int port, GlobPattern.Match path) { + this.scheme = scheme; + this.host = host; + this.port = port; + this.path = path; + } + + /** + * <p>Returns the number of captured groups of this match. Any non-negative integer smaller than the value + * returned by this method is a valid group index for this match.</p> + * + * @return The number of captured groups. + */ + public int groupCount() { + return scheme.groupCount() + host.groupCount() + (port > 0 ? 1 : 0) + path.groupCount(); + } + + /** + * <p>Returns the input subsequence captured by the given group by this match. Groups are indexed from left to + * right, starting at zero. Note that some groups may match an empty string, in which case this method returns + * the empty string. This method never returns null.</p> + * + * @param idx The index of the group to return. + * @return The (possibly empty) substring captured by the group during matching, never <tt>null</tt>. + * @throws IndexOutOfBoundsException If there is no group in the match with the given index. + */ + public String group(int idx) { + int len = scheme.groupCount(); + if (idx < len) { + return scheme.group(idx); + } + idx = idx - len; + len = host.groupCount(); + if (idx < len) { + return host.group(idx); + } + idx = idx - len; + len = port > 0 ? 1 : 0; + if (idx < len) { + return String.valueOf(port); + } + idx = idx - len; + return path.group(idx); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/package-info.java new file mode 100644 index 00000000000..1e864cc4688 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/package-info.java @@ -0,0 +1,152 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * <p>Provides classes and interfaces for implementing an {@link com.yahoo.jdisc.application.Application + * Application}.</p> + * + * <h3>Application</h3> + * <p>In every jDISC process there is exactly one Application instance, it is created during jDISC startup, and it is + * destroyed during jDISC shutdown. The Application uses the {@link com.yahoo.jdisc.application.ContainerBuilder + * ContainerBuilder} interface to load OSGi {@link org.osgi.framework.Bundle Bundles}, install Guice {@link + * com.google.inject.Module Modules}, create and start {@link com.yahoo.jdisc.service.ServerProvider ServerProviders}, + * inject a {@link com.yahoo.jdisc.application.BindingSetSelector BindingSetSelector}, and configure {@link + * com.yahoo.jdisc.application.BindingRepository BindingSets} with {@link com.yahoo.jdisc.handler.RequestHandler + * RequestHandlers} and {@link com.yahoo.jdisc.service.ClientProvider ClientProviders}. Once the ContainerBuilder is + * appropriately configured, it is passed to the local {@link com.yahoo.jdisc.application.ContainerActivator} to perform + * an atomic switch from current to new {@link com.yahoo.jdisc.Container Container}.</p> + * +<pre> +@Inject +MyApplication(ContainerActivator activator) { + ContainerBuilder builder = activator.newContainerBuilder(); + builder.guiceModules().install(new MyBindings()); + Bundle bundle = builder.osgiBundles().install("file:$VESPA_HOME/lib/jars/jdisc_http.jar"); + builder.serverProviders().install(bundle, "com.yahoo.disc.service.http.HttpServer"); + builder.serverBindings().bind("http://localhost/admin/*", new MyAdminHandler()); + builder.serverBindings().bind("http://localhost/*", new MyRequestHandler()); + activator.activateContainer(builder); +} +</pre> + * + * <p>Because the {@link com.yahoo.jdisc.Request Request} owns a reference to the Container that was active on Request- + * construction, jDISC is able to guarantee that no component is shut down as long as there are pending Requests that + * can reach them. When activating a new Container, the previous Container is returned as a {@link + * com.yahoo.jdisc.application.DeactivatedContainer DeactivatedContainer} instance - an API that can be used by the + * Application to asynchronously wait for Container termination in order to completely shut down components that are no + * longer required. This activation pattern is used both for Application startup, runtime reconfigurations, as well as + * for Application shutdown. It allows all jDISC Application to continously serve Requests during reconfiguration, + * causing no down time other than what the Application itself explicitly enforces.</p> + * +<pre> +void reconfigureApplication() { + (...) + reconfiguredContainerBuilder.handlers().install(myRetainedClients); + reconfiguredContainerBuilder.servers().install(myRetainedServers); + myExpiredServers.close(); + DeactivatedContainer deactivatedContainer = containerActivator.activateContainer(reconfiguredContainerBuilder); + deactivatedContainer.notifyTermination(new Runnable() { + void run() { + myExpiredClients.destroy(); + myExpiredServers.destroy(); + } + }); +} +</pre> + * + * <h3>Application and OSGi</h3> + * <p>At the heart of jDISC is an OSGi framework. An Application is always packaged as an OSGi bundle. The OSGi + * technology itself is a set of specifications that define a dynamic component system for Java. These specifications + * enable a development model where applications are (dynamically) composed of many different (reusable) components. The + * OSGi specifications enable components to hide their implementations from other components while communicating through + * common interfaces (in our case, defined by jDISC's core API) or services (which are objects that are explicitly + * shared between components). Initially this framework is used to load and bootstrap the application from an OSGi + * bundle specified on deployment, but because it is exposed through the ContainerBuilder interface, an Application + * itself can load other bundles as required.</p> + * + * <p>The OSGi integration in jDISC adds the following manifest instructions:</p> + * <dl> + * <dt>X-JDisc-Privileged-Activator</dt> + * <dd> + * if "true", this tells jDISC that this bundle requires root privileges for its {@link + * org.osgi.framework.BundleActivator BundleActivator}. If privileges can not be provided, this bundle should not be + * installed. Only the Application bundle and its dependencies can ever be given privileges, as jDISC itself drops + * its privileges after the bootstrapping step. + * </dd> + * <dt>X-JDisc-Preinstall-Bundle</dt> + * <dd> + * a comma-separated list of bundle locations that must be installed prior to this. Because the named bundles are + * loaded through the same framework, all transitive dependencies are also resolved. This is an extension to the + * standard OSGi instruction "Require-Bundle" which simply states that this bundle requires another. + * + * It is fairly tricky to get this right during integration testing, since dependencies might be part of the build + * tree instead of being installed on the host. To facilitate this, JDisc will prefix any non-schemed location (e.g. + * "my_dependency.jar") with the system property "jdisc.bundle.path". This property defaults to the current + * directory when running inside an IDE, but is set to "$VESPA_HOME/lib/jars/" by the jdisc_start script. + * + * One may also reference system properties in a bundle location using the syntax "${propertyName}". If the property + * is not found, it defaults to an empty string. + * </dd> + * <dt>X-JDisc-Application</dt> + * <dd> + * the name of the Application class to load from the bundle. This instruction is ignored unless it is part of the + * first loaded bundle. + * </dd> + * </dl> + * + * <p>One of the benefits of using OSGi is that it provides Classloader isolation, meaning that one bundle can not + * inadvertently affect the inernals of another. jDISC leverages this to isolate the different implementations of + * RequestHandlers, ServerProviders, and jDISC's core internals.</p> + * + * <p>The OSGi manifest instruction "X-JDisc-Application" tells jDISC the name of the Application class to inject from + * the loaded bundle during startup. To this end, it is necessary for the named Application to offer an + * injection-enabled constructor (annotated with the <code>Inject</code> keyword). At a minimum, an Application + * typically needs to have the ContainerActivator injected and saved to a member variable. Because of jDISC's additional + * OSGi manifest instruction "X-JDisc-Preinstall-Bundle", an Application bundle can be built with compile-time + * dependencies on other OSGi bundles (using the "provided" scope in maven) without having to repack those dependency + * into the application itself. Unless incompatible API changes are made to 3rd party jDISC components, it should be + * possible to upgrade dependencies without having to recompile and redeploy the Application.</p> + * + * <h3>Application deployment</h3> + * <p>jDISC allows a single binary to execute any application without having to change the command line parameters. + * Instead of + * modifying the parameters of the single application binary, changing the application is achieved by setting a single + * environment variable. The planned method of deployment is therefore to 1) install the application's OSGi bundle, + * 2) set the necessary "jdisc.application" environment variable, and 3) restart the package.</p> + * +<pre> +$ install myapp_jar +$ set jdisc.application="myapp.jar" +$ restart jdisc +</pre> + * + * For testing and development, the jDISC binary also supports command line parameters to start and stop a local + * application. + * +<pre> +$ install jdisc-dev +$ emacs src/main/java/edu/disc/MyApplication.java +$ mvn install +$ sudo jdisc_start target/myapp.jar +</pre> + * + * <p>It is the responsibility of the Application itself to create, configure + * and activate a Container instance. Although jDISC offers an API that allows for- and manages the change of an active + * Container instance, making the necessary calls to do so is also considered Application logic. When jDISC receives an + * external signal to shut down, it instructs the running Application to initiate a graceful shutdown, and waits for it + * to terminate. Any in-flight Requests should complete, and all services will close.</p> + * + * <p>Because jDISC runs as a Daemon it has the opportunity to run code with root privileges, and it can be configured + * to provide these privileges to an application's initialization code. However, 1) deployment-time configuration must + * explicitly enable this capability (by setting the environment variable "jdisc.privileged" to "true"), and 2) the + * application bundle must explicitly declare that it requires privileges (by including the manifest header + * "X-JDisc-Privileged-Activator" with the value "true"). If privileges are required but unavailable, deployment of the + * application will fail. Code that requires privileges will never be run WITHOUT privileges, and code that does not + * explicitly request privileges will never be run WITH privileges. Finally, the code snippet that is run with + * privileges is separate from the Application class to avoid unintentionally passing privileges to third-party + * code.</p> + * + * @see com.yahoo.jdisc + * @see com.yahoo.jdisc.handler + * @see com.yahoo.jdisc.service + */ +@com.yahoo.api.annotations.PublicApi +package com.yahoo.jdisc.application; diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/client/AbstractClientApplication.java b/jdisc_core/src/main/java/com/yahoo/jdisc/client/AbstractClientApplication.java new file mode 100644 index 00000000000..032384cb9ba --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/client/AbstractClientApplication.java @@ -0,0 +1,55 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.client; + +import com.google.inject.Inject; +import com.yahoo.jdisc.application.AbstractApplication; +import com.yahoo.jdisc.application.BundleInstaller; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * <p>This is a convenient parent class for {@link ClientApplication} developers. It extends {@link AbstractApplication} + * and implements {@link Runnable} to wait for {@link #shutdown()} to be called. When using this class, you implement + * {@link #start()} (and optionally {@link #stop()}), and provide a reference to it to whatever component is responsible + * for signaling shutdown.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class AbstractClientApplication extends AbstractApplication implements ClientApplication { + + private final CountDownLatch done = new CountDownLatch(1); + + @Inject + public AbstractClientApplication(BundleInstaller bundleInstaller, ContainerActivator activator, + CurrentContainer container) { + super(bundleInstaller, activator, container); + } + + @Override + public final void run() { + try { + done.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public final void shutdown() { + done.countDown(); + } + + public final boolean isShutdown() { + return done.getCount() == 0; + } + + public final boolean awaitShutdown(int timeout, TimeUnit unit) throws InterruptedException { + return done.await(timeout, unit); + } + + public final void awaitShutdown() throws InterruptedException { + done.await(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientApplication.java b/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientApplication.java new file mode 100644 index 00000000000..27e4a65b96f --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientApplication.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.jdisc.client; + +import com.yahoo.jdisc.application.Application; + +/** + * <p>This interface extends the {@link Application} interface, and is intended to be used with the {@link ClientDriver} + * to implement stand-alone client applications on top of jDISC. The difference from Application is that this interface + * provides a {@link Runnable#run()} method that will be invoked once the Application has been created and {@link + * Application#start() started}. When run() returns, the {@link ClientDriver} will initiate Application shutdown.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ClientApplication extends Application, Runnable { + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientDriver.java b/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientDriver.java new file mode 100644 index 00000000000..f06be2af155 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientDriver.java @@ -0,0 +1,133 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.client; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.yahoo.jdisc.application.Application; +import com.yahoo.jdisc.application.OsgiFramework; +import com.yahoo.jdisc.core.ApplicationLoader; +import com.yahoo.jdisc.core.FelixFramework; +import com.yahoo.jdisc.core.FelixParams; +import com.yahoo.jdisc.test.NonWorkingOsgiFramework; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * <p>This class provides a unified way to set up and run a {@link ClientApplication}. It provides you with a + * programmable interface to instantiate and run the whole jDISC framework as if it was started as a Daemon, and it + * provides you with a thread in which to run your application logic. Once your return from the {@link + * ClientApplication#run()} method, the ClientProvider will initiate {@link Application} shutdown.</p> + * + * <p>A ClientApplication is typically a self-contained JAR file that bundles all of its dependencies, and contains a + * single "main" method. The typical implementation of that method is:</p> + * <pre> + * public static void main(String[] args) throws Exception { + * ClientDriver.runApplication(MyApplication.class); + * } + * </pre> + * + * <p>Alternatively, the ClientApplication can be created up front:</p> + * <pre> + * public static void main(String[] args) throws Exception { + * MyApplication app = new MyApplication(); + * (... configure app ...) + * ClientDriver.runApplication(app); + * } + * </pre> + * + * <p>Because all of the dependencies of a ClientApplication is expected to be part of the application JAR, the OSGi + * framework created by this ClientDriver is disabled. Calling any method on that framework will throw an + * exception. If you need OSGi support, use either of the runApplicationWithOsgi() methods.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class ClientDriver { + + /** + * <p>Creates and runs the given {@link ClientApplication}.</p> + * + * @param app The ClientApplication to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @throws Exception If an exception was thrown by the ClientApplication. + */ + public static void runApplication(ClientApplication app, Module... guiceModules) + throws Exception + { + runApplication(newNonWorkingOsgiFramework(), newModuleList(app, guiceModules)); + } + + /** + * <p>Creates and runs an instance of the given {@link ClientApplication} class.</p> + * + * @param appClass The ClientApplication class to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @throws Exception If an exception was thrown by the ClientApplication. + */ + public static void runApplication(Class<? extends ClientApplication> appClass, Module... guiceModules) + throws Exception + { + runApplication(newNonWorkingOsgiFramework(), newModuleList(appClass, guiceModules)); + } + + /** + * <p>Creates and runs an instance of the the given {@link ClientApplication} class with OSGi support.</p> + * + * @param cachePath The path to use for the OSGi bundle cache. + * @param appClass The ClientApplication class to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @throws Exception If an exception was thrown by the ClientApplication. + */ + public static void runApplicationWithOsgi(String cachePath, Class<? extends ClientApplication> appClass, + Module... guiceModules) throws Exception + { + runApplication(newOsgiFramework(cachePath), newModuleList(appClass, guiceModules)); + } + + private static OsgiFramework newNonWorkingOsgiFramework() { + return new NonWorkingOsgiFramework(); + } + + private static FelixFramework newOsgiFramework(String cachePath) { + return new FelixFramework(new FelixParams().setCachePath(cachePath)); + } + + private static List<Module> newModuleList(final ClientApplication appInstance, Module... guiceModules) { + List<Module> lst = new LinkedList<>(Arrays.asList(guiceModules)); + lst.add(new AbstractModule() { + + @Override + protected void configure() { + bind(Application.class).toInstance(appInstance); + } + }); + return lst; + } + + private static List<Module> newModuleList(final Class<? extends ClientApplication> appClass, + Module... guiceModules) + { + List<Module> lst = new LinkedList<>(Arrays.asList(guiceModules)); + lst.add(new AbstractModule() { + + @Override + protected void configure() { + bind(Application.class).to(appClass); + } + }); + return lst; + } + + private static void runApplication(OsgiFramework osgi, List<Module> modules) throws Exception { + ApplicationLoader loader = new ApplicationLoader(osgi, modules); + loader.init(null, false); + try { + loader.start(); + ((ClientApplication)loader.application()).run(); + loader.stop(); + } finally { + loader.destroy(); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/client/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/client/package-info.java new file mode 100644 index 00000000000..c5e53ed8a90 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/client/package-info.java @@ -0,0 +1,9 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * <p>Provides classes and interfaces for implementing a {@link com.yahoo.jdisc.client.ClientApplication + * ClientApplication}.</p> + * + * @see com.yahoo.jdisc.client.ClientApplication + */ +@com.yahoo.api.annotations.PublicApi +package com.yahoo.jdisc.client; diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java new file mode 100644 index 00000000000..a296bd1e327 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java @@ -0,0 +1,137 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.google.inject.Injector; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.SharedResource; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.BindingSetSelector; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.ResourcePool; +import com.yahoo.jdisc.application.UriPattern; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.service.BindingSetNotFoundException; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.service.NoBindingSetSelectedException; +import com.yahoo.jdisc.service.ServerProvider; + +import java.net.URI; +import java.util.Map; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ActiveContainer extends AbstractResource implements CurrentContainer { + + private final ContainerTermination termination; + private final Injector guiceInjector; + private final Iterable<ServerProvider> serverProviders; + private final ResourcePool resourceReferences = new ResourcePool(); + private final Map<String, BindingSet<RequestHandler>> serverBindings; + private final Map<String, BindingSet<RequestHandler>> clientBindings; + private final BindingSetSelector bindingSetSelector; + private final TimeoutManagerImpl timeoutMgr; + + public ActiveContainer(ContainerBuilder builder) { + serverProviders = builder.serverProviders().activate(); + for (SharedResource resource : serverProviders) { + resourceReferences.retain(resource); + } + serverBindings = builder.activateServerBindings(); + for (BindingSet<RequestHandler> set : serverBindings.values()) { + for (Map.Entry<UriPattern, RequestHandler> entry : set) { + resourceReferences.retain(entry.getValue()); + } + } + clientBindings = builder.activateClientBindings(); + for (BindingSet<RequestHandler> set : clientBindings.values()) { + for (Map.Entry<UriPattern, RequestHandler> entry : set) { + resourceReferences.retain(entry.getValue()); + } + } + bindingSetSelector = builder.getInstance(BindingSetSelector.class); + timeoutMgr = builder.getInstance(TimeoutManagerImpl.class); + timeoutMgr.start(); + builder.guiceModules().install(new AbstractModule() { + + @Override + protected void configure() { + bind(TimeoutManagerImpl.class).toInstance(timeoutMgr); + } + }); + guiceInjector = builder.guiceModules().activate(); + termination = new ContainerTermination(builder.appContext()); + } + + @Override + protected void destroy() { + resourceReferences.release(); + timeoutMgr.shutdown(); + termination.run(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (retainCount() > 0) { + destroy(); + } + } finally { + super.finalize(); + } + } + + /** + * Make this instance retain a reference to the resource until it is destroyed. + */ + void retainReference(SharedResource resource) { + resourceReferences.retain(resource); + } + + public ContainerTermination shutdown() { + return termination; + } + + public Injector guiceInjector() { + return guiceInjector; + } + + public Iterable<ServerProvider> serverProviders() { + return serverProviders; + } + + public Map<String, BindingSet<RequestHandler>> serverBindings() { + return serverBindings; + } + + public BindingSet<RequestHandler> serverBindings(String setName) { + return serverBindings.get(setName); + } + + public Map<String, BindingSet<RequestHandler>> clientBindings() { + return clientBindings; + } + + public BindingSet<RequestHandler> clientBindings(String setName) { + return clientBindings.get(setName); + } + + TimeoutManagerImpl timeoutManager() { + return timeoutMgr; + } + + @Override + public ContainerSnapshot newReference(URI uri) { + String name = bindingSetSelector.select(uri); + if (name == null) { + throw new NoBindingSetSelectedException(uri); + } + BindingSet<RequestHandler> serverBindings = serverBindings(name); + BindingSet<RequestHandler> clientBindings = clientBindings(name); + if (serverBindings == null || clientBindings == null) { + throw new BindingSetNotFoundException(name); + } + return new ContainerSnapshot(this, serverBindings, clientBindings); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java new file mode 100644 index 00000000000..00908df4249 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java @@ -0,0 +1,64 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class ApplicationConfigModule extends AbstractModule { + + private final Map<String, String> config; + + ApplicationConfigModule(Map<String, String> config) { + this.config = normalizeConfig(config); + } + + @Override + protected void configure() { + for (Map.Entry<String, String> entry : config.entrySet()) { + bind(String.class).annotatedWith(Names.named(entry.getKey())).toInstance(entry.getValue()); + } + } + + public static ApplicationConfigModule newInstanceFromFile(String fileName) throws IOException { + Properties props = new Properties(); + InputStream in = null; + try { + in = new FileInputStream(fileName); + props.load(in); + } finally { + if (in != null) { + in.close(); + } + } + Map<String, String> ret = new HashMap<>(); + for (String name : props.stringPropertyNames()) { + ret.put(name, props.getProperty(name)); + } + return new ApplicationConfigModule(ret); + } + + private static Map<String, String> normalizeConfig(Map<String, String> raw) { + List<String> names = new ArrayList<>(raw.keySet()); + Collections.sort(names, new Comparator<String>() { + + @Override + public int compare(String lhs, String rhs) { + return -lhs.compareTo(rhs); // reverse alphabetical order, i.e. lower-case before upper-case + } + }); + Map<String, String> ret = new HashMap<>(); + for (String name : names) { + ret.put(name.toLowerCase(Locale.US), raw.get(name)); + } + return ImmutableMap.copyOf(ret); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java new file mode 100644 index 00000000000..c6d6efd0ee9 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.ContainerThread; +import com.yahoo.jdisc.application.OsgiFramework; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.util.concurrent.ThreadFactory; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +class ApplicationEnvironmentModule extends AbstractModule { + + private final ApplicationLoader loader; + + public ApplicationEnvironmentModule(ApplicationLoader loader) { + this.loader = loader; + } + + @Override + protected void configure() { + bind(ContainerActivator.class).toInstance(loader); + bind(CurrentContainer.class).toInstance(loader); + bind(OsgiFramework.class).toInstance(loader.osgiFramework()); + bind(ThreadFactory.class).to(ContainerThread.Factory.class); + } + + @Provides + public ContainerBuilder containerBuilder() { + return loader.newContainerBuilder(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java new file mode 100644 index 00000000000..2dd7f7eb879 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java @@ -0,0 +1,261 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.application.*; +import com.yahoo.jdisc.service.ContainerNotReadyException; +import com.yahoo.jdisc.service.CurrentContainer; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; + +import java.lang.ref.WeakReference; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ApplicationLoader implements BootstrapLoader, ContainerActivator, CurrentContainer { + + private static final Logger log = Logger.getLogger(ApplicationLoader.class.getName()); + private final OsgiFramework osgiFramework; + private final GuiceRepository guiceModules = new GuiceRepository(); + private final AtomicReference<ActiveContainer> containerRef = new AtomicReference<>(); + private final Object appLock = new Object(); + private final List<Bundle> appBundles = new ArrayList<>(); + private Application application; + private ApplicationInUseTracker applicationInUseTracker; + + public ApplicationLoader(OsgiFramework osgiFramework, Iterable<? extends Module> guiceModules) { + this.osgiFramework = osgiFramework; + this.guiceModules.install(new ApplicationEnvironmentModule(this)); + this.guiceModules.installAll(guiceModules); + } + + @Override + public ContainerBuilder newContainerBuilder() { + return new ContainerBuilder(guiceModules); + } + + @Override + public DeactivatedContainer activateContainer(ContainerBuilder builder) { + ActiveContainer next = builder != null ? new ActiveContainer(builder) : null; + final ActiveContainer prev; + synchronized (appLock) { + if (application == null && next != null) { + next.release(); + throw new ApplicationNotReadyException(); + } + + if (next != null) { + next.retainReference(applicationInUseTracker); + } + + prev = containerRef.getAndSet(next); + if (prev == null) { + return null; + } + } + prev.release(); + DeactivatedContainer deactivatedContainer = prev.shutdown(); + + final WeakReference<ActiveContainer> prevContainerReference = new WeakReference<>(prev); + final Runnable deactivationMonitor = () -> { + long waitTimeSeconds = 30L; + long totalTimeWaited = 0L; + + while (!Thread.interrupted()) { + final long currentWaitTimeSeconds = waitTimeSeconds; + totalTimeWaited += currentWaitTimeSeconds; + + Interruption.mapExceptionToThreadState(() -> + Thread.sleep(TimeUnit.MILLISECONDS.convert(currentWaitTimeSeconds, TimeUnit.SECONDS)) + ); + + final ActiveContainer prevContainer = prevContainerReference.get(); + if (prevContainer == null) { + return; + } + if (prevContainer.retainCount() == 0) { + return; + } + log.warning("Previous container not terminated in the last " + totalTimeWaited + " seconds." + + " Reference state={ " + prevContainer.currentState() + " }"); + + waitTimeSeconds = (long) (waitTimeSeconds * 1.2); + } + log.warning("Deactivation monitor thread unexpectedly interrupted"); + }; + final Thread deactivationMonitorThread = new Thread(deactivationMonitor, "Container deactivation monitor"); + deactivationMonitorThread.setDaemon(true); + deactivationMonitorThread.start(); + + return deactivatedContainer; + } + + @Override + public ContainerSnapshot newReference(URI uri) { + ActiveContainer container = containerRef.get(); + if (container == null) { + throw new ContainerNotReadyException(); + } + return container.newReference(uri); + } + + @Override + public void init(String appLocation, boolean privileged) throws Exception { + log.finer("Initializing application loader."); + osgiFramework.start(); + BundleContext ctx = osgiFramework.bundleContext(); + if (ctx != null) { + ctx.registerService(CurrentContainer.class.getName(), this, null); + } + if(appLocation == null) { + return; // application class bound by another module + } + try { + final Class<Application> appClass = ContainerBuilder.safeClassCast(Application.class, Class.forName(appLocation)); + guiceModules.install(new AbstractModule() { + @Override + public void configure() { + bind(Application.class).to(appClass); + } + }); + return; // application class found on class path + } catch (ClassNotFoundException e) { + // location is not a class name + if (log.isLoggable(Level.FINE)) { + log.fine("App location is not a class name. Installing bundle"); + } + } + appBundles.addAll(osgiFramework.installBundle(appLocation)); + if (OsgiHeader.isSet(appBundles.get(0), OsgiHeader.PRIVILEGED_ACTIVATOR)) { + osgiFramework.startBundles(appBundles, privileged); + } + + } + + @Override + public void start() throws Exception { + log.finer("Initializing application."); + Injector injector = guiceModules.activate(); + Application app; + if (!appBundles.isEmpty()) { + Bundle appBundle = appBundles.get(0); + if (!OsgiHeader.isSet(appBundle, OsgiHeader.PRIVILEGED_ACTIVATOR)) { + osgiFramework.startBundles(appBundles, false); + } + List<String> header = OsgiHeader.asList(appBundle, OsgiHeader.APPLICATION); + if (header.size() != 1) { + throw new IllegalArgumentException("OSGi header '" + OsgiHeader.APPLICATION + "' has " + header.size() + + " entries, expected 1."); + } + String appName = header.get(0); + log.finer("Loading application class " + appName + " from bundle '" + appBundle.getSymbolicName() + "'."); + Class<Application> appClass = ContainerBuilder.safeClassCast(Application.class, + appBundle.loadClass(appName)); + app = injector.getInstance(appClass); + } else { + app = injector.getInstance(Application.class); + log.finer("Injecting instance of " + app.getClass().getName() + "."); + } + try { + synchronized (appLock) { + application = app; + applicationInUseTracker = new ApplicationInUseTracker(); + } + app.start(); + } catch (Exception e) { + log.log(Level.WARNING, "Exception thrown while activating application.", e); + synchronized (appLock) { + application = null; + applicationInUseTracker = null; + } + app.destroy(); + throw e; + } + } + + @Override + public void stop() throws Exception { + log.finer("Destroying application."); + Application app; + ApplicationInUseTracker applicationInUseTracker; + synchronized (appLock) { + app = application; + applicationInUseTracker = this.applicationInUseTracker; + } + if (app == null || applicationInUseTracker == null) { + return; + } + try { + app.stop(); + } catch (Exception e) { + log.log(Level.WARNING, "Exception thrown while deactivating application.", e); + } + synchronized (appLock) { + application = null; + } + activateContainer(null); + synchronized (appLock) { + this.applicationInUseTracker = null; + } + applicationInUseTracker.release(); + applicationInUseTracker.applicationInUseLatch.await(); + app.destroy(); + } + + @Override + public void destroy() { + log.finer("Destroying application loader."); + try { + osgiFramework.stop(); + } catch (BundleException e) { + e.printStackTrace(); + } + } + + public Application application() { + synchronized (appLock) { + return application; + } + } + + public OsgiFramework osgiFramework() { + return osgiFramework; + } + + private static class ApplicationInUseTracker extends AbstractResource { + //opened when the application has been stopped and there's no active containers + final CountDownLatch applicationInUseLatch = new CountDownLatch(1); + + @Override + protected void destroy() { + applicationInUseLatch.countDown(); + } + } + + private static class Interruption { + interface Runnable_throws<E extends Throwable> { + void run() throws E; + } + + public static void mapExceptionToThreadState(Runnable_throws<InterruptedException> runnable) { + try { + runnable.run(); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java new file mode 100644 index 00000000000..21c52d6047d --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.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.jdisc.core; + +import com.google.inject.Module; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.OsgiFramework; +import org.apache.commons.daemon.Daemon; +import org.apache.commons.daemon.DaemonContext; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BootstrapDaemon implements Daemon { + + private static final Logger log = Logger.getLogger(BootstrapDaemon.class.getName()); + private final BootstrapLoader loader; + private final boolean privileged; + private String bundleLocation; + + static { + // force load slf4j to avoid other logging frameworks from initializing before it + org.slf4j.LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + } + + public BootstrapDaemon() { + this(new ApplicationLoader(newOsgiFramework(), newConfigModule()), + Boolean.valueOf(System.getProperty("jdisc.privileged"))); + } + + BootstrapDaemon(BootstrapLoader loader, boolean privileged) { + this.loader = loader; + this.privileged = privileged; + } + + BootstrapLoader loader() { + return loader; + } + + @Override + public void init(DaemonContext context) throws Exception { + String[] args = context.getArguments(); + if (args == null || args.length != 1 || args[0] == null) { + throw new IllegalArgumentException("Expected 1 argument, got " + Arrays.toString(args) + "."); + } + bundleLocation = args[0]; + if (privileged) { + log.finer("Initializing application with privileges."); + loader.init(bundleLocation, true); + } + } + + @Override + public void start() throws Exception { + if (!privileged) { + log.finer("Initializing application without privileges."); + loader.init(bundleLocation, false); + } + loader.start(); + } + + @Override + public void stop() throws Exception { + loader.stop(); + } + + @Override + public void destroy() { + loader.destroy(); + } + + private static OsgiFramework newOsgiFramework() { + String cachePath = System.getProperty("jdisc.cache.path"); + if (cachePath == null) { + throw new IllegalStateException("System property 'jdisc.cache.path' not set."); + } + FelixParams params = new FelixParams() + .setCachePath(cachePath) + .setLoggerEnabled(Boolean.valueOf(System.getProperty("jdisc.logger.enabled", "true"))); + for (String str : ContainerBuilder.safeStringSplit(System.getProperty("jdisc.export.packages"), ",")) { + params.exportPackage(str); + } + return new FelixFramework(params); + } + + private static Iterable<Module> newConfigModule() { + String configFile = System.getProperty("jdisc.config.file"); + if (configFile == null) { + return Collections.emptyList(); + } + Module configModule; + try { + configModule = ApplicationConfigModule.newInstanceFromFile(configFile); + } catch (IOException e) { + throw new IllegalStateException("Exception thrown while reading config file '" + configFile + "'.", e); + } + return Arrays.asList(configModule); + } + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java new file mode 100644 index 00000000000..68e9f58c7ff --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.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.jdisc.core; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface BootstrapLoader { + + public void init(String bundleLocation, boolean privileged) throws Exception; + + public void start() throws Exception; + + public void stop() throws Exception; + + public void destroy(); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java new file mode 100644 index 00000000000..a65040b0451 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java @@ -0,0 +1,70 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import java.io.File; +import java.io.IOException; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class BundleLocationResolver { + + static final String BUNDLE_PATH = System.getProperty("jdisc.bundle.path", ".") + "/"; + + public static String resolve(String bundleLocation) { + bundleLocation = expandSystemProperties(bundleLocation); + bundleLocation = bundleLocation.trim(); + String scheme = getLocationScheme(bundleLocation); + if (scheme == null) { + bundleLocation = "file:" + getCanonicalPath(BUNDLE_PATH + bundleLocation); + } else if (scheme.equalsIgnoreCase("file")) { + bundleLocation = "file:" + getCanonicalPath(bundleLocation.substring(5)); + } + return bundleLocation; + } + + private static String expandSystemProperties(String str) { + StringBuilder ret = new StringBuilder(); + int prev = 0; + while (true) { + int from = str.indexOf("${", prev); + if (from < 0) { + break; + } + ret.append(str.substring(prev, from)); + prev = from; + + int to = str.indexOf("}", from); + if (to < 0) { + break; + } + ret.append(System.getProperty(str.substring(from + 2, to), "")); + prev = to + 1; + } + if (prev >= 0) { + ret.append(str.substring(prev)); + } + return ret.toString(); + } + + private static String getCanonicalPath(String path) { + try { + return new File(path).getCanonicalPath(); + } catch (IOException e) { + return path; + } + } + + private static String getLocationScheme(String bundleLocation) { + char[] arr = bundleLocation.toCharArray(); + for (int i = 0; i < arr.length; ++i) { + if (arr[i] == ':' && i > 0) { + return bundleLocation.substring(0, i); + } + if (!Character.isLetterOrDigit(arr[i])) { + return null; + } + } + return null; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java new file mode 100644 index 00000000000..899e8a98aa7 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java @@ -0,0 +1,199 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogService; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class ConsoleLogFormatter { + + // The string used as a replacement for absent/null values. + static final String ABSENCE_REPLACEMENT = "-"; + + private final String hostName; + private final String processId; + private final String serviceName; + + public ConsoleLogFormatter(String hostName, String processId, String serviceName) { + this.hostName = formatOptional(hostName); + this.processId = formatOptional(processId); + this.serviceName = formatOptional(serviceName); + } + + public String formatEntry(LogEntry entry) { + StringBuilder ret = new StringBuilder(); + formatTime(entry, ret).append('\t'); + formatHostName(ret).append('\t'); + formatProcessId(entry, ret).append('\t'); + formatServiceName(ret).append('\t'); + formatComponent(entry, ret).append('\t'); + formatLevel(entry, ret).append('\t'); + formatMessage(entry, ret); + formatException(entry, ret); + return ret.toString(); + } + + // TODO: The non-functional, side effect-laden coding style here is ugly and makes testing hard. See ticket 7128315. + + private StringBuilder formatTime(LogEntry entry, StringBuilder out) { + String str = Long.toString(Long.MAX_VALUE & entry.getTime()); // remove sign bit for good measure + int len = str.length(); + if (len > 3) { + out.append(str, 0, len - 3); + } else { + out.append('0'); + } + out.append('.'); + if (len > 2) { + out.append(str, len - 3, len); + } else if (len == 2) { + out.append('0').append(str, len - 2, len); // should never happen + } else if (len == 1) { + out.append("00").append(str, len - 1, len); // should never happen + } + return out; + } + + private StringBuilder formatHostName(StringBuilder out) { + out.append(hostName); + return out; + } + + private StringBuilder formatProcessId(LogEntry entry, StringBuilder out) { + out.append(processId); + String threadId = getProperty(entry, "THREAD_ID"); + if (threadId != null) { + out.append('/').append(threadId); + } + return out; + } + + private StringBuilder formatServiceName(StringBuilder out) { + out.append(serviceName); + return out; + } + + private StringBuilder formatComponent(LogEntry entry, StringBuilder out) { + Bundle bundle = entry.getBundle(); + String loggerName = getProperty(entry, "LOGGER_NAME"); + if (bundle == null && loggerName == null) { + out.append("-"); + } else { + if (bundle != null) { + out.append(bundle.getSymbolicName()); + } + if (loggerName != null) { + out.append('/').append(loggerName); + } + } + return out; + } + + private StringBuilder formatLevel(LogEntry entry, StringBuilder out) { + switch (entry.getLevel()) { + case LogService.LOG_ERROR: + out.append("error"); + break; + case LogService.LOG_WARNING: + out.append("warning"); + break; + case LogService.LOG_INFO: + out.append("info"); + break; + case LogService.LOG_DEBUG: + out.append("debug"); + break; + default: + out.append("unknown"); + break; + } + return out; + } + + private StringBuilder formatMessage(LogEntry entry, StringBuilder out) { + String msg = entry.getMessage(); + if (msg != null) { + formatString(msg, out); + } + return out; + } + + private StringBuilder formatException(LogEntry entry, StringBuilder out) { + Throwable t = entry.getException(); + if (t != null) { + if (entry.getLevel() == LogService.LOG_INFO) { + out.append(": "); + String msg = t.getMessage(); + if (msg != null) { + formatString(msg, out); + } else { + out.append(t.getClass().getName()); + } + } else { + Writer buf = new StringWriter(); + t.printStackTrace(new PrintWriter(buf)); + formatString("\n" + buf, out); + } + } + return out; + } + + private static StringBuilder formatString(String str, StringBuilder out) { + for (int i = 0, len = str.length(); i < len; ++i) { + char c = str.charAt(i); + switch (c) { + case '\n': + out.append("\\n"); + break; + case '\r': + out.append("\\r"); + break; + case '\t': + out.append("\\t"); + break; + case '\\': + out.append("\\\\"); + break; + default: + out.append(c); + break; + } + } + return out; + } + + private static String getProperty(LogEntry entry, String name) { + ServiceReference<?> ref = entry.getServiceReference(); + if (ref == null) { + return null; + } + Object val = ref.getProperty(name); + if (val == null) { + return null; + } + return val.toString(); + } + + static String formatOptional(String str) { + return formatOptional(str, ABSENCE_REPLACEMENT); + } + + private static String formatOptional(final String str, final String replacementIfAbsent) { + if (str == null) { + return replacementIfAbsent; + } + final String result = str.trim(); + if (result.isEmpty()) { + return replacementIfAbsent; + } + return result; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java new file mode 100644 index 00000000000..b41e195f6a7 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java @@ -0,0 +1,109 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogListener; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.management.ManagementFactory; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +class ConsoleLogListener implements LogListener { + + public static final int DEFAULT_LOG_LEVEL = Integer.MAX_VALUE; + private final ConsoleLogFormatter formatter; + private final PrintStream out; + private final int maxLevel; + + ConsoleLogListener(PrintStream out, String serviceName, String logLevel) { + this.out = out; + this.formatter = new ConsoleLogFormatter(getHostname(), getProcessId(), serviceName); + this.maxLevel = parseLogLevel(logLevel); + } + + @Override + public void logged(LogEntry entry) { + if (entry.getLevel() > maxLevel) { + return; + } + out.println(formatter.formatEntry(entry)); + } + + public static int parseLogLevel(String logLevel) { + if (logLevel == null || logLevel.isEmpty()) { + return DEFAULT_LOG_LEVEL; + } + if (logLevel.equalsIgnoreCase("OFF")) { + return Integer.MIN_VALUE; + } + if (logLevel.equalsIgnoreCase("ERROR")) { + return 1; + } + if (logLevel.equalsIgnoreCase("WARNING")) { + return 2; + } + if (logLevel.equalsIgnoreCase("INFO")) { + return 3; + } + if (logLevel.equalsIgnoreCase("DEBUG")) { + return 4; + } + if (logLevel.equalsIgnoreCase("ALL")) { + return Integer.MAX_VALUE; + } + try { + return Integer.valueOf(logLevel); + } catch (NumberFormatException e) { + // fall through + } + return DEFAULT_LOG_LEVEL; + } + + public static ConsoleLogListener newInstance() { + return new ConsoleLogListener(System.out, + System.getProperty("jdisc.logger.tag"), + System.getProperty("jdisc.logger.level")); + } + + static String getProcessId() { + // platform independent + String jvmName = ManagementFactory.getRuntimeMXBean().getName(); + if (jvmName != null) { + int idx = jvmName.indexOf('@'); + if (idx > 0) { + try { + return Long.toString(Long.valueOf(jvmName.substring(0, jvmName.indexOf('@')))); + } catch (NumberFormatException e) { + // fall through + } + } + } + + // linux specific + File file = new File("/proc/self"); + if (file.exists()) { + try { + return file.getCanonicalFile().getName(); + } catch (IOException e) { + return null; + } + } + + // fallback + return null; + } + + static String getHostname() { + try { + return InetAddress.getLocalHost().getCanonicalHostName(); + } catch (UnknownHostException e) { + return null; + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java new file mode 100644 index 00000000000..c5e8602c861 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogReaderService; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +class ConsoleLogManager { + + private final ConsoleLogListener listener = ConsoleLogListener.newInstance(); + private ServiceTracker<LogReaderService,LogReaderService> tracker; + + @SuppressWarnings("unchecked") + public void install(final BundleContext osgiContext) { + if (tracker != null) { + throw new IllegalStateException("ConsoleLogManager already installed."); + } + tracker = new ServiceTracker<LogReaderService,LogReaderService>(osgiContext, LogReaderService.class.getName(), + new ServiceTrackerCustomizer<LogReaderService,LogReaderService>() { + + @Override + public LogReaderService addingService(ServiceReference<LogReaderService> reference) { + LogReaderService service = osgiContext.getService(reference); + service.addLogListener(listener); + return service; + } + + @Override + public void modifiedService(ServiceReference<LogReaderService> reference, LogReaderService service) { + + } + + @Override + public void removedService(ServiceReference<LogReaderService> reference, LogReaderService service) { + service.removeLogListener(listener); + } + }); + tracker.open(); + } + + public boolean uninstall() { + if (tracker == null) { + return false; + } + tracker.close(); + tracker = null; + return true; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java new file mode 100644 index 00000000000..4f4544fa8f8 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java @@ -0,0 +1,112 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Key; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.NullContent; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; + +import java.util.Objects; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +class ContainerSnapshot extends AbstractResource implements Container { + + private final TimeoutManagerImpl timeoutMgr; + private final ActiveContainer container; + private final ResourceReference containerReference; + private final BindingSet<RequestHandler> serverBindings; + private final BindingSet<RequestHandler> clientBindings; + + ContainerSnapshot(ActiveContainer container, BindingSet<RequestHandler> serverBindings, + BindingSet<RequestHandler> clientBindings) + { + this.timeoutMgr = container.timeoutManager(); + this.container = container; + this.serverBindings = serverBindings; + this.clientBindings = clientBindings; + this.containerReference = container.refer(); + } + + @Override + public <T> T getInstance(Key<T> key) { + return container.guiceInjector().getInstance(key); + } + + @Override + public <T> T getInstance(Class<T> type) { + return container.guiceInjector().getInstance(type); + } + + @Override + public RequestHandler resolveHandler(Request request) { + BindingMatch<RequestHandler> match = request.isServerRequest() ? serverBindings.match(request.getUri()) + : clientBindings.match(request.getUri()); + if (match == null) { + return null; + } + request.setBindingMatch(match); + RequestHandler ret = new NullContentRequestHandler(match.target()); + if (request.getTimeoutManager() == null) { + ret = timeoutMgr.manageHandler(ret); + } + return ret; + } + + @Override + protected void destroy() { + containerReference.close(); + } + + @Override + public long currentTimeMillis() { + return timeoutMgr.timer().currentTimeMillis(); + } + + private static class NullContentRequestHandler implements RequestHandler { + + final RequestHandler delegate; + + NullContentRequestHandler(RequestHandler delegate) { + Objects.requireNonNull(delegate, "delegate"); + this.delegate = delegate; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) { + ContentChannel content = delegate.handleRequest(request, responseHandler); + if (content == null) { + content = NullContent.INSTANCE; + } + return content; + } + + @Override + public void handleTimeout(Request request, ResponseHandler responseHandler) { + delegate.handleTimeout(request, responseHandler); + } + + @Override + public ResourceReference refer() { + return delegate.refer(); + } + + @Override + public void release() { + delegate.release(); + } + + @Override + public String toString() { + return delegate.toString(); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java new file mode 100644 index 00000000000..0fd25bfb390 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.application.DeactivatedContainer; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerTermination implements DeactivatedContainer, Runnable { + + private final Object lock = new Object(); + private final Object appContext; + private Runnable task; + private boolean done; + + public ContainerTermination(Object appContext) { + this.appContext = appContext; + } + + @Override + public Object appContext() { + return appContext; + } + + @Override + public void notifyTermination(Runnable task) { + boolean done; + synchronized (lock) { + if (this.task != null) { + throw new IllegalStateException(); + } + this.task = task; + done = this.done; + } + if (done) { + task.run(); + } + } + + @Override + public void run() { + Runnable task; + synchronized (lock) { + done = true; + task = this.task; + } + if (task != null) { + task.run(); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java new file mode 100644 index 00000000000..7e4a7b6ec5e --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.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.jdisc.core; + +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.BindingSetSelector; + +import java.net.URI; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class DefaultBindingSelector implements BindingSetSelector { + + @Override + public String select(URI uri) { + return BindingSet.DEFAULT; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java new file mode 100644 index 00000000000..afe43718bc5 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java @@ -0,0 +1,98 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.container.plugin.bundle.AnalyzeBundle; +import com.yahoo.container.plugin.bundle.TransformExportPackages; +import com.yahoo.container.plugin.osgi.ExportPackages.Export; +import org.apache.felix.framework.util.Util; +import org.osgi.framework.Constants; +import scala.collection.immutable.List; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Properties; +import java.util.jar.JarInputStream; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ExportPackages { + + public static final String PROPERTIES_FILE = "/exportPackages.properties"; + public static final String EXPORT_PACKAGES = "exportPackages"; + private static final String REPLACE_VERSION_PREFIX = "__REPLACE_VERSION__"; + + public static void main(String[] args) throws IOException { + String fileName = args[0]; + if (!fileName.endsWith(PROPERTIES_FILE)) { + throw new IllegalArgumentException("Expected '" + PROPERTIES_FILE + "', got '" + fileName + "'."); + } + StringBuilder out = new StringBuilder(); + out.append(getSystemPackages()).append(",") + .append("com.sun.security.auth,") + .append("com.sun.security.auth.module,") + .append("com.sun.management,") + .append("com.yahoo.jdisc,") + .append("com.yahoo.jdisc.application,") + .append("com.yahoo.jdisc.handler,") + .append("com.yahoo.jdisc.service,") + .append("javax.inject;version=1.0.0,") // Included in guice, but not exported. Needed by container-jersey. + .append("org.aopalliance.intercept,") + .append("org.aopalliance.aop,") + .append("org.w3c.dom.css,") + .append("org.w3c.dom.html,") + .append("org.w3c.dom.ranges,") + .append("org.w3c.dom.stylesheets,") + .append("org.w3c.dom.traversal,") + .append("org.w3c.dom.views,") + .append("sun.misc,") + .append("sun.net.util,") + .append("sun.security.krb5"); + for (int i = 1; i < args.length; ++i) { + out.append(",").append(getExportedPackages(args[i])); + } + Properties props = new Properties(); + props.setProperty(EXPORT_PACKAGES, out.toString()); + + try (FileWriter writer = new FileWriter(new File(fileName))) { + props.store(writer, "generated by " + ExportPackages.class.getName()); + } + } + + public static String readExportProperty() { + Properties props = new Properties(); + try { + props.load(ExportPackages.class.getResourceAsStream(PROPERTIES_FILE)); + } catch (IOException e) { + throw new IllegalStateException("Failed to read resource '" + PROPERTIES_FILE + "'."); + } + return props.getProperty(EXPORT_PACKAGES); + } + + public static String getSystemPackages() { + return Util.getDefaultProperty(null, "org.osgi.framework.system.packages"); + } + + private static String getExportedPackages(String argument) throws IOException { + if (argument.startsWith(REPLACE_VERSION_PREFIX)) { + String jarFile = argument.substring(REPLACE_VERSION_PREFIX.length()); + return readExportHeader(jarFile); + } else { + return readExportHeader(argument); + } + } + + private static String readExportHeader(String jarFile) throws IOException { + try (JarInputStream jar = new JarInputStream(new FileInputStream(jarFile))) { + return jar.getManifest().getMainAttributes().getValue(Constants.EXPORT_PACKAGE); + } + } + + private static String transformExports(List<Export> exports, String newVersion) { + return TransformExportPackages.toExportPackageProperty( + TransformExportPackages.removeUses( + TransformExportPackages.replaceVersions(exports, newVersion))); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java new file mode 100644 index 00000000000..6509d505c70 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java @@ -0,0 +1,175 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Inject; +import com.yahoo.jdisc.application.BundleInstallationException; +import com.yahoo.jdisc.application.OsgiFramework; +import com.yahoo.jdisc.application.OsgiHeader; +import org.apache.felix.framework.Felix; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.wiring.FrameworkWiring; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class FelixFramework implements OsgiFramework { + + private static final Logger log = Logger.getLogger(FelixFramework.class.getName()); + private final OsgiLogManager logHandler = OsgiLogManager.newInstance(); + private final OsgiLogService logService = new OsgiLogService(); + private final ConsoleLogManager logListener; + private final Felix felix; + + @Inject + public FelixFramework(FelixParams params) { + deleteDirContents(new File(params.getCachePath())); + felix = new Felix(params.toConfig()); + logListener = params.isLoggerEnabled() ? new ConsoleLogManager() : null; + } + + @Override + public void start() throws BundleException { + log.finer("Starting Felix."); + felix.start(); + + BundleContext ctx = felix.getBundleContext(); + logService.start(ctx); + logHandler.install(ctx); + if (logListener != null) { + logListener.install(ctx); + } + } + + @Override + public void stop() throws BundleException { + log.fine("Stopping felix."); + BundleContext ctx = felix.getBundleContext(); + if (ctx != null) { + if (logListener != null) { + logListener.uninstall(); + } + logHandler.uninstall(); + logService.stop(); + } + felix.stop(); + try { + felix.waitForStop(0); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public List<Bundle> installBundle(String bundleLocation) throws BundleException { + List<Bundle> bundles = new LinkedList<>(); + try { + installBundle(bundleLocation, new HashSet<>(), bundles); + } catch (Exception e) { + throw new BundleInstallationException(bundles, e); + } + return bundles; + } + + @Override + public void startBundles(List<Bundle> bundles, boolean privileged) throws BundleException { + for (Bundle bundle : bundles) { + if (!privileged && OsgiHeader.isSet(bundle, OsgiHeader.PRIVILEGED_ACTIVATOR)) { + log.log(Level.INFO, "OSGi bundle '" + bundle.getSymbolicName() + "' " + + "states that it requires privileged " + + "initialization, but privileges are not available. YMMV."); + } + if (bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null) { + continue; // fragments can not be started + } + bundle.start(); + } + } + + @Override + public void refreshPackages() { + FrameworkWiring wiring = felix.adapt(FrameworkWiring.class); + final CountDownLatch latch = new CountDownLatch(1); + wiring.refreshBundles(null, + event -> { + switch (event.getType()) { + case FrameworkEvent.PACKAGES_REFRESHED: + latch.countDown(); + break; + case FrameworkEvent.ERROR: + log.log(Level.SEVERE, "ERROR FrameworkEvent received.", event.getThrowable()); + break; + } + }); + try { + long TIMEOUT_SECONDS = 60L; + if (!latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + log.warning("No PACKAGES_REFRESHED FrameworkEvent received within " + TIMEOUT_SECONDS + + " seconds of calling FrameworkWiring.refreshBundles()"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public BundleContext bundleContext() { + return felix.getBundleContext(); + } + + @Override + public List<Bundle> bundles() { + return Arrays.asList(felix.getBundleContext().getBundles()); + } + + private void installBundle(String bundleLocation, Set<String> mask, List<Bundle> out) throws BundleException { + bundleLocation = BundleLocationResolver.resolve(bundleLocation); + if (mask.contains(bundleLocation)) { + log.finer("OSGi bundle from '" + bundleLocation + "' already installed."); + return; + } + log.finer("Installing OSGi bundle from '" + bundleLocation + "'."); + mask.add(bundleLocation); + + Bundle bundle = felix.getBundleContext().installBundle(bundleLocation); + String symbol = bundle.getSymbolicName(); + if (symbol == null) { + bundle.uninstall(); + throw new BundleException("Missing Bundle-SymbolicName in manifest from '" + bundleLocation + " " + + "(it might not be an OSGi bundle)."); + } + out.add(bundle); + for (String preInstall : OsgiHeader.asList(bundle, OsgiHeader.PREINSTALL_BUNDLE)) { + log.finer("OSGi bundle '" + symbol + "' requires install from '" + preInstall + "'."); + installBundle(preInstall, mask, out); + } + } + + private static void deleteDirContents(File parent) { + File[] children = parent.listFiles(); + if (children != null) { + for (File child : children) { + deleteDirContents(child); + boolean deleted = child.delete(); + if (! deleted) + throw new RuntimeException( + "Could not delete file '" + child.getAbsolutePath() +"'. Please check file permissions!"); + } + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java new file mode 100644 index 00000000000..0fe09798ccc --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.apache.felix.framework.cache.BundleCache; +import org.osgi.framework.Constants; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class FelixParams { + + private final StringBuilder exportPackages = new StringBuilder(ExportPackages.readExportProperty()); + private String cachePath = null; + private boolean loggerEnabled = true; + + public FelixParams exportPackage(String pkg) { + exportPackages.append(",").append(pkg); + return this; + } + + public FelixParams setCachePath(String cachePath) { + this.cachePath = cachePath; + return this; + } + + public String getCachePath() { + return cachePath; + } + + public FelixParams setLoggerEnabled(boolean loggerEnabled) { + this.loggerEnabled = loggerEnabled; + return this; + } + + public boolean isLoggerEnabled() { + return loggerEnabled; + } + + public Map<String, String> toConfig() { + Map<String, String> ret = new HashMap<>(); + ret.put(BundleCache.CACHE_ROOTDIR_PROP, cachePath); + ret.put(Constants.FRAMEWORK_SYSTEMPACKAGES, exportPackages.toString()); + ret.put(Constants.SUPPORTS_BOOTCLASSPATH_EXTENSION, "true"); + ret.put(Constants.FRAMEWORK_BOOTDELEGATION, "com.yourkit.runtime,com.yourkit.probes,com.yourkit.probes.builtin"); + return ret; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java new file mode 100644 index 00000000000..c4de1d5a7ac --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java @@ -0,0 +1,164 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.common.collect.ImmutableMap; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class OsgiLogHandler extends Handler { + + private static enum LogRecordProperty { + + LEVEL, + LOGGER_NAME, + MESSAGE, + MILLIS, + PARAMETERS, + RESOURCE_BUNDLE, + RESOURCE_BUNDLE_NAME, + SEQUENCE_NUMBER, + SOURCE_CLASS_NAME, + SOURCE_METHOD_NAME, + THREAD_ID, + THROWN + } + + private final static Map<String, LogRecordProperty> PROPERTY_MAP = createDictionary(LogRecordProperty.values()); + private final static String[] PROPERTY_KEYS = toStringArray(LogRecordProperty.values()); + private final LogService logService; + + public OsgiLogHandler(LogService logService) { + this.logService = logService; + } + + @Override + public void publish(LogRecord record) { + logService.log(new LogRecordReference(record), toServiceLevel(record.getLevel()), record.getMessage(), + record.getThrown()); + } + + @Override + public void flush() { + // empty + } + + @Override + public void close() { + // empty + } + + public static int toServiceLevel(Level level) { + int val = level.intValue(); + if (val >= Level.SEVERE.intValue()) { + return LogService.LOG_ERROR; + } + if (val >= Level.WARNING.intValue()) { + return LogService.LOG_WARNING; + } + if (val >= Level.INFO.intValue()) { + return LogService.LOG_INFO; + } + // Level.CONFIG + // Level.FINE + // Level.FINER + // Level.FINEST + return LogService.LOG_DEBUG; + } + + private static <T> Map<String, T> createDictionary(T[] in) { + Map<String, T> out = new HashMap<>(); + for (T t : in) { + out.put(String.valueOf(t), t); + } + return ImmutableMap.copyOf(out); + } + + private static String[] toStringArray(Object[] in) { + String[] out = new String[in.length]; + for (int i = 0; i < in.length; ++i) { + out[i] = String.valueOf(in[i]); + } + return out; + } + + private static class LogRecordReference implements ServiceReference<LogRecord> { + + final LogRecord record; + + LogRecordReference(LogRecord record) { + this.record = record; + } + + @Override + public Object getProperty(String s) { + LogRecordProperty property = PROPERTY_MAP.get(s); + if (property == null) { + return null; + } + switch (property) { + case LEVEL: + return record.getLevel(); + case LOGGER_NAME: + return record.getLoggerName(); + case MESSAGE: + return record.getMessage(); + case MILLIS: + return record.getMillis(); + case PARAMETERS: + return record.getParameters(); + case RESOURCE_BUNDLE: + return record.getResourceBundle(); + case RESOURCE_BUNDLE_NAME: + return record.getResourceBundleName(); + case SEQUENCE_NUMBER: + return record.getSequenceNumber(); + case SOURCE_CLASS_NAME: + return record.getSourceClassName(); + case SOURCE_METHOD_NAME: + return record.getSourceMethodName(); + case THREAD_ID: + return record.getThreadID(); + case THROWN: + return record.getThrown(); + default: + throw new UnsupportedOperationException(); + } + } + + @Override + public String[] getPropertyKeys() { + return PROPERTY_KEYS; + } + + @Override + public Bundle getBundle() { + return null; + } + + @Override + public Bundle[] getUsingBundles() { + return new Bundle[0]; + } + + @Override + public boolean isAssignableTo(Bundle bundle, String s) { + return false; + } + + @Override + public int compareTo(Object o) { + return 0; + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java new file mode 100644 index 00000000000..af2ee5832aa --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java @@ -0,0 +1,102 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class OsgiLogManager implements LogService { + + private static final Object globalLock = new Object(); + private final CopyOnWriteArrayList<LogService> services = new CopyOnWriteArrayList<>(); + private final boolean configureLogLevel; + private ServiceTracker<LogService,LogService> tracker; + + OsgiLogManager(boolean configureLogLevel) { + this.configureLogLevel = configureLogLevel; + } + + @SuppressWarnings("unchecked") + public void install(final BundleContext osgiContext) { + if (tracker != null) { + throw new IllegalStateException("OsgiLogManager already installed."); + } + tracker = new ServiceTracker<>(osgiContext, LogService.class, new ServiceTrackerCustomizer<LogService,LogService>() { + + @Override + public LogService addingService(ServiceReference<LogService> reference) { + LogService service = osgiContext.getService(reference); + services.add(service); + return service; + } + + @Override + public void modifiedService(ServiceReference<LogService> reference, LogService service) { + + } + + @Override + public void removedService(ServiceReference<LogService> reference, LogService service) { + services.remove(service); + } + }); + tracker.open(); + synchronized (globalLock) { + Logger root = Logger.getLogger(""); + if (configureLogLevel) { + root.setLevel(Level.ALL); + } + for (Handler handler : root.getHandlers()) { + root.removeHandler(handler); + } + root.addHandler(new OsgiLogHandler(this)); + } + } + + public boolean uninstall() { + if (tracker == null) { + return false; + } + tracker.close(); // implicitly clears the services array + tracker = null; + return true; + } + + @Override + public void log(int level, String message) { + log(null, level, message, null); + } + + @Override + public void log(int level, String message, Throwable throwable) { + log(null, level, message, throwable); + } + + @SuppressWarnings("rawtypes") + @Override + public void log(ServiceReference serviceRef, int level, String message) { + log(serviceRef, level, message, null); + } + + @SuppressWarnings("rawtypes") + @Override + public void log(ServiceReference serviceRef, int level, String message, Throwable throwable) { + for (LogService obj : services) { + obj.log(serviceRef, level, message, throwable); + } + } + + public static OsgiLogManager newInstance() { + return new OsgiLogManager(System.getProperty("java.util.logging.config.file") == null); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java new file mode 100644 index 00000000000..0e2a31938ce --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.framework.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class OsgiLogService { + + private ServiceRegistration<OsgiLogService> registration; + + public void start(BundleContext ctx) { + if (registration != null) { + throw new IllegalStateException(); + } + ctx.addServiceListener(new ActivatorProxy(ctx)); + registration = ctx.registerService(OsgiLogService.class, this, null); + } + + public void stop() { + registration.unregister(); + registration = null; + } + + private class ActivatorProxy implements ServiceListener { + + final BundleActivator activator = new org.apache.felix.log.Activator(); + final BundleContext ctx; + + ActivatorProxy(BundleContext ctx) { + this.ctx = ctx; + } + + @Override + public void serviceChanged(ServiceEvent event) { + if (ctx.getService(event.getServiceReference()) != OsgiLogService.this) { + return; + } + switch (event.getType()) { + case ServiceEvent.REGISTERED: + try { + activator.start(ctx); + } catch (Exception e) { + throw new RuntimeException("Exception thrown while starting " + + activator.getClass().getName() + ".", e); + } + break; + case ServiceEvent.UNREGISTERING: + try { + activator.stop(ctx); + } catch (Exception e) { + throw new RuntimeException("Exception thrown while stopping " + + activator.getClass().getName() + ".", e); + } + break; + } + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java new file mode 100644 index 00000000000..ef0e549516a --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java @@ -0,0 +1,136 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import java.util.Objects; +import java.util.Queue; + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class ScheduledQueue { + + public static final int MILLIS_PER_SLOT = 100; + public static final int NUM_SLOTS = 512; + public static final int NUM_SLOTS_UNDILATED = 3; + public static final int SLOT_MASK = 511; // bitmask to modulo NUM_SLOTS + public static final int ITER_SHIFT = 9; // number of bits to shift off SLOT_MASK + + private final Entry[] slots = new Entry[NUM_SLOTS + 1]; + private final int[] counts = new int[NUM_SLOTS + 1]; + private int currIter = 0; + private int currSlot = 0; + private long nextTick; + + public ScheduledQueue(long currentTimeMillis) { + this.nextTick = currentTimeMillis + MILLIS_PER_SLOT; + } + + public Entry newEntry(Object payload) { + Objects.requireNonNull(payload, "payload"); + return new Entry(payload); + } + + public synchronized void drainTo(long currentTimeMillis, Queue<Object> out) { + if (slots[NUM_SLOTS] == null && currentTimeMillis < nextTick) { + return; + } + drainTo(NUM_SLOTS, 0, out); + for (int i = 0; currentTimeMillis >= nextTick; i++, nextTick += MILLIS_PER_SLOT) { + if (i < NUM_SLOTS_UNDILATED) { + if (++currSlot >= NUM_SLOTS) { + currSlot = 0; + currIter++; + } + drainTo(currSlot, currIter, out); + } + } + } + + private void drainTo(int slot, int iter, Queue<Object> out) { + int cnt = counts[slot]; + Entry entry = slots[slot]; + for (int i = 0; i < cnt; i++) { + Entry next = entry.next; + if (entry.iter == iter) { + linkOut(entry); + out.add(entry.payload); + } + entry = next; + } + } + + private synchronized void scheduleAt(Entry entry, long expireAtMillis) { + if (entry.next != null) { + linkOut(entry); + } + long delayMillis = expireAtMillis - nextTick; + if (delayMillis < 0) { + entry.slot = NUM_SLOTS; + entry.iter = 0; + } else { + long ticks = 1 + (int)((delayMillis + MILLIS_PER_SLOT / 2) / MILLIS_PER_SLOT); + entry.slot = (int)((ticks + currSlot) & SLOT_MASK); + entry.iter = currIter + (int)((ticks + currSlot) >> ITER_SHIFT); + } + linkIn(entry); + } + + private synchronized void unschedule(Entry entry) { + if (entry.next != null) { + linkOut(entry); + } + } + + private void linkIn(Entry entry) { + Entry head = slots[entry.slot]; + if (head == null) { + entry.next = entry; + entry.prev = entry; + slots[entry.slot] = entry; + } else { + entry.next = head; + entry.prev = head.prev; + head.prev.next = entry; + head.prev = entry; + } + ++counts[entry.slot]; + } + + private void linkOut(Entry entry) { + Entry head = slots[entry.slot]; + if (entry.next == entry) { + slots[entry.slot] = null; + } else { + entry.prev.next = entry.next; + entry.next.prev = entry.prev; + if (head == entry) { + slots[entry.slot] = entry.next; + } + } + entry.next = null; + entry.prev = null; + --counts[entry.slot]; + } + + public class Entry { + + private final Object payload; + private int slot; + private int iter; + private Entry next; + private Entry prev; + + private Entry(Object payload) { + this.payload = payload; + } + + public void scheduleAt(long expireAtMillis) { + ScheduledQueue.this.scheduleAt(this, expireAtMillis); + } + + public void unschedule() { + ScheduledQueue.this.unschedule(this); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java new file mode 100644 index 00000000000..371ab52f26b --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.Timer; + +/** + * A timer which returns the System time + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class SystemTimer implements Timer { + + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java new file mode 100644 index 00000000000..8e0c624b348 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java @@ -0,0 +1,244 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Inject; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.TimeoutManager; +import com.yahoo.jdisc.Timer; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TimeoutManagerImpl { + + private static final ContentChannel IGNORED_CONTENT = new IgnoredContent(); + private static final Logger log = Logger.getLogger(TimeoutManagerImpl.class.getName()); + private final ScheduledQueue schedules[] = new ScheduledQueue[Runtime.getRuntime().availableProcessors()]; + private final Thread thread; + private final Timer timer; + private volatile int nextScheduler = 0; + private volatile int queueSize = 0; + private volatile boolean done = false; + + @Inject + public TimeoutManagerImpl(ThreadFactory factory, Timer timer) { + this.thread = factory.newThread(new ManagerTask()); + this.thread.setName(getClass().getName()); + this.timer = timer; + + long now = timer.currentTimeMillis(); + for (int i = 0; i < schedules.length; ++i) { + schedules[i] = new ScheduledQueue(now); + } + } + + public void start() { + thread.start(); + } + + public void shutdown() { + done = true; + } + + public RequestHandler manageHandler(RequestHandler handler) { + return new ManagedRequestHandler(handler); + } + + int queueSize() { + return queueSize; // unstable snapshot, only for test purposes + } + + Timer timer() { + return timer; + } + + void checkTasks(long currentTimeMillis) { + Queue<Object> queue = new LinkedList<>(); + for (ScheduledQueue schedule : schedules) { + schedule.drainTo(currentTimeMillis, queue); + } + while (!queue.isEmpty()) { + TimeoutHandler timeoutHandler = (TimeoutHandler)queue.poll(); + invokeTimeout(timeoutHandler.requestHandler, timeoutHandler.request, timeoutHandler); + } + } + + private void invokeTimeout(RequestHandler requestHandler, Request request, ResponseHandler responseHandler) { + try { + requestHandler.handleTimeout(request, responseHandler); + } catch (RuntimeException e) { + log.log(Level.WARNING, "Ignoring exception thrown by " + requestHandler.getClass().getName() + + " in timeout manager.", e); + } + if (Thread.currentThread().isInterrupted()) { + log.log(Level.WARNING, "Ignoring interrupt signal from " + requestHandler.getClass().getName() + + " in timeout manager."); + Thread.interrupted(); + } + } + + private class ManagerTask implements Runnable { + + @Override + public void run() { + while (!done) { + try { + Thread.sleep(ScheduledQueue.MILLIS_PER_SLOT); + } catch (InterruptedException e) { + log.log(Level.WARNING, "Ignoring interrupt signal in timeout manager.", e); + } + checkTasks(timer.currentTimeMillis()); + } + } + } + + private class ManagedRequestHandler implements RequestHandler { + + final RequestHandler delegate; + + ManagedRequestHandler(RequestHandler delegate) { + this.delegate = delegate; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) { + TimeoutHandler timeoutHandler = new TimeoutHandler(request, delegate, responseHandler); + request.setTimeoutManager(timeoutHandler); + try { + return delegate.handleRequest(request, timeoutHandler); + } catch (Throwable throwable) { + //This is only needed when this method is invoked outside of Request.connect, + //and that seems to be the case for jetty right now. + //To prevent this from being called outside Request.connect, + //manageHandler() and com.yahoo.jdisc.Container.resolveHandler() must also be made non-public. + // + //The underlying framework will handle the request, + //the application code is no longer responsible for calling responseHandler.handleResponse. + timeoutHandler.unscheduleTimeout(); + throw throwable; + } + } + + @Override + public void handleTimeout(Request request, ResponseHandler responseHandler) { + delegate.handleTimeout(request, responseHandler); + } + + @Override + public ResourceReference refer() { + return delegate.refer(); + } + + @Override + public void release() { + delegate.release(); + } + + @Override + public String toString() { + return delegate.toString(); + } + } + + private class TimeoutHandler implements ResponseHandler, TimeoutManager { + + final ResponseHandler responseHandler; + final RequestHandler requestHandler; + final Request request; + ScheduledQueue.Entry timeoutQueueEntry = null; + boolean responded = false; + + TimeoutHandler(Request request, RequestHandler requestHandler, ResponseHandler responseHandler) { + this.request = request; + this.requestHandler = requestHandler; + this.responseHandler = responseHandler; + } + + @Override + public synchronized void scheduleTimeout(Request request) { + if (responded) { + return; + } + if (timeoutQueueEntry == null) { + timeoutQueueEntry = schedules[(++nextScheduler & 0xffff) % schedules.length].newEntry(this); + } + timeoutQueueEntry.scheduleAt(request.creationTime(TimeUnit.MILLISECONDS) + request.getTimeout(TimeUnit.MILLISECONDS)); + ++queueSize; + } + + synchronized void unscheduleTimeout() { + if (!responded && timeoutQueueEntry != null) { + timeoutQueueEntry.unschedule(); + //guard against unscheduling from ManagedRequestHandler.handleRequest catch block + //followed by unscheduling in another thread from TimeoutHandler.handleResponse + timeoutQueueEntry = null; + } + --queueSize; + } + + @Override + public void unscheduleTimeout(Request request) { + unscheduleTimeout(); + } + + @Override + public ContentChannel handleResponse(Response response) { + synchronized (this) { + unscheduleTimeout(); + if (responded) { + return IGNORED_CONTENT; + } + responded = true; + } + return responseHandler.handleResponse(response); + } + + @Override + public String toString() { + return responseHandler.toString(); + } + } + + private static class IgnoredContent implements ContentChannel { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + if (handler == null) { + return; + } + try { + handler.completed(); + } catch (RuntimeException e) { + log.log(Level.WARNING, "Ignoring exception thrown by " + handler.getClass().getName() + + " in timeout manager.", e); + } + } + + @Override + public void close(CompletionHandler handler) { + if (handler == null) { + return; + } + try { + handler.completed(); + } catch (RuntimeException e) { + log.log(Level.WARNING, "Ignoring exception thrown by " + handler.getClass().getName() + + " in timeout manager.", e); + } + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractContentOutputStream.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractContentOutputStream.java new file mode 100644 index 00000000000..de656842f10 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractContentOutputStream.java @@ -0,0 +1,68 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +abstract class AbstractContentOutputStream extends OutputStream { + + public static final int BUFFERSIZE = 4096; + private ByteBuffer current; + + @Override + public final void write(int b) { + if (current == null) { + current = ByteBuffer.allocate(BUFFERSIZE); + } + current.put((byte)b); + if (current.remaining() == 0) { + flush(); + } + } + + @Override + public final void write(byte[] buf, int offset, int length) { + Objects.requireNonNull(buf, "buf"); + if (current == null) { + current = ByteBuffer.allocate(BUFFERSIZE + length); + } + int part = Math.min(length, current.remaining()); + current.put(buf, offset, part); + if (current.remaining() == 0) { + flush(); + } + if (part < length) { + write(buf, offset + part, length - part); + } + } + + @Override + public final void write(byte[] buf) { + write(buf, 0, buf.length); + } + + @Override + public final void flush() { + if (current == null || current.position() == 0) { + return; + } + ByteBuffer buf = current; + current = null; + buf.flip(); + doFlush(buf); + } + + @Override + public final void close() { + flush(); + doClose(); + } + + protected abstract void doFlush(ByteBuffer buf); + + protected abstract void doClose(); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractRequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractRequestHandler.java new file mode 100644 index 00000000000..9bc934cf724 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractRequestHandler.java @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; + +/** + * <p>This class provides an abstract {@link RequestHandler} implementation with reasonable defaults for everything but + * {@link #handleRequest(Request, ResponseHandler)}.</p> + * + * <p>A very simple hello world handler could be implemented like this:</p> + * <pre> + * class HelloWorldHandler extends AbstractRequestHandler { + * + * @Override + * public ContentChannel handleRequest(Request request, ResponseHandler handler) { + * ContentWriter writer = ResponseDispatch.newInstance(Response.Status.OK).connectWriter(handler); + * try { + * writer.write("Hello World!"); + * } finally { + * writer.close(); + * } + * return null; + * } + * } + * </pre> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class AbstractRequestHandler extends com.yahoo.jdisc.AbstractResource implements RequestHandler { + + @Override + public void handleTimeout(Request request, ResponseHandler responseHandler) { + Response.dispatchTimeout(responseHandler); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BindingNotFoundException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BindingNotFoundException.java new file mode 100644 index 00000000000..8cfb894b6bf --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BindingNotFoundException.java @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.application.BindingSet; + +import java.net.URI; + +/** + * This exception is used to signal that no binding was found for the {@link URI} of a given {@link Request}. An + * instance of this class will be thrown by the {@link Request#connect(ResponseHandler)} method when the current {@link + * BindingSet} has not binding that matches the corresponding Request's URI. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class BindingNotFoundException extends RuntimeException { + + private final URI uri; + + /** + * Constructs a new instance of this class with a detail message that contains the {@link URI} that has no binding. + * + * @param uri The URI that has no binding. + */ + public BindingNotFoundException(URI uri) { + super("No binding for URI '" + uri + "'."); + this.uri = uri; + } + + /** + * Returns the {@link URI} that has no binding. + * + * @return The URI. + */ + public URI uri() { + return uri; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BlockingContentWriter.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BlockingContentWriter.java new file mode 100644 index 00000000000..fc30ee11faf --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BlockingContentWriter.java @@ -0,0 +1,78 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutionException; +import java.util.Objects; + +/** + * <p>This class provides a blocking <em>write</em>-interface to a {@link ContentChannel}. Both {@link + * #write(ByteBuffer)} and {@link #close()} methods provide an internal {@link CompletionHandler} to the decorated + * {@link ContentChannel} calls, and wait for these to be called before returning. If {@link + * CompletionHandler#failed(Throwable)} is called, the corresponding Throwable is thrown to the caller.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @see FastContentWriter + */ +public final class BlockingContentWriter { + + private final ContentChannel channel; + + /** + * <p>Creates a new BlockingContentWriter that encapsulates a given {@link ContentChannel}.</p> + * + * @param content The ContentChannel to encapsulate. + * @throws NullPointerException If the <em>content</em> argument is null. + */ + public BlockingContentWriter(ContentChannel content) { + Objects.requireNonNull(content, "content"); + this.channel = content; + } + + /** + * <p>Writes to the underlying {@link ContentChannel} and waits for the operation to complete.</p> + * + * @param buf The ByteBuffer to write. + * @throws InterruptedException If the thread was interrupted while waiting. + * @throws RuntimeException If the operation failed to complete, see cause for details. + */ + public void write(ByteBuffer buf) throws InterruptedException { + try { + FutureCompletion future = new FutureCompletion(); + channel.write(buf, future); + future.get(); + } catch (ExecutionException e) { + Throwable t = e.getCause(); + if (t instanceof RuntimeException) { + throw (RuntimeException)t; + } + if (t instanceof Error) { + throw (Error)t; + } + throw new RuntimeException(t); + } + } + + /** + * <p>Closes the underlying {@link ContentChannel} and waits for the operation to complete.</p> + * + * @throws InterruptedException If the thread was interrupted while waiting. + * @throws RuntimeException If the operation failed to complete, see cause for details. + */ + public void close() throws InterruptedException { + try { + FutureCompletion future = new FutureCompletion(); + channel.close(future); + future.get(); + } catch (ExecutionException e) { + Throwable t = e.getCause(); + if (t instanceof RuntimeException) { + throw (RuntimeException)t; + } + if (t instanceof Error) { + throw (Error)t; + } + throw new RuntimeException(t); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BufferedContentChannel.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BufferedContentChannel.java new file mode 100644 index 00000000000..79bd340df55 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BufferedContentChannel.java @@ -0,0 +1,156 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +/** + * <p>This class implements an unlimited, non-blocking content queue. All {@link ContentChannel} methods are implemented + * by pushing to a thread-safe internal queue. All of the queued calls are forwarded to another ContentChannel when + * {@link #connectTo(ContentChannel)} is called. Once connected, this class becomes a non-buffering proxy for the + * connected ContentChannel.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class BufferedContentChannel implements ContentChannel { + + private final Object lock = new Object(); + private List<Entry> queue = new LinkedList<>(); + private ContentChannel content = null; + private boolean closed = false; + private CompletionHandler closeCompletion = null; + + /** + * <p>Connects this BufferedContentChannel to a ContentChannel. First, this method forwards all queued calls to the + * connected ContentChannel. Once this method has been called, all future calls to {@link #write(ByteBuffer, + * CompletionHandler)} and {@link #close(CompletionHandler)} are synchronously forwarded to the connected + * ContentChannel.</p> + * + * @param content The ContentChannel to connect to. + * @throws NullPointerException If the <em>content</em> argument is null. + * @throws IllegalStateException If another ContentChannel has already been connected. + */ + public void connectTo(ContentChannel content) { + Objects.requireNonNull(content, "content"); + boolean closed; + List<Entry> queue; + synchronized (lock) { + if (this.content != null || this.queue == null) { + throw new IllegalStateException(); + } + closed = this.closed; + queue = this.queue; + this.queue = null; + } + for (Entry entry : queue) { + content.write(entry.buf, entry.handler); + } + if (closed) { + content.close(closeCompletion); + } + synchronized (lock) { + this.content = content; + lock.notifyAll(); + } + } + + /** + * <p>Returns whether or not {@link #connectTo(ContentChannel)} has been called. Even if this method returns false, + * calling {@link #connectTo(ContentChannel)} might still throw an IllegalStateException if there is a race.</p> + * + * @return True if {@link #connectTo(ContentChannel)} has been called. + */ + public boolean isConnected() { + synchronized (lock) { + return content != null; + } + } + + /** + * <p>Creates a {@link ReadableContentChannel} and {@link #connectTo(ContentChannel) connects} to it. </p> + * + * @return The new ReadableContentChannel that this connected to. + */ + public ReadableContentChannel toReadable() { + ReadableContentChannel ret = new ReadableContentChannel(); + connectTo(ret); + return ret; + } + + /** + * <p>Creates a {@link ContentInputStream} and {@link #connectTo(ContentChannel) connects} to its internal + * ContentChannel.</p> + * + * @return The new ContentInputStream that this connected to. + */ + public ContentInputStream toStream() { + return toReadable().toStream(); + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + ContentChannel content; + synchronized (lock) { + if (closed) { + throw new IllegalStateException(); + } + if (queue != null) { + queue.add(new Entry(buf, handler)); + return; + } + try { + while (this.content == null) { + lock.wait(); // waiting for connecTo() + } + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + if (closed) { + throw new IllegalStateException(); + } + content = this.content; + } + content.write(buf, handler); + } + + @Override + public void close(CompletionHandler handler) { + ContentChannel content; + synchronized (lock) { + if (closed) { + throw new IllegalStateException(); + } + if (queue != null) { + closed = true; + closeCompletion = handler; + return; + } + try { + while (this.content == null) { + lock.wait(); // waiting for connecTo() + } + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + if (closed) { + throw new IllegalStateException(); + } + closed = true; + content = this.content; + } + content.close(handler); + } + + private static class Entry { + + final ByteBuffer buf; + final CompletionHandler handler; + + Entry(ByteBuffer buf, CompletionHandler handler) { + this.handler = handler; + this.buf = buf; + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableRequestDispatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableRequestDispatch.java new file mode 100644 index 00000000000..06421b2bfe2 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableRequestDispatch.java @@ -0,0 +1,22 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Response; + +import java.util.concurrent.Callable; + +/** + * <p>This is a convenient subclass of {@link RequestDispatch} that implements the {@link Callable} interface. This + * should be used in place of {@link RequestDispatch} if you intend to schedule its execution. Because {@link #call()} + * does not return until a {@link Response} becomes available, you can use the <tt>Future</tt> return value of + * <tt>ExecutorService.submit(Callable)</tt> to wait for it.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public abstract class CallableRequestDispatch extends RequestDispatch implements Callable<Response> { + + @Override + public final Response call() throws Exception { + return dispatch().get(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableResponseDispatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableResponseDispatch.java new file mode 100644 index 00000000000..9a22ec1c0e4 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableResponseDispatch.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Response; + +import java.util.concurrent.Callable; + +/** + * <p>This is a convenient subclass of {@link ResponseDispatch} that implements the {@link Callable} interface. This + * should be used in place of {@link ResponseDispatch} if you intend to schedule its execution. Because {@link #call()} + * does not return until the entirety of the {@link Response} and its content have been consumed, you can use the + * <tt>Future</tt> return value of <tt>ExecutorService.submit(Callable)</tt> to wait for it to complete.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public abstract class CallableResponseDispatch extends ResponseDispatch implements Callable<Boolean> { + + private final ResponseHandler handler; + + /** + * <p>Constructs a new instances of this class over the given {@link ResponseHandler}. Invoking {@link #call()} will + * dispatch to this handler.</p> + * + * @param handler The ResponseHandler to dispatch to. + */ + public CallableResponseDispatch(ResponseHandler handler) { + this.handler = handler; + } + + @Override + public final Boolean call() throws Exception { + return dispatch(handler).get(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CompletionHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CompletionHandler.java new file mode 100644 index 00000000000..ca2e61fff52 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CompletionHandler.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.jdisc.handler; + +import com.yahoo.jdisc.Container; + +/** + * This interface defines a handler for consuming the result of an asynchronous I/O operation. + * <p> + * The asynchronous channels defined in this package allow a completion handler to be specified to consume the result of + * an asynchronous operation. The {@link #completed()} method is invoked when the I/O operation completes successfully. + * The {@link #failed(Throwable)} method is invoked if the I/O operations fails. The implementations of these methods + * should complete in a timely manner so as to avoid keeping the invoking thread from dispatching to other completion + * handlers. + * <p> + * Because a CompletionHandler might have a completely different lifespan than the originating ContentChannel objects, + * all instances of this interface are internally backed by a reference to the {@link Container} that was active when + * the initial Request was created. This ensures that the configured environment of the CompletionHandler is stable + * throughout its lifetime. This also means that the either {@link #completed()} or {@link #failed(Throwable)} MUST be + * called in order to release that reference. Failure to do so will prevent the Container from ever shutting down. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface CompletionHandler { + + /** + * Invoked when an operation has completed. Notice that you MUST call either this or {@link #failed(Throwable)} to + * release the internal {@link Container} reference. Failure to do so will prevent the Container from ever shutting + * down. + */ + public void completed(); + + /** + * Invoked when an operation fails. Notice that you MUST call either this or {@link #completed()} to release the + * internal {@link Container} reference. Failure to do so will prevent the Container from ever shutting down. + * + * @param t The exception to indicate why the I/O operation failed. + */ + public void failed(Throwable t); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentChannel.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentChannel.java new file mode 100644 index 00000000000..7a4a6e46fe7 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentChannel.java @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; + +import java.nio.ByteBuffer; + +/** + * This interface defines a callback for asynchronously writing the content of a {@link Request} or a {@link Response} + * to a recipient. It is the returned both by {@link RequestHandler#handleRequest(Request, ResponseHandler)} and {@link + * ResponseHandler#handleResponse(Response)}. Note that methods of this channel only <em>schedule</em> the appropriate + * action - if you need to act on the result you will need submit a {@link CompletionHandler} to the appropriate method. + * <p> + * Because a ContentChannel might have a different lifespan than the originating Request and Response + * objects, all instances of this interface are internally backed by a reference to the {@link Container} that was + * active when the initial Request was created. This ensures that the configured environment of the ContentChannel is + * stable throughout its lifetime. This also means that the {@link #close(CompletionHandler)} method MUST be called in + * order to release that reference. Failure to do so will prevent the Container from ever shutting down. This + * requirement is regardless of any errors that may occur while calling any of its other methods or its derived {@link + * CompletionHandler}s. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ContentChannel { + + /** + * Schedules the given {@link ByteBuffer} to be written to the content corresponding to this ContentChannel. This + * call <em>transfers ownership</em> of the given ByteBuffer to this ContentChannel, i.e. no further calls can be + * made to the buffer. The execution of writes happen in the same order as this method was invoked. + * + * @param buf The {@link ByteBuffer} to schedule for write. No further calls can be made to this buffer. + * @param handler The {@link CompletionHandler} to call after the write has been executed. + */ + public void write(ByteBuffer buf, CompletionHandler handler); + + /** + * Closes this ContentChannel. After a channel is closed, any further attempt to invoke {@link #write(ByteBuffer, + * CompletionHandler)} upon it will cause an {@link IllegalStateException} to be thrown. If this channel is already + * closed then invoking this method has no effect, but {@link CompletionHandler#completed()} will still be called. + * + * Notice that you MUST call this method, regardless of any exceptions that might have occurred while writing to this + * ContentChannel. Failure to do so will prevent the {@link Container} from ever shutting down. + * + * @param handler The {@link CompletionHandler} to call after the close has been executed. + */ + public void close(CompletionHandler handler); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentInputStream.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentInputStream.java new file mode 100644 index 00000000000..d59bb893a2f --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentInputStream.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.jdisc.handler; + +/** + * <p>This class extends {@link UnsafeContentInputStream} and adds a finalizer to it that calls {@link #close()}. This + * has a performance impact, but ensures that an unclosed stream does not prevent shutdown.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class ContentInputStream extends UnsafeContentInputStream { + + /** + * <p>Constructs a new ContentInputStream that reads from the given {@link ReadableContentChannel}.</p> + * + * @param content The content to read the stream from. + */ + public ContentInputStream(ReadableContentChannel content) { + super(content); + } + + @Override + public void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentOutputStream.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentOutputStream.java new file mode 100644 index 00000000000..eed3210f57e --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentOutputStream.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.jdisc.handler; + +import com.google.common.util.concurrent.ListenableFuture; + +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * <p>This class extends the {@link AbstractContentOutputStream}, and forwards all write() and close() calls to a {@link + * FastContentWriter}. This means that once {@link #close()} has been called, the asynchronous completion of all pending + * operations can be awaited using the ListenableFuture interface of this class. Any asynchronous failure will be + * rethrown when calling either of the get() methods on this class.</p> + * <p>Please notice that the Future implementation of this class will NEVER complete unless {@link #close()} has been + * called.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class FastContentOutputStream extends AbstractContentOutputStream implements ListenableFuture<Boolean> { + + private final FastContentWriter out; + + /** + * <p>Constructs a new FastContentOutputStream that writes into the given {@link ContentChannel}.</p> + * + * @param out The ContentChannel to write the stream into. + */ + public FastContentOutputStream(ContentChannel out) { + this(new FastContentWriter(out)); + } + + /** + * <p>Constructs a new FastContentOutputStream that writes into the given {@link FastContentWriter}.</p> + * + * @param out The ContentWriter to write the stream into. + */ + public FastContentOutputStream(FastContentWriter out) { + Objects.requireNonNull(out, "out"); + this.out = out; + } + + @Override + protected void doFlush(ByteBuffer buf) { + out.write(buf); + } + + @Override + protected void doClose() { + out.close(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return out.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return out.isCancelled(); + } + + @Override + public boolean isDone() { + return out.isDone(); + } + + @Override + public Boolean get() throws InterruptedException, ExecutionException { + return out.get(); + } + + @Override + public Boolean get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return out.get(timeout, unit); + } + + @Override + public void addListener(Runnable listener, Executor executor) { + out.addListener(listener, executor); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentWriter.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentWriter.java new file mode 100644 index 00000000000..5c6e8334891 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentWriter.java @@ -0,0 +1,156 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * <p>This class provides a non-blocking, awaitable <em>write</em>-interface to a {@link ContentChannel}. + * The ListenableFuture<Boolean> interface can be used to await + * the asynchronous completion of all pending operations. Any asynchronous + * failure will be rethrown when calling either of the get() methods on + * this class.</p> + * <p>Please notice that the Future implementation of this class will NEVER complete unless {@link #close()} has been + * called; please use try-with-resources to ensure that close() is called.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class FastContentWriter implements ListenableFuture<Boolean>, AutoCloseable { + + private final AtomicBoolean closed = new AtomicBoolean(false); + private final AtomicInteger numPendingCompletions = new AtomicInteger(); + private final CompletionHandler completionHandler = new SimpleCompletionHandler(); + private final ContentChannel out; + private final SettableFuture<Boolean> future = SettableFuture.create(); + + /** + * <p>Creates a new FastContentWriter that encapsulates a given {@link ContentChannel}.</p> + * + * @param out The ContentChannel to encapsulate. + * @throws NullPointerException If the <em>content</em> argument is null. + */ + public FastContentWriter(ContentChannel out) { + Objects.requireNonNull(out, "out"); + this.out = out; + } + + /** + * <p>This is a convenience method to convert the given string to a ByteBuffer of UTF8 bytes, and then passing that + * to {@link #write(ByteBuffer)}.</p> + * + * @param str The string to write. + */ + public void write(String str) { + write(str.getBytes(StandardCharsets.UTF_8)); + } + + /** + * <p>This is a convenience method to convert the given byte array into a ByteBuffer object, and then passing that + * to {@link #write(java.nio.ByteBuffer)}.</p> + * + * @param buf The bytes to write. + */ + public void write(byte[] buf) { + write(buf, 0, buf.length); + } + + /** + * <p>This is a convenience method to convert a subarray of the given byte array into a ByteBuffer object, and then + * passing that to {@link #write(java.nio.ByteBuffer)}.</p> + * + * @param buf The bytes to write. + * @param offset The offset of the subarray to be used. + * @param length The length of the subarray to be used. + */ + public void write(byte[] buf, int offset, int length) { + write(ByteBuffer.wrap(buf, offset, length)); + } + + /** + * <p>Writes to the underlying {@link ContentChannel}. If {@link CompletionHandler#failed(Throwable)} is called, + * either of the get() methods will rethrow that Throwable.</p> + * + * @param buf The ByteBuffer to write. + */ + public void write(ByteBuffer buf) { + numPendingCompletions.incrementAndGet(); + try { + out.write(buf, completionHandler); + } catch (Throwable t) { + future.setException(t); + throw t; + } + } + + /** + * <p>Closes the underlying {@link ContentChannel}. If {@link CompletionHandler#failed(Throwable)} is called, + * either of the get() methods will rethrow that Throwable.</p> + */ + @Override + public void close() { + numPendingCompletions.incrementAndGet(); + closed.set(true); + try { + out.close(completionHandler); + } catch (Throwable t) { + future.setException(t); + throw t; + } + } + + @Override + public void addListener(Runnable listener, Executor executor) { + future.addListener(listener, executor); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public Boolean get() throws InterruptedException, ExecutionException { + return future.get(); + } + + @Override + public Boolean get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return future.get(timeout, unit); + } + + private class SimpleCompletionHandler implements CompletionHandler { + + @Override + public void completed() { + numPendingCompletions.decrementAndGet(); + if (closed.get() && numPendingCompletions.get() == 0) { + future.set(true); + } + } + + @Override + public void failed(Throwable t) { + future.setException(t); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureCompletion.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureCompletion.java new file mode 100644 index 00000000000..ed26678c7ac --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureCompletion.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.AbstractFuture; + +/** + * <p>This class provides an implementation of {@link CompletionHandler} that allows you to wait for either {@link + * #completed()} or {@link #failed(Throwable)} to be called. If failed() was called, the corresponding Throwable will + * be rethrown when calling either of the get() methods. Unless an exception is thrown, the get() methods will always + * return Boolean.TRUE.</p> + * + * <p>Notice that calling {@link #cancel(boolean)} throws an UnsupportedOperationException.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class FutureCompletion extends AbstractFuture<Boolean> implements CompletionHandler { + + @Override + public void completed() { + set(true); + } + + @Override + public void failed(Throwable t) { + setException(t); + } + + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean isCancelled() { + return false; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureConjunction.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureConjunction.java new file mode 100644 index 00000000000..eebb0ea266b --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureConjunction.java @@ -0,0 +1,97 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.JdkFutureAdapters; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.*; + +/** + * <p>This class implements a Future<Boolean> that is conjunction of zero or more other Future<Boolean>s, + * i.e. it evaluates to <tt>true</tt> if, and only if, all its operands evaluate to <tt>true</tt>. To use this class, + * simply create an instance of it and add operands to it using the {@link #addOperand(ListenableFuture)} method.</p> + * TODO: consider rewriting usage of FutureConjunction to use CompletableFuture instead. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class FutureConjunction implements ListenableFuture<Boolean> { + + private final List<ListenableFuture<Boolean>> operands = new LinkedList<>(); + + /** + * <p>Adds a ListenableFuture<Boolean> to this conjunction. This can be called at any time, even after having called + * {@link #get()} previously.</p> + * + * @param operand The operand to add to this conjunction. + */ + public void addOperand(ListenableFuture<Boolean> operand) { + operands.add(operand); + } + + @Override + public void addListener(Runnable listener, Executor executor) { + Futures.allAsList(operands).addListener(listener, executor); + } + + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + boolean ret = true; + for (Future<Boolean> op : operands) { + if (!op.cancel(mayInterruptIfRunning)) { + ret = false; + } + } + return ret; + } + + @Override + public final boolean isCancelled() { + for (Future<Boolean> op : operands) { + if (!op.isCancelled()) { + return false; + } + } + return true; + } + + @Override + public final boolean isDone() { + for (Future<Boolean> op : operands) { + if (!op.isDone()) { + return false; + } + } + return true; + } + + @Override + public final Boolean get() throws InterruptedException, ExecutionException { + Boolean ret = Boolean.TRUE; + for (Future<Boolean> op : operands) { + if (!op.get()) { + ret = Boolean.FALSE; + } + } + return ret; + } + + @Override + public final Boolean get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, + TimeoutException { + Boolean ret = Boolean.TRUE; + long nanos = unit.toNanos(timeout); + long lastTime = System.nanoTime(); + for (Future<Boolean> op : operands) { + if (!op.get(nanos, TimeUnit.NANOSECONDS)) { + ret = Boolean.FALSE; + } + long now = System.nanoTime(); + nanos -= now - lastTime; + lastTime = now; + } + return ret; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureResponse.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureResponse.java new file mode 100644 index 00000000000..ce772ff0340 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureResponse.java @@ -0,0 +1,66 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.AbstractFuture; +import com.yahoo.jdisc.Response; + +/** + * <p>This class provides an implementation of {@link ResponseHandler} that allows you to wait for a {@link Response} to + * be returned.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class FutureResponse extends AbstractFuture<Response> implements ResponseHandler { + + private final ResponseHandler handler; + + /** + * <p>Constructs a new FutureResponse that returns a {@link NullContent} when {@link #handleResponse(Response)} is + * invoked.</p> + */ + public FutureResponse() { + this(NullContent.INSTANCE); + } + + /** + * <p>Constructs a new FutureResponse that returns the given {@link ContentChannel} when {@link + * #handleResponse(Response)} is invoked.</p> + * + * @param content The content channel for the Response. + */ + public FutureResponse(final ContentChannel content) { + this(new ResponseHandler() { + + @Override + public ContentChannel handleResponse(Response response) { + return content; + } + }); + } + + /** + * <p>Constructs a new FutureResponse that calls the given {@link ResponseHandler} when {@link + * #handleResponse(Response)} is invoked.</p> + * + * @param handler The ResponseHandler to invoke. + */ + public FutureResponse(ResponseHandler handler) { + this.handler = handler; + } + + @Override + public ContentChannel handleResponse(Response response) { + set(response); + return handler.handleResponse(response); + } + + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean isCancelled() { + return false; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/NullContent.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/NullContent.java new file mode 100644 index 00000000000..e231674ad30 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/NullContent.java @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Request; + +import java.nio.ByteBuffer; + +/** + * <p>This class provides a convenient implementation of {@link ContentChannel} that does not support being written to. + * If {@link #write(ByteBuffer, CompletionHandler)} is called, it throws an UnsupportedOperationException. If {@link + * #close(CompletionHandler)} is called, it calls the given {@link CompletionHandler}.</p> + * + * <p>A {@link RequestHandler}s that does not expect content can simply return the {@link #INSTANCE} of this class for + * every invocation of its {@link RequestHandler#handleRequest(Request, ResponseHandler)}.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class NullContent implements ContentChannel { + + public static final NullContent INSTANCE = new NullContent(); + + private NullContent() { + // hide + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + if (buf.hasRemaining()) { + throw new UnsupportedOperationException(); + } + if (handler != null) { + handler.completed(); + } + } + + @Override + public void close(CompletionHandler handler) { + if (handler != null) { + handler.completed(); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/OverloadException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/OverloadException.java new file mode 100644 index 00000000000..22bd5cc14c7 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/OverloadException.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +/** + * An exception to signal abort current action, as the container is overloaded. + * Just unroll state as cheaply as possible. + * + * <p> + * The contract of OverloadException (for Jetty) is: + * </p> + * + * <ol> + * <li>You must set the response yourself first, or you'll get 500 internal + * server error.</li> + * <li>You must throw it from handleRequest synchronously.</li> + * </ol> + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public class OverloadException extends RuntimeException { + public OverloadException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ReadableContentChannel.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ReadableContentChannel.java new file mode 100644 index 00000000000..c887f4bfbab --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ReadableContentChannel.java @@ -0,0 +1,181 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Queue; + +/** + * <p>This class implements a {@link ContentChannel} that has a blocking <em>read</em> interface. Use this class if you + * intend to consume the content of the ContentChannel yourself. If you intend to forward the content to another + * ContentChannel, use {@link BufferedContentChannel} instead. If you <em>might</em> want to consume the content, return + * a {@link BufferedContentChannel} up front, and {@link BufferedContentChannel#connectTo(ContentChannel) connect} that + * to a ReadableContentChannel at the point where you decide to consume the data.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class ReadableContentChannel implements ContentChannel, Iterable<ByteBuffer> { + + private final Object lock = new Object(); + private Queue<Entry> queue = new LinkedList<>(); + private boolean closed = false; + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + Objects.requireNonNull(buf, "buf"); + synchronized (lock) { + if (closed || queue == null) { + throw new IllegalStateException(this + " is closed"); + } + queue.add(new Entry(buf, handler)); + lock.notifyAll(); + } + } + + @Override + public void close(CompletionHandler handler) { + synchronized (lock) { + if (closed || queue == null) { + throw new IllegalStateException(this + " is already closed"); + } + closed = true; + queue.add(new Entry(null, handler)); + lock.notifyAll(); + } + } + + @Override + public Iterator<ByteBuffer> iterator() { + return new MyIterator(); + } + + /** + * <p>Returns a lower-bound estimate on the number of bytes available to be {@link #read()} without blocking. If + * the returned number is larger than zero, the next call to {@link #read()} is guaranteed to not block.</p> + * + * @return The number of bytes available to be read without blocking. + */ + public int available() { + Entry entry; + synchronized (lock) { + if (queue == null) { + return 0; + } + entry = queue.peek(); + } + if (entry == null || entry.buf == null) { + return 0; + } + return entry.buf.remaining(); + } + + /** + * <p>Returns the next ByteBuffer in the internal queue. Before returning, this method calls {@link + * CompletionHandler#completed()} on the {@link CompletionHandler} that was submitted along with the ByteBuffer. If + * there are no ByteBuffers in the queue, this method waits indefinitely for either {@link + * #write(ByteBuffer, CompletionHandler)} or {@link #close(CompletionHandler)} to be called. Once closed and the + * internal queue drained, this method returns null.</p> + * + * @return The next ByteBuffer in queue, or null if this ReadableContentChannel is closed. + * @throws IllegalStateException If the current thread is interrupted while waiting. + */ + public ByteBuffer read() { + Entry entry; + synchronized (lock) { + try { + while (queue != null && queue.isEmpty()) { + lock.wait(); + } + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + if (queue == null) { + return null; + } + entry = queue.poll(); + if (entry.buf == null) { + queue = null; + } + } + if (entry.handler != null) { + entry.handler.completed(); + } + return entry.buf; + } + + /** + * <p>This method calls {@link CompletionHandler#failed(Throwable)} on all pending {@link CompletionHandler}s, and + * blocks all future operations to this ContentChannel (i.e. calls to {@link #write(ByteBuffer, CompletionHandler)} + * and {@link #close(CompletionHandler)} throw IllegalStateExceptions).</p> + * + * <p>This method will also notify any thread waiting in {@link #read()}.</p> + * + * @param t The Throwable to pass to all pending CompletionHandlers. + * @throws IllegalStateException If this method is called more than once. + */ + public void failed(Throwable t) { + Queue<Entry> queue; + synchronized (lock) { + if ((queue = this.queue) == null) { + throw new IllegalStateException(); + } + this.queue = null; + lock.notifyAll(); + } + for (Entry entry : queue) { + entry.handler.failed(t); + } + } + + /** + * <p>Creates a {@link ContentInputStream} that wraps this ReadableContentChannel.</p> + * + * @return The new ContentInputStream that wraps this. + */ + public ContentInputStream toStream() { + return new ContentInputStream(this); + } + + private class MyIterator implements Iterator<ByteBuffer> { + + ByteBuffer next; + + @Override + public boolean hasNext() { + if (next != null) { + return true; + } + next = read(); + return next != null; + } + + @Override + public ByteBuffer next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + ByteBuffer ret = next; + next = null; + return ret; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private static class Entry { + + final ByteBuffer buf; + final CompletionHandler handler; + + Entry(ByteBuffer buf, CompletionHandler handler) { + this.handler = handler; + this.buf = buf; + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDeniedException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDeniedException.java new file mode 100644 index 00000000000..b46751d5a3c --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDeniedException.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.jdisc.handler; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.service.ClientProvider; + +import java.net.URI; + +/** + * <p>This exception is used to signal that a {@link Request} was rejected by the corresponding {@link ClientProvider} + * or {@link RequestHandler}. There is no automation in throwing an instance of this class, but all RequestHandlers are + * encouraged to use this where appropriate.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class RequestDeniedException extends RuntimeException { + + private final Request request; + + /** + * <p>Constructs a new instance of this class with a detail message that contains the {@link URI} of the {@link + * Request} that was denied.</p> + * + * @param request The Request that was denied. + */ + public RequestDeniedException(Request request) { + super("Request with URI '" + request.getUri() + "' denied."); + this.request = request; + } + + /** + * <p>Returns the {@link Request} that was denied.</p> + * + * @return The Request that was denied. + */ + public Request request() { + return request; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDispatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDispatch.java new file mode 100644 index 00000000000..02c752ceae9 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDispatch.java @@ -0,0 +1,156 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.SharedResource; +import com.yahoo.jdisc.References; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.concurrent.*; + +/** + * <p>This class provides a convenient way of safely dispatching a {@link Request}. Using this class you do not have to + * worry about the exception safety surrounding the {@link SharedResource} logic. The internal mechanics of this class + * will ensure that anything that goes wrong during dispatch is safely handled according to jDISC contracts.</p> + * + * <p>It also provides a default implementation of the {@link ResponseHandler} interface that returns a {@link + * NullContent}. If you want to return a different {@link ContentChannel}, you need to override {@link + * #handleResponse(Response)}.</p> + * + * <p>The following is a simple example on how to use this class:</p> + * <pre> + * public void handleRequest(final Request parent, final ResponseHandler handler) { + * new RequestDispatch() { + * @Override + * protected Request newRequest() { + * return new Request(parent, URI.create("http://remotehost/")); + * } + * @Override + * protected Iterable<ByteBuffer> requestContent() { + * return Collections.singleton(ByteBuffer.wrap(new byte[] { 6, 9 })); + * } + * @Override + * public ContentChannel handleResponse(Response response) { + * return handler.handleResponse(response); + * } + * }.dispatch(); + * } + * </pre> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public abstract class RequestDispatch implements ListenableFuture<Response>, ResponseHandler { + + private final FutureConjunction completions = new FutureConjunction(); + private final FutureResponse futureResponse = new FutureResponse(this); + + /** + * <p>Creates and returns the {@link Request} to dispatch. The internal code that calls this method takes care of + * the necessary exception safety of connecting the Request.</p> + * + * @return The Request to dispatch. + */ + protected abstract Request newRequest(); + + /** + * <p>Returns an Iterable for the ByteBuffers that the {@link #dispatch()} method should write to the {@link + * Request} once it has {@link #connect() connected}. The default implementation returns an empty list. Because this + * method uses the Iterable interface, you can create the ByteBuffers lazily, or provide them as they become + * available.</p> + * + * @return The ByteBuffers to write to the Request's ContentChannel. + */ + protected Iterable<ByteBuffer> requestContent() { + return Collections.emptyList(); + } + + /** + * <p>This methods calls {@link #newRequest()} to create a new {@link Request}, and then calls {@link + * Request#connect(ResponseHandler)} on that. This method uses a <tt>finally</tt> block to make sure that the + * Request is always {@link Request#release() released}.</p> + * + * @return The ContentChannel to write the Request's content to. + */ + public final ContentChannel connect() { + final Request request = newRequest(); + try (final ResourceReference ref = References.fromResource(request)) { + return request.connect(futureResponse); + } + } + + /** + * <p>This is a convenient method to construct a {@link FastContentWriter} over the {@link ContentChannel} returned by + * calling {@link #connect()}.</p> + * + * @return The ContentWriter for the connected Request. + */ + public final FastContentWriter connectFastWriter() { + return new FastContentWriter(connect()); + } + + /** + * <p>This method calls {@link #connect()} to establish a {@link ContentChannel} for the {@link Request}, and then + * iterates through all the ByteBuffers returned by {@link #requestContent()} and writes them to that + * ContentChannel. This method uses a <tt>finally</tt> block to make sure that the ContentChannel is always {@link + * ContentChannel#close(CompletionHandler) closed}.</p> + * + * <p>The returned Future will wait for all CompletionHandlers associated with the Request have been completed, and + * a {@link Response} has been received.</p> + * + * @return A Future that can be waited for. + */ + public final ListenableFuture<Response> dispatch() { + try (FastContentWriter writer = new FastContentWriter(connect())) { + for (ByteBuffer buf : requestContent()) { + writer.write(buf); + } + completions.addOperand(writer); + } + return this; + } + + @Override + public void addListener(Runnable listener, Executor executor) { + Futures.allAsList(completions, futureResponse).addListener(listener, executor); + } + + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean isCancelled() { + return false; + } + + @Override + public final boolean isDone() { + return completions.isDone() && futureResponse.isDone(); + } + + @Override + public final Response get() throws InterruptedException, ExecutionException { + completions.get(); + return futureResponse.get(); + } + + @Override + public final Response get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, + TimeoutException + { + long now = System.nanoTime(); + completions.get(timeout, unit); + return futureResponse.get(unit.toNanos(timeout) - (System.nanoTime() - now), TimeUnit.NANOSECONDS); + } + + @Override + public ContentChannel handleResponse(Response response) { + return NullContent.INSTANCE; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestHandler.java new file mode 100644 index 00000000000..3fc3dbb8a82 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestHandler.java @@ -0,0 +1,62 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.SharedResource; +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.UriPattern; + +import java.util.concurrent.TimeUnit; + +/** + * <p>This interface defines a component that is capable of acting as a handler for a {@link Request}. To activate a + * RequestHandler it must be {@link BindingRepository#bind(String, Object) bound} to a {@link UriPattern} within a + * {@link ContainerBuilder}, and that builder must be {@link ContainerActivator#activateContainer(ContainerBuilder) + * activated}.</p> +* + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface RequestHandler extends SharedResource { + + /** + * <p>This method will process the given {@link Request} and return a {@link ContentChannel} into which the caller + * can write the Request's content. For every call to this method, the implementation must call the provided {@link + * ResponseHandler} exactly once.</p> + * + * <p>Notice that unless this method throws an Exception, a reference to the currently active {@link Container} + * instance is kept internally until {@link ResponseHandler#handleResponse(Response)} has been called. This ensures + * that the configured environment of the Request is stable throughout its lifetime. Failure to call back with a + * Response will prevent the release of that reference, and therefore prevent the corresponding Container from ever + * shutting down. The requirement to call {@link ResponseHandler#handleResponse(Response)} is regardless of any + * subsequent errors that may occur while working with the returned ContentChannel.</p> + * + * @param request The Request to handle. + * @param handler The handler to pass the corresponding {@link Response} to. + * @return The ContentChannel to write the Request content to. Notice that the ContentChannel itself also holds a + * Container reference, so failure to close this will prevent the Container from ever shutting down. + */ + public ContentChannel handleRequest(Request request, ResponseHandler handler); + + /** + * <p>This method is called by the {@link Container} when a {@link Request} that was previously accepted by {@link + * #handleRequest(Request, ResponseHandler)} has timed out. If the Request has no timeout (i.e. {@link + * Request#getTimeout(TimeUnit)} returns <em>null</em>), then this method is never called.</p> + * + * <p>The given {@link ResponseHandler} is the same ResponseHandler that was initially passed to the {@link + * #handleRequest(Request, ResponseHandler)} method, and it is guarded by a volatile boolean so that only the first + * call to {@link ResponseHandler#handleResponse(Response)} is actually passed on. This means that you do NOT need + * to manage the ResponseHandlers yourself to prevent a late Response from calling the same ResponseHandler.</p> + * + * <p>Notice that you MUST call {@link ResponseHandler#handleResponse(Response)} as a reaction to having this method + * invoked. Failure to do so will prevent the Container from ever shutting down.</p> + * + * @param request The Request that has timed out. + * @param handler The handler to pass the timeout {@link Response} to. + * @see Response#dispatchTimeout(ResponseHandler) + */ + public void handleTimeout(Request request, ResponseHandler handler); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseDispatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseDispatch.java new file mode 100644 index 00000000000..dfcda9ee85d --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseDispatch.java @@ -0,0 +1,179 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.ForwardingListenableFuture; +import com.google.common.util.concurrent.ListenableFuture; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.SharedResource; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.Future; + +/** + * <p>This class provides a convenient way of safely dispatching a {@link Response}. It is similar in use to {@link + * RequestDispatch}, where you need to subclass and implement and override the appropriate methods. Because a Response + * is not a {@link SharedResource}, its construction is less strenuous, and this class is able to provide a handful of + * convenient factory methods to dispatch the simplest of Responses.</p> + * <p>The following is a simple example on how to use this class without the factories:</p> + * <pre> + * public void signalInternalError(ResponseHandler handler) { + * new ResponseDispatch() { + * @Override + * protected Response newResponse() { + * return new Response(Response.Status.INTERNAL_SERVER_ERROR); + * } + * @Override + * protected Iterable<ByteBuffer> responseContent() { + * return Collections.singleton(ByteBuffer.wrap(new byte[] { 6, 9 })); + * } + * }.dispatch(handler); + * } + * </pre> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public abstract class ResponseDispatch extends ForwardingListenableFuture<Boolean> { + + private final FutureConjunction completions = new FutureConjunction(); + + /** + * <p>Creates and returns the {@link Response} to dispatch.</p> + * + * @return The Response to dispatch. + */ + protected abstract Response newResponse(); + + /** + * <p>Returns an Iterable for the ByteBuffers that the {@link #dispatch(ResponseHandler)} method should write to the + * {@link Response} once it has {@link ResponseHandler#handleResponse(Response) connected}. The default + * implementation returns an empty list. Because this method uses the Iterable interface, you can provide the + * ByteBuffers lazily, or as they become available.</p> + * + * @return The ByteBuffers to write to the Response's ContentChannel. + */ + protected Iterable<ByteBuffer> responseContent() { + return Collections.emptyList(); + } + + /** + * <p>This methods calls {@link #newResponse()} to create a new {@link Response}, and then calls {@link + * ResponseHandler#handleResponse(Response)} with that.</p> + * + * @param responseHandler The ResponseHandler to connect to. + * @return The ContentChannel to write the Response's content to. + */ + public final ContentChannel connect(ResponseHandler responseHandler) { + return responseHandler.handleResponse(newResponse()); + } + + /** + * <p>Convenience method for constructing a {@link FastContentWriter} over the {@link ContentChannel} returned by + * calling {@link #connect(ResponseHandler)}.</p> + * + * @param responseHandler The ResponseHandler to connect to. + * @return The FastContentWriter for the connected Response. + */ + public final FastContentWriter connectFastWriter(ResponseHandler responseHandler) { + return new FastContentWriter(connect(responseHandler)); + } + + /** + * <p>This method calls {@link #connect(ResponseHandler)} to establish a {@link ContentChannel} for the {@link + * Response}, and then iterates through all the ByteBuffers returned by {@link #responseContent()} and writes them + * to that ContentChannel. This method uses a <tt>finally</tt> block to make sure that the ContentChannel is always + * {@link ContentChannel#close(CompletionHandler) closed}.</p> + * <p>The returned Future will wait for all CompletionHandlers associated with the Response have been + * completed.</p> + * + * @param responseHandler The ResponseHandler to dispatch to. + * @return A Future that can be waited for. + */ + public final ListenableFuture<Boolean> dispatch(ResponseHandler responseHandler) { + try (FastContentWriter writer = new FastContentWriter(connect(responseHandler))) { + for (ByteBuffer buf : responseContent()) { + writer.write(buf); + } + completions.addOperand(writer); + } + return this; + } + + @Override + protected final ListenableFuture<Boolean> delegate() { + return completions; + } + + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean isCancelled() { + return false; + } + + /** + * <p>Factory method for creating a ResponseDispatch with a {@link Response} that has the given status code, and + * ByteBuffer content.</p> + * + * @param responseStatus The status code of the Response to dispatch. + * @param content The ByteBuffer content of the Response, may be empty. + * @return The created ResponseDispatch. + */ + public static ResponseDispatch newInstance(int responseStatus, ByteBuffer... content) { + return newInstance(new Response(responseStatus), Arrays.asList(content)); + } + + /** + * <p>Factory method for creating a ResponseDispatch with a {@link Response} that has the given status code, and + * collection of ByteBuffer content. + * Because this method uses the Iterable interface, you can create the ByteBuffers lazily, or + * provide them as they become available.</p> + * + * @param responseStatus The status code of the Response to dispatch. + * @param content The provider of the Response's ByteBuffer content. + * @return The created ResponseDispatch. + */ + public static ResponseDispatch newInstance(int responseStatus, Iterable<ByteBuffer> content) { + return newInstance(new Response(responseStatus), content); + } + + /** + * <p>Factory method for creating a ResponseDispatch over a given {@link Response} and ByteBuffer content.</p> + * + * @param response The Response to dispatch. + * @param content The ByteBuffer content of the Response, may be empty. + * @return The created ResponseDispatch. + */ + public static ResponseDispatch newInstance(Response response, ByteBuffer... content) { + return newInstance(response, Arrays.asList(content)); + } + + /** + * <p>Factory method for creating a ResponseDispatch over a given {@link Response} and ByteBuffer content. + * Because this method uses the Iterable interface, you can create the ByteBuffers lazily, or provide them as they + * become available.</p> + * + * @param response The Response to dispatch. + * @param content The provider of the Response's ByteBuffer content. + * @return The created ResponseDispatch. + */ + public static ResponseDispatch newInstance(final Response response, final Iterable<ByteBuffer> content) { + return new ResponseDispatch() { + + @Override + protected Response newResponse() { + return response; + } + + @Override + public Iterable<ByteBuffer> responseContent() { + return content; + } + }; + } + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java new file mode 100644 index 00000000000..5c6abf64013 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.service.ClientProvider; + +/** + * <p>This interface defines a component that is capable of acting as a handler for a {@link Response}. An + * implementation of this interface is required to be passed alongside every {@link Request} as part of the API (see + * {@link ClientProvider#handleRequest(Request, ResponseHandler)} and {@link RequestHandler#handleRequest(Request, + * ResponseHandler)}).</p> + * + * <p>The jDISC API has intentionally been designed as not to provide a implicit reference from Response to + * corresponding Request, but rather leave that to the implementation of context-aware ResponseHandlers. By creating + * light-weight ResponseHandlers on a per-Request basis, any necessary reference can be embedded within.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ResponseHandler { + + /** + * <p>This method will process the given {@link Response} and return a {@link ContentChannel} into which the caller + * can write the Response's content.</p> + * + * @param response The Response to handle. + * @return The ContentChannel to write the Response content to. Notice that the ContentChannel holds a Container + * reference, so failure to close this will prevent the Container from ever shutting down. + */ + ContentChannel handleResponse(Response response); + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ThreadedRequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ThreadedRequestHandler.java new file mode 100644 index 00000000000..64bcf91edbd --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ThreadedRequestHandler.java @@ -0,0 +1,156 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; + +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * <p>This class implements a {@link RequestHandler} with a synchronous {@link #handleRequest(Request, + * BufferedContentChannel, ResponseHandler)} API for handling {@link Request}s. An Executor is provided at construction + * time, and all Requests are automatically scheduled for processing on that Executor.</p> + * + * <p>A very simple echo handler could be implemented like this:</p> + * <pre> + * class MyRequestHandler extends ThreadedRequestHandler { + * + * @Inject + * MyRequestHandler(Executor executor) { + * super(executor); + * } + * + * @Override + * protected void handleRequest(Request request, ReadableContentChannel requestContent, ResponseHandler handler) { + * ContentWriter responseContent = ResponseDispatch.newInstance(Response.Status.OK).connectWriter(handler); + * try { + * for (ByteBuffer buf : requestContent) { + * responseContent.write(buf); + * } + * } catch (RuntimeException e) { + * requestContent.failed(e); + * throw e; + * } finally { + * responseContent.close(); + * } + * } + * } + * </pre> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class ThreadedRequestHandler extends AbstractRequestHandler { + + private final Executor executor; + private volatile long timeout = 0; + + protected ThreadedRequestHandler(Executor executor) { + Objects.requireNonNull(executor, "executor"); + this.executor = executor; + } + + /** + * <p>Sets the timeout that this ThreadedRequestHandler sets on all handled {@link Request}s. If the + * <em>timeout</em> value is less than or equal to zero, no timeout will be applied.</p> + * + * @param timeout The allocated amount of time. + * @param unit The time unit of the <em>timeout</em> argument. + */ + public final void setTimeout(long timeout, TimeUnit unit) { + this.timeout = unit.toMillis(timeout); + } + + /** + * <p>Returns the timeout that this ThreadedRequestHandler sets on all handled {@link Request}s.</p> + * + * @param unit The unit to use for the return value. + * @return The timeout in the appropriate unit. + */ + public final long getTimeout(TimeUnit unit) { + return unit.convert(timeout, TimeUnit.MILLISECONDS); + } + + @Override + public final ContentChannel handleRequest(Request request, ResponseHandler responseHandler) { + if (timeout > 0) { + request.setTimeout(timeout, TimeUnit.MILLISECONDS); + } + BufferedContentChannel content = new BufferedContentChannel(); + executor.execute(new RequestTask(request, content, responseHandler)); + return content; + } + + /** + * <p>Override this method if you want to access the {@link Request}'s content using a {@link + * BufferedContentChannel}. If you do not override this method, it will call {@link #handleRequest(Request, + * ReadableContentChannel, ResponseHandler)}.</p> + * + * @param request The Request to handle. + * @param responseHandler The handler to pass the corresponding {@link Response} to. + * @param requestContent The content of the Request. + */ + protected void handleRequest(Request request, BufferedContentChannel requestContent, + ResponseHandler responseHandler) + { + handleRequest(request, requestContent.toReadable(), responseHandler); + } + + /** + * <p>Implement this method if you want to access the {@link Request}'s content using a {@link + * ReadableContentChannel}. If you do not override this method, it will call {@link #handleRequest(Request, + * ContentInputStream, ResponseHandler)}.</p> + * + * @param request The Request to handle. + * @param responseHandler The handler to pass the corresponding {@link Response} to. + * @param requestContent The content of the Request. + */ + protected void handleRequest(Request request, ReadableContentChannel requestContent, + ResponseHandler responseHandler) + { + handleRequest(request, requestContent.toStream(), responseHandler); + } + + /** + * <p>Implement this method if you want to access the {@link Request}'s content using a {@link ContentInputStream}. + * If you do not override this method, it will dispatch a {@link Response} to the {@link ResponseHandler} with a + * <tt>Response.Status.NOT_IMPLEMENTED</tt> status.</p> + * + * @param request The Request to handle. + * @param responseHandler The handler to pass the corresponding {@link Response} to. + * @param requestContent The content of the Request. + */ + @SuppressWarnings("UnusedParameters") + protected void handleRequest(Request request, ContentInputStream requestContent, + ResponseHandler responseHandler) + { + while (requestContent.read() >= 0) { + // drain content stream + } + ResponseDispatch.newInstance(Response.Status.NOT_IMPLEMENTED).dispatch(responseHandler); + } + + private class RequestTask implements Runnable { + + final Request request; + final BufferedContentChannel content; + final ResponseHandler responseHandler; + private final ResourceReference requestReference; + + RequestTask(Request request, BufferedContentChannel content, ResponseHandler responseHandler) { + this.request = request; + this.content = content; + this.responseHandler = responseHandler; + this.requestReference = request.refer(); + } + + @Override + public void run() { + try (final ResourceReference ref = requestReference) { + ThreadedRequestHandler.this.handleRequest(request, content, responseHandler); + } + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/UnsafeContentInputStream.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/UnsafeContentInputStream.java new file mode 100644 index 00000000000..115b5383302 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/UnsafeContentInputStream.java @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * <p>This class provides an adapter from a {@link ReadableContentChannel} to an InputStream. This class supports all + * regular InputStream operations, and can be combined with any other InputStream API.</p> + * + * <p>Because this class encapsulates the reference-counted {@link ContentChannel} operations, one must be sure to + * always call {@link #close()} before discarding it. Failure to do so will prevent the Container from ever shutting + * down.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class UnsafeContentInputStream extends InputStream { + + private final ReadableContentChannel content; + private ByteBuffer buf = ByteBuffer.allocate(0); + + /** + * <p>Constructs a new ContentInputStream that reads from the given {@link ReadableContentChannel}.</p> + * + * @param content The content to read the stream from. + */ + public UnsafeContentInputStream(ReadableContentChannel content) { + this.content = content; + } + + @Override + public int read() { + while (buf != null && buf.remaining() == 0) { + buf = content.read(); + } + if (buf == null) { + return -1; + } + return ((int)buf.get()) & 0xFF; + } + + @Override + public int read(byte buf[], int off, int len) { + Objects.requireNonNull(buf, "buf"); + if (off < 0 || len < 0 || len > buf.length - off) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return 0; + } + int c = read(); + if (c == -1) { + return -1; + } + buf[off] = (byte)c; + int cnt = 1; + for (; cnt < len && available() > 0; ++cnt) { + if ((c = read()) == -1) { + break; + } + buf[off + cnt] = (byte)c; + } + return cnt; + } + + @Override + public int available() { + if (buf != null && buf.remaining() > 0) { + return buf.remaining(); + } + return content.available(); + } + + @Override + public void close() { + // noinspection StatementWithEmptyBody + while (content.read() != null) { + + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/package-info.java new file mode 100644 index 00000000000..8f44495222b --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/package-info.java @@ -0,0 +1,68 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * <p>Provides classes and interfaces for implementing a {@link com.yahoo.jdisc.handler.RequestHandler + * RequestHandler}.</p> + * + * <h3>RequestHandler</h3> + * <p>All {@link com.yahoo.jdisc.Request Requests} in a jDISC application are processed by RequestHandlers. These are + * components created by the {@link com.yahoo.jdisc.application.Application Application}, and bound to one or more URI + * patterns through the {@link com.yahoo.jdisc.application.ContainerBuilder ContainerBuilder} API. Upon receiving a + * Request, a RequestHandler must return a {@link com.yahoo.jdisc.handler.ContentChannel ContentChannel} into which the + * caller can asynchronously write the Request's payload. The ContentChannel is an asynchronous API for ByteBuffer + * hand-over, with support for asynchronous completion-notification (through the {@link + * com.yahoo.jdisc.handler.CompletionHandler CompletionHandler} interface). Once the Request has been processed (which + * may or may not involve dispatching one or more child-Requests), the RequestHandler must prepare a {@link + * com.yahoo.jdisc.Response Response} object and asynchronously pass that to the corresponding {@link + * com.yahoo.jdisc.handler.ResponseHandler ResponseHandler}. One of the most vital parts of the RequestHandler definition + * is that it must provide exactly one Response for every Request. This guarantee simplifies the usage pattern of + * RequestHandlers, and allows other components to skip a lot of bookkeeping. If a RequestHandler decides to create and + * dispatch a child-Request, it is done through the same {@link com.yahoo.jdisc.application.BindingSet BindingSet} + * mechanics that was used to resolve the current RequestHandler. Because all {@link + * com.yahoo.jdisc.service.ServerProvider ServerProviders} use "localhost" for Request URI hostname, most RequestHandlers + * are also bound to "localhost". Those that are not typically provide a specific service for one or more remote hosts + * (these are {@link com.yahoo.jdisc.service.ClientProvider ClientProviders}).</p> + * +<pre> +@Inject +MyApplication(ContainerActivator activator, CurrentContainer container) { + ContainerBuilder builder = activator.newContainerBuilder(); + builder.serverBindings().bind("http://localhost/*", new MyRequestHandler()); + activator.activateContainer(builder); +} +</pre> + * + * <p>Because the entirety of the RequestHandler stack (RequestHandler, ResponseHandler, ContentChannel and + * CompletionHandler) is asynchronous, an active {@link com.yahoo.jdisc.Container Container} can handle as many + * concurrent Requests as the sum capacity of all installed ServerProviders. Furthermore, the APIs have been designed in + * such a way that the ContentChannel returned back to the initial call to a RequestHandler can be the very same + * ContentChannel as is returned by the final destination of a Request. This means that, unless explicitly implemented + * otherwise, a jDISC application that is intended to forward large streams of data can do so without having to make any + * copies of that data as it is passing through.</p> + * + * <h3>ResponseHandler</h3> + * <p>The complement of the Request is the Response. A Response is a numeric status code and a set of header fields. + * Just as Requests are processed by RequestHandlers, Responses are processed by ResponseHandlers. The ResponseHandler + * interface is fully asynchronous, and uses the ContentChannel class to encapsulate the asynchronous passing of + * Response content. Where the RequestHandler is part of the Container and it's BindingSets, the ResponseHandler is part + * of the Request context. With every call to a RequestHandler you must also provide a ResponseHandler. Because the + * Request itself is not part of the ResponseHandler API, there is no built-in feature to tell a ResponseHandler which + * Request the Response corresponds to. Instead, one should create per-Request light-weight ResponseHandler objects that + * encapsulate the necessary context for Response processing. This was a deliberate design choice based on observed + * usage patterns of a different but similar architecture (the messaging layer of the Vespa platform).</p> + * + * <p>A Request may or may not have an assigned timeout. Both a ServerProvider and a RequestHandler may choose to assign + * a timeout to a Request, but only the first to assign it has an effect. The timeout is the maximum allowed time for a + * RequestHandler to wait before calling the ResponseHandler. There is no monitoring of the associated ContentChannels + * of either Request or Response, so once a Response has been dispatched a ContentChannel can stay open indefinetly. + * Timeouts are managed by a jDISC core component, but a RequestHandler may ask a Request at any time whether or not it + * has timed out. This allows RequestHandlers to terminate CPU-intensive processing of Requests whose Response will be + * discarded anyway. Once timeout occurs, the timeout manager calls the appropriate {@link + * com.yahoo.jdisc.handler.RequestHandler#handleTimeout(Request, ResponseHandler)} method. All future calls to that + * ResponseHandler is blocked, as to uphold the guarantee that a Request should have exactly one Response.</p> + * + * @see com.yahoo.jdisc + * @see com.yahoo.jdisc.application + * @see com.yahoo.jdisc.service + */ +@com.yahoo.api.annotations.PublicApi +package com.yahoo.jdisc.handler; diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/package-info.java new file mode 100644 index 00000000000..2ad31099e07 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/package-info.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * <p>Provides the common classes and interfaces of the jDISC core.</p> + * + * <p>jDISC is a single-process, multi-threaded application container that consists of exactly one {@link + * com.yahoo.jdisc.application.Application Application} with an optional {@link com.yahoo.jdisc.Metric Metric} + * configuration, one or more {@link com.yahoo.jdisc.handler.RequestHandler RequestHandlers}, one or more {@link + * com.yahoo.jdisc.service.ServerProvider ServerProviders}, and one or more named {@link + * com.yahoo.jdisc.application.BindingSet BindingSets}. When starting an Application, and whenever else the current + * configuration changes, it is the responsibility of the Application to create and activate a new {@link + * com.yahoo.jdisc.Container Container} that matches the most recent configuration. The Container itself is an immutable + * object, ensuring that the context of a {@link com.yahoo.jdisc.Request Request} never changes during its execution. + * When a new Container is activated, the previous is deactivated and scheduled for shutdown as soon as it finishes + * processing all previously accepted Requests. At any time, a jDISC process will therefore have zero (typically during + * application startup and shutdown) or one active Container, and zero or more deactivated Containers. The currently + * active Container is available to ServerProviders through an application-scoped singleton, making sure that no new + * Request is ever passed to a deactivated Container.</p> + * + * <p>A Request is created when either a) a ServerProvider accepts an incoming connection, or b) a RequestHandler + * creates a child Request of another. In the case of the ServerProvider, the {@link + * com.yahoo.jdisc.service.CurrentContainer CurrentContainer} interface provides a reference to the currently active + * Container, and the Application's {@link com.yahoo.jdisc.application.BindingSetSelector BindingSetSelector} (provided + * during configuration) selects a BindingSet based on the Request's URI. The BindingSet is what the Container uses to + * match a Request's URI to an appropriate RequestHandler. Together, the Container reference and the selected BindingSet + * make up the context of the Request. When a RequestHandler chooses to create a child Request, it reuses both the + * Container reference and the BindingSet of the original Request, ensuring that all processing of a single connection + * happens within the same Container instance. For every dispatched Request there is always exactly one {@link + * com.yahoo.jdisc.Response Response}. The Response is never routed, it simply follows the call stack of the + * corresponding Request.</p> + * + * <p>Because BindingSets decide on the RequestHandler which is to process a Request, using multiple BindingSets and a + * property-specific BindingSetSelector, one is able to create a Container capable of rewiring itself on a per-Request + * basis. This can be used for running production code in a mock-up environment for offline regression tests, and also + * for features such as Request bucketing (selecting a bucket BindingSet for n percent of the URIs) and rate-limiting + * (selecting a rejecting-type RequestHandler if the system is in some specific state).</p> + * + * <p>Finally, the Container provides a minimal Metric API that consists of a {@link com.yahoo.jdisc.Metric Metric} + * producer and a {@link com.yahoo.jdisc.application.MetricConsumer MetricConsumer}. Any component may choose to inject + * and use the Metric API, but all its calls are ignored unless the Application has chosen to inject a MetricConsumer + * provider during configuration. For efficiency reasons, the Container provides the {@link + * com.yahoo.jdisc.application.ContainerThread ContainerThread} which offers thread local access to the Metric API. This + * is a class that needs to be explicitly used in whatever Executor or ThreadFactory the Application chooses to inject + * into the Container.</p> + * + * <p>For unit testing purposes, the {@link com.yahoo.jdisc.test} package provides classes and interfaces to help setup + * and run a jDISC application in a test environment with as little effort as possible.</p> + * + * @see com.yahoo.jdisc.application + * @see com.yahoo.jdisc.handler + * @see com.yahoo.jdisc.service + * @see com.yahoo.jdisc.test + */ +@com.yahoo.api.annotations.PublicApi +package com.yahoo.jdisc; diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractClientProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractClientProvider.java new file mode 100644 index 00000000000..3f2ebc67aa5 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractClientProvider.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.jdisc.service; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; + +/** + * <p>This is a convenient parent class for {@link ClientProvider} with default implementations for all but the + * essential {@link #handleRequest(Request, ResponseHandler)} method.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class AbstractClientProvider extends AbstractRequestHandler implements ClientProvider { + + @Override + public void start() { + + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractServerProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractServerProvider.java new file mode 100644 index 00000000000..15363ded3e0 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractServerProvider.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.google.inject.Inject; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; + +import java.util.Objects; + +/** + * <p>This is a convenient parent class for {@link ServerProvider} with default implementations for all but the + * essential {@link #start()} and {@link #close()} methods. It requires that the {@link CurrentContainer} is injected in + * the constructor, since that interface is needed to dispatch {@link Request}s.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class AbstractServerProvider extends AbstractResource implements ServerProvider { + + private final CurrentContainer container; + + @Inject + protected AbstractServerProvider(CurrentContainer container) { + Objects.requireNonNull(container, "container"); + this.container = container; + } + + public final CurrentContainer container() { + return container; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/BindingSetNotFoundException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/BindingSetNotFoundException.java new file mode 100644 index 00000000000..b02fab2eba8 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/BindingSetNotFoundException.java @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.yahoo.jdisc.application.BindingSet; + +import java.net.URI; + +/** + * This exception is used to signal that a named {@link BindingSet} was not found. An instance of this class will be + * thrown by the {@link CurrentContainer#newReference(URI)} method when a BindingSet with the specified name does not + * exist. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class BindingSetNotFoundException extends RuntimeException { + + private final String bindingSet; + + /** + * Constructs a new instance of this class with a detail message that contains the name of the {@link BindingSet} + * that was not found. + * + * @param bindingSet The name of the {@link BindingSet} that was not found. + */ + public BindingSetNotFoundException(String bindingSet) { + super("No binding set named '" + bindingSet + "'."); + this.bindingSet = bindingSet; + } + + /** + * Returns the name of the {@link BindingSet} that was not found. + * + * @return The name of the BindingSet. + */ + public String bindingSet() { + return bindingSet; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/ClientProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ClientProvider.java new file mode 100644 index 00000000000..96583217721 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ClientProvider.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.application.*; +import com.yahoo.jdisc.handler.RequestHandler; + +/** + * <p>This interface defines a component that is capable of acting as a client to an external server. To activate a + * ClientProvider it must be {@link BindingRepository#bind(String, Object) bound} to a {@link UriPattern} within a + * {@link ContainerBuilder}, and that builder must be {@link ContainerActivator#activateContainer(ContainerBuilder) + * activated}.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ClientProvider extends RequestHandler { + + /** + * <p>This is a synchronous method to configure this ClientProvider. The {@link Container} does <em>not</em> call + * this method, instead it is a required step in the {@link Application} initialization code.</p> + */ + void start(); + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/ContainerNotReadyException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ContainerNotReadyException.java new file mode 100644 index 00000000000..fa9dfd3b6f6 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ContainerNotReadyException.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.application.ContainerBuilder; + +import java.net.URI; + +/** + * This exception is used to signal that no {@link Container} is ready to serve {@link Request}s. An instance of this + * class will be thrown by the {@link CurrentContainer#newReference(URI)} method if it is called before a Container has + * been activated, or after a <em>null</em> argument has been passed to {@link ContainerActivator#activateContainer(ContainerBuilder)}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class ContainerNotReadyException extends RuntimeException { + + /** + * Constructs a new instance of this class with a detail message. + */ + public ContainerNotReadyException() { + super("Container not ready."); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/CurrentContainer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/CurrentContainer.java new file mode 100644 index 00000000000..7e4625277be --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/CurrentContainer.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.BindingSetSelector; +import com.yahoo.jdisc.handler.RequestHandler; + +import java.net.URI; + +/** + * This interface declares a method to retrieve a reference to the current {@link Container}. Note that a {@link + * Container} which has <em>not</em> been {@link Container#release() closed} will actively keep it alive, preventing it + * from shutting down when expired. Failure to call close() will eventually lead to an {@link OutOfMemoryError}. A + * {@link ServerProvider} should have an instance of this class injected in its constructor, and simply use the {@link + * Request#Request(CurrentContainer, URI) appropriate Request constructor} to avoid having to worry about the keep-alive + * issue. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface CurrentContainer { + + /** + * Returns a reference to the currently active {@link Container}. Until {@link Container#release()} has been called, + * the Container can not shut down. + * + * @param uri The identifier used to match this Request to an appropriate {@link ClientProvider} or {@link + * RequestHandler}. The hostname must be "localhost" or a fully qualified domain name. + * @return A reference to the current Container. + * @throws NoBindingSetSelectedException If no {@link BindingSet} was selected by the {@link BindingSetSelector}. + * @throws BindingSetNotFoundException If the named BindingSet was not found. + * @throws ContainerNotReadyException If no active Container was found, this can only happen during initial + * setup. + */ + public Container newReference(URI uri); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/NoBindingSetSelectedException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/NoBindingSetSelectedException.java new file mode 100644 index 00000000000..382262e52cd --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/NoBindingSetSelectedException.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.jdisc.service; + +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.BindingSetSelector; + +import java.net.URI; + +/** + * This exception is used to signal that no {@link BindingSet} was selected for a given {@link URI}. An instance of this + * class will be thrown by the {@link CurrentContainer#newReference(URI)} method if {@link + * BindingSetSelector#select(URI)} returned <em>null</em>. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class NoBindingSetSelectedException extends RuntimeException { + + private final URI uri; + + /** + * Constructs a new instance of this class with a detail message that contains the {@link URI} for which there was + * no {@link BindingSet} selected. + * + * @param uri The URI for which there was no BindingSet selected. + */ + public NoBindingSetSelectedException(URI uri) { + super("No binding set selected for URI '" + uri + "'."); + this.uri = uri; + } + + /** + * Returns the {@link URI} for which there was no {@link BindingSet} selected. + * + * @return The URI. + */ + public URI uri() { + return uri; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java new file mode 100644 index 00000000000..ea895088492 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.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.jdisc.service; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.SharedResource; +import com.yahoo.jdisc.application.Application; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.ServerRepository; + +import java.net.URI; + +/** + * <p>This interface defines a component that is capable of acting as a server for an external client. To activate a + * ServerProvider it must be {@link ServerRepository#install(ServerProvider) installed} in a {@link ContainerBuilder}, + * and that builder must be {@link ContainerActivator#activateContainer(ContainerBuilder) activated}.</p> + * + * <p>If a ServerProvider is to expire due to {@link Application} reconfiguration, it is necessary to close() that + * ServerProvider before deactivating the owning {@link Container}. Typically:</p> + * + * <pre> + * myExpiredServers.close(); + * reconfiguredContainerBuilder.servers().install(myRetainedServers); + * containerActivator.activateContainer(reconfiguredContainerBuilder); + * </pre> + * + * <p>All implementations of this interface will need to have a {@link CurrentContainer} injected into its constructor + * so that it is able to create and dispatch new {@link Request}s.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ServerProvider extends SharedResource { + + /** + * <p>This is a synchronous method to configure this ServerProvider and bind the listen port (or equivalent). The + * {@link Container} does <em>not</em> call this method, instead it is a required step in the {@link Application} + * initialization code.</p> + */ + public void start(); + + /** + * <p>This is a synchronous method to close the listen port (or equivalent) of this ServerProvider and flush any + * input buffers that will cause calls to {@link CurrentContainer#newReference(URI)}. This method <em>must not</em> + * return until the implementation can guarantee that there will be no further calls to CurrentContainer. All + * previously dispatched {@link Request}s are processed as before.</p> + * + * <p>The {@link Container} does <em>not</em> call this method, instead it is a required step in the {@link + * Application} shutdown code.</p> + */ + public void close(); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/package-info.java new file mode 100644 index 00000000000..445ddd9c726 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/package-info.java @@ -0,0 +1,77 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * <p>Provides classes and interfaces for implementing a {@link com.yahoo.jdisc.service.ClientProvider ClientProvider} or + * a {@link com.yahoo.jdisc.service.ServerProvider ServerProvider}.</p> + * + * <h3>ServerProvider</h3> + * <p>All {@link com.yahoo.jdisc.Request Requests} that are processed in a jDISC application are created by + * ServerProviders. These are components created by the {@link com.yahoo.jdisc.application.Application Application}, and + * they are the parts of jDISC that accept incoming connections. The ServerProvider creates and dispatches Request + * instances to the {@link com.yahoo.jdisc.service.CurrentContainer CurrentContainer}. No Request is ever dispatched to a + * ServerProvider, so a ServerProvider is considered part of the Application and not part of a Container (as opposed to + * {@link com.yahoo.jdisc.handler.RequestHandler RequestHandlers} and ClientProviders). To create a Request the + * ServerProvider first composes a URI on the form <code><scheme>://localhost[:<port>]/<path></code> + * that matches the content of the accepted connection, and passes that URI to the CurrentContainer interface. This + * creates a com.yahoo.jdisc.core.ContainerSnapshot that holds a reference to the {@link + * com.yahoo.jdisc.Container Container} that is currently active, and resolves the appropriate {@link + * com.yahoo.jdisc.application.BindingSet BindingSet} for the given URI through the Application's {@link + * com.yahoo.jdisc.application.BindingSetSelector BindingSetSelector}. This snapshot becomes the context of the new + * Request to ensure that all further processing of that Request happens within the same Container instace. Finally, the + * appropriate RequestHandler is resolved by the selected BindingSet, and the Request is dispatched.</p> + * +<pre> +private final ServerProvider server; + +@Inject +MyApplication(CurrentContainer container) { + server = new MyServerProvider(container); + server.start(); +} +</pre> + * + * <h3>ClientProvider</h3> + * <p>A ClientProvider extends the RequestHandler interface, adding a method for initiating the startup of the provider. + * This is to allow an Application to develop a common ClientProvider install path. As opposed to RequestHandlers that + * are bound to URIs with the "localhost" hostname that the ServerProviders use when creating a Request, a + * ClientProvider is typically bound using a hostname wildcard (the '*' character). Because BindingSet considers a + * wildcard match to be weaker than a verbatim match, only Requests with URIs that are not bound to a local + * RequestHandler are passed to the ClientProvider.</p> + * +<pre> +private final ClientProvider client; + +@Inject +MyApplication(ContainerActivator activator, CurrentContainer container) { + client = new MyClientProvider(); + client.start(); + + ContainerBuilder builder = activator.newContainerBuilder(); + builder.serverBindings().bind("http://localhost/*", new MyRequestHandler()); + builder.clientBindings().bind("http://*/*", client); + activator.activateContainer(builder); +} +</pre> + * + * <p>Because the dispatch to a ClientProvider uses the same mechanics as the dispatch to an ordinary RequestHandler + * (i.e. the BindingSet), it is possible to create a test-mode BindingSet and a test-aware BindingSetSelector which + * dispatches to mock-up RequestHandlers instead of remote servers. The immediate benefit of this is that regression + * tests can be run on an Application otherwise configured for production traffic, allowing you to stress actual + * production code instead of targeted-only unit tests. This is how you would install a custom BindingSetSelector:</p> + * +<pre> +@Inject +MyApplication(ContainerActivator activator, CurrentContainer container) { + ContainerBuilder builder = activator.newContainerBuilder(); + builder.clientBindings().bind("http://bing.com/*", new BingClientProvider()); + builder.clientBindings("test").bind("http://bing.com/*", new BingMockupProvider()); + builder.guiceModules().install(new MyBindingSetSelector()); + activator.activateContainer(builder); +} +</pre> + * + * @see com.yahoo.jdisc + * @see com.yahoo.jdisc.application + * @see com.yahoo.jdisc.handler + */ +@com.yahoo.api.annotations.PublicApi +package com.yahoo.jdisc.service; diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingClientProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingClientProvider.java new file mode 100644 index 00000000000..5c384fd2ddf --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingClientProvider.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.jdisc.test; + +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.ClientProvider; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class NonWorkingClientProvider extends NoopSharedResource implements ClientProvider { + + @Override + public void start() { + + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public void handleTimeout(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingCompletionHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingCompletionHandler.java new file mode 100644 index 00000000000..406e8a0235f --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingCompletionHandler.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.jdisc.test; + +import com.yahoo.jdisc.handler.CompletionHandler; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class NonWorkingCompletionHandler implements CompletionHandler { + + @Override + public void completed() { + throw new UnsupportedOperationException(); + } + + @Override + public void failed(Throwable t) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingContentChannel.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingContentChannel.java new file mode 100644 index 00000000000..c8019eb513b --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingContentChannel.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; + +import java.nio.ByteBuffer; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class NonWorkingContentChannel implements ContentChannel { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public void close(CompletionHandler handler) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingOsgiFramework.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingOsgiFramework.java new file mode 100644 index 00000000000..1e7d46bda4d --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingOsgiFramework.java @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.application.OsgiFramework; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +import java.util.Collections; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingOsgiFramework implements OsgiFramework { + + @Override + public List<Bundle> installBundle(String bundleLocation) { + throw new UnsupportedOperationException(); + } + + @Override + public void startBundles(List<Bundle> bundles, boolean privileged) { + throw new UnsupportedOperationException(); + } + + @Override + public void refreshPackages() { + throw new UnsupportedOperationException(); + } + + @Override + public BundleContext bundleContext() { + return null; + } + + @Override + public List<Bundle> bundles() { + return Collections.emptyList(); + } + + @Override + public void start() { + + } + + @Override + public void stop() { + + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequest.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequest.java new file mode 100644 index 00000000000..1a285f3bf22 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequest.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.google.inject.Module; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.application.Application; + +import java.net.URI; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class NonWorkingRequest { + + private NonWorkingRequest() { + // hide + } + + /** + * <p>Factory method to create a {@link Request} without an associated {@link Container}. The design of jDISC does + * not allow this, so this method internally creates TestDriver, activates a Container, and creates a new Request + * from that Container. Before returning, this method {@link Request#release() closes} the Request, and calls {@link + * TestDriver#close()} on the TestDriver. This means that you MUST NOT attempt to access any Container features + * through the created Request. This factory is only for directed feature tests that require a non-null + * Request.</p> + * + * @param uri The URI string to assign to the Request. + * @param guiceModules The guice modules to inject into the {@link Application}. + * @return A non-working Request. + */ + public static Request newInstance(String uri, Module... guiceModules) { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(guiceModules); + driver.activateContainer(driver.newContainerBuilder()); + Request request = new Request(driver, URI.create(uri)); + request.release(); + driver.close(); + return request; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequestHandler.java new file mode 100644 index 00000000000..d95b62186b2 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequestHandler.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class NonWorkingRequestHandler extends NoopSharedResource implements RequestHandler { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public void handleTimeout(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingResponseHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingResponseHandler.java new file mode 100644 index 00000000000..4f82df1c3e7 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingResponseHandler.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(Response response) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingServerProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingServerProvider.java new file mode 100644 index 00000000000..79d57024359 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingServerProvider.java @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.service.ServerProvider; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class NonWorkingServerProvider extends NoopSharedResource implements ServerProvider { + + @Override + public void start() { + + } + + @Override + public void close() { + + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/ServerProviderConformanceTest.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/ServerProviderConformanceTest.java new file mode 100644 index 00000000000..ca52f3ab95b --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/ServerProviderConformanceTest.java @@ -0,0 +1,3143 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.google.common.annotations.Beta; +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.name.Names; +import com.google.inject.util.Modules; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingSetSelector; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.ServerProvider; + +import javax.annotation.CheckReturnValue; +import java.io.Closeable; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +@SuppressWarnings("UnusedDeclaration") +@Beta +public abstract class ServerProviderConformanceTest { + private static final int NUM_RUNS_EACH_TEST = 10; + + /** + * <p>This interface declares the adapter between the general conformance test and an actual <tt>ServerProvider</tt> + * implementation. Every test runs as follows:</p> + * <ol> + * <li>{@link #newConfigModule()} is called to bind server-specific configuration.</li> + * <li>{@link #getServerProviderClass()} is called, and guice is asked to construct an instance of that class.</li> + * <li>{@link #newClient(ServerProvider)} is called one or more times as required by the test case.</li> + * <li>{@link #executeRequest(Object, boolean)} is called one or more times per client, as required by the test case.</li> + * <li>{@link #validateResponse(Object)} is called once per call to {@link #executeRequest(Object, boolean)}.</li> + * </ol> + * + * @param <T> The <tt>ServerProvider</tt> under test. + * @param <U> An object that represents a remote client that can connect to the server. + * @param <V> An object that holds the response generated by the client when executing a request. + */ + public interface Adapter<T extends ServerProvider, U, V> { + + Module newConfigModule(); + + Class<T> getServerProviderClass(); + + U newClient(T server) throws Throwable; + + V executeRequest(U client, boolean withRequestContent) throws Throwable; + + Iterable<ByteBuffer> newResponseContent(); + + void validateResponse(V response) throws Throwable; + } + + /** + * <p>An instance of this exception is thrown within the conformance tests that imply that they will throw an + * exception. If your <tt>ServerProvider</tt> is capable of exposing such information, then this class is what you + * need to look for in the output.</p> + */ + public static class ConformanceException extends RuntimeException { + private final Event peekEvent; + + public ConformanceException() { + peekEvent = null; + } + + /** + * In some tests, we want to ensure that a thrown exception has been handled by the framework before + * we do something else. There is no official hook to receive notification that the framework has + * handled an exception, but we assume (actually know) that the message of the exception will be + * accessed to create an error message. The provided event will signal that the exception + * has been _looked at_ by the framework, which we treat as synonymous with "handled" (due to + * synchronization in the framework, it is). + */ + public ConformanceException(final Event peekEvent) { + this.peekEvent = peekEvent; + } + + @Override + public String getMessage() { + if (peekEvent != null) { + peekEvent.happened(); + } + return super.getMessage(); + } + } + + /* The following section declares and implements all test cases for the ServerProvider conformance test. When + * subclassing this test, you must implement these methods, annotate them as test methods and call runTest() + * from within each of them with an appropriate adapter instance. + * + * The test set up various scenarios with successes, failures and exceptions in different places and with + * different timing. There are many dimensions to test across, hence some really long method names. Some + * notes about the naming "scheme": + * - "testRequest<Something>" means the funky stuff happens in the handleRequest() method. + * - "testRequestContent<Something>" indicates that the funky stuff happens in the request content channel's code. + * - "testResponse<Something>" indicates that the funky stuff happens with the response content channel. + * - "Failure" means that failed() is called on some completion handler (the method name should indicate which). + * - "Nondeterministic" exception/failure means that it can occur before, during or after writing response content. + * The reason we include non-deterministic tests is that the deterministic ones involve synchronization, which + * may hide race conditions in the underlying processing. So we want some tests that "run free" as well. + * - "WithSync<Something>" means that anything NOT mentioned happens asynchronously, in a different thread. + * - "Before"/"After" is significant in some protocols; e.g. in http, status and headers are committed at one point. + * - "NoContent" refers to response content. + * There are quite likely possible scenarios that are not tested, but this is a good portion. + */ + + public abstract void testContainerNotReadyException() throws Throwable; + private <T extends ServerProvider, U, V> void testContainerNotReadyException( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.override(Modules.combine(config)).with(newActivateContainer(false)), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(Request request, ResponseHandler handler) { + throw new AssertionError(); + } + }); + } + + public abstract void testBindingSetNotFoundException() throws Throwable; + private <T extends ServerProvider, U, V> void testBindingSetNotFoundException( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.override(Modules.combine()).with(newBindingSetSelector("unknown")), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(Request request, ResponseHandler handler) { + throw new AssertionError(); + } + }); + } + + public abstract void testNoBindingSetSelectedException() throws Throwable; + private <T extends ServerProvider, U, V> void testNoBindingSetSelectedException( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.override(Modules.combine()).with(newBindingSetSelector(null)), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(Request request, ResponseHandler handler) { + throw new AssertionError(); + } + }); + } + + public abstract void testBindingNotFoundException() throws Throwable; + private <T extends ServerProvider, U, V> void testBindingNotFoundException( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.override(Modules.combine(config)).with(newServerBinding("not://found/")), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(Request request, ResponseHandler handler) { + throw new AssertionError(); + } + }); + } + + public abstract void testRequestHandlerWithSyncCloseResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestHandlerWithSyncCloseResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + writeResponse(out); + closeResponse(out); + return null; + } + }); + } + + public abstract void testRequestHandlerWithSyncWriteResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestHandlerWithSyncWriteResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + writeResponse(out); + closeResponseInOtherThread(out); + return null; + } + }); + } + + public abstract void testRequestHandlerWithSyncHandleResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestHandlerWithSyncHandleResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + return null; + } + }); + } + + public abstract void testRequestHandlerWithAsyncHandleResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestHandlerWithAsyncHandleResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondWithContentInOtherThread(handler); + return null; + } + }); + } + + public abstract void testRequestException() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestException( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(Request request, ResponseHandler handler) { + throw new ConformanceException(); + } + }); + } + + public abstract void testRequestExceptionWithSyncCloseResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestExceptionWithSyncCloseResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + writeResponse(out); + closeResponse(out); + throw new ConformanceException(); + } + }); + } + + public abstract void testRequestExceptionWithSyncWriteResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestExceptionWithSyncWriteResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + writeResponse(out); + closeResponseInOtherThread(out); + throw new ConformanceException(); + } + }); + } + + public abstract void testRequestNondeterministicExceptionWithSyncHandleResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestNondeterministicExceptionWithSyncHandleResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + throw new ConformanceException(); + } + }); + } + + public abstract void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event exceptionHandledByFramework = new Event(); + callInOtherThread(() -> { + exceptionHandledByFramework.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + throw new ConformanceException(exceptionHandledByFramework); + } + }); + } + + public abstract void testRequestExceptionAfterResponseWriteWithSyncHandleResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestExceptionAfterResponseWriteWithSyncHandleResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + responseWritten.await(); + throw new ConformanceException(); + } + }); + } + + public abstract void testRequestNondeterministicExceptionWithAsyncHandleResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestNondeterministicExceptionWithAsyncHandleResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + callInOtherThread(new Callable<Void>() { + + @Override + public Void call() throws Exception { + try { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + closeResponse(out); + } catch (Throwable ignored) { + + } + return null; + } + }); + throw new ConformanceException(); + } + }); + } + + public abstract void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final Event exceptionHandledByFramework = new Event(); + callInOtherThread(new Callable<Void>() { + + @Override + public Void call() throws Exception { + exceptionHandledByFramework.await(); + try { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + exceptionHandledByFramework.await(); + writeResponse(out); + closeResponse(out); + } catch (Throwable ignored) { + + } + return null; + } + }); + throw new ConformanceException(exceptionHandledByFramework); + } + }); + } + + public abstract void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + callInOtherThread(() -> { + try { + respondNoContent(handler); + } catch (Throwable ignored) { + + } + return null; + }); + responseClosed.await(); + throw new ConformanceException(); + } + }); + } + + public abstract void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + callInOtherThread(new Callable<Void>() { + + @Override + public Void call() throws Exception { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + writeResponse(out); + closeResponse(out); + return null; + } + }); + responseWritten.await(); + throw new ConformanceException(); + } + }); + } + + public abstract void testRequestContentWriteWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondWithContentInOtherThread(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondWithContentInOtherThread(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + completeInOtherThread(handler); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteWithNondeterministicSyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteWithNondeterministicSyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.failed(new ConformanceException()); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteWithSyncFailureBeforeResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteWithSyncFailureBeforeResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event failDone = new Event(); + callInOtherThread(() -> { + failDone.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + try { + handler.failed(new ConformanceException()); + } finally { + failDone.happened(); + } + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteWithSyncFailureAfterResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteWithSyncFailureAfterResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + responseWritten.await(); + handler.failed(new ConformanceException()); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteWithNondeterministicAsyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteWithNondeterministicAsyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + callInOtherThread(() -> { + handler.failed(new ConformanceException()); + return null; + }); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteWithAsyncFailureBeforeResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteWithAsyncFailureBeforeResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event failDone = new Event(); + callInOtherThread(() -> { + failDone.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + callInOtherThread(() -> { + try { + handler.failed(new ConformanceException()); + } finally { + failDone.happened(); + } + return null; + }); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteWithAsyncFailureAfterResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteWithAsyncFailureAfterResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + callInOtherThread(() -> { + responseWritten.await(); + handler.failed(new ConformanceException()); + return null; + }); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + callInOtherThread(() -> { + responseClosed.await(); + handler.failed(new ConformanceException()); + return null; + }); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteNondeterministicException() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteNondeterministicException( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionBeforeResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionBeforeResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event exceptionHandledByFramework = new Event(); + callInOtherThread(() -> { + exceptionHandledByFramework.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + throw new ConformanceException(exceptionHandledByFramework); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionAfterResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + responseWritten.await(); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionAfterResponseCloseNoContent() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseCloseNoContent( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + responseClosed.await(); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteNondeterministicExceptionWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteNondeterministicExceptionWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event exceptionHandledByFramework = new Event(); + callInOtherThread(() -> { + exceptionHandledByFramework.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + throw new ConformanceException(exceptionHandledByFramework); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + responseWritten.await(); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + responseClosed.await(); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + completeInOtherThread(handler, IllegalStateException.class); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event exceptionHandledByFramework = new Event(); + callInOtherThread(() -> { + exceptionHandledByFramework.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + completeInOtherThread(handler, IllegalStateException.class); + throw new ConformanceException(exceptionHandledByFramework); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + completeInOtherThread(handler, IllegalStateException.class); + responseWritten.await(); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + completeInOtherThread(handler, IllegalStateException.class); + responseClosed.await(); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionWithNondeterministicSyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithNondeterministicSyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.failed(new ConformanceException()); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event failDone = new Event(); + callInOtherThread(() -> { + failDone.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + try { + handler.failed(new ConformanceException()); + } finally { + failDone.happened(); + } + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + responseWritten.await(); + handler.failed(new ConformanceException()); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + responseClosed.await(); + handler.failed(new ConformanceException()); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionWithNondeterministicAsyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithNondeterministicAsyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + callInOtherThread(() -> { + fail(handler, new ConformanceException(), IllegalStateException.class); + return null; + }); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event exceptionHandled = new Event(); + + callInOtherThread(() -> { + exceptionHandled.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + callInOtherThread(() -> { + fail(handler, new ConformanceException(exceptionHandled), IllegalStateException.class); + return null; + }); + throw new ConformanceException(exceptionHandled); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + callInOtherThread(() -> { + responseWritten.await(); + fail(handler, new ConformanceException(), IllegalStateException.class); + return null; + }); + responseWritten.await(); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITH_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + callInOtherThread(() -> { + responseClosed.await(); + fail(handler, new ConformanceException(), IllegalStateException.class); + return null; + }); + responseClosed.await(); + throw new ConformanceException(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondWithContentInOtherThread(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondWithContentInOtherThread(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + completeInOtherThread(handler); + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithNondeterministicSyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithNondeterministicSyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.failed(new ConformanceException()); + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithSyncFailureBeforeResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithSyncFailureBeforeResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event failDone = new Event(); + callInOtherThread(() -> { + failDone.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + try { + handler.failed(new ConformanceException()); + } finally { + failDone.happened(); + } + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithSyncFailureAfterResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithSyncFailureAfterResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + responseWritten.await(); + handler.failed(new ConformanceException()); + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + responseClosed.await(); + handler.failed(new ConformanceException()); + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithNondeterministicAsyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithNondeterministicAsyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondWithContentInOtherThread(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + failInOtherThread(handler, new ConformanceException()); + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithAsyncFailureBeforeResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithAsyncFailureBeforeResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final Event failDone = new Event(); + callInOtherThread(() -> { + failDone.await(); + respondWithContent(handler); + return null; + }); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + callInOtherThread(() -> { + try { + fail(handler, new ConformanceException()); + } finally { + failDone.happened(); + } + return null; + }); + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithAsyncFailureAfterResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithAsyncFailureAfterResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + callInOtherThread(() -> { + respondWithContent(handler); + return null; + }); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + callInOtherThread(() -> { + responseWritten.await(); + fail(handler, new ConformanceException()); + return null; + }); + } + }; + } + }); + } + + public abstract void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + callInOtherThread(() -> { + respondNoContent(handler); + return null; + }); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + callInOtherThread(() -> { + responseClosed.await(); + fail(handler, new ConformanceException()); + return null; + }); + } + }; + } + }); + } + + public abstract void testRequestContentCloseNondeterministicException() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicException( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionBeforeResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event exceptionHandledByFramework = new Event(); + callInOtherThread(() -> { + exceptionHandledByFramework.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + throw new ConformanceException(exceptionHandledByFramework); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseWrite() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWrite( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + responseWritten.await(); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContent() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContent( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + responseClosed.await(); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseNondeterministicExceptionWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicExceptionWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondWithContentInOtherThread(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final Event exceptionHandledByFramework = new Event(); + callInOtherThread(() -> { + exceptionHandledByFramework.await(); + respondWithContent(handler); + return null; + }); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + throw new ConformanceException(exceptionHandledByFramework); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + callInOtherThread(() -> { + respondWithContent(handler); + return null; + }); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + responseWritten.await(); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + callInOtherThread(() -> { + respondNoContent(handler); + return null; + }); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.completed(); + responseClosed.await(); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondWithContentInOtherThread(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + completeInOtherThread(handler, IllegalStateException.class); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final Event exceptionHandledByFramework = new Event(); + callInOtherThread(() -> { + exceptionHandledByFramework.await(); + respondWithContent(handler); + return null; + }); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + completeInOtherThread(handler, IllegalStateException.class); + throw new ConformanceException(exceptionHandledByFramework); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondWithContentInOtherThread(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + completeInOtherThread(handler, IllegalStateException.class); + responseWritten.await(); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + respondNoContentInOtherThread(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + completeInOtherThread(handler, IllegalStateException.class); + responseClosed.await(); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseNondeterministicExceptionWithSyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicExceptionWithSyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + handler.failed(new ConformanceException()); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event failDone = new Event(); + callInOtherThread(() -> { + failDone.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + try { + handler.failed(new ConformanceException()); + } finally { + failDone.happened(); + } + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + responseWritten.await(); + handler.failed(new ConformanceException()); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + responseClosed.await(); + handler.failed(new ConformanceException()); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseNondeterministicExceptionWithAsyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicExceptionWithAsyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + callInOtherThread(() -> { + fail(handler, new ConformanceException(), IllegalStateException.class); + return null; + }); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + final Event exceptionHandled = new Event(); + + callInOtherThread(() -> { + exceptionHandled.await(); + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + + callInOtherThread(() -> { + fail(handler, new ConformanceException(exceptionHandled), IllegalStateException.class); + return null; + }); + + throw new ConformanceException(exceptionHandled); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + writeResponse(out); + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + callInOtherThread(() -> { + responseWritten.await(); + fail(handler, new ConformanceException(), IllegalStateException.class); + return null; + }); + responseWritten.await(); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure() throws Throwable; + private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(final Request request, final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + callInOtherThread(() -> { + responseClosed.await(); + fail(handler, new ConformanceException(), IllegalStateException.class); + return null; + }); + responseClosed.await(); + throw new ConformanceException(); + } + }; + } + }); + } + + public abstract void testResponseWriteCompletionException() throws Throwable; + private <T extends ServerProvider, U, V> void testResponseWriteCompletionException( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(Request request, ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + for (ByteBuffer buf : adapter.newResponseContent()) { + out.write(buf, EXCEPTION_COMPLETION_HANDLER); + } + closeResponse(out); + return null; + } + }); + } + + public abstract void testResponseCloseCompletionException() throws Throwable; + private <T extends ServerProvider, U, V> void testResponseCloseCompletionException( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(Request request, ResponseHandler handler) { + ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + writeResponse(out); + out.close(EXCEPTION_COMPLETION_HANDLER); + return null; + } + }); + } + + public abstract void testResponseCloseCompletionExceptionNoContent() throws Throwable; + private <T extends ServerProvider, U, V> void testResponseCloseCompletionExceptionNoContent( + final Adapter<T, U, V> adapter, + final Module... config) + throws Throwable { + runTest(adapter, + Modules.combine(config), + RequestType.WITHOUT_CONTENT, + new TestRequestHandler() { + + @Override + public ContentChannel handle(Request request, ResponseHandler handler) { + ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + out.close(EXCEPTION_COMPLETION_HANDLER); + return null; + } + }); + } + + // -------------------------------------------------------------------------------------------------------------- // + // // + // The following section is implementation details that are not necessary to understand in order to implement a // + // conformance test for a ServerProvider. // + // // + // -------------------------------------------------------------------------------------------------------------- // + + protected <T extends ServerProvider, U, V> void runTest( + final Adapter<T, U, V> adapter, + final Module... guiceModules) + throws Throwable { + Class<ServerProviderConformanceTest> clazz = ServerProviderConformanceTest.class; + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (StackTraceElement element : stack) { + Method method; + final String methodName = element.getMethodName(); + try { + method = clazz.getDeclaredMethod(methodName); + } catch (NoSuchMethodException e) { + continue; + } + if (!Modifier.isAbstract(method.getModifiers())) { + continue; + } + try { + method = clazz.getDeclaredMethod(methodName, Adapter.class, Module[].class); + System.out.println("Invoking test method " + methodName); + method.invoke(this, adapter, guiceModules); + return; + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + throw new UnsupportedOperationException("Method runTest() not called from overridden testXXX() method."); + } + + // The only purpose of this is to avoid magic literals in calls to runTest (which we'd have if we used a bool flag). + private enum RequestType { + WITHOUT_CONTENT, WITH_CONTENT + } + + private <T extends ServerProvider, U, V> void runTest( + final Adapter<T, U, V> adapter, + final Module testConfig, + final RequestType requestType, + final TestRequestHandler requestHandler) + throws Throwable { + final Module config = Modules.override(newDefaultConfig(), adapter.newConfigModule()).with(testConfig); + final TestDriver driver = TestDriver.newSimpleApplicationInstance(config); + final ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind(builder.getInstance(Key.get(String.class, Names.named("serverBinding"))), + requestHandler); + final T serverProvider = builder.guiceModules().getInstance(adapter.getServerProviderClass()); + builder.serverProviders().install(serverProvider); + if (builder.getInstance(Key.get(Boolean.class, Names.named("activateContainer")))) { + driver.activateContainer(builder); + } + serverProvider.start(); + serverProvider.release(); + + for (int i = 0; i < NUM_RUNS_EACH_TEST; ++i) { + System.out.println("Test run #" + i); + requestHandler.reset(adapter.newResponseContent()); + final U client = adapter.newClient(serverProvider); + final boolean withRequestContent = requestType == RequestType.WITH_CONTENT; + final V result = adapter.executeRequest(client, withRequestContent); + adapter.validateResponse(result); + if (client instanceof Closeable) { + ((Closeable) client).close(); + } + requestHandler.awaitAsyncTasks(); + } + + serverProvider.close(); + driver.close(); + } + + private static Module newDefaultConfig() { + return Modules.combine(newServerBinding("*://*/*"), + newActivateContainer(true)); + } + + private static Module newBindingSetSelector(final String bindingSetName) { + return new AbstractModule() { + + @Override + protected void configure() { + bind(BindingSetSelector.class).toInstance(uri -> bindingSetName); + } + }; + } + + private static Module newServerBinding(final String serverBinding) { + return new AbstractModule() { + + @Override + protected void configure() { + bind(String.class).annotatedWith(Names.named("serverBinding")).toInstance(serverBinding); + } + }; + } + + private static Module newActivateContainer(final boolean activateContainer) { + return new AbstractModule() { + + @Override + protected void configure() { + bind(Boolean.class).annotatedWith(Names.named("activateContainer")).toInstance(activateContainer); + } + }; + } + + /** + * Wrapper around CountDownLatch for single-occurrence events. + */ + private static class Event { + private final CountDownLatch latch = new CountDownLatch(1); + + public void happened() { + latch.countDown(); + } + + public void await() { + try { + final boolean success = latch.await(600, TimeUnit.SECONDS); + if (!success) { + throw new IllegalStateException("Wait for required condition timed out"); + } + } catch (InterruptedException e) { + throw new IllegalStateException("Wait for required condition was interrupted", e); + } + } + } + + private static abstract class TestRequestHandler extends AbstractRequestHandler { + + private static class TaskHandle { + private final Exception stackTrace = new Exception(); + + @Override + public String toString() { + final StringWriter stringWriter = new StringWriter(); + stackTrace.printStackTrace(new PrintWriter(stringWriter)); + return "(" + stringWriter.toString() + ")"; + } + } + + protected Event responseWritten; + protected Event responseClosed; + private ExecutorService executor; + private final Object taskMonitor = new Object(); + private Set<TaskHandle> pendingTasks = new HashSet<>(); + private Exception taskException; + private Iterable<ByteBuffer> responseContent; + + public void reset(final Iterable<ByteBuffer> responseContent) { + synchronized (taskMonitor) { + if (!pendingTasks.isEmpty()) { + throw new AssertionError("pendingTasks should be empty, was " + pendingTasks); + } + } + this.executor = Executors.newCachedThreadPool(); + this.responseWritten = new Event(); + this.responseClosed = new Event(); + this.responseContent = responseContent; + this.taskException = null; + } + + protected final void callInOtherThread(final Callable<Void> task) { + final TaskHandle taskHandle = addTask(); + final Runnable runnable = () -> { + try { + task.call(); + } catch (Exception e) { + setTaskFailure(e); + } + removeTask(taskHandle); + }; + try { + executor.submit(runnable); + } catch (RejectedExecutionException e) { + setTaskFailure(e); + removeTask(taskHandle); + } + } + + private void setTaskFailure(Exception e) { + synchronized (taskMonitor) { + if (taskException == null) { + taskException = e; + } else { + System.out.println("Got subsequent exception in task execution: "); + e.printStackTrace(); + } + } + } + + private void removeTask(final TaskHandle taskHandle) { + synchronized (taskMonitor) { + pendingTasks.remove(taskHandle); + taskMonitor.notifyAll(); + } + } + + @CheckReturnValue + private TaskHandle addTask() { + final TaskHandle taskHandle = new TaskHandle(); + synchronized (taskMonitor) { + pendingTasks.add(taskHandle); + } + return taskHandle; + } + + protected final void writeResponse(final ContentChannel out) { + try { + writeAll(out, responseContent); + } finally { + responseWritten.happened(); + } + } + + private void writeAll(final ContentChannel out, final Iterable<ByteBuffer> content) { + for (ByteBuffer buf : content) { + out.write(buf, newCompletionHandler()); + } + } + + protected final void closeResponseInOtherThread(final ContentChannel out) { + callInOtherThread(() -> { + closeResponse(out); + return null; + }); + } + + protected final void closeResponse(final ContentChannel out) { + try { + out.close(newCompletionHandler()); + } finally { + responseClosed.happened(); + } + } + + protected final CompletionHandler newCompletionHandler() { + final CallableCompletionHandler handler = new CallableCompletionHandler(); + callInOtherThread(handler); + return handler; + } + + protected final void respondWithContentInOtherThread(final ResponseHandler handler) { + callInOtherThread(() -> { + respondWithContent(handler); + return null; + }); + } + + protected final void respondNoContentInOtherThread(final ResponseHandler handler) { + callInOtherThread(() -> { + respondNoContent(handler); + return null; + }); + } + + protected void respondWithContent(final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + writeResponse(out); + closeResponse(out); + } + + protected void respondNoContent(final ResponseHandler handler) { + final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK)); + closeResponse(out); + } + + protected final void completeInOtherThread( + final CompletionHandler handler, + final Class<?>... allowedExceptionTypes) { + callInOtherThread(() -> { + try { + handler.completed(); + } catch (Throwable t) { + if (!isInstanceOfAnyOf(t, allowedExceptionTypes)) { + throw t; + } + } + return null; + }); + } + + protected final void fail( + final CompletionHandler handler, + final Throwable failure, + final Class<?>... allowedExceptionTypes) { + try { + handler.failed(failure); + } catch (Throwable t) { + if (!isInstanceOfAnyOf(t, allowedExceptionTypes)) { + throw t; + } + } + } + + private static boolean isInstanceOfAnyOf(final Object object, final Class<?>... types) { + return Stream.of(types).anyMatch(type -> type.isAssignableFrom(object.getClass())); + } + + protected final void failInOtherThread( + final CompletionHandler handler, + final Throwable failure, + final Class<?>... allowedExceptionTypes) { + callInOtherThread(() -> { + fail(handler, failure, allowedExceptionTypes); + return null; + }); + } + + @Override + public final ContentChannel handleRequest(final Request request, final ResponseHandler responseHandler) { + // Ensure that task executor is not shut down before handleResponse() is done. + final TaskHandle handleResponseTask = addTask(); + try { + final ContentChannel requestContentChannel = handle(request, responseHandler); + if (requestContentChannel == null) { + return null; + } + // Ensure that task executor is not shut down before close() is done. + final TaskHandle requestContentChannelCloseTask = addTask(); + return new ContentChannel() { + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + requestContentChannel.write(buf, handler); + } + + @Override + public void close(CompletionHandler handler) { + try { + requestContentChannel.close(handler); + } finally { + removeTask(requestContentChannelCloseTask); + } + } + }; + } finally { + removeTask(handleResponseTask); + } + } + + protected abstract ContentChannel handle(Request request, ResponseHandler responseHandler); + + public final void awaitAsyncTasks() throws Exception { + final long maxWaitTimeMillis = 600_000L; + final long startTimeMillis = System.currentTimeMillis(); + synchronized (taskMonitor) { + while (!pendingTasks.isEmpty()) { + final long currentTimeMillis = System.currentTimeMillis(); + final long timeElapsedMillis = currentTimeMillis - startTimeMillis; + if (timeElapsedMillis >= maxWaitTimeMillis) { + throw new IllegalStateException( + "Wait timed out, still have the following pending tasks: " + pendingTasks); + } + final long waitTimeMillis = maxWaitTimeMillis - timeElapsedMillis; + taskMonitor.wait(waitTimeMillis); + } + } + executor.shutdown(); + final boolean haltedCleanly = executor.awaitTermination(600, TimeUnit.SECONDS); + if (!haltedCleanly) { + throw new IllegalStateException("Some tasks did not finish. executor=" + executor); + } + synchronized (taskMonitor) { + if (taskException != null) { + throw new Exception("Task threw exception", taskException); + } + } + } + } + + private static class CallableCompletionHandler implements Callable<Void>, CompletionHandler { + + final CountDownLatch done = new CountDownLatch(1); + + @Override + public void completed() { + done.countDown(); + } + + @Override + public void failed(final Throwable t) { + done.countDown(); + } + + @Override + public Void call() throws Exception { + if (!done.await(600, TimeUnit.SECONDS)) { + throw new TimeoutException(); + } + return null; + } + } + + private static final CompletionHandler EXCEPTION_COMPLETION_HANDLER = new CompletionHandler() { + + @Override + public void completed() { + throw new ConformanceException(); + } + + @Override + public void failed(Throwable t) { + throw new ConformanceException(); + } + }; +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java new file mode 100644 index 00000000000..f5a0f83f4ba --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java @@ -0,0 +1,402 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.binder.AnnotatedBindingBuilder; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.*; +import com.yahoo.jdisc.core.ApplicationLoader; +import com.yahoo.jdisc.core.BootstrapLoader; +import com.yahoo.jdisc.core.FelixFramework; +import com.yahoo.jdisc.core.FelixParams; +import com.yahoo.jdisc.handler.*; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.net.URI; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * <p>This class provides a unified way to set up and run unit tests on jDISC components. In short, it is a programmable + * {@link BootstrapLoader} that provides convenient access to the {@link ContainerActivator} and {@link + * CurrentContainer} interfaces. A typical test case using this class looks as follows:</p> + * <pre> + * {@literal @}Test + * public void requireThatMyComponentIsWellBehaved() { + * TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + * ContainerBuilder builder = driver.newContainerBuilder(); + * (... configure builder ...) + * driver.activateContainer(builder); + * (... run tests ...) + * assertTrue(driver.close()); + * } + * </pre> + * <p>One of the most important things to remember when using this class is to always call {@link #close()} at the end + * of your test case. This ensures that the tested configuration does not prevent graceful shutdown. If close() returns + * FALSE, it means that either your components or the test case itself does not conform to the reference counting + * requirements of {@link Request}, {@link RequestHandler}, {@link ContentChannel}, or {@link CompletionHandler}.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TestDriver implements ContainerActivator, CurrentContainer { + + private static final AtomicInteger testId = new AtomicInteger(0); + private final FutureTask<Boolean> closeTask = new FutureTask<>(new CloseTask()); + private final ApplicationLoader loader; + + private TestDriver(ApplicationLoader loader) { + this.loader = loader; + } + + @Override + public ContainerBuilder newContainerBuilder() { + return loader.newContainerBuilder(); + } + + @Override + public DeactivatedContainer activateContainer(ContainerBuilder builder) { + return loader.activateContainer(builder); + } + + @Override + public Container newReference(URI uri) { + return loader.newReference(uri); + } + + /** + * <p>Returns the {@link BootstrapLoader} used by this TestDriver. Use caution when invoking methods on the + * BootstrapLoader directly, since the lifecycle management done by this TestDriver may become corrupt.</p> + * + * @return The BootstrapLoader. + */ + public BootstrapLoader bootstrapLoader() { + return loader; + } + + /** + * <p>Returns the {@link Application} loaded by this TestDriver. Until {@link #close()} is called, this method will + * never return null.</p> + * + * @return The loaded Application. + */ + public Application application() { + return loader.application(); + } + + /** + * <p>Returns the {@link OsgiFramework} created by this TestDriver. Although this method will never return null, it + * might return a {@link NonWorkingOsgiFramework} depending on the factory method used to instantiate it.</p> + * + * @return The OSGi framework. + */ + public OsgiFramework osgiFramework() { + return loader.osgiFramework(); + } + + /** + * <p>Convenience method to create and {@link Request#connect(ResponseHandler)} a {@link Request} on the {@link + * CurrentContainer}. This method will either return the corresponding {@link ContentChannel} or throw the + * appropriate exception (see {@link Request#connect(ResponseHandler)}).</p> + * + * @param requestUri The URI string to parse and pass to the Request constructor. + * @param responseHandler The ResponseHandler to pass to {@link Request#connect(ResponseHandler)}. + * @return The ContentChannel returned by {@link Request#connect(ResponseHandler)}. + * @throws NullPointerException If the URI string or the {@link ResponseHandler} is null. + * @throws IllegalArgumentException If the URI string violates RFC 2396. + * @throws BindingNotFoundException If the corresponding call to {@link Container#resolveHandler(Request)} + * returns null. + * @throws RequestDeniedException If the corresponding call to {@link RequestHandler#handleRequest(Request, + * ResponseHandler)} returns null. + */ + public ContentChannel connectRequest(String requestUri, ResponseHandler responseHandler) { + return newRequestDispatch(requestUri, responseHandler).connect(); + } + + /** + * <p>Convenience method to create a {@link Request}, connect it to a {@link RequestHandler}, and close the returned + * {@link ContentChannel}. This is the same as calling:</p> + * <pre> + * connectRequest(uri, responseHandler).close(null); + * </pre> + * + * @param requestUri The URI string to parse and pass to the Request constructor. + * @param responseHandler The ResponseHandler to pass to {@link Request#connect(ResponseHandler)}. + * @return A waitable Future that provides access to the corresponding {@link Response}. + * @throws NullPointerException If the URI string or the {@link ResponseHandler} is null. + * @throws IllegalArgumentException If the URI string violates RFC 2396. + * @throws BindingNotFoundException If the corresponding call to {@link Container#resolveHandler(Request)} + * returns null. + * @throws RequestDeniedException If the corresponding call to {@link RequestHandler#handleRequest(Request, + * ResponseHandler)} returns null. + */ + public Future<Response> dispatchRequest(String requestUri, ResponseHandler responseHandler) { + return newRequestDispatch(requestUri, responseHandler).dispatch(); + } + + /** + * <p>Initiates the shut down of this TestDriver in another thread. By doing this in a separate thread, it allows + * other code to monitor its progress. Unless you need the added monitoring capability, you should use {@link + * #close()} instead.</p> + * + * @see #awaitClose(long, TimeUnit) + */ + public void scheduleClose() { + new Thread(closeTask, "TestDriver.Closer").start(); + } + + /** + * <p>Waits for shut down of this TestDriver to complete. This call must be preceded by a call to {@link + * #scheduleClose()}.</p> + * + * @param timeout The maximum time to wait. + * @param unit The time unit of the timeout argument. + * @return True if shut down completed within the allocated time. + */ + public boolean awaitClose(long timeout, TimeUnit unit) { + try { + closeTask.get(timeout, unit); + return true; + } catch (TimeoutException e) { + return false; + } catch (Exception e) { + throw e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e); + } + } + + /** + * <p>Initiatiates shut down of this TestDriver and waits for it to complete. If shut down fails to complete within + * 60 seconds, this method throws an exception.</p> + * + * @return True if shut down completed within the allocated time. + * @throws IllegalStateException If shut down failed to complete within the allocated time. + */ + public boolean close() { + scheduleClose(); + if ( ! awaitClose(600, TimeUnit.SECONDS)) { + throw new IllegalStateException("Application failed to terminate within allocated time."); + } + return true; + } + + /** + * <p>Creates a new {@link RequestDispatch} that dispatches a {@link Request} with the given URI and {@link + * ResponseHandler}.</p> + * + * @param requestUri The uri of the Request to create. + * @param responseHandler The ResponseHandler to use for the dispather. + * @return The created RequestDispatch. + */ + public RequestDispatch newRequestDispatch(final String requestUri, final ResponseHandler responseHandler) { + return new RequestDispatch() { + + @Override + protected Request newRequest() { + return new Request(loader, URI.create(requestUri)); + } + + @Override + public ContentChannel handleResponse(Response response) { + return responseHandler.handleResponse(response); + } + }; + } + + /** + * <p>Creates a new TestDriver with an injected {@link Application}.</p> + * + * @param appClass The Application class to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newInjectedApplicationInstance(Class<? extends Application> appClass, + Module... guiceModules) { + return newInstance(newOsgiFramework(), null, false, + newModuleList(null, appClass, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with an injected {@link Application}, but without OSGi support.</p> + * + * @param appClass The Application class to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + * @see #newInjectedApplicationInstance(Class, Module...) + * @see #newNonWorkingOsgiFramework() + */ + public static TestDriver newInjectedApplicationInstanceWithoutOsgi(Class<? extends Application> appClass, + Module... guiceModules) { + return newInstance(newNonWorkingOsgiFramework(), null, false, + newModuleList(null, appClass, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with an injected {@link Application}.</p> + * + * @param app The Application to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newInjectedApplicationInstance(Application app, Module... guiceModules) { + return newInstance(newOsgiFramework(), null, false, newModuleList(app, null, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with an injected {@link Application}, but without OSGi support.</p> + * + * @param app The Application to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + * @see #newInjectedApplicationInstance(Application, Module...) + * @see #newNonWorkingOsgiFramework() + */ + public static TestDriver newInjectedApplicationInstanceWithoutOsgi(Application app, Module... guiceModules) { + return newInstance(newNonWorkingOsgiFramework(), null, false, newModuleList(app, null, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with a predefined {@link Application} implementation. The injected Application class + * implements nothing but the bare minimum to conform to the Application interface.</p> + * + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newSimpleApplicationInstance(Module... guiceModules) { + return newInstance(newOsgiFramework(), null, false, + newModuleList(null, SimpleApplication.class, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with a predefined {@link Application} implementation, but without OSGi support. The + * injected Application class implements nothing but the bare minimum to conform to the Application interface.</p> + * + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + * @see #newSimpleApplicationInstance(Module...) + * @see #newNonWorkingOsgiFramework() + */ + public static TestDriver newSimpleApplicationInstanceWithoutOsgi(Module... guiceModules) { + return newInstance(newNonWorkingOsgiFramework(), null, false, + newModuleList(null, SimpleApplication.class, guiceModules)); + } + + /** + * <p>Creates a new TestDriver from an application bundle. This runs the same code path as the actual jDISC startup + * code. Note that the named bundle must have a "X-JDisc-Application" bundle instruction, or setup will fail.</p> + * + * @param bundleLocation The location of the application bundle to load. + * @param privileged Whether or not privileges should be marked as available to the application bundle. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newApplicationBundleInstance(String bundleLocation, boolean privileged, + Module... guiceModules) { + return newInstance(newOsgiFramework(), bundleLocation, privileged, Arrays.asList(guiceModules)); + } + + /** + * <p>Creates a new TestDriver with the given parameters. This is the factory method that all other factory methods + * call. It allows you to specify all parts of the TestDriver manually.</p> + * + * @param osgiFramework The {@link OsgiFramework} to assign to the created TestDriver. + * @param bundleLocation The location of the application bundle to load, may be null. + * @param privileged Whether or not privileges should be marked as available to the application bundle. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newInstance(OsgiFramework osgiFramework, String bundleLocation, boolean privileged, + Module... guiceModules) { + return newInstance(osgiFramework, bundleLocation, privileged, Arrays.asList(guiceModules)); + } + + /** + * <p>Factory method to create a working {@link OsgiFramework}. This method is used by all {@link TestDriver} + * factories that DO NOT have the "WithoutOsgi" suffix.</p> + * + * @return A working OsgiFramework. + */ + public static FelixFramework newOsgiFramework() { + return new FelixFramework(new FelixParams().setCachePath("target/bundlecache" + testId.getAndIncrement())); + } + + /** + * <p>Factory method to create a light-weight {@link OsgiFramework} that throws {@link + * UnsupportedOperationException} if {@link OsgiFramework#installBundle(String)} or {@link + * OsgiFramework#startBundles(List, boolean)} is called. This allows for unit testing without the footprint of OSGi + * support. This method is used by {@link TestDriver} factories that have the "WithoutOsgi" suffix.</p> + * + * @return A non-working OsgiFramework. + */ + public static OsgiFramework newNonWorkingOsgiFramework() { + return new NonWorkingOsgiFramework(); + } + + private class CloseTask implements Callable<Boolean> { + + @Override + public Boolean call() throws Exception { + loader.stop(); + loader.destroy(); + return true; + } + } + + private static TestDriver newInstance(OsgiFramework osgiFramework, String bundleLocation, boolean privileged, + Iterable<? extends Module> guiceModules) { + ApplicationLoader loader = new ApplicationLoader(osgiFramework, guiceModules); + try { + loader.init(bundleLocation, privileged); + } catch (Exception e) { + throw e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e); + } + try { + loader.start(); + } catch (Exception e) { + loader.destroy(); + throw e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e); + } + return new TestDriver(loader); + } + + private static List<Module> newModuleList(final Application app, final Class<? extends Application> appClass, + Module... guiceModules) { + List<Module> lst = new LinkedList<>(); + lst.addAll(Arrays.asList(guiceModules)); + lst.add(new AbstractModule() { + + @Override + public void configure() { + AnnotatedBindingBuilder<Application> builder = bind(Application.class); + if (app != null) { + builder.toInstance(app); + } else { + builder.to(appClass); + } + } + }); + return lst; + } + + private static class SimpleApplication implements Application { + + @Override + public void start() { + + } + + @Override + public void stop() { + + } + + @Override + public void destroy() { + + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/package-info.java new file mode 100644 index 00000000000..c84cb01e47d --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * <p>Provides classes and interfaces for implementing unit tests of jDISC components.</p> + * + * @see com.yahoo.jdisc.test.TestDriver + */ +@com.yahoo.api.annotations.PublicApi +package com.yahoo.jdisc.test; diff --git a/jdisc_core/src/main/perl/jdisc_logfmt b/jdisc_core/src/main/perl/jdisc_logfmt new file mode 100755 index 00000000000..1a05e229832 --- /dev/null +++ b/jdisc_core/src/main/perl/jdisc_logfmt @@ -0,0 +1,324 @@ +#!/usr/local/bin/perl +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +# BEGIN perl environment bootstrap section +# Do not edit between here and END as this section should stay identical in all scripts + +use File::Basename; +use File::Path; + +sub findpath { + my $myfullname = ${0}; + my($myname, $mypath) = fileparse($myfullname); + + return $mypath if ( $mypath && -d $mypath ); + $mypath=`pwd`; + + my $pwdfullname = $mypath . "/" . $myname; + return $mypath if ( -f $pwdfullname ); + return 0; +} + +# Returns the argument path if it seems to point to VESPA_HOME, 0 otherwise +sub is_vespa_home { + my($VESPA_HOME) = shift; + my $COMMON_ENV="libexec/vespa/common-env.sh"; + if ( $VESPA_HOME && -d $VESPA_HOME ) { + my $common_env = $VESPA_HOME . "/" . $COMMON_ENV; + return $VESPA_HOME if -f $common_env; + } + return 0; +} + +# Returns the home of Vespa, or dies if it cannot +sub findhome { + # Try the VESPA_HOME env variable + return $ENV{'VESPA_HOME'} if is_vespa_home($ENV{'VESPA_HOME'}); + if ( $ENV{'VESPA_HOME'} ) { # was set, but not correctly + die "FATAL: bad VESPA_HOME value '" . $ENV{'VESPA_HOME'} . "'\n"; + } + + # Try the ROOT env variable + $ROOT = $ENV{'ROOT'}; + return $ROOT if is_vespa_home($ROOT); + + # Try the script location or current dir + my $mypath = findpath(); + if ($mypath) { + while ( $mypath =~ s|/[^/]*$|| ) { + return $mypath if is_vespa_home($mypath); + } + } + die "FATAL: Missing VESPA_HOME environment variable\n"; +} + +BEGIN { + my $tmp = findhome(); + if ( $tmp !~ m{[/]$} ) { $tmp .= "/"; } + $ENV{'VESPA_HOME'} = $tmp; +} +my $VESPA_HOME = $ENV{'VESPA_HOME'}; + +# END perl environment bootstrap section + +use 5.006_001; +use strict; +use warnings; + +use File::Basename; +use Getopt::Long qw(:config no_ignore_case); + +my %showflags = ( + time => 1, + fmttime => 1, + msecs => 1, + usecs => 0, + host => 0, + level => 1, + pid => 0, + service => 1, + component => 1, + message => 1 + ); + +my %levelflags = ( + error => 1, + warning => 1, + info => 1, + debug => 0, + unknown => 0 + ); + +# Do not buffer the output +$| = 1; + +my $compore; +my $msgtxre; +my $onlypid; +my $onlysvc; +my $onlyhst; + +my $shortsvc; +my $shortcmp; + +my @optlevels; +my @optaddlevels; +my @optshow; +my $optaddlevels; +my $optlevels; +my $optfollow; +my $optnldequote; +my $opthelp = ''; + +my $bad = 0; + +GetOptions ('level|l=s' => \@optlevels, + 'add-level|L=s' => \@optaddlevels, + 'service|S=s' => \$onlysvc, + 'show|s=s' => \@optshow, + 'pid|p=s' => \$onlypid, + 'component|c=s' => \$compore, + 'message|m=s' => \$msgtxre, + 'help|h' => \$opthelp, + 'follow|f' => \$optfollow, + 'nldequote|N' => \$optnldequote, + 'host|H=s' => \$onlyhst, + 'truncateservice|ts' => \$shortsvc, + 'truncatecomponent|tc|t' => \$shortcmp, + ) or $bad=1; + +if ( @ARGV == 0 and ! -p STDIN) { + push(@ARGV, "$VESPA_HOME/logs/jdisc_core/jdisc_core.log"); +} + +if ( $optfollow ) { + my $filearg = ""; + if ( @ARGV > 1 ) { + print STDERR "ERROR: Cannot follow more than one file\n\n"; + $bad=1; + } else { + $filearg = shift @ARGV if (@ARGV > 0); + open(STDIN, "tail -F $filearg |") + or die "cannot open 'tail -F $filearg' as input pipe\n"; + } +} + +$optaddlevels = join(",", @optaddlevels ); +if ( $optaddlevels ) { + my @l = split(/,/, $optaddlevels); + my $l; + foreach $l ( @l ) { + $levelflags{$l} = 0; + } +} + +if ( $opthelp || $bad ) { + print STDERR "Usage: ", basename($0), " [options] [inputfile ...]\n", + "Options:\n", + " -l LEVELLIST\t--level=LEVELLIST\tselect levels to include\n", + " -L LEVELLIST\t--add-level=LEVELLIST\tdefine extra levels\n", + " -s FIELDLIST\t--show=FIELDLIST\tselect fields to print\n", + " -p PID\t--pid=PID\t\tselect messages from given PID\n", + " -S SERVICE\t--service=SERVICE\tselect messages from given SERVICE\n", + " -H HOST\t--host=HOST\t\tselect messages from given HOST\n", + " -c REGEX\t--component=REGEX\tselect components matching REGEX\n", + " -m REGEX\t--message=REGEX\t\tselect message text matching REGEX\n", + " -f\t\t--follow\t\tinvoke tail -F to follow input file\n", + " -N\t\t--nldequote\t\tdequote newlines in message text field\n", + " -t\t--tc\t--truncatecomponent\tchop component to 15 chars\n", + " \t--ts\t--truncateservice\tchop service to 9 chars\n", + "\n", + "FIELDLIST is comma separated, available fields:\n", + "\t time fmttime msecs usecs host level pid service component message\n", + "Available levels for LEVELLIST:\n", + "\t ", join(" ", sort keys(%levelflags)), "\n", + "for both lists, use 'all' for all possible values, and -xxx to disable xxx.\n"; + exit $bad; +} + +$optlevels = join(",", @optlevels ); +if ( $optlevels ) { + my $k; + unless ( $optlevels =~ s/^\+// or $optlevels =~ m/^-/ ) { + $levelflags{$_} = 0 foreach ( keys %levelflags ); + } + my @l = split(/,|(?=-)/, $optlevels); + my $l; + foreach $l ( @l ) { + my $v = 1; + my $minus = ""; + if ( $l =~ s/^-// ) { $v = 0; $minus = "-"; } + if ( $l eq "all" ) { + foreach $k ( keys %levelflags ) { + $levelflags{$k} = $v; + } + } elsif ( defined $levelflags{$l} ) { + $levelflags{$l} = $v; + } else { + print STDERR "bad level option '$minus$l'\n"; + exit 1; + } + } +} + +my $optshow; +$optshow = join(",", @optshow ); +if ( $optshow ) { + my $k; + unless ( $optshow =~ s/^\+// or $optshow =~ m/^-/ ) { + $showflags{$_} = 0 foreach ( keys %showflags ); + } + my @l = split(/,|(?=-)/, $optshow); + my $l; + foreach $l ( @l ) { + my $v = 1; + my $minus = ""; + if ( $l =~ s/^-// ) { $v = 0; $minus = "-"; } + if ( $l eq "all" ) { + foreach $k ( keys %showflags ) { + $showflags{$k} = $v; + } + } elsif ( defined $showflags{$l} ) { + $showflags{$l} = $v; + } else { + print STDERR "bad show option '$minus$l'\n"; + exit 1; + } + } +} + +while (<>) { + chomp; + if ( /^ + (\d+)\.?(\d*) # seconds, optional fractional seconds + \t + ([^\t]*) # host + \t + (\d+\/?\d*|\-\/\d+) # pid, optional tid + \t + ([^\t]*) # servicename + \t + ([^\t]*) # componentname + \t + (\w+) # level + \t + (.*) # message text + $/x ) + { + my $secs = $1; + my $usec = $2 . "000000"; # make sure we have atleast 6 digits + my $host = $3; + my $pidn = $4; + my $svcn = $5; + my $comp = $6; + my $levl = $7; + my $msgt = $8; + + if ( ! defined $levelflags{$levl} ) { + print STDERR "Warning: unknown level '$levl' in input\n"; + $levelflags{$levl} = 1; + } + next unless ( $levelflags{$levl} ); + + if ($compore && $comp !~ m/$compore/o) { next; } + if ($msgtxre && $msgt !~ m/$msgtxre/o) { next; } + if ($onlypid && $pidn ne $onlypid) { next; } + if ($onlysvc && $svcn ne $onlysvc) { next; } + if ($onlyhst && $host ne $onlyhst) { next; } + + $levl = "\U$levl"; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday); + ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday)=localtime($secs); + my $datestr = sprintf("%04d-%02d-%02d", + 1900+$year, 1+$mon, $mday); + my $timestr = sprintf("%02d:%02d:%02d", + $hour, $min, $sec); + + if ( $showflags{"time"} || $showflags{"fmttime"} ) { + if ($showflags{"fmttime"} ) { + print "[$datestr $timestr"; + if ( $showflags{"usecs"} ) { + printf ".%.6s", $usec; + } elsif ( $showflags{"msecs"} ) { + printf ".%.3s", $usec; + } + print "] "; + } else { + printf "%s.%.6s ", $secs, $usec; + } + } + if ( $showflags{"host"} ) { + printf "%-8s ", $host; + } + if ( $showflags{"level"} ) { + printf "%-7s : ", $levl; + } + if ( $showflags{"pid"} ) { + printf "%5s ", $pidn; + } + if ( $showflags{"service"} ) { + if ( $shortsvc ) { + printf "%-9.9s ", $svcn; + } else { + printf "%-16s ", $svcn; + } + } + if ( $showflags{"component"} ) { + if ( $shortcmp ) { + printf "%-15.15s ", $comp; + } else { + printf "%s\t", $comp; + } + } + if ( $showflags{"message"} ) { + if ( $optnldequote ) { + $msgt = "\n\t${msgt}" if ( $msgt =~ s/\\n/\n\t/g ); + } + print $msgt; + } + print "\n"; + } else { + print STDERR "bad log line: '$_'\n"; + } +} diff --git a/jdisc_core/src/main/perl/jdisc_logfmt.1 b/jdisc_core/src/main/perl/jdisc_logfmt.1 new file mode 100644 index 00000000000..0a884c0ec85 --- /dev/null +++ b/jdisc_core/src/main/perl/jdisc_logfmt.1 @@ -0,0 +1,214 @@ +.\" $Id: logfmt.1,v 1.12 2007-06-19 09:37:25 daljord Exp $ +.\" +.Dd October 29, 2004 +.Dt LOGFMT \&1 "JDisc documentation" +.Os "Yahoo! JDisc" "2.3" +.Os +.Sh NAME +.Nm logfmt +.Nd select and format messages from JDisc log files +.Sh SYNOPSIS +.Nm +.Op Fl L Ar levellist +.Op Fl l Ar levellist +.Op Fl s Ar fieldlist +.Op Fl p Ar pid +.Op Fl S Ar service +.Op Fl H Ar host +.Op Fl c Ar regex +.Op Fl m Ar regex +.Op Fl t +.Op Fl f +.Op Fl N +.Op Fl ts +.Op Ar +.Sh DESCRIPTION +The +.Nm +utility reads JDisc log files, select messages and writes a formatted +version of selected messages to the standard output. +.Pp +The options are as follows: +.Bl -tag -width ".It Fl l Ar levellist" +.It Fl L Ar levellist +Declares additional log levels that should be treated as known. These +levels are suppressed unless also given as argument to option -l. +.Ar levellist +is a comma separated list of level names. +.It Fl l Ar levellist +Select which log levels to select. +The default is to select "error", "warning" and "info" levels, and +suppress "debug" and "unknown" levels; but when using this option, only +the named levels will be selected. +The +.Ar levellist +is a comma separated list of level names. +The name +.Em all +may be used to add all known levels. +Prepending a minus sign will deselect the level named. +Starting the list with a plus sign will add and remove levels +from the current (or default) list of levels instead +of replacing it. +.It Fl s Ar fieldlist +Select which fields of log messages to show. +The order of the actual output fields is fixed. +When using this option, only the named fields will be shown. The +fieldlist is a comma separated list of field names. The name +.Em all +may be used to add all possible fields. +Prepending a minus sign will turn off display of the named field. +Starting the list with a plus sign will add and remove fields +from the current (or default) list of fields instead +of replacing it. +.Pp +The fields which may be named are: +.Bl -tag -width component +.It time +Print the time in seconds since the epoch. +Ignored if +.Em fmttime +is shown. +.It fmttime +Print the time in human-readable [YYYY-MM-DD HH:mm:ss] format. +Note that the time is printed in the local timezone; to get GMT +output use +.Nm "\*[q]env TZ=GMT logfmt\*[q]" +as your command. +.It msecs +Add milliseconds after the seconds in +.Em time +and +.Em fmttime +output. Ignored if +.Em usecs +is in effect. +.It usecs +Add microseconds after the seconds in +.Em time +and +.Em fmttime +output. +.It host +Print the hostname field. +.It level +Print the level field (uppercased). +.It pid +Print the pid field. +.It service +Print the service field. +.It component +Print the component field. +.It message +Print the message text field. +You probably always want to add this. +.El +.Pp +Using this option several times works as if the given +.Ar fieldlist +arguments had been concatenated into one comma-separated list. +The default fields to show are as if +.Bk +.Op Fl s Ar fmttime,msecs,level,service,component,message +.Ek +had been given. +.It Fl p Ar pid +Select only messages where the pid field matches the +.Ar pid +string exactly. +.It Fl S Ar service +Select only messages where the service field matches the +.Ar service +string exactly. +.It Fl H Ar host +Select only messages where the hostname field matches the +.Ar host +string exactly. +.It Fl c Ar regex +Select only messages where the component field matches the +.Ar regex +given, using +.Xr perlre +regular expression matching. +.It Fl m Ar regex +Select only messages where the message text field matches the +.Ar regex +given, using +.Xr perlre +regular expression matching. +.It Fl f +Invoke tail -F to follow input file +.It Fl N +Dequote quoted newlines in the message text field to an actual newline plus tab. +.It Fl t +Format the component field (if shown) as a fixed-with string, +truncating if necessary. +.It Fl ts +Format the service field (if shown) as a fixed-with string, +truncating if necessary. +.El +.Sh EXAMPLES +The command: +.Pp +.Bd -literal -offset indent +logfmt -l event -s service,message,fmttime,message +.Ed +.Pp +will display only messages with log level "event", +printing a human-readable time (without any fractional seconds), +the service generating the event and the event message, like this: +.Bd -literal -offset indent +[2004-12-07 18:43:01] config-sentinel starting/1 name="logd" +[2004-12-07 18:43:01] logd started/1 name="logdemon" +[2004-12-07 18:45:51] rtc starting/1 name="rtc.index0" +[2004-12-07 18:45:51] rtc.index0 started/1 name="flexindexer.index" +[2004-12-07 18:45:51] rtc.index0 stopping/1 name="flexindexer.index" why="done" +[2004-12-07 18:45:53] rtc stopped/1 name="rtc.index0" pid=50600 exitcode=0 +[2004-12-07 18:46:13] logd stopping/1 name="logdemon" why="done ok." +[2004-12-07 18:46:13] config-sentinel stopped/1 name="logd" pid=49633 exitcode=0 +.Ed +.Pp +Note that the second "message" item in the fieldlist is redundant, +and that order of printed field is fixed no matter what the fieldlist +order is. +.Pp +The command: +.Pp +.Bd -literal -offset indent +logfmt -l all-info,-debug -s level \e + -s time,usecs,component,message -t -l -event +.Ed +.Pp +will display messages with log levels that are +.Em not +any of +.Em info, debug, +or +.Em event, +printing the time in seconds and microseconds, the log level, the +component name, and the message text, possibly somewhat like this: +.Bd -literal -offset indent +1102441382.530423 CONFIG : nc Config handle: 'pandora.0-rtx' +1102441551.471568 CONFIG : flexindexer.doc Adding document type typetest-0 +1102441573.148211 WARNING : logdemon stopping on signal 15 +1102441887.158000 WARNING : com.yahoo.fs4.m read exception +1102441935.569567 WARNING : rtc Dispatch inherited job failed for dir dispatch0 +1102442115.746001 WARNING : fdispatch Search node 172.24.94.75:10124 down +1102442474.205920 WARNING : rtx RTC (tcp/172.24.94.75:10161) : DOWN +1102442474.515877 WARNING : fdispatch Search node localhost:10128 down +1102442983.075669 ERROR : flexindexer.std Unable to find cluster map defaultcluster +.Ed +.Sh FILES +If no file argument is given, +.Nm +will read the last JDisc log file $VESPA_HOME/logs/jdisc_core/jdisc_core.log (this also works with the +.Fl f +option). +Otherwise, reads only the files given as arguments. +To read standard input, supply a single dash '-' as a file argument. +.Sh SEE ALSO +Documentation in the "log" module for input file format. +.Sh HISTORY +Developed as part of Vespa 1.1, later moved to JDisc 2.3. The default output +format reflects the old "fastlib" log formatting, with minor differences +and is intended to be human-readable, not parsed. diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/AbstractResourceTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/AbstractResourceTestCase.java new file mode 100644 index 00000000000..d43771c9cfd --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/AbstractResourceTestCase.java @@ -0,0 +1,133 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class AbstractResourceTestCase { + + @Test + public void requireThatDestroyIsCalledWhenReleased() { + MyResource res = new MyResource(); + assertFalse(res.destroyed); + res.release(); + assertTrue(res.destroyed); + } + + @Test + public void requireThatDestroyIsCalledWhenRetainCountReachesZero() { + MyResource res = new MyResource(); + assertEquals(1, res.retainCount()); + assertFalse(res.destroyed); + final ResourceReference reference = res.refer(); + assertEquals(2, res.retainCount()); + res.release(); + assertEquals(1, res.retainCount()); + assertFalse(res.destroyed); + reference.close(); + assertEquals(0, res.retainCount()); + assertTrue(res.destroyed); + } + + @Test + public void requireThatDestroyIsCalledWhenRetainCountReachesZeroOppositeOrder() { + MyResource res = new MyResource(); + assertEquals(1, res.retainCount()); + assertFalse(res.destroyed); + final ResourceReference reference = res.refer(); + assertEquals(2, res.retainCount()); + reference.close(); + assertEquals(1, res.retainCount()); + assertFalse(res.destroyed); + res.release(); + assertEquals(0, res.retainCount()); + assertTrue(res.destroyed); + } + + @Test + public void requireThatReleaseCanOnlyBeCalledOnceEvenWhenReferenceCountIsPositive() { + MyResource res = new MyResource(); + final ResourceReference secondReference = res.refer(); + res.release(); + try { + res.release(); + fail(); + } catch (IllegalStateException e) { + // As expected. + } + secondReference.close(); + } + + @Test + public void requireThatSecondaryReferenceCanOnlyBeClosedOnceEvenWhenReferenceCountIsPositive() { + MyResource res = new MyResource(); + final ResourceReference secondReference = res.refer(); + secondReference.close(); + try { + secondReference.close(); + fail(); + } catch (IllegalStateException e) { + // As expected. + } + res.release(); + } + + @Test + public void requireThatReleaseAfterDestroyThrows() { + MyResource res = new MyResource(); + res.release(); + assertTrue(res.destroyed); + try { + res.release(); + fail(); + } catch (IllegalStateException e) { + + } + assertEquals(0, res.retainCount()); + try { + res.release(); + fail(); + } catch (IllegalStateException e) { + + } + assertEquals(0, res.retainCount()); + } + + @Test + public void requireThatReferAfterDestroyThrows() { + MyResource res = new MyResource(); + res.release(); + assertTrue(res.destroyed); + try { + res.refer(); + fail(); + } catch (IllegalStateException e) { + + } + assertEquals(0, res.retainCount()); + try { + res.refer(); + fail(); + } catch (IllegalStateException e) { + + } + assertEquals(0, res.retainCount()); + } + + private static class MyResource extends AbstractResource { + + boolean destroyed = false; + + @Override + protected void destroy() { + destroyed = true; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ContainerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ContainerTestCase.java new file mode 100644 index 00000000000..9eef00b2cbe --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ContainerTestCase.java @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.google.inject.AbstractModule; +import com.yahoo.jdisc.service.BindingSetNotFoundException; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerTestCase { + + @Test + public void requireThatNewRequestsReferenceSameSnapshot() throws Exception { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + Request foo = new Request(driver, URI.create("http://foo")); + Request bar = new Request(foo, URI.create("http://bar")); + assertNotNull(foo.container()); + assertSame(foo.container(), bar.container()); + foo.release(); + bar.release(); + driver.close(); + } + + @Test + public void requireThatInjectionWorks() throws BindingSetNotFoundException { + final Object foo = new Object(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() { + + @Override + protected void configure() { + bind(Object.class).toInstance(foo); + } + }); + driver.activateContainer(driver.newContainerBuilder()); + Request request = new Request(driver, URI.create("http://host/path")); + assertSame(foo, request.container().getInstance(Object.class)); + request.release(); + driver.close(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/HeaderFieldsTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/HeaderFieldsTestCase.java new file mode 100644 index 00000000000..2d0ada8113c --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/HeaderFieldsTestCase.java @@ -0,0 +1,372 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class HeaderFieldsTestCase { + + @Test + public void requireThatSizeWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertEquals(0, headers.size()); + headers.add("foo", "bar"); + assertEquals(1, headers.size()); + headers.add("foo", "baz"); + assertEquals(1, headers.size()); + headers.add("bar", "baz"); + assertEquals(2, headers.size()); + headers.remove("foo"); + assertEquals(1, headers.size()); + headers.remove("bar"); + assertEquals(0, headers.size()); + } + + @Test + public void requireThatIsEmptyWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertTrue(headers.isEmpty()); + headers.add("foo", "bar"); + assertFalse(headers.isEmpty()); + headers.remove("foo"); + assertTrue(headers.isEmpty()); + } + + @Test + public void requireThatContainsKeyWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertFalse(headers.containsKey("foo")); + assertFalse(headers.containsKey("FOO")); + headers.add("foo", "bar"); + assertTrue(headers.containsKey("foo")); + assertTrue(headers.containsKey("FOO")); + } + + @Test + public void requireThatContainsValueWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertFalse(headers.containsValue(Arrays.asList("bar"))); + headers.add("foo", "bar"); + assertTrue(headers.containsValue(Arrays.asList("bar"))); + } + + @Test + public void requireThatContainsWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertFalse(headers.contains("foo", "bar")); + assertFalse(headers.contains("FOO", "bar")); + assertFalse(headers.contains("foo", "BAR")); + assertFalse(headers.contains("FOO", "BAR")); + headers.add("foo", "bar"); + assertTrue(headers.contains("foo", "bar")); + assertTrue(headers.contains("FOO", "bar")); + assertFalse(headers.contains("foo", "BAR")); + assertFalse(headers.contains("FOO", "BAR")); + } + + @Test + public void requireThatContainsIgnoreCaseWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertFalse(headers.containsIgnoreCase("foo", "bar")); + assertFalse(headers.containsIgnoreCase("FOO", "bar")); + assertFalse(headers.containsIgnoreCase("foo", "BAR")); + assertFalse(headers.containsIgnoreCase("FOO", "BAR")); + headers.add("foo", "bar"); + assertTrue(headers.containsIgnoreCase("foo", "bar")); + assertTrue(headers.containsIgnoreCase("FOO", "bar")); + assertTrue(headers.containsIgnoreCase("foo", "BAR")); + assertTrue(headers.containsIgnoreCase("FOO", "BAR")); + } + + @Test + public void requireThatAddStringWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertNull(headers.get("foo")); + headers.add("foo", "bar"); + assertEquals(Arrays.asList("bar"), headers.get("foo")); + headers.add("foo", "baz"); + assertEquals(Arrays.asList("bar", "baz"), headers.get("foo")); + } + + @Test + public void requireThatAddListWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertNull(headers.get("foo")); + headers.add("foo", Arrays.asList("bar")); + assertEquals(Arrays.asList("bar"), headers.get("foo")); + headers.add("foo", Arrays.asList("baz", "cox")); + assertEquals(Arrays.asList("bar", "baz", "cox"), headers.get("foo")); + } + + @Test + public void requireThatAddAllWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + headers.add("foo", "bar"); + headers.add("bar", "baz"); + assertEquals(Arrays.asList("bar"), headers.get("foo")); + assertEquals(Arrays.asList("baz"), headers.get("bar")); + + Map<String, List<String>> map = new HashMap<>(); + map.put("foo", Arrays.asList("baz", "cox")); + map.put("bar", Arrays.asList("cox")); + headers.addAll(map); + + assertEquals(Arrays.asList("bar", "baz", "cox"), headers.get("foo")); + assertEquals(Arrays.asList("baz", "cox"), headers.get("bar")); + } + + @Test + public void requireThatPutStringWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertNull(headers.get("foo")); + headers.put("foo", "bar"); + assertEquals(Arrays.asList("bar"), headers.get("foo")); + headers.put("foo", "baz"); + assertEquals(Arrays.asList("baz"), headers.get("foo")); + } + + @Test + public void requireThatPutListWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertNull(headers.get("foo")); + headers.put("foo", Arrays.asList("bar")); + assertEquals(Arrays.asList("bar"), headers.get("foo")); + headers.put("foo", Arrays.asList("baz", "cox")); + assertEquals(Arrays.asList("baz", "cox"), headers.get("foo")); + } + + @Test + public void requireThatPutAllWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + headers.add("foo", "bar"); + headers.add("bar", "baz"); + assertEquals(Arrays.asList("bar"), headers.get("foo")); + assertEquals(Arrays.asList("baz"), headers.get("bar")); + + Map<String, List<String>> map = new HashMap<>(); + map.put("foo", Arrays.asList("baz", "cox")); + map.put("bar", Arrays.asList("cox")); + headers.putAll(map); + + assertEquals(Arrays.asList("baz", "cox"), headers.get("foo")); + assertEquals(Arrays.asList("cox"), headers.get("bar")); + } + + @Test + public void requireThatRemoveWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + headers.put("foo", Arrays.asList("bar", "baz")); + assertEquals(Arrays.asList("bar", "baz"), headers.get("foo")); + assertEquals(Arrays.asList("bar", "baz"), headers.remove("foo")); + assertNull(headers.get("foo")); + assertNull(headers.remove("foo")); + } + + @Test + public void requireThatRemoveStringWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + headers.put("foo", Arrays.asList("bar", "baz")); + assertEquals(Arrays.asList("bar", "baz"), headers.get("foo")); + assertTrue(headers.remove("foo", "bar")); + assertFalse(headers.remove("foo", "cox")); + assertEquals(Arrays.asList("baz"), headers.get("foo")); + assertTrue(headers.remove("foo", "baz")); + assertFalse(headers.remove("foo", "cox")); + assertNull(headers.get("foo")); + } + + @Test + public void requireThatClearWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + headers.add("foo", "bar"); + headers.add("bar", "baz"); + assertEquals(Arrays.asList("bar"), headers.get("foo")); + assertEquals(Arrays.asList("baz"), headers.get("bar")); + headers.clear(); + assertNull(headers.get("foo")); + assertNull(headers.get("bar")); + } + + @Test + public void requireThatGetWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertNull(headers.get("foo")); + headers.add("foo", "bar"); + assertEquals(Arrays.asList("bar"), headers.get("foo")); + } + + @Test + public void requireThatGetFirstWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertNull(headers.getFirst("foo")); + headers.add("foo", Arrays.asList("bar", "baz")); + assertEquals("bar", headers.getFirst("foo")); + } + + @Test + public void requireThatIsTrueWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertFalse(headers.isTrue("foo")); + headers.put("foo", Arrays.asList("true")); + assertTrue(headers.isTrue("foo")); + headers.put("foo", Arrays.asList("true", "true")); + assertTrue(headers.isTrue("foo")); + headers.put("foo", Arrays.asList("true", "false")); + assertFalse(headers.isTrue("foo")); + headers.put("foo", Arrays.asList("false", "true")); + assertFalse(headers.isTrue("foo")); + headers.put("foo", Arrays.asList("false", "false")); + assertFalse(headers.isTrue("foo")); + headers.put("foo", Arrays.asList("false")); + assertFalse(headers.isTrue("foo")); + } + + @Test + public void requireThatKeySetWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertTrue(headers.keySet().isEmpty()); + headers.add("foo", "bar"); + assertEquals(new HashSet<>(Arrays.asList("foo")), headers.keySet()); + headers.add("bar", "baz"); + assertEquals(new HashSet<>(Arrays.asList("foo", "bar")), headers.keySet()); + } + + @Test + public void requireThatValuesWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertTrue(headers.values().isEmpty()); + headers.add("foo", "bar"); + Collection<List<String>> values = headers.values(); + assertEquals(1, values.size()); + assertTrue(values.contains(Arrays.asList("bar"))); + + headers.add("bar", "baz"); + values = headers.values(); + assertEquals(2, values.size()); + assertTrue(values.contains(Arrays.asList("bar"))); + assertTrue(values.contains(Arrays.asList("baz"))); + } + + @Test + public void requireThatEntrySetWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertTrue(headers.entrySet().isEmpty()); + headers.put("foo", Arrays.asList("bar", "baz")); + + Set<Map.Entry<String, List<String>>> entries = headers.entrySet(); + assertEquals(1, entries.size()); + Map.Entry<String, List<String>> entry = entries.iterator().next(); + assertNotNull(entry); + assertEquals("foo", entry.getKey()); + assertEquals(Arrays.asList("bar", "baz"), entry.getValue()); + } + + @Test + public void requireThatEntriesWorksAsExpected() { + HeaderFields headers = new HeaderFields(); + assertTrue(headers.entries().isEmpty()); + headers.put("foo", Arrays.asList("bar", "baz")); + + List<Map.Entry<String, String>> entries = headers.entries(); + assertEquals(2, entries.size()); + + Map.Entry<String, String> entry = entries.get(0); + assertNotNull(entry); + assertEquals("foo", entry.getKey()); + assertEquals("bar", entry.getValue()); + + assertNotNull(entry = entries.get(1)); + assertEquals("foo", entry.getKey()); + assertEquals("baz", entry.getValue()); + } + + @Test + public void requireThatEntryIsUnmodifiable() { + HeaderFields headers = new HeaderFields(); + headers.put("foo", "bar"); + Map.Entry<String, String> entry = headers.entries().get(0); + try { + entry.setValue("baz"); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatEntriesAreUnmodifiable() { + HeaderFields headers = new HeaderFields(); + headers.put("foo", "bar"); + List<Map.Entry<String, String>> entries = headers.entries(); + try { + entries.add(new MyEntry()); + fail(); + } catch (UnsupportedOperationException e) { + + } + try { + entries.remove(new MyEntry()); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatEqualsWorksAsExpected() { + HeaderFields lhs = new HeaderFields(); + HeaderFields rhs = new HeaderFields(); + assertTrue(lhs.equals(rhs)); + lhs.add("foo", "bar"); + assertFalse(lhs.equals(rhs)); + rhs.add("foo", "bar"); + assertTrue(lhs.equals(rhs)); + } + + @Test + public void requireThatHashCodeWorksAsExpected() { + HeaderFields lhs = new HeaderFields(); + HeaderFields rhs = new HeaderFields(); + assertTrue(lhs.hashCode() == rhs.hashCode()); + lhs.add("foo", "bar"); + assertTrue(lhs.hashCode() != rhs.hashCode()); + rhs.add("foo", "bar"); + assertTrue(lhs.hashCode() == rhs.hashCode()); + } + + private static class MyEntry implements Map.Entry<String, String> { + + @Override + public String getKey() { + return "key"; + } + + @Override + public String getValue() { + return "value"; + } + + @Override + public String setValue(String value) { + return "value"; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ProxyRequestHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ProxyRequestHandlerTestCase.java new file mode 100644 index 00000000000..54a5a697791 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ProxyRequestHandlerTestCase.java @@ -0,0 +1,583 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ProxyRequestHandlerTestCase { + + @Test + public void requireThatRequestHandlerIsProxied() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Request request = newRequest(driver, requestHandler); + RequestHandler resolvedHandler = new ProxyRequestHandler(request.container().resolveHandler(request)); + MyResponseHandler responseHandler = MyResponseHandler.newEagerCompletion(); + resolvedHandler.handleRequest(request, responseHandler).close(null); + request.release(); + assertNotNull(requestHandler.handler); + resolvedHandler.handleTimeout(request, responseHandler); + assertTrue(requestHandler.timeout); + requestHandler.respond(); + + requestHandler.release(); + final ResourceReference resolvedHandlerReference = resolvedHandler.refer(); + assertTrue(driver.close()); // release installed ref + + assertFalse(requestHandler.destroyed); + resolvedHandlerReference.close(); + assertTrue(requestHandler.destroyed); + } + + @Test + public void requireThatRequestContentCompletedIsProxied() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newInstance(); + Request request = newRequest(driver, requestHandler); + ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion()); + request.release(); + + assertSame(request, requestHandler.request); + + ByteBuffer buf = ByteBuffer.allocate(69); + resolvedContent.write(buf, null); + assertSame(buf, requestHandler.content.writeBuf); + requestHandler.content.writeCompletion.completed(); + MyCompletion writeCompletion = new MyCompletion(); + resolvedContent.write(buf = ByteBuffer.allocate(69), writeCompletion); + assertSame(buf, requestHandler.content.writeBuf); + assertFalse(writeCompletion.completed); + assertNull(writeCompletion.failed); + requestHandler.content.writeCompletion.completed(); + assertTrue(writeCompletion.completed); + assertNull(writeCompletion.failed); + + MyCompletion closeCompletion = new MyCompletion(); + resolvedContent.close(closeCompletion); + assertTrue(requestHandler.content.closed); + assertFalse(closeCompletion.completed); + assertNull(writeCompletion.failed); + requestHandler.content.closeCompletion.completed(); + assertTrue(closeCompletion.completed); + assertNull(closeCompletion.failed); + + requestHandler.respond(); + assertTrue(driver.close()); + } + + @Test + public void requireThatRequestContentFailedIsProxied() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newInstance(); + Request request = newRequest(driver, requestHandler); + ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion()); + request.release(); + + assertSame(request, requestHandler.request); + + ByteBuffer buf = ByteBuffer.allocate(69); + resolvedContent.write(buf, null); + assertSame(buf, requestHandler.content.writeBuf); + requestHandler.content.writeCompletion.completed(); + MyCompletion writeCompletion = new MyCompletion(); + resolvedContent.write(buf = ByteBuffer.allocate(69), writeCompletion); + assertSame(buf, requestHandler.content.writeBuf); + assertFalse(writeCompletion.completed); + assertNull(writeCompletion.failed); + MyException writeFailed = new MyException(); + requestHandler.content.writeCompletion.failed(writeFailed); + assertFalse(writeCompletion.completed); + assertSame(writeFailed, writeCompletion.failed); + + MyCompletion closeCompletion = new MyCompletion(); + resolvedContent.close(closeCompletion); + assertTrue(requestHandler.content.closed); + assertFalse(closeCompletion.completed); + assertNull(closeCompletion.failed); + MyException closeFailed = new MyException(); + requestHandler.content.closeCompletion.failed(closeFailed); + assertFalse(writeCompletion.completed); + assertSame(closeFailed, closeCompletion.failed); + + requestHandler.respond(); + assertTrue(driver.close()); + } + + @Test + public void requireThatNullRequestContentIsProxied() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newNullContent(); + Request request = newRequest(driver, requestHandler); + request.connect(MyResponseHandler.newEagerCompletion()).close(null); + request.release(); + + requestHandler.respond(); + assertTrue(driver.close()); + } + + @Test + public void requireThatRequestWriteCompletionCanOnlyBeCalledOnce() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newInstance(); + Request request = newRequest(driver, requestHandler); + ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion()); + request.release(); + + CountingCompletionHandler completion = new CountingCompletionHandler(); + resolvedContent.write(ByteBuffer.allocate(0), completion); + assertEquals(0, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + requestHandler.content.writeCompletion.completed(); + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + try { + requestHandler.content.writeCompletion.completed(); + fail(); + } catch (IllegalStateException e) { + // ignore + } + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + try { + requestHandler.content.writeCompletion.failed(new Throwable()); + fail(); + } catch (IllegalStateException e) { + // ignore + } + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + + resolvedContent.close(null); + requestHandler.content.closeCompletion.completed(); + requestHandler.respond(); + assertTrue(driver.close()); + } + + @Test + public void requireThatRequestCloseCompletionCanOnlyBeCalledOnce() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newInstance(); + Request request = newRequest(driver, requestHandler); + ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion()); + request.release(); + + CountingCompletionHandler completion = new CountingCompletionHandler(); + resolvedContent.close(completion); + assertEquals(0, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + requestHandler.content.closeCompletion.completed(); + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + try { + requestHandler.content.closeCompletion.completed(); + fail(); + } catch (IllegalStateException e) { + // ignore + } + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + try { + requestHandler.content.closeCompletion.failed(new Throwable()); + fail(); + } catch (IllegalStateException e) { + // ignore + } + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + + requestHandler.respond(); + assertTrue(driver.close()); + } + + @Test + public void requireThatResponseContentCompletedIsProxied() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Request request = newRequest(driver, requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + request.connect(responseHandler).close(null); + request.release(); + Response response = new Response(Response.Status.OK); + ContentChannel resolvedContent = requestHandler.handler.handleResponse(response); + + assertSame(response, responseHandler.response); + + ByteBuffer buf = ByteBuffer.allocate(69); + resolvedContent.write(buf, null); + assertSame(buf, responseHandler.content.writeBuf); + responseHandler.content.writeCompletion.completed(); + MyCompletion writeCompletion = new MyCompletion(); + resolvedContent.write(buf = ByteBuffer.allocate(69), writeCompletion); + assertSame(buf, responseHandler.content.writeBuf); + assertFalse(writeCompletion.completed); + assertNull(writeCompletion.failed); + responseHandler.content.writeCompletion.completed(); + assertTrue(writeCompletion.completed); + assertNull(writeCompletion.failed); + + MyCompletion closeCompletion = new MyCompletion(); + resolvedContent.close(closeCompletion); + assertTrue(responseHandler.content.closed); + assertFalse(closeCompletion.completed); + assertNull(closeCompletion.failed); + responseHandler.content.closeCompletion.completed(); + assertTrue(closeCompletion.completed); + assertNull(closeCompletion.failed); + assertTrue(driver.close()); + } + + @Test + public void requireThatResponseContentFailedIsProxied() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Request request = newRequest(driver, requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + request.connect(responseHandler).close(null); + request.release(); + Response response = new Response(Response.Status.OK); + ContentChannel resolvedContent = requestHandler.handler.handleResponse(response); + + assertSame(response, responseHandler.response); + + ByteBuffer buf = ByteBuffer.allocate(69); + resolvedContent.write(buf, null); + assertSame(buf, responseHandler.content.writeBuf); + responseHandler.content.writeCompletion.completed(); + MyCompletion writeCompletion = new MyCompletion(); + resolvedContent.write(buf = ByteBuffer.allocate(69), writeCompletion); + assertSame(buf, responseHandler.content.writeBuf); + assertFalse(writeCompletion.completed); + assertNull(writeCompletion.failed); + MyException writeFailed = new MyException(); + responseHandler.content.writeCompletion.failed(writeFailed); + assertFalse(writeCompletion.completed); + assertSame(writeFailed, writeCompletion.failed); + + MyCompletion closeCompletion = new MyCompletion(); + resolvedContent.close(closeCompletion); + assertTrue(responseHandler.content.closed); + assertFalse(closeCompletion.completed); + assertNull(closeCompletion.failed); + MyException closeFailed = new MyException(); + responseHandler.content.closeCompletion.failed(closeFailed); + assertFalse(closeCompletion.completed); + assertSame(closeFailed, closeCompletion.failed); + assertTrue(driver.close()); + } + + @Test + public void requireThatNullResponseContentIsProxied() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Request request = newRequest(driver, requestHandler); + ResponseHandler responseHandler = new ResponseHandler() { + + @Override + public ContentChannel handleResponse(Response response) { + return null; + } + }; + request.connect(responseHandler).close(null); + requestHandler.handler.handleResponse(new Response(Response.Status.OK)).close(null); + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatResponseWriteCompletionCanOnlyBeCalledOnce() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + Request request = newRequest(driver, requestHandler); + request.connect(responseHandler).close(null); + request.release(); + ContentChannel resolvedContent = requestHandler.handler.handleResponse(new Response(Response.Status.OK)); + + CountingCompletionHandler completion = new CountingCompletionHandler(); + resolvedContent.write(ByteBuffer.allocate(0), completion); + assertEquals(0, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + responseHandler.content.writeCompletion.completed(); + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + try { + responseHandler.content.writeCompletion.completed(); + fail(); + } catch (IllegalStateException e) { + // ignore + } + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + try { + responseHandler.content.writeCompletion.failed(new Throwable()); + fail(); + } catch (IllegalStateException e) { + // ignore + } + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + + resolvedContent.close(null); + responseHandler.content.closeCompletion.completed(); + assertTrue(driver.close()); + } + + @Test + public void requireThatResponseCloseCompletionCanOnlyBeCalledOnce() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + Request request = newRequest(driver, requestHandler); + request.connect(responseHandler).close(null); + request.release(); + ContentChannel resolvedContent = requestHandler.handler.handleResponse(new Response(Response.Status.OK)); + + CountingCompletionHandler completion = new CountingCompletionHandler(); + resolvedContent.close(completion); + assertEquals(0, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + responseHandler.content.closeCompletion.completed(); + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + try { + responseHandler.content.closeCompletion.completed(); + fail(); + } catch (IllegalStateException e) { + // ignore + } + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + try { + responseHandler.content.closeCompletion.failed(new Throwable()); + fail(); + } catch (IllegalStateException e) { + // ignore + } + assertEquals(1, completion.numCompleted.get()); + assertEquals(0, completion.numFailed.get()); + assertTrue(driver.close()); + } + + @Test + public void requireThatUncaughtCompletionFailureIsLogged() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = MyRequestHandler.newInstance(); + Request request = newRequest(driver, requestHandler); + ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion()); + request.release(); + + MyLogHandler logHandler = new MyLogHandler(); + Logger.getLogger(ProxyRequestHandler.class.getName()).addHandler(logHandler); + + resolvedContent.write(ByteBuffer.allocate(69), null); + MyException writeFailed = new MyException(); + requestHandler.content.writeCompletion.failed(writeFailed); + assertNotNull(logHandler.record); + assertSame(writeFailed, logHandler.record.getThrown()); + + resolvedContent.close(null); + MyException closeFailed = new MyException(); + requestHandler.content.closeCompletion.failed(closeFailed); + assertNotNull(logHandler.record); + assertSame(closeFailed, logHandler.record.getThrown()); + + requestHandler.respond(); + assertTrue(driver.close()); + } + + private static Request newRequest(TestDriver driver, RequestHandler requestHandler) { + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://host/path", requestHandler); + driver.activateContainer(builder); + return new Request(driver, URI.create("http://host/path")); + } + + private static class MyException extends RuntimeException { + + } + + private static class MyCompletion implements CompletionHandler { + + boolean completed = false; + Throwable failed = null; + + @Override + public void completed() { + completed = true; + } + + @Override + public void failed(Throwable t) { + failed = t; + } + } + + private static class MyContent implements ContentChannel { + + final boolean eagerCompletion; + CompletionHandler writeCompletion = null; + CompletionHandler closeCompletion = null; + ByteBuffer writeBuf = null; + boolean closed = false; + + MyContent(boolean eagerCompletion) { + this.eagerCompletion = eagerCompletion; + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + writeBuf = buf; + writeCompletion = handler; + if (eagerCompletion) { + writeCompletion.completed(); + } + } + + @Override + public void close(CompletionHandler handler) { + closed = true; + closeCompletion = handler; + if (eagerCompletion) { + closeCompletion.completed(); + } + } + + static MyContent newInstance() { + return new MyContent(false); + } + + static MyContent newEagerCompletion() { + return new MyContent(true); + } + } + + private static class MyRequestHandler extends AbstractResource implements RequestHandler { + + final MyContent content; + Request request = null; + ResponseHandler handler = null; + boolean timeout = false; + boolean destroyed = false; + + MyRequestHandler(MyContent content) { + this.content = content; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + this.request = request; + this.handler = handler; + return content; + } + + @Override + public void handleTimeout(Request request, ResponseHandler handler) { + timeout = true; + } + + @Override + public void destroy() { + destroyed = true; + } + + void respond() { + handler.handleResponse(new Response(Response.Status.OK)).close(null); + } + + static MyRequestHandler newInstance() { + return new MyRequestHandler(MyContent.newInstance()); + } + + static MyRequestHandler newEagerCompletion() { + return new MyRequestHandler(MyContent.newEagerCompletion()); + } + + static MyRequestHandler newNullContent() { + return new MyRequestHandler(null); + } + } + + private static class MyResponseHandler implements ResponseHandler { + + final MyContent content; + Response response; + + MyResponseHandler(MyContent content) { + this.content = content; + } + + @Override + public ContentChannel handleResponse(Response response) { + this.response = response; + return content; + } + + static MyResponseHandler newInstance() { + return new MyResponseHandler(MyContent.newInstance()); + } + + static MyResponseHandler newEagerCompletion() { + return new MyResponseHandler(MyContent.newEagerCompletion()); + } + } + + private static class MyLogHandler extends Handler { + + LogRecord record; + + @Override + public void publish(LogRecord record) { + this.record = record; + } + + @Override + public void flush() { + + } + + @Override + public void close() throws SecurityException { + + } + } + + private static class CountingCompletionHandler implements CompletionHandler { + + final AtomicInteger numCompleted = new AtomicInteger(0); + final AtomicInteger numFailed = new AtomicInteger(0); + + @Override + public void completed() { + numCompleted.incrementAndGet(); + } + + @Override + public void failed(Throwable t) { + numFailed.incrementAndGet(); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencedResourceTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencedResourceTestCase.java new file mode 100644 index 00000000000..4337d8b8c6c --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencedResourceTestCase.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class ReferencedResourceTestCase { + @Test + public void requireThatGettersMatchConstructor() { + final SharedResource resource = mock(SharedResource.class); + final ResourceReference reference = mock(ResourceReference.class); + final ReferencedResource<SharedResource> referencedResource = new ReferencedResource<>(resource, reference); + assertThat(referencedResource.getResource(), is(sameInstance(resource))); + assertThat(referencedResource.getReference(), is(sameInstance(reference))); + } + + @Test + public void requireThatCloseCallsReferenceClose() { + final SharedResource resource = mock(SharedResource.class); + final ResourceReference reference = mock(ResourceReference.class); + final ReferencedResource<SharedResource> referencedResource = new ReferencedResource<>(resource, reference); + referencedResource.close(); + verify(reference, times(1)).close(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencesTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencesTestCase.java new file mode 100644 index 00000000000..d8cbe36c04f --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencesTestCase.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class ReferencesTestCase { + @Test + public void requireThatFromResourceCallsReleaseOnResource() { + final SharedResource resource = mock(SharedResource.class); + final ResourceReference reference = References.fromResource(resource); + reference.close(); + verify(resource, times(1)).release(); + } + + @Test + public void requireThatNoopReferenceCanBeCalledMultipleTimes() { + References.NOOP_REFERENCE.close(); + References.NOOP_REFERENCE.close(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/RequestTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/RequestTestCase.java new file mode 100644 index 00000000000..fe5af79a6d3 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/RequestTestCase.java @@ -0,0 +1,381 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Key; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.jdisc.application.UriPattern; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.BindingSetNotFoundException; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RequestTestCase { + + @Test + public void requireThatAccessorsWork() throws BindingSetNotFoundException { + MyTimer timer = new MyTimer(); + timer.currentTime = 69; + + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer); + driver.activateContainer(driver.newContainerBuilder()); + Request request = new Request(driver, URI.create("http://foo/bar")); + assertNotNull(request); + assertEquals(URI.create("http://foo/bar"), request.getUri()); + request.setUri(URI.create("http://baz/cox")); + assertEquals(URI.create("http://baz/cox"), request.getUri()); + assertTrue(request.isServerRequest()); + request.setServerRequest(false); + assertFalse(request.isServerRequest()); + assertEquals(69, request.creationTime(TimeUnit.MILLISECONDS)); + assertNull(request.getTimeout(TimeUnit.MILLISECONDS)); + request.setTimeout(10, TimeUnit.MILLISECONDS); + assertNotNull(request.getTimeout(TimeUnit.MILLISECONDS)); + assertEquals(10, request.timeRemaining(TimeUnit.MILLISECONDS).longValue()); + assertTrue(request.context().isEmpty()); + assertTrue(request.headers().isEmpty()); + TimeoutManager timeoutManager = new MyTimeoutManager(); + request.setTimeoutManager(timeoutManager); + assertSame(timeoutManager, request.getTimeoutManager()); + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatCancelWorks() { + MyTimer timer = new MyTimer(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer); + Request request = newRequest(driver); + assertFalse(request.isCancelled()); + request.cancel(); + assertTrue(request.isCancelled()); + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatDefaultTimeoutIsInfinite() { + MyTimer timer = new MyTimer(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer); + Request request = newRequest(driver); + assertNull(request.getTimeout(TimeUnit.MILLISECONDS)); + assertNull(request.timeRemaining(TimeUnit.MILLISECONDS)); + assertFalse(request.isCancelled()); + timer.currentTime = Long.MAX_VALUE; + assertFalse(request.isCancelled()); + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatTimeRemainingUsesTimer() { + MyTimer timer = new MyTimer(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer); + Request request = newRequest(driver); + request.setTimeout(10, TimeUnit.MILLISECONDS); + for (timer.currentTime = 0; timer.currentTime <= request.getTimeout(TimeUnit.MILLISECONDS); + ++timer.currentTime) + { + assertEquals(request.getTimeout(TimeUnit.MILLISECONDS) - timer.currentTime, + request.timeRemaining(TimeUnit.MILLISECONDS).longValue()); + } + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatTimeoutCausesCancel() { + MyTimer timer = new MyTimer(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer); + Request request = newRequest(driver); + request.setTimeout(10, TimeUnit.MILLISECONDS); + assertFalse(request.isCancelled()); + timer.currentTime = 10; + assertTrue(request.isCancelled()); + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatCancelIsTrueIfParentIsCancelled() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + Request parent = newRequest(driver); + Request child = new Request(parent, URI.create("http://localhost/")); + parent.cancel(); + assertTrue(child.isCancelled()); + parent.release(); + child.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatDestroyReleasesContainer() { + final MyContainer container = new MyContainer(); + Request request = new Request(new CurrentContainer() { + + @Override + public Container newReference(URI uri) { + return container; + } + }, URI.create("http://localhost/")); + assertEquals(1, container.refCount); + request.release(); + assertEquals(0, container.refCount); + } + + @Test + public void requireThatServerConnectResolvesToServerBinding() { + MyContainer container = new MyContainer(); + Request request = new Request(container, URI.create("http://localhost/")); + request.connect(new MyResponseHandler()); + assertNotNull(container.asServer); + assertTrue(container.asServer); + } + + @Test + public void requireThatClientConnectResolvesToClientBinding() { + MyContainer container = new MyContainer(); + Request serverReq = new Request(container, URI.create("http://localhost/")); + Request clientReq = new Request(serverReq, URI.create("http://localhost/")); + clientReq.connect(new MyResponseHandler()); + assertNotNull(container.asServer); + assertFalse(container.asServer); + } + + @Test + public void requireThatNullTimeoutManagerThrowsException() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + Request request = newRequest(driver); + + try { + request.setTimeoutManager(null); + fail(); + } catch (NullPointerException e) { + + } + + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatTimeoutManagerCanNotBeReplaced() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + Request request = newRequest(driver); + + TimeoutManager manager = new MyTimeoutManager(); + request.setTimeoutManager(manager); + try { + request.setTimeoutManager(manager); + fail(); + } catch (IllegalStateException e) { + assertEquals("Timeout manager already set.", e.getMessage()); + } + + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatSetTimeoutCallsTimeoutManager() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + Request request = newRequest(driver); + + MyTimeoutManager timeoutManager = new MyTimeoutManager(); + request.setTimeoutManager(timeoutManager); + request.setTimeout(6, TimeUnit.SECONDS); + assertEquals(6000, timeoutManager.timeoutMillis); + + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatSetTimeoutManagerPropagatesCurrentTimeout() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + Request request = newRequest(driver); + + MyTimeoutManager timeoutManager = new MyTimeoutManager(); + request.setTimeout(6, TimeUnit.SECONDS); + request.setTimeoutManager(timeoutManager); + assertEquals(6000, timeoutManager.timeoutMillis); + + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatUriIsNormalized() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + + assertUri(driver, "http://host/foo", "http://host/foo"); + assertUri(driver, "http://host/./foo", "http://host/foo"); + assertUri(driver, "http://host/././foo", "http://host/foo"); + assertUri(driver, "http://host/foo/", "http://host/foo/"); + assertUri(driver, "http://host/foo/.", "http://host/foo/"); + assertUri(driver, "http://host/foo/./", "http://host/foo/"); + assertUri(driver, "http://host/foo/./.", "http://host/foo/"); + assertUri(driver, "http://host/foo/././", "http://host/foo/"); + assertUri(driver, "http://host/foo/..", "http://host/"); + assertUri(driver, "http://host/foo/../", "http://host/"); + assertUri(driver, "http://host/foo/../bar", "http://host/bar"); + assertUri(driver, "http://host/foo/../bar/", "http://host/bar/"); + assertUri(driver, "http://host//foo//", "http://host/foo/"); + assertUri(driver, "http://host///foo///", "http://host/foo/"); + assertUri(driver, "http://host///foo///bar///", "http://host/foo/bar/"); + + assertTrue(driver.close()); + } + + private static void assertUri(CurrentContainer container, String requestUri, String expectedUri) { + Request serverReq = new Request(container, URI.create(requestUri)); + assertEquals(expectedUri, serverReq.getUri().toString()); + + serverReq.setUri(URI.create(requestUri)); + assertEquals(expectedUri, serverReq.getUri().toString()); + + Request clientReq = new Request(serverReq, URI.create(requestUri)); + assertEquals(expectedUri, clientReq.getUri().toString()); + + serverReq.release(); + clientReq.release(); + } + + private static Request newRequest(TestDriver driver) { + driver.activateContainer(driver.newContainerBuilder()); + return new Request(driver, URI.create("http://host/path")); + } + + private static class MyTimer extends AbstractModule implements Timer { + + long currentTime = 0; + + @Override + public long currentTimeMillis() { + return currentTime; + } + + @Override + protected void configure() { + bind(Timer.class).toInstance(this); + } + } + + private static class MyContainer implements CurrentContainer, Container { + + Boolean asServer = null; + int refCount = 1; + + @Override + public Container newReference(URI uri) { + return this; + } + + @Override + public RequestHandler resolveHandler(Request request) { + this.asServer = request.isServerRequest(); + RequestHandler requestHandler = new MyRequestHandler(); + request.setBindingMatch(new BindingMatch<>( + new UriPattern("http://*/*").match(request.getUri()), + requestHandler)); + return requestHandler; + } + + @Override + public <T> T getInstance(Key<T> key) { + return Guice.createInjector().getInstance(key); + } + + @Override + public <T> T getInstance(Class<T> type) { + return Guice.createInjector().getInstance(type); + } + + @Override + public ResourceReference refer() { + ++refCount; + return new ResourceReference() { + @Override + public void close() { + --refCount; + } + }; + } + + @Override + public void release() { + --refCount; + } + + @Override + public long currentTimeMillis() { + return 0; + } + } + + private static class MyRequestHandler extends NoopSharedResource implements RequestHandler { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + return new MyContentChannel(); + } + + @Override + public void handleTimeout(Request request, ResponseHandler handler) { + + } + } + + private static class MyResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(Response response) { + return null; + } + } + + private static class MyContentChannel implements ContentChannel { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + + } + + @Override + public void close(CompletionHandler handler) { + + } + } + + private static class MyTimeoutManager implements TimeoutManager { + + long timeoutMillis; + + @Override + public void scheduleTimeout(Request request) { + timeoutMillis = request.getTimeout(TimeUnit.MILLISECONDS); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ResponseTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ResponseTestCase.java new file mode 100644 index 00000000000..303d6b0dc55 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ResponseTestCase.java @@ -0,0 +1,86 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ResponseTestCase { + + @Test + public void requireThatAccessorsWork() { + Response response = new Response(69); + assertEquals(69, response.getStatus()); + response.setStatus(96); + assertEquals(96, response.getStatus()); + Throwable t = new Throwable(); + response.setError(t); + assertSame(t, response.getError()); + assertTrue(response.context().isEmpty()); + assertTrue(response.headers().isEmpty()); + } + + @Test + public void requireThatStatusCodesDoNotChange() { + assertEquals(100, Response.Status.CONTINUE); + assertEquals(101, Response.Status.SWITCHING_PROTOCOLS); + assertEquals(102, Response.Status.PROCESSING); + + assertEquals(200, Response.Status.OK); + assertEquals(201, Response.Status.CREATED); + assertEquals(202, Response.Status.ACCEPTED); + assertEquals(203, Response.Status.NON_AUTHORITATIVE_INFORMATION); + assertEquals(204, Response.Status.NO_CONTENT); + assertEquals(205, Response.Status.RESET_CONTENT); + assertEquals(206, Response.Status.PARTIAL_CONTENT); + assertEquals(207, Response.Status.MULTI_STATUS); + + assertEquals(300, Response.Status.MULTIPLE_CHOICES); + assertEquals(301, Response.Status.MOVED_PERMANENTLY); + assertEquals(302, Response.Status.FOUND); + assertEquals(303, Response.Status.SEE_OTHER); + assertEquals(304, Response.Status.NOT_MODIFIED); + assertEquals(305, Response.Status.USE_PROXY); + assertEquals(307, Response.Status.TEMPORARY_REDIRECT); + + assertEquals(400, Response.Status.BAD_REQUEST); + assertEquals(401, Response.Status.UNAUTHORIZED); + assertEquals(402, Response.Status.PAYMENT_REQUIRED); + assertEquals(403, Response.Status.FORBIDDEN); + assertEquals(404, Response.Status.NOT_FOUND); + assertEquals(405, Response.Status.METHOD_NOT_ALLOWED); + assertEquals(406, Response.Status.NOT_ACCEPTABLE); + assertEquals(407, Response.Status.PROXY_AUTHENTICATION_REQUIRED); + assertEquals(408, Response.Status.REQUEST_TIMEOUT); + assertEquals(409, Response.Status.CONFLICT); + assertEquals(410, Response.Status.GONE); + assertEquals(411, Response.Status.LENGTH_REQUIRED); + assertEquals(412, Response.Status.PRECONDITION_FAILED); + assertEquals(413, Response.Status.REQUEST_TOO_LONG); + assertEquals(414, Response.Status.REQUEST_URI_TOO_LONG); + assertEquals(415, Response.Status.UNSUPPORTED_MEDIA_TYPE); + assertEquals(416, Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE); + assertEquals(417, Response.Status.EXPECTATION_FAILED); + assertEquals(419, Response.Status.INSUFFICIENT_SPACE_ON_RESOURCE); + assertEquals(420, Response.Status.METHOD_FAILURE); + assertEquals(422, Response.Status.UNPROCESSABLE_ENTITY); + assertEquals(423, Response.Status.LOCKED); + assertEquals(424, Response.Status.FAILED_DEPENDENCY); + + assertEquals(505, Response.Status.VERSION_NOT_SUPPORTED); + assertEquals(500, Response.Status.INTERNAL_SERVER_ERROR); + assertEquals(501, Response.Status.NOT_IMPLEMENTED); + assertEquals(502, Response.Status.BAD_GATEWAY); + assertEquals(503, Response.Status.SERVICE_UNAVAILABLE); + assertEquals(504, Response.Status.GATEWAY_TIMEOUT); + assertEquals(505, Response.Status.VERSION_NOT_SUPPORTED); + assertEquals(507, Response.Status.INSUFFICIENT_STORAGE); + } + +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/AbstractApplicationTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/AbstractApplicationTestCase.java new file mode 100644 index 00000000000..da5f046ef1f --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/AbstractApplicationTestCase.java @@ -0,0 +1,98 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.Inject; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class AbstractApplicationTestCase { + + @Test + public void requireThatContainerApiIsAvailable() { + TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.class); + MyApplication app = (MyApplication)driver.application(); + app.activateContainer(app.newContainerBuilder()); + assertNotNull(app.container()); + assertTrue(driver.close()); + } + + @Test + public void requireThatDestroySignalsTermination() { + TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.class); + MyApplication app = (MyApplication)driver.application(); + assertFalse(app.isTerminated()); + assertTrue(driver.close()); + assertTrue(app.isTerminated()); + } + + @Test + public void requireThatTerminationCanBeWaitedForWithTimeout() throws InterruptedException { + TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.class); + final MyApplication app = (MyApplication)driver.application(); + final CountDownLatch latch = new CountDownLatch(1); + Executors.newSingleThreadExecutor().execute(new Runnable() { + + @Override + public void run() { + try { + app.awaitTermination(600, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + latch.countDown(); + } + }); + assertFalse(latch.await(100, TimeUnit.MILLISECONDS)); + assertTrue(driver.close()); + assertTrue(latch.await(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatTerminationCanBeWaitedForWithoutTimeout() throws InterruptedException { + TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.class); + final MyApplication app = (MyApplication)driver.application(); + final CountDownLatch latch = new CountDownLatch(1); + Executors.newSingleThreadExecutor().execute(new Runnable() { + + @Override + public void run() { + try { + app.awaitTermination(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + latch.countDown(); + } + }); + assertFalse(latch.await(100, TimeUnit.MILLISECONDS)); + assertTrue(driver.close()); + assertTrue(latch.await(600, TimeUnit.SECONDS)); + } + + private static class MyApplication extends AbstractApplication { + + @Inject + public MyApplication(BundleInstaller bundleInstaller, ContainerActivator activator, + CurrentContainer container) { + super(bundleInstaller, activator, container); + } + + @Override + public void start() { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ApplicationNotReadyTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ApplicationNotReadyTestCase.java new file mode 100644 index 00000000000..1351e717015 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ApplicationNotReadyTestCase.java @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.Inject; +import com.google.inject.ProvisionException; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ApplicationNotReadyTestCase { + + @Test + public void requireThatExceptionIsThrown() { + try { + TestDriver.newInjectedApplicationInstanceWithoutOsgi(MyApplication.class); + fail(); + } catch (ProvisionException e) { + Throwable t = e.getCause(); + assertNotNull(t); + assertTrue(t instanceof ApplicationNotReadyException); + } + } + + private static class MyApplication implements Application { + + @Inject + MyApplication(ContainerActivator activator) { + activator.activateContainer(activator.newContainerBuilder()); + } + + @Override + public void start() { + + } + + @Override + public void stop() { + + } + + @Override + public void destroy() { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingMatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingMatchTestCase.java new file mode 100644 index 00000000000..59d0535f99e --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingMatchTestCase.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.jdisc.application; + +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BindingMatchTestCase { + + @Test + public void requireThatAccessorsWork() { + Object obj = new Object(); + BindingMatch<Object> match = new BindingMatch<>( + new UriPattern("http://*/*").match(URI.create("http://localhost:69/status.html")), + obj); + assertSame(obj, match.target()); + assertEquals(3, match.groupCount()); + assertEquals("localhost", match.group(0)); + assertEquals("69", match.group(1)); + assertEquals("status.html", match.group(2)); + } + + @Test + public void requireThatConstructorArgumentsCanNotBeNull() { + try { + new BindingMatch<>(null, null); + fail(); + } catch (NullPointerException e) { + + } + try { + new BindingMatch<>(new UriPattern("http://*/*").match(URI.create("http://localhost/")), null); + fail(); + } catch (NullPointerException e) { + + } + try { + new BindingMatch<>(null, new Object()); + fail(); + } catch (NullPointerException e) { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingRepositoryTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingRepositoryTestCase.java new file mode 100644 index 00000000000..aa9bc783b74 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingRepositoryTestCase.java @@ -0,0 +1,181 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.test.NonWorkingRequestHandler; +import org.junit.Test; + +import java.net.URI; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BindingRepositoryTestCase { + + @Test + public void requireThatRepositoryCanBeActivated() { + BindingRepository<Object> bindings = new BindingRepository<>(); + bindings.bind("http://host/path", new Object()); + + BindingSet<Object> set = bindings.activate(); + assertNotNull(set); + Iterator<Map.Entry<UriPattern, Object>> it = set.iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + assertNotNull(it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatActivationIsSnapshotOfRepository() { + BindingRepository<Object> bindings = new BindingRepository<>(); + bindings.bind("http://host/path", new Object()); + + BindingSet<Object> set = bindings.activate(); + assertNotNull(set); + bindings.clear(); + + Iterator<Map.Entry<UriPattern, Object>> it = set.iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + assertNotNull(it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatObjectsCanBeBound() { + BindingRepository<Object> bindings = new BindingRepository<>(); + Object foo = new Object(); + Object bar = new Object(); + bindings.bind("http://host/foo", foo); + bindings.bind("http://host/bar", bar); + + Iterator<Map.Entry<UriPattern, Object>> it = bindings.activate().iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + Map.Entry<UriPattern, Object> entry = it.next(); + assertNotNull(entry); + assertEquals(new UriPattern("http://host/foo"), entry.getKey()); + assertSame(foo, entry.getValue()); + assertTrue(it.hasNext()); + assertNotNull(entry = it.next()); + assertEquals(new UriPattern("http://host/bar"), entry.getKey()); + assertSame(bar, entry.getValue()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatPatternCannotBeStolen() { + final String pattern = "http://host/path"; + final RequestHandler originallyBoundHandler = new NonWorkingRequestHandler(); + + BindingRepository<Object> bindings = new BindingRepository<>(); + bindings.bind(pattern, originallyBoundHandler); + bindings.bind(pattern, new PatternStealingRequestHandler()); + + BindingSet bindingSet = bindings.activate(); + assertEquals(originallyBoundHandler, bindingSet.resolve(URI.create(pattern))); + } + + @Test + public void requireThatBindAllMethodWorks() { + Object foo = new Object(); + Object bar = new Object(); + Object baz = new Object(); + + Map<String, Object> toAdd = new HashMap<>(); + toAdd.put("http://host/foo", foo); + toAdd.put("http://host/bar", bar); + + BindingRepository<Object> addTo = new BindingRepository<>(); + addTo.bind("http://host/baz", baz); + addTo.bindAll(toAdd); + + Iterator<Map.Entry<UriPattern, Object>> it = addTo.activate().iterator(); + Map.Entry<UriPattern, Object> entry = it.next(); + assertNotNull(entry); + assertEquals(new UriPattern("http://host/foo"), entry.getKey()); + assertSame(foo, entry.getValue()); + assertTrue(it.hasNext()); + assertNotNull(entry = it.next()); + assertEquals(new UriPattern("http://host/baz"), entry.getKey()); + assertSame(baz, entry.getValue()); + assertTrue(it.hasNext()); + assertNotNull(entry = it.next()); + assertEquals(new UriPattern("http://host/bar"), entry.getKey()); + assertSame(bar, entry.getValue()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatPutAllMethodWorks() { + Object foo = new Object(); + Object bar = new Object(); + Object baz = new Object(); + + BindingRepository<Object> toAdd = new BindingRepository<>(); + toAdd.bind("http://host/foo", foo); + toAdd.bind("http://host/bar", bar); + + BindingRepository<Object> addTo = new BindingRepository<>(); + addTo.bind("http://host/baz", baz); + addTo.putAll(toAdd); + + Iterator<Map.Entry<UriPattern, Object>> it = addTo.activate().iterator(); + assertTrue(it.hasNext()); + Map.Entry<UriPattern, Object> entry = it.next(); + assertNotNull(entry); + assertEquals(new UriPattern("http://host/foo"), entry.getKey()); + assertSame(foo, entry.getValue()); + assertTrue(it.hasNext()); + assertNotNull(entry = it.next()); + assertEquals(new UriPattern("http://host/baz"), entry.getKey()); + assertSame(baz, entry.getValue()); + assertTrue(it.hasNext()); + assertNotNull(entry = it.next()); + assertEquals(new UriPattern("http://host/bar"), entry.getKey()); + assertSame(bar, entry.getValue()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatPutNullThrowsException() { + try { + new BindingRepository<>().put(null, new Object()); + fail(); + } catch (NullPointerException e) { + + } + try { + new BindingRepository<>().put(new UriPattern("http://host/foo"), null); + fail(); + } catch (NullPointerException e) { + + } + } + + static class PatternStealingRequestHandler extends NoopSharedResource implements RequestHandler { + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public void handleTimeout(Request request, ResponseHandler handler) { } + + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingSetTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingSetTestCase.java new file mode 100644 index 00000000000..7ff2aa6908b --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingSetTestCase.java @@ -0,0 +1,506 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.test.NonWorkingRequestHandler; +import static com.yahoo.vespa.defaults.Defaults.getDefaults; +import org.junit.Test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BindingSetTestCase { + + @Test + public void requireThatAccessorsWork() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host/foo"), foo); + RequestHandler bar = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host/bar"), bar); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + + Iterator<Map.Entry<UriPattern, RequestHandler>> it = bindings.iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + Map.Entry<UriPattern, RequestHandler> entry = it.next(); + assertNotNull(entry); + assertEquals(new UriPattern("http://host/foo"), entry.getKey()); + assertSame(foo, entry.getValue()); + assertTrue(it.hasNext()); + assertNotNull(entry = it.next()); + assertEquals(new UriPattern("http://host/bar"), entry.getKey()); + assertSame(bar, entry.getValue()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatSimpleResolutionWorks() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host/foo"), foo); + RequestHandler bar = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host/bar"), bar); + + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + BindingMatch<RequestHandler> match = bindings.match(URI.create("http://host/foo")); + assertNotNull(match); + assertEquals(0, match.groupCount()); + assertSame(foo, match.target()); + assertSame(foo, bindings.resolve(URI.create("http://host/foo"))); + + assertNotNull(match = bindings.match(URI.create("http://host/bar"))); + assertEquals(0, match.groupCount()); + assertSame(bar, match.target()); + assertSame(bar, bindings.resolve(URI.create("http://host/bar"))); + } + + @Test + public void requireThatPatternResolutionWorks() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host/*"), foo); + RequestHandler bar = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host/path"), bar); + + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + BindingMatch<RequestHandler> match = bindings.match(URI.create("http://host/anon")); + assertNotNull(match); + assertEquals(1, match.groupCount()); + assertEquals("anon", match.group(0)); + assertSame(foo, match.target()); + assertSame(foo, bindings.resolve(URI.create("http://host/anon"))); + + assertNotNull(match = bindings.match(URI.create("http://host/path"))); + assertEquals(0, match.groupCount()); + assertSame(bar, match.target()); + assertSame(bar, bindings.resolve(URI.create("http://host/path"))); + } + + @Test + public void requireThatPatternResolutionWorksForWildCards() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host:*/bar"), foo); + RequestHandler bob = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://*abc:*/*bar"), bob); + RequestHandler car = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("*://*:21/*"), car); + + + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + BindingMatch<RequestHandler> match = bindings.match(URI.create("http://host:8080/bar")); + assertNotNull(match); + assertEquals(1, match.groupCount()); + assertEquals("8080", match.group(0)); + assertSame(foo, match.target()); + assertSame(foo, bindings.resolve(URI.create("http://host:8080/bar"))); + + match = bindings.match(URI.create("http://host:8080/foo/bar")); + assertNull(match); + + match = bindings.match(URI.create("http://xyzabc:8080/pqrbar")); + assertNotNull(match); + assertSame(bob, match.target()); + + match = bindings.match(URI.create("ftp://lmn:21/abc")); + assertNotNull(match); + assertSame(car, match.target()); + } + + @Test + public void requireThatPatternResolutionWorksForFilters() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://*/filtered/*"), foo); + + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + BindingMatch<RequestHandler> match = bindings.match(URI.create("http://localhost:80/status.html")); + assertNull(match); + match = bindings.match(URI.create("http://localhost/filtered/status.html")); + assertNotNull(match); + assertSame(foo, match.target()); + } + + @Test + public void requireThatTreeSplitCanBeBoundForSchemes() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler httpfoo = new NonWorkingRequestHandler(); + RequestHandler httpsfoo = new NonWorkingRequestHandler(); + RequestHandler ftpfoo = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host/foo"), httpfoo); + handlers.put(new UriPattern("https://host/foo"), httpsfoo); + handlers.put(new UriPattern("ftp://host/foo"), ftpfoo); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + } + + @Test + public void requireThatTreeSplitCanBeBoundForHosts() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler foobar = new NonWorkingRequestHandler(); + RequestHandler fooqux = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://hostabc/foo"), foobar); + handlers.put(new UriPattern("http://hostpqr/foo"), fooqux); + handlers.put(new UriPattern("http://host/foo"), foo); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + } + + @Test + public void requireThatTreeSplitCanBeBoundForPorts() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo8080 = new NonWorkingRequestHandler(); + RequestHandler foo80 = new NonWorkingRequestHandler(); + RequestHandler foobar = new NonWorkingRequestHandler(); + RequestHandler foopqrbar = new NonWorkingRequestHandler(); + + handlers.put(new UriPattern("http://host:8080/foo"), foo8080); + handlers.put(new UriPattern("http://host:70/foo"), foo80); + handlers.put(new UriPattern("http://hostpqr:70/foo"), foopqrbar); + handlers.put(new UriPattern("http://host:80/foobar"), foobar); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + } + + @Test + public void requireThatTreeSplitCanBeBoundForPaths() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler foobar = new NonWorkingRequestHandler(); + RequestHandler fooqux = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host/foobar"), foobar); + handlers.put(new UriPattern("http://host/fooqux"), fooqux); + handlers.put(new UriPattern("http://host/foo"), foo); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + } + + @Test + public void requireThatTreeSplitCanBeBoundForWildcards() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo8080 = new NonWorkingRequestHandler(); + RequestHandler foo80 = new NonWorkingRequestHandler(); + RequestHandler foobar = new NonWorkingRequestHandler(); + RequestHandler foopqrbar = new NonWorkingRequestHandler(); + + handlers.put(new UriPattern("http://host:8080/foo"), foo8080); + handlers.put(new UriPattern("http://host:708/foo"), foo80); + handlers.put(new UriPattern("http://host:80/foobar"), foobar); + handlers.put(new UriPattern("http://hos*:708/foo"), foopqrbar); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + assertSame(foopqrbar, bindings.resolve(URI.create("http://hostabc:708/foo"))); + assertSame(foo80, bindings.resolve(URI.create("http://host:708/foo"))); + assertSame(foo8080, bindings.resolve(URI.create("http://host:8080/foo"))); + } + + @Test + public void requireThatTreeWorksForURIWithQueryOrFragments() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://*/application/v1/session"), foo); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + assertSame(foo, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v1/session?name=base"))); + assertSame(foo, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v1/session#application"))); + } + + @Test + public void requireThatTreeWorksForURIWithPathWildCards() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler foo1 = new NonWorkingRequestHandler(); + RequestHandler foo2 = new NonWorkingRequestHandler(); + RequestHandler foo3 = new NonWorkingRequestHandler(); + RequestHandler foo4 = new NonWorkingRequestHandler(); + RequestHandler foo5 = new NonWorkingRequestHandler(); + RequestHandler foo6 = new NonWorkingRequestHandler(); + RequestHandler foo7 = new NonWorkingRequestHandler(); + RequestHandler foo8 = new NonWorkingRequestHandler(); + RequestHandler foo9 = new NonWorkingRequestHandler(); + RequestHandler foo10 = new NonWorkingRequestHandler(); + RequestHandler foo11 = new NonWorkingRequestHandler(); + RequestHandler foo12 = new NonWorkingRequestHandler(); + RequestHandler foo13 = new NonWorkingRequestHandler(); + RequestHandler foo14 = new NonWorkingRequestHandler(); + RequestHandler foo15 = new NonWorkingRequestHandler(); + + handlers.put(new UriPattern("http://*/config/v1/*"), foo); + handlers.put(new UriPattern("http://*/config/v1/*/"), foo1); + handlers.put(new UriPattern("http://*/config/v1/*/*"), foo2); + handlers.put(new UriPattern("http://*/config/v1/*/*/"), foo3); + handlers.put(new UriPattern("http://*/application/v2/tenant/"), foo4); + handlers.put(new UriPattern("http://*/application/v2/tenant/*"), foo5); + handlers.put(new UriPattern("http://*/application/v2/tenant/*/session"), foo6); + handlers.put(new UriPattern("http://*/application/v2/tenant/*/session/*/prepared"), foo7); + handlers.put(new UriPattern("http://*/application/v2/tenant/*/session/*/active"), foo8); + handlers.put(new UriPattern("http://*/application/v2/tenant/*/session/*/content/*"), foo9); + handlers.put(new UriPattern("http://*/application/v2/tenant/*/application/"), foo10); + handlers.put(new UriPattern("http://*/application/v2/tenant/*/application/*/environment/*/" + + "region/*/instance/*/content/*"), foo11); + handlers.put(new UriPattern("http://*/config/v2/tenant/*/application/*/*"), foo12); + handlers.put(new UriPattern("http://*/config/v2/tenant/*/application/*/*/*"), foo13); + handlers.put(new UriPattern("http://*/config/v2/tenant/*/application/*/environment" + + "/*/region/*/instance/*/*"), foo14); + handlers.put(new UriPattern("http://*/config/v2/tenant/*/application/*/*/*/"), foo15); + + + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + assertSame(foo, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/config/v1/cloud.config.log.logd"))); + assertSame(foo1, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/config/v1/cloud.config.log.logd/"))); + assertSame(foo2, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/config/v1/cloud.config.log.logd/admin"))); + assertSame(foo3, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/config/v1/cloud.config.log.logd/admin/"))); + assertSame(foo4, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v2/tenant/"))); + assertSame(foo5, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v2/tenant/b"))); + assertSame(foo6, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v2/tenant/bar/session"))); + assertSame(foo7, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v2/tenant/bar/session/aef/prepared"))); + assertSame(foo8, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v2/tenant/bar/session/a/active"))); + assertSame(foo9, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v2/tenant/bar/session/aef/content/x"))); + assertSame(foo10, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v2/tenant/bar/session/application/"))); + assertSame(foo11, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/application/v2/tenant/bar/application/bbc/environment/xyz/region/m/inst" + + "ance/a/content/l"))); + assertSame(foo12, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/config/v2/tenant/bar/application/bbc/xyz"))); + assertSame(foo13, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/config/v2/tenant/bar/application/bbc/xyz/a"))); + assertSame(foo14, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/config/v2/tenant/bar/application/bbc/environment/a/region/b/instance/a/b"))); + assertSame(foo15, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" + + "/config/v2/tenant/bar/application/bbc/xyz/a/c/"))); + } + + @Test + public void requireThatPathOverPortWorks() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler applicationStatus = new NonWorkingRequestHandler(); + RequestHandler search = new NonWorkingRequestHandler(); + RequestHandler legacy = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://*/processing/*"), new NonWorkingRequestHandler()); + handlers.put(new UriPattern("http://*/statistics/*"), new NonWorkingRequestHandler()); + handlers.put(new UriPattern("http://*/state/v1/*"), new NonWorkingRequestHandler()); + handlers.put(new UriPattern("http://*/search/*"), search); + handlers.put(new UriPattern("http://*/status.html"), new NonWorkingRequestHandler()); + handlers.put(new UriPattern("http://*/ApplicationStatus"), applicationStatus); + handlers.put(new UriPattern("http://*:" + getDefaults().vespaWebServicePort() + "/*"), legacy); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + + assertSame(applicationStatus, bindings.resolve(URI.create + ("http://abcxyz.yahoo.com:" + getDefaults().vespaWebServicePort() + "/ApplicationStatus"))); + assertSame(search, bindings.resolve(URI.create + ("http://abcxyz.yahoo.com:" + getDefaults().vespaWebServicePort() + "/search/?query=sddocname:music"))); + assertSame(legacy, bindings.resolve(URI.create + ("http://abcxyz.yahoo.com:" + getDefaults().vespaWebServicePort() + "/stats/?query=stat:query"))); + } + + @Test + public void requireThatPathOverPortsDoNotWorkOverStricterPatterns() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler bar = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host:4050/a/"), foo); + handlers.put(new UriPattern("http://host/a/"), bar); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + assertSame(foo, bindings.resolve(URI.create("http://host:4050/a/"))); + } + + @Test + public void requireThatSchemeOrderOverHost() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler bar = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host:5050/a/"), foo); + handlers.put(new UriPattern("ftp://host:5050/a/"), bar); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + assertSame(foo, bindings.resolve(URI.create("http://host:5050/a/"))); + assertSame(bar, bindings.resolve(URI.create("ftp://host:5050/a/"))); + } + + @Test + public void requireThatPortsAreOrdered() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler bar = new NonWorkingRequestHandler(); + RequestHandler car = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host:5050/a/"), foo); + handlers.put(new UriPattern("http://host:5051/a/"), bar); + handlers.put(new UriPattern("http://host/a/"), car); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + assertSame(foo, bindings.resolve(URI.create("http://host:5050/a/"))); + assertSame(bar, bindings.resolve(URI.create("http://host:5051/a/"))); + assertSame(car, bindings.resolve(URI.create("http://host/a/"))); + assertSame(car, bindings.resolve(URI.create("http://host:8080/a/"))); + assertSame(car, bindings.resolve(URI.create("http://host:80/a/"))); + } + + @Test + public void requireThatPathsAreOrdered() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler bar = new NonWorkingRequestHandler(); + RequestHandler car = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host:5050/a/"), foo); + handlers.put(new UriPattern("http://host:5050/b/"), bar); + handlers.put(new UriPattern("http://host/a/"), car); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + assertSame(foo, bindings.resolve(URI.create("http://host:5050/a/"))); + assertSame(bar, bindings.resolve(URI.create("http://host:5050/b/"))); + assertSame(car, bindings.resolve(URI.create("http://host/a/"))); + assertSame(car, bindings.resolve(URI.create("http://host:8080/a/"))); + assertSame(car, bindings.resolve(URI.create("http://host:80/a/"))); + } + + @Test + public void requireThatStrictPatternsOrderBeforeWildcards() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + + RequestHandler fooScheme = new NonWorkingRequestHandler(); + RequestHandler barScheme = new NonWorkingRequestHandler(); + + RequestHandler fooHost = new NonWorkingRequestHandler(); + RequestHandler barHost = new NonWorkingRequestHandler(); + + RequestHandler fooPort = new NonWorkingRequestHandler(); + RequestHandler barPort = new NonWorkingRequestHandler(); + RequestHandler carPort = new NonWorkingRequestHandler(); + + RequestHandler fooPath = new NonWorkingRequestHandler(); + RequestHandler barPath = new NonWorkingRequestHandler(); + + handlers.put(new UriPattern("http://host/x/"), fooScheme); + handlers.put(new UriPattern("*://host/x/"), barScheme); + + handlers.put(new UriPattern("http://host/abc/"), fooHost); + handlers.put(new UriPattern("http://*/abc/"), barHost); + + handlers.put(new UriPattern("http://host:*/a/"), fooPort); + handlers.put(new UriPattern("http://host:5050/b/"), barPort); + handlers.put(new UriPattern("http://host/b/"), carPort); + + handlers.put(new UriPattern("http://hostname/abcde/"), fooPath); + handlers.put(new UriPattern("http://hostname/*/"), barPath); + + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + assertSame(fooScheme, bindings.resolve(URI.create("http://host/x/"))); + assertSame(barScheme, bindings.resolve(URI.create("ftp://host/x/"))); + + assertSame(fooHost, bindings.resolve(URI.create("http://host:8080/abc/"))); + assertSame(barHost, bindings.resolve(URI.create("http://lmn:5050/abc/"))); + + assertSame(fooPort, bindings.resolve(URI.create("http://host:5050/a/"))); + assertSame(barPort, bindings.resolve(URI.create("http://host:5050/b/"))); + assertSame(carPort, bindings.resolve(URI.create("http://host/b/"))); + assertSame(carPort, bindings.resolve(URI.create("http://host:8080/b/"))); + assertSame(carPort, bindings.resolve(URI.create("http://host:80/b/"))); + assertSame(fooPath, bindings.resolve(URI.create("http://hostname/abcde/"))); + assertSame(barPath, bindings.resolve(URI.create("http://hostname/abcd/"))); + + } + + @Test + public void requireThatToStringMethodWorks() { + Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler bar = new NonWorkingRequestHandler(); + handlers.put(new UriPattern("http://host/foo"), foo); + handlers.put(new UriPattern("http://host/bar"), bar); + BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet()); + assertNotNull(bindings); + assertNotNull(bindings.toString()); //Just to get code coverage. + } + + + @Test + public void requireThatPatternsAreOrderedMoreSpecificToLess() { + assertOrder("3://host/path", "2://host/path", "1://host/path"); + assertOrder("http://3/path", "http://2/path", "http://1/path"); + assertOrder("http://host:3/path", "http://host:2/path", "http://host:1/path"); + assertOrder("http://host/3", "http://host/2", "http://host/1"); + assertOrder("http://*/*", "*://host/2", "*://host/1"); + assertOrder("http://host/*", "http://*/2", "http://*/1"); + assertOrder("http://host:*/3", "http://host:2/2", "http://host:1/1"); + assertOrder("http://host/*/3/2/", "http://host/*/1/2", "http://host/*/2/*"); + assertOrder("http://host:69/path", + "http://host/*", + "http://*:69/path", + "http://*/path", + "http://*:69/*", + "http://*/*", + "*://host/path", + "*://*/path", + "*://*/*"); + assertOrder("http://*/HelloWorld", + "http://*:4080/state/v1/*", + "http://*:4083/*", + "http://*:4081/*", + "http://*:4080/*"); + } + + private static void assertOrder(String... expected) { + for (int off = 0; off < expected.length; ++off) { + List<String> actual = new ArrayList<>(); + for (int i = 0; i < expected.length; ++i) { + actual.add(expected[(off + i) % expected.length]); + } + assertOrder(Arrays.asList(expected), actual); + + actual = new ArrayList<>(); + for (int i = expected.length; --i >= 0; ) { + actual.add(expected[(off + i) % expected.length]); + } + assertOrder(Arrays.asList(expected), actual); + } + } + + private static void assertOrder(List<String> expected, List<String> actual) { + BindingRepository<Object> repo = new BindingRepository<>(); + for (String pattern : actual) { + repo.bind(pattern, new Object()); + } + BindingSet<Object> bindings = repo.activate(); + Iterator<Map.Entry<UriPattern, Object>> it = bindings.iterator(); + for (String pattern : expected) { + assertTrue(it.hasNext()); + assertEquals(new UriPattern(pattern), it.next().getKey()); + } + assertFalse(it.hasNext()); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/BundleInstallationExceptionTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BundleInstallationExceptionTestCase.java new file mode 100644 index 00000000000..870db066dc0 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BundleInstallationExceptionTestCase.java @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class BundleInstallationExceptionTestCase { + + @Test + public void requireThatAccessorsWork() { + Throwable t = new Throwable("foo"); + Collection<Bundle> bundles = new LinkedList<>(); + bundles.add(Mockito.mock(Bundle.class)); + BundleInstallationException e = new BundleInstallationException(bundles, t); + assertSame(t, e.getCause()); + assertEquals(t.getMessage(), e.getCause().getMessage()); + assertEquals(bundles, e.installedBundles()); + } + + @Test + public void requireThatBundlesCollectionIsDefensivelyCopied() { + Collection<Bundle> bundles = new LinkedList<>(); + bundles.add(Mockito.mock(Bundle.class)); + BundleInstallationException e = new BundleInstallationException(bundles, new Throwable()); + bundles.add(Mockito.mock(Bundle.class)); + assertEquals(1, e.installedBundles().size()); + } + + @Test + public void requireThatBundlesCollectionIsUnmodifiable() { + BundleInstallationException e = new BundleInstallationException(Arrays.asList(Mockito.mock(Bundle.class)), + new Throwable()); + try { + e.installedBundles().add(Mockito.mock(Bundle.class)); + fail(); + } catch (UnsupportedOperationException f) { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerBuilderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerBuilderTestCase.java new file mode 100644 index 00000000000..811f8fa901b --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerBuilderTestCase.java @@ -0,0 +1,116 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.name.Names; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerBuilderTestCase { + + @Test + public void requireThatAccessorsWork() throws URISyntaxException { + final Object obj = new Object(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() { + + @Override + protected void configure() { + bind(Object.class).toInstance(obj); + bind(String.class).annotatedWith(Names.named("foo")).toInstance("foo"); + } + }); + ContainerBuilder builder = driver.newContainerBuilder(); + assertSame(obj, builder.getInstance(Object.class)); + assertEquals("foo", builder.getInstance(Key.get(String.class, Names.named("foo")))); + + Object ctx = new Object(); + builder.setAppContext(ctx); + assertSame(ctx, builder.appContext()); + + assertTrue(driver.close()); + } + + @Test + public void requireThatContainerThreadFactoryIsBound() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + assertSame(ContainerThread.Factory.class, builder.getInstance(ThreadFactory.class).getClass()); + assertTrue(driver.close()); + } + + @Test + public void requireThatThreadFactoryCanBeReconfigured() { + final ThreadFactory factory = Executors.defaultThreadFactory(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.guiceModules().install(new AbstractModule() { + + @Override + protected void configure() { + bind(ThreadFactory.class).toInstance(factory); + } + }); + assertSame(factory, builder.getInstance(ThreadFactory.class)); + assertTrue(driver.close()); + } + + @Test + public void requireThatBindingSetsAreCreatedOnDemand() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + BindingRepository repo = builder.serverBindings("foo"); + assertNotNull(repo); + assertSame(repo, builder.serverBindings("foo")); + assertNotNull(repo = builder.serverBindings("bar")); + assertSame(repo, builder.serverBindings("bar")); + assertNotNull(repo = builder.clientBindings("baz")); + assertSame(repo, builder.clientBindings("baz")); + assertNotNull(repo = builder.clientBindings("cox")); + assertSame(repo, builder.clientBindings("cox")); + driver.close(); + } + + @Test + public void requireThatSafeClassCastWorks() { + ContainerBuilder.safeClassCast(Integer.class, Integer.class); + } + + @Test + public void requireThatSafeClassCastThrowsIllegalArgument() { + try { + ContainerBuilder.safeClassCast(Integer.class, Double.class); + fail(); + } catch (IllegalArgumentException e) { + + } + } + + @Test + public void requireThatSafeStringSplitWorks() { + assertTrue(ContainerBuilder.safeStringSplit(new Object(), ",").isEmpty()); + assertTrue(ContainerBuilder.safeStringSplit("", ",").isEmpty()); + assertTrue(ContainerBuilder.safeStringSplit(" \f\n\r\t", ",").isEmpty()); + assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit("foo", ",")); + assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit(" foo", ",")); + assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit("foo ", ",")); + assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit("foo, ", ",")); + assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit("foo ,", ",")); + assertEquals(Arrays.asList("foo", "bar"), ContainerBuilder.safeStringSplit("foo, bar", ",")); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerThreadTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerThreadTestCase.java new file mode 100644 index 00000000000..d92512b3650 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerThreadTestCase.java @@ -0,0 +1,61 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.yahoo.jdisc.Metric; +import org.junit.Test; + +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertSame; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerThreadTestCase { + + @Test + public void requireThatAccessorsWork() { + MetricConsumer consumer = new MyConsumer(); + ContainerThread thread = new ContainerThread(new MyTask(), consumer); + assertSame(consumer, thread.consumer()); + } + + @Test + public void requireThatTaskIsRun() throws InterruptedException { + MyTask task = new MyTask(); + ContainerThread thread = new ContainerThread(task, null); + thread.start(); + task.latch.await(600, TimeUnit.SECONDS); + } + + private static class MyConsumer implements MetricConsumer { + + @Override + public void set(String key, Number val, Metric.Context ctx) { + + } + + @Override + public void add(String key, Number val, Metric.Context ctx) { + + } + + @Override + public Metric.Context createContext(Map<String, ?> properties) { + return null; + } + } + + private static class MyTask implements Runnable { + + final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void run() { + latch.countDown(); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/GlobPatternTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/GlobPatternTestCase.java new file mode 100644 index 00000000000..c9b4650e572 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/GlobPatternTestCase.java @@ -0,0 +1,157 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class GlobPatternTestCase { + + @Test + public void requireThatCompileCreatesExpectedParts() { + assertToString("foo"); + assertToString("*foo"); + assertToString("*oo"); + assertToString("f*o"); + assertToString("fo*"); + assertToString("foo*"); + assertToString("**foo"); + assertToString("**oo"); + assertToString("**o"); + assertToString("f**"); + assertToString("fo**"); + assertToString("foo**"); + assertToString(""); + assertToString("*"); + } + + @Test + public void requireThatGlobMatcherWorks() { + assertMatch("foo", "foo", Collections.<String>emptyList()); + assertNotMatch("foo", "bar"); + + assertMatch("*", "foo", Arrays.asList("foo")); + assertMatch("*", "bar", Arrays.asList("bar")); + + assertMatch("*foo", "foo", Arrays.asList("")); + assertMatch("*oo", "foo", Arrays.asList("f")); + assertMatch("f*o", "foo", Arrays.asList("o")); + assertMatch("fo*", "foo", Arrays.asList("o")); + assertMatch("foo*", "foo", Arrays.asList("")); + + assertNotMatch("*foo", "bar"); + assertNotMatch("*oo", "bar"); + assertNotMatch("f*o", "bar"); + assertNotMatch("fo*", "bar"); + assertNotMatch("foo*", "bar"); + + assertMatch("**foo", "foo", Arrays.asList("", "")); + assertMatch("**oo", "foo", Arrays.asList("", "f")); + assertMatch("f**o", "foo", Arrays.asList("", "o")); + assertMatch("fo**", "foo", Arrays.asList("", "o")); + assertMatch("foo**", "foo", Arrays.asList("", "")); + + assertNotMatch("**foo", "bar"); + assertNotMatch("**oo", "bar"); + assertNotMatch("f**o", "bar"); + assertNotMatch("fo**", "bar"); + assertNotMatch("foo**", "bar"); + + assertMatch("foo bar", "foo bar", Collections.<String>emptyList()); + assertMatch("*foo *bar", "foo bar", Arrays.asList("", "")); + assertMatch("foo* bar*", "foo bar", Arrays.asList("", "")); + assertMatch("f* *r", "foo bar", Arrays.asList("oo", "ba")); + + assertNotMatch("foo bar", "baz cox"); + assertNotMatch("*foo *bar", "baz cox"); + assertNotMatch("foo* bar*", "baz cox"); + assertNotMatch("f* *r", "baz cox"); + } + + @Test + public void requireThatGlobPatternOrdersMoreSpecificFirst() { + assertCompareEq("foo", "foo"); + assertCompareLt("foo", "foo*"); + assertCompareLt("foo", "*foo"); + + assertCompareEq("foo/bar", "foo/bar"); + assertCompareLt("foo/bar", "foo"); + assertCompareLt("foo/bar", "foo*"); + assertCompareLt("foo/bar", "*foo"); + + assertCompareLt("foo/bar", "foo*bar"); + assertCompareLt("foo/bar", "foo*bar*"); + assertCompareLt("foo/bar", "*foo*bar"); + + assertCompareLt("foo*bar", "foo"); + assertCompareLt("foo*bar", "foo*"); + assertCompareLt("foo*bar", "*foo"); + + assertCompareLt("foo", "foo*bar*"); + assertCompareLt("foo*bar*", "foo*"); + assertCompareLt("*foo", "foo*bar*"); + + assertCompareLt("foo", "*foo*bar"); + assertCompareLt("*foo*bar", "foo*"); + assertCompareLt("*foo*bar", "*foo"); + + assertCompareLt("*/3/2", "*/1/2"); + assertCompareLt("*/1/2", "*/2/*"); + } + + @Test + public void requireThatEqualsIsImplemented() { + assertTrue(GlobPattern.compile("foo").equals(GlobPattern.compile("foo"))); + assertFalse(GlobPattern.compile("foo").equals(GlobPattern.compile("bar"))); + } + + @Test + public void requireThatHashCodeIsImplemented() { + assertTrue(GlobPattern.compile("foo").hashCode() == GlobPattern.compile("foo").hashCode()); + assertFalse(GlobPattern.compile("foo").hashCode() == GlobPattern.compile("bar").hashCode()); + } + + private static void assertCompareLt(String lhs, String rhs) { + assertTrue(compare(lhs, rhs) < 0); + assertTrue(compare(rhs, lhs) > 0); + } + + private static void assertCompareEq(String lhs, String rhs) { + assertEquals(0, compare(lhs, rhs)); + assertEquals(0, compare(rhs, lhs)); + } + + private static int compare(String lhs, String rhs) { + return GlobPattern.compile(lhs).compareTo(GlobPattern.compile(rhs)); + } + + private static void assertMatch(String glob, String str, List<String> expected) { + GlobPattern.Match match = GlobPattern.match(glob, str); + assertNotNull(match); + List<String> actual = new ArrayList<>(match.groupCount()); + for (int i = 0, len = match.groupCount(); i < len; ++i) { + actual.add(match.group(i)); + } + assertEquals(expected, actual); + } + + private static void assertNotMatch(String glob, String str) { + assertNull(GlobPattern.match(glob, str)); + } + + private static void assertToString(String pattern) { + assertEquals(pattern, GlobPattern.compile(pattern).toString()); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/GuiceRepositoryTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/GuiceRepositoryTestCase.java new file mode 100644 index 00000000000..39981a812b5 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/GuiceRepositoryTestCase.java @@ -0,0 +1,197 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.AbstractModule; +import com.google.inject.ConfigurationException; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.PrivateModule; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class GuiceRepositoryTestCase { + + @Test + public void requireThatInstallWorks() { + GuiceRepository guice = new GuiceRepository(); + StringBinding module = new StringBinding("fooKey", "fooVal"); + guice.install(module); + assertBinding(guice, "fooKey", "fooVal"); + + Iterator<Module> it = guice.iterator(); + assertTrue(it.hasNext()); + assertSame(module, it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatInstallAllWorks() { + GuiceRepository guice = new GuiceRepository(); + StringBinding foo = new StringBinding("fooKey", "fooVal"); + StringBinding bar = new StringBinding("barKey", "barVal"); + guice.installAll(Arrays.asList(foo, bar)); + assertBinding(guice, "fooKey", "fooVal"); + assertBinding(guice, "barKey", "barVal"); + + Iterator<Module> it = guice.iterator(); + assertTrue(it.hasNext()); + assertSame(foo, it.next()); + assertTrue(it.hasNext()); + assertSame(bar, it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatUninstallWorks() { + GuiceRepository guice = new GuiceRepository(); + StringBinding module = new StringBinding("fooKey", "fooVal"); + guice.install(module); + assertBinding(guice, "fooKey", "fooVal"); + + guice.uninstall(module); + assertNoBinding(guice, "fooKey"); + assertFalse(guice.iterator().hasNext()); + } + + @Test + public void requireThatUninstallAllWorks() { + GuiceRepository guice = new GuiceRepository(); + StringBinding foo = new StringBinding("fooKey", "fooVal"); + StringBinding bar = new StringBinding("barKey", "barVal"); + StringBinding baz = new StringBinding("bazKey", "bazVal"); + guice.installAll(Arrays.asList(foo, bar, baz)); + assertBinding(guice, "fooKey", "fooVal"); + assertBinding(guice, "barKey", "barVal"); + assertBinding(guice, "bazKey", "bazVal"); + + guice.uninstallAll(Arrays.asList(foo, baz)); + assertNoBinding(guice, "fooKey"); + assertBinding(guice, "barKey", "barVal"); + assertNoBinding(guice, "bazKey"); + + Iterator<Module> it = guice.iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + assertSame(bar, it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatBindingsCanBeOverridden() { + GuiceRepository guice = new GuiceRepository(); + guice.install(new StringBinding("fooKey", "fooVal1")); + assertBinding(guice, "fooKey", "fooVal1"); + guice.install(new StringBinding("fooKey", "fooVal2")); + assertBinding(guice, "fooKey", "fooVal2"); + } + + @Test + public void requireThatModulesAreOnlyEvaluatedOnce() { + GuiceRepository guice = new GuiceRepository(); + EvalCounter foo = new EvalCounter(); + EvalCounter bar = new EvalCounter(); + assertEquals(0, foo.cnt); + assertEquals(0, bar.cnt); + guice.install(foo); + assertEquals(1, foo.cnt); + assertEquals(0, bar.cnt); + guice.install(bar); + assertEquals(1, foo.cnt); + assertEquals(1, bar.cnt); + } + + @Test + public void requireThatPrivateModulesWorks() { + GuiceRepository guice = new GuiceRepository(); + + List<Named> names = Arrays.asList(Names.named("A"), Names.named("B")); + + for (Named name: names) { + guice.install(createPrivateInjectNameModule(name)); + } + + Injector injector = guice.getInjector(); + + for (Named name: names) { + NameHolder nameHolder = injector.getInstance(Key.get(NameHolder.class, name)); + assertEquals(name, nameHolder.name); + } + } + + private Module createPrivateInjectNameModule(final Named name) { + return new PrivateModule() { + @Override + protected void configure() { + bind(NameHolder.class).annotatedWith(name).to(NameHolder.class); + expose(NameHolder.class).annotatedWith(name); + bind(Named.class).toInstance(name); + } + }; + } + + private static void assertBinding(GuiceRepository guice, String name, String expected) { + assertEquals(expected, guice.getInjector().getInstance(Key.get(String.class, Names.named(name)))); + } + + private static void assertNoBinding(GuiceRepository guice, String name) { + try { + guice.getInjector().getInstance(Key.get(String.class, Names.named(name))); + fail(); + } catch (ConfigurationException e) { + + } + } + + private static class EvalCounter extends AbstractModule { + + int cnt = 0; + + @Override + protected void configure() { + ++cnt; + } + } + + private static class StringBinding extends AbstractModule { + + final String name; + final String val; + + StringBinding(String name, String val) { + this.name = name; + this.val = val; + } + + @Override + protected void configure() { + bind(String.class).annotatedWith(Names.named(name)).toInstance(val); + } + } + + public static final class NameHolder { + public final Named name; + + @Inject + public NameHolder(Named name) { + this.name = name; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/MetricImplTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/MetricImplTestCase.java new file mode 100644 index 00000000000..84ee425f55f --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/MetricImplTestCase.java @@ -0,0 +1,150 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.yahoo.jdisc.Metric; +import org.junit.Test; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class MetricImplTestCase { + + @Test + public void requireThatClassIsInjectedByDefault() { + Metric metric = Guice.createInjector().getInstance(Metric.class); + assertTrue(metric instanceof MetricImpl); + } + + @Test + public void requireThatConsumerIsOptional() { + Injector injector = Guice.createInjector(); + Metric metric = injector.getInstance(Metric.class); + metric.set("foo", 6, null); + metric.add("foo", 9, null); + } + + @Test + public void requireThatConsumerIsCalled() throws InterruptedException { + final MyConsumer consumer = new MyConsumer(); + Injector injector = Guice.createInjector(new AbstractModule() { + + @Override + protected void configure() { + bind(MetricConsumer.class).toInstance(consumer); + } + }); + Metric metric = injector.getInstance(Metric.class); + metric.set("foo", 6, null); + assertEquals(6, consumer.map.get("foo").intValue()); + metric.add("foo", 9, null); + assertEquals(15, consumer.map.get("foo").intValue()); + Metric.Context ctx = metric.createContext(null); + assertEquals(consumer.ctx, ctx); + } + + @Test + public void requireThatWorkerMetricHasPrecedence() throws InterruptedException { + final MyConsumer globalConsumer = new MyConsumer(); + Injector injector = Guice.createInjector(new AbstractModule() { + + @Override + protected void configure() { + bind(MetricConsumer.class).toInstance(globalConsumer); + } + }); + Metric metric = injector.getInstance(Metric.class); + + MyConsumer localConsumer = new MyConsumer(); + localConsumer.latchRef.set(new CountDownLatch(1)); + new ContainerThread(new SetTask(metric, "foo", 6), localConsumer).start(); + localConsumer.latchRef.get().await(600, TimeUnit.SECONDS); + assertEquals(6, localConsumer.map.get("foo").intValue()); + assertTrue(globalConsumer.map.isEmpty()); + + localConsumer.latchRef.set(new CountDownLatch(1)); + new ContainerThread(new AddTask(metric, "foo", 9), localConsumer).start(); + localConsumer.latchRef.get().await(600, TimeUnit.SECONDS); + assertEquals(15, localConsumer.map.get("foo").intValue()); + assertTrue(globalConsumer.map.isEmpty()); + } + + private static class SetTask implements Runnable { + + final Metric metric; + final String key; + final Number val; + + public SetTask(Metric metric, String key, Number val) { + this.metric = metric; + this.key = key; + this.val = val; + } + + @Override + public void run() { + metric.set(key, val, null); + } + } + + private static class AddTask implements Runnable { + + final Metric metric; + final String key; + final Number val; + + public AddTask(Metric metric, String key, Number val) { + this.metric = metric; + this.key = key; + this.val = val; + } + + @Override + public void run() { + metric.add(key, val, null); + } + } + + private static class MyConsumer implements MetricConsumer { + + final ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>(); + final AtomicReference<CountDownLatch> latchRef = new AtomicReference<>(); + final Metric.Context ctx = new Metric.Context() { }; + + @Override + public void set(String key, Number val, Metric.Context ctx) { + map.put(key, val.intValue()); + CountDownLatch latch = latchRef.get(); + if (latch != null) { + latch.countDown(); + } + } + + @Override + public void add(String key, Number val, Metric.Context ctx) { + map.put(key, map.get(key) + val.intValue()); + CountDownLatch latch = this.latchRef.get(); + if (latch != null) { + latch.countDown(); + } + } + + @Override + public Metric.Context createContext(Map<String, ?> properties) { + return ctx; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiHeaderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiHeaderTestCase.java new file mode 100644 index 00000000000..2d167aa08b6 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiHeaderTestCase.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.jdisc.application; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class OsgiHeaderTestCase { + + @Test + public void requireThatOsgiHeadersDoNotChange() { + assertEquals("X-JDisc-Application", OsgiHeader.APPLICATION); + assertEquals("X-JDisc-Preinstall-Bundle", OsgiHeader.PREINSTALL_BUNDLE); + assertEquals("X-JDisc-Privileged-Activator", OsgiHeader.PRIVILEGED_ACTIVATOR); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiRepositoryTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiRepositoryTestCase.java new file mode 100644 index 00000000000..2da63190616 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiRepositoryTestCase.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.jdisc.application; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class OsgiRepositoryTestCase { + + @Test + public void requireNothingSinceIntegrationModuleTestsThis() { + assertTrue(true); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ResourcePoolTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ResourcePoolTestCase.java new file mode 100644 index 00000000000..ed113572bb7 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ResourcePoolTestCase.java @@ -0,0 +1,168 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ResourcePoolTestCase { + + @Test + public void requireThatAddReturnsArgument() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyResource foo = new MyResource(); + assertSame(foo, new ResourcePool(driver.newContainerBuilder()).add(foo)); + assertTrue(driver.close()); + } + + @Test + public void requireThatAddDoesNotRetainArgument() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyResource foo = new MyResource(); + assertEquals(1, foo.retainCount()); + new ResourcePool(driver.newContainerBuilder()).add(foo); + assertEquals(1, foo.retainCount()); + assertTrue(driver.close()); + } + + @Test + public void requireThatAddCanBeUsedWithoutContainerBuilder() { + new ResourcePool().add(new MyResource()); + } + + @Test + public void requireThatRetainReturnsArgument() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyResource foo = new MyResource(); + assertSame(foo, new ResourcePool(driver.newContainerBuilder()).retain(foo)); + assertTrue(driver.close()); + } + + @Test + public void requireThatRetainRetainsArgument() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyResource foo = new MyResource(); + assertEquals(1, foo.retainCount()); + new ResourcePool(driver.newContainerBuilder()).retain(foo); + assertEquals(2, foo.retainCount()); + assertTrue(driver.close()); + } + + @Test + public void requireThatRetainCanBeUsedWithoutContainerBuilder() { + new ResourcePool().retain(new MyResource()); + } + + @Test + public void requireThatGetReturnsBoundInstance() { + final MyResource foo = new MyResource(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() { + + @Override + protected void configure() { + bind(MyResource.class).toInstance(foo); + } + }); + ResourcePool pool = new ResourcePool(driver.newContainerBuilder()); + assertSame(foo, pool.get(MyResource.class)); + assertSame(foo, pool.get(Key.get(MyResource.class))); + assertTrue(driver.close()); + } + + @Test + public void requireThatGetDoesNotRetainArgument() { + final MyResource foo = new MyResource(); + assertEquals(1, foo.retainCount()); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() { + + @Override + protected void configure() { + bind(MyResource.class).toInstance(foo); + } + }); + ResourcePool pool = new ResourcePool(driver.newContainerBuilder()); + pool.get(MyResource.class); + assertEquals(1, foo.retainCount()); + pool.get(Key.get(MyResource.class)); + assertEquals(1, foo.retainCount()); + assertTrue(driver.close()); + } + + @Test + public void requireThatGetCanNotBeUsedWithoutContainerBuilder() { + ResourcePool pool = new ResourcePool(); + try { + pool.get(MyResource.class); + fail(); + } catch (NullPointerException e) { + + } + try { + pool.get(Key.get(MyResource.class)); + fail(); + } catch (NullPointerException e) { + + } + } + + @Test + public void requireThatResourcesAreReleasedOnDestroy() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + + ResourcePool pool = new ResourcePool(driver.newContainerBuilder()); + MyResource foo = pool.add(new MyResource()); + MyResource bar = pool.add(new MyResource()); + MyResource baz = pool.add(new MyResource()); + assertEquals(1, pool.retainCount()); + assertEquals(1, foo.retainCount()); + assertEquals(1, bar.retainCount()); + assertEquals(1, baz.retainCount()); + + final ResourceReference secondPoolReference = pool.refer(); + assertEquals(2, pool.retainCount()); + assertEquals(1, foo.retainCount()); + assertEquals(1, bar.retainCount()); + assertEquals(1, baz.retainCount()); + + secondPoolReference.close(); + assertEquals(1, pool.retainCount()); + assertEquals(1, foo.retainCount()); + assertEquals(1, bar.retainCount()); + assertEquals(1, baz.retainCount()); + + pool.release(); + assertEquals(0, pool.retainCount()); + assertEquals(0, foo.retainCount()); + assertEquals(0, bar.retainCount()); + assertEquals(0, baz.retainCount()); + + assertTrue(driver.close()); + } + + @Test + public void requireThatAutoCloseCallsRelease() throws Exception { + MyResource foo = new MyResource(); + assertEquals(1, foo.retainCount()); + try (ResourcePool pool = new ResourcePool()) { + pool.retain(foo); + assertEquals(2, foo.retainCount()); + } + assertEquals(1, foo.retainCount()); + } + + private static class MyResource extends AbstractResource { + + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ServerRepositoryTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ServerRepositoryTestCase.java new file mode 100644 index 00000000000..6ce125ff590 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ServerRepositoryTestCase.java @@ -0,0 +1,88 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.service.ServerProvider; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Iterator; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ServerRepositoryTestCase { + + @Test + public void requireThatInstallWorks() { + ServerRepository servers = newServerRepository(); + MyServer server = new MyServer(); + servers.install(server); + + Iterator<ServerProvider> it = servers.iterator(); + assertTrue(it.hasNext()); + assertSame(server, it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatInstallAllWorks() { + ServerRepository servers = newServerRepository(); + ServerProvider foo = new MyServer(); + ServerProvider bar = new MyServer(); + servers.installAll(Arrays.asList(foo, bar)); + + Iterator<ServerProvider> it = servers.iterator(); + assertTrue(it.hasNext()); + assertSame(foo, it.next()); + assertTrue(it.hasNext()); + assertSame(bar, it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatUninstallWorks() { + ServerRepository servers = newServerRepository(); + ServerProvider server = new MyServer(); + servers.install(server); + servers.uninstall(server); + assertFalse(servers.iterator().hasNext()); + } + + @Test + public void requireThatUninstallAllWorks() { + ServerRepository servers = newServerRepository(); + ServerProvider foo = new MyServer(); + ServerProvider bar = new MyServer(); + ServerProvider baz = new MyServer(); + servers.installAll(Arrays.asList(foo, bar, baz)); + servers.uninstallAll(Arrays.asList(foo, bar)); + Iterator<ServerProvider> it = servers.iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + assertSame(baz, it.next()); + assertFalse(it.hasNext()); + } + + private static ServerRepository newServerRepository() { + return new ServerRepository(new GuiceRepository()); + } + + private static class MyServer extends NoopSharedResource implements ServerProvider { + + @Override + public void start() { + + } + + @Override + public void close() { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/UriPatternTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/UriPatternTestCase.java new file mode 100644 index 00000000000..c7c45be481a --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/UriPatternTestCase.java @@ -0,0 +1,342 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.application; + +import org.junit.Test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class UriPatternTestCase { + + private static final List<String> NO_GROUPS = Collections.emptyList(); + + @Test + public void requireThatIllegalPatternsAreDetected() { + assertIllegalPattern("scheme"); + assertIllegalPattern("scheme://"); + assertIllegalPattern("scheme://host"); + assertIllegalPattern("scheme://host:0"); + assertIllegalPattern("scheme://host:69"); + assertIllegalPattern("scheme://host:-69"); + assertIllegalPattern("scheme://host:6*/"); + assertIllegalPattern("scheme://host:6*9/"); + assertIllegalPattern("scheme://host:*9/"); + } + + @Test + public void requireThatNoPortImpliesWildcard() { + assertEquals(new UriPattern("scheme://host/path"), + new UriPattern("scheme://host:*/path")); + } + + @Test + public void requireThatPatternMatches() { + // scheme matching + UriPattern pattern = new UriPattern("bar://host:69/path"); + assertNotMatch(pattern, "foobar://host:69/path"); + assertMatch(pattern, "bar://host:69/path", NO_GROUPS); + assertNotMatch(pattern, "barbaz://host:69/path"); + + pattern = new UriPattern("*://host:69/path"); + assertMatch(pattern, "foobar://host:69/path", Arrays.asList("foobar")); + assertMatch(pattern, "bar://host:69/path", Arrays.asList("bar")); + assertMatch(pattern, "barbaz://host:69/path", Arrays.asList("barbaz")); + + pattern = new UriPattern("*bar://host:69/path"); + assertMatch(pattern, "foobar://host:69/path", Arrays.asList("foo")); + assertMatch(pattern, "bar://host:69/path", Arrays.asList("")); + assertNotMatch(pattern, "barbaz://host:69/path"); + + pattern = new UriPattern("bar*://host:69/path"); + assertNotMatch(pattern, "foobar://host:69/path"); + assertMatch(pattern, "bar://host:69/path", Arrays.asList("")); + assertMatch(pattern, "barbaz://host:69/path", Arrays.asList("baz")); + + // host matching + pattern = new UriPattern("scheme://bar:69/path"); + assertNotMatch(pattern, "scheme://foobar:69/path"); + assertMatch(pattern, "scheme://bar:69/path", NO_GROUPS); + assertNotMatch(pattern, "scheme://barbaz:69/path"); + + pattern = new UriPattern("scheme://*:69/path"); + assertMatch(pattern, "scheme://foobar:69/path", Arrays.asList("foobar")); + assertMatch(pattern, "scheme://bar:69/path", Arrays.asList("bar")); + assertMatch(pattern, "scheme://barbaz:69/path", Arrays.asList("barbaz")); + + pattern = new UriPattern("scheme://*bar:69/path"); + assertMatch(pattern, "scheme://foobar:69/path", Arrays.asList("foo")); + assertMatch(pattern, "scheme://bar:69/path", Arrays.asList("")); + assertNotMatch(pattern, "scheme://barbaz:69/path"); + + pattern = new UriPattern("scheme://bar*:69/path"); + assertNotMatch(pattern, "scheme://foobar:69/path"); + assertMatch(pattern, "scheme://bar:69/path", Arrays.asList("")); + assertMatch(pattern, "scheme://barbaz:69/path", Arrays.asList("baz")); + + // port matching + pattern = new UriPattern("scheme://host:69/path"); + assertNotMatch(pattern, "scheme://host:669/path"); + assertMatch(pattern, "scheme://host:69/path", NO_GROUPS); + assertNotMatch(pattern, "scheme://host:699/path"); + + pattern = new UriPattern("scheme://host:*/path"); + assertMatch(pattern, "scheme://host:669/path", Arrays.asList("669")); + assertMatch(pattern, "scheme://host:69/path", Arrays.asList("69")); + assertMatch(pattern, "scheme://host:699/path", Arrays.asList("699")); + + // path matching + pattern = new UriPattern("scheme://host:69/"); + assertMatch(pattern, "scheme://host:69/", NO_GROUPS); + assertNotMatch(pattern, "scheme://host:69/foo"); + + pattern = new UriPattern("scheme://host:69/bar"); + assertNotMatch(pattern, "scheme://host:69/foobar"); + assertMatch(pattern, "scheme://host:69/bar", NO_GROUPS); + assertNotMatch(pattern, "scheme://host:69/barbaz"); + + pattern = new UriPattern("scheme://host:69/*"); + assertMatch(pattern, "scheme://host:69/", Arrays.asList("")); + assertMatch(pattern, "scheme://host:69/foobar", Arrays.asList("foobar")); + assertMatch(pattern, "scheme://host:69/bar", Arrays.asList("bar")); + assertMatch(pattern, "scheme://host:69/barbaz", Arrays.asList("barbaz")); + + pattern = new UriPattern("scheme://host:69/*bar"); + assertMatch(pattern, "scheme://host:69/foobar", Arrays.asList("foo")); + assertMatch(pattern, "scheme://host:69/bar", Arrays.asList("")); + assertNotMatch(pattern, "scheme://host:69/barbaz"); + + pattern = new UriPattern("scheme://host:69/bar*"); + assertNotMatch(pattern, "scheme://host:69/foobar"); + assertMatch(pattern, "scheme://host:69/bar", Arrays.asList("")); + assertMatch(pattern, "scheme://host:69/barbaz", Arrays.asList("baz")); + } + + @Test + public void requireThatUriWithoutHostDoesNotThrowException() { + String schemeOnly = "scheme:schemeSpecificPart"; + String schemeAndPath = "scheme:/path"; + String pathOnly = "path"; + String pathOnlyWithSlash = "/path"; + + UriPattern pattern = new UriPattern("scheme://host/path"); + assertNotMatch(pattern, schemeOnly); + assertNotMatch(pattern, schemeAndPath); + assertNotMatch(pattern, pathOnly); + assertNotMatch(pattern, pathOnlyWithSlash); + + pattern = new UriPattern("scheme*://host*/path*"); + assertNotMatch(pattern, schemeOnly); + assertNotMatch(pattern, schemeAndPath); + assertNotMatch(pattern, pathOnly); + assertNotMatch(pattern, pathOnlyWithSlash); + + pattern = new UriPattern("*://*/*"); + assertMatch(pattern, schemeOnly, Arrays.asList("scheme", "", "")); + assertMatch(pattern, schemeAndPath, Arrays.asList("scheme", "", "path")); + assertMatch(pattern, pathOnly, Arrays.asList("", "", "path")); + assertMatch(pattern, pathOnlyWithSlash, Arrays.asList("", "", "path")); + } + + @Test + public void requireThatUriWithoutPathDoesNotThrowException() { + UriPattern pattern = new UriPattern("scheme://host/path"); + assertNotMatch(pattern, "scheme://host"); + + pattern = new UriPattern("scheme://host/*"); + assertMatch(pattern, "scheme://host", Arrays.asList("")); + } + + @Test + public void requireThatOnlySchemeHostPortAndPathIsMatched() { + UriPattern pattern = new UriPattern("scheme://host:69/path"); + assertMatch(pattern, "scheme://host:69/path?foo", NO_GROUPS); + assertMatch(pattern, "scheme://host:69/path?foo#bar", NO_GROUPS); + } + + @Test + public void requireThatHostSupportsWildcard() { + UriPattern pattern = new UriPattern("scheme://*.host/path"); + assertMatch(pattern, "scheme://a.host/path", Arrays.asList("a")); + assertMatch(pattern, "scheme://a.b.host/path", Arrays.asList("a.b")); + } + + @Test + public void requireThatPrioritiesAreOrderedDescending() { + assertCompareLt(new UriPattern("scheme://host:69/path", 1), + new UriPattern("scheme://host:69/path", 0)); + } + + @Test + public void requireThatPriorityOrdersBeforeScheme() { + assertCompareLt(new UriPattern("*://host:69/path", 1), + new UriPattern("scheme://host:69/path", 0)); + } + + @Test + public void requireThatSchemesAreOrdered() { + assertCompareLt("b://host:69/path", + "a://host:69/path"); + } + + @Test + public void requireThatSchemeOrdersBeforeHost() { + assertCompareLt("b://*:69/path", + "a://host:69/path"); + } + + @Test + public void requireThatHostsAreOrdered() { + assertCompareLt("scheme://b:69/path", + "scheme://a:69/path"); + } + + @Test + public void requireThatHostOrdersBeforePath() { + assertCompareLt("scheme://b:69/*", + "scheme://a:69/path"); + } + + @Test + public void requireThatPortsAreOrdered() { + for (int i = 1; i < 69; ++i) { + assertCompareEq("scheme://host:" + i + "/path", + "scheme://host:" + i + "/path"); + assertCompareLt("scheme://host:" + (i + 1) + "/path", + "scheme://host:" + i + "/path"); + assertCompareLt("scheme://host:" + i + "/path", + "scheme://host:*/path"); + } + } + + @Test + public void requireThatPathsAreOrdered() { + assertCompareLt("scheme://host:69/b", + "scheme://host:69/a"); + } + + @Test + public void requireThatPathOrdersBeforePort() { + assertCompareLt("scheme://host:*/b", + "scheme://host:69/a"); + } + + @Test + public void requireThatEqualPatternsOrderEqual() { + assertCompareEq("scheme://host:69/path", + "scheme://host:69/path"); + assertCompareEq("*://host:69/path", + "*://host:69/path"); + assertCompareEq("scheme://*:69/path", + "scheme://*:69/path"); + assertCompareEq("scheme://host:*/path", + "scheme://host:*/path"); + assertCompareEq("scheme://host:69/*", + "scheme://host:69/*"); + } + + @Test + public void requireThatStrictPatternsOrderBeforeWildcards() { + assertCompareLt("scheme://host:69/path", + "*://host:69/path"); + assertCompareLt("scheme://a:69/path", + "scheme://*:69/path"); + assertCompareLt("scheme://a:69/path", + "scheme://*a:69/path"); + assertCompareLt("scheme://*aa:69/path", + "scheme://*a:69/path"); + assertCompareLt("scheme://host:69/path", + "scheme://host:*/path"); + assertCompareLt("scheme://host:69/a", + "scheme://host:69/*"); + assertCompareLt("scheme://host:69/a", + "scheme://host:69/a*"); + assertCompareLt("scheme://host:69/aa*", + "scheme://host:69/a*"); + assertCompareLt("scheme://*:69/path", + "*://host:69/path"); + assertCompareLt("scheme://host:*/path", + "scheme://*:69/path"); + assertCompareLt("scheme://host:*/path", + "scheme://host:69/*"); + assertCompareLt("scheme://host:69/foo", + "scheme://host:69/*"); + assertCompareLt("scheme://host:69/foo/bar", + "scheme://host:69/foo/*"); + assertCompareLt("scheme://host:69/foo/bar/baz", + "scheme://host:69/foo/bar/*"); + } + + @Test + public void requireThatLongPatternsOrderBeforeShort() { + assertCompareLt("scheme://host:69/foo/bar", + "scheme://host:69/foo"); + assertCompareLt("scheme://host:69/foo/bar/baz", + "scheme://host:69/foo/bar"); + } + + private static void assertIllegalPattern(String uri) { + try { + new UriPattern(uri); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + } + + private static void assertCompareLt(String lhs, String rhs) { + assertCompareLt(new UriPattern(lhs, 0), new UriPattern(rhs, 0)); + } + + private static void assertCompareLt(UriPattern lhs, UriPattern rhs) { + assertEquals(-1, compare(lhs, rhs)); + } + + private static void assertCompareEq(String lhs, String rhs) { + assertCompareEq(new UriPattern(lhs, 0), new UriPattern(rhs, 0)); + } + + private static void assertCompareEq(UriPattern lhs, UriPattern rhs) { + assertEquals(0, compare(lhs, rhs)); + } + + private static int compare(UriPattern lhs, UriPattern rhs) { + int lhsCmp = lhs.compareTo(rhs); + int rhsCmp = rhs.compareTo(lhs); + if (lhsCmp < 0) { + assertTrue(rhsCmp > 0); + return -1; + } + if (lhsCmp > 0) { + assertTrue(rhsCmp < 0); + return 1; + } + assertTrue(rhsCmp == 0); + return 0; + } + + private static void assertMatch(UriPattern pattern, String uri, List<String> expected) { + UriPattern.Match match = pattern.match(URI.create(uri)); + assertNotNull(match); + List<String> actual = new ArrayList<>(match.groupCount()); + for (int i = 0, len = match.groupCount(); i < len; ++i) { + actual.add(match.group(i)); + } + assertEquals(expected, actual); + } + + private static void assertNotMatch(UriPattern pattern, String uri) { + assertNull(pattern.match(URI.create(uri))); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/BindingMatchingTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/BindingMatchingTestCase.java new file mode 100644 index 00000000000..7c858a38b80 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/BindingMatchingTestCase.java @@ -0,0 +1,126 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.benchmark; + +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.UriPattern; +import org.junit.Test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class BindingMatchingTestCase { + + private static final int NUM_CANDIDATES = 1024; + private static final int NUM_MATCHES = 100; + private static final int MIN_THREADS = 1; + private static final int MAX_THREADS = 64; + private static final Random random = new Random(); + private static final ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS); + + @Test + public void runThroughtputMeasurements() throws Exception { + System.err.format("%15s%15s%15s%15s%15s%15s%15s%15s\n", + "No. of Bindings", "1 thread", "2 thread", "4 thread", "8 thread", "16 thread", "32 thread", "64 thread"); + for (int numBindings : Arrays.asList(1, 10, 25, 50, 100, 250)) { + BindingRepository<Object> repo = new BindingRepository<>(); + for (int binding = 0; binding < numBindings; ++binding) { + repo.bind("http://*/v" + binding + "/*/data/", new Object()); + } + System.err.format("%15s", numBindings + " binding(s)"); + + List<URI> candidates = newCandidates(repo); + measureThroughput(repo.activate(), candidates, MAX_THREADS); // warmup + + BindingSet<Object> bindings = repo.activate(); + for (int numThreads = MIN_THREADS; + numThreads <= MAX_THREADS; + numThreads *= 2) + { + System.err.format("%15s", measureThroughput(bindings, candidates, numThreads)); + } + System.err.format("\n"); + } + } + + private static long measureThroughput(BindingSet<Object> bindings, List<URI> candidates, int numThreads) throws Exception { + List<MatchTask> tasks = new LinkedList<>(); + for (int i = 0; i < numThreads; ++i) { + MatchTask task = new MatchTask(bindings, candidates); + tasks.add(task); + } + List<Future<Long>> results = executor.invokeAll(tasks); + long nanos = 0; + for (Future<Long> res : results) { + nanos = Math.max(nanos, res.get()); + } + return (numThreads * NUM_MATCHES * TimeUnit.SECONDS.toNanos(1)) / nanos; + } + + private List<URI> newCandidates(BindingRepository<Object> bindings) { + List<URI> lst = new ArrayList<>(NUM_CANDIDATES); + Iterator<Map.Entry<UriPattern, Object>> it = bindings.iterator(); + for (int i = 0; i < NUM_CANDIDATES; ++i) { + if (!it.hasNext()) { + it = bindings.iterator(); + } + lst.add(newCandidate(it.next().getKey())); + } + return lst; + } + + private URI newCandidate(UriPattern key) { + String pattern = key.toString(); + StringBuilder uri = new StringBuilder(); + for (int i = 0, len = pattern.length(); i < len; ++i) { + char c = pattern.charAt(i); + if (c == '*') { + uri.append(random.nextInt(Integer.MAX_VALUE)); + } else { + uri.append(c); + } + } + return URI.create(uri.toString()); + } + + private static class MatchTask implements Callable<Long> { + + final BindingSet<Object> bindings; + final List<URI> candidates; + + MatchTask(BindingSet<Object> bindings, List<URI> candidates) { + this.bindings = bindings; + this.candidates = candidates; + } + + @Override + public Long call() throws Exception { + Iterator<URI> it = candidates.iterator(); + for (int i = 0, len = random.nextInt(candidates.size()); i < len; ++i) { + it.next(); + } + long time = System.nanoTime(); + for (int i = 0; i < NUM_MATCHES; ++i) { + if (!it.hasNext()) { + it = candidates.iterator(); + } + bindings.match(it.next()); + } + return System.nanoTime() - time; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/LatencyTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/LatencyTestCase.java new file mode 100644 index 00000000000..ec5d1d2f908 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/LatencyTestCase.java @@ -0,0 +1,264 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.benchmark; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class LatencyTestCase { + + private static final int NUM_REQUESTS = 100; + + @Test + public void runLatencyMeasurements() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + MyRequestHandler foo = new MyRequestHandler("foo"); + MyRequestHandler bar = new MyRequestHandler("bar"); + MyRequestHandler baz = new MyRequestHandler("baz"); + builder.serverBindings().bind(foo.uri, foo); + builder.serverBindings().bind(bar.uri, bar); + builder.serverBindings().bind(baz.uri, baz); + driver.activateContainer(builder); + + measureLatencies(NUM_REQUESTS, driver, foo, bar, baz); + TimeTrack time = measureLatencies(NUM_REQUESTS, driver, foo, bar, baz); + System.err.println("\n" + time); + + foo.release(); + bar.release(); + baz.release(); + assertTrue(driver.close()); + } + + private static TimeTrack measureLatencies(int numRequests, CurrentContainer container, + MyRequestHandler... requestHandlers) + { + TimeTrack track = new TimeTrack(); + Random rnd = new Random(); + for (int i = 0; i < numRequests; ++i) { + track.add(measureLatency(container, requestHandlers[rnd.nextInt(requestHandlers.length)])); + } + return track; + } + + private static TimeFrame measureLatency(CurrentContainer container, MyRequestHandler requestHandler) { + TimeFrame frame = new TimeFrame(); + + Request request = null; + ContentChannel requestContent = null; + MyResponseHandler responseHandler = new MyResponseHandler(); + try { + URI uri = URI.create(requestHandler.uri); + request = new Request(container, uri); + frame.handleRequestBegin = System.nanoTime(); + requestContent = request.connect(responseHandler); + frame.handleRequestEnd = requestHandler.handleTime; + } finally { + if (request != null) { + request.release(); + } + } + ByteBuffer buf = ByteBuffer.allocate(69); + MyCompletion requestWrite = new MyCompletion(); + frame.requestWriteBegin = System.nanoTime(); + requestContent.write(buf, requestWrite); + frame.requestWriteEnd = requestHandler.requestContent.writeTime; + frame.requestWriteCompletionBegin = System.nanoTime(); + requestHandler.requestContent.writeCompletion.completed(); + frame.requestWriteCompletionEnd = requestWrite.completedTime; + + MyCompletion requestClose = new MyCompletion(); + frame.requestCloseBegin = System.nanoTime(); + requestContent.close(requestClose); + frame.requestCloseEnd = requestHandler.requestContent.closeTime; + frame.requestCloseCompletionBegin = System.nanoTime(); + requestHandler.requestContent.closeCompletion.completed(); + frame.requestCloseCompletionEnd = requestClose.completedTime; + + Response response = new Response(Response.Status.OK); + frame.handleResponseBegin = System.nanoTime(); + ContentChannel responseContent = requestHandler.responseHandler.handleResponse(response); + frame.handleResponseEnd = responseHandler.handleTime; + MyCompletion responseWrite = new MyCompletion(); + frame.responseWriteBegin = System.nanoTime(); + responseContent.write(buf, responseWrite); + frame.responseWriteEnd = responseHandler.responseContent.writeTime; + frame.responseWriteCompletionBegin = System.nanoTime(); + responseHandler.responseContent.writeCompletion.completed(); + frame.responseWriteCompletionEnd = responseWrite.completedTime; + + MyCompletion responseClose = new MyCompletion(); + frame.responseCloseBegin = System.nanoTime(); + responseContent.close(responseClose); + frame.responseCloseEnd = responseHandler.responseContent.closeTime; + frame.responseCloseCompletionBegin = System.nanoTime(); + responseHandler.responseContent.closeCompletion.completed(); + frame.responseCloseCompletionEnd = responseClose.completedTime; + + return frame; + } + + private static class MyRequestHandler extends AbstractRequestHandler { + + final MyContent requestContent = new MyContent(); + final String uri; + long handleTime; + Request request; + ResponseHandler responseHandler; + + MyRequestHandler(String path) { + this.uri = "http://localhost/" + path; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + handleTime = System.nanoTime(); + this.request = request; + responseHandler = handler; + return requestContent; + } + } + + private static class MyResponseHandler implements ResponseHandler { + + final MyContent responseContent = new MyContent(); + long handleTime; + + @Override + public ContentChannel handleResponse(Response response) { + handleTime = System.nanoTime(); + return responseContent; + } + } + + private static class MyContent implements ContentChannel { + + long writeTime; + long closeTime; + CompletionHandler writeCompletion; + CompletionHandler closeCompletion; + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + writeTime = System.nanoTime(); + writeCompletion = handler; + } + + @Override + public void close(CompletionHandler handler) { + closeTime = System.nanoTime(); + closeCompletion = handler; + } + } + + private static class MyCompletion implements CompletionHandler { + + long completedTime; + + @Override + public void completed() { + completedTime = System.nanoTime(); + } + + @Override + public void failed(Throwable t) { + + } + } + + private static class TimeFrame { + + long handleRequestBegin; + long handleRequestEnd; + long requestWriteBegin; + long requestWriteEnd; + long requestWriteCompletionBegin; + long requestWriteCompletionEnd; + long requestCloseBegin; + long requestCloseEnd; + long requestCloseCompletionBegin; + long requestCloseCompletionEnd; + long handleResponseBegin; + long handleResponseEnd; + long responseWriteBegin; + long responseWriteEnd; + long responseWriteCompletionBegin; + long responseWriteCompletionEnd; + long responseCloseBegin; + long responseCloseEnd; + long responseCloseCompletionBegin; + long responseCloseCompletionEnd; + } + + private static class TimeTrack { + + long frameCnt = 0; + long handleRequest; + long requestWrite; + long requestWriteCompletion; + long requestClose; + long requestCloseCompletion; + long handleResponse; + long responseWrite; + long responseWriteCompletion; + long responseClose; + long responseCloseCompletion; + + public void add(TimeFrame frame) { + ++frameCnt; + handleRequest += frame.handleRequestEnd - frame.handleRequestBegin; + requestWrite += frame.requestWriteEnd - frame.requestWriteBegin; + requestWriteCompletion += frame.requestWriteCompletionEnd - frame.requestWriteCompletionBegin; + requestClose += frame.requestCloseEnd - frame.requestCloseBegin; + requestCloseCompletion += frame.requestCloseCompletionEnd - frame.requestCloseCompletionBegin; + handleResponse += frame.handleResponseEnd - frame.handleResponseBegin; + responseWrite += frame.responseWriteEnd - frame.responseWriteBegin; + responseWriteCompletion += frame.responseWriteCompletionEnd - frame.responseWriteCompletionBegin; + responseClose += frame.responseCloseEnd - frame.responseCloseBegin; + responseCloseCompletion += frame.responseCloseCompletionEnd - frame.responseCloseCompletionBegin; + } + + @Override + public String toString() { + StringBuilder ret = new StringBuilder(); + ret.append("------------------------------------\n"); + ret.append(String.format("HandleRequest : %10.2f\n", (double)handleRequest / frameCnt)); + ret.append(String.format("RequestWrite : %10.2f\n", (double)requestWrite / frameCnt)); + ret.append(String.format("RequestWriteCompletion : %10.2f\n", (double)requestWriteCompletion / frameCnt)); + ret.append(String.format("RequestClose : %10.2f\n", (double)requestClose / frameCnt)); + ret.append(String.format("RequestCloseCompletion : %10.2f\n", (double)requestCloseCompletion / frameCnt)); + ret.append(String.format("HandleResponse : %10.2f\n", (double)handleResponse / frameCnt)); + ret.append(String.format("ResponseWrite : %10.2f\n", (double)responseWrite / frameCnt)); + ret.append(String.format("ResponseWriteCompletion : %10.2f\n", (double)responseWriteCompletion / frameCnt)); + ret.append(String.format("ResponseClose : %10.2f\n", (double)responseClose / frameCnt)); + ret.append(String.format("ResponseCloseCompletion : %10.2f\n", (double)responseCloseCompletion / frameCnt)); + ret.append("------------------------------------\n"); + + double time = (handleRequest + requestWrite + requestWriteCompletion + requestClose + + requestCloseCompletion + handleResponse + responseWrite + responseWriteCompletion + + responseClose + responseCloseCompletion) / frameCnt; + ret.append(String.format("Total nanos : %10.2f\n", time)); + ret.append(String.format("Requests per second : %10.2f\n", TimeUnit.SECONDS.toNanos(1) / time)); + ret.append("------------------------------------\n"); + return ret.toString(); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/ThroughputTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/ThroughputTestCase.java new file mode 100644 index 00000000000..54a94e3e2dd --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/ThroughputTestCase.java @@ -0,0 +1,180 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.benchmark; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CallableResponseDispatch; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestDispatch; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ThroughputTestCase { + + private static final int NUM_REQUESTS = 100; + private static final int MIN_THREADS = 1; + private static final int MAX_THREADS = 64; + private static final int MIN_LOOPS = 0; + private static final int MAX_LOOPS = 1024; + + private static final String HANDLER_URI = "http://localhost/"; + private static final URI REQUEST_URI = URI.create(HANDLER_URI); + private static final ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS * 2); + private static long preventOptimization = 0; + + @Test + public void runUnthreadedMeasurementsWithWorkload() throws Exception { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + runMeasurements(driver, new UnthreadedHandler(MAX_LOOPS)); // warmup + + StringBuilder out = new StringBuilder(); + out.append("\n"); + out.append(" | "); + for (int i = MIN_THREADS; i <= MAX_THREADS; i *= 2) { + out.append(String.format("%10d", i)); + } + out.append("\n"); + out.append("------+-"); + for (int i = MIN_THREADS; i <= MAX_THREADS; i *= 2) { + out.append("----------"); + } + out.append("\n"); + for (int i = MIN_LOOPS; i <= MAX_LOOPS; i = Math.max(1, i * 2)) { + out.append(String.format("%5d | ", i)); + RequestHandler handler = new UnthreadedHandler(i); + for (Long val : runMeasurements(driver, handler)) { + out.append(String.format("%10d", val)); + } + out.append("\n"); + } + System.err.println(out); + System.err.println(preventOptimization); + assertTrue(driver.close()); + } + + @Test + public void runThreadedMeasurements() throws Exception { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + runMeasurements(driver, new ThreadedHandler()); // warmup + + Iterator<Long> it = runMeasurements(driver, new ThreadedHandler()).iterator(); + for (int numThreads = MIN_THREADS; numThreads <= MAX_THREADS; numThreads *= 2) { + System.err.println(String.format("%2d threads: %10d", numThreads, it.next())); + } + assertTrue(driver.close()); + } + + private static List<Long> runMeasurements(TestDriver driver, RequestHandler handler) throws Exception { + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind(HANDLER_URI, handler); + driver.activateContainer(builder); + handler.release(); + List<Long> ret = new LinkedList<>(); + for (int i = MIN_THREADS; i <= MAX_THREADS; i *= 2) { + ret.add(measureThroughput(driver, i)); + } + return ret; + } + + private static long measureThroughput(CurrentContainer container, int numThreads) throws Exception { + List<RequestTask> tasks = new LinkedList<>(); + for (int i = 0; i < numThreads; ++i) { + RequestTask task = new RequestTask(container); + tasks.add(task); + } + List<Future<Long>> results = executor.invokeAll(tasks); + long nanos = 0; + for (Future<Long> res : results) { + nanos = Math.max(nanos, res.get()); + } + return (numThreads * NUM_REQUESTS * TimeUnit.SECONDS.toNanos(1)) / nanos; + } + + private static class RequestTask implements Callable<Long> { + + final CurrentContainer container; + + RequestTask(CurrentContainer container) { + this.container = container; + } + + @Override + public Long call() throws Exception { + long time = System.nanoTime(); + for (int i = 0; i < NUM_REQUESTS; ++i) { + new RequestDispatch() { + + @Override + protected Request newRequest() { + Request request = new Request(container, REQUEST_URI); + request.setTimeout(600, TimeUnit.SECONDS); + return request; + } + }.dispatch().get(); + } + return System.nanoTime() - time; + } + } + + private static class UnthreadedHandler extends AbstractRequestHandler { + + final int numLoops; + + UnthreadedHandler(int numLoops) { + this.numLoops = numLoops; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + ResponseDispatch.newInstance(Response.Status.OK).dispatch(handler); + preventOptimization += nextLong(); + return null; + } + + long nextLong() { + Random rnd = new Random(); + int k = 0; + for (int i = 0; i < numLoops; ++i) { + k += rnd.nextInt(); + } + return k; + } + } + + private static class ThreadedHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + executor.submit(new CallableResponseDispatch(handler) { + + @Override + public Response newResponse() { + return new Response(Response.Status.OK); + } + }); + return null; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/UriMatchingTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/UriMatchingTestCase.java new file mode 100644 index 00000000000..df2402d1283 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/UriMatchingTestCase.java @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.benchmark; + +import com.yahoo.jdisc.application.UriPattern; +import org.junit.Test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class UriMatchingTestCase { + + private static final int NUM_MATCHES = 100; + private static long preventOptimization = 0; + + @Test + public void requireThatUriPatternMatchingIsFast() { + List<String> inputs = Arrays.asList( + "other://host/", + "scheme://other/", + "scheme://host/", + "scheme://host/foo", + "scheme://host/foo/bar", + "scheme://host/foo/bar/baz", + "scheme://host/other", + "scheme://host/other/bar", + "scheme://host/other/bar/baz", + "scheme://host/foo/other", + "scheme://host/foo/other/baz", + "scheme://host/foo/bar/other", + "scheme://host:69/", + "scheme://host:69/foo", + "scheme://host:69/foo/bar", + "scheme://host:69/foo/bar/baz", + "scheme://host:96/"); + benchmarkMatch("*://*/*", inputs); // warmup + + runBenchmark("*://*/*", inputs); + runBenchmark("scheme://*/*", inputs); + runBenchmark("scheme://host/*", inputs); + runBenchmark("scheme://host:69/*", inputs); + runBenchmark("scheme://host:69/foo", inputs); + runBenchmark("scheme://host:69/foo/bar", inputs); + runBenchmark("scheme://host:69/foo/bar/baz", inputs); + runBenchmark("*://host:69/foo/bar/baz", inputs); + runBenchmark("*://*/foo/*", inputs); + runBenchmark("*://*/foo/*/baz", inputs); + runBenchmark("*://*/foo/bar/*", inputs); + runBenchmark("*://*/foo/bar/baz", inputs); + runBenchmark("*://*/*/bar", inputs); + runBenchmark("*://*/*/bar/baz", inputs); + runBenchmark("*://*/*/*/baz", inputs); + + System.out.println(">>>>> " + preventOptimization); + } + + private static void runBenchmark(String pattern, List<String> inputs) { + System.out.format("%-30s %10d\n", pattern, benchmarkMatch(pattern, inputs)); + } + + private static long benchmarkMatch(String pattern, List<String> inputs) { + UriPattern compiled = new UriPattern(pattern); + List<URI> uriList = new ArrayList<>(inputs.size()); + for (String input : inputs) { + uriList.add(URI.create(input)); + } + long now = System.nanoTime(); + for (int i = 0; i < NUM_MATCHES; ++i) { + for (URI uri : uriList) { + UriPattern.Match match = compiled.match(uri); + preventOptimization += match != null ? match.groupCount() : 1; + } + } + return TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - now); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/client/AbstractClientApplicationTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/client/AbstractClientApplicationTestCase.java new file mode 100644 index 00000000000..3f04d86c170 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/client/AbstractClientApplicationTestCase.java @@ -0,0 +1,138 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.client; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.yahoo.jdisc.application.BundleInstaller; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.service.CurrentContainer; +import org.junit.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class AbstractClientApplicationTestCase { + + @Test + public void requireThatApplicationCanBeShutdown() throws Exception { + MyDriver driver = newDriver(); + assertFalse(driver.awaitDone(100, TimeUnit.MILLISECONDS)); + assertTrue(driver.awaitApp(600, TimeUnit.SECONDS)); + driver.app.shutdown(); + assertTrue(driver.app.isShutdown()); + assertTrue(driver.close()); + } + + @Test + public void requireThatShutdownCanBeWaitedForWithTimeout() throws Exception { + final MyDriver driver = newDriver(); + assertFalse(driver.awaitDone(100, TimeUnit.MILLISECONDS)); + assertTrue(driver.awaitApp(600, TimeUnit.SECONDS)); + + final CountDownLatch latch = new CountDownLatch(1); + Executors.newSingleThreadExecutor().submit(new Callable<Boolean>() { + + @Override + public Boolean call() throws Exception { + driver.app.awaitShutdown(600, TimeUnit.SECONDS); + latch.countDown(); + return Boolean.TRUE; + } + }); + assertFalse(latch.await(100, TimeUnit.MILLISECONDS)); + driver.app.shutdown(); + assertTrue(driver.close()); + assertTrue(latch.await(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatShutdownCanBeWaitedForWithoutTimeout() throws Exception { + final MyDriver driver = newDriver(); + assertFalse(driver.awaitDone(100, TimeUnit.MILLISECONDS)); + assertTrue(driver.awaitApp(600, TimeUnit.SECONDS)); + + final CountDownLatch latch = new CountDownLatch(1); + Executors.newSingleThreadExecutor().submit(new Callable<Boolean>() { + + @Override + public Boolean call() throws Exception { + driver.app.awaitShutdown(); + latch.countDown(); + return Boolean.TRUE; + } + }); + assertFalse(latch.await(100, TimeUnit.MILLISECONDS)); + driver.app.shutdown(); + assertTrue(driver.close()); + assertTrue(latch.await(600, TimeUnit.SECONDS)); + } + + private static MyDriver newDriver() { + final MyDriver driver = new MyDriver(); + driver.done = Executors.newSingleThreadExecutor().submit(new Callable<Boolean>() { + + @Override + public Boolean call() throws Exception { + ClientDriver.runApplication(MyApplication.class, driver); + return Boolean.TRUE; + } + }); + return driver; + } + + private static class MyDriver extends AbstractModule { + + final CountDownLatch appLatch = new CountDownLatch(1); + Future<Boolean> done; + MyApplication app; + + @Override + protected void configure() { + bind(MyDriver.class).toInstance(this); + } + + boolean awaitApp(int timeout, TimeUnit unit) throws InterruptedException { + return appLatch.await(timeout, unit); + } + + boolean awaitDone(int timeout, TimeUnit unit) throws ExecutionException, InterruptedException { + try { + done.get(timeout, unit); + return app.isTerminated(); + } catch (TimeoutException e) { + return false; + } + } + + boolean close() throws ExecutionException, InterruptedException { + return awaitDone(600, TimeUnit.SECONDS); + } + } + + private static class MyApplication extends AbstractClientApplication { + + @Inject + MyApplication(BundleInstaller bundleInstaller, ContainerActivator activator, + CurrentContainer container, MyDriver driver) { + super(bundleInstaller, activator, container); + driver.app = this; + driver.appLatch.countDown(); + } + + @Override + public void start() { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/client/ClientDriverTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/client/ClientDriverTestCase.java new file mode 100644 index 00000000000..bef78a22de7 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/client/ClientDriverTestCase.java @@ -0,0 +1,78 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.client; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ClientDriverTestCase { + + @Test + public void requireThatApplicationInstanceInjectionWorks() throws Exception { + MyModule module = new MyModule(); + ClientDriver.runApplication(new MyApplication(module)); + assertEquals(5, module.state); + } + + @Test + public void requireThatApplicationClassInjectionWorks() throws Exception { + MyModule module = new MyModule(); + ClientDriver.runApplication(MyApplication.class, module); + assertEquals(5, module.state); + } + + private static class MyApplication implements ClientApplication { + + final MyModule module; + + @Inject + MyApplication(MyModule module) { + this.module = module; + module.state = 1; + } + + @Override + public void start() { + if (++module.state != 2) { + throw new IllegalStateException(); + } + } + + @Override + public void run() { + if (++module.state != 3) { + throw new IllegalStateException(); + } + } + + @Override + public void stop() { + if (++module.state != 4) { + throw new IllegalStateException(); + } + } + + @Override + public void destroy() { + if (++module.state != 5) { + throw new IllegalStateException(); + } + } + } + + private static class MyModule extends AbstractModule { + + int state = 0; + + @Override + protected void configure() { + bind(MyModule.class).toInstance(this); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerFinalizerTest.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerFinalizerTest.java new file mode 100644 index 00000000000..b2fd357b30c --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerFinalizerTest.java @@ -0,0 +1,75 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.test.TestDriver; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +@SuppressWarnings("UnusedAssignment") +public class ActiveContainerFinalizerTest { + + @Test + public void requireThatMissingContainerReleaseDoesNotPreventShutdown() throws InterruptedException { + final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + Container container = driver.newReference(URI.create("scheme://host")); + assertNotNull(container); + + final Termination termination = new Termination(); + driver.activateContainer(null).notifyTermination(termination); + assertFalse(termination.await(100, TimeUnit.MILLISECONDS)); + + container = null; // intentionally doing this instead of container.release() + assertTrue(termination.await(600, TimeUnit.SECONDS)); + assertTrue(driver.close()); + } + + @Test + public void requireThatMissingRequestReleaseDoesNotPreventShutdown() throws InterruptedException { + final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + Request request = new Request(driver, URI.create("scheme://host")); + assertNotNull(request); + + final Termination termination = new Termination(); + driver.activateContainer(null).notifyTermination(termination); + assertFalse(termination.await(100, TimeUnit.MILLISECONDS)); + + request = null; // intentionally doing this instead of request.release() + assertTrue(termination.await(600, TimeUnit.SECONDS)); + assertTrue(driver.close()); + } + + private static class Termination implements Runnable { + + volatile boolean done; + + @Override + public void run() { + done = true; + } + + boolean await(final int timeout, final TimeUnit unit) throws InterruptedException { + final long timeoutAt = System.currentTimeMillis() + unit.toMillis(timeout); + while (!done) { + if (System.currentTimeMillis() > timeoutAt) { + return false; + } + Thread.sleep(10); + System.gc(); + } + return true; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerTestCase.java new file mode 100644 index 00000000000..5d61e55b7b4 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerTestCase.java @@ -0,0 +1,160 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.UriPattern; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.service.ServerProvider; +import com.yahoo.jdisc.test.NonWorkingRequestHandler; +import com.yahoo.jdisc.test.NonWorkingServerProvider; +import com.yahoo.jdisc.test.TestDriver; + +import java.util.Iterator; +import java.util.Map; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ActiveContainerTestCase { + + @Test + public void requireThatGuiceAccessorWorks() { + final Object obj = new Object(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() { + + @Override + protected void configure() { + bind(Object.class).toInstance(obj); + } + }); + ActiveContainer container = new ActiveContainer(driver.newContainerBuilder()); + assertSame(obj, container.guiceInjector().getInstance(Object.class)); + driver.close(); + } + + @Test + public void requireThatServerAccessorWorks() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + ServerProvider foo = new NonWorkingServerProvider(); + builder.serverProviders().install(foo); + ServerProvider bar = new NonWorkingServerProvider(); + builder.serverProviders().install(bar); + ActiveContainer container = new ActiveContainer(builder); + + Iterator<ServerProvider> it = container.serverProviders().iterator(); + assertTrue(it.hasNext()); + assertSame(foo, it.next()); + assertTrue(it.hasNext()); + assertSame(bar, it.next()); + assertFalse(it.hasNext()); + driver.close(); + } + + @Test + public void requireThatServerBindingAccessorWorks() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler bar = new NonWorkingRequestHandler(); + builder.serverBindings().bind("http://host/foo", foo); + builder.serverBindings("bar").bind("http://host/bar", bar); + ActiveContainer container = new ActiveContainer(builder); + + Map<String, BindingSet<RequestHandler>> bindings = container.serverBindings(); + assertNotNull(bindings); + assertEquals(2, bindings.size()); + + BindingSet<RequestHandler> set = bindings.get(BindingSet.DEFAULT); + assertNotNull(set); + Iterator<Map.Entry<UriPattern, RequestHandler>> it = set.iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + Map.Entry<UriPattern, RequestHandler> entry = it.next(); + assertNotNull(entry); + assertEquals(new UriPattern("http://host/foo"), entry.getKey()); + assertSame(foo, entry.getValue()); + assertFalse(it.hasNext()); + + assertNotNull(set = bindings.get("bar")); + assertNotNull(it = set.iterator()); + assertTrue(it.hasNext()); + assertNotNull(entry = it.next()); + assertEquals(new UriPattern("http://host/bar"), entry.getKey()); + assertSame(bar, entry.getValue()); + assertFalse(it.hasNext()); + + assertNotNull(bindings = container.clientBindings()); + assertEquals(1, bindings.size()); + assertNotNull(set = bindings.get(BindingSet.DEFAULT)); + assertNotNull(it = set.iterator()); + assertFalse(it.hasNext()); + + driver.close(); + } + + @Test + public void requireThatClientBindingAccessorWorks() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + RequestHandler foo = new NonWorkingRequestHandler(); + RequestHandler bar = new NonWorkingRequestHandler(); + builder.clientBindings().bind("http://host/foo", foo); + builder.clientBindings("bar").bind("http://host/bar", bar); + ActiveContainer container = new ActiveContainer(builder); + + Map<String, BindingSet<RequestHandler>> bindings = container.clientBindings(); + assertNotNull(bindings); + assertEquals(2, bindings.size()); + + BindingSet<RequestHandler> set = bindings.get(BindingSet.DEFAULT); + assertNotNull(set); + Iterator<Map.Entry<UriPattern, RequestHandler>> it = set.iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + Map.Entry<UriPattern, RequestHandler> entry = it.next(); + assertNotNull(entry); + assertEquals(new UriPattern("http://host/foo"), entry.getKey()); + assertSame(foo, entry.getValue()); + assertFalse(it.hasNext()); + + assertNotNull(set = bindings.get("bar")); + assertNotNull(it = set.iterator()); + assertTrue(it.hasNext()); + assertNotNull(entry = it.next()); + assertEquals(new UriPattern("http://host/bar"), entry.getKey()); + assertSame(bar, entry.getValue()); + assertFalse(it.hasNext()); + + assertNotNull(bindings = container.serverBindings()); + assertEquals(1, bindings.size()); + assertNotNull(set = bindings.get(BindingSet.DEFAULT)); + assertNotNull(it = set.iterator()); + assertFalse(it.hasNext()); + + driver.close(); + } + + @Test + public void requireThatDefaultBindingsAreAlwaysCreated() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + ActiveContainer container = new ActiveContainer(builder); + + Map<String, BindingSet<RequestHandler>> bindings = container.serverBindings(); + assertNotNull(bindings); + assertEquals(1, bindings.size()); + BindingSet<RequestHandler> set = bindings.get(BindingSet.DEFAULT); + assertFalse(set.iterator().hasNext()); + driver.close(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationConfigModuleTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationConfigModuleTestCase.java new file mode 100644 index 00000000000..1d23d671e0f --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationConfigModuleTestCase.java @@ -0,0 +1,114 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.name.Names; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.junit.Test; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertEquals; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ApplicationConfigModuleTestCase { + + @Test + public void requireThatEntriesAreBoundWithLowerCaseKeys() { + Map<String, String> config = new HashMap<>(); + config.put("foo_key", "foo"); + config.put("BAR_key", "bar"); + config.put("BAZ_KEY", "baz"); + + Injector injector = Guice.createInjector(new ApplicationConfigModule(config)); + assertBinding(injector, "foo_key", "foo"); + assertBinding(injector, "bar_key", "bar"); + assertBinding(injector, "baz_key", "baz"); + } + + @Test + public void requireThatEntriesAreBoundWithUnmodifiedValue() { + Map<String, String> config = new HashMap<>(); + config.put("foo", "foo_val"); + config.put("bar", "BAR_val"); + config.put("baz", "BAZ_VAL"); + + Injector injector = Guice.createInjector(new ApplicationConfigModule(config)); + assertBinding(injector, "foo", "foo_val"); + assertBinding(injector, "bar", "BAR_val"); + assertBinding(injector, "baz", "BAZ_VAL"); + } + + @Test + public void requireThatUpperCaseKeysPrecedeLowerCaseKeys() { + Map<String, String> config = new HashMap<>(); + config.put("foo", "lower-case"); + assertBinding(config, "foo", "lower-case"); + + config.put("Foo", "mixed-case 1"); + assertBinding(config, "foo", "mixed-case 1"); + + config.put("FOO", "upper-case"); + assertBinding(config, "foo", "upper-case"); + + config.put("FOo", "mixed-case 2"); + assertBinding(config, "foo", "upper-case"); + } + + @Test + public void requireThatNullFileNameThrowsException() throws IOException { + try { + ApplicationConfigModule.newInstanceFromFile(null); + fail(); + } catch (NullPointerException e) { + + } + } + + @Test + public void requireThatFileNotFoundThrowsException() throws IOException { + try { + ApplicationConfigModule.newInstanceFromFile("/file/not/found"); + fail(); + } catch (FileNotFoundException e) { + + } + } + + @Test + public void requireThatPropertieFilesCanBeRead() throws IOException { + Properties props = new Properties(); + props.put("foo_key", "foo_val"); + + File file = File.createTempFile("config-", ".properties"); + file.deleteOnExit(); + FileOutputStream out = new FileOutputStream(file); + props.store(out, null); + out.close(); + + assertBinding(ApplicationConfigModule.newInstanceFromFile(file.getAbsolutePath()), "foo_key", "foo_val"); + } + + private static void assertBinding(Map<String, String> config, String stringName, String expected) { + assertBinding(new ApplicationConfigModule(config), stringName, expected); + } + + private static void assertBinding(Module module, String stringName, String expected) { + assertBinding(Guice.createInjector(module), stringName, expected); + } + + private static void assertBinding(Injector injector, String stringName, String expected) { + assertEquals(expected, injector.getInstance(Key.get(String.class, Names.named(stringName)))); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationEnvironmentModuleTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationEnvironmentModuleTestCase.java new file mode 100644 index 00000000000..77af705cfac --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationEnvironmentModuleTestCase.java @@ -0,0 +1,57 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.*; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.OsgiFramework; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.NonWorkingOsgiFramework; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadFactory; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ApplicationEnvironmentModuleTestCase { + + @Test + public void requireThatBindingsExist() { + List<Class> expected = new LinkedList<>(); + expected.add(ContainerActivator.class); + expected.add(ContainerBuilder.class); + expected.add(CurrentContainer.class); + expected.add(OsgiFramework.class); + expected.add(ThreadFactory.class); + + Injector injector = Guice.createInjector(); + for (Map.Entry<Key<?>, Binding<?>> entry : injector.getBindings().entrySet()) { + expected.add(entry.getKey().getTypeLiteral().getRawType()); + } + + ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(), + Collections.<Module>emptyList()); + injector = Guice.createInjector(new ApplicationEnvironmentModule(loader)); + for (Map.Entry<Key<?>, Binding<?>> entry : injector.getBindings().entrySet()) { + assertNotNull(expected.remove(entry.getKey().getTypeLiteral().getRawType())); + } + assertTrue(expected.isEmpty()); + } + + @Test + public void requireThatContainerBuilderCanBeInjected() { + ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(), + Collections.<Module>emptyList()); + assertNotNull(new ApplicationEnvironmentModule(loader).containerBuilder()); + assertNotNull(Guice.createInjector(new ApplicationEnvironmentModule(loader)) + .getInstance(ContainerBuilder.class)); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationLoaderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationLoaderTestCase.java new file mode 100644 index 00000000000..398fbcba839 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationLoaderTestCase.java @@ -0,0 +1,259 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.google.inject.ConfigurationException; +import com.google.inject.Module; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.Application; +import com.yahoo.jdisc.application.ApplicationNotReadyException; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.NonWorkingOsgiFramework; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; +import org.osgi.framework.BundleContext; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ApplicationLoaderTestCase { + + @Test + public void requireThatStartFailsWithoutApplication() throws Exception { + ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(), + Collections.<Module>emptyList()); + try { + loader.init(null, false); + loader.start(); + fail(); + } catch (ConfigurationException e) { + + } + } + + @Test + public void requireThatStopDoesNotFailWithoutStart() throws Exception { + ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(), + Collections.<Module>emptyList()); + loader.stop(); + loader.destroy(); + } + + @Test + public void requireThatDestroyDoesNotFailWithActiveContainer() throws Exception { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + assertNull(driver.activateContainer(driver.newContainerBuilder())); + assertTrue(driver.close()); + } + + @Test + public void requireThatApplicationStartExceptionUnsetsAndDestroysApplication() throws Exception { + MyApplication app = MyApplication.newStartException(); + ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(), + Arrays.asList(new MyApplicationModule(app))); + loader.init(null, false); + try { + loader.start(); + fail(); + } catch (MyException e) { + + } + assertNull(loader.application()); + assertFalse(app.stop.await(100, TimeUnit.MILLISECONDS)); + assertTrue(app.destroy.await(600, TimeUnit.SECONDS)); + try { + loader.activateContainer(loader.newContainerBuilder()); + fail(); + } catch (ApplicationNotReadyException e) { + + } + loader.stop(); + loader.destroy(); + } + + @Test + public void requireThatApplicationStopExceptionDestroysApplication() throws Exception { + MyApplication app = MyApplication.newStopException(); + ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(), + Arrays.asList(new MyApplicationModule(app))); + loader.init(null, false); + loader.start(); + try { + loader.stop(); + } catch (MyException e) { + + } + assertTrue(app.destroy.await(600, TimeUnit.SECONDS)); + loader.destroy(); + } + + @Test + public void requireThatApplicationDestroyIsCalledAfterContainerTermination() throws InterruptedException { + MyApplication app = MyApplication.newInstance(); + TestDriver driver = TestDriver.newInjectedApplicationInstance(app); + ContainerBuilder builder = driver.newContainerBuilder(); + MyRequestHandler requestHandler = new MyRequestHandler(); + builder.serverBindings().bind("scheme://host/path", requestHandler); + driver.activateContainer(builder); + driver.dispatchRequest("scheme://host/path", new MyResponseHandler()); + driver.scheduleClose(); + assertFalse(app.destroy.await(100, TimeUnit.MILLISECONDS)); + requestHandler.responseHandler.handleResponse(new Response(Response.Status.OK)).close(null); + assertTrue(app.destroy.await(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatContainerActivatorReturnsPrev() throws Exception { + TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.newInstance()); + assertNull(driver.activateContainer(driver.newContainerBuilder())); + assertNotNull(driver.activateContainer(null)); + assertTrue(driver.close()); + } + + @Test + public void requireThatOsgiServicesAreRegistered() { + TestDriver driver = TestDriver.newSimpleApplicationInstance(); + BundleContext ctx = driver.osgiFramework().bundleContext(); + Object service = ctx.getService(ctx.getServiceReference(CurrentContainer.class.getName())); + assertTrue(service instanceof CurrentContainer); + assertTrue(driver.close()); + } + + @Test + public void requireThatThreadFactoryCanBeBound() { + final ThreadFactory factory = Executors.defaultThreadFactory(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() { + + @Override + protected void configure() { + bind(ThreadFactory.class).toInstance(factory); + } + }); + ContainerBuilder builder = driver.newContainerBuilder(); + assertSame(factory, builder.getInstance(ThreadFactory.class)); + assertTrue(driver.close()); + } + + private static class MyApplicationModule extends AbstractModule { + + final Application application; + + public MyApplicationModule(Application application) { + this.application = application; + } + + @Override + protected void configure() { + bind(Application.class).toInstance(application); + } + } + + private static class MyApplication implements Application { + + final CountDownLatch start = new CountDownLatch(1); + final CountDownLatch stop = new CountDownLatch(1); + final CountDownLatch destroy = new CountDownLatch(1); + final boolean startException; + final boolean stopException; + + MyApplication(boolean startException, boolean stopException) { + this.startException = startException; + this.stopException = stopException; + } + + @Override + public void start() { + start.countDown(); + if (startException) { + throw new MyException(); + } + } + + @Override + public void stop() { + stop.countDown(); + if (stopException) { + throw new MyException(); + } + } + + @Override + public void destroy() { + destroy.countDown(); + } + + public static MyApplication newInstance() { + return new MyApplication(false, false); + } + + public static MyApplication newStartException() { + return new MyApplication(true, false); + } + + public static MyApplication newStopException() { + return new MyApplication(false, true); + } + } + + private static class MyRequestHandler extends AbstractRequestHandler { + + ResponseHandler responseHandler; + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + responseHandler = handler; + return new MyContentChannel(); + } + } + + private static class MyResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(Response response) { + return new MyContentChannel(); + } + } + + private static class MyContentChannel implements ContentChannel { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + if (handler != null) { + handler.completed(); + } + } + + @Override + public void close(CompletionHandler handler) { + if (handler != null) { + handler.completed(); + } + } + } + + private static class MyException extends RuntimeException { + + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationRestartTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationRestartTestCase.java new file mode 100644 index 00000000000..2943e44bc4c --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationRestartTestCase.java @@ -0,0 +1,153 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.Application; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.test.NonWorkingOsgiFramework; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ApplicationRestartTestCase { + + @Test + public void requireThatStopStartDoesNotBreakShutdown() throws Exception { + ApplicationLoader loader = newApplicationLoader(); + loader.init(null, false); + loader.start(); + assertGracefulStop(loader); + loader.start(); + assertGracefulStop(loader); + loader.destroy(); + } + + @Test + public void requireThatDestroyInitDoesNotBreakShutdown() throws Exception { + ApplicationLoader loader = newApplicationLoader(); + loader.init(null, false); + loader.start(); + assertGracefulStop(loader); + loader.destroy(); + loader.init(null, false); + loader.start(); + assertGracefulStop(loader); + loader.destroy(); + } + + private static ApplicationLoader newApplicationLoader() { + return new ApplicationLoader(new NonWorkingOsgiFramework(), + Arrays.asList(new AbstractModule() { + @Override + public void configure() { + bind(Application.class).to(SimpleApplication.class); + } + })); + } + + private static void assertGracefulStop(ApplicationLoader loader) throws Exception { + MyRequestHandler requestHandler = new MyRequestHandler(); + ContainerBuilder builder = loader.newContainerBuilder(); + builder.serverBindings().bind("http://host/path", requestHandler); + loader.activateContainer(builder); + + MyResponseHandler responseHandler = new MyResponseHandler(); + Request request = new Request(loader, URI.create("http://host/path")); + request.connect(responseHandler).close(null); + request.release(); + + StopTask task = new StopTask(loader); + task.start(); + assertFalse(task.latch.await(100, TimeUnit.MILLISECONDS)); + requestHandler.responseHandler.handleResponse(new Response(Response.Status.OK)).close(null); + assertTrue(task.latch.await(600, TimeUnit.SECONDS)); + } + + private static class StopTask extends Thread { + + final ApplicationLoader loader; + final CountDownLatch latch = new CountDownLatch(1); + + StopTask(ApplicationLoader loader) { + this.loader = loader; + } + + @Override + public void run() { + try { + loader.stop(); + } catch (Exception e) { + e.printStackTrace(); + return; + } + latch.countDown(); + } + } + + private static class SimpleApplication implements Application { + + @Override + public void start() { + + } + + @Override + public void stop() { + + } + + @Override + public void destroy() { + + } + } + + private static class MyRequestHandler extends AbstractRequestHandler { + + ResponseHandler responseHandler; + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + this.responseHandler = handler; + return new MyContentChannel(); + } + } + + private static class MyResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(Response response) { + return new MyContentChannel(); + } + } + + private static class MyContentChannel implements ContentChannel { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(CompletionHandler handler) { + handler.completed(); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationShutdownTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationShutdownTestCase.java new file mode 100644 index 00000000000..986ceddb3e4 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationShutdownTestCase.java @@ -0,0 +1,122 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ApplicationShutdownTestCase { + + @Test + public void requireThatStopWaitsForPreviousContainer() throws Exception { + Context ctx = new Context(); + MyRequestHandler requestHandler = new MyRequestHandler(); + ctx.activateContainer(requestHandler); + ctx.dispatchRequest(); + ctx.activateContainer(null); + ctx.driver.scheduleClose(); + assertFalse(ctx.driver.awaitClose(100, TimeUnit.MILLISECONDS)); + requestHandler.respond(); + assertTrue(ctx.driver.awaitClose(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatStopWaitsForAllPreviousContainers() { + Context ctx = new Context(); + MyRequestHandler requestHandlerA = new MyRequestHandler(); + ctx.activateContainer(requestHandlerA); + ctx.dispatchRequest(); + + MyRequestHandler requestHandlerB = new MyRequestHandler(); + ctx.activateContainer(requestHandlerB); + ctx.dispatchRequest(); + + MyRequestHandler requestHandlerC = new MyRequestHandler(); + ctx.activateContainer(requestHandlerC); + ctx.dispatchRequest(); + + ctx.driver.scheduleClose(); + assertFalse(ctx.driver.awaitClose(100, TimeUnit.MILLISECONDS)); + requestHandlerB.respond(); + assertFalse(ctx.driver.awaitClose(100, TimeUnit.MILLISECONDS)); + requestHandlerC.respond(); + assertFalse(ctx.driver.awaitClose(100, TimeUnit.MILLISECONDS)); + requestHandlerA.respond(); + assertTrue(ctx.driver.awaitClose(600, TimeUnit.SECONDS)); + } + + private static class Context { + + final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + + void activateContainer(RequestHandler requestHandler) { + ContainerBuilder builder; + if (requestHandler != null) { + builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://host/path", requestHandler); + } else { + builder = null; + } + driver.activateContainer(builder); + } + + void dispatchRequest() { + Request request = new Request(driver, URI.create("http://host/path")); + request.connect(new MyResponseHandler()).close(null); + request.release(); + } + } + + private static class MyRequestHandler extends AbstractRequestHandler { + + ResponseHandler handler = null; + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + this.handler = handler; + return new MyContent(); + } + + void respond() { + handler.handleResponse(new Response(Response.Status.OK)).close(null); + } + } + + private static class MyResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(Response response) { + return new MyContent(); + } + } + + private static class MyContent implements ContentChannel { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(CompletionHandler handler) { + handler.completed(); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java new file mode 100644 index 00000000000..bc80358b760 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java @@ -0,0 +1,154 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.apache.commons.daemon.DaemonContext; +import org.apache.commons.daemon.DaemonController; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BootstrapDaemonTestCase { + + @Test + public void requireThatPrivilegedLifecycleWorks() throws Exception { + MyLoader loader = new MyLoader(); + BootstrapDaemon daemon = new BootstrapDaemon(loader, true); + daemon.init(new MyContext("foo")); + assertTrue(loader.hasState(true, false, false, false)); + assertTrue(loader.privileged); + daemon.start(); + assertTrue(loader.hasState(true, true, false, false)); + daemon.stop(); + assertTrue(loader.hasState(true, true, true, false)); + daemon.destroy(); + assertTrue(loader.hasState(true, true, true, true)); + } + + @Test + public void requireThatNonPrivilegedLifecycleWorks() throws Exception { + MyLoader loader = new MyLoader(); + BootstrapDaemon daemon = new BootstrapDaemon(loader, false); + daemon.init(new MyContext("foo")); + assertTrue(loader.hasState(false, false, false, false)); + daemon.start(); + assertTrue(loader.hasState(true, true, false, false)); + assertFalse(loader.privileged); + daemon.stop(); + assertTrue(loader.hasState(true, true, true, false)); + daemon.destroy(); + assertTrue(loader.hasState(true, true, true, true)); + } + + @Test + public void requireThatBundleLocationIsRequired() throws Exception { + MyLoader loader = new MyLoader(); + BootstrapDaemon daemon = new BootstrapDaemon(loader, true); + try { + daemon.init(new MyContext((String[])null)); + fail(); + } catch (IllegalArgumentException e) { + assertNull(loader.bundleLocation); + } + try { + daemon.init(new MyContext()); + fail(); + } catch (IllegalArgumentException e) { + assertNull(loader.bundleLocation); + } + try { + daemon.init(new MyContext((String)null)); + fail(); + } catch (IllegalArgumentException e) { + assertNull(loader.bundleLocation); + } + try { + daemon.init(new MyContext("foo", "bar")); + fail(); + } catch (IllegalArgumentException e) { + assertNull(loader.bundleLocation); + } + + daemon.init(new MyContext("foo")); + daemon.start(); + + assertNotNull(loader.bundleLocation); + assertEquals("foo", loader.bundleLocation); + + daemon.stop(); + daemon.destroy(); + } + + @Test + public void requireThatEnvironmentIsRequired() { + try { + new BootstrapDaemon(); + fail(); + } catch (IllegalStateException e) { + + } + } + + private static class MyLoader implements BootstrapLoader { + + String bundleLocation = null; + boolean privileged = false; + boolean initCalled = false; + boolean startCalled = false; + boolean stopCalled = false; + boolean destroyCalled = false; + + boolean hasState(boolean initCalled, boolean startCalled, boolean stopCalled, boolean destroyCalled) { + return this.initCalled == initCalled && this.startCalled == startCalled && + this.stopCalled == stopCalled && this.destroyCalled == destroyCalled; + } + + @Override + public void init(String bundleLocation, boolean privileged) throws Exception { + this.bundleLocation = bundleLocation; + this.privileged = privileged; + initCalled = true; + } + + @Override + public void start() throws Exception { + startCalled = true; + } + + @Override + public void stop() throws Exception { + stopCalled = true; + } + + @Override + public void destroy() { + destroyCalled = true; + } + } + + private static class MyContext implements DaemonContext { + + final String[] args; + + MyContext(String... args) { + this.args = args; + } + + @Override + public DaemonController getController() { + return null; + } + + @Override + public String[] getArguments() { + return args; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/BundleLocationResolverTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/BundleLocationResolverTestCase.java new file mode 100644 index 00000000000..843ca91db68 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/BundleLocationResolverTestCase.java @@ -0,0 +1,87 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class BundleLocationResolverTestCase { + + @Test + public void requireThatDollarsAreIncludedInLocation() { + assertLocation("scheme:$foo", "scheme:$foo"); + assertLocation("scheme:foo$bar", "scheme:foo$bar"); + } + + @Test + public void requireThatCurlyBracesAreIncludedInLocation() { + assertLocation("scheme:{foo", "scheme:{foo"); + assertLocation("scheme:foo{", "scheme:foo{"); + assertLocation("scheme:foo{bar", "scheme:foo{bar"); + assertLocation("scheme:}foo", "scheme:}foo"); + assertLocation("scheme:foo}", "scheme:foo}"); + assertLocation("scheme:foo}bar", "scheme:foo}bar"); + assertLocation("scheme:{foo}bar", "scheme:{foo}bar"); + assertLocation("scheme:foo{bar}", "scheme:foo{bar}"); + } + + @Test + public void requireThatUnterminatedPropertiesAreIncludedInLocation() { + assertLocation("scheme:${foo", "scheme:${foo"); + assertLocation("scheme:foo${", "scheme:foo${"); + assertLocation("scheme:foo${bar", "scheme:foo${bar"); + } + + @Test + public void requireThatAllSystemPropertiesAreExpanded() throws IOException { + assertCanonicalPath("", "${foo}"); + assertCanonicalPath("barcox", "${foo}bar${baz}cox"); + assertCanonicalPath("foobaz", "foo${bar}baz${cox}"); + + System.setProperty("requireThatAllSystemPropertiesAreExpanded.foo", "FOO"); + System.setProperty("requireThatAllSystemPropertiesAreExpanded.bar", "BAR"); + System.setProperty("requireThatAllSystemPropertiesAreExpanded.baz", "BAZ"); + System.setProperty("requireThatAllSystemPropertiesAreExpanded.cox", "COX"); + assertCanonicalPath("FOO", "${requireThatAllSystemPropertiesAreExpanded.foo}"); + assertCanonicalPath("FOObarBAZcox", "${requireThatAllSystemPropertiesAreExpanded.foo}bar" + + "${requireThatAllSystemPropertiesAreExpanded.baz}cox"); + assertCanonicalPath("fooBARbazCOX", "foo${requireThatAllSystemPropertiesAreExpanded.bar}" + + "baz${requireThatAllSystemPropertiesAreExpanded.cox}"); + } + + @Test + public void requireThatUnschemedLocationsAreExpandedToBundleLocationProperty() throws IOException { + assertCanonicalPath(BundleLocationResolver.BUNDLE_PATH + "foo", "foo"); + } + + @Test + public void requireThatFileSchemedLocationsAreCanonicalized() throws IOException { + assertCanonicalPath("", "file:"); + assertCanonicalPath("foo", "file:foo"); + assertCanonicalPath("foo", "file:./foo"); + assertCanonicalPath("foo/bar", "file:foo/bar"); + assertCanonicalPath("foo/bar", "file:./foo/../foo/./bar"); + assertCanonicalPath("foo", " \f\n\r\tfile:foo"); + } + + @Test + public void requireThatOtherSchemedLocationsAreUntouched() { + assertLocation("foo:", "foo:"); + assertLocation("foo:bar", "foo:bar"); + assertLocation("foo:bar/baz", "foo:bar/baz"); + } + + private static void assertCanonicalPath(String expected, String bundleLocation) throws IOException { + assertLocation("file:" + new File(expected).getCanonicalPath(), bundleLocation); + } + + private static void assertLocation(String expected, String bundleLocation) { + assertEquals(expected, BundleLocationResolver.resolve(bundleLocation)); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogFormatterTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogFormatterTestCase.java new file mode 100644 index 00000000000..901817dbd26 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogFormatterTestCase.java @@ -0,0 +1,270 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogService; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +import static org.junit.Assert.assertEquals; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ConsoleLogFormatterTestCase { + + private static final ConsoleLogFormatter SIMPLE_FORMATTER = new ConsoleLogFormatter(null, null, null); + private static final LogEntry SIMPLE_ENTRY = new MyEntry(0, 0, null); + + // TODO: Should (at least) use ConsoleLogFormatter.ABSENCE_REPLACEMENT instead of literal '-'. See ticket 7128315. + + @Test + public void requireThatMillisecondsArePadded() { + for (int i = 0; i < 10000; ++i) { + LogEntry entry = new MyEntry(i, 0, null); + assertEquals(String.format("%d.%03d\t-\t-\t-\t-\tunknown\t", i / 1000, i % 1000), + SIMPLE_FORMATTER.formatEntry(entry)); + } + } + + @Test + public void requireThatHostNameIsIncluded() { + assertEquals("0.000\thostName\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter("hostName", null, null).formatEntry(SIMPLE_ENTRY)); + } + + @Test + public void requireThatHostNameIsOptional() { + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter(null, null, null).formatEntry(SIMPLE_ENTRY)); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter("", null, null).formatEntry(SIMPLE_ENTRY)); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter(" ", null, null).formatEntry(SIMPLE_ENTRY)); + } + + @Test + public void requireThatProcessIdIsIncluded() { + assertEquals("0.000\t-\tprocessId\t-\t-\tunknown\t", + new ConsoleLogFormatter(null, "processId", null).formatEntry(SIMPLE_ENTRY)); + } + + @Test + public void requireThatProcessIdIsOptional() { + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter(null, null, null).formatEntry(SIMPLE_ENTRY)); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter(null, "", null).formatEntry(SIMPLE_ENTRY)); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter(null, " ", null).formatEntry(SIMPLE_ENTRY)); + } + + @Test + public void requireThatProcessIdIncludesThreadIdWhenAvailable() { + LogEntry entry = new MyEntry(0, 0, null).putProperty("THREAD_ID", "threadId"); + assertEquals("0.000\t-\tprocessId/threadId\t-\t-\tunknown\t", + new ConsoleLogFormatter(null, "processId", null).formatEntry(entry)); + } + + @Test + public void requireThatServiceNameIsIncluded() { + assertEquals("0.000\t-\t-\tserviceName\t-\tunknown\t", + new ConsoleLogFormatter(null, null, "serviceName").formatEntry(SIMPLE_ENTRY)); + } + + @Test + public void requireThatServiceNameIsOptional() { + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter(null, null, null).formatEntry(SIMPLE_ENTRY)); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter(null, null, "").formatEntry(SIMPLE_ENTRY)); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + new ConsoleLogFormatter(null, null, " ").formatEntry(SIMPLE_ENTRY)); + } + + @Test + public void requireThatBundleNameIsIncluded() { + LogEntry entry = new MyEntry(0, 0, null).setBundleSymbolicName("bundleName"); + assertEquals("0.000\t-\t-\t-\tbundleName\tunknown\t", + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatBundleNameIsOptional() { + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + SIMPLE_FORMATTER.formatEntry(SIMPLE_ENTRY)); + } + + @Test + public void requireThatLoggerNameIsIncluded() { + LogEntry entry = new MyEntry(0, 0, null).putProperty("LOGGER_NAME", "loggerName"); + assertEquals("0.000\t-\t-\t-\t/loggerName\tunknown\t", + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatLoggerNameIsOptional() { + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + SIMPLE_FORMATTER.formatEntry(SIMPLE_ENTRY)); + } + + @Test + public void requireThatBundleAndLoggerNameIsCombined() { + LogEntry entry = new MyEntry(0, 0, null).setBundleSymbolicName("bundleName") + .putProperty("LOGGER_NAME", "loggerName"); + assertEquals("0.000\t-\t-\t-\tbundleName/loggerName\tunknown\t", + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatLevelNameIsIncluded() { + ConsoleLogFormatter formatter = SIMPLE_FORMATTER; + assertEquals("0.000\t-\t-\t-\t-\terror\t", + formatter.formatEntry(new MyEntry(0, LogService.LOG_ERROR, null))); + assertEquals("0.000\t-\t-\t-\t-\twarning\t", + formatter.formatEntry(new MyEntry(0, LogService.LOG_WARNING, null))); + assertEquals("0.000\t-\t-\t-\t-\tinfo\t", + formatter.formatEntry(new MyEntry(0, LogService.LOG_INFO, null))); + assertEquals("0.000\t-\t-\t-\t-\tdebug\t", + formatter.formatEntry(new MyEntry(0, LogService.LOG_DEBUG, null))); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + formatter.formatEntry(new MyEntry(0, 69, null))); + } + + @Test + public void requireThatMessageIsIncluded() { + LogEntry entry = new MyEntry(0, 0, "message"); + assertEquals("0.000\t-\t-\t-\t-\tunknown\tmessage", + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatMessageIsOptional() { + LogEntry entry = new MyEntry(0, 0, null); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t", + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatMessageIsEscaped() { + LogEntry entry = new MyEntry(0, 0, "\\\n\r\t"); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t\\\\\\n\\r\\t", + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatExceptionIsIncluded() { + Throwable t = new Throwable(); + LogEntry entry = new MyEntry(0, 0, null).setException(t); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t\\n" + formatThrowable(t), + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatExceptionIsEscaped() { + Throwable t = new Throwable("\\\n\r\t"); + LogEntry entry = new MyEntry(0, 0, null).setException(t); + assertEquals("0.000\t-\t-\t-\t-\tunknown\t\\n" + formatThrowable(t), + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatExceptionIsSimplifiedForInfoEntries() { + Throwable t = new Throwable("exception"); + LogEntry entry = new MyEntry(0, LogService.LOG_INFO, "entry").setException(t); + assertEquals("0.000\t-\t-\t-\t-\tinfo\tentry: exception", + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatSimplifiedExceptionIsEscaped() { + Throwable t = new Throwable("\\\n\r\t"); + LogEntry entry = new MyEntry(0, LogService.LOG_INFO, "entry").setException(t); + assertEquals("0.000\t-\t-\t-\t-\tinfo\tentry: \\\\\\n\\r\\t", + SIMPLE_FORMATTER.formatEntry(entry)); + } + + @Test + public void requireThatSimplifiedExceptionMessageIsOptional() { + Throwable t = new Throwable(); + LogEntry entry = new MyEntry(0, LogService.LOG_INFO, "entry").setException(t); + assertEquals("0.000\t-\t-\t-\t-\tinfo\tentry: java.lang.Throwable", + SIMPLE_FORMATTER.formatEntry(entry)); + } + + private static String formatThrowable(Throwable t) { + Writer out = new StringWriter(); + t.printStackTrace(new PrintWriter(out)); + return out.toString().replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t"); + } + + private static class MyEntry implements LogEntry { + + final String message; + final int level; + final long time; + Bundle bundle = null; + ServiceReference serviceReference = null; + Throwable exception; + + MyEntry(long time, int level, String message) { + this.message = message; + this.level = level; + this.time = time; + } + + MyEntry setBundleSymbolicName(String symbolicName) { + this.bundle = Mockito.mock(Bundle.class); + Mockito.doReturn(symbolicName).when(this.bundle).getSymbolicName(); + return this; + } + + MyEntry setException(Throwable exception) { + this.exception = exception; + return this; + } + + MyEntry putProperty(String key, String val) { + this.serviceReference = Mockito.mock(ServiceReference.class); + Mockito.doReturn(val).when(this.serviceReference).getProperty(key); + return this; + } + + @Override + public long getTime() { + return time; + } + + @Override + public int getLevel() { + return level; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public Throwable getException() { + return exception; + } + + @Override + public Bundle getBundle() { + return bundle; + } + + @Override + public ServiceReference getServiceReference() { + return serviceReference; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogListenerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogListenerTestCase.java new file mode 100644 index 00000000000..3ac30f0456e --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogListenerTestCase.java @@ -0,0 +1,115 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogListener; +import org.osgi.service.log.LogService; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +public class ConsoleLogListenerTestCase { + + private static final String HOSTNAME = ConsoleLogFormatter.formatOptional(ConsoleLogListener.getHostname()); + private static final String PROCESS_ID = ConsoleLogListener.getProcessId(); + + @Test + public void requireThatLogLevelParserKnowsOsgiLogLevels() { + assertEquals(LogService.LOG_ERROR, ConsoleLogListener.parseLogLevel("ERROR")); + assertEquals(LogService.LOG_WARNING, ConsoleLogListener.parseLogLevel("WARNING")); + assertEquals(LogService.LOG_INFO, ConsoleLogListener.parseLogLevel("INFO")); + assertEquals(LogService.LOG_DEBUG, ConsoleLogListener.parseLogLevel("DEBUG")); + } + + @Test + public void requireThatLogLevelParserKnowsOff() { + assertEquals(Integer.MIN_VALUE, ConsoleLogListener.parseLogLevel("OFF")); + } + + @Test + public void requireThatLogLevelParserKnowsAll() { + assertEquals(Integer.MAX_VALUE, ConsoleLogListener.parseLogLevel("ALL")); + } + + @Test + public void requireThatLogLevelParserKnowsIntegers() { + for (int i = -69; i < 69; ++i) { + assertEquals(i, ConsoleLogListener.parseLogLevel(String.valueOf(i))); + } + } + + @Test + public void requireThatLogLevelParserErrorsReturnDefault() { + assertEquals(ConsoleLogListener.DEFAULT_LOG_LEVEL, ConsoleLogListener.parseLogLevel(null)); + assertEquals(ConsoleLogListener.DEFAULT_LOG_LEVEL, ConsoleLogListener.parseLogLevel("")); + assertEquals(ConsoleLogListener.DEFAULT_LOG_LEVEL, ConsoleLogListener.parseLogLevel("foo")); + } + + @Test + public void requireThatLogEntryWithLevelAboveThresholdIsNotOutput() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + LogListener listener = new ConsoleLogListener(new PrintStream(out), null, "5"); + for (int i = 0; i < 10; ++i) { + listener.logged(new MyEntry(0, i, "message")); + } + // TODO: Should use ConsoleLogFormatter.ABSENCE_REPLACEMENT instead of literal '-'. See ticket 7128315. + assertEquals("0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\tunknown\tmessage\n" + + "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\terror\tmessage\n" + + "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\twarning\tmessage\n" + + "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\tinfo\tmessage\n" + + "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\tdebug\tmessage\n" + + "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\tunknown\tmessage\n", + out.toString()); + } + + private static class MyEntry implements LogEntry { + + final String message; + final int level; + final long time; + + MyEntry(long time, int level, String message) { + this.message = message; + this.level = level; + this.time = time; + } + + @Override + public long getTime() { + return time; + } + + @Override + public int getLevel() { + return level; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public Throwable getException() { + return null; + } + + @Override + public Bundle getBundle() { + return null; + } + + @Override + public ServiceReference getServiceReference() { + return null; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogManagerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogManagerTestCase.java new file mode 100644 index 00000000000..5bc1a29e2f7 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogManagerTestCase.java @@ -0,0 +1,90 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.service.log.LogListener; +import org.osgi.service.log.LogReaderService; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ConsoleLogManagerTestCase { + + @Test + public void requireThatManagerCanNotBeInstalledTwice() throws BundleException { + FelixFramework felix = TestDriver.newOsgiFramework(); + felix.start(); + + ConsoleLogManager manager = new ConsoleLogManager(); + manager.install(felix.bundleContext()); + try { + manager.install(felix.bundleContext()); + fail(); + } catch (IllegalStateException e) { + assertEquals("ConsoleLogManager already installed.", e.getMessage()); + } + + felix.stop(); + } + + @Test + public void requireThatManagerCanBeUninstalledTwice() throws BundleException { + FelixFramework felix = TestDriver.newOsgiFramework(); + felix.start(); + + ConsoleLogManager manager = new ConsoleLogManager(); + assertFalse(manager.uninstall()); + manager.install(felix.bundleContext()); + assertTrue(manager.uninstall()); + assertFalse(manager.uninstall()); + + felix.stop(); + } + + @Test + public void requireThatLogReaderServicesAreTracked() throws BundleException { + FelixFramework felix = TestDriver.newOsgiFramework(); + felix.start(); + BundleContext ctx = felix.bundleContext(); + + LogReaderService foo = Mockito.mock(LogReaderService.class); + ctx.registerService(LogReaderService.class.getName(), foo, null); + Mockito.verify(foo).addLogListener(Mockito.any(LogListener.class)); + + LogReaderService bar = Mockito.mock(LogReaderService.class); + ctx.registerService(LogReaderService.class.getName(), bar, null); + Mockito.verify(bar).addLogListener(Mockito.any(LogListener.class)); + + ConsoleLogManager manager = new ConsoleLogManager(); + manager.install(felix.bundleContext()); + + Mockito.verify(foo, Mockito.times(2)).addLogListener(Mockito.any(LogListener.class)); + Mockito.verify(bar, Mockito.times(2)).addLogListener(Mockito.any(LogListener.class)); + + LogReaderService baz = Mockito.mock(LogReaderService.class); + ctx.registerService(LogReaderService.class.getName(), baz, null); + Mockito.verify(baz, Mockito.times(2)).addLogListener(Mockito.any(LogListener.class)); + + assertTrue(manager.uninstall()); + + Mockito.verify(foo).removeLogListener(Mockito.any(LogListener.class)); + Mockito.verify(bar).removeLogListener(Mockito.any(LogListener.class)); + Mockito.verify(baz).removeLogListener(Mockito.any(LogListener.class)); + + felix.stop(); + + Mockito.verify(foo, Mockito.times(2)).removeLogListener(Mockito.any(LogListener.class)); + Mockito.verify(bar, Mockito.times(2)).removeLogListener(Mockito.any(LogListener.class)); + Mockito.verify(baz, Mockito.times(2)).removeLogListener(Mockito.any(LogListener.class)); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerResourceTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerResourceTestCase.java new file mode 100644 index 00000000000..11b6f27296d --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerResourceTestCase.java @@ -0,0 +1,162 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.ServerProvider; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerResourceTestCase { + + @Test + public void requireThatBoundRequestHandlersAreRetainedOnActivate() { + MyRequestHandler foo = new MyRequestHandler(); + MyRequestHandler bar = new MyRequestHandler(); + + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings("foo").bind("http://foo/", foo); + builder.serverBindings("bar").bind("http://bar/", bar); + assertEquals(0, foo.retainCnt.get()); + assertEquals(0, bar.retainCnt.get()); + + driver.activateContainer(builder); + assertEquals(1, foo.retainCnt.get()); + assertEquals(1, bar.retainCnt.get()); + assertTrue(driver.close()); + } + + @Test + public void requireThatBoundRequestHandlersAreReleasedOnTermination() { + MyRequestHandler handler = new MyRequestHandler(); + + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://localhost/", handler); + driver.activateContainer(builder); + + Container container = driver.newReference(URI.create("http://localhost/")); + assertEquals(1, handler.retainCnt.get()); + driver.activateContainer(null); + assertEquals(1, handler.retainCnt.get()); + container.release(); + assertEquals(0, handler.retainCnt.get()); + + assertTrue(driver.close()); + } + + @Test + public void requireThatServerProvidersAreRetainedOnActivate() { + MyServerProvider foo = new MyServerProvider(); + MyServerProvider bar = new MyServerProvider(); + + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverProviders().install(foo); + builder.serverProviders().install(bar); + assertEquals(0, foo.retainCnt.get()); + assertEquals(0, bar.retainCnt.get()); + + driver.activateContainer(builder); + assertEquals(1, foo.retainCnt.get()); + assertEquals(1, bar.retainCnt.get()); + assertTrue(driver.close()); + } + + @Test + public void requireThatServerProvidersAreReleasedOnTermination() { + MyServerProvider server = new MyServerProvider(); + + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverProviders().install(server); + driver.activateContainer(builder); + + Container container = driver.newReference(URI.create("http://localhost/")); + assertEquals(1, server.retainCnt.get()); + driver.activateContainer(null); + assertEquals(1, server.retainCnt.get()); + container.release(); + assertEquals(0, server.retainCnt.get()); + + assertTrue(driver.close()); + } + + private static class MyRequestHandler implements RequestHandler { + + final AtomicInteger retainCnt = new AtomicInteger(0); + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public void handleTimeout(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public ResourceReference refer() { + retainCnt.incrementAndGet(); + return new ResourceReference() { + @Override + public void close() { + retainCnt.decrementAndGet(); + } + }; + } + + @Override + public void release() { + retainCnt.decrementAndGet(); + } + } + + private static class MyServerProvider implements ServerProvider { + + final AtomicInteger retainCnt = new AtomicInteger(0); + + @Override + public void start() { + + } + + @Override + public void close() { + + } + + @Override + public ResourceReference refer() { + retainCnt.incrementAndGet(); + return new ResourceReference() { + @Override + public void close() { + retainCnt.decrementAndGet(); + } + }; + } + + @Override + public void release() { + retainCnt.decrementAndGet(); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerShutdownTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerShutdownTestCase.java new file mode 100644 index 00000000000..488628867b4 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerShutdownTestCase.java @@ -0,0 +1,848 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerShutdownTestCase { + + @Test + public void requireThatContainerBlocksTermination() { + Context ctx = Context.newInstance(); + Container container = ctx.driver.newReference(URI.create("http://host/path")); + assertFalse(ctx.shutdown()); + container.release(); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatNewRequestBlocksTermination() { + Context ctx = Context.newPendingRequest(MyRequestHandler.newInstance()); + assertFalse(ctx.shutdown()); + ctx.request.release(); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatOpenRequestBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + ctx.request.release(); + requestHandler.respond().close(null); + assertFalse(ctx.shutdown()); + requestContent.close(null); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponsePendingBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + ctx.request.connect(MyResponseHandler.newEagerCompletion()).close(null); + ctx.request.release(); + assertFalse(ctx.shutdown()); + requestHandler.respond().close(null); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatOpenResponseBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + ctx.request.connect(MyResponseHandler.newEagerCompletion()).close(null); + ctx.request.release(); + ContentChannel responseContent = requestHandler.respond(); + assertFalse(ctx.shutdown()); + responseContent.close(null); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestExceptionDoesNotBlockTermination() { + Context ctx = Context.newPendingRequest(MyRequestHandler.newRequestException()); + try { + ctx.request.connect(MyResponseHandler.newEagerCompletion()); + fail(); + } catch (MyException e) { + // ignore + } + ctx.request.release(); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestExceptionWithEagerHandleResponseBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newRequestExceptionWithEagerHandleResponse(); + Context ctx = Context.newPendingRequest(requestHandler); + try { + ctx.request.connect(MyResponseHandler.newEagerCompletion()); + fail(); + } catch (MyException e) { + // ignore + } + ctx.request.release(); + assertFalse(ctx.shutdown()); + requestHandler.responseContent.close(null); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestExceptionWithEagerCloseResponseDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newRequestExceptionWithEagerCloseResponse(); + Context ctx = Context.newPendingRequest(requestHandler); + try { + ctx.request.connect(MyResponseHandler.newEagerCompletion()); + fail(); + } catch (MyException e) { + // ignore + } + ctx.request.release(); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatNullRequestContentBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newNullContent(); + Context ctx = Context.newPendingRequest(requestHandler); + ctx.request.connect(MyResponseHandler.newEagerCompletion()).close(null); + ctx.request.release(); + assertFalse(ctx.shutdown()); + requestHandler.respond(); + requestHandler.responseContent.close(null); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatNullRequestContentWithEagerHandleResponseBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newNullContentWithEagerHandleResponse(); + Context ctx = Context.newPendingRequest(requestHandler); + ctx.request.connect(MyResponseHandler.newEagerCompletion()).close(null); + ctx.request.release(); + assertFalse(ctx.shutdown()); + requestHandler.responseContent.close(null); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatNullRequestContentWithEagerCloseResponseBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newNulContentWithEagerCloseResponse(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + ctx.request.release(); + assertFalse(ctx.shutdown()); + requestContent.close(null); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestContentWriteFailedDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerFail(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + requestContent.write(ByteBuffer.allocate(69), null); + requestContent.close(null); + ctx.request.release(); + requestHandler.respond().close(null); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestContentWriteExceptionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newContentWriteExceptionWithEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + try { + requestContent.write(ByteBuffer.allocate(69), null); + fail(); + } catch (MyException e) { + // ignore + } + requestContent.close(null); + ctx.request.release(); + requestHandler.respond().close(null); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestContentWriteExceptionDoesNotForceTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newContentWriteExceptionWithEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + try { + requestContent.write(ByteBuffer.allocate(69), null); + fail(); + } catch (MyException e) { + // ignore + } + ctx.request.release(); + requestHandler.respond().close(null); + assertFalse(ctx.shutdown()); + requestContent.close(null); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestContentWriteExceptionWithCompletionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newContentWriteExceptionWithEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + try { + requestContent.write(ByteBuffer.allocate(69), MyCompletion.newInstance()); + fail(); + } catch (MyException e) { + // ignore + } + requestContent.close(null); + ctx.request.release(); + requestHandler.respond().close(null); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestContentCloseFailedDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerFail(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + requestContent.close(null); + ctx.request.release(); + requestHandler.respond().close(null); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestContentCloseExceptionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newContentCloseException(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + try { + requestContent.close(null); + fail(); + } catch (MyException e) { + // ignore + } + ctx.request.release(); + requestHandler.respond().close(null); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestContentCloseExceptionWithCompletionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newContentCloseException(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + try { + requestContent.close(MyCompletion.newInstance()); + fail(); + } catch (MyException e) { + // ignore + } + ctx.request.release(); + requestHandler.respond().close(null); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestWriteCompletionBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCloseResponse(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + ctx.request.release(); + requestContent.write(null, MyCompletion.newInstance()); + requestContent.close(null); + requestHandler.requestContent.closeCompletion.completed(); + assertFalse(ctx.shutdown()); + requestHandler.requestContent.writeCompletion.completed(); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestWriteCompletionExceptionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCloseResponse(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + ctx.request.release(); + requestContent.write(null, MyCompletion.newException()); + requestContent.close(null); + requestHandler.requestContent.closeCompletion.completed(); + assertFalse(ctx.shutdown()); + try { + requestHandler.requestContent.writeCompletion.completed(); + fail(); + } catch (MyException e) { + // ignore + } + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestCloseCompletionBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCloseResponse(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + ctx.request.release(); + requestContent.close(MyCompletion.newInstance()); + assertFalse(ctx.shutdown()); + requestHandler.requestContent.closeCompletion.completed(); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatRequestCloseCompletionExceptionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCloseResponse(); + Context ctx = Context.newPendingRequest(requestHandler); + ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion()); + ctx.request.release(); + requestContent.close(MyCompletion.newException()); + assertFalse(ctx.shutdown()); + try { + requestHandler.requestContent.closeCompletion.completed(); + fail(); + } catch (MyException e) { + // ignore + } + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatNullResponseContentBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newNullContent(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + + assertFalse(ctx.shutdown()); + requestHandler.responseContent.close(MyCompletion.newInstance()); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseExceptionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + ctx.request.connect(MyResponseHandler.newResponseException()).close(null); + ctx.request.release(); + try { + requestHandler.respond(); + fail(); + } catch (MyException e) { + // ignore + } + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseContentWriteFailedDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newEagerFail(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + requestHandler.respond(); + requestHandler.responseContent.write(ByteBuffer.allocate(69), null); + requestHandler.responseContent.close(null); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseContentCloseFailedDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newEagerFail(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + requestHandler.respond(); + requestHandler.responseContent.close(null); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseContentWriteExceptionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newContentWriteException(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + requestHandler.respond(); + try { + requestHandler.responseContent.write(ByteBuffer.allocate(69), null); + fail(); + } catch (MyException e) { + // ignore + } + requestHandler.responseContent.close(null); + responseHandler.content.closeCompletion.completed(); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseContentWriteExceptionDoesNotForceTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newContentWriteException(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + requestHandler.respond(); + try { + requestHandler.responseContent.write(ByteBuffer.allocate(69), null); + fail(); + } catch (MyException e) { + // ignore + } + assertFalse(ctx.shutdown()); + requestHandler.responseContent.close(null); + responseHandler.content.closeCompletion.completed(); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseContentWriteExceptionWithCompletionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newContentWriteException(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + requestHandler.respond(); + try { + requestHandler.responseContent.write(ByteBuffer.allocate(69), MyCompletion.newInstance()); + fail(); + } catch (MyException e) { + // ignore + } + requestHandler.responseContent.close(null); + responseHandler.content.closeCompletion.completed(); + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseContentCloseExceptionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + ctx.request.connect(MyResponseHandler.newContentCloseException()).close(null); + ctx.request.release(); + try { + requestHandler.respond().close(null); + fail(); + } catch (MyException e) { + // ignore + } + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseContentCloseExceptionWithCompletionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + ctx.request.connect(MyResponseHandler.newContentCloseException()).close(null); + ctx.request.release(); + try { + requestHandler.respond().close(MyCompletion.newInstance()); + fail(); + } catch (MyException e) { + // ignore + } + assertTrue(ctx.shutdown()); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseWriteCompletionBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + + requestHandler.responseContent.write(null, MyCompletion.newInstance()); + requestHandler.responseContent.close(null); + responseHandler.content.closeCompletion.completed(); + assertFalse(ctx.shutdown()); + responseHandler.content.writeCompletion.completed(); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseWriteCompletionExceptionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + + requestHandler.responseContent.write(null, MyCompletion.newException()); + requestHandler.responseContent.close(null); + responseHandler.content.closeCompletion.completed(); + assertFalse(ctx.shutdown()); + try { + responseHandler.content.writeCompletion.completed(); + fail(); + } catch (MyException e) { + // ignore + } + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseCloseCompletionBlocksTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + + requestHandler.responseContent.close(MyCompletion.newInstance()); + assertFalse(ctx.shutdown()); + responseHandler.content.closeCompletion.completed(); + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + @Test + public void requireThatResponseCloseCompletionExceptionDoesNotBlockTermination() { + MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion(); + Context ctx = Context.newPendingRequest(requestHandler); + MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + ctx.request.connect(responseHandler).close(null); + ctx.request.release(); + + requestHandler.responseContent.close(MyCompletion.newException()); + assertFalse(ctx.shutdown()); + try { + responseHandler.content.closeCompletion.completed(); + fail(); + } catch (MyException e) { + // ignore + } + assertTrue(ctx.terminated); + assertTrue(ctx.driver.close()); + } + + private static class Context { + + final TestDriver driver; + final Request request; + boolean terminated = false; + + Context(TestDriver driver, Request request) { + this.driver = driver; + this.request = request; + } + + boolean shutdown() { + driver.activateContainer(null).notifyTermination(new Runnable() { + + @Override + public void run() { + terminated = true; + } + }); + return terminated; + } + + static Context newInstance() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + return new Context(driver, null); + } + + static Context newPendingRequest(RequestHandler requestHandler) { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://host/path", requestHandler); + driver.activateContainer(builder); + return new Context(driver, new Request(driver, URI.create("http://host/path"))); + } + } + + private static class MyCompletion implements CompletionHandler { + + final boolean throwException; + + MyCompletion(boolean throwException) { + this.throwException = throwException; + } + + @Override + public void completed() { + if (throwException) { + throw new MyException(); + } + } + + @Override + public void failed(Throwable t) { + if (throwException) { + throw new MyException(); + } + } + + static MyCompletion newInstance() { + return new MyCompletion(false); + } + + static MyCompletion newException() { + return new MyCompletion(true); + } + } + + private static class MyContent implements ContentChannel { + + final boolean eagerCompletion; + final boolean eagerFail; + final boolean writeException; + final boolean closeException; + CompletionHandler writeCompletion = null; + CompletionHandler closeCompletion = null; + + MyContent(boolean eagerCompletion, boolean eagerFail, boolean writeException, boolean closeException) { + this.eagerCompletion = eagerCompletion; + this.eagerFail = eagerFail; + this.writeException = writeException; + this.closeException = closeException; + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + writeCompletion = handler; + if (eagerCompletion) { + writeCompletion.completed(); + } else if (eagerFail) { + writeCompletion.failed(new MyException()); + } + if (writeException) { + throw new MyException(); + } + } + + @Override + public void close(CompletionHandler handler) { + closeCompletion = handler; + if (eagerCompletion) { + closeCompletion.completed(); + } else if (eagerFail) { + closeCompletion.failed(new MyException()); + } + if (closeException) { + throw new MyException(); + } + } + + static MyContent newInstance() { + return new MyContent(false, false, false, false); + } + + static MyContent newEagerCompletion() { + return new MyContent(true, false, false, false); + } + + static MyContent newEagerFail() { + return new MyContent(false, true, false, false); + } + + static MyContent newWriteException() { + return new MyContent(false, false, true, false); + } + + static MyContent newWriteExceptionWithEagerCompletion() { + return new MyContent(true, false, true, false); + } + + static MyContent newCloseException() { + return new MyContent(false, false, false, true); + } + } + + private static class MyRequestHandler extends AbstractRequestHandler { + + final MyContent requestContent; + final boolean eagerRespond; + final boolean closeResponse; + final boolean throwException; + ContentChannel responseContent = null; + ResponseHandler handler = null; + + MyRequestHandler(MyContent requestContent, boolean eagerRespond, boolean closeResponse, + boolean throwException) + { + this.requestContent = requestContent; + this.eagerRespond = eagerRespond; + this.closeResponse = closeResponse; + this.throwException = throwException; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + this.handler = handler; + if (eagerRespond) { + respond(); + } + if (throwException) { + throw new MyException(); + } + return requestContent; + } + + ContentChannel respond() { + responseContent = handler.handleResponse(new Response(Response.Status.OK)); + if (responseContent != null && closeResponse) { + responseContent.close(null); + } + return responseContent; + } + + static MyRequestHandler newInstance() { + return new MyRequestHandler(MyContent.newInstance(), false, false, false); + } + + static MyRequestHandler newEagerCompletion() { + return new MyRequestHandler(MyContent.newEagerCompletion(), false, false, false); + } + + static MyRequestHandler newEagerFail() { + return new MyRequestHandler(MyContent.newEagerFail(), false, false, false); + } + + static RequestHandler newRequestException() { + return new MyRequestHandler(null, false, false, true); + } + + static MyRequestHandler newNullContent() { + return new MyRequestHandler(null, false, false, false); + } + + static MyRequestHandler newNullContentWithEagerHandleResponse() { + return new MyRequestHandler(null, true, false, false); + } + + static MyRequestHandler newNulContentWithEagerCloseResponse() { + return new MyRequestHandler(null, true, true, false); + } + + static MyRequestHandler newRequestExceptionWithEagerHandleResponse() { + return new MyRequestHandler(null, true, false, true); + } + + static MyRequestHandler newRequestExceptionWithEagerCloseResponse() { + return new MyRequestHandler(null, true, true, true); + } + + static MyRequestHandler newContentWriteExceptionWithEagerCompletion() { + return new MyRequestHandler(MyContent.newWriteExceptionWithEagerCompletion(), true, true, false); + } + + static MyRequestHandler newContentCloseException() { + return new MyRequestHandler(MyContent.newCloseException(), true, true, false); + } + + static MyRequestHandler newEagerRespondWithEagerCompletion() { + return new MyRequestHandler(MyContent.newEagerCompletion(), true, false, false); + } + + static MyRequestHandler newEagerCloseResponse() { + return new MyRequestHandler(MyContent.newInstance(), true, true, false); + } + } + + private static class MyResponseHandler implements ResponseHandler { + + final MyContent content; + final boolean throwException; + + MyResponseHandler(MyContent content, boolean throwException) { + this.content = content; + this.throwException = throwException; + } + + @Override + public ContentChannel handleResponse(Response response) { + if (throwException) { + throw new MyException(); + } + return content; + } + + static MyResponseHandler newInstance() { + return new MyResponseHandler(MyContent.newInstance(), false); + } + + static MyResponseHandler newEagerCompletion() { + return new MyResponseHandler(MyContent.newEagerCompletion(), false); + } + + static MyResponseHandler newEagerFail() { + return new MyResponseHandler(MyContent.newEagerFail(), false); + } + + static MyResponseHandler newNullContent() { + return new MyResponseHandler(null, false); + } + + static MyResponseHandler newResponseException() { + return new MyResponseHandler(null, true); + } + + static MyResponseHandler newContentWriteException() { + return new MyResponseHandler(MyContent.newWriteException(), false); + } + + static MyResponseHandler newContentCloseException() { + return new MyResponseHandler(MyContent.newCloseException(), false); + } + } + + private static final class MyException extends RuntimeException { + + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerSnapshotTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerSnapshotTestCase.java new file mode 100644 index 00000000000..15bd3b050d4 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerSnapshotTestCase.java @@ -0,0 +1,211 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.name.Names; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerSnapshotTestCase { + + @Test + public void requireThatServerHandlerCanBeResolved() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://foo/*", MyRequestHandler.newInstance()); + driver.activateContainer(builder); + + Request request = new Request(driver, URI.create("http://foo/")); + assertNotNull(request.container().resolveHandler(request)); + assertNotNull(request.getBindingMatch()); + request.release(); + + request = new Request(driver, URI.create("http://foo/")); + request.setServerRequest(false); + assertNull(request.container().resolveHandler(request)); + assertNull(request.getBindingMatch()); + request.release(); + + request = new Request(driver, URI.create("http://bar/")); + assertNull(request.container().resolveHandler(request)); + assertNull(request.getBindingMatch()); + request.release(); + + request = new Request(driver, URI.create("http://bar/")); + request.setServerRequest(false); + assertNull(request.container().resolveHandler(request)); + assertNull(request.getBindingMatch()); + request.release(); + + assertTrue(driver.close()); + } + + @Test + public void requireThatClientHandlerCanBeResolved() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.clientBindings().bind("http://foo/*", MyRequestHandler.newInstance()); + driver.activateContainer(builder); + + Request request = new Request(driver, URI.create("http://foo/")); + assertNull(request.container().resolveHandler(request)); + assertNull(request.getBindingMatch()); + request.release(); + + request = new Request(driver, URI.create("http://foo/")); + request.setServerRequest(false); + assertNotNull(request.container().resolveHandler(request)); + assertNotNull(request.getBindingMatch()); + request.release(); + + request = new Request(driver, URI.create("http://bar/")); + assertNull(request.container().resolveHandler(request)); + assertNull(request.getBindingMatch()); + request.release(); + + request = new Request(driver, URI.create("http://bar/")); + request.setServerRequest(false); + assertNull(request.container().resolveHandler(request)); + assertNull(request.getBindingMatch()); + request.release(); + + assertTrue(driver.close()); + } + + @Test + public void requireThatClientBindingsAreUsed() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.clientBindings().bind("http://host/path", MyRequestHandler.newInstance()); + driver.activateContainer(builder); + Request request = new Request(driver, URI.create("http://host/path")); + assertNull(request.container().resolveHandler(request)); + request.setServerRequest(false); + assertNotNull(request.container().resolveHandler(request)); + request.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatBindingMatchIsSetByResolveHandler() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://*/*", MyRequestHandler.newInstance()); + driver.activateContainer(builder); + + Request request = new Request(driver, URI.create("http://localhost:69/status.html")); + assertNotNull(request.container().resolveHandler(request)); + BindingMatch<RequestHandler> match = request.getBindingMatch(); + assertNotNull(match); + assertEquals(3, match.groupCount()); + assertEquals("localhost", match.group(0)); + assertEquals("69", match.group(1)); + assertEquals("status.html", match.group(2)); + request.release(); + + assertTrue(driver.close()); + } + + @Test + public void requireThatNewRequestHasSameSnapshot() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + Request foo = new Request(driver, URI.create("http://host/foo")); + Request bar = new Request(foo, URI.create("http://host/bar")); + assertSame(foo.container(), bar.container()); + foo.release(); + bar.release(); + assertTrue(driver.close()); + } + + @Test + public void requireThatActiveInjectorIsUsed() { + final Object obj = new Object(); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() { + + @Override + protected void configure() { + bind(Object.class).toInstance(obj); + bind(String.class).annotatedWith(Names.named("foo")).toInstance("foo"); + } + }); + ActiveContainer active = new ActiveContainer(driver.newContainerBuilder()); + ContainerSnapshot snapshot = new ContainerSnapshot(active, null, null); + assertSame(obj, snapshot.getInstance(Object.class)); + assertEquals("foo", snapshot.getInstance(Key.get(String.class, Names.named("foo")))); + snapshot.release(); + assertTrue(driver.close()); + } + + private static class MyContent implements ContentChannel { + + CompletionHandler writeCompletion = null; + CompletionHandler closeCompletion = null; + ByteBuffer writeBuf = null; + boolean closed = false; + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + writeBuf = buf; + writeCompletion = handler; + } + + @Override + public void close(CompletionHandler handler) { + closed = true; + closeCompletion = handler; + } + } + + private static class MyRequestHandler extends AbstractResource implements RequestHandler { + + final MyContent content = new MyContent(); + Request request = null; + ResponseHandler handler = null; + boolean timeout = false; + boolean destroyed = false; + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + this.request = request; + this.handler = handler; + return content; + } + + @Override + public void handleTimeout(Request request, ResponseHandler handler) { + timeout = true; + } + + @Override + public void destroy() { + destroyed = true; + } + + static MyRequestHandler newInstance() { + return new MyRequestHandler(); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerTerminationTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerTerminationTestCase.java new file mode 100644 index 00000000000..bc2591d94b0 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerTerminationTestCase.java @@ -0,0 +1,76 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.DeactivatedContainer; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerTerminationTestCase { + + @Test + public void requireThatAccessorsWork() { + Object obj = new Object(); + ContainerTermination termination = new ContainerTermination(obj); + assertSame(obj, termination.appContext()); + } + + @Test + public void requireThatAppContextIsFromBuilder() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + Object obj = new Object(); + builder.setAppContext(obj); + driver.activateContainer(builder); + DeactivatedContainer container = driver.activateContainer(null); + assertSame(obj, container.appContext()); + assertTrue(driver.close()); + } + + @Test + public void requireThatEarlyTerminationIsNotified() { + ContainerTermination termination = new ContainerTermination(null); + termination.run(); + MyTask task = new MyTask(); + termination.notifyTermination(task); + assertTrue(task.done); + } + + @Test + public void requireThatLaterTerminationIsNotified() { + ContainerTermination termination = new ContainerTermination(null); + MyTask task = new MyTask(); + termination.notifyTermination(task); + assertFalse(task.done); + termination.run(); + assertTrue(task.done); + } + + @Test + public void requireThatNotifyCanOnlyBeCalledOnce() { + ContainerTermination termination = new ContainerTermination(null); + termination.notifyTermination(new MyTask()); + try { + termination.notifyTermination(new MyTask()); + } catch (IllegalStateException e) { + + } + } + + private static class MyTask implements Runnable { + + boolean done = false; + + @Override + public void run() { + done = true; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/DefaultBindingSelectorTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/DefaultBindingSelectorTestCase.java new file mode 100644 index 00000000000..1154d01dfe5 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/DefaultBindingSelectorTestCase.java @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Guice; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.BindingSetSelector; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class DefaultBindingSelectorTestCase { + + @Test + public void requireThatClassIsInjectedByDefault() { + BindingSetSelector selector = Guice.createInjector().getInstance(BindingSetSelector.class); + assertTrue(selector instanceof DefaultBindingSelector); + } + + @Test + public void requireThatDefaultSetIsAlwaysSelected() { + DefaultBindingSelector selector = new DefaultBindingSelector(); + assertEquals(BindingSet.DEFAULT, selector.select(null)); + for (int i = 0; i < 69; ++i) { + assertEquals(BindingSet.DEFAULT, selector.select(newUri())); + } + } + + private static URI newUri() { + return URI.create("foo" + System.nanoTime() + "://bar" + System.nanoTime() + "/baz" + System.nanoTime()); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ExportPackagesTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ExportPackagesTestCase.java new file mode 100644 index 00000000000..a61ee4efc2d --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ExportPackagesTestCase.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.junit.Test; + +import java.io.File; +import java.io.FileReader; +import java.util.Properties; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ExportPackagesTestCase { + + @Test + public void requireThatPropertiesAreWritten() throws Exception { + File file = new File("target", ExportPackages.PROPERTIES_FILE); + file.deleteOnExit(); + ExportPackages.main(new String[] { file.getAbsolutePath() }); + assertTrue(file.exists()); + Properties props = new Properties(); + try (FileReader reader = new FileReader(file)) { + props.load(reader); + assertNotNull(props.getProperty(ExportPackages.EXPORT_PACKAGES)); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixFrameworkTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixFrameworkTestCase.java new file mode 100644 index 00000000000..69696ff62ec --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixFrameworkTestCase.java @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; +import org.osgi.framework.BundleException; + +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class FelixFrameworkTestCase { + + @Test + public void requireThatLifecycleWorks() throws BundleException { + FelixFramework felix = TestDriver.newOsgiFramework(); + felix.start(); + felix.stop(); + } + + @Test + public void requireThatStopWithoutStartDoesNotThrowException() throws BundleException { + FelixFramework felix = TestDriver.newOsgiFramework(); + felix.stop(); + } + + @Test + public void requireThatInstallCanThrowException() throws BundleException { + FelixFramework felix = TestDriver.newOsgiFramework(); + felix.start(); + try { + felix.installBundle("file:notfound.jar"); + fail(); + } catch (BundleException e) { + + } + felix.stop(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixParamsTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixParamsTestCase.java new file mode 100644 index 00000000000..216b79c1d7f --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixParamsTestCase.java @@ -0,0 +1,67 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.junit.Test; +import org.osgi.framework.Constants; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class FelixParamsTestCase { + + @Test + public void requireThatAccessorsWork() { + FelixParams params = new FelixParams(); + params.setCachePath("foo"); + assertEquals("foo", params.getCachePath()); + params.setLoggerEnabled(true); + assertTrue(params.isLoggerEnabled()); + } + + @Test + public void requireThatSystemPackagesAreNotReplaced() { + FelixParams params = new FelixParams(); + Map<String, String> config = params.toConfig(); + assertNotNull(config); + String str = config.get(Constants.FRAMEWORK_SYSTEMPACKAGES); + assertNotNull(str); + assertTrue(str.contains(ExportPackages.getSystemPackages())); + + params.exportPackage("foo"); + assertNotNull(config = params.toConfig()); + assertNotNull(str = config.get(Constants.FRAMEWORK_SYSTEMPACKAGES)); + assertTrue(str.contains(ExportPackages.getSystemPackages())); + assertTrue(str.contains("foo")); + } + + @Test + public void requireThatExportsAreIncludedInConfig() { + FelixParams params = new FelixParams(); + Map<String, String> config = params.toConfig(); + assertNotNull(config); + String[] prev = config.get(Constants.FRAMEWORK_SYSTEMPACKAGES).split(","); + + params.exportPackage("foo"); + params.exportPackage("bar"); + assertNotNull(config = params.toConfig()); + String[] next = config.get(Constants.FRAMEWORK_SYSTEMPACKAGES).split(","); + + assertEquals(prev.length + 2, next.length); + + List<String> diff = new LinkedList<>(); + diff.addAll(Arrays.asList(next)); + diff.removeAll(Arrays.asList(prev)); + assertEquals(2, diff.size()); + assertTrue(diff.contains("foo")); + assertTrue(diff.contains("bar")); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogHandlerTestCase.java new file mode 100644 index 00000000000..eb18e6f8e49 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogHandlerTestCase.java @@ -0,0 +1,192 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.junit.Test; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; + +import java.util.Arrays; +import java.util.Enumeration; +import java.util.ResourceBundle; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class OsgiLogHandlerTestCase { + + @Test + public void requireThatLogRecordsArePublishedToLogService() { + MyLogService logService = new MyLogService(); + Logger log = newLogger(logService); + + log.log(Level.INFO, "foo"); + assertEquals(OsgiLogHandler.toServiceLevel(Level.INFO), logService.lastLevel); + assertEquals("foo", logService.lastMessage); + assertNull(logService.lastThrowable); + + Throwable t = new Throwable(); + log.log(Level.SEVERE, "bar", t); + assertEquals(OsgiLogHandler.toServiceLevel(Level.SEVERE), logService.lastLevel); + assertEquals("bar", logService.lastMessage); + assertEquals(t, logService.lastThrowable); + } + + @Test + public void requireThatStadardLogLevelsAreConverted() { + assertLogLevel(LogService.LOG_ERROR, Level.SEVERE); + assertLogLevel(LogService.LOG_WARNING, Level.WARNING); + assertLogLevel(LogService.LOG_INFO, Level.INFO); + assertLogLevel(LogService.LOG_DEBUG, Level.CONFIG); + assertLogLevel(LogService.LOG_DEBUG, Level.FINE); + assertLogLevel(LogService.LOG_DEBUG, Level.FINER); + assertLogLevel(LogService.LOG_DEBUG, Level.FINEST); + } + + @Test + public void requireThatCustomLogLevelsAreConverted() { + for (int i = Level.ALL.intValue() - 69; i < Level.OFF.intValue() + 69; ++i) { + int expectedLevel; + if (i >= Level.SEVERE.intValue()) { + expectedLevel = LogService.LOG_ERROR; + } else if (i >= Level.WARNING.intValue()) { + expectedLevel = LogService.LOG_WARNING; + } else if (i >= Level.INFO.intValue()) { + expectedLevel = LogService.LOG_INFO; + } else { + expectedLevel = LogService.LOG_DEBUG; + } + assertLogLevel(expectedLevel, new MyLogLevel(i)); + } + } + + @Test + public void requireThatJdk14PropertiesAreAvailableThroughServiceReference() { + MyLogService logService = new MyLogService(); + + Logger log = newLogger(logService); + LogRecord record = new LogRecord(Level.INFO, "message"); + record.setLoggerName("loggerName"); + record.setMillis(69); + Object[] parameters = new Object[0]; + record.setParameters(parameters); + ResourceBundle resouceBundle = new MyResourceBundle(); + record.setResourceBundle(resouceBundle); + record.setResourceBundleName("resourceBundleName"); + record.setSequenceNumber(69); + record.setSourceClassName("sourceClassName"); + record.setSourceMethodName("sourceMethodName"); + record.setThreadID(69); + Throwable thrown = new Throwable(); + record.setThrown(thrown); + log.log(record); + + ServiceReference ref = logService.lastServiceReference; + assertNotNull(ref); + assertTrue(Arrays.equals(new String[] { "LEVEL", + "LOGGER_NAME", + "MESSAGE", + "MILLIS", + "PARAMETERS", + "RESOURCE_BUNDLE", + "RESOURCE_BUNDLE_NAME", + "SEQUENCE_NUMBER", + "SOURCE_CLASS_NAME", + "SOURCE_METHOD_NAME", + "THREAD_ID", + "THROWN" }, + ref.getPropertyKeys())); + assertEquals(Level.INFO, ref.getProperty("LEVEL")); + assertEquals("loggerName", ref.getProperty("LOGGER_NAME")); + assertEquals("message", ref.getProperty("MESSAGE")); + assertEquals(69L, ref.getProperty("MILLIS")); + assertSame(parameters, ref.getProperty("PARAMETERS")); + assertSame(resouceBundle, ref.getProperty("RESOURCE_BUNDLE")); + assertEquals("resourceBundleName", ref.getProperty("RESOURCE_BUNDLE_NAME")); + assertEquals(69L, ref.getProperty("SEQUENCE_NUMBER")); + assertEquals("sourceClassName", ref.getProperty("SOURCE_CLASS_NAME")); + assertEquals("sourceMethodName", ref.getProperty("SOURCE_METHOD_NAME")); + assertEquals(69, ref.getProperty("THREAD_ID")); + assertSame(thrown, ref.getProperty("THROWN")); + assertNull(ref.getProperty("unknown")); + } + + private static void assertLogLevel(int expectedLevel, Level level) { + MyLogService logService = new MyLogService(); + Logger log = newLogger(logService); + log.log(level, "message"); + assertEquals(expectedLevel, logService.lastLevel); + } + + @SuppressWarnings("unchecked") + private static Logger newLogger(LogService logService) { + Logger log = Logger.getAnonymousLogger(); + log.setUseParentHandlers(false); + log.setLevel(Level.ALL); + for (Handler handler : log.getHandlers()) { + log.removeHandler(handler); + } + log.addHandler(new OsgiLogHandler(logService)); + return log; + } + + private static class MyLogLevel extends Level { + + protected MyLogLevel(int val) { + super("foo", val); + } + } + + private static class MyLogService implements LogService { + + ServiceReference lastServiceReference; + int lastLevel; + String lastMessage; + Throwable lastThrowable; + + @Override + public void log(int level, String message) { + log(null, level, message, null); + } + + @Override + public void log(int level, String message, Throwable throwable) { + log(null, level, message, throwable); + } + + @Override + public void log(ServiceReference serviceReference, int level, String message) { + log(serviceReference, level, message, null); + } + + @Override + public void log(ServiceReference serviceReference, int level, String message, Throwable throwable) { + lastServiceReference = serviceReference; + lastLevel = level; + lastMessage = message; + lastThrowable = throwable; + } + } + + private static class MyResourceBundle extends ResourceBundle { + + @Override + protected Object handleGetObject(String key) { + return null; + } + + @Override + public Enumeration<String> getKeys() { + return null; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogManagerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogManagerTestCase.java new file mode 100644 index 00000000000..32e4b37aca8 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogManagerTestCase.java @@ -0,0 +1,184 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.log.LogService; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class OsgiLogManagerTestCase { + + @Test + public void requireThatAllLogMethodsAreImplemented() throws BundleException { + FelixFramework felix = TestDriver.newOsgiFramework(); + felix.start(); + + BundleContext ctx = felix.bundleContext(); + OsgiLogManager manager = new OsgiLogManager(true); + manager.install(ctx); + MyLogService service = new MyLogService(); + ctx.registerService(LogService.class.getName(), service, null); + + manager.log(2, "a"); + assertLast(service, null, 2, "a", null); + + Throwable t1 = new Throwable(); + manager.log(4, "b", t1); + assertLast(service, null, 4, "b", t1); + + ServiceReference ref1 = Mockito.mock(ServiceReference.class); + manager.log(ref1, 8, "c"); + assertLast(service, ref1, 8, "c", null); + + ServiceReference ref2 = Mockito.mock(ServiceReference.class); + Throwable t2 = new Throwable(); + manager.log(ref2, 16, "d", t2); + assertLast(service, ref2, 16, "d", t2); + + manager.uninstall(); + felix.stop(); + } + + @Test + public void requireThatLogManagerWritesToAllRegisteredLogServices() throws BundleException { + FelixFramework felix = TestDriver.newOsgiFramework(); + felix.start(); + + BundleContext ctx = felix.bundleContext(); + MyLogService foo = new MyLogService(); + ServiceRegistration<LogService> fooReg = ctx.registerService(LogService.class, foo, null); + + OsgiLogManager manager = new OsgiLogManager(true); + manager.install(ctx); + + ServiceReference ref1 = Mockito.mock(ServiceReference.class); + Throwable t1 = new Throwable(); + manager.log(ref1, 2, "a", t1); + assertLast(foo, ref1, 2, "a", t1); + + MyLogService bar = new MyLogService(); + ServiceRegistration<LogService> barReg = ctx.registerService(LogService.class, bar, null); + + ServiceReference ref2 = Mockito.mock(ServiceReference.class); + Throwable t2 = new Throwable(); + manager.log(ref2, 4, "b", t2); + assertLast(foo, ref2, 4, "b", t2); + assertLast(bar, ref2, 4, "b", t2); + + MyLogService baz = new MyLogService(); + ServiceRegistration<LogService> bazReg = ctx.registerService(LogService.class, baz, null); + + ServiceReference ref3 = Mockito.mock(ServiceReference.class); + Throwable t3 = new Throwable(); + manager.log(ref3, 8, "c", t3); + assertLast(foo, ref3, 8, "c", t3); + assertLast(bar, ref3, 8, "c", t3); + assertLast(baz, ref3, 8, "c", t3); + + fooReg.unregister(); + + ServiceReference ref4 = Mockito.mock(ServiceReference.class); + Throwable t4 = new Throwable(); + manager.log(ref4, 16, "d", t4); + assertLast(foo, ref3, 8, "c", t3); + assertLast(bar, ref4, 16, "d", t4); + assertLast(baz, ref4, 16, "d", t4); + + barReg.unregister(); + + ServiceReference ref5 = Mockito.mock(ServiceReference.class); + Throwable t5 = new Throwable(); + manager.log(ref5, 32, "e", t5); + assertLast(foo, ref3, 8, "c", t3); + assertLast(bar, ref4, 16, "d", t4); + assertLast(baz, ref5, 32, "e", t5); + + bazReg.unregister(); + + ServiceReference ref6 = Mockito.mock(ServiceReference.class); + Throwable t6 = new Throwable(); + manager.log(ref6, 64, "f", t6); + assertLast(foo, ref3, 8, "c", t3); + assertLast(bar, ref4, 16, "d", t4); + assertLast(baz, ref5, 32, "e", t5); + + manager.uninstall(); + felix.stop(); + } + + @Test + public void requireThatRootLoggerModificationCanBeDisabled() throws BundleException { + Logger logger = Logger.getLogger(""); + logger.setLevel(Level.WARNING); + + new OsgiLogManager(false).install(Mockito.mock(BundleContext.class)); + assertEquals(Level.WARNING, logger.getLevel()); + + new OsgiLogManager(true).install(Mockito.mock(BundleContext.class)); + assertEquals(Level.ALL, logger.getLevel()); + } + + @Test + public void requireThatRootLoggerLevelIsModifiedIfNoLoggerConfigIsGiven() { + Logger logger = Logger.getLogger(""); + logger.setLevel(Level.WARNING); + + OsgiLogManager.newInstance().install(Mockito.mock(BundleContext.class)); + + assertNull(System.getProperty("java.util.logging.config.file")); + assertEquals(Level.ALL, logger.getLevel()); + } + + private static void assertLast(MyLogService service, ServiceReference ref, int level, String message, Throwable t) { + assertSame(ref, service.lastServiceReference); + assertEquals(level, service.lastLevel); + assertEquals(message, service.lastMessage); + assertSame(t, service.lastThrowable); + } + + private static class MyLogService implements LogService { + + ServiceReference lastServiceReference; + int lastLevel; + String lastMessage; + Throwable lastThrowable; + + @Override + public void log(int level, String message) { + log(null, level, message, null); + } + + @Override + public void log(int level, String message, Throwable throwable) { + log(null, level, message, throwable); + } + + @Override + public void log(ServiceReference serviceReference, int level, String message) { + log(serviceReference, level, message, null); + } + + @Override + public void log(ServiceReference serviceReference, int level, String message, Throwable throwable) { + lastServiceReference = serviceReference; + lastLevel = level; + lastMessage = message; + lastThrowable = throwable; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogServiceTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogServiceTestCase.java new file mode 100644 index 00000000000..fbd6f5a3f88 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogServiceTestCase.java @@ -0,0 +1,105 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.application.OsgiFramework; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.service.log.LogReaderService; +import org.osgi.service.log.LogService; +import org.osgi.util.tracker.ServiceTracker; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class OsgiLogServiceTestCase { + + @Test + public void requireThatLogServiceIsRegistered() throws BundleException, InterruptedException { + OsgiFramework osgi = TestDriver.newOsgiFramework(); + osgi.start(); + + ServiceTracker logs = newTracker(osgi, LogService.class); + ServiceTracker logReaders = newTracker(osgi, LogReaderService.class); + assertEquals(1, logs.getTrackingCount()); + assertEquals(1, logReaders.getTrackingCount()); + + OsgiLogService service = new OsgiLogService(); + service.start(osgi.bundleContext()); + + assertEquals(2, logs.getTrackingCount()); + assertEquals(2, logReaders.getTrackingCount()); + osgi.stop(); + } + + @Test + public void requireThatLogServiceCanNotBeStartedTwice() throws BundleException { + OsgiFramework osgi = TestDriver.newOsgiFramework(); + osgi.start(); + + BundleContext ctx = osgi.bundleContext(); + OsgiLogService service = new OsgiLogService(); + service.start(ctx); + + try { + service.start(ctx); + fail(); + } catch (IllegalStateException e) { + + } + + osgi.stop(); + } + + @Test + public void requireThatLogServiceCanNotBeStoppedTwice() throws BundleException { + OsgiFramework osgi = TestDriver.newOsgiFramework(); + osgi.start(); + + BundleContext ctx = osgi.bundleContext(); + OsgiLogService service = new OsgiLogService(); + service.start(ctx); + service.stop(); + + try { + service.stop(); + fail(); + } catch (NullPointerException e) { + + } + + osgi.stop(); + } + + @Test + public void requireThatUnstartedLogServiceCanNotBeStopped() throws BundleException { + try { + new OsgiLogService().stop(); + fail(); + } catch (NullPointerException e) { + + } + } + + @Test + public void requireThatLogServiceCanNotStartWithoutBundleContext() throws BundleException { + try { + new OsgiLogService().start(null); + fail(); + } catch (NullPointerException e) { + + } + } + + @SuppressWarnings("unchecked") + private static ServiceTracker newTracker(OsgiFramework osgi, Class trackedClass) { + ServiceTracker tracker = new ServiceTracker(osgi.bundleContext(), trackedClass, null); + tracker.open(); + return tracker; + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ScheduledQueueTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ScheduledQueueTestCase.java new file mode 100644 index 00000000000..b208d5e5b9d --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ScheduledQueueTestCase.java @@ -0,0 +1,149 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import static com.yahoo.jdisc.core.ScheduledQueue.MILLIS_PER_SLOT; +import static com.yahoo.jdisc.core.ScheduledQueue.NUM_SLOTS; +import static com.yahoo.jdisc.core.ScheduledQueue.NUM_SLOTS_UNDILATED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ScheduledQueueTestCase { + + @Test + public void requireThatSlotMaskPreventsOverflow() { + for (int slot = 0; slot < NUM_SLOTS * 2; ++slot) { + assertTrue((slot & ScheduledQueue.SLOT_MASK) < NUM_SLOTS); + } + } + + @Test + public void requireThatIterShiftDiscardsSlotBits() { + for (int slot = 0; slot < NUM_SLOTS * 2; ++slot) { + assertEquals(slot / NUM_SLOTS, slot >> ScheduledQueue.ITER_SHIFT); + } + } + + @Test + public void requireThatNewEntryDoesNotAcceptNull() { + ScheduledQueue queue = new ScheduledQueue(0); + try { + queue.newEntry(null); + fail(); + } catch (NullPointerException e) { + + } + } + + @Test + public void requireThatEntriesCanBeScheduled() { + ScheduledQueue queue = new ScheduledQueue(0); + Object foo = new Object(); + ScheduledQueue.Entry entry = queue.newEntry(foo); + entry.scheduleAt(200); + + assertDrainTo(queue, 150); + assertDrainTo(queue, 250, foo); + } + + @Test + public void requireThatEntriesCanBeRescheduled() { + ScheduledQueue queue = new ScheduledQueue(0); + Object foo = new Object(); + ScheduledQueue.Entry entry = queue.newEntry(foo); + entry.scheduleAt(200); + entry.scheduleAt(100); + + assertDrainTo(queue, 150, foo); + assertDrainTo(queue, 250); + } + + @Test + public void requireThatEntriesCanBeUnscheduled() { + ScheduledQueue queue = new ScheduledQueue(0); + Object foo = new Object(); + ScheduledQueue.Entry entry = queue.newEntry(foo); + entry.scheduleAt(100); + entry.unschedule(); + + assertDrainTo(queue, 150); + } + + @Test + public void requireThatDrainToOnlyDrainsExpiredEntries() { + ScheduledQueue queue = new ScheduledQueue(0); + Object foo = scheduleAt(queue, 100); + Object bar = scheduleAt(queue, 300); + Object baz = scheduleAt(queue, 200); + + assertDrainTo(queue, 150, foo); + assertDrainTo(queue, 250, baz); + assertDrainTo(queue, 350, bar); + assertDrainTo(queue, 450); + } + + @Test + public void requireThatEntriesDoNotExpireMoreThanOnce() { + ScheduledQueue queue = new ScheduledQueue(0); + Object foo = scheduleAt(queue, NUM_SLOTS * MILLIS_PER_SLOT + 50); + + long now = 0; + for (int i = 0; i < NUM_SLOTS; ++i, now += MILLIS_PER_SLOT) { + assertDrainTo(queue, now); + } + assertDrainTo(queue, now += MILLIS_PER_SLOT, foo); + for (int i = 0; i < NUM_SLOTS; ++i, now += MILLIS_PER_SLOT) { + assertDrainTo(queue, now); + } + } + + @Test + public void requireThatNegativeScheduleTranslatesToNow() { + ScheduledQueue queue = new ScheduledQueue(0); + Object foo = scheduleAt(queue, -100); + + assertDrainTo(queue, 0, foo); + } + + @Test + public void requireThatDrainToPerformsTimeDilationWhenOverloaded() { + ScheduledQueue queue = new ScheduledQueue(0); + List<Object> payloads = new LinkedList<>(); + for (int i = 1; i <= NUM_SLOTS_UNDILATED + 1; ++i) { + payloads.add(scheduleAt(queue, i * MILLIS_PER_SLOT)); + } + + Queue<Object> expired = new LinkedList<>(); + long currentTimeMillis = payloads.size() * MILLIS_PER_SLOT; + queue.drainTo(currentTimeMillis, expired); + assertEquals(NUM_SLOTS_UNDILATED, expired.size()); + + expired = new LinkedList<>(); + currentTimeMillis += MILLIS_PER_SLOT; + queue.drainTo(currentTimeMillis, expired); + assertEquals(1, expired.size()); + } + + private static Object scheduleAt(ScheduledQueue queue, long expireAtMillis) { + Object obj = new Object(); + queue.newEntry(obj).scheduleAt(expireAtMillis); + return obj; + } + + private static void assertDrainTo(ScheduledQueue queue, long currentTimeMillis, Object... expected) { + Queue<Object> expired = new LinkedList<>(); + queue.drainTo(currentTimeMillis, expired); + assertEquals(expected.length, expired.size()); + assertEquals(Arrays.asList(expected), expired); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/SystemTimerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/SystemTimerTestCase.java new file mode 100644 index 00000000000..e4d0df36260 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/SystemTimerTestCase.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.jdisc.core; + +import com.google.inject.Guice; +import com.yahoo.jdisc.Timer; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class SystemTimerTestCase { + + @Test + public void requireThatClassIsInjectedByDefault() { + Timer timer = Guice.createInjector().getInstance(Timer.class); + assertTrue(timer instanceof SystemTimer); + } + + @Test + public void requireThatSystemTimerIsSane() { + long before = System.currentTimeMillis(); + long millis = new SystemTimer().currentTimeMillis(); + long after = System.currentTimeMillis(); + + assertTrue(before <= millis); + assertTrue(after >= millis); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/TimeoutManagerImplTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/TimeoutManagerImplTestCase.java new file mode 100644 index 00000000000..46464a9b05a --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/TimeoutManagerImplTestCase.java @@ -0,0 +1,579 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.TimeoutManager; +import com.yahoo.jdisc.Timer; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestDeniedException; +import com.yahoo.jdisc.handler.RequestDispatch; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.NonWorkingRequest; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TimeoutManagerImplTestCase { + + private static final String REQUEST_URI = "http://host/path"; + + @Test + public void requireThatDefaultIsNoTimeout() { + Context ctx = new Context(MyRequestHandler.newEagerResponse()); + assertNull(ctx.dispatchRequest(null, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatTimeoutCanBeSetByServerProvider() { + Context ctx = new Context(MyRequestHandler.newEagerResponse()); + assertEquals(Long.valueOf(69), ctx.dispatchRequest(69L, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatTimeoutCanBeSetByRequestHandler() { + Context ctx = new Context(MyRequestHandler.newTimeoutWithEagerResponse(69)); + assertEquals(Long.valueOf(69), ctx.dispatchRequest(null, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatTimeoutRequestHandlerTimeoutHasPrecedence() { + Context ctx = new Context(MyRequestHandler.newTimeoutWithEagerResponse(6)); + assertEquals(Long.valueOf(6), ctx.dispatchRequest(9L, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatResponseCancelsTimeout() throws InterruptedException { + Context ctx = new Context(MyRequestHandler.newEagerResponse()); + assertEquals(Response.Status.OK, ctx.awaitResponse(69L, MyResponseHandler.newInstance())); + assertEquals(Response.Status.OK, ctx.awaitResponse(69L, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatNullRequestContentCanTimeout() throws InterruptedException { + Context ctx = new Context(MyRequestHandler.newNullContent()); + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance())); + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatTimeoutWorksAfterRequestDenied() throws InterruptedException { + Context ctx = new Context(MyRequestHandler.newFirstRequestDenied()); + try { + ctx.dispatchRequest(null, MyResponseHandler.newInstance()); + fail(); + } catch (RequestDeniedException e) { + + } + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatTimeoutWorksAfterResponseDenied() throws InterruptedException { + Context ctx = new Context(MyRequestHandler.newInstance()); + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newResponseDenied())); + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatTimeoutWorksAfterResponseThrowsException() throws InterruptedException { + Context ctx = new Context(MyRequestHandler.newInstance()); + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newThrowException())); + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatTimeoutWorksAfterResponseInterruptsThread() throws InterruptedException { + Context ctx = new Context(MyRequestHandler.newInstance()); + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInterruptThread())); + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance())); + assertTrue(ctx.close()); + } + + @Test + public void requireThatTimeoutOccursInOrder() throws InterruptedException { + Context ctx = new Context(MyRequestHandler.newInstance()); + MyResponseHandler foo = MyResponseHandler.newInstance(); + ctx.dispatchRequest(300L, foo); + + MyResponseHandler bar = MyResponseHandler.newInstance(); + ctx.dispatchRequest(100L, bar); + + MyResponseHandler baz = MyResponseHandler.newInstance(); + ctx.dispatchRequest(200L, baz); + + ctx.forwardToTime(100); + assertFalse(foo.await(10, TimeUnit.MILLISECONDS)); + assertTrue(bar.await(600, TimeUnit.SECONDS)); + assertFalse(baz.await(10, TimeUnit.MILLISECONDS)); + + ctx.forwardToTime(200); + assertFalse(foo.await(10, TimeUnit.MILLISECONDS)); + assertTrue(baz.await(600, TimeUnit.SECONDS)); + + ctx.forwardToTime(300); + assertTrue(foo.await(600, TimeUnit.SECONDS)); + + assertTrue(ctx.close()); + } + + @Test + public void requireThatResponseHandlerIsWellBehavedAfterTimeout() throws InterruptedException { + Context ctx = new Context(MyRequestHandler.newInstance()); + assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance())); + + ContentChannel content = ctx.requestHandler.responseHandler.handleResponse(new Response(Response.Status.OK)); + assertNotNull(content); + + content.write(ByteBuffer.allocate(69), null); + MyCompletion completion = new MyCompletion(); + content.write(ByteBuffer.allocate(69), completion); + assertTrue(completion.completed.await(600, TimeUnit.SECONDS)); + + completion = new MyCompletion(); + content.close(completion); + assertTrue(completion.completed.await(600, TimeUnit.SECONDS)); + + assertTrue(ctx.close()); + } + + @Test + public void requireThatManagedHandlerForwardsAllCalls() throws InterruptedException { + Request request = NonWorkingRequest.newInstance(REQUEST_URI); + MyRequestHandler requestHandler = MyRequestHandler.newInstance(); + TimeoutManagerImpl timeoutManager = new TimeoutManagerImpl(Executors.defaultThreadFactory(), + new SystemTimer()); + RequestHandler managedHandler = timeoutManager.manageHandler(requestHandler); + + MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + ContentChannel requestContent = managedHandler.handleRequest(request, responseHandler); + assertNotNull(requestContent); + + ByteBuffer buf = ByteBuffer.allocate(69); + requestContent.write(buf, null); + assertSame(buf, requestHandler.content.buf); + MyCompletion writeCompletion = new MyCompletion(); + requestContent.write(buf = ByteBuffer.allocate(69), writeCompletion); + assertSame(buf, requestHandler.content.buf); + requestHandler.content.writeCompletion.completed(); + assertTrue(writeCompletion.completed.await(600, TimeUnit.SECONDS)); + + MyCompletion closeCompletion = new MyCompletion(); + requestContent.close(closeCompletion); + requestHandler.content.closeCompletion.completed(); + assertTrue(closeCompletion.completed.await(600, TimeUnit.SECONDS)); + + managedHandler.release(); + assertTrue(requestHandler.destroyed); + + Response response = new Response(Response.Status.OK); + ContentChannel responseContent = requestHandler.responseHandler.handleResponse(response); + assertNotNull(responseContent); + + responseContent.write(buf = ByteBuffer.allocate(69), null); + assertSame(buf, responseHandler.content.buf); + responseContent.write(buf = ByteBuffer.allocate(69), writeCompletion = new MyCompletion()); + assertSame(buf, responseHandler.content.buf); + responseHandler.content.writeCompletion.completed(); + assertTrue(writeCompletion.completed.await(600, TimeUnit.SECONDS)); + + responseContent.close(closeCompletion = new MyCompletion()); + responseHandler.content.closeCompletion.completed(); + assertTrue(closeCompletion.completed.await(600, TimeUnit.SECONDS)); + + assertSame(response, responseHandler.response.get()); + } + + @Test + public void requireThatTimeoutOccursAtExpectedTime() throws InterruptedException { + final Context ctx = new Context(MyRequestHandler.newInstance()); + final MyResponseHandler responseHandler = MyResponseHandler.newInstance(); + + ctx.forwardToTime(100); + new RequestDispatch() { + + @Override + protected Request newRequest() { + Request request = new Request(ctx.driver, URI.create(REQUEST_URI)); + request.setTimeout(300, TimeUnit.MILLISECONDS); + return request; + } + + @Override + public ContentChannel handleResponse(Response response) { + return responseHandler.handleResponse(response); + } + }.dispatch(); + + ctx.forwardToTime(300); + assertFalse(responseHandler.await(100, TimeUnit.MILLISECONDS)); + ctx.forwardToTime(400); + assertTrue(responseHandler.await(600, TimeUnit.SECONDS)); + + Response response = responseHandler.response.get(); + assertNotNull(response); + assertEquals(Response.Status.REQUEST_TIMEOUT, response.getStatus()); + assertTrue(ctx.close()); + } + + @Test + public void requireThatQueueEntryIsRemovedWhenResponseHandlerIsCalledBeforeTimeout() { + Context ctx = new Context(MyRequestHandler.newInstance()); + ctx.dispatchRequest(69L, MyResponseHandler.newInstance()); + assertTrue(ctx.awaitQueueSize(1, 600, TimeUnit.SECONDS)); + ctx.requestHandler.respond(); + assertTrue(ctx.awaitQueueSize(0, 600, TimeUnit.SECONDS)); + assertTrue(ctx.close()); + } + + @Test + public void requireThatNoEntryIsMadeIfTimeoutIsNull() { + Context ctx = new Context(MyRequestHandler.newInstance()); + ctx.dispatchRequest(null, MyResponseHandler.newInstance()); + assertFalse(ctx.awaitQueueSize(1, 100, TimeUnit.MILLISECONDS)); + assertTrue(ctx.awaitQueueSize(0, 600, TimeUnit.SECONDS)); + ctx.requestHandler.respond(); + assertTrue(ctx.close()); + } + + @Test + public void requireThatNoEntryIsMadeIfHandleRequestCallsHandleResponse() { + Context ctx = new Context(MyRequestHandler.newEagerResponse()); + ctx.dispatchRequest(69L, MyResponseHandler.newInstance()); + assertFalse(ctx.awaitQueueSize(1, 100, TimeUnit.MILLISECONDS)); + assertTrue(ctx.awaitQueueSize(0, 600, TimeUnit.SECONDS)); + assertTrue(ctx.close()); + } + + @Test + public void requireThatNoEntryIsMadeIfTimeoutHandlerHasBeenSet() { + final Context ctx = new Context(MyRequestHandler.newInstance()); + new RequestDispatch() { + + @Override + protected Request newRequest() { + Request request = new Request(ctx.driver, URI.create(REQUEST_URI)); + request.setTimeout(10, TimeUnit.MILLISECONDS); + request.setTimeoutManager(new TimeoutManager() { + + @Override + public void scheduleTimeout(Request request) { + + } + }); + return request; + } + }.dispatch(); + + assertFalse(ctx.awaitQueueSize(1, 100, TimeUnit.MILLISECONDS)); + assertTrue(ctx.awaitQueueSize(0, 600, TimeUnit.SECONDS)); + ctx.requestHandler.respond(); + assertTrue(ctx.close()); + } + + private static class Context implements Module, Timer { + + final MyRequestHandler requestHandler; + final TimeoutManagerImpl timeoutManager; + final TestDriver driver; + long millis = 0; + + Context(MyRequestHandler requestHandler) { + this.requestHandler = requestHandler; + this.driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(this); + + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind(REQUEST_URI, requestHandler); + driver.activateContainer(builder); + + Container ref = driver.newReference(URI.create(REQUEST_URI)); + timeoutManager = ref.getInstance(TimeoutManagerImpl.class); + ref.release(); + } + + void forwardToTime(long millis) { + while (this.millis < millis) { + this.millis += ScheduledQueue.MILLIS_PER_SLOT; + timeoutManager.checkTasks(this.millis); + } + } + + boolean close() { + return driver.close(); + } + + @Override + public void configure(Binder binder) { + binder.bind(Timer.class).toInstance(this); + } + + @Override + public long currentTimeMillis() { + return millis; + } + + int awaitResponse(Long serverProviderTimeout, MyResponseHandler responseHandler) throws InterruptedException { + Long timeout = new MyServerProvider(serverProviderTimeout).dispatchRequest(driver, responseHandler); + long timeoutAt; + if (timeout == null) { + timeoutAt = millis + TimeUnit.SECONDS.toMillis(120); + } else { + timeoutAt = millis + timeout; + } + forwardToTime(timeoutAt); + if (!responseHandler.await(600, TimeUnit.SECONDS)) { + fail("Request handler failed to respond within allocated time."); + } + return responseHandler.response.get().getStatus(); + } + + boolean awaitQueueSize(int expectedSize, int timeout, TimeUnit unit) { + for (long i = 0, len = unit.toMillis(timeout) / 100; i < len; ++i) { + if (timeoutManager.queueSize() == expectedSize) { + return true; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + fail(); + } + } + return false; + } + + public Long dispatchRequest(Long serverProviderTimeout, MyResponseHandler responseHandler) { + return new MyServerProvider(serverProviderTimeout).dispatchRequest(driver, responseHandler); + } + } + + private static class MyServerProvider { + + final Long timeout; + + MyServerProvider(Long timeout) { + this.timeout = timeout; + } + + Long dispatchRequest(CurrentContainer container, ResponseHandler responseHandler) { + Request request = null; + ContentChannel content = null; + try { + request = new Request(container, URI.create(REQUEST_URI)); + if (timeout != null) { + request.setTimeout(timeout, TimeUnit.MILLISECONDS); + } + content = request.connect(responseHandler); + } finally { + if (request != null) { + request.release(); + } + if (content != null) { + content.close(null); + } + } + return request.getTimeout(TimeUnit.MILLISECONDS); + } + } + + private static class MyCompletion implements CompletionHandler { + + final CountDownLatch completed = new CountDownLatch(1); + + @Override + public void completed() { + completed.countDown(); + } + + @Override + public void failed(Throwable t) { + + } + } + + private static class MyContent implements ContentChannel { + + ByteBuffer buf; + CompletionHandler writeCompletion; + CompletionHandler closeCompletion; + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + this.buf = buf; + this.writeCompletion = handler; + if (handler != null) { + handler.completed(); + } + } + + @Override + public void close(CompletionHandler handler) { + this.closeCompletion = handler; + if (handler != null) { + handler.completed(); + } + } + + static MyContent newInstance() { + return new MyContent(); + } + } + + private static class MyResponseHandler implements ResponseHandler { + + final AtomicReference<CountDownLatch> latch = new AtomicReference<>(new CountDownLatch(1)); + final AtomicReference<Response> response = new AtomicReference<>(); + final MyContent content; + final boolean throwException; + final boolean interruptThread; + + MyResponseHandler(MyContent content, boolean throwException, boolean interruptThread) { + this.content = content; + this.throwException = throwException; + this.interruptThread = interruptThread; + } + + boolean await(long timeout, TimeUnit unit) throws InterruptedException { + return latch.get().await(timeout, unit); + } + + @Override + public ContentChannel handleResponse(Response response) { + if (this.response.getAndSet(response) != null) { + throw new IllegalStateException("Response already received."); + } + latch.get().countDown(); + if (interruptThread) { + Thread.currentThread().interrupt(); + } + if (throwException) { + throw new MyException(); + } + return content; + } + + static MyResponseHandler newInstance() { + return new MyResponseHandler(MyContent.newInstance(), false, false); + } + + static MyResponseHandler newResponseDenied() { + return new MyResponseHandler(null, false, false); + } + + static MyResponseHandler newThrowException() { + return new MyResponseHandler(null, true, false); + } + + static MyResponseHandler newInterruptThread() { + return new MyResponseHandler(MyContent.newInstance(), false, true); + } + } + + private static class MyRequestHandler extends AbstractResource implements RequestHandler { + + final MyContent content; + final Long timeout; + int numDenied; + int numEager; + Request request = null; + ResponseHandler responseHandler = null; + boolean destroyed = false; + + MyRequestHandler(int numDenied, MyContent content, Long timeout, int numEager) { + this.numDenied = numDenied; + this.content = content; + this.timeout = timeout; + this.numEager = numEager; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + if (--numDenied >= 0) { + throw new RequestDeniedException(request); + } + this.request = request; + this.responseHandler = handler; + if (timeout != null) { + request.setTimeout(timeout, TimeUnit.MILLISECONDS); + } + if (--numEager >= 0) { + respond(); + } + return content; + } + + @Override + public void handleTimeout(Request request, ResponseHandler handler) { + Response.dispatchTimeout(handler); + } + + @Override + protected void destroy() { + destroyed = true; + } + + void respond() { + ContentChannel content = responseHandler.handleResponse(new Response(Response.Status.OK)); + if (content != null) { + content.close(null); + } + } + + static MyRequestHandler newInstance() { + return new MyRequestHandler(0, MyContent.newInstance(), null, 0); + } + + static MyRequestHandler newTimeoutWithEagerResponse(long millis) { + return new MyRequestHandler(0, MyContent.newInstance(), millis, Integer.MAX_VALUE); + } + + static MyRequestHandler newFirstRequestDenied() { + return new MyRequestHandler(1, MyContent.newInstance(), null, 0); + } + + static MyRequestHandler newEagerResponse() { + return new MyRequestHandler(0, MyContent.newInstance(), null, Integer.MAX_VALUE); + } + + public static MyRequestHandler newNullContent() { + return new MyRequestHandler(0, null, null, 0); + } + } + + private static class MyException extends RuntimeException { + + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractContentOutputStreamTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractContentOutputStreamTestCase.java new file mode 100644 index 00000000000..d314b303bd4 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractContentOutputStreamTestCase.java @@ -0,0 +1,127 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class AbstractContentOutputStreamTestCase { + + @Test + public void requireThatStreamCanBeWrittenTo() throws IOException { + MyOutputStream out = new MyOutputStream(); + int len = 2 * AbstractContentOutputStream.BUFFERSIZE; + for (int i = 0; i < len; ++i) { + out.write(69); + out.write(new byte[] { }); + out.write(new byte[] { 6, 9 }); + out.write(new byte[] { 6, 69, 9 }, 1, 0); // zero length + out.write(new byte[] { 6, 69, 9 }, 1, 1); + } + out.close(); + + InputStream in = out.toInputStream(); + for (int i = 0; i < len; ++i) { + assertEquals(69, in.read()); + assertEquals(6, in.read()); + assertEquals(9, in.read()); + assertEquals(69, in.read()); + } + assertEquals(-1, in.read()); + assertTrue(out.closed); + } + + @Test + public void requireThatBigBuffersAreWrittenInOrder() throws IOException { + MyOutputStream out = new MyOutputStream(); + out.write(6); + out.write(new byte[2 * AbstractContentOutputStream.BUFFERSIZE]); + out.write(9); + out.close(); + InputStream in = out.toInputStream(); + assertEquals(6, in.read()); + for (int i = 0, len = 2 * AbstractContentOutputStream.BUFFERSIZE; i < len; ++i) { + assertEquals(0, in.read()); + } + assertEquals(9, in.read()); + assertEquals(-1, in.read()); + assertTrue(out.closed); + } + + @Test + public void requireThatEmptyBuffersAreNotFlushed() throws Exception { + MyOutputStream out = new MyOutputStream(); + out.close(); + assertTrue(out.writes.isEmpty()); + assertTrue(out.closed); + } + + @Test + public void requireThatNoExcessiveBytesAreWritten() throws Exception { + MyOutputStream out = new MyOutputStream(); + out.write(new byte[] { 6, 9 }); + out.close(); + + InputStream in = out.toInputStream(); + assertEquals(2, in.available()); + assertEquals(6, in.read()); + assertEquals(9, in.read()); + assertEquals(0, in.available()); + assertEquals(-1, in.read()); + assertTrue(out.closed); + } + + @Test + public void requireThatWrittenArraysAreCopied() throws Exception { + MyOutputStream out = new MyOutputStream(); + byte[] buf = new byte[1]; + for (byte b = 0; b < 127; ++b) { + buf[0] = b; + out.write(buf); + } + out.close(); + + InputStream in = out.toInputStream(); + for (byte b = 0; b < 127; ++b) { + assertEquals(b, in.read()); + } + } + + private static class MyOutputStream extends AbstractContentOutputStream { + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final List<ByteBuffer> writes = new ArrayList<>(); + boolean closed; + + @Override + protected void doFlush(ByteBuffer buf) { + writes.add(buf); + buf = buf.slice(); + while (buf.hasRemaining()) { + out.write(buf.get()); + } + } + + @Override + protected void doClose() { + closed = true; + } + + InputStream toInputStream() { + return new ByteArrayInputStream(out.toByteArray()); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractRequestHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractRequestHandlerTestCase.java new file mode 100644 index 00000000000..661165ac5e8 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractRequestHandlerTestCase.java @@ -0,0 +1,187 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.test.NonWorkingRequest; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class AbstractRequestHandlerTestCase { + + private static final Charset UTF8 = Charset.forName("utf-8"); + private static int NUM_REQUESTS = 666; + + @Test + public void requireThatHandleTimeoutIsImplemented() throws Exception { + FutureResponse handler = new FutureResponse(); + new AbstractRequestHandler() { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + return null; + } + }.handleTimeout(NonWorkingRequest.newInstance("http://localhost/"), handler); + Response response = handler.get(600, TimeUnit.SECONDS); + assertNotNull(response); + assertEquals(Response.Status.REQUEST_TIMEOUT, response.getStatus()); + } + + @Test + public void requireThatHelloWorldWorks() throws InterruptedException { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://localhost/", new HelloWorldHandler()); + driver.activateContainer(builder); + + for (int i = 0; i < NUM_REQUESTS; ++i) { + MyResponseHandler responseHandler = new MyResponseHandler(); + driver.newRequestDispatch("http://localhost/", responseHandler).dispatch(); + + ByteBuffer buf = responseHandler.content.read(); + assertNotNull(buf); + assertEquals("Hello World!", new String(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining(), UTF8)); + assertNull(responseHandler.content.read()); + } + assertTrue(driver.close()); + } + + @Test + public void requireThatEchoWorks() throws InterruptedException { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://localhost/", new EchoHandler()); + driver.activateContainer(builder); + + for (int i = 0; i < NUM_REQUESTS; ++i) { + MyResponseHandler responseHandler = new MyResponseHandler(); + RequestDispatch dispatch = driver.newRequestDispatch("http://localhost/", responseHandler); + FastContentWriter requestContent = dispatch.connectFastWriter(); + ByteBuffer buf = ByteBuffer.allocate(69); + requestContent.write(buf); + requestContent.close(); + + assertSame(buf, responseHandler.content.read()); + assertNull(responseHandler.content.read()); + } + assertTrue(driver.close()); + } + + @Test + public void requireThatForwardWorks() throws InterruptedException { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://localhost/", new ForwardHandler()); + builder.clientBindings().bind("http://remotehost/", new EchoHandler()); + driver.activateContainer(builder); + + for (int i = 0; i < NUM_REQUESTS; ++i) { + MyResponseHandler responseHandler = new MyResponseHandler(); + RequestDispatch dispatch = driver.newRequestDispatch("http://localhost/", responseHandler); + FastContentWriter requestContent = dispatch.connectFastWriter(); + ByteBuffer buf = ByteBuffer.allocate(69); + requestContent.write(buf); + requestContent.close(); + + assertSame(buf, responseHandler.content.read()); + assertNull(responseHandler.content.read()); + } + assertTrue(driver.close()); + } + + private static class HelloWorldHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + FastContentWriter writer = ResponseDispatch.newInstance(Response.Status.OK).connectFastWriter(handler); + try { + writer.write("Hello World!"); + } finally { + writer.close(); + } + return null; + } + } + + private static class EchoHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + return new WritingContentChannel(new FastContentWriter(ResponseDispatch.newInstance(Response.Status.OK).connect(handler))); + } + } + + private static class ForwardHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + return new WritingContentChannel(new FastContentWriter(new RequestDispatch() { + + @Override + public Request newRequest() { + return new Request(request, URI.create("http://remotehost/")); + } + + @Override + public ContentChannel handleResponse(Response response) { + return handler.handleResponse(response); + } + }.connect())); + } + } + + private static class WritingContentChannel implements ContentChannel { + + final FastContentWriter writer; + + WritingContentChannel(FastContentWriter writer) { + this.writer = writer; + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + try { + writer.write(buf); + handler.completed(); + } catch (Exception e) { + handler.failed(e); + } + } + + @Override + public void close(CompletionHandler handler) { + try { + writer.close(); + handler.completed(); + } catch (Exception e) { + handler.failed(e); + } + } + } + + private static class MyResponseHandler implements ResponseHandler { + + final ReadableContentChannel content = new ReadableContentChannel(); + + @Override + public ContentChannel handleResponse(Response response) { + return content; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BindingNotFoundTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BindingNotFoundTestCase.java new file mode 100644 index 00000000000..58900174ee2 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BindingNotFoundTestCase.java @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BindingNotFoundTestCase { + + @Test + public void requireThatAccessorsWork() { + URI uri = URI.create("http://host/path"); + BindingNotFoundException e = new BindingNotFoundException(uri); + assertEquals(uri, e.uri()); + } + + @Test + public void requireThatBindingNotFoundIsThrown() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + Request request = new Request(driver, URI.create("http://host/path")); + try { + request.connect(new MyResponseHandler()); + fail(); + } catch (BindingNotFoundException e) { + assertEquals(request.getUri(), e.uri()); + } + request.release(); + driver.close(); + } + + private class MyResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(Response response) { + return null; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BlockingContentWriterTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BlockingContentWriterTestCase.java new file mode 100644 index 00000000000..b539c135edb --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BlockingContentWriterTestCase.java @@ -0,0 +1,210 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import org.junit.Test; + +import java.nio.ByteBuffer; +import java.util.concurrent.*; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BlockingContentWriterTestCase { + + @Test + public void requireThatContentChannelIsNotNull() { + try { + new BlockingContentWriter(null); + fail(); + } catch (NullPointerException e) { + + } + } + + @Test + public void requireThatWriteDeliversBuffer() throws InterruptedException { + MyContent content = MyContent.newNonBlockingContent(); + BlockingContentWriter writer = new BlockingContentWriter(content); + ByteBuffer buf = ByteBuffer.allocate(69); + writer.write(buf); + assertSame(buf, content.writeBuf); + } + + @Test + public void requireThatWriteIsBlocking() throws Exception { + MyContent content = MyContent.newBlockingContent(); + BlockingContentWriter writer = new BlockingContentWriter(content); + FutureTask<Boolean> task = new FutureTask<>(new WriteTask(writer, ByteBuffer.allocate(69))); + Executors.newSingleThreadExecutor().submit(task); + content.writeLatch.await(600, TimeUnit.SECONDS); + try { + task.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + content.writeCompletion.completed(); + assertTrue(task.get(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatWriteExceptionIsThrown() throws Exception { + Throwable throwMe = new RuntimeException(); + try { + new BlockingContentWriter(MyContent.newFailedContent(throwMe)).write(ByteBuffer.allocate(69)); + } catch (Throwable t) { + assertSame(throwMe, t); + } + throwMe = new Error(); + try { + new BlockingContentWriter(MyContent.newFailedContent(throwMe)).write(ByteBuffer.allocate(69)); + } catch (Throwable t) { + assertSame(throwMe, t); + } + throwMe = new Exception(); + try { + new BlockingContentWriter(MyContent.newFailedContent(throwMe)).write(ByteBuffer.allocate(69)); + } catch (Throwable t) { + assertNotSame(throwMe, t); + assertSame(throwMe, t.getCause()); + } + } + + @Test + public void requireThatCloseIsBlocking() throws Exception { + MyContent content = MyContent.newBlockingContent(); + BlockingContentWriter writer = new BlockingContentWriter(content); + FutureTask<Boolean> task = new FutureTask<>(new CloseTask(writer)); + Executors.newSingleThreadExecutor().submit(task); + content.closeLatch.await(600, TimeUnit.SECONDS); + try { + task.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + content.closeCompletion.completed(); + assertTrue(task.get(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatCloseExceptionIsThrown() throws Exception { + Throwable throwMe = new RuntimeException(); + try { + new BlockingContentWriter(MyContent.newFailedContent(throwMe)).close(); + } catch (Throwable t) { + assertSame(throwMe, t); + } + throwMe = new Error(); + try { + new BlockingContentWriter(MyContent.newFailedContent(throwMe)).close(); + } catch (Throwable t) { + assertSame(throwMe, t); + } + throwMe = new Exception(); + try { + new BlockingContentWriter(MyContent.newFailedContent(throwMe)).close(); + } catch (Throwable t) { + assertNotSame(throwMe, t); + assertSame(throwMe, t.getCause()); + } + } + + private static class MyContent implements ContentChannel { + + final CountDownLatch writeLatch = new CountDownLatch(1); + final CountDownLatch closeLatch = new CountDownLatch(1); + final Throwable eagerFailure; + final boolean eagerCompletion; + CompletionHandler writeCompletion; + CompletionHandler closeCompletion; + ByteBuffer writeBuf; + + MyContent(boolean eagerCompletion, Throwable eagerFailure) { + this.eagerCompletion = eagerCompletion; + this.eagerFailure = eagerFailure; + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + writeBuf = buf; + if (eagerFailure != null) { + handler.failed(eagerFailure); + } else if (eagerCompletion) { + handler.completed(); + } else { + writeCompletion = handler; + } + writeLatch.countDown(); + } + + @Override + public void close(CompletionHandler handler) { + if (eagerFailure != null) { + handler.failed(eagerFailure); + } else if (eagerCompletion) { + handler.completed(); + } else { + closeCompletion = handler; + } + closeLatch.countDown(); + } + + static MyContent newBlockingContent() { + return new MyContent(false, null); + } + + static MyContent newNonBlockingContent() { + return new MyContent(true, null); + } + + static MyContent newFailedContent(Throwable e) { + return new MyContent(false, e); + } + } + + private static class WriteTask implements Callable<Boolean> { + + final BlockingContentWriter writer; + final ByteBuffer buf; + + WriteTask(BlockingContentWriter writer, ByteBuffer buf) { + this.writer = writer; + this.buf = buf; + } + + @Override + public Boolean call() { + try { + writer.write(buf); + } catch (Throwable t) { + return false; + } + return true; + } + } + + private static class CloseTask implements Callable<Boolean> { + + final BlockingContentWriter writer; + + CloseTask(BlockingContentWriter writer) { + this.writer = writer; + } + + @Override + public Boolean call() { + try { + writer.close(); + } catch (Throwable t) { + return false; + } + return true; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BufferedContentChannelTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BufferedContentChannelTestCase.java new file mode 100644 index 00000000000..c6714f11203 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BufferedContentChannelTestCase.java @@ -0,0 +1,257 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import org.junit.Test; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.*; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BufferedContentChannelTestCase { + + @Test + public void requireThatIsConnectedWorks() { + MyContent target = new MyContent(); + BufferedContentChannel content = new BufferedContentChannel(); + assertFalse(content.isConnected()); + content.connectTo(target); + assertTrue(content.isConnected()); + } + + @Test + public void requireThatConnectToNullThrowsException() { + BufferedContentChannel content = new BufferedContentChannel(); + try { + content.connectTo(null); + fail(); + } catch (NullPointerException e) { + + } + } + + @Test + public void requireThatWriteAfterCloseThrowsException() { + BufferedContentChannel content = new BufferedContentChannel(); + content.close(null); + try { + content.write(ByteBuffer.allocate(69), null); + fail(); + } catch (IllegalStateException e) { + + } + } + + @Test + public void requireThatCloseAfterCloseThrowsException() { + BufferedContentChannel content = new BufferedContentChannel(); + content.close(null); + try { + content.close(null); + fail(); + } catch (IllegalStateException e) { + + } + } + + @Test + public void requireThatConnecToAfterConnecToThrowsException() { + BufferedContentChannel content = new BufferedContentChannel(); + content.connectTo(new MyContent()); + try { + content.connectTo(new MyContent()); + fail(); + } catch (IllegalStateException e) { + + } + } + + @Test + public void requireThatWriteBeforeConnectToWritesToTarget() { + BufferedContentChannel content = new BufferedContentChannel(); + ByteBuffer buf = ByteBuffer.allocate(69); + MyCompletion completion = new MyCompletion(); + content.write(buf, completion); + MyContent target = new MyContent(); + content.connectTo(target); + assertSame(buf, target.writeBuf); + assertSame(completion, target.writeCompletion); + } + + @Test + public void requireThatWriteAfterConnectToWritesToTarget() { + MyContent target = new MyContent(); + BufferedContentChannel content = new BufferedContentChannel(); + content.connectTo(target); + ByteBuffer buf = ByteBuffer.allocate(69); + MyCompletion completion = new MyCompletion(); + content.write(buf, completion); + assertSame(buf, target.writeBuf); + assertSame(completion, target.writeCompletion); + } + + @Test + public void requireThatCloseBeforeConnectToClosesTarget() { + BufferedContentChannel content = new BufferedContentChannel(); + MyCompletion completion = new MyCompletion(); + content.close(completion); + MyContent target = new MyContent(); + content.connectTo(target); + assertTrue(target.closed); + assertSame(completion, target.closeCompletion); + } + + @Test + public void requireThatCloseAfterConnectToClosesTarget() { + MyContent target = new MyContent(); + BufferedContentChannel content = new BufferedContentChannel(); + content.connectTo(target); + MyCompletion completion = new MyCompletion(); + content.close(completion); + assertTrue(target.closed); + assertSame(completion, target.closeCompletion); + } + + @Test + public void requireThatIsConnectedIsTrueWhenConnectedBeforeClose() { + BufferedContentChannel content = new BufferedContentChannel(); + assertFalse(content.isConnected()); + content.connectTo(new MyContent()); + assertTrue(content.isConnected()); + content.close(null); + assertTrue(content.isConnected()); + } + + @Test + public void requireThatIsConnectedIsTrueWhenClosedBeforeConnected() { + BufferedContentChannel content = new BufferedContentChannel(); + assertFalse(content.isConnected()); + content.close(null); + assertFalse(content.isConnected()); + content.connectTo(new MyContent()); + assertTrue(content.isConnected()); + } + + @Test + public void requireThatContentIsThreadSafe() throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(101); + for (int run = 0; run < 69; ++run) { + List<ByteBuffer> bufs = new LinkedList<>(); + for (int buf = 0; buf < 100; ++buf) { + bufs.add(ByteBuffer.allocate(buf)); + } + BufferedContentChannel content = new BufferedContentChannel(); + List<Callable<Boolean>> tasks = new LinkedList<>(); + for (ByteBuffer buf : bufs) { + tasks.add(new WriteTask(content, buf)); + } + MyConcurrentContent target = new MyConcurrentContent(); + tasks.add(new ConnectTask(content, target)); + List<Future<Boolean>> results = executor.invokeAll(tasks); + for (Future<Boolean> result : results) { + assertTrue(result.get()); + } + assertEquals(bufs.size(), target.bufs.size()); + for (ByteBuffer buf : target.bufs) { + assertTrue(bufs.remove(buf)); + } + assertTrue(bufs.isEmpty()); + } + } + + private static class WriteTask implements Callable<Boolean> { + + final Random rnd = new Random(); + final BufferedContentChannel content; + final ByteBuffer buf; + + WriteTask(BufferedContentChannel content, ByteBuffer buf) { + this.content = content; + this.buf = buf; + } + + @Override + public Boolean call() throws Exception { + if (rnd.nextBoolean()) { + Thread.sleep(rnd.nextInt(5)); + } + content.write(buf, null); + return Boolean.TRUE; + } + } + + private static class ConnectTask implements Callable<Boolean> { + + final BufferedContentChannel content; + final ContentChannel target; + + ConnectTask(BufferedContentChannel content, ContentChannel target) { + this.content = content; + this.target = target; + } + + @Override + public Boolean call() throws Exception { + content.connectTo(target); + return Boolean.TRUE; + } + } + + private static class MyContent implements ContentChannel { + + ByteBuffer writeBuf = null; + CompletionHandler writeCompletion; + CompletionHandler closeCompletion; + boolean closed = false; + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + writeBuf = buf; + writeCompletion = handler; + } + + @Override + public void close(CompletionHandler handler) { + closeCompletion = handler; + closed = true; + } + } + + private static class MyConcurrentContent implements ContentChannel { + + ConcurrentLinkedQueue<ByteBuffer> bufs = new ConcurrentLinkedQueue<>(); + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + bufs.add(buf); + } + + @Override + public void close(CompletionHandler handler) { + + } + } + + private static class MyCompletion implements CompletionHandler { + + @Override + public void completed() { + + } + + @Override + public void failed(Throwable throwable) { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableRequestDispatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableRequestDispatchTestCase.java new file mode 100644 index 00000000000..d2768707528 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableRequestDispatchTestCase.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class CallableRequestDispatchTestCase { + + @Test + public void requireThatDispatchIsCalled() throws Exception { + final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + Response response = new Response(Response.Status.OK); + builder.serverBindings().bind("http://host/path", new MyRequestHandler(response)); + driver.activateContainer(builder); + assertSame(response, new CallableRequestDispatch() { + + @Override + protected Request newRequest() { + return new Request(driver, URI.create("http://host/path")); + } + }.call()); + assertTrue(driver.close()); + } + + private static class MyRequestHandler extends AbstractRequestHandler { + + final Response response; + + MyRequestHandler(Response response) { + this.response = response; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + ResponseDispatch.newInstance(response).dispatch(handler); + return null; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableResponseDispatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableResponseDispatchTestCase.java new file mode 100644 index 00000000000..9b107f93178 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableResponseDispatchTestCase.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.Response; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertSame; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class CallableResponseDispatchTestCase { + + @Test + public void requireThatDispatchIsCalled() throws Exception { + final Response response = new Response(Response.Status.OK); + FutureResponse handler = new FutureResponse(); + new CallableResponseDispatch(handler) { + + @Override + protected Response newResponse() { + return response; + } + }.call(); + assertSame(response, handler.get(600, TimeUnit.SECONDS)); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ContentInputStreamTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ContentInputStreamTestCase.java new file mode 100644 index 00000000000..618c1b1ed1c --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ContentInputStreamTestCase.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import org.junit.Test; + +import java.util.concurrent.Future; + +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContentInputStreamTestCase { + + @Test + public void requireThatContentInputStreamExtendsUnsafeContentInputStream() { + assertTrue(UnsafeContentInputStream.class.isAssignableFrom(ContentInputStream.class)); + } + + @Test + @SuppressWarnings("FinalizeCalledExplicitly") + public void requireThatFinalizerClosesStream() throws Throwable { + BufferedContentChannel channel = new BufferedContentChannel(); + FastContentWriter writer = new FastContentWriter(channel); + writer.write("foo"); + writer.close(); + + new ContentInputStream(channel.toReadable()).finalize(); + } + +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentOutputStreamTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentOutputStreamTestCase.java new file mode 100644 index 00000000000..00ea92ff246 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentOutputStreamTestCase.java @@ -0,0 +1,70 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import org.junit.Test; +import org.mockito.Mockito; + +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class FastContentOutputStreamTestCase { + + @Test + public void requireThatNullConstructorArgumentThrows() { + try { + new FastContentOutputStream((ContentChannel)null); + fail(); + } catch (NullPointerException e) { + assertEquals("out", e.getMessage()); + } + try { + new FastContentOutputStream((FastContentWriter)null); + fail(); + } catch (NullPointerException e) { + assertEquals("out", e.getMessage()); + } + } + + @Test + public void requireThatAllMethodsDelegateToWriter() throws Exception { + FastContentWriter writer = Mockito.mock(FastContentWriter.class); + FastContentOutputStream out = new FastContentOutputStream(writer); + + out.write(new byte[] { 6, 9 }); + out.flush(); + Mockito.verify(writer).write(Mockito.any(ByteBuffer.class)); + + out.close(); + Mockito.verify(writer).close(); + + out.cancel(true); + Mockito.verify(writer).cancel(true); + out.cancel(false); + Mockito.verify(writer).cancel(false); + + out.isCancelled(); + Mockito.verify(writer).isCancelled(); + + out.isDone(); + Mockito.verify(writer).isDone(); + + out.get(); + Mockito.verify(writer).get(); + + out.get(600, TimeUnit.SECONDS); + Mockito.verify(writer).get(600, TimeUnit.SECONDS); + + Runnable listener = Mockito.mock(Runnable.class); + Executor executor = Mockito.mock(Executor.class); + out.addListener(listener, executor); + Mockito.verify(writer).addListener(listener, executor); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentWriterTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentWriterTestCase.java new file mode 100644 index 00000000000..e3bedaf5c2a --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentWriterTestCase.java @@ -0,0 +1,241 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.Test; +import org.mockito.Mockito; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertArrayEquals; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class FastContentWriterTestCase { + + @Test + public void requireThatContentCanBeWritten() throws ExecutionException, InterruptedException { + ReadableContentChannel content = new ReadableContentChannel(); + FastContentWriter out = new FastContentWriter(content); + + ByteBuffer foo = ByteBuffer.allocate(69); + out.write(foo); + ByteBuffer bar = ByteBuffer.allocate(69); + out.write(bar); + out.close(); + + assertFalse(out.isDone()); + assertSame(foo, content.read()); + assertFalse(out.isDone()); + assertSame(bar, content.read()); + assertFalse(out.isDone()); + assertNull(content.read()); + assertTrue(out.isDone()); + } + + @Test + public void requireThatStringsAreUtf8Encoded() { + ReadableContentChannel content = new ReadableContentChannel(); + FastContentWriter out = new FastContentWriter(content); + + String in = "\u6211\u80FD\u541E\u4E0B\u73BB\u7483\u800C\u4E0D\u4F24\u8EAB\u4F53\u3002"; + out.write(in); + out.close(); + + ByteBuffer buf = content.read(); + byte[] arr = new byte[buf.remaining()]; + buf.get(arr); + assertArrayEquals(in.getBytes(StandardCharsets.UTF_8), arr); + } + + @Test + public void requireThatCancelThrowsUnsupportedOperation() { + try { + new FastContentWriter(Mockito.mock(ContentChannel.class)).cancel(true); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatCancelIsAlwaysFalse() { + FastContentWriter writer = new FastContentWriter(Mockito.mock(ContentChannel.class)); + assertFalse(writer.isCancelled()); + try { + writer.cancel(true); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertFalse(writer.isCancelled()); + } + + @Test + public void requireThatGetThrowsTimeoutUntilCloseCompletionHandlerIsCalled() throws Exception { + ReadableContentChannel buf = new ReadableContentChannel(); + FastContentWriter out = new FastContentWriter(buf); + + out.write(new byte[] { 6, 9 }); + assertFalse(out.isDone()); + try { + out.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + + assertNotNull(buf.read()); + assertFalse(out.isDone()); + try { + out.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + + out.close(); + assertFalse(out.isDone()); + try { + out.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + + assertNull(buf.read()); + assertTrue(out.isDone()); + assertTrue(out.get(600, TimeUnit.SECONDS)); + assertTrue(out.get()); + } + + @Test + public void requireThatSyncWriteExceptionFailsFuture() throws InterruptedException { + IllegalStateException expected = new IllegalStateException(); + ContentChannel content = Mockito.mock(ContentChannel.class); + Mockito.doThrow(expected) + .when(content).write(Mockito.any(ByteBuffer.class), Mockito.any(CompletionHandler.class)); + FastContentWriter out = new FastContentWriter(content); + try { + out.write("foo"); + fail(); + } catch (Throwable t) { + assertSame(expected, t); + } + try { + out.get(); + fail(); + } catch (ExecutionException e) { + assertSame(expected, e.getCause()); + } + } + + @Test + public void requireThatSyncCloseExceptionFailsFuture() throws InterruptedException { + IllegalStateException expected = new IllegalStateException(); + ContentChannel content = Mockito.mock(ContentChannel.class); + Mockito.doThrow(expected) + .when(content).close(Mockito.any(CompletionHandler.class)); + FastContentWriter out = new FastContentWriter(content); + try { + out.close(); + fail(); + } catch (Throwable t) { + assertSame(expected, t); + } + try { + out.get(); + fail(); + } catch (ExecutionException e) { + assertSame(expected, e.getCause()); + } + } + + @Test + public void requireThatAsyncExceptionFailsFuture() throws InterruptedException { + IllegalStateException expected = new IllegalStateException(); + ReadableContentChannel content = new ReadableContentChannel(); + FastContentWriter out = new FastContentWriter(content); + out.write("foo"); + content.failed(expected); + try { + out.get(); + fail(); + } catch (ExecutionException e) { + assertSame(expected, e.getCause()); + } + } + + @Test + public void requireThatWriterCanBeListenedTo() throws InterruptedException { + ReadableContentChannel buf = new ReadableContentChannel(); + FastContentWriter out = new FastContentWriter(buf); + RunnableLatch listener = new RunnableLatch(); + out.addListener(listener, MoreExecutors.sameThreadExecutor()); + + out.write(new byte[] { 6, 9 }); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + assertNotNull(buf.read()); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + out.close(); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + assertNull(buf.read()); + assertTrue(listener.await(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatWriterIsThreadSafe() throws Exception { + final CountDownLatch latch = new CountDownLatch(2); + final ReadableContentChannel content = new ReadableContentChannel(); + Future<Integer> read = Executors.newSingleThreadExecutor().submit(new Callable<Integer>() { + + @Override + public Integer call() throws Exception { + latch.countDown(); + latch.await(600, TimeUnit.SECONDS); + + int bufCnt = 0; + while (content.read() != null) { + ++bufCnt; + } + return bufCnt; + } + }); + Future<Integer> write = Executors.newSingleThreadExecutor().submit(new Callable<Integer>() { + + @Override + public Integer call() throws Exception { + FastContentWriter out = new FastContentWriter(content); + ByteBuffer buf = ByteBuffer.wrap(new byte[69]); + int bufCnt = 4096 + new Random().nextInt(4096); + + latch.countDown(); + latch.await(600, TimeUnit.SECONDS); + for (int i = 0; i < bufCnt; ++i) { + out.write(buf.slice()); + } + out.close(); + return bufCnt; + } + }); + assertEquals(read.get(600, TimeUnit.SECONDS), + write.get(600, TimeUnit.SECONDS)); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureCompletionTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureCompletionTestCase.java new file mode 100644 index 00000000000..e886d663a72 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureCompletionTestCase.java @@ -0,0 +1,106 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.Test; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class FutureCompletionTestCase { + + @Test + public void requireThatCancelIsUnsupported() { + FutureCompletion future = new FutureCompletion(); + assertFalse(future.isCancelled()); + try { + future.cancel(true); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertFalse(future.isCancelled()); + try { + future.cancel(false); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertFalse(future.isCancelled()); + } + + @Test + public void requireThatCompletedReturnsTrue() throws Exception { + FutureCompletion future = new FutureCompletion(); + try { + future.get(0, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + future.completed(); + assertTrue(future.get(0, TimeUnit.MILLISECONDS)); + assertTrue(future.get()); + } + + @Test + public void requireThatCompletionIsDoneWhenCompleted() { + FutureCompletion future = new FutureCompletion(); + assertFalse(future.isDone()); + future.completed(); + assertTrue(future.isDone()); + } + + @Test + public void requireThatCompletionIsDoneWhenFailed() { + FutureCompletion future = new FutureCompletion(); + assertFalse(future.isDone()); + future.failed(new Throwable()); + assertTrue(future.isDone()); + } + + @Test + public void requireThatFailedCauseIsRethrown() throws Exception { + FutureCompletion future = new FutureCompletion(); + Throwable t = new Throwable(); + future.failed(t); + try { + future.get(0, TimeUnit.SECONDS); + fail(); + } catch (ExecutionException e) { + assertSame(t, e.getCause()); + } + try { + future.get(); + fail(); + } catch (ExecutionException e) { + assertSame(t, e.getCause()); + } + } + + @Test + public void requireThatCompletionCanBeListenedTo() throws InterruptedException { + FutureCompletion completion = new FutureCompletion(); + RunnableLatch listener = new RunnableLatch(); + completion.addListener(listener, MoreExecutors.sameThreadExecutor()); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + completion.completed(); + assertTrue(listener.await(600, TimeUnit.SECONDS)); + + completion = new FutureCompletion(); + listener = new RunnableLatch(); + completion.addListener(listener, MoreExecutors.sameThreadExecutor()); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + completion.failed(new Throwable()); + assertTrue(listener.await(600, TimeUnit.SECONDS)); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureConjunctionTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureConjunctionTestCase.java new file mode 100644 index 00000000000..3916eb7ffd6 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureConjunctionTestCase.java @@ -0,0 +1,255 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.AbstractFuture; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class FutureConjunctionTestCase { + + private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + + @Test + public void requireThatAllFuturesAreWaitedFor() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + FutureConjunction future = new FutureConjunction(); + future.addOperand(executor.submit(new Callable<Boolean>() { + + @Override + public Boolean call() throws Exception { + return latch.await(600, TimeUnit.SECONDS); + } + })); + try { + future.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + latch.countDown(); + assertTrue(future.get(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatGetReturnValueIsAConjunction() throws Exception { + assertTrue(tryGet(true)); + assertTrue(tryGet(true, true)); + assertTrue(tryGet(true, true, true)); + + assertFalse(tryGet(false)); + assertFalse(tryGet(false, true)); + assertFalse(tryGet(true, false)); + assertFalse(tryGet(false, true, true)); + assertFalse(tryGet(true, false, true)); + assertFalse(tryGet(true, true, false)); + assertFalse(tryGet(false, false, true)); + assertFalse(tryGet(false, true, false)); + assertFalse(tryGet(true, false, false)); + } + + @Test + public void requireThatIsDoneReturnValueIsAConjunction() { + assertTrue(tryIsDone(true)); + assertTrue(tryIsDone(true, true)); + assertTrue(tryIsDone(true, true, true)); + + assertFalse(tryIsDone(false)); + assertFalse(tryIsDone(false, true)); + assertFalse(tryIsDone(true, false)); + assertFalse(tryIsDone(false, true, true)); + assertFalse(tryIsDone(true, false, true)); + assertFalse(tryIsDone(true, true, false)); + assertFalse(tryIsDone(false, false, true)); + assertFalse(tryIsDone(false, true, false)); + assertFalse(tryIsDone(true, false, false)); + } + + @Test + public void requireThatCancelReturnValueIsAConjuction() { + assertTrue(tryCancel(true)); + assertTrue(tryCancel(true, true)); + assertTrue(tryCancel(true, true, true)); + + assertFalse(tryCancel(false)); + assertFalse(tryCancel(false, true)); + assertFalse(tryCancel(true, false)); + assertFalse(tryCancel(false, true, true)); + assertFalse(tryCancel(true, false, true)); + assertFalse(tryCancel(true, true, false)); + assertFalse(tryCancel(false, false, true)); + assertFalse(tryCancel(false, true, false)); + assertFalse(tryCancel(true, false, false)); + } + + @Test + public void requireThatIsCancelledReturnValueIsAConjuction() { + assertTrue(tryIsCancelled(true)); + assertTrue(tryIsCancelled(true, true)); + assertTrue(tryIsCancelled(true, true, true)); + + assertFalse(tryIsCancelled(false)); + assertFalse(tryIsCancelled(false, true)); + assertFalse(tryIsCancelled(true, false)); + assertFalse(tryIsCancelled(false, true, true)); + assertFalse(tryIsCancelled(true, false, true)); + assertFalse(tryIsCancelled(true, true, false)); + assertFalse(tryIsCancelled(false, false, true)); + assertFalse(tryIsCancelled(false, true, false)); + assertFalse(tryIsCancelled(true, false, false)); + } + + @Test + public void requireThatConjunctionCanBeListenedTo() throws InterruptedException { + FutureConjunction conjunction = new FutureConjunction(); + RunnableLatch listener = new RunnableLatch(); + conjunction.addListener(listener, MoreExecutors.sameThreadExecutor()); + assertTrue(listener.await(600, TimeUnit.SECONDS)); + + conjunction = new FutureConjunction(); + FutureBoolean foo = new FutureBoolean(); + conjunction.addOperand(foo); + FutureBoolean bar = new FutureBoolean(); + conjunction.addOperand(bar); + listener = new RunnableLatch(); + conjunction.addListener(listener, MoreExecutors.sameThreadExecutor()); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + foo.set(true); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + bar.set(true); + assertTrue(listener.await(600, TimeUnit.SECONDS)); + + conjunction = new FutureConjunction(); + foo = new FutureBoolean(); + conjunction.addOperand(foo); + bar = new FutureBoolean(); + conjunction.addOperand(bar); + listener = new RunnableLatch(); + conjunction.addListener(listener, MoreExecutors.sameThreadExecutor()); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + bar.set(true); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + foo.set(true); + assertTrue(listener.await(600, TimeUnit.SECONDS)); + } + + private static boolean tryGet(boolean... operands) throws Exception { + FutureConjunction foo = new FutureConjunction(); + FutureConjunction bar = new FutureConjunction(); + for (boolean op : operands) { + foo.addOperand(MyFuture.newInstance(op)); + bar.addOperand(MyFuture.newInstance(op)); + } + boolean fooResult = foo.get(); + boolean barResult = foo.get(0, TimeUnit.SECONDS); + assertEquals(fooResult, barResult); + return fooResult; + } + + private static boolean tryIsDone(boolean... operands) { + FutureConjunction foo = new FutureConjunction(); + for (boolean op : operands) { + foo.addOperand(MyFuture.newIsDone(op)); + } + return foo.isDone(); + } + + private static boolean tryCancel(boolean... operands) { + FutureConjunction foo = new FutureConjunction(); + FutureConjunction bar = new FutureConjunction(); + for (boolean op : operands) { + foo.addOperand(MyFuture.newCanCancel(op)); + bar.addOperand(MyFuture.newCanCancel(op)); + } + boolean fooResult = foo.cancel(true); + boolean barResult = foo.cancel(false); + assertEquals(fooResult, barResult); + return fooResult; + } + + private static boolean tryIsCancelled(boolean... operands) { + FutureConjunction foo = new FutureConjunction(); + for (boolean op : operands) { + foo.addOperand(MyFuture.newIsCancelled(op)); + } + return foo.isCancelled(); + } + + private static class FutureBoolean extends AbstractFuture<Boolean> { + + public boolean set(Boolean val) { + return super.set(val); + } + } + + private static class MyFuture extends AbstractFuture<Boolean> { + + final boolean value; + final boolean isDone; + final boolean canCancel; + final boolean isCancelled; + + MyFuture(boolean value, boolean isDone, boolean canCancel, boolean isCancelled) { + this.value = value; + this.isDone = isDone; + this.canCancel = canCancel; + this.isCancelled = isCancelled; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return canCancel; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + @Override + public boolean isDone() { + return isDone; + } + + @Override + public Boolean get() { + return value; + } + + @Override + public Boolean get(long timeout, TimeUnit unit) { + return value; + } + + static ListenableFuture<Boolean> newInstance(boolean value) { + return new MyFuture(value, false, false, false); + } + + static ListenableFuture<Boolean> newIsDone(boolean isDone) { + return new MyFuture(false, isDone, false, false); + } + + static ListenableFuture<Boolean> newCanCancel(boolean canCancel) { + return new MyFuture(false, false, canCancel, false); + } + + static ListenableFuture<Boolean> newIsCancelled(boolean isCancelled) { + return new MyFuture(false, false, false, isCancelled); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureResponseTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureResponseTestCase.java new file mode 100644 index 00000000000..925c09f763d --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureResponseTestCase.java @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.MoreExecutors; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.test.NonWorkingContentChannel; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class FutureResponseTestCase { + + @Test + public void requireThatCancelIsUnsupported() { + FutureResponse future = new FutureResponse(); + assertFalse(future.isCancelled()); + try { + future.cancel(true); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertFalse(future.isCancelled()); + try { + future.cancel(false); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertFalse(future.isCancelled()); + } + + @Test + public void requireThatCompletionIsDoneWhenHandlerIsCalled() { + FutureResponse future = new FutureResponse(); + assertFalse(future.isDone()); + future.handleResponse(new Response(69)); + assertTrue(future.isDone()); + } + + @Test + public void requireThatResponseBecomesAvailable() throws Exception { + FutureResponse future = new FutureResponse(); + try { + future.get(0, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + Response response = new Response(Response.Status.OK); + future.handleResponse(response); + assertSame(response, future.get(0, TimeUnit.MILLISECONDS)); + } + + @Test + public void requireThatResponseContentIsReturnedToCaller() throws Exception { + ContentChannel content = new NonWorkingContentChannel(); + FutureResponse future = new FutureResponse(content); + Response response = new Response(Response.Status.OK); + assertSame(content, future.handleResponse(response)); + } + + @Test + public void requireThatResponseCanBeListenedTo() throws InterruptedException { + FutureResponse response = new FutureResponse(); + RunnableLatch listener = new RunnableLatch(); + response.addListener(listener, MoreExecutors.sameThreadExecutor()); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + response.handleResponse(new Response(Response.Status.OK)); + assertTrue(listener.await(600, TimeUnit.SECONDS)); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/NullContentTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/NullContentTestCase.java new file mode 100644 index 00000000000..40f6cd88216 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/NullContentTestCase.java @@ -0,0 +1,48 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import org.junit.Test; +import org.mockito.Mockito; + +import java.nio.ByteBuffer; + +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NullContentTestCase { + + @Test + public void requireThatWriteThrowsException() { + CompletionHandler completion = Mockito.mock(CompletionHandler.class); + try { + NullContent.INSTANCE.write(ByteBuffer.allocate(69), completion); + fail(); + } catch (UnsupportedOperationException e) { + + } + Mockito.verifyZeroInteractions(completion); + } + + @Test + public void requireThatWriteEmptyDoesNotThrowException() { + CompletionHandler completion = Mockito.mock(CompletionHandler.class); + NullContent.INSTANCE.write(ByteBuffer.allocate(0), completion); + Mockito.verify(completion).completed(); + Mockito.verifyNoMoreInteractions(completion); + } + + @Test + public void requireThatCloseCallsCompletion() { + CompletionHandler completion = Mockito.mock(CompletionHandler.class); + NullContent.INSTANCE.close(completion); + Mockito.verify(completion).completed(); + Mockito.verifyNoMoreInteractions(completion); + } + + @Test + public void requireThatCloseWithoutCompletionDoesNotThrow() { + NullContent.INSTANCE.close(null); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ReadableContentChannelTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ReadableContentChannelTestCase.java new file mode 100644 index 00000000000..378da5449c2 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ReadableContentChannelTestCase.java @@ -0,0 +1,320 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import org.junit.Test; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.*; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ReadableContentChannelTestCase { + + @Test + public void requireThatWriteNullThrowsException() { + ReadableContentChannel content = new ReadableContentChannel(); + try { + content.write(null, new MyCompletion()); + fail(); + } catch (NullPointerException e) { + + } + } + + @Test + public void requireThatWriteAfterCloseThrowsException() { + ReadableContentChannel content = new ReadableContentChannel(); + content.close(null); + try { + content.write(ByteBuffer.allocate(69), new MyCompletion()); + fail(); + } catch (IllegalStateException e) { + + } + } + + @Test + public void requireThatWriteAfterFailedThrowsException() { + ReadableContentChannel content = new ReadableContentChannel(); + content.failed(new RuntimeException()); + try { + content.write(ByteBuffer.allocate(69), new MyCompletion()); + fail(); + } catch (IllegalStateException e) { + + } + } + + @Test + public void requireThatCloseAfterCloseThrowsException() { + ReadableContentChannel content = new ReadableContentChannel(); + content.close(null); + try { + content.close(null); + fail(); + } catch (IllegalStateException e) { + + } + } + + @Test + public void requireThatCloseAfterFailedThrowsException() { + ReadableContentChannel content = new ReadableContentChannel(); + content.failed(new RuntimeException()); + try { + content.close(null); + fail(); + } catch (IllegalStateException e) { + + } + } + + @Test + public void requireThatFailedAfterFailedThrowsException() { + ReadableContentChannel content = new ReadableContentChannel(); + content.failed(new RuntimeException()); + try { + content.failed(new RuntimeException()); + fail(); + } catch (IllegalStateException e) { + + } + } + + @Test + public void requireThatIteratorDoesNotSupportRemove() { + try { + new ReadableContentChannel().iterator().remove(); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatWrittenBufferCanBeRead() { + ReadableContentChannel content = new ReadableContentChannel(); + ByteBuffer buf = ByteBuffer.allocate(69); + content.write(buf, null); + assertSame(buf, content.read()); + } + + @Test + public void requireThatWrittenBuffersAreReadInOrder() { + ReadableContentChannel content = new ReadableContentChannel(); + ByteBuffer foo = ByteBuffer.allocate(69); + content.write(foo, null); + ByteBuffer bar = ByteBuffer.allocate(69); + content.write(bar, null); + content.close(null); + assertSame(foo, content.read()); + assertSame(bar, content.read()); + } + + @Test + public void requireThatReadAfterCloseIsNull() { + ReadableContentChannel content = new ReadableContentChannel(); + content.close(null); + assertNull(content.read()); + assertNull(content.read()); + } + + @Test + public void requireThatWrittenBufferCanBeReadByIterator() { + ReadableContentChannel content = new ReadableContentChannel(); + ByteBuffer foo = ByteBuffer.allocate(69); + content.write(foo, null); + ByteBuffer bar = ByteBuffer.allocate(69); + content.write(bar, null); + content.close(null); + + Iterator<ByteBuffer> it = content.iterator(); + assertTrue(it.hasNext()); + assertSame(foo, it.next()); + assertTrue(it.hasNext()); + assertSame(bar, it.next()); + assertFalse(it.hasNext()); + try { + it.next(); + fail(); + } catch (NoSuchElementException e) { + + } + } + + @Test + public void requireThatReadAfterFailedIsNull() { + ReadableContentChannel content = new ReadableContentChannel(); + content.failed(new RuntimeException()); + assertNull(content.read()); + assertNull(content.read()); + } + + @Test + public void requireThatReadCallsCompletion() { + ReadableContentChannel content = new ReadableContentChannel(); + ByteBuffer buf = ByteBuffer.allocate(69); + MyCompletion completion = new MyCompletion(); + content.write(buf, completion); + assertFalse(completion.completed); + assertSame(buf, content.read()); + assertTrue(completion.completed); + + completion = new MyCompletion(); + content.close(completion); + assertFalse(completion.completed); + assertNull(content.read()); + assertTrue(completion.completed); + } + + @Test + public void requireThatReadWaitsForWrite() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ReadableContentChannel content = new ReadableContentChannel(); + Future<ByteBuffer> readBuf = executor.submit(new ReadTask(content)); + try { + readBuf.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + ByteBuffer buf = ByteBuffer.allocate(69); + content.write(buf, null); + assertSame(buf, readBuf.get(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatCloseNotifiesRead() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ReadableContentChannel content = new ReadableContentChannel(); + Future<ByteBuffer> buf = executor.submit(new ReadTask(content)); + try { + buf.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + content.close(null); + assertNull(buf.get(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatFailedNotifiesRead() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ReadableContentChannel content = new ReadableContentChannel(); + Future<ByteBuffer> buf = executor.submit(new ReadTask(content)); + try { + buf.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + content.failed(new RuntimeException()); + assertNull(buf.get(600, TimeUnit.SECONDS)); + } + + @Test + public void requireThatFailedCallsPendingCompletions() { + MyCompletion foo = new MyCompletion(); + MyCompletion bar = new MyCompletion(); + ReadableContentChannel content = new ReadableContentChannel(); + content.write(ByteBuffer.allocate(69), foo); + content.write(ByteBuffer.allocate(69), bar); + RuntimeException e = new RuntimeException(); + content.failed(e); + assertSame(e, foo.failed); + assertSame(e, bar.failed); + } + + @Test + public void requireThatAvailableIsNotBlocking() { + ReadableContentChannel content = new ReadableContentChannel(); + assertEquals(0, content.available()); + ByteBuffer buf = ByteBuffer.wrap(new byte[] { 6, 9 }); + content.write(buf, null); + assertTrue(content.available() > 0); + assertSame(buf, content.read()); + assertEquals(0, content.available()); + content.close(null); + assertNull(content.read()); + assertEquals(0, content.available()); + } + + @Test + public void requireThatContentIsThreadSafe() { + ExecutorService executor = Executors.newFixedThreadPool(100); + for (int run = 0; run < 69; ++run) { + List<ByteBuffer> bufs = new LinkedList<>(); + for (int buf = 0; buf < 100; ++buf) { + bufs.add(ByteBuffer.allocate(buf)); + } + ReadableContentChannel content = new ReadableContentChannel(); + for (ByteBuffer buf : bufs) { + executor.execute(new WriteTask(content, buf)); + } + for (int buf = 0; buf < 100; ++buf) { + assertTrue(bufs.remove(content.read())); + } + content.close(null); + assertNull(content.read()); + } + } + + private static class MyCompletion implements CompletionHandler { + + boolean completed = false; + Throwable failed = null; + + @Override + public void completed() { + completed = true; + } + + @Override + public void failed(Throwable t) { + failed = t; + } + } + + private static class ReadTask implements Callable<ByteBuffer> { + + final ReadableContentChannel content; + + ReadTask(ReadableContentChannel content) { + this.content = content; + } + + @Override + public ByteBuffer call() throws Exception { + return content.read(); + } + } + + private static class WriteTask implements Runnable { + + final ContentChannel content; + final ByteBuffer buf; + + WriteTask(ContentChannel content, ByteBuffer buf) { + this.content = content; + this.buf = buf; + } + + @Override + public void run() { + content.write(buf, null); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDeniedTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDeniedTestCase.java new file mode 100644 index 00000000000..3cfe794dfed --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDeniedTestCase.java @@ -0,0 +1,70 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertSame; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RequestDeniedTestCase { + + @Test + public void requireThatAccessorsWork() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + Request request = new Request(driver, URI.create("http://host/path")); + RequestDeniedException e = new RequestDeniedException(request); + assertSame(request, e.request()); + request.release(); + driver.close(); + } + + @Test + public void requireThatRequestDeniedIsThrown() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + RequestHandler requestHandler = new MyRequestHandler(); + builder.serverBindings().bind("http://host/path", requestHandler); + driver.activateContainer(builder); + Request request = new Request(driver, URI.create("http://host/path")); + try { + request.connect(new MyResponseHandler()); + fail(); + } catch (RequestDeniedException e) { + assertSame(request, e.request()); + } + request.release(); + driver.close(); + } + + private static class MyRequestHandler extends NoopSharedResource implements RequestHandler { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + throw new RequestDeniedException(request); + } + + @Override + public void handleTimeout(Request request, ResponseHandler handler) { + + } + } + + private static class MyResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(Response response) { + return null; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDispatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDispatchTestCase.java new file mode 100644 index 00000000000..f13473a1660 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDispatchTestCase.java @@ -0,0 +1,253 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.MoreExecutors; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class RequestDispatchTestCase { + + @Test + public void requireThatRequestCanBeDispatched() throws Exception { + final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + final List<ByteBuffer> writtenContent = Arrays.asList(ByteBuffer.allocate(6), ByteBuffer.allocate(9)); + ReadableContentChannel receivedContent = new ReadableContentChannel(); + ContainerBuilder builder = driver.newContainerBuilder(); + Response response = new Response(Response.Status.OK); + builder.serverBindings().bind("http://localhost/", new MyRequestHandler(receivedContent, response)); + driver.activateContainer(builder); + RequestDispatch dispatch = new RequestDispatch() { + + @Override + protected Request newRequest() { + return new Request(driver, URI.create("http://localhost/")); + } + + @Override + protected Iterable<ByteBuffer> requestContent() { + return writtenContent; + } + }; + dispatch.dispatch(); + assertFalse(dispatch.isDone()); + assertSame(writtenContent.get(0), receivedContent.read()); + assertFalse(dispatch.isDone()); + assertSame(writtenContent.get(1), receivedContent.read()); + assertFalse(dispatch.isDone()); + assertNull(receivedContent.read()); + assertTrue(dispatch.isDone()); + assertSame(response, dispatch.get(600, TimeUnit.SECONDS)); + assertTrue(driver.close()); + } + + @Test + public void requireThatStreamCanBeConnected() throws IOException { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + ReadableContentChannel content = new ReadableContentChannel(); + MyRequestHandler requestHandler = new MyRequestHandler(content, new Response(Response.Status.OK)); + builder.serverBindings().bind("http://localhost/", requestHandler); + driver.activateContainer(builder); + + OutputStream out = new FastContentOutputStream(driver.newRequestDispatch("http://localhost/", new FutureResponse()).connect()); + out.write(6); + out.write(9); + out.close(); + + InputStream in = content.toStream(); + assertEquals(6, in.read()); + assertEquals(9, in.read()); + assertEquals(-1, in.read()); + + assertTrue(driver.close()); + } + + @Test + public void requireThatCancelIsUnsupported() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + RequestDispatch dispatch = driver.newRequestDispatch("http://localhost/", new FutureResponse()); + assertFalse(dispatch.isCancelled()); + try { + dispatch.cancel(true); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertFalse(dispatch.isCancelled()); + try { + dispatch.cancel(false); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertFalse(dispatch.isCancelled()); + assertTrue(driver.close()); + } + + @Test + public void requireThatDispatchHandlesConnectException() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://localhost/", new AbstractRequestHandler() { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + throw new RuntimeException(); + } + }); + driver.activateContainer(builder); + try { + driver.newRequestDispatch("http://localhost/", new FutureResponse()).dispatch(); + fail(); + } catch (RuntimeException e) { + + } + assertTrue(driver.close()); + } + + @Test + public void requireThatDispatchHandlesWriteException() { + final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + Response response = new Response(Response.Status.OK); + builder.serverBindings().bind("http://localhost/", new MyRequestHandler(new ContentChannel() { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + throw new RuntimeException(); + } + + @Override + public void close(CompletionHandler handler) { + handler.completed(); + } + }, response)); + driver.activateContainer(builder); + try { + new RequestDispatch() { + + @Override + protected Request newRequest() { + return new Request(driver, URI.create("http://localhost/")); + } + + @Override + protected Iterable<ByteBuffer> requestContent() { + return Arrays.asList(ByteBuffer.allocate(69)); + } + }.dispatch(); + fail(); + } catch (RuntimeException e) { + + } + assertTrue(driver.close()); + } + + @Test + public void requireThatDispatchHandlesCloseException() { + final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + Response response = new Response(Response.Status.OK); + builder.serverBindings().bind("http://localhost/", new MyRequestHandler(new ContentChannel() { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(CompletionHandler handler) { + throw new RuntimeException(); + } + }, response)); + driver.activateContainer(builder); + try { + new RequestDispatch() { + + @Override + protected Request newRequest() { + return new Request(driver, URI.create("http://localhost/")); + } + + @Override + protected Iterable<ByteBuffer> requestContent() { + return Arrays.asList(ByteBuffer.allocate(69)); + } + }.dispatch(); + fail(); + } catch (RuntimeException e) { + + } + assertTrue(driver.close()); + } + + @Test + public void requireThatDispatchCanBeListenedTo() throws InterruptedException { + final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + ReadableContentChannel requestContent = new ReadableContentChannel(); + MyRequestHandler requestHandler = new MyRequestHandler(requestContent, null); + builder.serverBindings().bind("http://localhost/", requestHandler); + driver.activateContainer(builder); + RunnableLatch listener = new RunnableLatch(); + new RequestDispatch() { + + @Override + protected Request newRequest() { + return new Request(driver, URI.create("http://localhost/")); + } + }.dispatch().addListener(listener, MoreExecutors.sameThreadExecutor()); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + ContentChannel responseContent = ResponseDispatch.newInstance(Response.Status.OK) + .connect(requestHandler.responseHandler); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + assertNull(requestContent.read()); + assertTrue(listener.await(600, TimeUnit.SECONDS)); + responseContent.close(null); + assertTrue(driver.close()); + } + + private static class MyRequestHandler extends AbstractRequestHandler { + + final ContentChannel content; + final Response response; + ResponseHandler responseHandler; + + MyRequestHandler(ContentChannel content, Response response) { + this.content = content; + this.response = response; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + if (response != null) { + ResponseDispatch.newInstance(response).dispatch(handler); + } else { + responseHandler = handler; + } + return content; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ResponseDispatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ResponseDispatchTestCase.java new file mode 100644 index 00000000000..92fc0f90c07 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ResponseDispatchTestCase.java @@ -0,0 +1,206 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.MoreExecutors; +import com.yahoo.jdisc.Response; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ResponseDispatchTestCase { + + @Test + public void requireThatFactoryMethodsWork() throws Exception { + { + FutureResponse handler = new FutureResponse(); + ResponseDispatch.newInstance(69).dispatch(handler); + Response response = handler.get(600, TimeUnit.SECONDS); + assertNotNull(response); + assertEquals(69, response.getStatus()); + } + { + FutureResponse handler = new FutureResponse(); + Response sentResponse = new Response(69); + ResponseDispatch.newInstance(sentResponse).dispatch(handler); + Response receivedResponse = handler.get(600, TimeUnit.SECONDS); + assertSame(sentResponse, receivedResponse); + } + { + ReadableContentChannel content = new ReadableContentChannel(); + FutureResponse handler = new FutureResponse(content); + ByteBuffer buf = ByteBuffer.allocate(69); + ResponseDispatch.newInstance(69, Arrays.asList(buf)).dispatch(handler); + Response response = handler.get(600, TimeUnit.SECONDS); + assertNotNull(response); + assertEquals(69, response.getStatus()); + assertSame(buf, content.read()); + assertNull(content.read()); + } + { + ReadableContentChannel content = new ReadableContentChannel(); + FutureResponse handler = new FutureResponse(content); + ByteBuffer buf = ByteBuffer.allocate(69); + ResponseDispatch.newInstance(69, Arrays.asList(buf)).dispatch(handler); + Response response = handler.get(600, TimeUnit.SECONDS); + assertNotNull(response); + assertEquals(69, response.getStatus()); + assertSame(buf, content.read()); + assertNull(content.read()); + } + { + ReadableContentChannel content = new ReadableContentChannel(); + FutureResponse handler = new FutureResponse(content); + ByteBuffer buf = ByteBuffer.allocate(69); + Response sentResponse = new Response(69); + ResponseDispatch.newInstance(sentResponse, Arrays.asList(buf)).dispatch(handler); + Response receivedResponse = handler.get(600, TimeUnit.SECONDS); + assertSame(sentResponse, receivedResponse); + assertSame(buf, content.read()); + assertNull(content.read()); + } + } + + @Test + public void requireThatResponseCanBeDispatched() throws Exception { + final Response response = new Response(Response.Status.OK); + final List<ByteBuffer> writtenContent = Arrays.asList(ByteBuffer.allocate(6), ByteBuffer.allocate(9)); + ResponseDispatch dispatch = new ResponseDispatch() { + + @Override + protected Response newResponse() { + return response; + } + + @Override + protected Iterable<ByteBuffer> responseContent() { + return writtenContent; + } + }; + ReadableContentChannel receivedContent = new ReadableContentChannel(); + MyResponseHandler responseHandler = new MyResponseHandler(receivedContent); + dispatch.dispatch(responseHandler); + assertFalse(dispatch.isDone()); + assertSame(response, responseHandler.response); + assertSame(writtenContent.get(0), receivedContent.read()); + assertFalse(dispatch.isDone()); + assertSame(writtenContent.get(1), receivedContent.read()); + assertFalse(dispatch.isDone()); + assertNull(receivedContent.read()); + assertTrue(dispatch.isDone()); + assertTrue(dispatch.get(600, TimeUnit.SECONDS)); + assertTrue(dispatch.get()); + } + + @Test + public void requireThatStreamCanBeConnected() throws IOException { + ReadableContentChannel responseContent = new ReadableContentChannel(); + OutputStream out = new FastContentOutputStream(new ResponseDispatch() { + + @Override + protected Response newResponse() { + return new Response(Response.Status.OK); + } + }.connect(new MyResponseHandler(responseContent))); + out.write(6); + out.write(9); + out.close(); + + InputStream in = responseContent.toStream(); + assertEquals(6, in.read()); + assertEquals(9, in.read()); + assertEquals(-1, in.read()); + } + + @Test + public void requireThatCancelIsUnsupported() { + ResponseDispatch dispatch = ResponseDispatch.newInstance(69); + assertFalse(dispatch.isCancelled()); + try { + dispatch.cancel(true); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertFalse(dispatch.isCancelled()); + try { + dispatch.cancel(false); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertFalse(dispatch.isCancelled()); + } + + @Test + public void requireThatDispatchClosesContentIfWriteThrowsException() { + final AtomicBoolean closed = new AtomicBoolean(false); + try { + ResponseDispatch.newInstance(6, ByteBuffer.allocate(9)).dispatch( + new MyResponseHandler(new ContentChannel() { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public void close(CompletionHandler handler) { + closed.set(true); + } + })); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertTrue(closed.get()); + } + + @Test + public void requireThatDispatchCanBeListenedTo() throws InterruptedException { + RunnableLatch listener = new RunnableLatch(); + ReadableContentChannel responseContent = new ReadableContentChannel(); + ResponseDispatch.newInstance(6, ByteBuffer.allocate(9)) + .dispatch(new MyResponseHandler(responseContent)) + .addListener(listener, MoreExecutors.sameThreadExecutor()); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + assertNotNull(responseContent.read()); + assertFalse(listener.await(100, TimeUnit.MILLISECONDS)); + assertNull(responseContent.read()); + assertTrue(listener.await(600, TimeUnit.SECONDS)); + } + + private static class MyResponseHandler implements ResponseHandler { + + final ContentChannel content; + Response response; + + MyResponseHandler(ContentChannel content) { + this.content = content; + } + + @Override + public ContentChannel handleResponse(Response response) { + this.response = response; + return content; + } + } + +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RunnableLatch.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RunnableLatch.java new file mode 100644 index 00000000000..e81d7eb16ef --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RunnableLatch.java @@ -0,0 +1,22 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class RunnableLatch implements Runnable { + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void run() { + latch.countDown(); + } + + public boolean await(long timeout, TimeUnit unit) throws InterruptedException { + return latch.await(timeout, unit); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ThreadedRequestHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ThreadedRequestHandlerTestCase.java new file mode 100644 index 00000000000..545be4e03ce --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ThreadedRequestHandlerTestCase.java @@ -0,0 +1,228 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import com.google.common.util.concurrent.ListenableFuture; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ThreadedRequestHandlerTestCase { + + @Test + public void requireThatNullExecutorThrowsException() { + try { + new ThreadedRequestHandler(null) { + + @Override + public void handleRequest(Request request, BufferedContentChannel content, ResponseHandler handler) { + + } + }; + fail(); + } catch (NullPointerException e) { + + } + } + + @Test + public void requireThatAccessorWork() { + MyRequestHandler requestHandler = new MyRequestHandler(newExecutor()); + requestHandler.setTimeout(1000, TimeUnit.MILLISECONDS); + assertEquals(1000, requestHandler.getTimeout(TimeUnit.MILLISECONDS)); + assertEquals(1, requestHandler.getTimeout(TimeUnit.SECONDS)); + } + + @Test + public void requireThatHandlerSetsRequestTimeout() throws InterruptedException { + MyRequestHandler requestHandler = new MyRequestHandler(newExecutor()); + requestHandler.setTimeout(600, TimeUnit.SECONDS); + TestDriver driver = newTestDriver("http://localhost/", requestHandler); + + MyResponseHandler responseHandler = new MyResponseHandler(); + driver.dispatchRequest("http://localhost/", responseHandler); + + requestHandler.entryLatch.countDown(); + assertTrue(requestHandler.exitLatch.await(600, TimeUnit.SECONDS)); + assertNull(requestHandler.content.read()); + assertNotNull(requestHandler.request.getTimeout(TimeUnit.MILLISECONDS)); + + assertTrue(responseHandler.latch.await(600, TimeUnit.SECONDS)); + assertNull(responseHandler.content.read()); + assertTrue(driver.close()); + } + + @Test + public void requireThatRequestAndResponseReachHandlers() throws InterruptedException { + MyRequestHandler requestHandler = new MyRequestHandler(newExecutor()); + TestDriver driver = newTestDriver("http://localhost/", requestHandler); + + MyResponseHandler responseHandler = new MyResponseHandler(); + Request request = new Request(driver, URI.create("http://localhost/")); + ContentChannel requestContent = request.connect(responseHandler); + ByteBuffer buf = ByteBuffer.allocate(69); + requestContent.write(buf, null); + requestContent.close(null); + request.release(); + + requestHandler.entryLatch.countDown(); + assertTrue(requestHandler.exitLatch.await(600, TimeUnit.SECONDS)); + assertSame(request, requestHandler.request); + assertSame(buf, requestHandler.content.read()); + assertNull(requestHandler.content.read()); + + assertTrue(responseHandler.latch.await(600, TimeUnit.SECONDS)); + assertSame(requestHandler.response, responseHandler.response); + assertNull(responseHandler.content.read()); + assertTrue(driver.close()); + } + + @Test + public void requireThatNotImplementedHandlerDoesNotPreventShutdown() throws Exception { + TestDriver driver = newTestDriver("http://localhost/", new ThreadedRequestHandler(newExecutor()) { + + }); + assertEquals(Response.Status.NOT_IMPLEMENTED, + dispatchRequest(driver, "http://localhost/", ByteBuffer.wrap(new byte[] { 69 })) + .get(600, TimeUnit.SECONDS).getStatus()); + assertTrue(driver.close()); + } + + @Test + public void requireThatThreadedRequestHandlerRetainsTheRequestUntilHandlerIsRun() throws Exception { + final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + final AtomicInteger baseRetainCount = new AtomicInteger(); + builder.serverBindings().bind("http://localhost/base", new AbstractRequestHandler() { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + baseRetainCount.set(request.retainCount()); + handler.handleResponse(new Response(Response.Status.OK)).close(null); + return null; + } + }); + final CountDownLatch entryLatch = new CountDownLatch(1); + final CountDownLatch exitLatch = new CountDownLatch(1); + final AtomicInteger testRetainCount = new AtomicInteger(); + builder.serverBindings().bind("http://localhost/test", new ThreadedRequestHandler(newExecutor()) { + + @Override + public void handleRequest(Request request, ReadableContentChannel requestContent, + ResponseHandler responseHandler) { + try { + entryLatch.await(600, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return; + } + testRetainCount.set(request.retainCount()); + responseHandler.handleResponse(new Response(Response.Status.OK)).close(null); + requestContent.read(); // drain content to call completion handlers + exitLatch.countDown(); + } + }); + driver.activateContainer(builder); + dispatchRequest(driver, "http://localhost/base"); + dispatchRequest(driver, "http://localhost/test"); + entryLatch.countDown(); + exitLatch.await(600, TimeUnit.SECONDS); + assertEquals(baseRetainCount.get(), testRetainCount.get()); + assertTrue(driver.close()); + } + + private static TestDriver newTestDriver(String uri, RequestHandler requestHandler) { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind(uri, requestHandler); + driver.activateContainer(builder); + return driver; + } + + private static ListenableFuture<Response> dispatchRequest(final CurrentContainer container, final String uri, + final ByteBuffer... content) { + return new RequestDispatch() { + + @Override + protected Request newRequest() { + return new Request(container, URI.create(uri)); + } + + @Override + protected Iterable<ByteBuffer> requestContent() { + return Arrays.asList(content); + } + }.dispatch(); + } + + private static Executor newExecutor() { + return Executors.newSingleThreadExecutor(); + } + + private static class MyRequestHandler extends ThreadedRequestHandler { + + final CountDownLatch entryLatch = new CountDownLatch(1); + final CountDownLatch exitLatch = new CountDownLatch(1); + final ReadableContentChannel content = new ReadableContentChannel(); + Response response = null; + Request request = null; + + MyRequestHandler(Executor executor) { + super(executor); + } + + @Override + public void handleRequest(Request request, BufferedContentChannel content, ResponseHandler handler) { + try { + if (!entryLatch.await(600, TimeUnit.SECONDS)) { + return; + } + this.request = request; + content.connectTo(this.content); + response = new Response(Response.Status.OK); + handler.handleResponse(response).close(null); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + exitLatch.countDown(); + } + } + } + + private static class MyResponseHandler implements ResponseHandler { + + final CountDownLatch latch = new CountDownLatch(1); + final ReadableContentChannel content = new ReadableContentChannel(); + Response response = null; + + @Override + public ContentChannel handleResponse(Response response) { + this.response = response; + latch.countDown(); + + BufferedContentChannel content = new BufferedContentChannel(); + content.connectTo(this.content); + return content; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/UnsafeContentInputStreamTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/UnsafeContentInputStreamTestCase.java new file mode 100644 index 00000000000..9aac2c4ea7f --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/UnsafeContentInputStreamTestCase.java @@ -0,0 +1,139 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.handler; + +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.util.concurrent.Future; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class UnsafeContentInputStreamTestCase { + + @Test + public void requireThatBytesCanBeRead() throws IOException { + BufferedContentChannel channel = new BufferedContentChannel(); + FastContentWriter writer = new FastContentWriter(channel); + writer.write("Hello "); + writer.write("World!"); + writer.close(); + + BufferedReader reader = asBufferedReader(channel); + assertEquals("Hello World!", reader.readLine()); + assertNull(reader.readLine()); + } + + @SuppressWarnings("deprecation") + @Test + public void requireThatCompletionsAreCalledWithDeprecatedContentWriter() throws IOException { + BufferedContentChannel channel = new BufferedContentChannel(); + FastContentWriter writer = new FastContentWriter(channel); + writer.write("foo"); + writer.close(); + + InputStream stream = asInputStream(channel); + assertEquals('f', stream.read()); + assertEquals('o', stream.read()); + assertEquals('o', stream.read()); + assertEquals(-1, stream.read()); + assertTrue(writer.isDone()); + } + + @Test + public void requireThatCompletionsAreCalled() throws IOException { + BufferedContentChannel channel = new BufferedContentChannel(); + FastContentWriter writer = new FastContentWriter(channel); + writer.write("foo"); + writer.close(); + + InputStream stream = asInputStream(channel); + assertEquals('f', stream.read()); + assertEquals('o', stream.read()); + assertEquals('o', stream.read()); + assertEquals(-1, stream.read()); + assertTrue(writer.isDone()); + } + + @SuppressWarnings("deprecation") + @Test + public void requireThatCloseDrainsStreamWithDeprecatedContentWriter() { + BufferedContentChannel channel = new BufferedContentChannel(); + FastContentWriter writer = new FastContentWriter(channel); + writer.write("foo"); + writer.close(); + + asInputStream(channel).close(); + assertTrue(writer.isDone()); + } + + @Test + public void requireThatCloseDrainsStream() { + BufferedContentChannel channel = new BufferedContentChannel(); + FastContentWriter writer = new FastContentWriter(channel); + writer.write("foo"); + writer.close(); + + asInputStream(channel).close(); + assertTrue(writer.isDone()); + } + + @Test + public void requireThatAvailableIsNotBlocking() throws IOException { + BufferedContentChannel channel = new BufferedContentChannel(); + InputStream stream = asInputStream(channel); + assertEquals(0, stream.available()); + channel.write(ByteBuffer.wrap(new byte[] { 6, 9 }), null); + assertTrue(stream.available() > 0); + assertEquals(6, stream.read()); + assertTrue(stream.available() > 0); + assertEquals(9, stream.read()); + assertEquals(0, stream.available()); + channel.close(null); + assertEquals(-1, stream.read()); + assertEquals(0, stream.available()); + } + + @Test + public void requireThatReadLargeArrayIsNotBlocking() throws IOException { + BufferedContentChannel channel = new BufferedContentChannel(); + InputStream stream = asInputStream(channel); + assertEquals(0, stream.available()); + channel.write(ByteBuffer.wrap(new byte[] { 6, 9 }), null); + assertTrue(stream.available() > 0); + byte[] buf = new byte[69]; + assertEquals(2, stream.read(buf)); + assertEquals(6, buf[0]); + assertEquals(9, buf[1]); + assertEquals(0, stream.available()); + channel.close(null); + assertEquals(-1, stream.read(buf)); + assertEquals(0, stream.available()); + } + + @Test + public void requireThatAllByteValuesCanBeRead() throws IOException { + ReadableContentChannel content = new ReadableContentChannel(); + InputStream in = new UnsafeContentInputStream(content); + for (int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE; ++i) { + content.write(ByteBuffer.wrap(new byte[] { (byte)i }), null); + assertEquals(i, (byte)in.read()); + } + } + + private static BufferedReader asBufferedReader(BufferedContentChannel channel) { + return new BufferedReader(new InputStreamReader(asInputStream(channel))); + } + + private static UnsafeContentInputStream asInputStream(BufferedContentChannel channel) { + return new UnsafeContentInputStream(channel.toReadable()); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractClientProviderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractClientProviderTestCase.java new file mode 100644 index 00000000000..93e96c29e6f --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractClientProviderTestCase.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class AbstractClientProviderTestCase { + + @Test + public void requireThatAbstractClassIsAClientProvider() { + assertTrue(ClientProvider.class.isInstance(new MyClientProvider())); + } + + @Test + public void requireThatStartDoesNotThrowAnException() { + new MyClientProvider().start(); + } + + private static class MyClientProvider extends AbstractClientProvider { + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + return null; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractServerProviderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractServerProviderTestCase.java new file mode 100644 index 00000000000..c6230e928b7 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractServerProviderTestCase.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.google.inject.Inject; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class AbstractServerProviderTestCase { + + @Test + public void requireThatAbstractClassIsAServerProvider() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + assertTrue(ServerProvider.class.isInstance(new MyServerProvider(driver))); + assertTrue(driver.close()); + } + + @Test + public void requireThatAccessorsWork() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + MyServerProvider server = builder.getInstance(MyServerProvider.class); + assertNotNull(server.container()); + assertTrue(driver.close()); + } + + private static class MyServerProvider extends AbstractServerProvider { + + @Inject + public MyServerProvider(CurrentContainer container) { + super(container); + } + + @Override + public void start() { + + } + + @Override + public void close() { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/BindingSetNotFoundTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/BindingSetNotFoundTestCase.java new file mode 100644 index 00000000000..6d3ed636972 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/BindingSetNotFoundTestCase.java @@ -0,0 +1,58 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.google.inject.AbstractModule; +import com.yahoo.jdisc.application.BindingSetSelector; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BindingSetNotFoundTestCase { + + @Test + public void requireThatAccessorsWork() { + BindingSetNotFoundException e = new BindingSetNotFoundException("foo"); + assertEquals("foo", e.bindingSet()); + } + + @Test + public void requireThatBindingSetNotFoundIsThrown() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() { + + @Override + protected void configure() { + bind(BindingSetSelector.class).toInstance(new MySelector("foo")); + } + }); + driver.activateContainer(driver.newContainerBuilder()); + try { + driver.newReference(URI.create("http://host")); + fail(); + } catch (BindingSetNotFoundException e) { + assertEquals("foo", e.bindingSet()); + } + driver.close(); + } + + private static class MySelector implements BindingSetSelector { + + final String setName; + + MySelector(String setName) { + this.setName = setName; + } + + @Override + public String select(URI uri) { + return setName; + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/ConnectToHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/ConnectToHandlerTestCase.java new file mode 100644 index 00000000000..a516e42c11a --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/ConnectToHandlerTestCase.java @@ -0,0 +1,99 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ConnectToHandlerTestCase { + + @Test + public void requireThatNullResponseHandlerThrowsException() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + Request request = new Request(driver, URI.create("http://host/path")); + try { + request.connect(null); + fail(); + } catch (NullPointerException e) { + // expected + } + request.release(); + driver.close(); + } + + @Test + public void requireThatConnectToHandlerWorks() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = new MyRequestHandler(new MyContent()); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("http://host/*", requestHandler); + driver.activateContainer(builder); + Request request = new Request(driver, URI.create("http://host/path")); + MyResponseHandler responseHandler = new MyResponseHandler(); + ContentChannel content = request.connect(responseHandler); + request.release(); + assertNotNull(content); + content.close(null); + assertNotNull(requestHandler.handler); + assertSame(request, requestHandler.request); + requestHandler.handler.handleResponse(new Response(Response.Status.OK)).close(null); + driver.close(); + } + + private class MyRequestHandler extends AbstractRequestHandler { + + final ContentChannel content; + Request request; + ResponseHandler handler; + + MyRequestHandler(ContentChannel content) { + this.content = content; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + this.request = request; + this.handler = handler; + return content; + } + } + + private class MyResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(Response response) { + return null; + } + } + + private static class MyContent implements ContentChannel { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(CompletionHandler handler) { + handler.completed(); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/ContainerNotReadyTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/ContainerNotReadyTestCase.java new file mode 100644 index 00000000000..423e6e4cc91 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/ContainerNotReadyTestCase.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.jdisc.service; + +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerNotReadyTestCase { + + @Test + public void requireThatExceptionIsThrown() throws BindingSetNotFoundException { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + try { + driver.newReference(URI.create("http://host")); + fail(); + } catch (ContainerNotReadyException e) { + + } + driver.close(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/CurrentContainerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/CurrentContainerTestCase.java new file mode 100644 index 00000000000..8a5a6aeb913 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/CurrentContainerTestCase.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertNotNull; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class CurrentContainerTestCase { + + @Test + public void requireThatNewRequestsCreateSnapshot() throws Exception { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + Request request = new Request(driver, URI.create("http://host/path")); + assertNotNull(request.container()); + request.release(); + driver.close(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/NoBindingSetSelectedTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/NoBindingSetSelectedTestCase.java new file mode 100644 index 00000000000..56ea80c2001 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/NoBindingSetSelectedTestCase.java @@ -0,0 +1,55 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.service; + +import com.google.inject.AbstractModule; +import com.yahoo.jdisc.application.BindingSetSelector; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NoBindingSetSelectedTestCase { + + @Test + public void requireThatAccessorsWork() { + URI uri = URI.create("http://host/path"); + NoBindingSetSelectedException e = new NoBindingSetSelectedException(uri); + assertEquals(uri, e.uri()); + } + + @Test + public void requireThatNoBindingSetSelectedIsThrown() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() { + + @Override + protected void configure() { + bind(BindingSetSelector.class).toInstance(new MySelector()); + } + }); + driver.activateContainer(driver.newContainerBuilder()); + URI uri = URI.create("http://host"); + try { + driver.newReference(uri); + fail(); + } catch (NoBindingSetSelectedException e) { + assertEquals(uri, e.uri()); + } + driver.close(); + } + + private static class MySelector implements BindingSetSelector { + + @Override + public String select(URI uri) { + return null; + } + } + +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingClientTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingClientTestCase.java new file mode 100644 index 00000000000..ff70ee35049 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingClientTestCase.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.service.ClientProvider; +import org.junit.Test; + +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingClientTestCase { + + @Test + public void requireThatHandleRequestThrowsException() { + ClientProvider client = new NonWorkingClientProvider(); + try { + client.handleRequest(null, null); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatHandleTimeoutThrowsException() { + ClientProvider client = new NonWorkingClientProvider(); + try { + client.handleTimeout(null, null); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatStartDoesNotThrow() { + ClientProvider client = new NonWorkingClientProvider(); + client.start(); + } + + @Test + public void requireThatRetainDoesNotThrow() { + ClientProvider client = new NonWorkingClientProvider(); + client.release(); + } + + @Test + public void requireThatReleaseDoesNotThrow() { + ClientProvider client = new NonWorkingClientProvider(); + client.release(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingCompletionHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingCompletionHandlerTestCase.java new file mode 100644 index 00000000000..9ee5f5dd265 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingCompletionHandlerTestCase.java @@ -0,0 +1,43 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.handler.CompletionHandler; +import org.junit.Test; + +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingCompletionHandlerTestCase { + + @Test + public void requireThatCompletedThrowsException() { + CompletionHandler completion = new NonWorkingCompletionHandler(); + try { + completion.completed(); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatFailedThrowsException() { + CompletionHandler completion = new NonWorkingCompletionHandler(); + try { + completion.failed(null); + fail(); + } catch (UnsupportedOperationException e) { + + } + completion = new NonWorkingCompletionHandler(); + try { + completion.failed(new Throwable()); + fail(); + } catch (UnsupportedOperationException e) { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingContentChannelTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingContentChannelTestCase.java new file mode 100644 index 00000000000..73adca81bf2 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingContentChannelTestCase.java @@ -0,0 +1,80 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import org.junit.Test; + +import java.nio.ByteBuffer; + +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingContentChannelTestCase { + + @Test + public void requireThatWriteThrowsException() { + ContentChannel content = new NonWorkingContentChannel(); + try { + content.write(null, null); + fail(); + } catch (UnsupportedOperationException e) { + + } + content = new NonWorkingContentChannel(); + try { + content.write(ByteBuffer.allocate(69), null); + fail(); + } catch (UnsupportedOperationException e) { + + } + content = new NonWorkingContentChannel(); + try { + content.write(ByteBuffer.allocate(69), new MyCompletion()); + fail(); + } catch (UnsupportedOperationException e) { + + } + content = new NonWorkingContentChannel(); + try { + content.write(null, new MyCompletion()); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatCloseThrowsException() { + ContentChannel content = new NonWorkingContentChannel(); + try { + content.close(null); + fail(); + } catch (UnsupportedOperationException e) { + + } + content = new NonWorkingContentChannel(); + try { + content.close(new MyCompletion()); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + private static class MyCompletion implements CompletionHandler { + + @Override + public void completed() { + + } + + @Override + public void failed(Throwable t) { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingOsgiFrameworkTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingOsgiFrameworkTestCase.java new file mode 100644 index 00000000000..ad1e34aba66 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingOsgiFrameworkTestCase.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.jdisc.test; + +import com.yahoo.jdisc.application.OsgiFramework; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; + +import java.util.Collections; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingOsgiFrameworkTestCase { + + @Test + public void requireThatFrameworkCanStartAndStop() throws BundleException { + OsgiFramework osgi = new NonWorkingOsgiFramework(); + osgi.start(); + osgi.stop(); + } + + @Test + public void requireThatFrameworkHasNoBundles() throws BundleException { + OsgiFramework osgi = new NonWorkingOsgiFramework(); + assertTrue(osgi.bundles().isEmpty()); + } + + @Test + public void requireThatFrameworkHasNoBundleContext() { + OsgiFramework osgi = new NonWorkingOsgiFramework(); + assertNull(osgi.bundleContext()); + } + + @Test + public void requireThatFrameworkThrowsOnInstallBundle() throws BundleException { + OsgiFramework osgi = new NonWorkingOsgiFramework(); + try { + osgi.installBundle("file:bundle.jar"); + fail(); + } catch (UnsupportedOperationException e) { + // expected + } + } + + @Test + public void requireThatFrameworkThrowsOnStartBundles() throws BundleException { + OsgiFramework osgi = new NonWorkingOsgiFramework(); + try { + osgi.startBundles(Collections.<Bundle>emptyList(), false); + fail(); + } catch (UnsupportedOperationException e) { + // expected + } + } + + @Test + public void requireThatFrameworkThrowsOnRefreshPackages() throws BundleException, InterruptedException { + OsgiFramework osgi = new NonWorkingOsgiFramework(); + try { + osgi.refreshPackages(); + fail(); + } catch (UnsupportedOperationException e) { + // expected + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestHandlerTestCase.java new file mode 100644 index 00000000000..7893a2d1cf7 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestHandlerTestCase.java @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.handler.RequestHandler; +import org.junit.Test; + +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingRequestHandlerTestCase { + + @Test + public void requireThatHandleRequestThrowsException() { + RequestHandler requestHandler = new NonWorkingRequestHandler(); + try { + requestHandler.handleRequest(null, null); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatHandleTimeoutThrowsException() { + RequestHandler requestHandler = new NonWorkingRequestHandler(); + try { + requestHandler.handleTimeout(null, null); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + @Test + public void requireThatDestroyDoesNotThrow() { + RequestHandler requestHandler = new NonWorkingRequestHandler(); + requestHandler.release(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestTestCase.java new file mode 100644 index 00000000000..0d1b33fa72f --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestTestCase.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.jdisc.test; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.name.Names; +import com.yahoo.jdisc.Request; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingRequestTestCase { + + @Test + public void requireThatFactoryMethodWorks() { + assertNotNull(NonWorkingRequest.newInstance("scheme://host/path")); + } + + @Test + public void requireThatGuiceModulesAreInjected() { + Request request = NonWorkingRequest.newInstance("scheme://host/path", new AbstractModule() { + + @Override + protected void configure() { + bind(String.class).annotatedWith(Names.named("foo")).toInstance("bar"); + } + }); + assertEquals("bar", request.container().getInstance(Key.get(String.class, Names.named("foo")))); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingResponseHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingResponseHandlerTestCase.java new file mode 100644 index 00000000000..fb76e66e1e1 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingResponseHandlerTestCase.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.Response; +import org.junit.Test; + +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingResponseHandlerTestCase { + + @Test + public void requireThatHandleResponseThrowsException() { + NonWorkingResponseHandler handler = new NonWorkingResponseHandler(); + try { + handler.handleResponse(new Response(Response.Status.OK)); + fail(); + } catch (UnsupportedOperationException e) { + + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingServerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingServerTestCase.java new file mode 100644 index 00000000000..eccf4dbc655 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingServerTestCase.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.jdisc.test; + +import com.yahoo.jdisc.service.ServerProvider; +import org.junit.Test; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class NonWorkingServerTestCase { + + @Test + public void requireThatStartDoesNotThrow() { + ServerProvider server = new NonWorkingServerProvider(); + server.start(); + } + + @Test + public void requireThatCloseDoesNotThrow() { + ServerProvider server = new NonWorkingServerProvider(); + server.close(); + } + + @Test + public void requireThatReferDoesNotThrow() { + ServerProvider server = new NonWorkingServerProvider(); + server.refer(); + } + + @Test + public void requireThatReleaseDoesNotThrow() { + ServerProvider server = new NonWorkingServerProvider(); + server.release(); + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/ServerProviderConformanceTestTest.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/ServerProviderConformanceTestTest.java new file mode 100644 index 00000000000..e59f8c96100 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/ServerProviderConformanceTestTest.java @@ -0,0 +1,657 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.google.common.util.concurrent.SettableFuture; +import com.google.inject.Inject; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.service.ServerProvider; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ServerProviderConformanceTestTest extends ServerProviderConformanceTest { + + @Override + @Test + public void testContainerNotReadyException() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testBindingSetNotFoundException() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testNoBindingSetSelectedException() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testBindingNotFoundException() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestHandlerWithSyncCloseResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestHandlerWithSyncWriteResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestHandlerWithSyncHandleResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestHandlerWithAsyncHandleResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestException() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestExceptionWithSyncCloseResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestExceptionWithSyncWriteResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithSyncHandleResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithSyncHandleResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithAsyncHandleResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicSyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureBeforeResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureAfterResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicAsyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureBeforeResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicException() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContent() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicSyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicAsyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicSyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureBeforeResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureAfterResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicAsyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureBeforeResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureAfterResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicException() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWrite() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContent() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testResponseWriteCompletionException() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testResponseCloseCompletionException() throws Throwable { + runTest(new MyAdapter()); + } + + @Override + @Test + public void testResponseCloseCompletionExceptionNoContent() throws Throwable { + runTest(new MyAdapter()); + } + + private static void tryWrite(final ContentChannel out, final String str) { + try { + out.write(StandardCharsets.UTF_8.encode(str), null); + } catch (Throwable t) { + // Simulate handling the failure. + t.getMessage(); + } + } + + private static void tryClose(final ContentChannel out) { + try { + out.close(null); + } catch (Throwable t) { + // Simulate handling the failure. + t.getMessage(); + } + } + + private static void tryComplete(final CompletionHandler handler) { + try { + handler.completed(); + } catch (Throwable t) { + // Simulate handling the failure. + t.getMessage(); + } + } + + private static class MyServer extends NoopSharedResource implements ServerProvider { + + final CurrentContainer container; + + @Inject + MyServer(final CurrentContainer container) { + this.container = container; + } + + @Override + public void start() { + + } + + @Override + public void close() { + + } + } + + private static class MyClient { + + final MyServer server; + + MyClient(final MyServer server) { + this.server = server; + } + + MyResponseHandler executeRequest(final boolean withRequestContent) + throws InterruptedException, ExecutionException, TimeoutException { + final MyResponseHandler responseHandler = new MyResponseHandler(); + final Request request; + try { + request = new Request(server.container, URI.create("http://localhost/")); + } catch (Throwable t) { + responseHandler.response.set(new Response(Response.Status.INTERNAL_SERVER_ERROR, t)); + return responseHandler; + } + try { + final ContentChannel out = request.connect(responseHandler); + if (withRequestContent) { + tryWrite(out, "myRequestContent"); + } + tryClose(out); + } catch (Throwable t) { + responseHandler.response.set(new Response(Response.Status.INTERNAL_SERVER_ERROR, t)); + // Simulate handling the failure. + t.getMessage(); + return responseHandler; + } finally { + request.release(); + } + return responseHandler; + } + } + + private static class MyResponseHandler implements ResponseHandler { + + final SettableFuture<Response> response = SettableFuture.create(); + final SettableFuture<String> content = SettableFuture.create(); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + @Override + public ContentChannel handleResponse(final Response response) { + this.response.set(response); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + while (buf.hasRemaining()) { + out.write(buf.get()); + } + tryComplete(handler); + } + + @Override + public void close(final CompletionHandler handler) { + content.set(new String(out.toByteArray(), StandardCharsets.UTF_8)); + tryComplete(handler); + } + }; + } + } + + private static class MyAdapter implements Adapter<MyServer, MyClient, MyResponseHandler> { + + @Override + public Module newConfigModule() { + return Modules.EMPTY_MODULE; + } + + @Override + public Class<MyServer> getServerProviderClass() { + return MyServer.class; + } + + @Override + public MyClient newClient(final MyServer server) throws Throwable { + return new MyClient(server); + } + + @Override + public MyResponseHandler executeRequest( + final MyClient client, + final boolean withRequestContent) throws Throwable { + return client.executeRequest(withRequestContent); + } + + @Override + public Iterable<ByteBuffer> newResponseContent() { + return Collections.singleton(StandardCharsets.UTF_8.encode("myResponseContent")); + } + + @Override + public void validateResponse(final MyResponseHandler responseHandler) throws Throwable { + responseHandler.response.get(600, TimeUnit.SECONDS); + } + } +} diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/TestDriverTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/TestDriverTestCase.java new file mode 100644 index 00000000000..bea231d19d9 --- /dev/null +++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/TestDriverTestCase.java @@ -0,0 +1,163 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.Application; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestDeniedException; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.service.ContainerNotReadyException; +import org.junit.Test; + +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TestDriverTestCase { + + @Test + public void requireThatFactoryMethodsWork() { + TestDriver.newInjectedApplicationInstance(MyApplication.class).close(); + TestDriver.newInjectedApplicationInstanceWithoutOsgi(MyApplication.class).close(); + TestDriver.newInjectedApplicationInstance(new MyApplication()).close(); + TestDriver.newInjectedApplicationInstanceWithoutOsgi(new MyApplication()).close(); + TestDriver.newSimpleApplicationInstance().close(); + TestDriver.newSimpleApplicationInstanceWithoutOsgi().close(); + } + + @Test + public void requireThatAccessorsWork() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + assertNotNull(driver.bootstrapLoader()); + assertTrue(driver.close()); + } + + @Test + public void requireThatConnectRequestWorks() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = new MyRequestHandler(new MyContentChannel()); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("scheme://host/path", requestHandler); + driver.activateContainer(builder); + ContentChannel content = driver.connectRequest("scheme://host/path", new MyResponseHandler()); + assertNotNull(content); + content.close(null); + assertNotNull(requestHandler.handler); + requestHandler.handler.handleResponse(new Response(Response.Status.OK)).close(null); + assertTrue(driver.close()); + } + + @Test + public void requireThatDispatchRequestWorks() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + MyRequestHandler requestHandler = new MyRequestHandler(new MyContentChannel()); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("scheme://host/path", requestHandler); + driver.activateContainer(builder); + driver.dispatchRequest("scheme://host/path", new MyResponseHandler()); + assertNotNull(requestHandler.handler); + assertTrue(requestHandler.content.closed); + requestHandler.handler.handleResponse(new Response(Response.Status.OK)).close(null); + assertTrue(driver.close()); + } + + @Test + public void requireThatFailedRequestCreateDoesNotBlockClose() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + try { + driver.connectRequest("scheme://host/path", new MyResponseHandler()); + fail(); + } catch (ContainerNotReadyException e) { + + } + assertTrue(driver.close()); + } + + @Test + public void requireThatFailedRequestConnectDoesNotBlockClose() { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("scheme://host/path", new MyRequestHandler(null)); + driver.activateContainer(builder); + try { + driver.connectRequest("scheme://host/path", new MyResponseHandler()); + fail(); + } catch (RequestDeniedException e) { + + } + assertTrue(driver.close()); + } + + private static class MyApplication implements Application { + + @Override + public void start() { + + } + + @Override + public void stop() { + + } + + @Override + public void destroy() { + + } + } + + private static class MyRequestHandler extends AbstractRequestHandler { + + final MyContentChannel content; + ResponseHandler handler; + + MyRequestHandler(MyContentChannel content) { + this.content = content; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + this.handler = handler; + if (content == null) { + throw new RequestDeniedException(request); + } + return content; + } + } + + private static class MyResponseHandler implements ResponseHandler { + + final MyContentChannel content = new MyContentChannel(); + + @Override + public ContentChannel handleResponse(Response response) { + return content; + } + } + + private static class MyContentChannel implements ContentChannel { + + boolean closed = false; + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public void close(CompletionHandler handler) { + closed = true; + handler.completed(); + } + } +} diff --git a/jdisc_core/src/test/perl/help.Levent.expected b/jdisc_core/src/test/perl/help.Levent.expected new file mode 100644 index 00000000000..b35e2d9c36b --- /dev/null +++ b/jdisc_core/src/test/perl/help.Levent.expected @@ -0,0 +1,20 @@ +Usage: jdisc_logfmt [options] [inputfile ...] +Options: + -l LEVELLIST --level=LEVELLIST select levels to include + -L LEVELLIST --add-level=LEVELLIST define extra levels + -s FIELDLIST --show=FIELDLIST select fields to print + -p PID --pid=PID select messages from given PID + -S SERVICE --service=SERVICE select messages from given SERVICE + -H HOST --host=HOST select messages from given HOST + -c REGEX --component=REGEX select components matching REGEX + -m REGEX --message=REGEX select message text matching REGEX + -f --follow invoke tail -F to follow input file + -N --nldequote dequote newlines in message text field + -t --tc --truncatecomponent chop component to 15 chars + --ts --truncateservice chop service to 9 chars + +FIELDLIST is comma separated, available fields: + time fmttime msecs usecs host level pid service component message +Available levels for LEVELLIST: + debug error event info unknown warning +for both lists, use 'all' for all possible values, and -xxx to disable xxx. diff --git a/jdisc_core/src/test/perl/help.expected b/jdisc_core/src/test/perl/help.expected new file mode 100644 index 00000000000..58da8183696 --- /dev/null +++ b/jdisc_core/src/test/perl/help.expected @@ -0,0 +1,20 @@ +Usage: jdisc_logfmt [options] [inputfile ...] +Options: + -l LEVELLIST --level=LEVELLIST select levels to include + -L LEVELLIST --add-level=LEVELLIST define extra levels + -s FIELDLIST --show=FIELDLIST select fields to print + -p PID --pid=PID select messages from given PID + -S SERVICE --service=SERVICE select messages from given SERVICE + -H HOST --host=HOST select messages from given HOST + -c REGEX --component=REGEX select components matching REGEX + -m REGEX --message=REGEX select message text matching REGEX + -f --follow invoke tail -F to follow input file + -N --nldequote dequote newlines in message text field + -t --tc --truncatecomponent chop component to 15 chars + --ts --truncateservice chop service to 9 chars + +FIELDLIST is comma separated, available fields: + time fmttime msecs usecs host level pid service component message +Available levels for LEVELLIST: + debug error info unknown warning +for both lists, use 'all' for all possible values, and -xxx to disable xxx. diff --git a/jdisc_core/src/test/perl/jdisc.expected b/jdisc_core/src/test/perl/jdisc.expected new file mode 100644 index 00000000000..6c5d139c0cf --- /dev/null +++ b/jdisc_core/src/test/perl/jdisc.expected @@ -0,0 +1,17 @@ +[2013-01-25 12:46:51.180] INFO : - org.apache.felix.framework ServiceEvent REGISTERED +[2013-01-25 12:46:51.192] INFO : - jdisc_core.app-a BundleEvent INSTALLED +[2013-01-25 12:46:51.249] INFO : - jdisc_core.app-a BundleEvent RESOLVED +[2013-01-25 12:46:51.249] INFO : - jdisc_core.app-a BundleEvent STARTED +[2013-01-25 12:46:51.334] INFO : - jdisc_core.app-a BundleEvent STOPPED +[2013-01-25 12:46:51.335] INFO : - jdisc_core.app-a BundleEvent UNRESOLVED +[2013-01-25 12:46:51.335] INFO : - jdisc_core.app-a BundleEvent UNINSTALLED +[2013-01-25 12:46:51.376] INFO : - org.apache.felix.framework ServiceEvent REGISTERED +[2013-01-25 12:46:51.377] ERROR : - jdisc_core.app-a my_error +[2013-01-25 12:46:51.377] WARNING : - jdisc_core.app-a my_warning +[2013-01-25 12:46:51.377] INFO : - jdisc_core.app-a my_info +[2013-01-25 12:46:51.379] INFO : - jdisc_core.app-a BundleEvent INSTALLED +[2013-01-25 12:46:51.383] INFO : - jdisc_core.app-a BundleEvent RESOLVED +[2013-01-25 12:46:51.383] INFO : - jdisc_core.app-a BundleEvent STARTED +[2013-01-25 12:46:51.389] INFO : - jdisc_core.app-a BundleEvent STOPPED +[2013-01-25 12:46:51.389] INFO : - jdisc_core.app-a BundleEvent UNRESOLVED +[2013-01-25 12:46:51.390] INFO : - jdisc_core.app-a BundleEvent UNINSTALLED diff --git a/jdisc_core/src/test/perl/jdisc.lall.expected b/jdisc_core/src/test/perl/jdisc.lall.expected new file mode 100644 index 00000000000..58875bba8db --- /dev/null +++ b/jdisc_core/src/test/perl/jdisc.lall.expected @@ -0,0 +1,19 @@ +[2013-01-25 12:46:51.180] INFO : - org.apache.felix.framework ServiceEvent REGISTERED +[2013-01-25 12:46:51.192] INFO : - jdisc_core.app-a BundleEvent INSTALLED +[2013-01-25 12:46:51.249] INFO : - jdisc_core.app-a BundleEvent RESOLVED +[2013-01-25 12:46:51.249] INFO : - jdisc_core.app-a BundleEvent STARTED +[2013-01-25 12:46:51.334] INFO : - jdisc_core.app-a BundleEvent STOPPED +[2013-01-25 12:46:51.335] INFO : - jdisc_core.app-a BundleEvent UNRESOLVED +[2013-01-25 12:46:51.335] INFO : - jdisc_core.app-a BundleEvent UNINSTALLED +[2013-01-25 12:46:51.376] INFO : - org.apache.felix.framework ServiceEvent REGISTERED +[2013-01-25 12:46:51.377] ERROR : - jdisc_core.app-a my_error +[2013-01-25 12:46:51.377] WARNING : - jdisc_core.app-a my_warning +[2013-01-25 12:46:51.377] INFO : - jdisc_core.app-a my_info +[2013-01-25 12:46:51.377] DEBUG : - jdisc_core.app-a my_debug +[2013-01-25 12:46:51.377] UNKNOWN : - jdisc_core.app-a my_unknown +[2013-01-25 12:46:51.379] INFO : - jdisc_core.app-a BundleEvent INSTALLED +[2013-01-25 12:46:51.383] INFO : - jdisc_core.app-a BundleEvent RESOLVED +[2013-01-25 12:46:51.383] INFO : - jdisc_core.app-a BundleEvent STARTED +[2013-01-25 12:46:51.389] INFO : - jdisc_core.app-a BundleEvent STOPPED +[2013-01-25 12:46:51.389] INFO : - jdisc_core.app-a BundleEvent UNRESOLVED +[2013-01-25 12:46:51.390] INFO : - jdisc_core.app-a BundleEvent UNINSTALLED diff --git a/jdisc_core/src/test/perl/jdisc.lall_info.expected b/jdisc_core/src/test/perl/jdisc.lall_info.expected new file mode 100644 index 00000000000..da834dddc9b --- /dev/null +++ b/jdisc_core/src/test/perl/jdisc.lall_info.expected @@ -0,0 +1,4 @@ +[2013-01-25 12:46:51.377] ERROR : - jdisc_core.app-a my_error +[2013-01-25 12:46:51.377] WARNING : - jdisc_core.app-a my_warning +[2013-01-25 12:46:51.377] DEBUG : - jdisc_core.app-a my_debug +[2013-01-25 12:46:51.377] UNKNOWN : - jdisc_core.app-a my_unknown diff --git a/jdisc_core/src/test/perl/jdisc.log b/jdisc_core/src/test/perl/jdisc.log new file mode 100644 index 00000000000..37a74a595ca --- /dev/null +++ b/jdisc_core/src/test/perl/jdisc.log @@ -0,0 +1,19 @@ +1359114411.180 gentleadd-lm 35172 - org.apache.felix.framework info ServiceEvent REGISTERED +1359114411.192 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent INSTALLED +1359114411.249 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent RESOLVED +1359114411.249 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent STARTED +1359114411.334 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent STOPPED +1359114411.335 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent UNRESOLVED +1359114411.335 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent UNINSTALLED +1359114411.376 gentleadd-lm 35172 - org.apache.felix.framework info ServiceEvent REGISTERED +1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a error my_error +1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a warning my_warning +1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a info my_info +1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a debug my_debug +1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a unknown my_unknown +1359114411.379 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent INSTALLED +1359114411.383 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent RESOLVED +1359114411.383 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent STARTED +1359114411.389 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent STOPPED +1359114411.389 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent UNRESOLVED +1359114411.390 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent UNINSTALLED diff --git a/jdisc_core/src/test/perl/jdisc.spid.expected b/jdisc_core/src/test/perl/jdisc.spid.expected new file mode 100644 index 00000000000..44963161dcf --- /dev/null +++ b/jdisc_core/src/test/perl/jdisc.spid.expected @@ -0,0 +1,17 @@ +[2013-01-25 12:46:51.180] INFO : 35172 - org.apache.felix.framework ServiceEvent REGISTERED +[2013-01-25 12:46:51.192] INFO : 35172 - jdisc_core.app-a BundleEvent INSTALLED +[2013-01-25 12:46:51.249] INFO : 35172 - jdisc_core.app-a BundleEvent RESOLVED +[2013-01-25 12:46:51.249] INFO : 35172 - jdisc_core.app-a BundleEvent STARTED +[2013-01-25 12:46:51.334] INFO : 35172 - jdisc_core.app-a BundleEvent STOPPED +[2013-01-25 12:46:51.335] INFO : 35172 - jdisc_core.app-a BundleEvent UNRESOLVED +[2013-01-25 12:46:51.335] INFO : 35172 - jdisc_core.app-a BundleEvent UNINSTALLED +[2013-01-25 12:46:51.376] INFO : 35172 - org.apache.felix.framework ServiceEvent REGISTERED +[2013-01-25 12:46:51.377] ERROR : 35172 - jdisc_core.app-a my_error +[2013-01-25 12:46:51.377] WARNING : 35172 - jdisc_core.app-a my_warning +[2013-01-25 12:46:51.377] INFO : 35172 - jdisc_core.app-a my_info +[2013-01-25 12:46:51.379] INFO : 35172 - jdisc_core.app-a BundleEvent INSTALLED +[2013-01-25 12:46:51.383] INFO : 35172 - jdisc_core.app-a BundleEvent RESOLVED +[2013-01-25 12:46:51.383] INFO : 35172 - jdisc_core.app-a BundleEvent STARTED +[2013-01-25 12:46:51.389] INFO : 35172 - jdisc_core.app-a BundleEvent STOPPED +[2013-01-25 12:46:51.389] INFO : 35172 - jdisc_core.app-a BundleEvent UNRESOLVED +[2013-01-25 12:46:51.390] INFO : 35172 - jdisc_core.app-a BundleEvent UNINSTALLED diff --git a/jdisc_core/src/test/perl/jdisc_logfmt_test.sh b/jdisc_core/src/test/perl/jdisc_logfmt_test.sh new file mode 100755 index 00000000000..bb7e92ed8cf --- /dev/null +++ b/jdisc_core/src/test/perl/jdisc_logfmt_test.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +MYPATH=`dirname ${0}` +DIFF=/usr/bin/diff +LOGFMT=${1} + +if [ -e "/usr/local/bin/perl" ]; then + echo "Running jdisc_logfmt test suite." +else + echo "Ignoring jdisc_logfmt test suite as there is no /usr/local/bin/perl" + exit 0 +fi + +set -e +export TZ=CET +export VESPA_HOME=$(mktemp -d /tmp/mockup-vespahome-XXXXXX)/ +mkdir -p $VESPA_HOME/libexec/vespa +touch $VESPA_HOME/libexec/vespa/common-env.sh + +echo + +${LOGFMT} -h 2>&1 | ${DIFF} - ${MYPATH}/help.expected +${LOGFMT} -h -L event 2>&1 | ${DIFF} - ${MYPATH}/help.Levent.expected + +${LOGFMT} ${MYPATH}/jdisc.log 2>&1 | ${DIFF} - ${MYPATH}/jdisc.expected +${LOGFMT} -l all ${MYPATH}/jdisc.log 2>&1 | ${DIFF} - ${MYPATH}/jdisc.lall.expected +${LOGFMT} -l all,-info ${MYPATH}/jdisc.log 2>&1 | ${DIFF} - ${MYPATH}/jdisc.lall_info.expected +${LOGFMT} -s +pid ${MYPATH}/jdisc.log 2>&1 | ${DIFF} - ${MYPATH}/jdisc.spid.expected + +${LOGFMT} ${MYPATH}/vespa.log 2>&1 | ${DIFF} - ${MYPATH}/vespa.expected +${LOGFMT} -L event ${MYPATH}/vespa.log 2>&1 | ${DIFF} - ${MYPATH}/vespa.Levent.expected +${LOGFMT} -L event -l all ${MYPATH}/vespa.log 2>&1 | ${DIFF} - ${MYPATH}/vespa.Levent.lall.expected + +rm -r ${VESPA_HOME} +echo All tests passed. diff --git a/jdisc_core/src/test/perl/vespa.Levent.expected b/jdisc_core/src/test/perl/vespa.Levent.expected new file mode 100644 index 00000000000..334ba5f5b28 --- /dev/null +++ b/jdisc_core/src/test/perl/vespa.Levent.expected @@ -0,0 +1,9 @@ +[2012-11-27 14:22:48.120] INFO : configserver stdout ROOT = /home/vespa +[2012-11-27 14:22:48.232] INFO : configserver stdout Running without a pid file. +[2012-11-27 14:22:48.336] INFO : configserver stdout LANG = en_US.UTF-8 +[2012-11-27 14:22:48.393] INFO : configserver stdout env LD_PRELOAD=/home/vespa/libexec64/yjava_daemon_preload.so: /home/vespa/bin64/yjava_daemon -sentinel -rlimit_files 16384 -logdest /home/vespa/logs/vespa/yjava_daemon.out -loglevel error -jvm server -maxrestarts 0 -procs 1 -home /home/vespa/share/yjava_jdk/java -Djava.library.path=/home/vespa/lib64:/home/vespa/lib64 -Dvespa.log.control.dir=/home/vespa/var/db/vespa/logcontrol -XX:ThreadStackSize=512 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=128 -XX:MaxPermSize=512m -Dconfig.id=dir:/home/vespa/conf/configserver -Dyjava_remote_ip_servlet_filter.logLevel=DEBUG -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger -Dcom.yahoo.protect.Process.forcedExitActive=true -Dzookeeperlogfile=/home/vespa/logs/vespa/zookeeper.log -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=267m -Djava.awt.headless=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/vespa/var/crash -Dsun.net.client.defaultConnectTimeout=5000 -Dsun.net.client.defaultReadTimeout=60000 -Djavax.net.ssl.keyStoreType=JKS -javaagent:/home/vespa/lib/jars/yjava_ysecure_agent.jar -Djdisc_core.config.file=/home/vespa/var/jdisc_core/config.properties -Djdisc.export.packages=yjava.security.ysecure,yjava.security.yca,yjava.security.yck,org.apache.velocity,org.apache.velocity.app,org.apache.velocity.context,org.apache.velocity.runtime,org.apache.velocity.runtime.log, -Djdisc.cache.path=/home/vespa/var/vespa/bundlecache/dir:_home_y_conf_configserver -Djdisc.debug.resources=false -Djdisc.bundle.path=/home/vespa/lib/jars -Djdisc.logger.enabled=false -Djdisc.logger.level=ALL -Djdisc.logger.tag=dir:/home/vespa/conf/configserver -user yahoo -cp /home/vespa/lib/jars/jdisc_core-with-dependencies.jar:lib/jars/yjava_bcookie.jar:lib/jars/yjava_bcookie_jni.jar:lib/jars/yjava_byauth.jar:lib/jars/yjava_cookie_data_servlet_filter.jar:lib/jars/yjava_daemon.jar:lib/jars/yjava_jmx_singleton_server.jar:lib/jars/yjava_remote_ip_servlet_filter.jar:lib/jars/yjava_resource_handler.jar:lib/jars/yjava_servlet.jar:lib/jars/yjava_servlet_filters.jar:lib/jars/yjava_yca.jar:lib/jars/yjava_yck.jar:lib/jars/yjava_yhdrs:lib/jars/yjava_yiv.jar:lib/jars/yjava_yiv_servlet.jar:lib/jars/yjava_ynet.jar:lib/jars/yjava_ysecure.jar:lib/jars/yjava_ysecure_agent.jar:lib/jars/yjava_ysecure_native.jar:share/jports/org_apache_velocity__velocity.jar:share/jports/commons_collections__commons_collections.jar:share/jports/commons_lang__commons_lang.jar: -ynet FILTER_YAHOO_ANY --wait-for-jvm-init 10 -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 -XX:OnOutOfMemoryError="kill -9 %p" com.yahoo.jdisc.core.BootstrapDaemon file:/home/vespa/lib/jars/container-disc-with-dependencies.jar +[2012-11-27 14:22:50.876] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application +[2012-11-27 14:22:50.884] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/config-models/sherpa-config-model-plugin.jar' +[2012-11-27 14:22:50.889] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/configserver-container-plugin.jar' +[2012-11-27 14:22:54.285] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application +[2012-11-27 14:22:57.489] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Switched to the latest deployed set of handlers, and dependent components, e.g. search chains, searchers and document processors. Application switch number: 0 diff --git a/jdisc_core/src/test/perl/vespa.Levent.lall.expected b/jdisc_core/src/test/perl/vespa.Levent.lall.expected new file mode 100644 index 00000000000..ef58eee7bfc --- /dev/null +++ b/jdisc_core/src/test/perl/vespa.Levent.lall.expected @@ -0,0 +1,19 @@ +[2012-11-27 14:22:48.091] EVENT : configserver runserver starting/1 name="/home/vespa/bin/vespa-start-container-daemon -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 (pid 18151)" +[2012-11-27 14:22:48.100] DEBUG : configserver qrs-start exporting: YELL_MA_EURO=INXIGHT +[2012-11-27 14:22:48.100] DEBUG : configserver qrs-start Not setting ulimit -v, no limit set. +[2012-11-27 14:22:48.120] INFO : configserver stdout ROOT = /home/vespa +[2012-11-27 14:22:48.232] INFO : configserver stdout Running without a pid file. +[2012-11-27 14:22:48.336] INFO : configserver stdout LANG = en_US.UTF-8 +[2012-11-27 14:22:48.393] INFO : configserver stdout env LD_PRELOAD=/home/vespa/libexec64/yjava_daemon_preload.so: /home/vespa/bin64/yjava_daemon -sentinel -rlimit_files 16384 -logdest /home/vespa/logs/vespa/yjava_daemon.out -loglevel error -jvm server -maxrestarts 0 -procs 1 -home /home/vespa/share/yjava_jdk/java -Djava.library.path=/home/vespa/lib64:/home/vespa/lib64 -Dvespa.log.control.dir=/home/vespa/var/db/vespa/logcontrol -XX:ThreadStackSize=512 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=128 -XX:MaxPermSize=512m -Dconfig.id=dir:/home/vespa/conf/configserver -Dyjava_remote_ip_servlet_filter.logLevel=DEBUG -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger -Dcom.yahoo.protect.Process.forcedExitActive=true -Dzookeeperlogfile=/home/vespa/logs/vespa/zookeeper.log -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=267m -Djava.awt.headless=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/vespa/var/crash -Dsun.net.client.defaultConnectTimeout=5000 -Dsun.net.client.defaultReadTimeout=60000 -Djavax.net.ssl.keyStoreType=JKS -javaagent:/home/vespa/lib/jars/yjava_ysecure_agent.jar -Djdisc_core.config.file=/home/vespa/var/jdisc_core/config.properties -Djdisc.export.packages=yjava.security.ysecure,yjava.security.yca,yjava.security.yck,org.apache.velocity,org.apache.velocity.app,org.apache.velocity.context,org.apache.velocity.runtime,org.apache.velocity.runtime.log, -Djdisc.cache.path=/home/vespa/var/vespa/bundlecache/dir:_home_y_conf_configserver -Djdisc.debug.resources=false -Djdisc.bundle.path=/home/vespa/lib/jars -Djdisc.logger.enabled=false -Djdisc.logger.level=ALL -Djdisc.logger.tag=dir:/home/vespa/conf/configserver -user yahoo -cp /home/vespa/lib/jars/jdisc_core-with-dependencies.jar:lib/jars/yjava_bcookie.jar:lib/jars/yjava_bcookie_jni.jar:lib/jars/yjava_byauth.jar:lib/jars/yjava_cookie_data_servlet_filter.jar:lib/jars/yjava_daemon.jar:lib/jars/yjava_jmx_singleton_server.jar:lib/jars/yjava_remote_ip_servlet_filter.jar:lib/jars/yjava_resource_handler.jar:lib/jars/yjava_servlet.jar:lib/jars/yjava_servlet_filters.jar:lib/jars/yjava_yca.jar:lib/jars/yjava_yck.jar:lib/jars/yjava_yhdrs:lib/jars/yjava_yiv.jar:lib/jars/yjava_yiv_servlet.jar:lib/jars/yjava_ynet.jar:lib/jars/yjava_ysecure.jar:lib/jars/yjava_ysecure_agent.jar:lib/jars/yjava_ysecure_native.jar:share/jports/org_apache_velocity__velocity.jar:share/jports/commons_collections__commons_collections.jar:share/jports/commons_lang__commons_lang.jar: -ynet FILTER_YAHOO_ANY --wait-for-jvm-init 10 -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 -XX:OnOutOfMemoryError="kill -9 %p" com.yahoo.jdisc.core.BootstrapDaemon file:/home/vespa/lib/jars/container-disc-with-dependencies.jar +[2012-11-27 14:22:50.876] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application +[2012-11-27 14:22:50.884] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/config-models/sherpa-config-model-plugin.jar' +[2012-11-27 14:22:50.889] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/configserver-container-plugin.jar' +[2012-11-27 14:22:54.285] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application +[2012-11-27 14:22:57.430] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requests value=0 +[2012-11-27 14:22:57.431] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requestsCached value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requestsNotCached value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=failedRequests value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTime value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTimeCached value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTimeNotCached value=0 +[2012-11-27 14:22:57.489] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Switched to the latest deployed set of handlers, and dependent components, e.g. search chains, searchers and document processors. Application switch number: 0 diff --git a/jdisc_core/src/test/perl/vespa.expected b/jdisc_core/src/test/perl/vespa.expected new file mode 100644 index 00000000000..897a7084dae --- /dev/null +++ b/jdisc_core/src/test/perl/vespa.expected @@ -0,0 +1,18 @@ +Warning: unknown level 'event' in input +[2012-11-27 14:22:48.091] EVENT : configserver runserver starting/1 name="/home/vespa/bin/vespa-start-container-daemon -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 (pid 18151)" +[2012-11-27 14:22:48.120] INFO : configserver stdout ROOT = /home/vespa +[2012-11-27 14:22:48.232] INFO : configserver stdout Running without a pid file. +[2012-11-27 14:22:48.336] INFO : configserver stdout LANG = en_US.UTF-8 +[2012-11-27 14:22:48.393] INFO : configserver stdout env LD_PRELOAD=/home/vespa/libexec64/yjava_daemon_preload.so: /home/vespa/bin64/yjava_daemon -sentinel -rlimit_files 16384 -logdest /home/vespa/logs/vespa/yjava_daemon.out -loglevel error -jvm server -maxrestarts 0 -procs 1 -home /home/vespa/share/yjava_jdk/java -Djava.library.path=/home/vespa/lib64:/home/vespa/lib64 -Dvespa.log.control.dir=/home/vespa/var/db/vespa/logcontrol -XX:ThreadStackSize=512 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=128 -XX:MaxPermSize=512m -Dconfig.id=dir:/home/vespa/conf/configserver -Dyjava_remote_ip_servlet_filter.logLevel=DEBUG -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger -Dcom.yahoo.protect.Process.forcedExitActive=true -Dzookeeperlogfile=/home/vespa/logs/vespa/zookeeper.log -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=267m -Djava.awt.headless=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/vespa/var/crash -Dsun.net.client.defaultConnectTimeout=5000 -Dsun.net.client.defaultReadTimeout=60000 -Djavax.net.ssl.keyStoreType=JKS -javaagent:/home/vespa/lib/jars/yjava_ysecure_agent.jar -Djdisc_core.config.file=/home/vespa/var/jdisc_core/config.properties -Djdisc.export.packages=yjava.security.ysecure,yjava.security.yca,yjava.security.yck,org.apache.velocity,org.apache.velocity.app,org.apache.velocity.context,org.apache.velocity.runtime,org.apache.velocity.runtime.log, -Djdisc.cache.path=/home/vespa/var/vespa/bundlecache/dir:_home_y_conf_configserver -Djdisc.debug.resources=false -Djdisc.bundle.path=/home/vespa/lib/jars -Djdisc.logger.enabled=false -Djdisc.logger.level=ALL -Djdisc.logger.tag=dir:/home/vespa/conf/configserver -user yahoo -cp /home/vespa/lib/jars/jdisc_core-with-dependencies.jar:lib/jars/yjava_bcookie.jar:lib/jars/yjava_bcookie_jni.jar:lib/jars/yjava_byauth.jar:lib/jars/yjava_cookie_data_servlet_filter.jar:lib/jars/yjava_daemon.jar:lib/jars/yjava_jmx_singleton_server.jar:lib/jars/yjava_remote_ip_servlet_filter.jar:lib/jars/yjava_resource_handler.jar:lib/jars/yjava_servlet.jar:lib/jars/yjava_servlet_filters.jar:lib/jars/yjava_yca.jar:lib/jars/yjava_yck.jar:lib/jars/yjava_yhdrs:lib/jars/yjava_yiv.jar:lib/jars/yjava_yiv_servlet.jar:lib/jars/yjava_ynet.jar:lib/jars/yjava_ysecure.jar:lib/jars/yjava_ysecure_agent.jar:lib/jars/yjava_ysecure_native.jar:share/jports/org_apache_velocity__velocity.jar:share/jports/commons_collections__commons_collections.jar:share/jports/commons_lang__commons_lang.jar: -ynet FILTER_YAHOO_ANY --wait-for-jvm-init 10 -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 -XX:OnOutOfMemoryError="kill -9 %p" com.yahoo.jdisc.core.BootstrapDaemon file:/home/vespa/lib/jars/container-disc-with-dependencies.jar +[2012-11-27 14:22:50.876] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application +[2012-11-27 14:22:50.884] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/config-models/sherpa-config-model-plugin.jar' +[2012-11-27 14:22:50.889] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/configserver-container-plugin.jar' +[2012-11-27 14:22:54.285] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application +[2012-11-27 14:22:57.430] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requests value=0 +[2012-11-27 14:22:57.431] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requestsCached value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requestsNotCached value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=failedRequests value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTime value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTimeCached value=0 +[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTimeNotCached value=0 +[2012-11-27 14:22:57.489] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Switched to the latest deployed set of handlers, and dependent components, e.g. search chains, searchers and document processors. Application switch number: 0 diff --git a/jdisc_core/src/test/perl/vespa.log b/jdisc_core/src/test/perl/vespa.log new file mode 100644 index 00000000000..36210bdb798 --- /dev/null +++ b/jdisc_core/src/test/perl/vespa.log @@ -0,0 +1,19 @@ +1354022568.091108 example.yahoo.com 18150/38735 configserver runserver event starting/1 name="/home/vespa/bin/vespa-start-container-daemon -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 (pid 18151)" +1354022568.100151 example.yahoo.com 18151 configserver qrs-start debug exporting: YELL_MA_EURO=INXIGHT +1354022568.100217 example.yahoo.com 18151 configserver qrs-start debug Not setting ulimit -v, no limit set. +1354022568.120716 example.yahoo.com 18151 configserver stdout info ROOT = /home/vespa +1354022568.232852 example.yahoo.com 18151 configserver stdout info Running without a pid file. +1354022568.336341 example.yahoo.com 18151 configserver stdout info LANG = en_US.UTF-8 +1354022568.393706 example.yahoo.com 18151 configserver stdout info env LD_PRELOAD=/home/vespa/libexec64/yjava_daemon_preload.so: /home/vespa/bin64/yjava_daemon -sentinel -rlimit_files 16384 -logdest /home/vespa/logs/vespa/yjava_daemon.out -loglevel error -jvm server -maxrestarts 0 -procs 1 -home /home/vespa/share/yjava_jdk/java -Djava.library.path=/home/vespa/lib64:/home/vespa/lib64 -Dvespa.log.control.dir=/home/vespa/var/db/vespa/logcontrol -XX:ThreadStackSize=512 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=128 -XX:MaxPermSize=512m -Dconfig.id=dir:/home/vespa/conf/configserver -Dyjava_remote_ip_servlet_filter.logLevel=DEBUG -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger -Dcom.yahoo.protect.Process.forcedExitActive=true -Dzookeeperlogfile=/home/vespa/logs/vespa/zookeeper.log -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=267m -Djava.awt.headless=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/vespa/var/crash -Dsun.net.client.defaultConnectTimeout=5000 -Dsun.net.client.defaultReadTimeout=60000 -Djavax.net.ssl.keyStoreType=JKS -javaagent:/home/vespa/lib/jars/yjava_ysecure_agent.jar -Djdisc_core.config.file=/home/vespa/var/jdisc_core/config.properties -Djdisc.export.packages=yjava.security.ysecure,yjava.security.yca,yjava.security.yck,org.apache.velocity,org.apache.velocity.app,org.apache.velocity.context,org.apache.velocity.runtime,org.apache.velocity.runtime.log, -Djdisc.cache.path=/home/vespa/var/vespa/bundlecache/dir:_home_y_conf_configserver -Djdisc.debug.resources=false -Djdisc.bundle.path=/home/vespa/lib/jars -Djdisc.logger.enabled=false -Djdisc.logger.level=ALL -Djdisc.logger.tag=dir:/home/vespa/conf/configserver -user yahoo -cp /home/vespa/lib/jars/jdisc_core-with-dependencies.jar:lib/jars/yjava_bcookie.jar:lib/jars/yjava_bcookie_jni.jar:lib/jars/yjava_byauth.jar:lib/jars/yjava_cookie_data_servlet_filter.jar:lib/jars/yjava_daemon.jar:lib/jars/yjava_jmx_singleton_server.jar:lib/jars/yjava_remote_ip_servlet_filter.jar:lib/jars/yjava_resource_handler.jar:lib/jars/yjava_servlet.jar:lib/jars/yjava_servlet_filters.jar:lib/jars/yjava_yca.jar:lib/jars/yjava_yck.jar:lib/jars/yjava_yhdrs:lib/jars/yjava_yiv.jar:lib/jars/yjava_yiv_servlet.jar:lib/jars/yjava_ynet.jar:lib/jars/yjava_ysecure.jar:lib/jars/yjava_ysecure_agent.jar:lib/jars/yjava_ysecure_native.jar:share/jports/org_apache_velocity__velocity.jar:share/jports/commons_collections__commons_collections.jar:share/jports/commons_lang__commons_lang.jar: -ynet FILTER_YAHOO_ANY --wait-for-jvm-init 10 -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 -XX:OnOutOfMemoryError="kill -9 %p" com.yahoo.jdisc.core.BootstrapDaemon file:/home/vespa/lib/jars/container-disc-with-dependencies.jar +1354022570.876 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi info Installing bundles from the latest application +1354022570.884 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.BundleLoader info Installing bundle from disk with reference 'file:/home/vespa/lib/jars/config-models/sherpa-config-model-plugin.jar' +1354022570.889 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.BundleLoader info Installing bundle from disk with reference 'file:/home/vespa/lib/jars/configserver-container-plugin.jar' +1354022574.285 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi info Installing bundles from the latest application +1354022577.430 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=requests value=0 +1354022577.431 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=requestsCached value=0 +1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=requestsNotCached value=0 +1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=failedRequests value=0 +1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=procTime value=0 +1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=procTimeCached value=0 +1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=procTimeNotCached value=0 +1354022577.489 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi info Switched to the latest deployed set of handlers, and dependent components, e.g. search chains, searchers and document processors. Application switch number: 0 |