Shared state in a web server

I'm considering writing something with the structure of a multiplayer game. Server will be a network-accessible Linux box. All I can assume about the clients is that they can use reasonably modern web browsers despite using disparate operating systems.
So I want to write the game in whatever language is comfortable for the application (for the moment I chose Racket) and do interactions via http messages.
It does mean that the multiple users have their own connexions to the server, and that the game itself should act like a single process, with shared state, handling all the player interactions.
The obvious mechanisms I've found so far are to write my own specialized webserver, based on the Racket web server, or to do the game using Racket's CGI with the web server (lighttpd) I already use for everything else.
In either case I need to get user actions to be processed by a single process that maintains shared state.

Is this straightforward if I use the Racket web-server? Will all requests from whatever user naturally get to the same process? (with, I hope some kind of indication of which user initiated it?)

And if I use CGI instead, is there a way to ensure that all the requests get to the same CGI process, and so use the same state?

Or am I doomed to store game state in the file system and carefully manage updates and mutexes?

Or is there some entirely different way of handling all this?

-- hendrik

2 Likes

I'm not sure if this answers your question, but:

Iā€™ll admit I donā€™t know this stuff.

I believe the web tutorial ā€˜listit2ā€™ does cover the situation where the shared state is in a database.

Link to source (shows preview if viewed on discourse):

Iā€™m guessing shared state is managed by the database in the case of listit2.

I would also like to know how to manage in-memory shared state, but the web-server libraries donā€™t seem to mention it so Iā€™m guessing the concurrency mechanisms are the way forward?

Listit2:

This will be easy if you use the Racket web server and hard if you use CGI. (I say this as someone who wrote a moderately complex application with net/cgi, because CGI was familiar, before learning to use web-server.)

The CGI standard specifies that a new Racket process is started for every request, with information about the request passed using environment variables. The web-server library is instead based on a single long-running server process that handles requests concurrently using Racket-level threads.

Of course, SQLite is an alternative to using the file system directly that might be useful in either case to get ACID transactions. But even so, I think you'll have an easier time with web-server in the long run.

Also, while the Racket web server can be useful even if you don't use its continuation functionality, they are especially convenient for a scenario when the generated URLs are internal to your application, not something users need to type manually.

1 Like

So if I use the web-server I'd have to use explicit synchronisation to separate http requests. Likely by using semaphores I guess I can do that.

And with CGI it would be a much heavier business synchronising what I presume are OS-level processes.

Too bad it'll mean I have two web-servers running. Oh, well.

-- hendrik

It depends on what you mean exactly by "to separate http requests", but, in general, no.

The simplest way to think about the web server is that a server is a (potentially impure) function from request values to response values, i.e. (-> request? response?). So, in your code, you always know what request you're handling.

It is true that the Racket web server may handle multiple requests concurrently. This is also true of every CGI server I know of. In both cases, if your application's logic requires that some sequence of multiple operations be atomic (like an ACID database transaction), you will need to do something about that.

With a CGI server, though, you get the OS memory model for processes, which means you need a lot of explicit synchronization. With the Racket web server, you get Racket threads, which give you sequential consistency, a very strong memory model. Per the reference:

Unless otherwise noted, all constant-time procedures and operations provided by Racket are thread-safe in the sense that they are atomic: they happen as a single evaluation step. For example, set! assigns to a variable as an atomic action with respect to all threads, so that no thread can see a ā€œhalf-assignedā€ variable. Similarly, vector-set! assigns to a vector atomically. Note that the evaluation of a set! expression with its subexpression is not necessarily atomic, because evaluating the subexpression involves a separate step of evaluation. Only the assignment action itself (which takes after the subexpression is evaluated to obtain a value) is atomic. Similarly, a procedure application can involve multiple steps that are not atomic, even if the procedure itself performs an atomic action.

The hash-set! procedure is not atomic, but the table is protected by a lock; see Hash Tables for more information.

I would describe all of these and more as implicit rather than explicit synchronization.

When you do need more explicit synchronization, box-cas! is another option. For stateless #lang web-server servlets, there is also the web-server/lang/file-box library. Most generally, you have the whole event system based on Concurrent ML, which is very powerful and a delight to use, though it does take some practice to learn.

There are a few options:

  1. You could have lighttpd proxy applicable requests to a Racket process only listening on localhost.
  2. You can have Racket listen on some non-default port.
  3. Racket can also serve static files and the like, so you could probably completely replace lighttpd with Racket.

I have used all of these approaches at various times, but it is definitely most convenient to just use Racket for everything, and that's what I've done for the largest projects I've built.

In any case, you will get better performance from the Racket web server than CGI because you will avoid the overhead of starting a new Racket process for every request. Bogdan gets very impressive benchmarks for the Racket web server.

2 Likes

@hendrikboom3 Regardless of how you handle shared state during gameplay, some persistent storage might still be important if you want to keep the game state between server restarts (either because you want to deploy a new version of your software or because of restarting the computer/VM/container).

In this case, you could still use the inter-thread communication of the Racket web server, but you'd need to save the state explicitly (by a single thread) before shutdown.

Somewhat off-topic but maybe still interesting:

If you want to change the data format or schema (because of changes in your software) at some point, you'd also need a strategy for migrating/converting this persistent data. That's a relatively complex topic in itself. I tried to find a web resource on this topic, but almost all I found were vague descriptions from companies selling their migration software or services. The most useful link I found was this Quora question. If someone knows another link I'd be very interested.

I recommend migrating the data with an extra program/script while the application is shut down instead of trying to do the migration on the fly during application start.

One detail I find useful is to have a format version number in the persistent data and a version number for the expected data in the application. (For an SQL database, this can be a version table with a single version row.) That way, the application server can check the data to load and refuse to start up if the data has a different version than the one in the application software.

I'm considering writing something with the structure of a multiplayer game.

I fell over this description of one way to put a client-server together.
It's well-written. It may or may not be overkill, but regardless it gives
something to think about.

https://www.gabrielgambetta.com/client-server-game-architecture.html

David Vanderson has written a multi-player game using Racket.

His RacketCon talk is online:
https://www.youtube.com/watch?v=Fuz0BtltU1g
The game Warp:
GitHub - david-vanderson/warp: coop networked game in Racket
The slides:
https://con.racket-lang.org/2014/vanderson.pdf

1 Like

Apparently my reply was cutoff. Here's the rest, which by now is probably well covered by the other responses (which I haven't yet read).

From my experience with Racket's web-server, each request gets its own (Racket, therefore in-process (?)) thread to handle responding. Any state that is external to the response function is thus effectively shared by all request handler threads. You'll still have to take some care with data structures that aren't thread-safe, if you use any.

See also serializable-struct and it's /version counterpart as one way to serialize and deserialize versioned data.

1 Like

This partially depends on what you mean by "explicitly", but it doesn't have to be very explicit. In particular, I'd recommend against depending on a single thread doing something special before shutdown: if you want persistent state, you probably also want it to persist across abnormal shutdowns (e.g. power failure).

For example, a file-box saves to disk on every file-box-set!. (Note that it doesn't do any locking to handle concurrent writes, so it might be just as easy to re-implement the idea as to use the library directly.) If you represent your state using a serializable-struct, you can later support migration using serializable-struct/versions.

1 Like

I was thinking of the main thread handling incoming signals and store the state.

Yes, that would be desirable, but I didn't want to go into this much detail. :slight_smile: I think if you want to do this in a robust way, you probably want some transaction support to avoid saving incomplete or inconsistent state. Then, you could also discuss how to deal with storage failures, backups etc. etc.

1 Like