Skip to content

Instantly share code, notes, and snippets.

@wmertens
Last active February 8, 2022 22:46
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wmertens/a408e15a08301081ebad to your computer and use it in GitHub Desktop.
Save wmertens/a408e15a08301081ebad to your computer and use it in GitHub Desktop.
making an awesome server with the redux model, Work In Progress

Thought experiment: Redux-like stateless server

Description

We describe a model for client-server processing where the Redux model is used to minimize stateful code. This should allow live-reloading server code, and make it possible to share code (e.g. optimistic updating) between client and server.

Dramatis Personae

  • Assume a server consisting of multiple worker processes that do not share memory and may be running on multiple hosts.
    • Workers have middleware, root reducers and an app state object
    • Workers can be dynamically added and removed
  • Assume multiple independent clients, using Redux
    • They use middleware to send messages over some bidirectional link
  • Assume a message bus with multiple queues
    • Assumed resilient
    • Responsible for authentication
    • Allows delayed delivery and putback
  • Communication with the message bus happens over an unspecified channel.
    • Long polling, websockets, even files or function calls, all these are possible.
  • A message is somewhat equivalent to an action in Redux, but is called an event once transformed to reducer-level.

Communication

  • Communication happens asynchronously, through a message passing model:
    • Messages are plain JS objects and are sent using non-specified serialization
      • Required attribute: type field (like Redux Actions)
      • Some possible calculated fields: sender (client id), ts (timestamp), v (global event number)
    • Messages from clients are put on a server queue for processing
    • Messages to clients are put on per-client reply queues
    • Sending a message is called "dispatch"
  • Only authenticated senders can send messages
    • Authentication could simply be "has a TCP connection ID", so the client can get replies
    • Thus, messages can be queued client-side until authentication is done, without retry logic in event handlers
  • We assume a shared knowledge between clients and server of type constants and their meaning
    • Thus, messages become an API
    • Clients can have older knowledge than server, thus:
      • type constants should not change over time
      • their meaning should not change over time
      • -> if you need to change or extend the API, use a new type constant and maintain old processing code as long as possible
  • To send a message from the client, a full-stack-redux middleware sends the message to the server as a side-effect, and dispatches incoming messages as actions.
    • The server message type should be moved to another field to prevent action constant clashes so perhaps it is better to name the message type something else. TBD.
  • App state synchronization between workers
    • App state is e.g. client session data, ongoing database actions, locks etc
    • During operation, all root reducers get the same messages and can therefore apply the same actions
    • At startup, the new worker needs the app state. Options:
      • Dispatch a message to the server asking for app state copy, queue incoming events and once copy received, apply queued events with higher v numbers.
      • Store an event log and snapshots and rebuild based on that
      • Don't keep anything in app state that cannot be retrieved from the database

Message handling

  • Server-bound messages:
    • Pass through workers middleware chain
      • middleware can amend/discard them
      • can result in dispatches to client or server, via ActionCreators
      • Side-effects happen here, asynchronously, with local state
      • Examples: Authorization, database reading and writing, Promise handling, logging, ...
    • After the middleware, messages describe a change to app state and are thus called 'events'. Redux keeps calling them 'actions'.
    • The events are passed to the root reducers of all workers
      • This results in shared state for the workers
      • The event passing could happen with a bus channel for event broadcasts, to be handled exactly like messages.
      • In order to minimize race conditions, it would be best to prioritize events over messages, but eliminating them is not possible
    • When worker state updates in-process listeners are notified
  • Client-bound messages:
    • Are retrieved by the client from the bus (e.g. long polling, websocket, ...)
    • TODO Redux handling

Failure Modes

Non-exhaustive list :) P: Problem, R: Resolution

Server

  • Worker dies while:
    • Idle
      • R No problem
    • Middleware processing
      • P client message not processed
        • R Acknowledge processed messages and let bus retry unhandled processes after a timeout
      • P Ongoing side effects are lost
        • R Allow replaying side effect by dispatching delayed message that verifies completion
    • Reducer processing
      • R no problem, on startup copy app state from other worker(s)
  • Worker is very slow (instead of killed)
    • P Bus will retry message resulting in double processing
      • R Watchdogs on worker to kill worker before bus timeout, and on message bus to break all communication with worker after timeout
  • Worker loses network access
    • P Same as being very slow, but hopefully database connectivity etc also breaks.
      • R Watchdogs
      • R not retrying external side-effects and instead killing the worker and letting another worker retry
  • P App state is too big
    • R Move app state to database
    • R Shard app state, using sharded-by-client server queues and thus multiple unrelated servers that talk to the same databases
  • All workers die
    • P app state is lost
      • R make a log worker that doesn't handle actions and stores app state snapshots and the log of events since the snapshot. At worker startup, read and reduce the log.
      • R or store all important data in a database and re-create the rest, making server state mostly a cache of the database
  • Different versions of workers
    • P app state can differ for same messages
      • R version app state copies and only send app state from workers with same version
    • P incoming message may be meant for newer version of worker
      • R allow worker to put back message in queue or to peek messages
    • P

Client

  • Race conditions between optimistically-updating client events and server roundtrip
  • P Multiple client-side events during server-roundtrip result in unexpected client state
    • R Don't allow updating components that are expecting a server update. This makes the app feel sluggish and unresponsive.
    • R For each server call, keep track of the optimistic response and don't emit the server result if it is the same
      • If the response differs, it allows you to notify the user of the failure

Message Bus

Optimizations

TODO flesh these out

  • The server could be a single process:

    • Pro:
      • Simple
      • No race conditions
    • at the cost of resiliency and TODO
  • The message bus could be in the same process as the worker

    • Pro:
      • Simple
      • Fast
    • Con:
      • Home-built implementation may have bugs
      • Harder to send events to all workers
  • Don't kill worker on side-effect failure

    • Pro:
      • Much less costly
    • Con:
      • Teardown of async .... TODO
  • Event broadcasts could bypass the middleware chain

  • Use integers instead of string constants

  • Batch messages

Examples

Requesting data needed for render

Sending a form

Uploading a file

Ecosystem

The currently dominating HTTP REST+CRUD model seems not well suited for stateless server processing. The expectation that clients get an answer in the same HTTP request skews handling to stateful processing.

If we want to allow REST access for non-Redux clients, we need to match the HTTP 1.0 model to the message bus model. The only way to do this is to have a server that converts REST calls so messages, and waits for one or more responses before sending them as the response.

However, since the Redux model is stateless, there is no obvious relation between request and response, so unless messages to the client are marked by the middleware as responses with ids to match and a "more following" boolean, the bridging server will need to have application-specific knowledge.

Conclusion

The server Redux model is very similar to the client model except for the asynchronous messaging, failure handling and the direct root-reducer injection of events from other workers. These extra requirements unfortunately make things more complex. The single worker optimization simplifies things again.

This model seems like a good way to limit side-effect code and thus make more server-side code testable (and even hot-replaceable!).

The log of server actions can be useful for debugging and replaying. A server devtools app (as a privileged client) seems plausible.

There are some stringent requirements:

  • The message bus has to be reliable and handle authentication, allow delaying message delivery and message putback
  • The server middleware code must consider replaying side effects and race conditions
@JustinLivi
Copy link

I'm curious, have you written any code based on this inquiry? I'm exploring a similar concept.

@justingreenberg
Copy link

i'm not sure if you are familiar with AWS, but i worked on a project with kinesis/sqs/lambda recently (using https://serverless.com/) and the thought of using redux to create a distributed, serverless CQRS system was on my mind non stop... admittedly, the tech is not quite there yet (lambda cold starts are a deal breaker for me right now), but it's getting very very close.

also maybe worth checking out:
https://github.com/elierotenberg/nexus-flux
http://martinfowler.com/bliki/CQRS.html
https://en.wikipedia.org/wiki/Command%E2%80%93query_separation

@wmertens
Copy link
Author

Note that github doesn't send notifications for gists, so you can email me at Wout.Mertens@gmail.com or @wmertens on twitter to discuss this further.

I haven't yet created a server based on this; it is very much on my mind still though.

@wmertens
Copy link
Author

wmertens commented Feb 8, 2022

@JustinLivi BTW I did implement this, in https://github.com/StratoKit/strato-db :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment