在处理下一个请求之前,如何强制yesod / warp关闭打开的文件句柄?

时间:2013-11-02 17:36:03

标签: haskell file-io concurrency yesod haskell-warp

我编写了一个小型服务器,它接受注册为POST请求,并通过将它们附加到文件来保留它们。一旦我把这个服务器加载(我使用Apache JMeter有50个并发线程,重复计数为10,并且帖子包含一个带有~7k文本数据的字段),我得到很多“资源忙,文件是锁定“错误:

02/Nov/2013:18:07:11 +0100 [Error#yesod-core] registrations.txt: openFile: resource busy (file is locked) @(yesod-core-1.2.4.2:Yesod.Core.Class.Yesod ./Yesod/Core/Class/Yesod.hs:485:5)

以下是代码的简化版本:

{-# LANGUAGE QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings, TypeFamilies #-}

import           Yesod
import           Text.Hamlet
import           Control.Applicative ((<$>), (<*>))
import           Control.Monad.IO.Class (liftIO)
import           Data.Text (Text, pack, unpack)
import           Data.String
import           System.IO (withFile, IOMode(..), hPutStrLn)

data Server = Server

data Registration = Registration
        { text      :: Text
        }
    deriving (Show, Read)

mkYesod "Server" [parseRoutes|
/reg    RegR    POST
|]

instance Yesod Server

instance RenderMessage Server FormMessage where
    renderMessage _ _ = defaultFormMessage

postRegR :: Handler Html
postRegR = do
    result <- runInputPost $ Registration
        <$> ireq textField "text"
    liftIO $ saveRegistration result
    defaultLayout [whamlet|<p>#{show result}|]

saveRegistration :: Registration -> IO ()
saveRegistration r = withFile "registrations.txt" AppendMode (\h -> hPutStrLn h $ "+" ++ show r)

main :: IO ()
main = warp 8080 Server

我在没有-threaded的情况下故意编译了代码,操作系统只显示了一个运行的线程。尽管如此,它看起来像请求没有完全序列化,并且在将旧请求写入磁盘之前已经处理了新请求。

您能否告诉我如何避免错误消息并确保所有请求都成功处理?性能还不是问题。

2 个答案:

答案 0 :(得分:4)

从几个线程写入Handle是完全可以的。实际上,Handle内部有MVar个,以防止奇怪的并发行为。您可能想要的是不要手动处理[sic] MVar(例如,如果处理程序抛出异常会导致死锁)但是将withFile调用提升到单个处理程序线程之外。该文件始终保持打开状态 - 无论如何,在每个请求上打开它都会很慢。

我对Yesod一无所知,但我推荐这样的东西(可能没有编译):

data Server = Server { handle :: Handle }

postRegR :: Handler Html
postRegR = do
    h <- handle `fmap` getYesod
    result <- runInputPost $ Registration
        <$> ireq textField "text"
    liftIO $ saveRegistration h result
    defaultLayout [whamlet|<p>#{show result}|]

saveRegistration :: Handle -> Registration -> IO ()
saveRegistration h r = hPutStrLn h $ "+" ++ show r

main :: IO ()
main = withFile "registrations.txt" AppendMode $ \h -> warp 8080 (Server h) 
-- maybe there's a better way?

除此之外:如果您想要异步写入文件,您可以写入队列(如果它是日志文件或其他东西),但在您的用例中,您可能希望让用户知道他们的注册是否失败,所以我建议保留此表格。

答案 1 :(得分:3)

即使没有-threaded,Haskell运行时也会有几个“绿色线程”协同运行。您需要使用Control.Concurrent来限制对文件的访问,因为您不能同时有多个线程写入它。

最简单的方法是在Server中添加MVar ()并让每个请求在打开文件之前从MVar“获取”该单元,然后在文件之后将其放回操作已经完成。您可以使用bracket确保即使写入文件失败也会释放锁定。例如。

之类的东西
import Control.Concurrent
import Control.Exception (bracket_)

type Lock = MVar ()
data Server = Server { fileLock :: Lock }

saveRegistration :: Registration -> Lock -> IO ()
saveRegistration r lock = bracket_ acquire release updateFile where
    acquire = takeMVar lock
    release = putMVar lock ()
    updateFile =
        withFile "registrations.txt" AppendMode (\h -> hPutStrLn h $ "+" ++ show r)