It seems the rules about concurrent access are undocumented (on the Haskell side) and simply assume the developer is familiar with the particular backend being used. For production needs this is a perfectly legitimate assumption, but for casual prototyping and development it would be nice if the persistent-* packages were a bit more self contained.
So, what are the rules governing concurrent access to persistent-sqlite and family? Implicitly, there must be some degree of concurrency allowed if we have pools of connections, but trivially creating a single connection pool and calling replicateM x $ forkIO (useThePool connectionPool)
gives the below error.
user error (SQLite3 returned ErrorBusy while attempting to perform step.)
EDIT: Some example code is now below.
In the below code I fork off 6 threads (an arbitrary number - my actual application does 3 threads). Each thread constantly stores and looks up a record (a unique record from the one being accessed by the other threads, but that doesn't matter), printing one of the fields.
{-# 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 The values of 40
, TEST
, and all records are arbitrary for this example. Many values, including more realistic ones, cause the same behavior.
Also note that, while it might be obviously broken when you nest a non-terminating action (via forever
) inside of a DB transaction (started by runSqlPool
), this isn't the core issue. You can invert those operations and make the transactions arbitrarily small but still end up with periodic exceptions.
The output is usually like:
$ ./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.)