com.pushtechnology.diffusion.client.session.Session Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2014, 2024 DiffusionData Ltd., All Rights Reserved.
*
* Use is subject to licence terms.
*
* NOTICE: All information contained herein is, and remains the
* property of DiffusionData. The intellectual and technical
* concepts contained herein are proprietary to DiffusionData 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.session;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.pushtechnology.diffusion.client.Diffusion;
import com.pushtechnology.diffusion.client.features.Messaging;
import com.pushtechnology.diffusion.client.features.Security;
import com.pushtechnology.diffusion.client.features.Topics;
import com.pushtechnology.diffusion.client.features.control.clients.ClientControl;
import com.pushtechnology.diffusion.client.types.Credentials;
import com.pushtechnology.diffusion.client.types.PathPermission;
/**
* A client session to a server or cluster of servers.
*
* A new session can be created by connecting to a server using
* {@link SessionFactory#open(String)}, specifying the server URL. There is also
* a non-blocking variant {@link SessionFactory#openAsync(String)}. The session
* factory can be configured to control the behavior the session.
*
* The session provides a variety of operations to the application. These are
* grouped into feature interfaces, such as {@link Topics} and
* {@link Messaging}, exposed to the application through the
* {@link #feature(Class)} method.
*
*
Session lifecycle
*
* Each session is managed by a server. The server assigns the session a
* {@link #getSessionId() unique identity}, and manages the session's topic
* subscriptions, security details, and session
* properties.
*
*
* A session can be terminated using {@link #close()}. A session may also be
* terminated by the server because of an error or a time out, or by other
* privileged sessions using the {@link ClientControl} feature.
*
*
* A client can become disconnected from the server, and reconnect to the server
* without loss of the session. Reconnection can be configured using
* {@link SessionFactory#reconnectionStrategy the session factory}. The server
* must be configured to allow reconnection.
*
* If a session is connected to a server that belongs to a cluster with session
* replication enabled, and then becomes disconnected, it will attempt to
* reconnect to the original server. A properly configured load balancer can
* detect that the original server is unavailable and re-route the reconnection
* request to a second server in the cluster. The second server can recover
* session data and continue the session. This process is known as "fail over".
* Unlike reconnection, in-flight messages can be lost during failover, and the
* application will be unsubscribed and re-subscribed to topics.
*
*
* The current state of the session can be retrieved with {@link #getState()}. A
* listener can be registered with {@link #addListener(Listener)} which will be
* notified when the session state changes.
*
*
Session properties
*
* For each session, the server stores a set of session properties that describe
* various attributes of the session.
*
* There are two types of session property. Fixed properties are assigned by
* Diffusion. User-defined properties are assigned by the application.
*
* Many operations use session filter expressions
* that use session properties to select sessions.
*
* A privileged client can monitor other sessions, including changes to their
* session properties, using a {@link ClientControl#addSessionEventListener
* session event listener}. When registering to receive session properties,
* special key values of {@link #ALL_FIXED_PROPERTIES} and
* {@link #ALL_USER_PROPERTIES} can be used.
*
*
* Each property is identified by a key. Most properties have a single string
* value. The exception is the $Roles fixed property which has a set of string
* values.
*
* Fixed properties are identified by keys with a '$' prefix. The available
* fixed session properties are:
*
*
* Key
* Description
*
*
* {@code $ClientIP}
* The Internet address of the client in string format.
*
*
* {@code $ClientType}
* The client type of the session. One of {@code ANDROID}, {@code C},
* {@code DOTNET}, {@code IOS}, {@code JAVA}, {@code JAVASCRIPT_BROWSER},
* {@code MQTT}, {@code PYTHON}, {@code REST}, or {@code OTHER}.
*
*
* {@code $Environment}
* The environment in which the client is running. For possible values see the user
* manual
*
*
*
* {@code $Connector}
* The configuration name of the server connector that the client connected
* to.
*
*
* {@code $Country}
* The country code for the country where the client's Internet address was
* allocated (for example, {@code NZ} for New Zealand). Country codes are as
* defined by {@link Locale}. If the country code could not be determined, this
* will be a zero length string.
*
*
* {@code $GatewayType}
* Gateway client type. Only set for gateway client sessions. If present it
* indicates the type of gateway client (e.g. Kafka).
*
*
* {@code $GatewayId}
* The identity of a gateway client session. Only present if the
* $GatewayType session property is present.
*
*
* {@code $Language}
* The language code for the official language of the country where the
* client's Internet address was allocated (for example, {@code en} for
* English). Language codes are as defined by {@link Locale}. If the language
* could not be determined or is not applicable, this will be a zero length
* string.
*
*
* {@code $Latitude}
* The client's latitude, if available. This will be the string
* representation of a floating point number and will be {@code NaN} if not
* available.
*
*
* {@code $Longitude}
* The client's longitude, if available. This will be the string
* representation of a floating point number and will be {@code NaN} if not
* available.
*
*
* {@code $MQQTClientId}
* The MQTT client identifier. Only set for MQTT sessions. If present, the
* value of the {@code $ClientType} session property will be {@code MQTT}.
*
*
* {@code $Principal}
* The security principal associated with the client session.
*
*
* {@code $Roles}
* Authorisation roles assigned to the session. This is a set of roles
* represented as quoted strings (for example, {@code "role1","role2"}). The
* utility method {@link Diffusion#stringToRoles(String)} can be used to parse
* the string value into a set of roles.
*
*
* {@code $ServerName}
* The name of the server to which the session is connected.
*
*
* {@code $SessionId}
* The session identifier. Equivalent to {@link SessionId#toString()}.
*
*
* {@code $StartTime}
* The session's start time in milliseconds since the epoch.
*
*
* {@code $Transport}
* The session transport type. One of {@code WEBSOCKET},
* {@code HTTP_LONG_POLL}, {@code TCP}, or {@code OTHER}.
*
*
*
* All user-defined property keys are non-empty strings. The characters ' ',
* '\t', '\r', '\n', '"', ''', '(', ')' are not allowed.
*
* Session properties are initially associated with a session as follows:
*
* - When a client starts a new session, it can optionally propose
* user-defined session properties (see
* {@link SessionFactory#property(String, String)} and
* {@link SessionFactory#properties(Map)}). Session properties proposed in this
* way must be accepted by the authenticator. This safeguard prevents abuse by a
* rogue, unprivileged client.
*
- Diffusion allocates all fixed property values.
*
- The new session is authenticated by registered authenticators. An
* authenticator that accepts a session can veto or change the user-defined
* session properties and add new user-defined session properties. The
* authenticator can also change certain fixed properties.
*
*
* Once a session is established, its user-defined session properties can be
* modified by clients with {@code VIEW_SESSION} and {@code MODIFY_SESSION}
* permissions using {@link ClientControl#setSessionProperties(SessionId, Map)}.
* A privileged client can also modify its own session properties.
*
* If a session re-authenticates (see
* {@link Security#changePrincipal(String, Credentials) changePrincipal}), the
* authenticator that allows the re-authentication can modify the user-defined
* session properties and a subset of the fixed properties as mentioned above.
*
*
Session filters
* Session filters are a mechanism of addressing a set of sessions by the values
* of their session properties.
*
* Session filters are specified using a Domain Specific Language (DSL). For a full and
* detailed description of the session filters DSL see the
*
* user manual.
*
*
Session locks
*
* The actions of multiple sessions can be coordinated using session locks. See
* {@link SessionLock}.
*
* @author DiffusionData Limited
* @since 5.0
*/
public interface Session extends AutoCloseable {
/**
* Value returned by {@link #getPrincipal} if no principal name is
* associated with the session.
*/
String ANONYMOUS = "";
/**
* This constant can be used instead of a property key in requests for
* session property values to indicate that all fixed session
* properties are required.
*
* @since 5.6
*/
String ALL_FIXED_PROPERTIES = "*F";
/**
* This constant can be used instead of a property key in requests for
* session property values to indicate that all user defined session
* properties are required.
*
* @since 5.6
*/
String ALL_USER_PROPERTIES = "*U";
/**
* Session property key for session identifier.
*
* @since 6.2
*/
String SESSION_ID = "$SessionId";
/**
* Session property key for principal.
*
* @since 6.2
*/
String PRINCIPAL = "$Principal";
/**
* Session property key for connector name.
*
* @since 6.2
*/
String CONNECTOR = "$Connector";
/**
* Session property key for transport.
*
* @since 6.2
*/
String TRANSPORT = "$Transport";
/**
* Session property key for client type.
*
* @since 6.2
*/
String CLIENT_TYPE = "$ClientType";
/**
* Session property key for country code.
*
* @since 6.2
*/
String COUNTRY = "$Country";
/**
* Session property key for language code.
*
* @since 6.2
*/
String LANGUAGE = "$Language";
/**
* Session property key for server name.
*
* @since 6.2
*/
String SERVER_NAME = "$ServerName";
/**
* Session property key for client IP address.
*
* @since 6.2
*/
String CLIENT_IP = "$ClientIP";
/**
* Session property key for client latitude.
*
* @since 6.2
*/
String LATITUDE = "$Latitude";
/**
* Session property key for client longitude.
*
* @since 6.2
*/
String LONGITUDE = "$Longitude";
/**
* Session property key for client start time.
*
* @since 6.2
*/
String START_TIME = "$StartTime";
/**
* Session property key for session roles.
*
* @since 6.2
*/
String ROLES = "$Roles";
/**
* Session property key for MQTT client identifier.
*
* @since 6.6
*/
String MQTT_CLIENT_ID = "$MQTTClientId";
/**
* Session property key for gateway client type.
*
* @since 6.6
*/
String GATEWAY_TYPE = "$GatewayType";
/**
* Session property key for gateway client identifier.
*
* @since 6.6
*/
String GATEWAY_ID = "$GatewayId";
/**
* Session property key for client environment.
*
* @since 6.11
*/
String ENVIRONMENT = "$Environment";
/**
* Returns the unique identifier for the session as assigned by the (first)
* server it connects to.
*
* @return the session identifier
*/
SessionId getSessionId();
/**
* Returns the name of the security principal requested when opening the
* session.
*
* @return the principal name. If the session was opened with no associated
* principal, (it is an "anonymous session"), the empty string (
* {@link #ANONYMOUS}) will be returned
*
*/
String getPrincipal();
/**
* Returns the session attributes.
*
* @return session attributes
*/
SessionAttributes getAttributes();
/**
* Returns the current state of the session.
*
* @return the current session state
*/
State getState();
/**
* Close the session.
*
* Has no effect if the session is already closed
*/
@Override
void close();
/**
* Obtain a feature.
*
* This can be used to get any feature. It will automatically instantiate
* the feature the first time it is called.
*
* @param featureInterface the feature interface
*
* @param feature type
*
* @return the feature
*
* @throws IllegalArgumentException if {@code featureInterface} is not an
* interface
*
* @throws UnsupportedOperationException if no implementation of the feature
* is supported or found
*/
T feature(Class featureInterface)
throws IllegalArgumentException, UnsupportedOperationException;
/**
* Add a session listener.
*
* @param listener the listener
* @since 5.1
*/
void addListener(Listener listener);
/**
* Remove a session listener. All session listeners {@link Object#equals
* equal} to {@code listener} will be removed.
*
* @param listener the listener
* @since 5.1
*/
void removeListener(Listener listener);
// @formatter:off
// @startuml
// [*] --> AWAITING : Session.lock()
// [*] -> OWNED : Session.lock() [lock already owned] /\n CompletableFuture completes with SessionLock \
// \nequal to the one provided when lock was acquired
// AWAITING --> OWNED : lock acquired / CompletableFuture completes with new SessionLock for the acquisition
// state OWNED : SessionLock.isOwned() == true
// OWNED --> RELEASED : SessionLock.unlock()
// OWNED --> RELEASED : session closed
// OWNED --> RELEASED : connection lost [scope is UNLOCK_ON_CONNECTION_LOSS]
// state RELEASED : SessionLock.isOwned() == false
// AWAITING -> [*] : session closed / CompletableFuture completes with SessionClosedException
// AWAITING -> [*] : no ACQUIRE_LOCK permission / CompletableFuture completes with SecurityException
// AWAITING -> [*] : CompletableFuture canceled
// @enduml
// @formatter:on
/**
* Attempt to acquire a {@link SessionLock session lock}.
*
* This method returns a CompletableFuture that will complete normally if
* the server assigns the requested lock to the session. Otherwise, the
* CompletableFuture will complete exceptionally with an exception
* indicating why the lock could not be acquired.
*
*
* Acquiring the lock can take an arbitrarily long time if other sessions
* are competing for the lock. The server will retain the session's request
* for the lock until it is assigned to the session, the session is closed,
* or the session cancels the CompletableFuture.
*
*
* A session can call this method multiple times. If the lock is acquired,
* all calls will complete successfully with equal SessionLocks.
*
*
* Canceling the returned CompletableFuture has no effect on other pending
* calls to {@code lock(...}} made by the session.
*
*
* If the CompletableFuture completes normally, the session owns the lock
* and is responsible for unlocking it. When canceling a CompletableFuture,
* take care that it has not already completed by checking the return value.
* The following code releases the lock if the request could not be
* canceled.
*
*
* CompletableFuture result = session.lock("my-lock");
*
* // ..
*
* if (!result.cancel(true)) {
* // The session acquired the lock. Release it.
* SessionLock lock = result.get();
* lock.unlock();
* }
*
*
*
* A session that acquires a lock will remain its owner until it is
* {@link SessionLock#unlock() unlocked} or the session closes. The
* {@link #lock(String, SessionLockScope)} variant of this method takes a
* scope parameter that provides the further option of releasing the lock
* when the session loses its connection to the server.
*
*
Access control
*
* To allow fine-grained access control, lock names are interpreted as path
* names, controlled with the {@link PathPermission#ACQUIRE_LOCK
* ACQUIRE_LOCK} permission. This allows permission to be granted to a
* session to acquire the lock {@code update-topic/a} while preventing the
* session from acquiring the lock {@code update-topic/b}, for example.
*
* @param lockName the name of the session lock
* @return a CompletableFuture that completes when a response is received
* from the server.
*
*
* If this session has successfully acquired the session lock, or
* this session already owns the session lock, the CompletableFuture
* will complete normally with a SessionLock result.
*
*
* If the CompletableFuture completes exceptionally, this session
* does not own the session lock. Common reasons for failure,
* indicated by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link PermissionsException} – if the calling
* session does not have the {@link PathPermission#ACQUIRE_LOCK
* ACQUIRE_LOCK} permission for {@code lockName};
*
*
- {@link SessionClosedException} – if the session is
* closed.
*
*
* @since 6.1
* @see SessionLock
*/
CompletableFuture lock(String lockName);
/**
* Variant of {@link #lock(String)} that provides control over when a lock
* will be released.
*
*
* If called with {@link SessionLockScope#UNLOCK_ON_SESSION_LOSS
* UNLOCK_ON_SESSION_LOSS}, this method behaves exactly like
* {@link #lock(String)}.
*
*
* If called with {@link SessionLockScope#UNLOCK_ON_CONNECTION_LOSS
* UNLOCK_ON_CONNECTION_LOSS}, any lock that is returned will be unlocked
* if the session loses its connection to the server. This is useful to
* allow another session to take ownership of the lock while this session is
* reconnecting.
*
* @param lockName the name of the session lock
* @param scope preferred scope. The scope of a lock controls when it will
* be released automatically. If a session makes multiple requests
* for a lock using different scopes, and the server assigns the lock
* to the session fulfilling the requests, the lock will be given the
* weakest scope (UNLOCK_ON_CONNECTION_LOSS).
* @return a CompletableFuture that completes when a response is received
* from the server. See {@link #lock(String)}.
*/
CompletableFuture lock(String lockName,
SessionLockScope scope);
/**
* Values for the {@code scope} parameter of
* {@link Session#lock(String, SessionLockScope)}.
*
* @since 6.1
*/
enum SessionLockScope {
/**
* The lock will be released when the acquiring session loses its
* current connection to the server.
*/
UNLOCK_ON_CONNECTION_LOSS,
/**
* The lock will be released when the acquiring session is closed.
*/
UNLOCK_ON_SESSION_LOSS,
}
/**
* A session lock is a server-managed resource that can be used to
* coordinate exclusive access to shared resources across sessions. For
* example, to ensure a single session has the right to update a topic; to
* ensure at most one session responds to an event; or to select a single
* session to perform a housekeeping task. Session locks support general
* collaborative locking schemes. The application architect is responsible
* for designing a suitable locking scheme and for ensuring each application
* component follows the scheme appropriately.
*
*
* Session locks are identified by a lock name. Lock names are arbitrary and
* chosen at will to suit the application. Each lock is owned by at most one
* session. Locks are established on demand; there is no separate operation
* to create or destroy a lock.
*
*
* A session lock is acquired using the {@link Session#lock(String)} method.
* If no other session owns the lock, the server will assign the lock to the
* calling session immediately. Otherwise, the server will record that the
* session is waiting to acquire the lock. A session can call {@code lock}
* more than once for a given session lock – if the lock is acquired,
* all calls will complete successfully with equal SessionLocks.
*
*
* If a session closes, the session locks it owns are automatically
* released. A session can also {@link SessionLock#unlock() release a lock}.
* When a session lock is released and other sessions are waiting to acquire
* the lock, the server will arbitrarily select one of the waiting sessions
* and notify it that it has acquired the lock. All of the newly selected
* session's pending {@code lock} calls will complete normally. Other
* sessions will continue to wait.
*
*
* The {@link #lock(String, SessionLockScope)} variant of this method takes
* a scope parameter that provides the further option of automatically
* releasing the lock when the session loses its connection to the server.
*
*
* The acquisition life cycle of a session lock from the perspective of a
* session is shown in the following diagram.
*
*
*
*
*
Differences to java.util.concurrent.locks.Lock
*
*
* Unlike the {@link java.util.concurrent.locks.Lock} API, there is no
* association between a lock and a thread. If a session calls this method
* for a lock it already owns, the call will complete normally and
* immediately with a {@code SessionLock} that is equal to the one returned
* when the lock was originally acquired. A single call to
* {@link SessionLock#unlock()} will release this session's claim to a lock.
*
*
* A further difference to {@code java.util.concurrent.locks.Lock} is that
* lock ownership can be lost due to an independent event such as loss of
* connection, and not only due to the use of the locking API by the owner.
* Consequently, the session should poll using {@link SessionLock#isOwned()}
* to check that it still owns the lock before accessing the protected
* resource.
*
*
Race conditions
*
* This session lock API has inherent race conditions. Even if an
* application is coded correctly to protect a shared resource using session
* locks, there may be a period where two or more sessions concurrently
* access the resource. The races arise for several reasons including
*
* - due to the check-then-act approach of polling
* {@code isOwned()}, the lock can be lost after the check has succeeded but
* before the resource is accessed;
*
- the server can detect a session is disconnected and assign the lock
* to another session before the original session has detected the
* disconnection.
*
*
* Despite this imprecision, session locks provide a useful way to
* coordinate session actions.
*
* @since 6.1
*/
interface SessionLock {
/**
* @return the name of the session lock
*/
String getName();
/**
* A value that identifies the acquisition of the lock with the given
* {@link #getName() name}. SessionLocks that are acquired later are
* guaranteed to have bigger sequence values, allowing the sequence
* number to be used as a fencing token.
*
* @return a value that identifies the acquisition of this lock
*/
long getSequence();
/**
* Test whether the session lock is still owned.
*
* @return true if the session lock is still owned by the session
*/
boolean isOwned();
/**
* The scope of the lock.
*
*
* The scope determines when the lock will be released automatically.
*
*
* If a session makes multiple
* {@link Session#lock(String, SessionLockScope) requests for a lock}
* using different scopes, and the server assigns the lock to the
* session fulfilling the requests, the lock will be given the weakest
* scope (UNLOCK_ON_CONNECTION_LOSS). Consequently, an individual
* request can complete with a lock that has a different scope to that
* requested.
*
* @return the lock scope
* @see Session#lock(String, SessionLockScope)
*/
SessionLockScope getScope();
/**
* Release a session lock, if owned.
*
* @return a CompletableFuture that completes when a response is
* received from the server.
*
*
* On completion, this session will no longer own the named
* session lock. If CompletableFuture completes normally, a true
* value indicates this session previously owned the lock and a
* false value indicates it did not.
*
*
* If the CompletableFuture completes exceptionally, this
* session does not own the session lock. Common reasons for
* failure, indicated by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link SessionClosedException} – if the session is
* closed.
*
*
* @since 6.1
* @see #lock(String)
*/
CompletableFuture unlock();
}
/**
* The optional listener interface for a session which may be used to
* receive state notifications. By default a session will not have a
* listener.
*/
interface Listener {
/**
* Called whenever the state of a session changes.
*
* @param session the session
*
* @param oldState the old state
*
* @param newState the new state
*/
void onSessionStateChanged(
Session session,
State oldState,
State newState);
/**
* Default {@link Listener} implementation which simply logs events at
* debug level.
*/
class Default implements Session.Listener {
private static final Logger LOG =
LoggerFactory.getLogger(Session.Listener.Default.class);
@Override
public void onSessionStateChanged(
Session session,
State oldState,
State newState) {
LOG.debug(
"{} - Session {} state changed from {} to {}",
this,
session,
oldState,
newState);
}
}
}
/**
* The error notification interface for a session.
*
* Session errors indicate that an unexpected condition has occurred. They
* have a similar purpose to Java exceptions, except they are delivered
* asynchronously. Session errors are used sparingly. Error conditions that
* occur in the context of an API operation and that an application can
* reasonably handle are reported to operation-specific callbacks, not as
* session errors.
*
* An application can typically do nothing about a session error other than
* to report it for diagnosis. The server log should be examined for further
* information. Errors may be a consequence of a failed client operation, so
* it is usually appropriate for the application to close the session on
* receiving an error.
*/
interface ErrorHandler {
/**
* Called when an error has occurred.
*
* @param session the session
*
* @param error the error detail
*/
void onError(Session session, SessionError error);
/**
* Default {@link ErrorHandler} implementation which simply logs errors
* at error level.
*/
class Default implements Session.ErrorHandler {
private static final Logger LOG =
LoggerFactory.getLogger(Session.ErrorHandler.Default.class);
@Override
public void onError(Session session, SessionError error) {
LOG.error(
"{} - An error occurred for session {}: {}",
this,
session,
error);
}
}
}
/**
* Encapsulates the detail of a reported error.
*/
interface SessionError {
/**
* Returns a description of the error.
*
* @return a description of the error
*/
String getMessage();
/**
* Returns a string representation of the error.
*
* @return the same as {@link #getMessage()}
*/
@Override
String toString();
}
/**
* Session state.
*/
enum State {
/**
* The session is establishing its initial connection.
*/
CONNECTING(false, false, false),
/**
* An active connection with the server has been established.
*/
CONNECTED_ACTIVE(true, false, false),
/**
* Connection with a server has been lost and the session is attempting
* reconnection.
*/
RECOVERING_RECONNECT(false, true, false),
/**
* The session has been closed by the client.
*/
CLOSED_BY_CLIENT(false, false, true),
/**
* The session has been closed (or rejected) by the server.
*/
CLOSED_BY_SERVER(false, false, true),
/**
* The session has lost its connection to a server and could not be
* recovered.
*/
CLOSED_FAILED(false, false, true);
private final boolean thisIsConnected;
private final boolean thisIsRecovering;
private final boolean thisIsClosed;
/**
* Constructor.
*/
State(boolean connected, boolean recovering, boolean closed) {
thisIsConnected = connected;
thisIsRecovering = recovering;
thisIsClosed = closed;
}
/**
* Returns true if a connected state.
*
* @return true if connected state
*/
public boolean isConnected() {
return thisIsConnected;
}
/**
* Returns true if a recovering state.
*
* @return true if recovering
*/
public boolean isRecovering() {
return thisIsRecovering;
}
/**
* Returns true if a disconnected state.
*
* @return true if disconnected
*/
public boolean isClosed() {
return thisIsClosed;
}
}
}