Подтвердить что ты не робот

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

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

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

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

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

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

4b9b3361

Ответ 2

Вот грубый первый разрез, основанный на атомарности базового 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

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

#!/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).

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