com.pushtechnology.diffusion.client.features.Messaging Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2023 DiffusionData Ltd., All Rights Reserved.
*
* Use is subject to license terms.
*
* NOTICE: All information contained herein is, and remains the
* property of Push Technology. The intellectual and technical
* concepts contained herein are proprietary to Push Technology and
* may be covered by U.S. and Foreign Patents, patents in process, and
* are protected by trade secret or copyright law.
******************************************************************************/
package com.pushtechnology.diffusion.client.features;
import static org.slf4j.LoggerFactory.getLogger;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import org.slf4j.Logger;
import com.pushtechnology.diffusion.client.callbacks.Registration;
import com.pushtechnology.diffusion.client.callbacks.Stream;
import com.pushtechnology.diffusion.client.session.Feature;
import com.pushtechnology.diffusion.client.session.PermissionsException;
import com.pushtechnology.diffusion.client.session.Session;
import com.pushtechnology.diffusion.client.session.SessionClosedException;
import com.pushtechnology.diffusion.client.session.SessionId;
import com.pushtechnology.diffusion.client.types.GlobalPermission;
import com.pushtechnology.diffusion.client.types.PathPermission;
/**
* This feature provides a client session with request-response messaging
* capabilities that can be used to implement application services.
*
*
* Request-response messaging allows a session to send requests to other
* sessions. Each receiving session provides a corresponding response, which is
* returned to the sending session. Each request and response carries an
* application provided value.
*
*
* The method used to send a request determines which sessions will receive it.
* Each request is routed using the provided message path – an
* application provided string. Two addressing schemes are provided:
* unaddressed requests and addressed requests.
*
*
Unaddressed requests
*
*
* A session can provide an application service by implementing a handler and
* registering it with the server. This is somewhat similar to implementing a
* REST service, except that interactions between the sender and receiver are
* asynchronous.
*
*
* Unaddressed requests sent using
* {@link #sendRequest(String, Object, Class, Class) sendRequest} are routed by
* the server to a handler that has been pre-registered by another session, and
* matches the message path.
*
*
* Handlers are registered with {@link #addRequestHandler}. Each session may
* register at most one handler for a given message path. Optionally, one or
* more session property names can be provided (see {@link Session} for a full
* description of session properties), in which case the values of the session
* properties for each recipient session will be returned along with its
* response. To add a request handler, the control client session must have
* {@link GlobalPermission#REGISTER_HANDLER REGISTER_HANDLER} permission. If
* registering to receive session property values, the session must also have
* {@link GlobalPermission#VIEW_SESSION VIEW_SESSION} permission.
*
*
* Routing works as follows:
*
*
* - The session {@link #sendRequest(String, Object, Class, Class) sends} the
* request, providing the message path, the request value and data type, and the
* expected response type.
*
- The server uses the message path to apply access control. The sender must
* have the {@link PathPermission#SEND_TO_MESSAGE_HANDLER
* SEND_TO_MESSAGE_HANDLER} path permission for the message path, or the request
* will be rejected.
*
- The server uses the message path to select a pre-registered handler and
* route the request to the appropriate recipient session. The server will
* consider all registered handlers and select one registered for the most
* specific path. If multiple sessions have registered a handler registered for
* a path, one will be chosen arbitrarily. If there is no registered handler
* matching the message path, the request will be rejected.
*
- Otherwise, the server forwards the request to one of the sessions
* registered to handle the message path. The message path is also passed to the
* recipient session, providing a hierarchical context.
*
- The recipient session processes the request and returns a response to the
* server, which forwards the response to the sending session.
*
*
*
* Registration works across a cluster of servers. If no matching handler is
* registered on the server to which the sending session is connected, the
* request will be routed to another server in the cluster that has one.
*
*
Addressed requests
*
*
* Addressed requests provide a way to perform actions on a group of sessions,
* or to notify sessions of one-off events (for repeating streams of events, use
* a topic instead).
*
*
* An addressed request can be sent to a set of sessions using
* {@link #sendRequestToFilter sendRequestToFilter}. For the details of session
* filters, see {@link Session}. Sending a request to a filter will match zero
* or more sessions. Each response received will be passed to the provided
* {@link Messaging.FilteredRequestCallback callback}. As a convenience, an
* addressed request can be sent a specific session using the overloaded variant
* of {@link #sendRequest(SessionId, String, Object, Class, Class) sendRequest}
* that accepts a session id.
*
*
* Sending an addressed request requires {@link PathPermission#SEND_TO_SESSION
* SEND_TO_SESSION} permission.
*
*
* If the sending session is connected to a server belonging to a cluster, the
* recipient sessions can be connected to other servers in the cluster. The
* filter will be evaluated against all sessions hosted by the cluster.
*
*
* To receive addressed requests, a session must set up a local request stream
* to handle the specific message path, using
* {@link #setRequestStream(String, Class, Class, Messaging.RequestStream)
* setRequestStream}. When a request is received for the message path, the
* {@link Messaging.RequestStream#onRequest onRequest} method on the stream is
* triggered. The session should respond using the provided
* {@link Messaging.RequestStream.Responder responder}. Streams receive an
* {@link Stream#onClose onClose} callback when unregistered and an
* {@link com.pushtechnology.diffusion.client.callbacks.Callback#onError
* onError} callback if the session is closed.
*
*
* If a request is sent to a session that does not have a matching stream for
* the message path, an error will be returned to the sending session.
*
*
Accessing the feature
*
*
* Obtain this feature from a {@link Session session} as follows:
*
*
*
* Messaging messaging = session.feature(Messaging.class);
*
*
*
* @author DiffusionData Limited
* @since 5.0
*/
public interface Messaging extends Feature {
/**
* Send a request.
*
* A response is returned when the {@link CompletableFuture} has been
* completed.
*
* @param path the path to send a request to
* @param request the request to send
* @param requestType the request type
* @param responseType the response type
*
* @param request object type
* @param response object type
*
* @return a CompletableFuture that completes when a response has been
* received by a handler.If the task completes successfully, the
* CompletableFuture result will be the response (to the request) of
* type R.
*
* Otherwise, the CompletableFuture will complete exceptionally with
* a {@link CompletionException}. Common reasons for failure, listed
* by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link UnhandledMessageException} – if there is no
* handler registered on the server to receive requests for this
* message path;
*
*
- {@link IncompatibleDatatypeException} – if the request
* is not compatible with the datatype bound to the handler's
* message path;
*
*
- {@link IllegalArgumentException} – if the response is
* not compatible with the specified response type;
*
*
- {@link RejectedRequestException} – if the request has
* been rejected by the recipient session calling
* {@code Responder.reject(message)};
*
*
- {@link SessionClosedException} – if the session is
* closed;
*
*
- {@link PermissionsException} – if the session does
* not have {@code SEND_TO_MESSAGE_HANDLER} permission;
*
*
- {@link CancellationException} – if the recipient
* session did not respond before the request timed out.
*
*
* @throws IllegalArgumentException if there is no data type matching the
* request type parameter
*
* @since 6.0
*/
CompletableFuture sendRequest(
String path,
T request,
Class requestType,
Class responseType);
/**
* Set a request stream to handle requests to a specified path.
*
* @param path path to receive requests on
* @param requestType the request type.
* @param responseType the response type.
* @param requestStream request stream to handle requests to this path
*
* @param request type
* @param response type
*
* @return null if the request stream is the first stream to be set to the
* path, otherwise this method will return the previously set
* request stream.
*
* @throws IllegalArgumentException if there is no data type matching the
* request type parameter
* @since 6.0
*/
RequestStream, ?> setRequestStream(
String path,
Class extends T> requestType,
Class super R> responseType,
RequestStream requestStream);
/**
* Remove the request stream at a particular path.
*
* @param path path at which to remove the request stream
*
* @return the request stream that was removed from the path. If the path
* does not have a request stream assigned (or the path does not
* exist), null will be returned instead
*
* @since 6.0
*/
RequestStream, ?> removeRequestStream(String path);
/**
* Interface which specifies a request stream to receive request
* notifications.
*
* @param request type
* @param response type
*
* @since 6.0
*/
interface RequestStream extends Stream {
/**
* Called to indicate a request has been received.
*
* @param path path the request was sent on
* @param request request that was received
* @param responder responder to dispatch a response back to the
* requester
*/
void onRequest(String path, T request, Responder responder);
/**
* Responder interface to dispatch responses to requests.
*
* @param response class type
*/
interface Responder {
/**
* Dispatch a response to a request.
*
* @param response the response
*/
void respond(R response);
/**
* Reject a request.
*
* @param message context message to be contained in the rejection.
*
* @see RejectedRequestException
*/
void reject(String message);
}
}
/**
* Send a request to a session.
*
* @param sessionId session to send the request to
* @param path message path used by the recipient to select an appropriate
* handler
* @param request request to send
* @param requestType the request type
* @param responseType the response type
*
* @param request type
* @param response type
*
* @return A CompletableFuture that completes when a response has been
* received by the session. If the task completes successfully, the
* CompletableFuture result will be a response (to the request) of
* type R.
*
* Otherwise, the CompletableFuture will complete exceptionally with
* a {@link CompletionException}. Common reasons for failure, listed
* by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link NoSuchSessionException} – if the session does
* not exist on the server;
*
*
- {@link UnhandledMessageException} – if recipient
* session does not have a local request stream registered for this
* path;
*
*
- {@link IncompatibleDatatypeException} – if the request
* is not compatible with the datatype bound to the handler's
* message path;
*
*
- {@link IllegalArgumentException} – if the response is
* not compatible with the specified response type;
*
*
- {@link RejectedRequestException} – if the request has
* been rejected by the recipient session calling
* {@code Responder.reject(message)};
*
*
- {@link SessionClosedException} – if the session is
* closed;
*
*
- {@link PermissionsException} – if the session does
* not have {@code SEND_TO_SESSION} permission;
*
*
* @since 6.0
*/
CompletableFuture sendRequest(
SessionId sessionId,
String path,
T request,
Class requestType,
Class responseType);
/**
* Register a request handler to handle requests from other client sessions
* for a branch of the message path hierarchy.
*
*
* Each control session may register a single handler for a branch. When the
* handler is no longer required, it may be closed using the
* {@link Registration} provided by the CompletableFuture result. To change
* the handler for a particular branch the previous handler must first be
* closed.
*
* @param path the request message path
* @param requestType the request type. If this class is not supported by
* the available datatypes, an {@link IllegalArgumentException} will
* be thrown.
* @param responseType the response type. If this class is not supported by
* the available datatypes, an {@link IllegalArgumentException} will
* be thrown.
* @param handler request handler to be registered at the server
* @param sessionProperties a list of keys of session properties that should
* be supplied with each request. See {@link Session} for a full list
* of available fixed property keys. To request no properties supply
* an empty list. To request all fixed properties include
* {@link Session#ALL_FIXED_PROPERTIES} as a key. In this case any
* other fixed property keys would be ignored. To request all user
* properties include {@link Session#ALL_USER_PROPERTIES} as a key.
* In this case any other user properties are ignored.
*
* @param request type
* @param response type
*
* @return a CompletableFuture that completes when the handler has been
* registered, returning a {@link Registration} which can be used to
* unregister the handler.
*
* Otherwise, the CompletableFuture will complete exceptionally with
* a {@link CompletionException}. Common reasons for failure, listed
* by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link SessionClosedException} – if the session is
* closed;
*
*
- {@link HandlerConflictException} – if the session has
* already registered a handler for this message path;
*
*
- {@link PermissionsException} – if the session does
* not have {@code REGISTER_HANDLER} permission to register a
* request handler on the server;
*
*
- {@link PermissionsException} – if the session does
* not have {@code VIEW_SESSION} permission to access the client's
* session properties.
*
*
* @since 6.0
*/
CompletableFuture addRequestHandler(
String path,
Class extends T> requestType,
Class super R> responseType,
RequestHandler handler,
String... sessionProperties);
/**
* Send a request to all sessions that satisfy a given session filter.
*
* @param filter the session filter expression. See {@link Session} for a
* full description of filter expressions.
* @param path message path used by the recipient to select an appropriate
* handler
* @param request the request object
* @param requestType the type of the request to be sent
* @param responseType the type of the response to be received
* @param callback the callback to receive notification of responses (or
* errors) from sessions
*
* @param request type
* @param response type
*
* @return a CompletableFuture that completes when the server has dispatched
* all the requests.
*
*
* If the server successfully evaluated the filter, the result of
* this contains the number of sessions the request was sent to.
* Failure to send a request to a particular matching session is
* reported to the {@code callback}.
*
*
* Otherwise, the CompletableFuture will complete exceptionally with
* a {@link CompletionException}. Common reasons for failure, listed
* by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link InvalidFilterException} – if the {@code filter}
* parameter could not be parsed;
*
*
- {@link PermissionsException} – if the calling
* session does not have {@code SEND_TO_SESSION} and
* {@code VIEW_SESSION} permissions;
*
*
- {@link SessionClosedException} – if the calling session
* is closed.
*
*
* @since 6.0
*/
CompletableFuture sendRequestToFilter(
String filter,
String path,
T request,
Class requestType,
Class responseType,
FilteredRequestCallback super R> callback);
/**
* Callback interface for requests dispatched through a filter.
*
* @param response type
*
* @since 6.0
*/
interface FilteredRequestCallback {
/**
* Called when a response has been received.
*
* @param sessionId sessionId of the session that sent the response
* @param response response object
*/
void onResponse(SessionId sessionId, R response);
/**
* Called when a response from a session results in an error.
*
* @param sessionId sessionId of the session in error
* @param throwable the throwable reason of the response error
*/
void onResponseError(SessionId sessionId, Throwable throwable);
/**
* Default implementation of {@link FilteredRequestCallback}.
*
* This simply logs the calls to each callback at either WARN or DEBUG.
*
* @param response class type
*/
class Default extends Stream.Default
implements FilteredRequestCallback {
private static final Logger LOG =
getLogger(FilteredRequestCallback.Default.class);
@Override
public void onResponse(SessionId sessionId, R response) {
LOG.debug("Response received: {} from session {}", response,
sessionId);
}
@Override
public void onResponseError(SessionId sessionId,
Throwable throwable) {
LOG.warn("Error on response from session {}", sessionId,
throwable);
}
}
}
/**
* Interface which specifies a request handler to receive request
* notifications. {@link RequestHandler.Responder#respond} must be called to
* dispatch a response to the request.
*
* @param request class type
* @param response class type
*
* @since 6.0
*/
interface RequestHandler extends Stream {
/**
* Context of the request received.
*/
interface RequestContext {
/**
* SessionId of the session that sent the request.
*
* @return the sessionId
*/
SessionId getSessionId();
/**
* Returns the message path of the request.
*
* @return the path
*/
String getPath();
/**
* Session properties of the session that sent the request.
*
* @return the session properties
*/
Map getSessionProperties();
}
/**
* Responder interface to dispatch responses to requests.
*
* @param response type
*/
interface Responder {
/**
* Dispatch a response to a request.
*
* @param response the response
*/
void respond(R response);
/**
* Reject a request.
*
* @param message context message to be contained in the rejection.
*
* @see RejectedRequestException
*/
void reject(String message);
}
/**
* Called to indicate a request has been received.
*
* @param request request that was received
* @param context context object that provides the session id (session
* that sent the request), path and session properties.
* @param responder responder to dispatch a response back to the
* requester
*/
void onRequest(
T request, RequestContext context, Responder responder);
}
}