Interface MatsSocketServer


public interface MatsSocketServer
The MatsSocket Java library, along with its several clients libraries, is a WebSocket-"extension" of the Mats library (there are currently clients for JavaScript (web and Node.js) and Dart (Dart and Flutter)). It provides for asynchronous communications between a Client and a MatsSocketServer using WebSockets, which again asynchronously interfaces with the Mats API. The result is a simple programming model on the client, providing Mats's asynchronous and guaranteed delivery aspects all the way from the client, to the server, and back, and indeed the other way ("push", including requests from Server to Client). It is a clearly demarcated solution in that it only utilizes the API of the Mats library and the API of the JSR 356 WebSocket API for Java (with some optional hooks that typically will be implemented using the Servlet API, but any HTTP server implementation can be employed). MatsSocketEndpoints are simple to define, code and reason about, simple to authenticate and authorize, and the interface with Mats is very simple, yet flexible.

Features:

  • Very lightweight transport protocol, which is human readable and understandable, with little overhead (all message types and all socket closure modes).
  • A TraceId is created on the Client, sent with the client call, through the MatsSocketServer, all the way through all Mats stages, back to the Reply - logged along all the steps.
  • Provides "Send" and "Request" features both Client-to-Server and Server-to-Client.
  • Reconnection in face of broken connections is handled by the Client library, with full feedback solution to the end user via event listeners
  • Guaranteed delivery both ways, also in face of reconnects at any point, employing an "outbox" and "inbox" on both sides, with a three-way handshake protocol for each "information bearing message": Message, acknowledge of message, acknowledge of acknowledge.
  • Client side Event Listener for ConnectionEvents (e.g. connecting, waiting, session_established, lost_connection), for display to end user.
  • Client side Event Listener for SessionClosedEvents, which is when the system does not manage to keep the guarantees in face of adverse situation on the server side (typically lost database connection in a bad spot).
  • Pipelining of messages, which is automatic (i.e. delay of 2 ms after last message enqueued before the pipeline is sent) - but with optional "flush()" command to send a pipeline right away.
  • Several debugging features in the protocol (full round-trip TraceId, comprehensive logging, and DebugOptions)
  • Built-in and simple statistics gathering on the client.
  • Simple and straight-forward, yet comprehensive and mandatory, authentication model employing a MatsSocketServer-wide server AuthenticationPlugin paired with a Client authentication callback, and a per-MatsSocketEndpoint, per-message IncomingAuthorizationAndAdapter.
  • The WebSocket protocol itself has built-in "per-message" compression.

Notes:

  • There is no way to cancel or otherwise control individual messages: The library's simple "send" and Promise-yielding "request" operations (with optional "receivedCallback" for the client) is the only way to talk with the library. When those methods returns, the operation is queued, and will be transferred to the other side ASAP. This works like magic for short and medium messages, but does not constitute a fantastic solution for large payloads over bad networks (as typically can be the case with Web apps and mobile apps).
  • MatsSockets does not provide "channel multiplexing", meaning that one message (or pipeline of messages) will have to be fully transferred before the next one is. This means that if you decide to send over a 200 MB PDF using the MatsSocket instance over a 2G cellular network, any subsequent messages issued in the same direction will experience a massive delay - i.e. MatsSocket is susceptible to head of line blocking. This again means that you should probably not do such large transfers over MatsSocket: Either you should do such a download or upload using ordinary HTTP solutions (which can go concurrent with MatsSocket's WebSocket), or employ a secondary MatsSocket instance for such larger message if you just cannot get enough of the MatsSocket API - but you would still get more control over e.g. progress and cancellation of the transfer with the HTTP approach.

WARNING! Please make absolutely certain that you understand that incoming messages to MatsSocketEndpoints originate directly from the hostile Internet, and you cannot assume that any values are benign - they might be specifically tailored to hack or crash your system. Act and code accordingly! It is imperative that you NEVER rely on information from the incoming message to determine which user to act as: It would e.g. be absolutely crazy to rely on a parameter in an incoming DTO declaring "userId" when deciding which user to place an order for, or to withdraw money from. Such information must be gotten by the authenticated elements, which are incomingContext.getPrincipal() and incomingContext.getUserId(). The same holds for authorization of access: If the incoming DTO from the Client demands to see 'top secret folder', you cannot rely on this, even though you filtered which elements the user can request in a 'folders you are allowed to access'-list in a previous message to the Client. The user handling the Client could just hack the request DTO to request the top secret folder even though this was not present in the list of allowed folders. You must therefore again authorize that the requesting user actually has access to the folder he requests before returning it. This is obviously exactly the same as for any other transport, e.g. HTTP: It is just to point out that MatsSocket doesn't magically relieve you from doing proper validation and authorization of incoming message.

  • Method Details

    • matsSocketEndpoint

      <I, MR, R> MatsSocketServer.MatsSocketEndpoint<I,MR,R> matsSocketEndpoint(String matsSocketEndpointId, Class<I> incomingClass, Class<MR> matsReplyClass, Class<R> replyClass, MatsSocketServer.IncomingAuthorizationAndAdapter<I,MR,R> incomingAuthEval, MatsSocketServer.ReplyAdapter<I,MR,R> replyAdapter)
      Registers a MatsSocket Endpoint, including a MatsSocketServer.ReplyAdapter which can adapt the reply from the Mats endpoint before being fed back to the MatsSocket - and also decide whether to resolve or reject the waiting Client Promise.

      NOTE: If you supply MatsObject as the type 'MR', you will get such an instance, and can decide yourself what to deserialize it to - it will be like having a Java method taking Object as argument. However, there is no "instanceof" functionality, so you will need to know what type of object it is by other means, e.g. by putting some up-flow information on the Mats ProcessContext as a TraceProperty.

      NOTE: You need not be specific with the 'R' type being created in the MatsSocketServer.ReplyAdapter - it can be any superclass of your intended Reply DTO(s), up to Object. However, the introspection aspects will take a hit, i.e. when listing all MatsSocketEndpoints on some monitoring/introspection page. This is also a bit like with Java: Methods returning Object as return type are annoying, but can potentially be of value in certain convoluted scenarios.

    • matsSocketEndpoint

      default <I, R> MatsSocketServer.MatsSocketEndpoint<I,R,R> matsSocketEndpoint(String matsSocketEndpointId, Class<I> incomingClass, Class<R> replyClass, MatsSocketServer.IncomingAuthorizationAndAdapter<I,R,R> incomingAuthEval)
      (Convenience-variant of the base method) Registers a MatsSocket Endpoint where there is no replyAdapter - the reply from the Mats endpoint is directly fed back (as "resolved") to the MatsSocket. The Mats Reply class and MatsSocket Reply class is thus the same.

      Types: MR = R

      NOTE: In this case, you cannot specify Object as the 'R' type - this is due to technical limitations with how MatsSocket interacts with Mats: You probably have something in mind where a Mats endpoint is configured to "return Object", i.e. can return whatever type of DTO it wants, and then feed the output of this directly over as the Reply of the MatsSocket endpoint, and over to the Client. However, Mats's and MatsSocket's serialization mechanisms are not the same, and can potentially be completely different. Therefore, there needs to be an intermediary that deserializes whatever comes out of Mats, and (re-)serializes this to the MatsSocket Endpoint's Reply. This can EITHER be accomplished by specifying a specific class, in which case MatsSocket can handle this task itself by asking Mats to deserialize to this specified type, and then returning the resulting instance as the MatsSocket Endpoint Reply (which then will be serialized using the MatsSocket serialization mechanism). With this solution, there is no need for ReplyAdapter, which is the very intent of the present variant of the endpoint-creation methods. OTHERWISE, this can be accomplished using user-supplied code, i.e. the ReplyAdapter. The MatsSocket endpoint can then forward to one, or one of several, Mats endpoints that return a Reply with one of a finite set of types. The ReplyAdapter would then have to choose which type to deserialize the Mats Reply into (using the matsObject.toClass(<class>) functionality), and then return the desired MatsSocket Reply (which, again, will be serialized using the MatsSocket serialization mechanism).

    • matsSocketDirectReplyEndpoint

      default <I, R> MatsSocketServer.MatsSocketEndpoint<I,?,R> matsSocketDirectReplyEndpoint(String matsSocketEndpointId, Class<I> incomingClass, Class<R> replyClass, MatsSocketServer.IncomingAuthorizationAndAdapter<I,Void,R> incomingAuthEval)
      (Convenience-variant of the base method) Registers a MatsSocket Endpoint meant for situations where you intend to reply directly in the MatsSocketServer.IncomingAuthorizationAndAdapter without forwarding to Mats.

      Types: MR = void

      NOTE: In this case, it is possible to specify 'R' = Object. This is because you do not intend to interface with Mats at all, so there is no need for MatsSocket Server to know which type any Mats Reply is.

    • matsSocketTerminator

      default <I> MatsSocketServer.MatsSocketEndpoint<I,Void,Void> matsSocketTerminator(String matsSocketEndpointId, Class<I> incomingClass, MatsSocketServer.IncomingAuthorizationAndAdapter<I,Void,Void> incomingAuthEval)
      (Convenience-variant of the base method) Registers a MatsSocket Terminator (no reply), specifically for Client-to-Server "SEND", and to accept a "REPLY" from a Server-to-Client "REQUEST".

      Types: MR = R = void

      request(String, String, String, Object, String, String, byte[]) request}) operations from the Client.

    • send

      void send(String sessionId, String traceId, String clientTerminatorId, Object messageDto) throws MatsSocketServer.DataStoreException
      Sends a message to the specified MatsSocketSession, to the specified Client TerminatorId. This is "fire into the void" style messaging, where you have no idea of whether the client received the message. Usage scenarios include "New information about order progress" which may or may not include said information (if not included, the client must do a request to update) - but where the server does not really care if the client gets the information, only that if he actually has the webpage/app open at the time, he will get the message and thus update his view of the changed world.

      Note: If the specified session is closed when this method is invoked, the message will (effectively) silently be dropped. Even if you just got hold of the sessionId and it was active then, it might asynchronously close while you invoke this method.

      Note: The message is put in the outbox, and if the session is actually connected, it will be delivered ASAP, otherwise it will rest in the outbox for delivery once the session reconnects. If the session then closes or times out while the message is in the outbox, it will be deleted.

      Note: Given that the session actually is live and the client is connected or connects before the session is closed or times out, the guaranteed delivery and exactly-once features are in effect, and this still holds in face of session reconnects.

      Throws:
      MatsSocketServer.DataStoreException - if the ClusterStoreAndForward makes any problems when putting the outgoing message in the outbox.
    • request

      void request(String sessionId, String traceId, String clientEndpointId, Object requestDto, String replyToMatsSocketTerminatorId, String correlationString, byte[] correlationBinary) throws MatsSocketServer.DataStoreException
      Initiates a request to the specified MatsSocketSession, to the specified Client EndpointId, with a replyTo specified to (typically) a MatsSocket terminator - which includes a String "correlationString" and byte array "correlationBinary" which can be used to correlate the reply to the request (available here and here for the reply processing). Do note that since you have no control of when the Client decides to close the browser or terminate the app, you have no guarantee that a reply will ever come - so code accordingly.

      Note: the correlationString and correlationBinary are not sent over to the client, but stored server side in the ClusterStoreAndForward. This both means that you do not need to be afraid of size (but storing megabytes is silly anyway), but more importantly, this data cannot be tampered with client side - you can be safe that what you gave in here is what you get out in the context.getCorrelationString() and context.getCorrelationBinary().

      Note: To check whether the client Resolved or Rejected the request, use MatsSocketServer.MatsSocketEndpointIncomingContext.getMessageType().

      Note: If the specified session is closed when this method is invoked, the message will (effectively) silently be dropped. Even if you just got hold of the sessionId and it was active then, it might asynchronously close while you invoke this method.

      Note: The message is put in the outbox, and if the session is actually connected, it will be delivered ASAP, otherwise it will rest in the outbox for delivery once the session reconnects. If the session then closes or times out while the message is in the outbox, it will be deleted.

      Note: Given that the session actually is live and the client is connected or connects before the session is closed or times out, the guaranteed delivery and exactly-once features are in effect, and this still holds in face of session reconnects.

      Throws:
      MatsSocketServer.DataStoreException - if the ClusterStoreAndForward makes any problems when putting the outgoing message in the outbox.
    • publish

      void publish(String traceId, String topicId, Object messageDto) throws io.mats3.MatsInitiator.MatsBackendRuntimeException
      Publish a Message to the specified Topic, with the specified TraceId. This is pretty much a direct invocation of MatsInitiator.MatsInitiate.publish(Object) on the MatsFactory, and thus you might get the MatsInitiator.MatsBackendRuntimeException which MatsInitiator.initiateUnchecked(InitiateLambda) raises.

      Note: A published message will be broadcast to all nodes in the MatsSocketServer instance (where each instance then evaluates if it have subscribers to the topic and forwards to those). In addition, a certain number of messages per topic will be retained in memory to support "replay of lost messages" when a Client looses connection and must reconnect. You should consider these facts when designing usage of pub/sub. Messages over topics should generally be of interest to more than one party. While it is certainly feasible to have user-specific, or even session-specific topics, which could be authorized to only be subscribable by the "owning user" or even "owning session" (by use of the AuthenticationPlugin), the current implementation of pub/sub will result in quite a bit of overhead with extensive use of such an approach. Also, even for messages that are of interest to multiple parties, you should consider the size of the messages: Maybe not send large PDFs or the entire ISO-images of "newly arrived BlueRays" over a topic - instead send a small notification about the fresh BlueRay availability including just essential information and an id, and then the client can decide whether he wants to download it.

      Parameters:
      traceId - traceId for the flow.
      topicId - which Topic to Publish on.
      messageDto - the message to Publish.
      Throws:
      io.mats3.MatsInitiator.MatsBackendRuntimeException - if the Mats implementation cannot connect to the underlying message broker, or are having problems interacting with it.
    • getMatsSocketEndpoints

      SortedMap<String,MatsSocketServer.MatsSocketEndpoint<?,?,?>> getMatsSocketEndpoints()
      Returns:
      all registered MatsSocketEndpoints, as a SortedMap[endpointId, endpoint].
    • getMatsSocketSessions

      List<MatsSocketServer.MatsSocketSessionDto> getMatsSocketSessions(boolean onlyActive, String userId, String appName, String appVersionAtOrAbove) throws MatsSocketServer.DataStoreException
      Unless restricted by the "constraint parameters", this method returns all MatsSocketSessions on this MatsSocketServer instance, regardless of whether the session currently is connected, and if connected, which node it is connected to. This is done by reading from the data store, as opposed to methods getActiveMatsSocketSessions() and getLiveMatsSocketSessions(), which returns result from this node's internal structures - and therefore only returns sessions that are connected right now, and are connected to this node. This means that you will get returned both connected sessions, and sessions that are not currently connected (unless restricting this via parameter 'onlyActive'). The latter implies that they are state=MatsSocketServer.ActiveMatsSocketSession.MatsSocketSessionState.DEREGISTERED, and the MatsSocketServer.MatsSocketSession.getNodeName() returns Optional.empty().

      The parameters are constraints - if a parameter is null or false, that parameter is not used in the search criteria, while if it is set, that parameter will constrain the search.

      Parameters:
      onlyActive - If true, only returns "active" MatsSocketSessions, currently being connected to some node, i.e. having MatsSocketServer.MatsSocketSession.getNodeName() NOT returning Optional.empty().
      userId - If non-null, restricts the results to sessions for this particular userId
      appName - If non-null, restricts the results to sessions for this particular app-name. Do realize that it is the Client that specifies this value, there is no restriction and you cannot trust that this String falls within your expected values.
      appVersionAtOrAbove - If non-null, restricts the results to sessions having app-version at or above the specified value, using ordinary alphanum comparison. Do realize that it is the Client that specifies this value, there is no restriction and you cannot trust that this String falls within your expected values.
      Returns:
      the list of all MatsSocketSessions currently registered with this MatsSocketServer instance matching the constraints if set - as read from the data store.
      Throws:
      MatsSocketServer.DataStoreException - if the ClusterStoreAndForward makes any problems when reading sessions from it.
    • getMatsSocketSessionsCount

      int getMatsSocketSessionsCount(boolean onlyActive, String userId, String appName, String appVersionAtOrAbove) throws MatsSocketServer.DataStoreException
      Like getMatsSocketSessions(boolean, String, String, String), only returning the count - this might be interesting if there are very many sessions, and you do not need the full DTOs of every Session, just the count for a metric to graph or similar.
      Returns:
      the count of all MatsSocketSessions currently registered with this MatsSocketServer instance matching the constraints if set - as read from the data store.
      Throws:
      MatsSocketServer.DataStoreException - if the ClusterStoreAndForward makes any problems when reading sessions from it.
    • getActiveMatsSocketSessions

      This returns static, frozen-in-time, "copied-out" DTO-variants of the LiveMatsSocketSessions. Please observe the difference between MatsSocketServer.ActiveMatsSocketSession and MatsSocketServer.LiveMatsSocketSession. If you have a massive amount of sessions, and only need the sessions for appName="MegaCorpWebBank", then you should consider not employing this method, but instead do a variant of what this method does, where you restrict the "copy out" to the relevant sessions:
       SortedMap<String, ActiveMatsSocketSessionDto> ret = new TreeMap<>();
       for (LiveMatsSocketSession liveSession : getLiveMatsSocketSessions().values()) {
           // === HERE YOU MAY ADD CRITERIA on the LiveMatsSocketSession, doing 'continue' if not matched ===
           // :: "Copy it out"
           ActiveMatsSocketSessionDto activeSession = liveSession.toActiveMatsSocketSession();
           // ?: Check that the LiveSession is still SESSION_ESTABLISHED
           if (liveSession.getState() != MatsSocketSessionState.SESSION_ESTABLISHED) {
               // -> No, it changed during copying, so then we drop this.
               continue;
           }
           // Add to result Map
           ret.put(activeSession.getMatsSocketSessionId(), activeSession);
       }
       return ret;
       
      Returns:
      a current snapshot of ActiveMatsSocketSessions - these are the active MatsSocketSessions which are active right now on this node of the set of nodes (i.e. cluster) that represents this instance of MatsSocketServer. Notice that all returned instances had state=SESSION_ESTABLISHED at the time of capture.
      See Also:
    • getLiveMatsSocketSessions

      Map<String,MatsSocketServer.LiveMatsSocketSession> getLiveMatsSocketSessions()
      Imagine that the MatsSocketServer uses a ConcurrentMap to keep its set of local, live, currently connected MatsSocketSessions. This method then returns an unmodifiable view of this Map. This means that you can get session instances, and iterate over it, but the contents will change over time as Clients come and go, i.e. connects and disconnects. It also means that you can get this Map instance once, and keep a local copy of it, and it will always be current. It again also means that if you want a "static list" of these sessions, either use getActiveMatsSocketSessions() which gives you a snapshot, "frozen-in-time" view of the active sessions, where both the sessions, and the contents of the sessions, are static. Or you may copy the values of this returned Map into another container - but in the latter case, the contents of those LiveMatsSocketSession instances are still live. Please observe the difference between MatsSocketServer.ActiveMatsSocketSession and MatsSocketServer.LiveMatsSocketSession.
      Returns:
      an unmodifiable concurrent live view of LiveMatsSocketSessions - these are the live MatsSocketSessions which are active right now on this node of the set of nodes (i.e. cluster) that represents this instance of MatsSocketServer.
      See Also:
    • addSessionEstablishedEventListener

      void addSessionEstablishedEventListener(MatsSocketServer.SessionEstablishedEventListener listener)
      SessionEstablishedEvent listeners will be invoked when an LiveMatsSocketSession is established on this node of the MatsSocketServer instance cluster, i.e. the authentication/authorization is accepted, HELLO message from Client is processed and MatsSocketSessionId is established. Note that this means that in a fairly load balanced 3-node MatsSocketServer cluster, you should get approximately 1/3 of the SessionEstablishedEvents on "this" node, while 2/3 of them will come on the "other" two nodes.

      Note: A specific MatsSocketSession with a specific MatsSocketSessionId can be established multiple times, due to RECONNECT.

      NOTE: You are advised against keeping hold of the LiveMatsSocketSession instance that is provided in the SessionEstablishedEvent. You can instead get a view of the currently live sessions for this node by means of getLiveMatsSocketSessions(). If you still decide to hold on to these active sessions instances, you must be very certain to remove it from your held instances when getting any SessionRemovedEvent, meaning that you must remove it for any of the DEREGISTER, CLOSE and TIMEOUT event types: The live session instance is dead for all of these events. If you were to remove it only on CLOSE or TIMEOUT, believing that a DEREGISTER is a "softer" removal, you have basically misunderstood! You could then get a DEREGISTER (which actually is the server informing you that it has ditched this LiveMatsSocketSession and the session is now solely represented in the data store, while you still stubbornly hold on to it!), and then not get a corresponding TIMEOUT for the same MatsSocketSessionId until many hours, or days, later. If you fail to remove it at all, you will eventually get an OutOfMemory situation. The reason here is that a MatsSocketSession instance is never "reanimated", even if the MatsSocketSession is just DEREGISTERed: A new LiveMatsSocketSession instance is always created upon a SessionEstablishedEvent, both for NEW and RECONNECT

      Parameters:
      listener - the SessionEstablishedListener that shall get invoked when MatsSocketSessions are established.
      See Also:
    • addSessionRemovedEventListener

      void addSessionRemovedEventListener(MatsSocketServer.SessionRemovedEventListener listener)
      MatsSocketServer.SessionRemovedEvent listeners will be invoked when an MatsSocketServer.LiveMatsSocketSession is removed from this node of the MatsSocketServer instance cluster - this is both when a MatsSocketSession is DEREGISTERed, in which case the Client can still RECONNECT to the same MatsSocketSessionId, and when a MatsSocketSession is CLOSEd or TIMEOUTed. In the latter cases, any information of the MatsSocketSession and its MatsSocketSessionId are deleted from the MatsSocketServer, and the session cannot be reconnected again.

      Note: A specific MatsSocketSession can DEREGISTER multiple times, due to it can RECONNECT again after each DEREGISTER. However, once it has CLOSE or TIMEOUT, the session cannot RECONNECT ever again, and hence those events are terminal wrt. to that specific MatsSocketSessionId.

      Parameters:
      listener - the SessionEstablishedListener that shall get invoked when MatsSocketSessions are removed (either deregistered, closed or timed out).
      See Also:
    • addMessageEventListener

      void addMessageEventListener(MatsSocketServer.MessageEventListener listener)
      MessageEventListeners will be invoked for every processed incoming and outgoing message for any session. It will be invoked after the message is processed OK on incoming, and after the message is sent for outgoing. Note that the MatsSocketServer.MatsSocketEnvelopeWithMetaDto contains more information than is sent over the wire, this is the "WithMeta" aspect which holds processing metadata - the wire-part is what is contained in MatsSocketServer.MatsSocketEnvelopeDto.

      Note wrt. modifications on the MatsSocketEnvelopeWithMetaDto! All fields are public and non-final, so you can modify it before e.g. sending it over Mats (e.g. nulling out the 'msg' field). However, read the JavaDoc comment on the class: There is only one single instance for all listeners and MatsSocketServer.ActiveMatsSocketSession.getLastEnvelopes() "last envelopes"}, so clone it before modifying!

      Note: The last messages per MatsSocketServer.ActiveMatsSocketSession is available via MatsSocketServer.ActiveMatsSocketSession.getLastEnvelopes().

      Parameters:
      listener - the MatsSocketServer.MessageEventListener that will be invoked for every processed incoming and outgoing envelope for any session.
      See Also:
    • closeSession

      void closeSession(String sessionId, String reason)
      Closes the specified MatsSocketSession - can be used to forcibly close an active MatsSocketSession (i.e. "kick it off"), and can also used to perform out-of-band closing of Session if the WebSocket is down (this is used in the MatsSocket.js Client, where an "onunload"-listener is attached, so that if the user navigates away, every effort is done to get the MatsSocketSession closed).

      Note: An invocation of any SessionRemoved listeners with type CLOSE will be issued.

      Note: This can be done on any node of the MatsSocketServer-instance cluster, as the instruction will be forwarded to the active node if the MatsSocketSession is not active on this node. If it is not active on any node, it will nevertheless be closed in the data store (i.e. the session cannot reconnect again).

      Parameters:
      sessionId - the id of the Session to close.
      reason - a short descriptive String of why it was closed.
    • stop

      void stop(int gracefulShutdownMillis)
      Closes all MatsSocketServer.ActiveMatsSocketSession on this node, closing the WebSocket with CloseReason.CloseCodes.SERVICE_RESTART (assuming that a MatsSocket service will never truly go down, thus effectively asking the client to reconnect, hopefully to another instance). Should be invoked at application shutdown.