Search code examples
haskellconcurrencystatehaskell-warphaskell-wai

Thread-safe state with Warp/WAI


I want to write a web server which stores its state in a State monad with wai/warp. Something like this:

{-# LANGUAGE OverloadedStrings #-}
import Network.Wai
import Network.Wai.Handler.Warp
import Network.HTTP.Types
import Control.Monad.State
import Data.ByteString.Lazy.Char8

main = run 3000 app

text x = responseLBS
        status200
        [("Content-Type", "text/plain")]
    x

app req = return $ text "Hello World"

app1 req = modify (+1) >>= return . text . pack . show

-- main1 = runStateT (run 3000 app1) 0

The commented line doesn't work, of course. The intent is to store a counter in a state monad and display its increasing value on every request.

Also, how do I get thread safety? Does warp run my middleware sequentially or in parallel?

What options are available for the state - is there anything at all besides IORef I can use in this scenario?

I understand that State gives safety but it seems wai doesn't allow State.

I only need a dead-simple single-threaded RPC I can call from somewhere else. Haxr package requires a separate web server which is an overkill. See Calling Haskell from Node.JS - it didn't have any suggestions so I wrote a simple server using Wai/Warp and Aeson. But it seems that WAI was designed to support concurrent implementatons so it complicates things.


Solution

  • If your interaction with the state can be expressed with a single call to atomicModifyIORef, you can use that, and you don't need to explicitly serialise access to the state.

    import Data.IORef
    
    main = do state <- newIORef 42
              run 3000 (app' state)
    
    app' :: IORef Int -> Application
    app' ref req
       = return . text . pack . show `liftM` atomicModifyIORef ref (\st -> (st + 1, st + 1))
    

    If your interaction is more complex and you need to enforce full serialisation of requests, you can use an MVar in conjunction with StateT.

    import Control.Concurrent.MVar
    import Control.Monad.State.Strict
    
    main = do state <- newMVar 42
              run 3000 (app' state)
    
    app' :: MVar Int -> Application
    app' ref request
       = do state <- takeMVar ref
            (response, newState) <- runStateT (application request) state
            putMVar newState --TODO: ensure putMVar happens even if an exception is thrown
            return response
    
    application :: Request -> StateT Int (ResourceT IO) Response
    application request = modify (+1) >>= return . text . pack . show