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/src/main/java/com |
Publish
Diffstat (limited to 'jdisc_core/src/main/java/com')
112 files changed, 12559 insertions, 0 deletions
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; |