hasql-pool 0.7.2.1 → 0.8
raw patch · 7 files changed
+233/−117 lines, 7 filesdep +asyncdep ~hasqlPVP ok
version bump matches the API change (PVP)
Dependencies added: async
Dependency ranges changed: hasql
API changes (from Hackage documentation)
- Hasql.Pool: PoolIsReleasedUsageError :: UsageError
+ Hasql.Pool: AcquisitionTimeoutUsageError :: UsageError
- Hasql.Pool: acquire :: Int -> Settings -> IO Pool
+ Hasql.Pool: acquire :: Int -> Maybe Int -> Settings -> IO Pool
- Hasql.Pool: acquireDynamically :: Int -> IO Settings -> IO Pool
+ Hasql.Pool: acquireDynamically :: Int -> Maybe Int -> IO Settings -> IO Pool
Files
- CHANGELOG.md +12/−0
- hasql-pool.cabal +32/−53
- library/Hasql/Pool.hs +82/−46
- library/Hasql/Pool/Prelude.hs +0/−8
- library/Hasql/Pool/TimeExtras/Conversions.hs +10/−0
- library/Hasql/Pool/TimeExtras/IO.hs +8/−0
- test/Main.hs +89/−10
CHANGELOG.md view
@@ -1,3 +1,15 @@+# 0.8++`release` became reusable. You can use it to destroy the whole pool (same as before), but now also you can use it to reset the connections.++Acquisition timeout added.++Breaking changes in API:++- Removed `PoolIsReleasedUsageError`+- `acquire` extended with the acquisition timeout parameter+- `acquireDynamically` extended with the acquisition timeout parameter+ # 0.7.2 Added support for dynamic connection configuration ([issue #11](https://github.com/nikita-volkov/hasql-pool/issues/11)).
hasql-pool.cabal view
@@ -1,74 +1,53 @@-name:- hasql-pool-version:- 0.7.2.1-category:- Hasql, Database, PostgreSQL-synopsis:- A pool of connections for Hasql-homepage:- https://github.com/nikita-volkov/hasql-pool -bug-reports:- https://github.com/nikita-volkov/hasql-pool/issues -author:- Nikita Volkov <nikita.y.volkov@mail.ru>-maintainer:- Nikita Volkov <nikita.y.volkov@mail.ru>-copyright:- (c) 2015, Nikita Volkov-license:- MIT-license-file:- LICENSE-build-type:- Simple-cabal-version:- >=1.10-extra-source-files:- CHANGELOG.md+cabal-version: 3.0 +name: hasql-pool+version: 0.8 +category: Hasql, Database, PostgreSQL+synopsis: Pool of connections for Hasql+homepage: https://github.com/nikita-volkov/hasql-pool+bug-reports: https://github.com/nikita-volkov/hasql-pool/issues+author: Nikita Volkov <nikita.y.volkov@mail.ru>+maintainer: Nikita Volkov <nikita.y.volkov@mail.ru>+copyright: (c) 2015, Nikita Volkov+license: MIT+license-file: LICENSE+extra-source-files: CHANGELOG.md+ source-repository head- type:- git- location:- git://github.com/nikita-volkov/hasql-pool.git+ type: git+ location: git://github.com/nikita-volkov/hasql-pool.git +common base-settings+ default-extensions: BangPatterns, BlockArguments, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DerivingVia, DeriveTraversable, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, InstanceSigs, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, StrictData, TemplateHaskell, TupleSections, TypeApplications, TypeFamilies, TypeOperators, UnboxedTuples+ default-language: Haskell2010 library- hs-source-dirs:- library- ghc-options:- default-extensions:- Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeFamilies, TypeOperators, UnboxedTuples- default-language:- Haskell2010+ import: base-settings+ hs-source-dirs: library exposed-modules: Hasql.Pool other-modules: Hasql.Pool.Prelude+ Hasql.Pool.TimeExtras.IO+ Hasql.Pool.TimeExtras.Conversions build-depends: base >=4.11 && <5,- hasql >=1.6 && <1.7,+ hasql >=1.6.0.1 && <1.7, stm >=2.5 && <3, time >=1.5 && <2,- transformers >=0.5 && <0.7-+ transformers >=0.5 && <0.7, test-suite test- type:- exitcode-stdio-1.0- hs-source-dirs:- test- main-is:- Main.hs- default-extensions:- Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeFamilies, TypeOperators, UnboxedTuples- default-language:- Haskell2010+ import: base-settings+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs+ ghc-options: -threaded build-depends:+ async >=2.2 && <3, hasql, hasql-pool, hspec >=2.6 && <3, rerebase >=1.15 && <2,- stm >=2.5 && <3+ stm >=2.5 && <3,
library/Hasql/Pool.hs view
@@ -17,48 +17,74 @@ import Hasql.Session (Session) import qualified Hasql.Session as Session +data ReuseConnection = Keep | Close+ -- | A pool of connections to DB. data Pool = Pool { -- | Connection settings. poolFetchConnectionSettings :: IO Connection.Settings,+ -- | Acquisition timeout, in microseconds.+ poolAcquisitionTimeout :: Maybe Int, -- | Avail connections. poolConnectionQueue :: TQueue Connection,- -- | Capacity.+ -- | Remaining capacity.+ -- The pool size limits the sum of poolCapacity, the length+ -- of length poolConnectionQueue and the number of in-flight+ -- connections. poolCapacity :: TVar Int,- -- | Alive.- poolAlive :: TVar Bool+ -- | Whether to return a connection to the pool.+ poolReuseToggle :: TVar (TVar ReuseConnection) } --- | Given the pool-size and connection settings create a connection-pool.+-- | Create a connection-pool. -- -- No connections actually get established by this function. It is delegated -- to 'use'.-acquire :: Int -> Connection.Settings -> IO Pool-acquire poolSize connectionSettings =- acquireDynamically poolSize (pure connectionSettings)+acquire ::+ -- | Pool size.+ Int ->+ -- | Connection acquisition timeout.+ Maybe Int ->+ -- | Connection settings.+ Connection.Settings ->+ IO Pool+acquire poolSize timeout connectionSettings =+ acquireDynamically poolSize timeout (pure connectionSettings) --- | Given the pool-size and connection settings constructor action--- create a connection-pool.------ No connections actually get established by this function. It is delegated--- to 'use'.+-- | Create a connection-pool. -- -- In difference to 'acquire' new settings get fetched each time a connection -- is created. This may be useful for some security models.-acquireDynamically :: Int -> IO Connection.Settings -> IO Pool-acquireDynamically poolSize fetchConnectionSettings = do- Pool fetchConnectionSettings+--+-- No connections actually get established by this function. It is delegated+-- to 'use'.+acquireDynamically ::+ -- | Pool size.+ Int ->+ -- | Connection acquisition timeout.+ Maybe Int ->+ -- | Action fetching connection settings settings.+ IO Connection.Settings ->+ IO Pool+acquireDynamically poolSize timeout fetchConnectionSettings = do+ Pool fetchConnectionSettings timeout <$> newTQueueIO <*> newTVarIO poolSize- <*> newTVarIO True+ <*> (newTVarIO =<< newTVarIO Keep) --- | Release all the connections in the pool.+-- | Release all the idle connections in the pool, and mark the in-use connections+-- to be released on return. Any connections acquired after the call will be+-- newly established. release :: Pool -> IO ()-release Pool {..} = do- connections <- atomically $ do- writeTVar poolAlive False- flushTQueue poolConnectionQueue- forM_ connections Connection.release+release Pool {..} =+ join . atomically $ do+ prevReuseToggle <- readTVar poolReuseToggle+ writeTVar prevReuseToggle Close+ newReuseToggle <- newTVar Keep+ writeTVar poolReuseToggle newReuseToggle+ conns <- flushTQueue poolConnectionQueue+ modifyTVar' poolCapacity (+ (length conns))+ return $ forM_ conns Connection.release -- | Use a connection from the pool to run a session and return the connection -- to the pool, when finished.@@ -68,32 +94,40 @@ -- and a slot gets freed up for a new connection to be established the next -- time one is needed. The error still gets returned from this function. use :: Pool -> Session.Session a -> IO (Either UsageError a)-use Pool {..} sess =+use Pool {..} sess = do+ timeout <- case poolAcquisitionTimeout of+ Just delta -> do+ delay <- registerDelay delta+ return $ readTVar delay+ Nothing ->+ return $ return False join . atomically $ do- alive <- readTVar poolAlive- if alive- then- asum- [ readTQueue poolConnectionQueue <&> onConn,- do- capVal <- readTVar poolCapacity- if capVal > 0- then do- writeTVar poolCapacity $! pred capVal- return onNewConn- else retry- ]- else return . return . Left $ PoolIsReleasedUsageError+ reuseToggle <- readTVar poolReuseToggle+ asum+ [ readTQueue poolConnectionQueue <&> onConn reuseToggle,+ do+ capVal <- readTVar poolCapacity+ if capVal > 0+ then do+ writeTVar poolCapacity $! pred capVal+ return $ onNewConn reuseToggle+ else retry,+ do+ timedOut <- timeout+ if timedOut+ then return . return . Left $ AcquisitionTimeoutUsageError+ else retry+ ] where- onNewConn = do+ onNewConn reuseToggle = do settings <- poolFetchConnectionSettings connRes <- Connection.acquire settings case connRes of Left connErr -> do atomically $ modifyTVar' poolCapacity succ return $ Left $ ConnectionUsageError connErr- Right conn -> onConn conn- onConn conn = do+ Right conn -> onConn reuseToggle conn+ onConn reuseToggle conn = do sessRes <- Session.run sess conn case sessRes of Left err -> case err of@@ -109,10 +143,12 @@ where returnConn = join . atomically $ do- alive <- readTVar poolAlive- if alive- then writeTQueue poolConnectionQueue conn $> return ()- else return $ Connection.release conn+ reuse <- readTVar reuseToggle+ case reuse of+ Keep -> writeTQueue poolConnectionQueue conn $> return ()+ Close -> do+ modifyTVar' poolCapacity succ+ return $ Connection.release conn -- | Union over all errors that 'use' can result in. data UsageError@@ -120,8 +156,8 @@ ConnectionUsageError Connection.ConnectionError | -- | Session execution failed. SessionUsageError Session.QueryError- | -- | Attempt to use a pool, which has already been called 'release' upon.- PoolIsReleasedUsageError+ | -- | Timeout acquiring a connection.+ AcquisitionTimeoutUsageError deriving (Show, Eq) instance Exception UsageError
library/Hasql/Pool/Prelude.hs view
@@ -1,6 +1,5 @@ module Hasql.Pool.Prelude ( module Exports,- getMillisecondsSinceEpoch, ) where @@ -81,10 +80,3 @@ import Text.Read as Exports (Read (..), readEither, readMaybe) import Unsafe.Coerce as Exports import Prelude as Exports hiding (all, and, any, concat, concatMap, elem, fail, foldl, foldl1, foldr, foldr1, id, mapM, mapM_, maximum, minimum, notElem, or, product, sequence, sequence_, sum, (.))--getMillisecondsSinceEpoch :: IO Int-getMillisecondsSinceEpoch =- fmap (fromIntegral . systemTimeToMicros) getSystemTime- where- systemTimeToMicros (MkSystemTime s ns) =- s * 1000 + fromIntegral (div ns 1000000)
+ library/Hasql/Pool/TimeExtras/Conversions.hs view
@@ -0,0 +1,10 @@+module Hasql.Pool.TimeExtras.Conversions where++import Hasql.Pool.Prelude++class ToMilliseconds a where+ toMilliseconds :: a -> Int++instance ToMilliseconds SystemTime where+ toMilliseconds (MkSystemTime s ns) =+ fromIntegral s * 1000 + fromIntegral (div ns 1000000)
+ library/Hasql/Pool/TimeExtras/IO.hs view
@@ -0,0 +1,8 @@+module Hasql.Pool.TimeExtras.IO where++import Hasql.Pool.Prelude+import Hasql.Pool.TimeExtras.Conversions++getMillisecondsSinceEpoch :: IO Int+getMillisecondsSinceEpoch =+ fmap toMilliseconds getSystemTime
test/Main.hs view
@@ -1,35 +1,39 @@ module Main where +import Control.Concurrent.Async (race)+import qualified Data.ByteString.Char8 as B8 import qualified Hasql.Connection as Connection import qualified Hasql.Decoders as Decoders import qualified Hasql.Encoders as Encoders import Hasql.Pool import qualified Hasql.Session as Session import qualified Hasql.Statement as Statement+import qualified System.Environment import Test.Hspec import Prelude -main = hspec $ do- describe "" $ do+main = do+ connectionSettings <- getConnectionSettings+ hspec . describe "" $ do it "Releases a spot in the pool when there is a query error" $ do- pool <- acquire 1 connectionSettings+ pool <- acquire 1 Nothing connectionSettings use pool badQuerySession `shouldNotReturn` (Right ()) use pool selectOneSession `shouldReturn` (Right 1) it "Simulation of connection error works" $ do- pool <- acquire 3 connectionSettings+ pool <- acquire 3 Nothing connectionSettings res <- use pool $ closeConnSession >> selectOneSession shouldSatisfy res $ \case Left (SessionUsageError (Session.QueryError _ _ (Session.ClientError _))) -> True _ -> False it "Connection errors cause eviction of connection" $ do- pool <- acquire 3 connectionSettings+ pool <- acquire 3 Nothing connectionSettings res <- use pool $ closeConnSession >> selectOneSession res <- use pool $ closeConnSession >> selectOneSession res <- use pool $ closeConnSession >> selectOneSession res <- use pool $ selectOneSession shouldSatisfy res $ isRight it "Connection gets returned to the pool after normal use" $ do- pool <- acquire 3 connectionSettings+ pool <- acquire 3 Nothing connectionSettings res <- use pool $ selectOneSession res <- use pool $ selectOneSession res <- use pool $ selectOneSession@@ -37,17 +41,75 @@ res <- use pool $ selectOneSession shouldSatisfy res $ isRight it "Connection gets returned to the pool after non-connection error" $ do- pool <- acquire 3 connectionSettings+ pool <- acquire 3 Nothing connectionSettings res <- use pool $ badQuerySession res <- use pool $ badQuerySession res <- use pool $ badQuerySession res <- use pool $ badQuerySession res <- use pool $ selectOneSession shouldSatisfy res $ isRight+ it "The pool remains usable after release" $ do+ pool <- acquire 1 Nothing connectionSettings+ res <- use pool $ selectOneSession+ release pool+ res <- use pool $ selectOneSession+ shouldSatisfy res $ isRight+ it "Getting and setting session variables works" $ do+ pool <- acquire 1 Nothing connectionSettings+ res <- use pool $ getSettingSession "testing.foo"+ res `shouldBe` Right Nothing+ res <- use pool $ do+ setSettingSession "testing.foo" "hello world"+ getSettingSession "testing.foo"+ res `shouldBe` Right (Just "hello world")+ it "Session variables stay set when a connection gets reused" $ do+ pool <- acquire 1 Nothing connectionSettings+ res <- use pool $ setSettingSession "testing.foo" "hello world"+ res `shouldBe` Right ()+ res2 <- use pool $ getSettingSession "testing.foo"+ res2 `shouldBe` Right (Just "hello world")+ it "Releasing the pool resets session variables" $ do+ pool <- acquire 1 Nothing connectionSettings+ res <- use pool $ setSettingSession "testing.foo" "hello world"+ res `shouldBe` Right ()+ release pool+ res <- use pool $ getSettingSession "testing.foo"+ res `shouldBe` Right Nothing+ it "Times out connection acquisition" $ do+ pool <- acquire 1 (Just 1000) connectionSettings -- 1ms timeout+ sleeping <- newEmptyMVar+ t0 <- getCurrentTime+ res <-+ race+ ( use pool $+ liftIO $ do+ putMVar sleeping ()+ threadDelay 1000000 -- 1s+ )+ ( do+ takeMVar sleeping+ use pool $ selectOneSession+ )+ t1 <- getCurrentTime+ res `shouldBe` Right (Left AcquisitionTimeoutUsageError)+ diffUTCTime t1 t0 `shouldSatisfy` (< 0.5) -- 0.5s -connectionSettings :: Connection.Settings-connectionSettings =- "host=localhost port=5432 user=postgres dbname=postgres"+getConnectionSettings :: IO Connection.Settings+getConnectionSettings =+ B8.unwords . catMaybes+ <$> sequence+ [ setting "host" $ defaultEnv "POSTGRES_HOST" "localhost",+ setting "port" $ defaultEnv "POSTGRES_PORT" "5432",+ setting "user" $ defaultEnv "POSTGRES_USER" "postgres",+ setting "password" $ maybeEnv "POSTGRES_PASSWORD",+ setting "dbname" $ defaultEnv "POSTGRES_DBNAME" "postgres"+ ]+ where+ maybeEnv env = fmap B8.pack <$> System.Environment.lookupEnv env+ defaultEnv env val = Just . fromMaybe val <$> maybeEnv env+ setting label getEnv = do+ val <- getEnv+ return $ (\v -> label <> "=" <> v) <$> val selectOneSession :: Session.Session Int64 selectOneSession =@@ -66,3 +128,20 @@ closeConnSession = do conn <- ask liftIO $ Connection.release conn++setSettingSession :: Text -> Text -> Session.Session ()+setSettingSession name value = do+ Session.statement (name, value) statement+ where+ statement = Statement.Statement "SELECT set_config($1, $2, false)" encoder Decoders.noResult True+ encoder =+ contramap fst (Encoders.param (Encoders.nonNullable Encoders.text))+ <> contramap snd (Encoders.param (Encoders.nonNullable Encoders.text))++getSettingSession :: Text -> Session.Session (Maybe Text)+getSettingSession name = do+ Session.statement name statement+ where+ statement = Statement.Statement "SELECT current_setting($1, true)" encoder decoder True+ encoder = Encoders.param (Encoders.nonNullable Encoders.text)+ decoder = Decoders.singleRow (Decoders.column (Decoders.nullable Decoders.text))