Каковы правила одновременного доступа к постоянной базе данных
Кажется, что правила для одновременного доступа недокументированы (на стороне 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 (что кажется не идеальным).