Каковы правила одновременного доступа к постоянной базе данных

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

Итак, каковы правила, регулирующие одновременный доступ к persistence-sqlite и семейству? Неявно должна быть разрешена некоторая степень параллелизма, если у нас есть пулы соединений, но тривиальное создание одного пула соединений и вызов replicateM x $ forkIO (useThePool connectionPool) дает следующую ошибку.

user error (SQLite3 returned ErrorBusy while attempting to perform step.)

РЕДАКТИРОВАТЬ: Ниже приведен пример кода.

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

{-# LANGUAGE TemplateHaskell, QuasiQuotes
           , TypeFamilies, FlexibleContexts, GADTs
           , OverloadedStrings #-}
import Control.Concurrent (forkIO, threadDelay)
import Database.Persist
import Database.Persist.Sqlite hiding (get)
import Database.Persist.TH
import Control.Monad
import Control.Monad.IO.Class

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
SomeData
    myId Int
    myData Double
    MyId myId
|]

main = withSqlitePool "TEST" 40 $ \pool -> do
  runSqlPool (runMigration migrateAll) pool
  mapM_ forkIO [runSqlPool (dbThread i) pool | i <- [0..5]]
  threadDelay maxBound

dbThread :: Int -> SqlPersist IO ()
dbThread i = forever $ do
   x <- getBy (MyId i)
   insert (SomeData i (fromIntegral i))
   liftIO (print x)
   liftIO (threadDelay 100000) -- Just to calm down the CPU,
                               -- not needed for demonstrating
                               -- the problem

NB. Значения 40, TEST и всех записей для этого примера произвольны. Многие значения, в том числе более реалистичные, вызывают такое же поведение.

Также обратите внимание, что, хотя это может быть явно нарушено, когда вы вкладываете не завершающее действие (через forever) в транзакцию БД (запущенную runSqlPool), это не основная проблема. Вы можете инвертировать эти операции и сделать транзакции сколь угодно маленькими, но все равно получать периодические исключения.

Вывод обычно такой:

$ ./so
Nothing
so: user error (SQLite3 returned ErrorBusy while attempting to perform step.)
so: user error (SQLite3 returned ErrorBusy while attempting to perform step.)
so: user error (SQLite3 returned ErrorBusy while attempting to perform step.)
so: user error (SQLite3 returned ErrorBusy while attempting to perform step.)
so: user error (SQLite3 returned ErrorBusy while attempting to perform step.)
so: user error (SQLite3 returned ErrorConstraint while attempting to perform step.)

person Thomas M. DuBuisson    schedule 29.01.2012    source источник
comment
Не могли бы вы рассказать подробнее о useThePool?   -  person Dan Burton    schedule 30.01.2012
comment
@DanBurton Я дал больше информации через редактирование. Теперь с примером кода, который, вероятно, абсолютно неверен для тех, кто хоть что-то знает о персистентном коде.   -  person Thomas M. DuBuisson    schedule 30.01.2012
comment
@ThomasM.DuBuisson, вы пытались использовать select вместо вставки и посмотреть, сможете ли вы воспроизвести ошибку? Если ошибка не возникает при выборе, ошибка может быть вызвана исключением предотвращения взаимоблокировки, особенно если вы пытаетесь выполнять одновременные вставки. Однако это не имеет смысла, если у вас есть пул потоков.   -  person Sal    schedule 30.01.2012
comment
Соответствующая тема здесь: comments.gmane.org/gmane.comp.lang .haskell.yesod/1268   -  person ja.    schedule 03.02.2012
comment
У вас есть последняя версия sqlite 3.7.10?   -  person unludo    schedule 09.02.2012


Ответы (1)


Стоит отметить, что SQLite имеет проблемы с блокировкой при хранении на томах, подобных NFS (vboxsf, NFS, SMB, mvfs и т. д.), во многих системах, из-за которых SQLite выдает эту ошибку даже до того, как вы успешно открыли базу данных. Эти тома могут неправильно реализовывать блокировки чтения/записи fcntl(). ( http://www.sqlite.org/faq.html#q5 )

Предполагая, что это не проблема, также стоит упомянуть, что SQLite на самом деле изначально не поддерживает одновременные «соединения» ( http://www.sqlite.org/faq.html#q6 ), так как он использует блокировки файловой системы, чтобы гарантировать, что две операции записи не будут выполняться одновременно. (См. раздел 3.0 документа http://www.sqlite.org/lockingv3.html).

Предполагая, что все это известно, вы также можете проверить, какая версия sqlite3 доступна для вашей среды, поскольку в серии 3.x произошли некоторые изменения в способе получения различных типов блокировок: http://www.sqlite.org/sharedcache.html

Изменить: некоторая дополнительная информация из библиотеки persist-sqlite3 This package includes a thin sqlite3 wrapper based on the direct-sqlite package, as well as the entire C library

«Тонкая» обертка заставила меня взглянуть на нее, чтобы увидеть, насколько она тонкая; Глядя на код, не похоже, что постоянная оболочка имеет какие-либо средства защиты от сбоя оператора в пуле, за исключением необходимого средства защиты для перевода/выдачи ошибки и прерывания выполнения, хотя я должен сделать предостережение, с которым мне неудобно Хаскелл.

Похоже, вам придется защищаться от сбоя оператора в пуле и повторной попытки или ограничить размер пула при инициализации до 1 (что кажется менее чем идеальным).

person Tom B    schedule 03.02.2012