com.yahoo.jdisc.Request Maven / Gradle / Ivy
// Copyright Vespa.ai. 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.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 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.
*
* 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.
*
* 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}.
*
* @author Simon Thoresen Hult
* @see Container
* @see Response
*/
public class Request extends AbstractResource {
private final Object monitor = new Object();
private final Map context = Collections.synchronizedMap(new HashMap<>());
private final HeaderFields headers = new HeaderFields();
private final Container container;
private final Request parent;
private final ResourceReference resourceReference;
private final long creationTime;
private final boolean serverRequest;
private final URI uri;
private boolean cancel = false;
private volatile BindingMatch bindingMatch;
private TimeoutManager timeoutManager;
private Long timeout;
public enum RequestType {
READ, WRITE, MONITORING
}
/**
* 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}.
*
* 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:
*
*
* Request request = null;
* ContentChannel content = null;
* try {
* request = new Request(currentContainer, uri);
* (...)
* content = request.connect(responseHandler);
* } finally {
* if (request != null) request.release();
* }
* content.write(...);
*
*
* @param current The CurrentContainer for which this Request is created.
* @param uri The identifier of this request.
*/
public Request(CurrentContainer current, URI uri) {
this(current, uri, true);
}
public Request(CurrentContainer current, URI uri, boolean isServerRequest) {
this(current, uri, isServerRequest, -1);
}
@SuppressWarnings("this-escape")
public Request(CurrentContainer current, URI uri, boolean isServerRequest, long creationTime) {
this.parent = null;
this.container = current.newReference(uri, this);
this.resourceReference = container::release;
this.uri = uri.normalize();
this.creationTime = creationTime >= 0 ? creationTime : container.currentTimeMillis();
this.serverRequest = isServerRequest;
}
/**
* 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}.
*
* 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:
*
*
* Request request = null;
* ContentChannel content = null;
* try {
* request = new Request(parentRequest, uri);
* (...)
* content = request.connect(responseHandler);
* } finally {
* if (request != null) request.release();
* }
* content.write(...);
*
*
* @param parent The parent Request of this.
* @param uri The identifier of this request.
*/
@SuppressWarnings("this-escape")
public Request(Request parent, URI uri) {
this.parent = parent;
this.container = null;
this.resourceReference = parent.refer(this);
this.uri = uri.normalize();
this.creationTime = parent.container().currentTimeMillis();
this.serverRequest = false;
}
/** Returns the {@link Container} for which this Request was created */
public Container container() {
return parent != null ? parent.container() : container;
}
/**
* Returns the Uniform Resource Identifier used by the {@link Container} to resolve the appropriate {@link
* RequestHandler} for this Request.
*/
public URI getUri() { return uri; }
/**
* Returns whether 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.
*
* @return true, if this is a server request
*/
public boolean isServerRequest() {
return serverRequest;
}
/**
* 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.
*
* @return the last resolved BindingMatch, or null
* @see #setBindingMatch(BindingMatch)
* @see Container#resolveHandler(Request)
*/
public BindingMatch getBindingMatch() {
return bindingMatch;
}
/**
* Sets the last resolved {@link BindingMatch} of this Request. This is called by the {@link
* Container#resolveHandler(Request)} method.
*
* @param bindingMatch The BindingMatch to set.
* @return This, to allow chaining.
* @see #getBindingMatch()
*/
public Request setBindingMatch(BindingMatch bindingMatch) {
this.bindingMatch = bindingMatch;
return this;
}
/**
* 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.
*
* @return The context map.
*/
public Map context() {
return context;
}
/**
* 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.
*
* @return The header fields.
*/
public HeaderFields headers() {
return headers;
}
/**
* 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.
*
* NOTE: This is used by the default timeout management implementation, so unless you are replacing that
* mechanism you should avoid calling this method. If you do want to replace that mechanism, you need to
* call this method prior to calling the target {@link RequestHandler} (since that injects the default manager).
*
* @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");
synchronized (monitor) {
if (this.timeoutManager != null) {
throw new IllegalStateException("Timeout manager already set.");
}
this.timeoutManager = timeoutManager;
if (timeout != null) {
timeoutManager.scheduleTimeout(this);
}
}
}
/**
* Returns the {@link TimeoutManager} of this request, or null if none has been assigned.
*
* @return The TimeoutManager of this Request.
* @see #setTimeoutManager(TimeoutManager)
*/
public TimeoutManager getTimeoutManager() {
synchronized (monitor) { return timeoutManager; }
}
/**
* 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.
*
* Once the allocated time has expired, unless the {@link ResponseHandler} has already been called, the {@link
* RequestHandler#handleTimeout(Request, ResponseHandler)} method is invoked.
*
* Calls to {@link #isCancelled()} return true if timeout has been exceeded.
*
* @param timeout The allocated amount of time.
* @param unit The time unit of the timeout argument.
* @see #getTimeout(TimeUnit)
* @see #timeRemaining(TimeUnit)
*/
public void setTimeout(long timeout, TimeUnit unit) {
synchronized (monitor) {
this.timeout = unit.toMillis(timeout);
if (timeoutManager != null) {
timeoutManager.scheduleTimeout(this);
}
}
}
/**
* Returns the allocated number of time units that this Request is allowed to exist. If no timeout has been set
* for this Request, this method returns null.
*
* @param unit The unit to return the timeout in.
* @return The timeout of this Request.
* @see #setTimeout(long, TimeUnit)
*/
public Long getTimeout(TimeUnit unit) {
synchronized (monitor) {
if (timeout == null) {
return null;
}
return unit.convert(timeout, TimeUnit.MILLISECONDS);
}
}
/**
* Returns the time that this Request is allowed to exist. If no timeout has been set, this method will return
* null.
*
* @param unit The unit to return the time in.
* @return The number of time units left until this Request times out, or null.
*/
public Long timeRemaining(TimeUnit unit) {
synchronized (monitor) {
if (timeout == null) {
return null;
}
return unit.convert(timeout - (container().currentTimeMillis() - creationTime), TimeUnit.MILLISECONDS);
}
}
/**
* Returns the time that this Request has existed so far.
*
* @param unit The unit to return the time in.
* @return The number of time units elapsed since this Request was created.
*/
public long timeElapsed(TimeUnit unit) {
return unit.convert(container().currentTimeMillis() - creationTime, TimeUnit.MILLISECONDS);
}
/**
*
Returns the time at which this Request was created. This is whatever value was returned by {@link
* Timer#currentTimeMillis()} when constructing this.
*
* @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);
}
/**
* 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.
*
* This method will also return true if the Request has a non-null timeout, and that timeout has
* expired.
*
* Finally, this method will also return true if this Request has a parent Request that has been
* cancelled.
*
* @return True if this Request has timed out or been cancelled.
* @see #cancel()
* @see #setTimeout(long, TimeUnit)
*/
public boolean isCancelled() {
synchronized (monitor) {
if (cancel) {
return true;
}
if (timeout != null && timeRemaining(TimeUnit.MILLISECONDS) <= 0) {
return true;
}
}
if (parent != null && parent.isCancelled()) {
return true;
}
return false;
}
/**
* 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 true.
*
* @see #isCancelled()
*/
public void cancel() {
synchronized (monitor) {
if (cancel) return;
if (timeoutManager != null && timeout != null)
timeoutManager.unscheduleTimeout(this);
cancel = true;
}
}
/**
* 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.
*
* @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(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() {
resourceReference.close();
}
}