GithubHelp home page GithubHelp logo

centiservice / matssocket Goto Github PK

View Code? Open in Web Editor NEW
5.0 2.0 1.0 1.25 MB

WebSocket-based server and client libs for asynchronous, bidirectional interaction with Mats3 from end-user clients

License: Other

Dart 25.46% JavaScript 27.22% Java 45.70% HTML 1.63%

matssocket's Introduction

MatsSocket - Mats3 to the client over WebSocket

MatsSocket is a WebSocket-based client-server solution which bridges the asynchronous message based nature of Mats3 all the way out to your end user client applications, featuring bidirectional communication. It consists of a small MatsSocketServer API which is implemented on top of the _ Mats3 API_ and JSR 356 Java API for WebSockets (which most Servlet Containers implement), as well as client libraries - for which there currently exists JavaScript and Dart/Flutter implementations.

Java server API and implementation: Maven Repository
JavaScript client: npm
Dart/Flutter client: pub.dev

To get a gist of how this works on the client, here is a small JavaScript client code example:

// Set up the MatsSocket.
var matsSocket = new MatsSocket("TestApp", "1.2.3",
    ['wss://matssocketserver-one.example.com/matssocket',
     'wss://matssocketserver-two.example.com/matssocket']);

// Using bogus example authorization.
matsSocket.setAuthorizationExpiredCallback(function (event) {
    // Emulate that it takes some time to get new auth.
    setTimeout(function () {
        var expiry = Date.now() + 20000;
        matsSocket.setCurrentAuthorization("DummyAuth:example", expiry, 10000);
    }, 100);
});

// Perform a Request to server, which will forward the Request to a Mats endpoint, whose Reply comes
// back here, resolving the returned Promise.
matsSocket.request("MatsSocketEndpoint", "TraceId_" + matsSocket.id(6), {
    string: "Request String",
    number: Math.E
}).then(function (messageEvent) {
    console.log("REQUEST-with-Promise resolved, i.e. REPLY from Mats Endpoint. Took "
        + messageEvent.roundTripMillis + " ms: " + JSON.stringify(messageEvent.data));
});

This could communicate with the following MatsSocket endpoint on the server side, which uses a Mats endpoint for processing (Note that the setup of the MatsFactory and MatsSocketServer, including the authentication plugin, is elided for brevity):

// :: Make MatsSocket Endpoint, taking MatsSocketRequestDto and replying MatsSocketReplyDto
matsSocketServer.matsSocketEndpoint("MatsSocketEndpoint",
        MatsSocketRequestDto.class, MatsDataTO.class, MatsSocketReplyDto.class,
        // IncomingAuthorizationAndAdapter - the provided Principal is already set up by the
        // AuthenticationPlugin which the MatsSocketServer was instantiated with.
        (ctx, principal, msg) -> {
            // Perform Authorization, by casting the provided Principal to the application specific
            // instance and deciding whether to deny further processing.
            if (! ((ApplicationSpecificPrincipal) principal).canAccess("MatsSocketEndpoint")) {
                ctx.deny();
                return;
            }
            // Handle message by forwarding Request to Mats endpoint.
            ctx.forwardNonessential("MatsEndpoint.exampleEndpoint",
                                    new MatsDataTO(msg.number, ctx.getUserId()));
        },
        // ReplyAdapter - receives the Reply from the Mats endpoint, and resolves the client Promise.
        (ctx, matsReply) -> {
            // Adapting Mats endpoint's reply (type MatsDataTO) to the MatsSocket reply
            // (type MatsSocketReplyDto).
            ctx.resolve(new MatsSocketReplyDto(matsReply.string.length(), matsReply.number));
        });

// :: Make Mats Endpoint, both taking and replying with type MatsDataTO
// This Mats Endpoint could reside on a different service employing the same "Mats fabric"
// (i.e. MQ server), or reside on this service, but perform requests to a Mats Endpoint residing
// on a different service before Replying.
matsFactory.single("MatsEndpoint.exampleEndpoint", MatsDataTO.class, MatsDataTO.class, 
        (processContext, incomingDto) -> {
            // "Process" incoming message and return a Reply.
            return new MatsDataTO(incomingDto.number + 10,
                                  incomingDto.string + ":FromExampleMatsEndpoint");
        });

The client example first sets up the MatsSocket using two urls - the MatsSocket will randomize the array and cycle through the result until it gets a reply. The authorization callback is set up, here using a dummy example with expiration time. Since it starts out without a current authorization string set, any first operation on it will invoke the callback.

It then sends a message to the "MatsSocketEndpoint", which is a named endpoint defined on the MatsSocketServer. That endpoint will receive the message with its IncomingAuthorizationAndAdapter, and either act on it directly by denying, replying or rejecting, or forward the request to a Mats endpoint - in this example it either denies or forwards to a Mats endpoint. When the Mats endpoint replies, the reply will pass the MatsSocket endpoint's ReplyAdapter, which again decides whether to resolve or reject - in this example it choose resolve, which then resolves the Promise on the client.

Bidirectional, Server-to-Client "Push", and Topics

The client may create both Terminators and Endpoints, which allows the server to send messages, and even Requests, to the client. Terminators may also be the target for replies to client-to-server requests, by using requestReplyTo(..) instead of request(..) as in the example above.

// Client side Terminator
matsSocket.terminator("ClientSideTerminator", function (messageEvent) {
    console.log("Got message! CorrelationId:" + messageEvent.correlationId + ": "
        + JSON.stringify(messageEvent.data));
});

// Client side Endpoint
matsSocket.endpoint("ClientSideEndpoint", function (messageEvent) {
    return new Promise(function (resolve, reject) {
        // Resolve it a tad later, to emulate some kind of processing
        setTimeout(function () {
            let data = messageEvent.data;
            let msg = {
                string: data.string + ":AddedFromClientSideEndpoint",
                number: data.number + Math.PI
            };
            // We choose to resolve the request
            resolve(msg);
        }, 25);
    });
});

The client may subscribe to topics, which the server may use to broadcast messages. The server side authentication plugin is queried whether a given user may subscribe to a given topic.

matsSocket.subscribe("NewProductAnnouncements", function(messageEvent) {
    console.log("Got Topic message! " + JSON.stringify(messageEvent.data));
});

Highly available, transparent reconnects

  • Multi node server: The MatsSocketServer may run on multiple nodes, to ensure that at least one server is always up, both handling unexpected outages and rolling deploys. It utilizes a shared database to handle session state - so that if the MatsSocket client reconnects to a different instance, the state - including outstanding messages - will follow along.

  • Client side high availability: The MatsSocket instance is instantiated using a set of URLs where it should connect. It will pick a random of these URLs and try to connect to that, rotating through the URLs if the first doesn't work.

  • Transparently handles lost connections: MatsSocket client handles all connection aspects, including lost connections and reconnects. The system employs an outbox solution, where outgoing messages from both client-to-server, and server-to-client, will queue up if there is no connection. When the connection is reestablished, these outboxes will empty out. This means that if e.g. a message is received from the client on the server and forwarded to Mats, and then the connection drops, the reply from that Mats service will be queued up, and seamlessly delivered to the client when the connection is restored.

  • "Guaranteed", exactly-once processing: When a message is sent from this side, the other side sends an acknowledgement - and puts a reference of the message in an inbox. When this side receives the acknowledgement, it removes the message from the outbox, and sends a second acknowledgement, which upon reception on the other side deletes the inbox-reference. This protocol ensures that even faced with lost connections, the state of a message transit can be recovered: messages which are in doubt will be redelivered, but if this would end up in a double delivery/processing, the message is deduplicated by the inbox reference - the result is exactly-once processing of messages.

Authentication and Authorization

Authentication is handled by a small authentication plugin on both client and server side, which is simple enough that you may use a cookie-based approach where the containing application already has authenticated and authorized the user and thus just want the MatsSocket to ride on that authentication. It is however also advanced enough to handle authentications with expiration times, e.g. direct use of Bearer access tokens, where both the MatsSocket and the MatsSocketServer may request the client to refresh the token if it has expired, and then perform seamless reauthentication.

Authorization is handled programmatically by the MatsSocket developer upon reception of messages, in the IncomingAuthorizationAndAdapter lambda which the MatsSocket Endpoint was set up with.

Compact and low overhead wire protocol

  • Persistent session-based setup, no per-request headers: WebSockets are by their nature persistent, so each message does not need a heap of headers - the authentication and thus identification of the user is only done at session setup (and when reauthenticating if using authentication with expiry).

  • Low overhead protocol, small envelopes: The system messages are few and compact, and the envelopes which carries the data messages are tiny.

  • Compression by default: All browsers implement the compression extension of WebSockets, thus the wire size is as short as can be.

  • Batching: MatsSockets has built-in batching of messages, both client-to-server and server-to-client, where if you issue multiple requests in a row, they will by default be batched, based on a small few-milliseconds timeout set after a request is issued. (This timeout may be overridden by matsSocket.flush()). This ensures that you do not need to think about e.g. how you perform the initial user information load upon login and make a "super request" that batches the content, you may instead use whatever amount and granularity of messages that is best appropriate for the backend storage. Batching also ensures that the compression has more information to go by, quite possibly getting multiple messages in a single TCP packet.

Asynchronous, concurrent

Each message is independent of any other, including each Request with their subsequent Replies: A batch of incoming messages are handled by a thread pool, being independently processed. Any Reply is sent over as soon as it is finished, not caring about the order the Requests were issued in (Server-to-Client batching will kick in if they are finished very close in time). This ensures that you do not need to care about ordering your requests in a particular way.

(Note that a MatsSocket instance, running over a single WebSocket, is however affected by head-of-line blocking: This is not the transport you would send a Blu-ray movie over, as that direction of the channel would then be blocked until this large message was finished transmitted. Keep your messages short and to the point!)

Long session times

A MatsSocketSession is either established, deregistered or closed. Deregister is what happens when the user looses connection. When a session is deregistered, it only uses resources on the backing database. This makes it possible to use long timeouts if this is desired, as in days - to let a user keep his state even through a long connection drop, e.g. a flight.

Instrumentable

There are a number of callbacks and event listening posts on both the client side, and the server side.

Client

  • ReceivedEvents: For each issuing of a message (Sends or Requests), you may be informed about your message being received (but not yet processed) by the other side. May be used for user information, e.g. when a button is clicked, it may transition to some "actually being processed" state. Also, the server may NACK your message.

  • matsSocket.addSessionClosedEventListener(..): Notifies about closing of the session, which might happen if e.g. the server gets uncorrectable problems with its backing store. It is suggested that you do register such a listener, as the session is then gone and you would need to "reboot" the application.

  • matsSocket.connected, matsSocket.state and matsSocket.addConnectionEventListener(..): Returns the current state of the underlying WebSocket, and the listener informs about the "state machine transitions" that the MatsSocket instance goes through, including lost connection and attempts to reconnect - enabling user feedback in the application (e.g. "connection lost, reconnecting in 5, 4, 3..")

  • matsSocket.initiations and matsSocket.addInitiationProcessedEventListener(..): Every time a request or send is finished, an InitiationProcessedEvent is created which includes timing information. You may get the latest matsSocket.numberOfInitiationsKept (default 10) of these, or register an event listener. May be used for an app-internal "debug monitor" to survey the traffic the app performs, with timings - or submitting stats to your backend for inspection.

  • matsSocket.pings and matsSocket.addPingPongListener(..): MatsSocket issues pings and receives pongs, which includes timings. You may get the latest 100 ping-pongs, or listen in on these, possibly submitting stats to your backend for inspection.

  • matsSocket.addErrorEventListener(..): The MatsSocket may encounter different error conditions, which is reported here. You may add a listener that sends these back to your server by out-of-bands means (e.g. a HTTP POST), so that you can inspect the health of your application's MatsSockets usage.

Server

  • server.get[Active|Live]MatsSocketSessions(): Returns the current set of MatsSocketSessions.
  • server.getMatsSocketEndpoints(): Introspection of the set up endpoints.
  • server.addSessionEstablishedEventListener(..) and server.addSessionRemovedEventListener(..): Listeners will be invoked when sessions are established (new or reconnect), deregistered, closed and timed out. Due to the nature of how MatsSocketSessions work wrt. reconnects, you may get multiple back-and-forths between states.
  • server.addMessageEventListener(..): Events issued both for client-to-server and server-to-client messages, so that you may create statistics and metrics on the usage and processing of all communications.

Developer friendly

MatsSockets is made to be simple to use - at least once you're done with setting up the authentication part! In addition to both a simple but rich API to actually do communications, and automatic handling of the connection lifecycle and reconnects, and the instrumenting options mentioned above, it also has a couple of specific features that aid development and debugging:

  • TraceId: This is a mandatory parameter for all things Mats - and MatsSocket. This ensures that you can trace a message all the way from the client, through any Mats flows, and back to the client. Distributed logging becomes amazing.
  • AppName and AppVersion: Mandatory parameters for creating a MatsSocket. This might be needed for server-to-client sends, as the client Terminator "NewProducts.personalized" is only available for the "ProductGuide" application, and was only added at version 1.2.5. Also, it aids debugging if you have different apps and versions of those apps out in the wild - these parameters are included in the logging on the server side, and available in the server.get[..]MatsSocketSessions() calls.
  • Debugging fields: The MatsSocket system has built-in optional debugging fields on each message, which explains key datapoints wrt. how the message was processed, and timings.

matssocket's People

Contributors

stolsvik avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

mortentv

matssocket's Issues

If a MatsSocket only performs subscribe, no other message sending, then sub was never sent to server

The problem was that the "SUB" message was not considered "information bearing", and thus the MatsSocket would never open and connect.

This was probably not a big problem in actual usage scenarios as one probably very seldom would only subscribe, but also at least send a "what is the latest news"-type message. However, it nevertheless it caused some developers to use a considerable time on debugging why they never got any published messages from the server.

Server debug flags should be explicitly set

Currently, the server uses whatever was last sent over (with a default of zero on both client and server). This is pretty annoying if you want debug on a particular request: After this request, the server will use this for any server initiated messages (server-to-client send and request). Thus, you will always have to basically send a dummy "reset" message afterwards. This effectively makes the ability to set debug options in the config object meaningless..

It would have been better if this was more explicit. One thought was to just use whatever was set on MatsSocket.debug at startup to be the server-to-client debug flags. However, this makes it impossible to set any other "in runtime", i.e. if you have an app, and want to turn on the server-side debug flags (using some admin config screen), you'd have to restart the MatsSocket.

It would be better if this was an explicit setting, preferably so that when setting it on the client, it was immediately sent over.

I am thinking a property with setter action. And renaming "AUTH"-type messages to "CONF" or something, so that one can use it for multiple such client-server and server-client comms which are only for the messaging layer, not user comms.

MatsSocket: Make "self-healer".

If we lose DB-connection, we cannot forward messages anymore. The "MessageToWebSocketForwarder" will then bail, and hope for the self-healer to pick up when the DB comes back up, i.e. check if there are outstanding messages for live sessions (the self-healer handles sessions on its own node), and forward them.

Think together with centiservice/mats#115.

MatsSocket: Handle RETRY in MatsSocket.js

... only relevant for Client MatsSocket.js when handling messages from Server, that is Server says "RETRY" to Client. The other way is not relevant, as Client won't ever say "RETRY" to Server - so there is no test other way.

MatsSocket: Enable to provide state object from handleIncoming to adaptReply

A situation came up where it would be nice to have a state object traverse from the handleIncoming to adaptReply. This is already possible by using traceProperties, but it came up quite fast when developers started using this, so add it directly in the API. Overload forward methods to also have a state object, which is simply added to the existing state object that the library uses.

NOTE! Must think about how this ends up with SpringConfig "@Class"-variant. One might want to have the handleIncoming, the mats endpoint and its stages, and the replyAdapter, all share the same state object: An instance of the class it is defined on.

MatsSocket: "pub/sub"-style unreliable topic-based messaging Server-to-Client

For certain usage scenarios, it would probably be nice if the client tells the server which "channels" it wants to listen to, and the server keeps a tab of which clients wants which channel messages. This would only be in memory, and purely a best-effort style delivery.

Backend, it would use a Mats topicTerminator. It would keep a Map<String:channelName, Collection>.

The client "registers" such listeners, and each time it connects, it would forward over this list. The server then registers them in - and when it gets such a message on the relevant topicTerminator, it'd simply iterate over them (can do this multi threaded) and forward over the message.

If the client is not hooked up (yet) when the message goes, the he does not get it.

If the server reboots, all clients will have to reconnect. In a multi node setup, the new connection would probably come in a different node that still is connected.

To keep a bit of "reliability" here, the server could keep a list of the 5 minutes worth of messages sent for each channel. The server-side initiator would tag the message with its timestamp, and a random unique Id. When the client reconnects, it would send the timestamp of the last message received. It would then get all messages from-and-including that timestamp. If it cared about double deliveries, it would keep an "inbox" of messageIds that it has received, and filter based on that. (This list could be culled by timestamp).

Since it is the different nodes that will tag the messages with timestamp (such that they are equal when in the lists of the websocket-holding nodes), you can get "last timestamp" jumping back and forth. This must be taken into consideration when doing reconnect-resends. Maybe also store the received timestamp on the websocket-holding nodes - then when the client says "since this timestamp", the server would look up into its list, check all messages that has that "initiatedTimestamp" or later, then of these find the earliest "receivedTimestamp", and then use this earliest receveidTimestamp to filter which messages to resend over.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.