Как я могу сделать файловый ввод / вывод более транзакционным? - PullRequest
18 голосов
/ 14 августа 2011

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

Какой хороший способ реализовать

transactionalUpdate :: FilePath -> (String -> String) -> IO ()

там, где функция ('update ') вычисляет новое содержимое файла из старого содержимого файла?Не безопасно предполагать, что «обновление» является строгим, но можно предположить, что оно является полным (устойчивость к функциям частичного обновления является бонусом).Транзакции могут быть предприняты одновременно, но ни одна транзакция не может быть в состоянии обновить, если файл был написан кем-то еще с момента его чтения.Это нормально, если транзакция отменяется в случае конкуренции за доступ к файлу.Мы можем предположить источник уникальных для всей системы временных имен файлов.

Моя текущая попытка записи во временный файл, а затем использует системную команду копирования для перезаписи.Кажется, это решает проблемы с ленивым вводом-выводом, но это не делает меня безопасным от гонок.Есть ли проверенная формула, которую мы могли бы просто разлить в бутылки?

Ответы [ 2 ]

6 голосов
/ 15 августа 2011

Самый идиоматичный неуклюжий способ сделать это с паствой:

3 голосов
/ 15 августа 2011

Вот грубый первый разрез, основанный на атомарности базового mkdir.Кажется, что он соответствует спецификации, но я не уверен, насколько он устойчив или быстр:

import Control.DeepSeq
import Control.Exception
import System.Directory
import System.IO

transactionalUpdate :: FilePath -> (String -> String) -> IO ()
transactionalUpdate file upd = bracket acquire release update
  where
    acquire = do
      let lockName = file ++ ".lock"
      createDirectory lockName
      return lockName
    release = removeDirectory
    update _ = nonTransactionalUpdate file upd

nonTransactionalUpdate :: FilePath -> (String -> String) -> IO ()
nonTransactionalUpdate file upd = do
  h <- openFile file ReadMode
  s <- upd `fmap` hGetContents h
  s `deepseq` hClose h
  h <- openFile file WriteMode
  hPutStr h s
  hClose h

Я проверил это, добавив следующее main и бросив threadDelay в середине nonTransactionalUpdate:

main = do
  [n] <- getArgs
  transactionalUpdate "foo.txt" ((show n ++ "\n") ++)
  putStrLn $ "successfully updated " ++ show n

Затем я скомпилировал и запустил несколько экземпляров с помощью этого сценария:

#!/bin/bash                                                                                                     

rm foo.txt
touch foo.txt
for i in {1..50}
do
    ./SO $i &
done

Процесс, который печатал сообщение об успешном обновлении тогда и только тогда, когда соответствующий номер былв foo.txt;все остальные напечатали ожидаемое SO: foo.txt.notveryunique: createDirectory: already exists (File exists).

Обновление: Вы на самом деле не хотите использовать здесь уникальные имена;это должно быть последовательное имя в конкурирующих процессах.Я обновил код соответственно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...