Scalable Messaging Protocol

When it comes to writing scalable applications you need a front-end which packages up a message, which is sent along into a queue, and on the other side controllers read from the queue, perform work, and optionally return a message. (I like to use ‘nats’ for this.)

Common front-ends use a REST api, websocket, or grpc.  They can all be used if desired, packaging up the same message which is passed along.

Along with a “request”, there is also optionally a “session”, as well as an authentication middle layer.

Here’s what I’ve standardized on:

  • user allowed multiple websockets, but the websocket name must be unique
  • if the websocket name already exists then drop the existing websocket and keep the new one
  • multiple sessions are allowed per websocket, but the session name must be unique
  • if websocket w/ session name already exists then the session create fails
  • messages received will have a utc timestamp in milliseconds, this is the timestamp of the message, data within the message may have its own timestamp; everything is sent as soon as available, this may result in messages being received out of order
  • all messages expect a session to be specified, defaulting to empty “”
  • all message replies will include the session tacked on to the websocket name, e.g.: <demo>:<session>
  • There are 5 types of messages:
    • send, auth:
      • sent as first message when using websocket
      • sent with every message via header middle layer when using rest api
    • send:
      • send request into queue, there is only one ‘request’ type message, the request is unique due to the ‘topic’ field which should include a version number and name.verb e.g.: “v1.session.create”, this allows for protocol improvements later on optionally allowing for an older topic to continue to be used while being replaced by a newer version (without requiring some of the complexities of scale-to-zero)
      • ‘id’ is optional, but is recommended, it will be included as ‘rid’ in the reply message; without an id there will be no way of matching an ‘rid’ with the original message when there are lots of messages
    • recv, reply:
      • includes an ‘rid’ if an ‘id’ was specified, this message is a reply to a request
    • recv, event:
      • does not include an ‘rid’, this is a message which is in response to an event, can be received at any time
    • recv, system:
      • does not include an ‘rid’, this is a message which is in response to a system event, which is rarely received, could be something like ‘os is shutting down’ or ‘maintenance will be performed tonight’

Notes:

  • Careful to make sure only one controller processes a message at a time, in general, and handle a timeout/retry in case the controller crashes or fails for some reason. (‘nats’ makes implementing these requirements easy.)

Examples of the 5 types of messages:

send, auth:
{
  auth: {
    type: "bearer",
    token: "",
  },
  name: ""
}

send: {
  id: "123",
  session: "",  // if session in use
  topic: "",
  body: {}
}

recv, reply:
{
  rid: 123,
  kind: "reply"
  session: "",  // if session in use
  topic: "",
  utc: z,
  body: {},
  status: {
    code: 200,
    detail: "",
    error: "",
    where: ""
  }
}

recv, event:
{
  kind: "event"
  session: "",  // if session in use
  topic: "",
  utc: z,
  body: {},
}

recv, system:
{
  kind: "system",
  topic: "",
  utc: z,
}

Standard messages:

// send, websocket authenticate
{
  auth: {
    type: "bearer",
    token: "<redacted>",
  },
  name: "demo"
}

// send, create two sessions on the websocket
{
  id: "1",
  topic: "v1.session.create",
  body: {"name":"aaa"}
}
{
  id: "2",
  topic: "v1.session.create",
  body: {"name":"bbb"}
}

// send, message from one session to another
{
  id: "3",
  session: "aaa",
  topic: "v1.message.create",
  body: {"to":"websocket:demo:bbb","message":"message to a session on the specified websocket"}
}
// send, message from one session to all sessions on the specified websocket
{
  id: "4",
  session: "aaa",
  topic: "v1.message.create",
  body: {"to":"websocket:demo","message":"message to all sessions on the specified websocket"}
}
// send, message from one session to specified user across all websockets
{
  id: "5",
  session: "aaa",
  topic: "v1.message.create",
  body: {"to":"user:tloyd","message":"message to user on all websockets where user has authenticated"}
}

Standard replies:

// recv, reply:
{
  rid: 4,
  kind: "reply"
  session: "aaa",
  topic: "v1.message.create",
  utc: "<millisecond_timestamp_int64>",
  body: {},
  status: {
    code: 200,
    detail: "",
    error: "",
    where: ""
  }
}

Standard events:

// recv, event:
{
  kind: "event"
  session: "demo:bbb",  // if session in use
  topic: "v1.message.create",
  utc: "<millisecond_timestamp_int64>",
  body: {"from":"websocket:demo:aaa","message":"message to all sessions on the specified websocket"},
  status: {
    code: 200,
    detail: "",
    error: "",
    where: ""
  }
}

Standard systems:

recv, system:
{
  kind: "system",
  topic: "v1.os.restart",
  utc: <millisecond_timestamp_int64>,
}