Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 884
nponeccop Avatar asked Feb 21 '23 14:02

nponeccop


2 Answers

If it is in the State monad, it is thread-safe by design. There is no concurrent IO actions to shared state possible. Either it is threadsafe, or it won't compile.

If you have truly parallel access to shared state as part of your design (i.e. separate forkIO threads updating a global counter), then you'll need to use and MVar or TVar in the STM monad (or some other transactional barrier) to ensure atomicity.

like image 101
Don Stewart Avatar answered Mar 04 '23 23:03

Don Stewart


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
like image 39
dave4420 Avatar answered Mar 04 '23 21:03

dave4420