【问题标题】:How to force yesod/warp to close open file handles before handling next request?如何在处理下一个请求之前强制 yesod/warp 关闭打开的文件句柄?
【发布时间】:2013-11-13 17:14:17
【问题描述】:

我编写了一个小型服务器,它接受作为 POST 请求的注册并通过将它们附加到文件来持久化它们。一旦我将此服务器置于负载状态(我使用具有 50 个并发线程和 10 个重复计数的 Apache JMeter,并且帖子包含一个包含约 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 的情况下编译了代码,操作系统显示只有一个线程在运行。尽管如此,在我看来,请求并未完全序列化,并且在旧请求写入磁盘之前已经处理了新请求。

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

【问题讨论】:

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


    【解决方案1】:

    从多个线程写入Handle 是完全可以的。事实上,Handles 内部有 MVars 以防止奇怪的并发行为。您可能想要的不是手动处理 [原文如此] MVars(例如,如果处理程序抛出异常,这可能会导致死锁),而是在各个处理程序线程之外解除 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?
    

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

    【讨论】:

    • 作为第一次尝试,我使用了您的示例,但将 withFile 留在了 saveRegistration 函数中,并将其传递给 MVar。然后我用takeMVar / putMVar 包围了hPutStrLn,并且我在数百个请求中都没有错误!为了避免您描述的死锁,我还尝试将putMVar 放在withFile 的处理程序函数中,但令人惊讶的是这不起作用。最后我尝试了你的解决方案,它确实更快。非常感谢您提供详细的示例!
    • 锁定 inside withFile(如果我理解你的话)不起作用,因为你的错误是由于尝试 open 一个已经打开的文件,这是由withFile 本身引起的。抱歉,如果不清楚。
    • 啊,当然了,既然你这么说,那就太有道理了!我希望您不介意我已将 shang 的答案标记为已接受的答案:您的解决方案更有效,但它完全避免了问题,而 shang 的答案包含对意外行为的解释,以及可能的通用解决方案也适用于其他人的代码。非常感谢你,我从你的回答中学到了很多东西!
    【解决方案2】:

    即使没有-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)
    

    【讨论】:

    • 澄清一下:从多个线程写入Handle 是完全可以的——Handles 内部有MVars 以防止奇怪的并发行为。您可能想要的不是手动处理MVars(如果处理程序抛出异常,这可能导致死锁),而是将withFile 提升到各个线程之外。该文件保持打开状态,但这可能是不可避免的(在每个请求上打开它都会很慢)。
    • Fixnum:这实际上是一个更好的方法。你应该把它作为答案。 :)
    • 不幸的是,我无法将您和 Fixnum 的答案都标记为已接受,因为两者的结合完美地解决了我的问题:我使用了您所描述的 MVar,Fixnum 的示例向我展示了如何做它。谢谢!!!
    • @AxelHanikel:我添加了一个使用bracket 的简短示例。否则,更新文件时可能发生的任何错误都会使MVar 为空并且应用程序死锁。
    • 我也尝试过使用bracket,但是您的示例比我所做的更具可读性,因此我不仅会记住它的功能,还可以作为编写示例易于阅读的代码。我现在将此答案标记为已接受的答案,因为我认为对于遇到相同问题的其他新手 Haskeller 来说,这是最简洁的方法。再次感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-05
    • 2016-07-29
    • 1970-01-01
    • 1970-01-01
    • 2014-12-26
    相关资源
    最近更新 更多