Как заставить yesod/warp закрыть дескрипторы открытых файлов перед обработкой следующего запроса?

Я написал небольшой сервер, который принимает регистрации как POST-запросы и сохраняет их, добавляя их в файл. Как только я нагружаю этот сервер (я использую Apache JMeter с 50 одновременными потоками и числом повторений 10, а сообщение состоит из одного поля с ~ 7 КБ текстовых данных), я получаю много «ресурс занят, файл заблокировано» ошибки:

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, и ОС показывает только один запущенный поток. Тем не менее, мне кажется, что запросы не полностью сериализованы, и новый запрос уже обрабатывается до того, как старый был записан на диск.

Не могли бы вы сказать мне, как я могу избежать сообщения об ошибке и убедиться, что все запросы обрабатываются успешно? Производительность пока не проблема.


person Axel Hanikel    schedule 02.11.2013    source источник


Ответы (2)


Даже без -threaded исполняющая среда Haskell будет иметь несколько совместно работающих «зеленых потоков». Вам нужно использовать Control.Concurrent, чтобы ограничить доступ к файл, потому что вы не можете иметь несколько потоков, записывающих в него одновременно.

Самый простой способ — добавить MVar () в ваш Server и пусть каждый запрос "берет" устройство из 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)
person shang    schedule 02.11.2013
comment
Чтобы уточнить: совершенно нормально писать в Handle из нескольких потоков - Handle содержат MVar внутри них, чтобы предотвратить странное параллельное поведение. Вероятно, вы хотите не обрабатывать MVar вручную (что может привести к взаимоблокировке, если обработчик выдает исключение), а поднимать withFile вне отдельных потоков. Файл остается открытым, но это, вероятно, неизбежно (открытие его при каждом запросе в любом случае будет медленным). - person Fixnum; 02.11.2013
comment
Fixnum: На самом деле это гораздо лучший способ. Вы должны поставить это как ответ. :) - person shang; 02.11.2013
comment
К сожалению, я не могу отметить ваши ответы и ответы Fixnum как принятые, потому что их комбинация отлично решила мою проблему: я использовал MVar, как вы описали, и пример Fixnum показал мне, как это сделать. Спасибо!!! - person Axel Hanikel; 03.11.2013
comment
@AxelHanikel: я добавил короткий пример, в котором используется bracket. Без этого любые ошибки, которые могут произойти при обновлении файла, оставят MVar пустым, а приложение заблокируется. - person shang; 03.11.2013
comment
Я также пытался использовать bracket, но ваш пример намного читабельнее, чем мой, поэтому я запомню его не только из-за функциональности, но и как пример написания кода, который легко читается. Я отмечу этот ответ как принятый ответ сейчас, потому что я думаю, что это самый краткий рецепт для других новичков Haskeller, сталкивающихся с той же проблемой. Спасибо еще раз! - person Axel Hanikel; 04.11.2013

Совершенно нормально писать в Handle из нескольких потоков. На самом деле внутри Handle есть MVar, чтобы предотвратить странное параллельное поведение. Вероятно, вы хотите не обрабатывать [sic] 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?

Кроме того: если вы хотите, чтобы файл записывался асинхронно, вы могли бы записать в очередь (если бы это был файл журнала или что-то в этом роде), но в вашем случае использования вы, вероятно, захотите сообщить пользователю, если его регистрация не удалась, поэтому я бы рекомендуем остаться с этой формой.

person Fixnum    schedule 02.11.2013
comment
В качестве первой попытки я использовал ваш пример, но оставил withFile в функции saveRegistration и передал ей MVar. Затем я окружил hPutStrLn takeMVar / putMVar, и у меня не было ошибок в сотнях запросов! Чтобы избежать описанного вами тупика, я также попытался поместить putMVar внутри функции обработчика в withFile, но, как ни странно, это не сработало. Наконец, я попробовал ваше решение, и оно действительно быстрее. Большое спасибо за подробный пример! - person Axel Hanikel; 03.11.2013
comment
Блокировка внутри withFile (насколько я вас понимаю) не работает, потому что ваша ошибка возникает из-за попытки открыть уже открытый файл, что вызвано самим withFile. Извините, если это было непонятно. - person Fixnum; 03.11.2013
comment
Ах, конечно, теперь, когда вы это говорите, это имеет смысл! Я надеюсь, вы не возражаете, что я пометил ответ Шанга как принятый: ваше решение более эффективно, но оно полностью позволяет избежать проблемы, тогда как ответ Шана содержит объяснение неожиданного поведения и общее решение, которое может также быть применимым к коду других людей. Большое спасибо, я многому научился из ваших ответов! - person Axel Hanikel; 04.11.2013