<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.gordo.life/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.gordo.life/" rel="alternate" type="text/html" /><updated>2026-04-15T22:13:30+00:00</updated><id>https://blog.gordo.life/feed.xml</id><title type="html">Gordon’s Blog</title><subtitle>I fanny aboot wae code and hings.</subtitle><entry><title type="html">Writing a Turn Based Game Websocket Server in Haskell</title><link href="https://blog.gordo.life/2026/04/12/haskell-websockets.html" rel="alternate" type="text/html" title="Writing a Turn Based Game Websocket Server in Haskell" /><published>2026-04-12T00:00:00+00:00</published><updated>2026-04-12T00:00:00+00:00</updated><id>https://blog.gordo.life/2026/04/12/haskell-websockets</id><content type="html" xml:base="https://blog.gordo.life/2026/04/12/haskell-websockets.html"><![CDATA[<p>I’d like to share a wee pattern I’ve found useful for writing simple WebSocket apps in Haskell - specifically for my side project <a href="https://wordify.gordo.life">wordify</a> - an <a href="https://github.com/Happy0/wordify-webapp/">open source</a> ~ multiplayer crossword board game ~ .</p>

<p>I have in mind websocket applications where there’s some sort of ‘resource’ that is shared between multiple websocket clients that need to be kept updated as the state changes such as:</p>

<ul>
  <li>A ‘Game’ resource for a turn based game such as a Chess or Noughts and Crosses (that’s “tic-tac-toe” to some of you) where we keep track of the game state.</li>
  <li>A ‘Chat’ resource where we keep track of things like the users in the room and whether they are currently online, etc</li>
</ul>

<p>For simplicity, we’ll implement a multiplayer server for Noughts and Crosses (as we’ll call it in a parochially defiant act of patriotism against the US-centric internet :P) to illustrate the pattern.</p>

<p>I’ll be exploring a pattern where we maintain a Map of all the Noughts and Crosses games we have in progress safely such that all the connected websockets see the same game updates.</p>

<h2 id="the-core-game-server">The Core Game Server</h2>

<p>There’s a few different options for writing web apps in haskell. Yesod is the one I’m most familiar with. The canonical examples on Yesod WebSockets illustate how to use a shared broadcast <a href="https://hackage.haskell.org/package/stm-2.5.3.1/docs/Control-Concurrent-STM-TChan.html">TChan</a> as the basis for a chat server (blog <a href="https://www.yesodweb.com/blog/2014/03/wai-yesod-websockets">here</a>, code example <a href="https://github.com/yesodweb/yesod/blob/master/yesod-websockets/chat.hs">here</a>.))</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">data</span> <span class="kt">App</span> <span class="o">=</span> <span class="kt">App</span> <span class="p">(</span><span class="kt">TChan</span> <span class="kt">Text</span><span class="p">)</span>

<span class="n">chatApp</span> <span class="o">::</span> <span class="kt">WebSocketsT</span> <span class="kt">Handler</span> <span class="nb">()</span>
<span class="n">chatApp</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="n">sendTextData</span> <span class="p">(</span><span class="s">"Welcome to the chat server, please enter your name."</span> <span class="o">::</span> <span class="kt">Text</span><span class="p">)</span>
    <span class="n">name</span> <span class="o">&lt;-</span> <span class="n">receiveData</span>
    <span class="n">sendTextData</span> <span class="o">$</span> <span class="s">"Welcome, "</span> <span class="o">&lt;&gt;</span> <span class="n">name</span>
    <span class="kt">App</span> <span class="n">writeChan</span> <span class="o">&lt;-</span> <span class="n">getYesod</span>
    <span class="n">readChan</span> <span class="o">&lt;-</span> <span class="n">atomically</span> <span class="o">$</span> <span class="kr">do</span>
        <span class="n">writeTChan</span> <span class="n">writeChan</span> <span class="o">$</span> <span class="n">name</span> <span class="o">&lt;&gt;</span> <span class="s">" has joined the chat"</span>
        <span class="n">dupTChan</span> <span class="n">writeChan</span>
    <span class="p">(</span><span class="n">e</span> <span class="o">::</span> <span class="kt">Either</span> <span class="kt">SomeException</span> <span class="nb">()</span><span class="p">)</span> <span class="o">&lt;-</span> <span class="n">try</span> <span class="o">$</span> <span class="n">race_</span>
        <span class="p">(</span><span class="n">forever</span> <span class="o">$</span> <span class="n">atomically</span> <span class="p">(</span><span class="n">readTChan</span> <span class="n">readChan</span><span class="p">)</span> <span class="o">&gt;&gt;=</span> <span class="n">sendTextData</span><span class="p">)</span>
        <span class="p">(</span><span class="n">sourceWS</span> <span class="o">$$</span> <span class="n">mapM_C</span> <span class="p">(</span><span class="nf">\</span><span class="n">msg</span> <span class="o">-&gt;</span>
            <span class="n">atomically</span> <span class="o">$</span> <span class="n">writeTChan</span> <span class="n">writeChan</span> <span class="o">$</span> <span class="n">name</span> <span class="o">&lt;&gt;</span> <span class="s">": "</span> <span class="o">&lt;&gt;</span> <span class="n">msg</span><span class="p">))</span>

    <span class="n">atomically</span> <span class="o">$</span> <span class="kr">case</span> <span class="n">e</span> <span class="kr">of</span>
        <span class="kt">Left</span> <span class="kr">_</span> <span class="o">-&gt;</span> <span class="n">writeTChan</span> <span class="n">writeChan</span> <span class="o">$</span> <span class="n">name</span> <span class="o">&lt;&gt;</span> <span class="s">" has left the chat"</span>
        <span class="kt">Right</span> <span class="nb">()</span> <span class="o">-&gt;</span> <span class="n">return</span> <span class="nb">()</span>

<span class="n">main</span> <span class="o">::</span> <span class="kt">IO</span> <span class="nb">()</span>
<span class="n">main</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="n">chan</span> <span class="o">&lt;-</span> <span class="n">atomically</span> <span class="n">newBroadcastTChan</span>
    <span class="n">warp</span> <span class="mi">3000</span> <span class="o">$</span> <span class="kt">App</span> <span class="n">chan</span>
</code></pre></div></div>

<p>In these examples, a global channel is accessed via <code class="language-plaintext highlighter-rouge">getYesod</code> which holds the channel state. It is duplicated by each websocket to subscribe to new messages in the chatroom.</p>

<p>This shared TChan broadcast channel is a useful abstraction which will form part of our implementation but unlike the materials above we’ll need to maintain multiple gmaes at once with their own channel.</p>

<p>A naive approach might be to plonk our games in a <code class="language-plaintext highlighter-rouge">Map GameId GameState</code> stored in a <a href="https://hackage.haskell.org/package/stm-2.4.2/docs/Control-Concurrent-STM-TVar.html">TVar</a>. A TVar is a mutable variable that can be updated by multiple threads (or websocket handlers) and can be updated transactionally along with other TVars (which we’ll get into later.)</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">import</span> <span class="nn">Control.Concurrent.STM</span> <span class="p">(</span><span class="nf">atomically</span><span class="p">)</span>
<span class="kr">import</span> <span class="nn">Control.Concurrent.STM.TVar</span> <span class="p">(</span><span class="kt">TVar</span><span class="p">,</span> <span class="nf">newTVarIO</span><span class="p">)</span>
<span class="kr">import</span> <span class="nn">Data.Map.Strict</span> <span class="p">(</span><span class="kt">Map</span><span class="p">)</span>
<span class="kr">import</span> <span class="k">qualified</span> <span class="nn">Data.Map.Strict</span> <span class="k">as</span> <span class="n">Map</span>

<span class="kr">data</span> <span class="kt">GameMap</span> <span class="o">=</span> <span class="kt">TVar</span> <span class="p">(</span><span class="kt">Map</span> <span class="kt">GameId</span> <span class="kt">GameState</span><span class="p">)</span>
<span class="kr">data</span> <span class="kt">App</span> <span class="o">=</span> <span class="kt">App</span> <span class="kt">GameMap</span>

<span class="n">main</span> <span class="o">::</span> <span class="kt">IO</span> <span class="nb">()</span>
<span class="n">main</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="n">gameMap</span> <span class="o">&lt;-</span> <span class="n">newTVarIO</span> <span class="kt">Map</span><span class="o">.</span><span class="n">empty</span>
    <span class="n">warp</span> <span class="mi">3000</span> <span class="o">$</span> <span class="kt">App</span> <span class="n">gameMap</span>
</code></pre></div></div>

<p>We’ll also set up our <code class="language-plaintext highlighter-rouge">GameState</code> types including our board state, our two players, channel to broadcast move updates, etc:</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">type</span> <span class="kt">GameId</span> <span class="o">=</span> <span class="kt">T</span><span class="o">.</span><span class="kt">Text</span>
<span class="kr">data</span> <span class="kt">Player</span> <span class="o">=</span> <span class="kt">Player</span> <span class="p">{</span> <span class="n">username</span> <span class="o">::</span><span class="kt">T</span><span class="o">.</span><span class="kt">Text</span><span class="p">,</span> <span class="n">userId</span><span class="o">::</span> <span class="kt">T</span><span class="o">.</span><span class="kt">Text</span> <span class="p">}</span>
<span class="kr">type</span> <span class="kt">ConnectionCount</span> <span class="o">=</span> <span class="kt">Int</span>

<span class="c1">-- A board square is either empty, an X or an O</span>
<span class="kr">data</span> <span class="kt">Square</span> <span class="o">=</span> <span class="kt">Empty</span> <span class="o">|</span> <span class="kt">X</span> <span class="o">|</span> <span class="kt">O</span> <span class="kr">deriving</span> <span class="p">(</span><span class="kt">Eq</span><span class="p">)</span>

<span class="kr">instance</span> <span class="kt">ToJSON</span> <span class="kt">Square</span> <span class="kr">where</span>
    <span class="n">toJSON</span> <span class="kt">Empty</span> <span class="o">=</span> <span class="n">toJSON</span> <span class="p">(</span><span class="s">""</span> <span class="o">::</span> <span class="kt">Text</span><span class="p">)</span>
    <span class="n">toJSON</span> <span class="kt">X</span> <span class="o">=</span> <span class="n">toJSON</span> <span class="p">(</span><span class="s">"X"</span> <span class="o">::</span> <span class="kt">Text</span><span class="p">)</span>
    <span class="n">toJSON</span> <span class="kt">O</span> <span class="o">=</span> <span class="n">toJSON</span> <span class="p">(</span><span class="s">"O"</span> <span class="o">::</span> <span class="kt">Text</span><span class="p">)</span>

<span class="c1">-- For simplicity, a board is just a list of 9 squares - we flatten it out to a 1 dimensional representation</span>
<span class="kr">type</span> <span class="kt">Board</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Square</span><span class="p">]</span> 

<span class="kr">data</span> <span class="kt">GameUpdate</span> <span class="o">=</span> <span class="kt">NewBoard</span> <span class="kt">Board</span>

<span class="kr">data</span> <span class="kt">GameState</span> <span class="o">=</span> <span class="kt">GameState</span> <span class="p">{</span>
    <span class="n">board</span><span class="o">::</span> <span class="kt">TVar</span> <span class="kt">Board</span><span class="p">,</span>
    <span class="n">player1</span> <span class="o">::</span> <span class="kt">Player</span><span class="p">,</span>
    <span class="n">player2</span> <span class="o">::</span> <span class="kt">Player</span><span class="p">,</span>
    <span class="c1">-- The channel will be updated each time a move is made to notify listening websockets</span>
    <span class="n">gameChan</span> <span class="o">::</span> <span class="kt">TChan</span> <span class="kt">GameUpdate</span><span class="p">,</span>

    <span class="c1">-- The number of connected websockets to this game</span>
    <span class="n">gameConnections</span> <span class="o">::</span> <span class="kt">TVar</span> <span class="kt">ConnectionCount</span>
<span class="p">}</span>

<span class="kr">data</span> <span class="kt">GameMap</span> <span class="o">=</span> <span class="kt">TVar</span> <span class="p">(</span><span class="kt">Map</span> <span class="kt">GameId</span> <span class="kt">GameState</span><span class="p">)</span>
</code></pre></div></div>

<p>Since we’re using a TVar each WebSocket thread can load a new game into the game map if necessary by running <a href="https://hackage.haskell.org/package/stm-2.5.3.1/docs/Control-Concurrent-STM-TVar.html#v:modifyTVar">modifyTVar</a> to update the map.</p>

<p>That just won’t do though! Only one thread can update the map entries at a time, and we have aspirations of being the Internet’s go-to Noughts and Crosses server! We’ll need something that scales better than that while we’ve still got the luxury of vertically scaling one machine along our path to world domination.</p>

<p><a href="https://nikita-volkov.github.io/stm-containers/">stm-containers</a> to the rescue! It uses a Hash Array Mapped Trie datastructure to split the map into chunks that can be updated inside independent STM transactions allowing for more concurrency.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">import</span> <span class="k">qualified</span> <span class="nn">StmContainers.Map</span> <span class="k">as</span> <span class="n">M</span>
<span class="kr">data</span> <span class="kt">GameMap</span> <span class="o">=</span> <span class="kt">M</span><span class="o">.</span><span class="kt">Map</span> <span class="kt">GameId</span> <span class="kt">GameState</span>
</code></pre></div></div>

<p>Our next challenge is to load the game state into the game map when the first websocket connects and remove it from the map when it’s no longer needed so that we don’t run out of memory in our path to world domination.</p>

<h2 id="loading-the-game">Loading the Game</h2>

<p>Imagine our Yesod ‘App’ type also had a GameRepository that was a data store we could use to access and persist our TicTacToe games:</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">data</span> <span class="kt">App</span> <span class="o">=</span> <span class="kt">App</span> <span class="p">{</span> <span class="n">gameRepository</span> <span class="o">::</span> <span class="kt">GameRepository</span><span class="p">,</span> <span class="n">gameMap</span> <span class="o">::</span> <span class="kt">GameMap</span> <span class="p">}</span>
</code></pre></div></div>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">data</span> <span class="kt">GameEntity</span> <span class="o">=</span> <span class="kt">GameEntity</span> <span class="p">{</span>
    <span class="n">gameEntityBoard</span> <span class="o">::</span> <span class="kt">Board</span><span class="p">,</span>
    <span class="n">gameEntityPlayer1</span> <span class="o">::</span> <span class="kt">Player</span><span class="p">,</span>
    <span class="n">gameEntityPlayer2</span> <span class="o">::</span> <span class="kt">Player</span>
<span class="p">}</span>

<span class="n">loadGame</span> <span class="o">::</span> <span class="kt">GameRepository</span> <span class="o">-&gt;</span> <span class="kt">GameId</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="p">(</span><span class="kt">Either</span> <span class="kt">Text</span> <span class="kt">GameEntity</span><span class="p">)</span>
</code></pre></div></div>

<p>We will leverage this function to load the game into the cache when it’s not already populated.</p>

<p>We will use <a href="https://academy.fpblock.com/haskell/library/stm/">Software Transactional Memory (STM)</a> abstraction to ensure that if two websocket connections try to insert an entry into the game map at the same time, they both end up with the same ‘GameState’ instance so that they receive game updates on the broadcast channel.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getGameState</span> <span class="o">::</span> <span class="kt">GameRepository</span> <span class="o">-&gt;</span> <span class="kt">GameMap</span> <span class="o">-&gt;</span> <span class="kt">GameId</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="p">(</span><span class="kt">Either</span> <span class="kt">Text</span> <span class="kt">GameState</span><span class="p">)</span>
<span class="n">getGameState</span> <span class="n">gameRepository</span> <span class="n">gameMap</span> <span class="n">gameId</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="n">existing</span> <span class="o">&lt;-</span> <span class="n">atomically</span> <span class="o">$</span> <span class="kr">do</span>
        <span class="n">cached</span> <span class="o">&lt;-</span> <span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="n">gameId</span> <span class="n">gameMap</span>
        <span class="kr">case</span> <span class="n">cached</span> <span class="kr">of</span>
            <span class="kt">Just</span> <span class="n">gameState</span> <span class="o">-&gt;</span> <span class="kt">Just</span> <span class="o">&lt;$&gt;</span> <span class="n">registerSharer</span> <span class="n">gameState</span>
            <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="n">return</span> <span class="kt">Nothing</span>
    <span class="kr">case</span> <span class="n">existing</span> <span class="kr">of</span>
        <span class="kt">Just</span> <span class="n">gameState</span> <span class="o">-&gt;</span>
            <span class="n">return</span> <span class="o">$</span> <span class="kt">Right</span> <span class="n">gameState</span>
        <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="kr">do</span>
            <span class="c1">-- Note: there are ways we could make this code less nested with branches but</span>
            <span class="c1">-- I have kept it like this for simplicity rather than get fancy</span>
            <span class="n">loaded</span> <span class="o">&lt;-</span> <span class="n">loadGame</span> <span class="n">gameRepository</span> <span class="n">gameId</span>
            <span class="kr">case</span> <span class="n">loaded</span> <span class="kr">of</span>
                <span class="kt">Left</span> <span class="n">err</span> <span class="o">-&gt;</span>
                    <span class="n">return</span> <span class="o">$</span> <span class="kt">Left</span> <span class="n">err</span>
                <span class="kt">Right</span> <span class="n">entity</span> <span class="o">-&gt;</span> <span class="kr">do</span>
                    <span class="c1">-- Set up the shared board state TVar and channel, etc</span>
                    <span class="n">freshState</span> <span class="o">&lt;-</span> <span class="n">mapGameState</span> <span class="n">entity</span>
                    <span class="c1">-- It's important we recheck in the same transaction as writing the entry</span>
                    <span class="c1">-- to the cache that another thread hasn't inserted it in the meantime so</span>
                    <span class="c1">-- that we can make sure we're sharing the same TVars and channels, etc</span>
                    <span class="n">atomically</span> <span class="o">$</span> <span class="kr">do</span>
                        <span class="n">cached'</span> <span class="o">&lt;-</span> <span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="n">gameId</span> <span class="n">gameMap</span>
                        <span class="kr">case</span> <span class="n">cached'</span> <span class="kr">of</span>
                            <span class="kt">Just</span> <span class="n">gameState</span> <span class="o">-&gt;</span> <span class="kt">Right</span> <span class="o">&lt;$&gt;</span> <span class="n">registerSharer</span> <span class="n">gameState</span>
                            <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="kr">do</span>
                                <span class="kt">M</span><span class="o">.</span><span class="n">insert</span> <span class="n">freshState</span> <span class="n">gameId</span> <span class="n">gameMap</span>
                                <span class="kt">Right</span> <span class="o">&lt;$&gt;</span> <span class="n">registerSharer</span> <span class="n">freshState</span>
  <span class="kr">where</span>
    <span class="n">registerSharer</span> <span class="o">::</span> <span class="kt">GameState</span> <span class="o">-&gt;</span> <span class="kt">STM</span> <span class="kt">GameState</span>
    <span class="n">registerSharer</span> <span class="n">gameState</span> <span class="o">=</span> <span class="kr">do</span>
        <span class="n">modifyTVar'</span> <span class="p">(</span><span class="n">gameConnections</span> <span class="n">gameState</span><span class="p">)</span> <span class="p">(</span><span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
        <span class="n">return</span> <span class="n">gameState</span>

    <span class="n">mapGameState</span> <span class="o">::</span> <span class="kt">GameEntity</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="kt">GameState</span>
    <span class="n">mapGameState</span> <span class="kt">GameEntity</span> <span class="p">{</span> <span class="n">gameEntityBoard</span> <span class="o">=</span> <span class="n">b</span><span class="p">,</span> <span class="n">gameEntityPlayer1</span> <span class="o">=</span> <span class="n">p1</span><span class="p">,</span> <span class="n">gameEntityPlayer2</span> <span class="o">=</span> <span class="n">p2</span> <span class="p">}</span> <span class="o">=</span> <span class="kr">do</span>
        <span class="n">boardVar</span> <span class="o">&lt;-</span> <span class="n">newTVarIO</span> <span class="n">b</span>
        <span class="n">chan</span> <span class="o">&lt;-</span> <span class="n">newBroadcastTChanIO</span>
        <span class="n">connVar</span> <span class="o">&lt;-</span> <span class="n">newTVarIO</span> <span class="mi">0</span>
        <span class="n">return</span> <span class="kt">GameState</span>
            <span class="p">{</span> <span class="n">board</span> <span class="o">=</span> <span class="n">boardVar</span>
            <span class="p">,</span> <span class="n">player1</span> <span class="o">=</span> <span class="n">p1</span>
            <span class="p">,</span> <span class="n">player2</span> <span class="o">=</span> <span class="n">p2</span>
            <span class="p">,</span> <span class="n">gameChan</span> <span class="o">=</span> <span class="n">chan</span>
            <span class="p">,</span> <span class="n">gameConnections</span> <span class="o">=</span> <span class="n">connVar</span>
            <span class="p">}</span>
</code></pre></div></div>

<p>In our <code class="language-plaintext highlighter-rouge">getGameState</code> function, if we don’t find the game already present in our game map then we attempt to load it freshly and insert it into the map. We do this insertion inside an STM transaction via the <a href="https://hackage.haskell.org/package/stm-2.5.3.1/docs/Control-Monad-STM.html#v:atomically">atomically</a> function.</p>

<p>This means that if there is a conflict in the middle of the transaction because one or more other websocket threads have tried to load the game in another concurrent transaction, the transaction inside the <code class="language-plaintext highlighter-rouge">atomically</code> ‘block’ will be retried by the unsuccessful writing websocket threads. They will then read the newly inserted entry and increment the sharer count via <code class="language-plaintext highlighter-rouge">registerSharer</code>, meaning  that all threads are reading from the same broadcast channel.</p>

<h2 id="taming-the-herd">Taming the Herd</h2>

<p>There’s an improvement we can make to make sure that multiple threads don’t try to load our game at the same time, wasting trips to the database just to throw the result away. This could happen in a thundering herd of observers to watch the latest tournament game between Noughts and Crosses grandmasters!</p>

<p>We will use an <a href="https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Concurrent-MVar.html">MVar</a> to allow a thread to lay claim to responsibility for loading the resource and signal any other threads waiting when it has done so. MVar’s <a href="https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Concurrent-MVar.html#v:readMVar">readMVar</a> function allows the caller to wait (block) until it has been populated with a value (or it will return immediately if it’s already populated with a value.)</p>

<p>The game entry in our game map will now either be a game loaded into the cache (<code class="language-plaintext highlighter-rouge">LoadedGame</code>) or a game that is currently being loaded by another websocket (<code class="language-plaintext highlighter-rouge">LoadingGame</code>.) <code class="language-plaintext highlighter-rouge">LoadingGame</code> will contain an MVar that can be read to receive a signal that the other thread has completed loading the game.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>data GameEntry = LoadedGame GameState | LoadingGame (MVar ())
type GameMap = M.Map GameId GameEntry
</code></pre></div></div>

<p>When we first try to retrieve a game, we have to decide the action we need to take based on the cache state, which we represent with this type:</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">data</span> <span class="kt">CacheDecision</span>
    <span class="c1">-- The game is already in the cache</span>
    <span class="o">=</span> <span class="kt">Hit</span> <span class="kt">GameState</span>
    <span class="c1">-- We're awaiting another thread loading the game</span>
    <span class="o">|</span> <span class="kt">Await</span> <span class="p">(</span><span class="kt">MVar</span> <span class="nb">()</span><span class="p">)</span>
    <span class="c1">-- We have claimed ownership over loading the game</span>
    <span class="o">|</span> <span class="kt">Claimed</span> <span class="p">(</span><span class="kt">MVar</span> <span class="nb">()</span><span class="p">)</span>
</code></pre></div></div>

<p>When we load a game we transactionally lay claim to loading the resource if it’s not already loaded or is currently being loaded by writing a ‘LoadingGame’ entry to cache:</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getGameState</span> <span class="o">::</span> <span class="kt">GameRepository</span> <span class="o">-&gt;</span> <span class="kt">GameMap</span> <span class="o">-&gt;</span> <span class="kt">GameId</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="p">(</span><span class="kt">Either</span> <span class="kt">Text</span> <span class="kt">GameState</span><span class="p">)</span>
<span class="n">getGameState</span> <span class="n">gameRepository</span> <span class="n">gameMap</span> <span class="n">gameId</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="n">pendingMvar</span> <span class="o">&lt;-</span> <span class="n">newEmptyMVar</span>
    <span class="n">decision</span> <span class="o">&lt;-</span> <span class="n">atomically</span> <span class="o">$</span> <span class="kr">do</span>
        <span class="n">cached</span> <span class="o">&lt;-</span> <span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="n">gameId</span> <span class="n">gameMap</span>
        <span class="kr">case</span> <span class="n">cached</span> <span class="kr">of</span>
            <span class="kt">Just</span> <span class="p">(</span><span class="kt">LoadedGame</span> <span class="n">gameState</span><span class="p">)</span> <span class="o">-&gt;</span>
                <span class="kt">Hit</span> <span class="o">&lt;$&gt;</span> <span class="n">registerSharer</span> <span class="n">gameState</span>
            <span class="kt">Just</span> <span class="p">(</span><span class="kt">LoadingGame</span> <span class="n">existingMvar</span><span class="p">)</span> <span class="o">-&gt;</span>
                <span class="n">return</span> <span class="o">$</span> <span class="kt">Await</span> <span class="n">existingMvar</span>
            <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="kr">do</span>
                <span class="kt">M</span><span class="o">.</span><span class="n">insert</span> <span class="p">(</span><span class="kt">LoadingGame</span> <span class="n">pendingMvar</span><span class="p">)</span> <span class="n">gameId</span> <span class="n">gameMap</span>
                <span class="n">return</span> <span class="o">$</span> <span class="kt">Claimed</span> <span class="n">pendingMvar</span>
    <span class="kr">case</span> <span class="n">decision</span> <span class="kr">of</span>
        <span class="kt">Hit</span> <span class="n">gameState</span> <span class="o">-&gt;</span>
            <span class="n">return</span> <span class="o">$</span> <span class="kt">Right</span> <span class="n">gameState</span>
        <span class="kt">Await</span> <span class="n">existingMvar</span> <span class="o">-&gt;</span> <span class="kr">do</span>
            <span class="c1">-- Once signalled that the game has been loaded, we recursively start again</span>
            <span class="n">readMVar</span> <span class="n">existingMvar</span>
            <span class="n">getGameState</span> <span class="n">gameRepository</span> <span class="n">gameMap</span> <span class="n">gameId</span>
        <span class="kt">Claimed</span> <span class="n">ownedMvar</span> <span class="o">-&gt;</span>
            <span class="n">loadAndPublish</span> <span class="n">ownedMvar</span>
</code></pre></div></div>

<p>This is performed in an STM transaction, so if multiple threads try to insert their ‘MVar’ claiming ownership then the losers of the race will retry the transaction and see the winning thread’s claim when they read the map again. They will then block on it being populated by the winning thread using using <a href="https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Concurrent-MVar.html#v:readMVar">readMVar</a>, which serves as a signal that they can now load the game from the cache.</p>

<p>When loading the resource we must make sure that threads don’t wait on the MVar indefinetly if an error is thrown while retrieving the game so we clean up the claim if an error occurs while trying to load the game.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">loadAndPublish</span> <span class="o">::</span> <span class="kt">MVar</span> <span class="nb">()</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="p">(</span><span class="kt">Either</span> <span class="kt">Text</span> <span class="kt">GameState</span><span class="p">)</span>
    <span class="n">loadAndPublish</span> <span class="n">ownedMvar</span> <span class="o">=</span> <span class="kr">do</span>
        <span class="n">attempted</span> <span class="o">&lt;-</span> <span class="n">tryLoad</span>
        <span class="n">result</span> <span class="o">&lt;-</span> <span class="kr">case</span> <span class="n">attempted</span> <span class="kr">of</span>
            <span class="kt">Left</span> <span class="n">ioErr</span> <span class="o">-&gt;</span> <span class="kr">do</span>
                <span class="c1">-- An exception was thrown in the IO action. We remove the claim</span>
                <span class="c1">-- to allow another threat to attempt to load it</span>
                <span class="n">atomically</span> <span class="o">$</span> <span class="kt">M</span><span class="o">.</span><span class="n">delete</span> <span class="n">gameId</span> <span class="n">gameMap</span>
                <span class="n">return</span> <span class="o">$</span> <span class="kt">Left</span> <span class="p">(</span><span class="kt">T</span><span class="o">.</span><span class="n">pack</span> <span class="p">(</span><span class="n">show</span> <span class="n">ioErr</span><span class="p">))</span>
            <span class="kt">Right</span> <span class="p">(</span><span class="kt">Left</span> <span class="n">err</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kr">do</span>
                <span class="c1">-- The action returned a 'Left.' We remove the claim to allow another thread</span>
                <span class="c1">-- to attempt to load it</span>
                <span class="n">atomically</span> <span class="o">$</span> <span class="kt">M</span><span class="o">.</span><span class="n">delete</span> <span class="n">gameId</span> <span class="n">gameMap</span>
                <span class="n">return</span> <span class="o">$</span> <span class="kt">Left</span> <span class="n">err</span>
            <span class="kt">Right</span> <span class="p">(</span><span class="kt">Right</span> <span class="n">entity</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kr">do</span>
                <span class="n">freshState</span> <span class="o">&lt;-</span> <span class="n">mapGameState</span> <span class="n">entity</span>
                <span class="n">atomically</span> <span class="o">$</span> <span class="kr">do</span>
                    <span class="c1">-- Replace the 'Loading' cache entry with the loaded game entry</span>
                    <span class="kt">M</span><span class="o">.</span><span class="n">insert</span> <span class="p">(</span><span class="kt">LoadedGame</span> <span class="n">freshState</span><span class="p">)</span> <span class="n">gameId</span> <span class="n">gameMap</span>
                    <span class="kr">_</span> <span class="o">&lt;-</span> <span class="n">registerSharer</span> <span class="n">freshState</span>
                    <span class="n">return</span> <span class="nb">()</span>
                <span class="n">return</span> <span class="o">$</span> <span class="kt">Right</span> <span class="n">freshState</span>
        <span class="c1">-- Wake up any threads currently waiting on our LoadingGame placeholder.</span>
        <span class="n">putMVar</span> <span class="n">ownedMvar</span> <span class="nb">()</span>
        <span class="n">return</span> <span class="n">result</span>
      <span class="kr">where</span>
        <span class="n">tryLoad</span> <span class="o">::</span> <span class="kt">IO</span> <span class="p">(</span><span class="kt">Either</span> <span class="kt">IOException</span> <span class="p">(</span><span class="kt">Either</span> <span class="kt">Text</span> <span class="kt">GameEntity</span><span class="p">))</span>
        <span class="n">tryLoad</span> <span class="o">=</span> <span class="n">try</span> <span class="p">(</span><span class="n">loadGame</span> <span class="n">gameRepository</span> <span class="n">gameId</span><span class="p">)</span>
</code></pre></div></div>

<h1 id="cache-expiry">Cache Expiry</h1>

<p>Our last challenge is to make sure that entries are removed from the cache when it’s safe to do so (there are no websocket subscriptions.) Our Noughts and Crosses server will be very busy with many games being started and ended and we can’t have ourselves running out of heap space due to a memory leak of games remaining in the cache!</p>

<p>When we think of garaunteeing the safe release of scarce resources, one of the options our mind turns to is the <a href="https://hackage.haskell.org/package/resourcet">resourcet</a> package. This package allows us to use the type system to make sure a resource will be cleaned up when it is no longer being used. See <a href="https://www.yesodweb.com/blog/2011/12/resourcet">this blog entry</a> for more on how it works if you are unfamilar with it.</p>

<p>We will use the resourcet package’s <a href="https://hackage.haskell.org/package/resourcet-1.3.0/docs/Control-Monad-Trans-Resource.html#v:allocate">allocate</a> function to register the action that must be taken when the game resource is no longer in scope (due to the websocket disconnecting or an exception being thrown.)</p>

<p>We have the ‘gameConnections’ variable already tracking the number of websocket handlers that are subscribed to the game. On connection, we will increment it via our <code class="language-plaintext highlighter-rouge">registerSharer</code> function as usual and on disconnection if the connection count has reached 0 we will remove it from the cache:</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getGameState</span> <span class="o">::</span> <span class="kt">MonadResource</span> <span class="n">m</span> <span class="o">=&gt;</span> <span class="kt">GameRepository</span> <span class="o">-&gt;</span> <span class="kt">GameMap</span> <span class="o">-&gt;</span> <span class="kt">GameId</span> <span class="o">-&gt;</span> <span class="n">m</span> <span class="p">(</span><span class="kt">Either</span> <span class="kt">Text</span> <span class="kt">GameState</span><span class="p">)</span>
<span class="n">getGameState</span> <span class="n">gameRepository</span> <span class="n">gameMap</span> <span class="n">gameId</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="p">(</span><span class="n">_releaseKey</span><span class="p">,</span> <span class="n">result</span><span class="p">)</span> <span class="o">&lt;-</span> <span class="n">allocate</span> <span class="n">acquireGameState</span> <span class="n">releaseGameState</span>
    <span class="n">return</span> <span class="n">result</span>
  <span class="kr">where</span>
    <span class="n">acquireGameState</span> <span class="o">::</span> <span class="kt">IO</span> <span class="p">(</span><span class="kt">Either</span> <span class="kt">Text</span> <span class="kt">GameState</span><span class="p">)</span>
    <span class="n">acquireGameState</span> <span class="o">=</span> <span class="kr">do</span>
        <span class="n">pendingMvar</span> <span class="o">&lt;-</span> <span class="n">newEmptyMVar</span>
        <span class="n">decision</span> <span class="o">&lt;-</span> <span class="n">atomically</span> <span class="o">$</span> <span class="kr">do</span>
            <span class="n">cached</span> <span class="o">&lt;-</span> <span class="kt">M</span><span class="o">.</span><span class="n">lookup</span> <span class="n">gameId</span> <span class="n">gameMap</span>
            <span class="kr">case</span> <span class="n">cached</span> <span class="kr">of</span>
                <span class="kt">Just</span> <span class="p">(</span><span class="kt">LoadedGame</span> <span class="n">gameState</span><span class="p">)</span> <span class="o">-&gt;</span>
                    <span class="kt">Hit</span> <span class="o">&lt;$&gt;</span> <span class="n">registerSharer</span> <span class="n">gameState</span>
                <span class="kt">Just</span> <span class="p">(</span><span class="kt">LoadingGame</span> <span class="n">existingMvar</span><span class="p">)</span> <span class="o">-&gt;</span>
                    <span class="n">return</span> <span class="o">$</span> <span class="kt">Await</span> <span class="n">existingMvar</span>
                <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="kr">do</span>
                    <span class="kt">M</span><span class="o">.</span><span class="n">insert</span> <span class="p">(</span><span class="kt">LoadingGame</span> <span class="n">pendingMvar</span><span class="p">)</span> <span class="n">gameId</span> <span class="n">gameMap</span>
                    <span class="n">return</span> <span class="o">$</span> <span class="kt">Claimed</span> <span class="n">pendingMvar</span>
        <span class="kr">case</span> <span class="n">decision</span> <span class="kr">of</span>
            <span class="kt">Hit</span> <span class="n">gameState</span> <span class="o">-&gt;</span>
                <span class="n">return</span> <span class="o">$</span> <span class="kt">Right</span> <span class="n">gameState</span>
            <span class="kt">Await</span> <span class="n">existingMvar</span> <span class="o">-&gt;</span> <span class="kr">do</span>
                <span class="n">readMVar</span> <span class="n">existingMvar</span>
                <span class="n">acquireGameState</span>
            <span class="kt">Claimed</span> <span class="n">ownedMvar</span> <span class="o">-&gt;</span>
                <span class="n">loadAndPublish</span> <span class="n">ownedMvar</span>

    <span class="n">releaseGameState</span> <span class="o">::</span> <span class="kt">Either</span> <span class="kt">Text</span> <span class="kt">GameState</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="nb">()</span>
    <span class="n">releaseGameState</span> <span class="p">(</span><span class="kt">Left</span> <span class="kr">_</span><span class="p">)</span> <span class="o">=</span>
        <span class="c1">-- Acquire reported a loading error and already cleaned up its placeholder,</span>
        <span class="c1">-- so there's no sharer count to decrement.</span>
        <span class="n">return</span> <span class="nb">()</span>
    <span class="n">releaseGameState</span> <span class="p">(</span><span class="kt">Right</span> <span class="n">gameState</span><span class="p">)</span> <span class="o">=</span> <span class="n">atomically</span> <span class="o">$</span> <span class="kr">do</span>
        <span class="n">modifyTVar'</span> <span class="p">(</span><span class="n">gameConnections</span> <span class="n">gameState</span><span class="p">)</span> <span class="p">(</span><span class="n">subtract</span> <span class="mi">1</span><span class="p">)</span>
        <span class="n">remaining</span> <span class="o">&lt;-</span> <span class="n">readTVar</span> <span class="p">(</span><span class="n">gameConnections</span> <span class="n">gameState</span><span class="p">)</span>
        <span class="n">when</span> <span class="p">(</span><span class="n">remaining</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">)</span> <span class="o">$</span> <span class="kt">M</span><span class="o">.</span><span class="n">delete</span> <span class="n">gameId</span> <span class="n">gameMap</span>
</code></pre></div></div>

<p>We registered the <code class="language-plaintext highlighter-rouge">releaseGameState</code> function as the function that should be ran when the resource goes out of scope. It will check if there’s any sharers left, and if there are not it will remove the game from the game map.</p>

<p>Again, the STM abstraction has served us well. The transaction inside <code class="language-plaintext highlighter-rouge">atomically</code> will be restarted if either the ‘gameConnections’ TVar or game map is updated by another websocket thread.</p>

<p>If a new websocket connection retrieves the game from the cache at the same time as it is being removed in another transaction, either:</p>
<ul>
  <li>The transaction removing the cache entry is restarted, meaning the websocket doesn’t end up with an orphaned game state and channel it will never get updates on</li>
  <li>Or the transaction retrieving the item from the cache is aborted, meaning the websocket thread has to insert it threshly and it will be visible to other websockets</li>
</ul>

<p><em>Note</em>: the conditional deletion can be achieved slightly more efficiently with stm-container’s <a href="https://hackage-content.haskell.org/package/stm-containers-1.2.2/docs/StmContainers-Map.html#v:focus">focus</a> function to avoid having to traverse the map a second time to do the deletion.</p>

<p>We can now use this game ‘resource’ in our websocket handler inside <code class="language-plaintext highlighter-rouge">runResourceT</code>:</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">websocketHandler</span> <span class="o">::</span> <span class="kt">App</span> <span class="o">-&gt;</span> <span class="kt">GameId</span> <span class="o">-&gt;</span> <span class="kt">WebSocketsT</span> <span class="kt">Handler</span> <span class="nb">()</span>
<span class="n">websocketHandler</span> <span class="n">app</span> <span class="n">gameId</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="n">userId</span> <span class="o">&lt;-</span> <span class="n">lift</span> <span class="n">requireAuthId</span>
    <span class="n">websocketConnection</span> <span class="o">&lt;-</span> <span class="n">ask</span>
    <span class="n">runResourceT</span> <span class="o">$</span> <span class="kr">do</span>
        <span class="n">result</span> <span class="o">&lt;-</span> <span class="n">getGameState</span> <span class="p">(</span><span class="n">gameRepository</span> <span class="n">app</span><span class="p">)</span> <span class="p">(</span><span class="n">gameMap</span> <span class="n">app</span><span class="p">)</span> <span class="n">gameId</span>
        <span class="kr">case</span> <span class="n">result</span> <span class="kr">of</span>
            <span class="kt">Left</span> <span class="n">_err</span> <span class="o">-&gt;</span> <span class="n">return</span> <span class="nb">()</span>
            <span class="kt">Right</span> <span class="n">gameState</span> <span class="o">-&gt;</span>
                <span class="n">liftIO</span> <span class="o">$</span> <span class="n">race_</span>
                    <span class="p">(</span><span class="n">handleIncomingMessages</span> <span class="n">websocketConnection</span> <span class="n">gameState</span> <span class="n">userId</span><span class="p">)</span>
                    <span class="p">(</span><span class="n">handleOutgoingMessages</span> <span class="n">websocketConnection</span> <span class="n">gameState</span> <span class="n">userId</span><span class="p">)</span>
</code></pre></div></div>

<p>We use <a href="https://hackage-content.haskell.org/package/async-2.2.6/docs/Control-Concurrent-Async.html#v:race_">race_</a> to spawn two green threads to manage the Websocket connection.</p>

<p><code class="language-plaintext highlighter-rouge">handleOutgoingMessages</code> will read from the shared broadcast channel and write to the websocket to inform the client of board changes.</p>

<p><code class="language-plaintext highlighter-rouge">handleIncomingMessages</code> will read from the websocket to make moves on behalf of the user. It will validate the move, update the state and write to the shared broadcast channel.</p>

<p>When either <code class="language-plaintext highlighter-rouge">handleIncomingMessages</code> or <code class="language-plaintext highlighter-rouge">handleOutgoingMessages</code> fails on reading or writing to the socket due to it being disconnected, <code class="language-plaintext highlighter-rouge">runResourceT</code> will catch the error and run the <code class="language-plaintext highlighter-rouge">releaseGameState</code> function that we registered to run on resource deallocation in our <code class="language-plaintext highlighter-rouge">getGameState</code> function before rethrowing the error.</p>

<p>If you want to see a full implementation (mostly vibe coded other than what we’ve discussed here) of our demo noughts and crosses server, I’ve uploaded it to github <a href="https://github.com/Happy0/vibe-noughts-and-crosses-demo">here</a>. The <a href="https://github.com/Happy0/vibe-noughts-and-crosses-demo/blob/main/src/Handler/NoughtsAndCrosses.hs">NoughtsAndCrosses.hs</a> file contains all the logic we’ve been discussing in this blog entry.</p>

<h2 id="conclusion">Conclusion</h2>

<p>This pattern can be applied in any situation where it’s important that multiple threads need to share the same reference to something that is dynamically loaded. This is a requirement beyond that of a traditional cache where it doesn’t particularly matter if an item is evicted from the cache (per some eviction policy) while there’s still threads holding on to it, for example.</p>

<p>If you think you might find this pattern useful for something, I’ve published it as a library named <a href="https://hackage.haskell.org/package/shared-resource-cache">shared-resource-cache</a>. Source <a href="https://github.com/Happy0/shared-resource-cache">here</a>.</p>

<p><em>Note</em>: it works slightly differently to what we’ve explored here: when there are no ‘sharers’ of the given resource, it is not cleared from the cache immediately. Instead, it is cleared when there has been no sharers for at least a configured amount of time.</p>

<p>Feel free to send me a <a href="https://wordify.gordo.life/create-lobby">game invite</a> on wordify. My username is happy0 :).</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’d like to share a wee pattern I’ve found useful for writing simple WebSocket apps in Haskell - specifically for my side project wordify - an open source ~ multiplayer crossword board game ~ .]]></summary></entry></feed>