@@ -97,6 +97,7 @@ module Database.PostgreSQL.Simple
9797-- , Base.insertID
9898 -- * Transaction handling
9999 , withTransaction
100+ , withTransactionSerializable
100101 , TransactionMode(..)
101102 , IsolationLevel(..)
102103 , ReadWriteMode(..)
@@ -105,6 +106,7 @@ module Database.PostgreSQL.Simple
105106 , defaultReadWriteMode
106107 , withTransactionLevel
107108 , withTransactionMode
109+ , withTransactionModeRetry
108110-- , Base.autocommit
109111 , begin
110112 , beginLevel
@@ -122,7 +124,8 @@ import Blaze.ByteString.Builder.Char8 (fromChar)
122124import Control.Applicative ((<$>), pure)
123125import Control.Concurrent.MVar
124126import Control.Exception
125− ( Exception, onException, throw, throwIO, finally )
127+ ( Exception, onException, throw, throwIO, finally
128+ , try, SomeException, fromException )
126129import Control.Monad (foldM)
127130import Data.ByteString (ByteString)
128131import Data.Int (Int64)
@@ -643,6 +646,24 @@ defaultReadWriteMode = DefaultReadWriteMode
643646withTransaction :: Connection -> IO a -> IO a
644647withTransaction = withTransactionMode defaultTransactionMode
645648
649+-- | Execute an action inside of a 'Serializable' transaction. If a
650+-- serialization failure occurs, roll back the transaction and try again.
651+-- Be warned that this may execute the IO action multiple times.
652+--
653+-- A 'Serializable' transaction creates the illusion that your program has
654+-- exclusive access to the database. This means that, even in a concurrent
655+-- setting, you can perform queries in sequence without having to worry about
656+-- what might happen between one statement and the next.
657+--
658+-- Think of it as STM, but without @retry@.
659+withTransactionSerializable :: Connection -> IO a -> IO a
660+withTransactionSerializable =
661+ withTransactionModeRetry
662+ TransactionMode
663+ { isolationLevel = Serializable
664+ , readWriteMode = ReadWrite
665+ }
666+
646667-- | Execute an action inside a SQL transaction with a given isolation level.
647668withTransactionLevel :: IsolationLevel -> Connection -> IO a -> IO a
648669withTransactionLevel lvl
@@ -657,6 +678,37 @@ withTransactionMode mode conn act =
657678 commit conn
658679 return r
659680
681+-- | Like 'withTransactionMode', but if a 'SqlError' arises whose 'sqlState' is
682+-- @\"40001\"@ (@serialization_failure@), this will issue a @ROLLBACK@, then
683+-- try the action again. If any other exception arises, this will issue a
684+-- @ROLLBACK@, but will propagate the exception instead of retrying.
685+--
686+-- This is used to implement 'withTransactionSerializable'.
687+withTransactionModeRetry :: TransactionMode -> Connection -> IO a -> IO a
688+withTransactionModeRetry mode conn act =
689+ mask $ \restore ->
690+ retryLoop $ try $ do
691+ a <- restore act
692+ commit conn
693+ return a
694+ where
695+ retryLoop :: IO (Either SomeException a) -> IO a
696+ retryLoop act' = do
697+ beginMode mode conn
698+ r <- act'
699+ case r of
700+ Left e -> do
701+ rollback conn
702+ case fromException e of
703+ Just SqlError{..} | sqlState == serialization_failure
704+ -> retryLoop act'
705+ _ -> throwIO e
706+ Right a ->
707+ return a
708+
709+ -- http://www.postgresql.org/docs/current/static/errcodes-appendix.html
710+ serialization_failure = "40001"
711+
660712-- | Rollback a transaction.
661713rollback :: Connection -> IO ()
662714rollback conn = execute_ conn "ABORT" >> return ()