同時アクセスに関するルールは (Haskell 側では) 文書化されていないようで、開発者が特定のバックエンドに精通していることを前提としているだけです。実稼働のニーズではこれは完全に正当な前提ですが、カジュアルなプロトタイピングと開発では、persistent-* パッケージがもう少し自己完結的であれば良いでしょう。
では、persistent-sqlite およびファミリへの同時アクセスを管理するルールは何でしょうか? 暗黙的に、接続プールがある場合はある程度の同時実行が許可されている必要がありますが、単一の接続プールを簡単に作成して呼び出すと、replicateM x $ forkIO (useThePool connectionPool)
以下のエラーが発生します。
user error (SQLite3 returned ErrorBusy while attempting to perform step.)
編集: いくつかのサンプルコードを以下に示します。
以下のコードでは、6 つのスレッドをフォークします (任意の数です。実際のアプリケーションでは 3 つのスレッドが実行されます)。各スレッドは、レコード (他のスレッドがアクセスしているレコードとは異なる一意のレコードですが、これは重要ではありません) を継続的に保存して検索し、フィールドの 1 つを出力します。
{-# 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
40
注意: この例では、、、およびすべてのレコードの値TEST
は任意です。より現実的な値も含め、多くの値で同じ動作が発生します。
forever
また、 DB トランザクション ( によって開始) 内に非終了アクション ( 経由) をネストすると明らかに壊れる場合がありますが、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
注目すべき点は、多くのシステムで NFS のようなボリューム (vboxsf、NFS、SMB、mvfs など) に保存されている場合、SQLite はロックの問題を抱えており、データベースを正常に開く前に SQLite がそのエラーを返すことです。これらのボリュームは、fcntl() 読み取り/書き込みロックを誤って実装している可能性があります。(http://www.sqlite.org/faq.html#q5)
それが問題ではないと仮定すると、SQLite は実際には同時「接続」をネイティブにサポートしていないことも言及する価値があります (http://www.sqlite.org/faq.html#q6)ファイルシステムロックを使用して、2つの書き込みが同時に発生しないようにするためです。(http://www.sqlite.org/lockingv3.html)
これらすべてがわかっていると仮定すると、3.x シリーズではさまざまな種類のロックを取得する方法にいくつかの変更があったため、環境で使用できる sqlite3 のバージョンも確認できます。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
「薄い」ラッパーを見て、どれだけ薄いのか調べてみることにしました。コードを見ると、エラーを変換/発行して実行を中断するために必要なガード以外に、永続ラッパーにはプールへのステートメントの失敗に対するガードがないように見えますが、私は Haskell に慣れていないという警告を述べなければなりません。
プール内のステートメントが失敗して再試行されることを防ぐか、初期化時にプール サイズを 1 に制限する必要があるようです (これは理想的とは言えません)。