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

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

Итак, каковы правила, регулирующие одновременный доступ к persistent-sqlite и family? Неявно, должна быть разрешена некоторая степень параллелизма, если у нас есть пулы соединений, но тривиально создание единого пула соединений и вызов 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.)

1 ответ

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

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

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

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

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

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

Другие вопросы по тегам