[Haskell-cafe] Database connection pool

Michael Snoyman michael at snoyman.com
Fri May 7 08:50:43 EDT 2010


On Fri, May 7, 2010 at 1:02 AM, Bas van Dijk <v.dijk.bas at gmail.com> wrote:

> On Thu, May 6, 2010 at 11:54 PM, Bas van Dijk <v.dijk.bas at gmail.com>
> wrote:
> > On Thu, May 6, 2010 at 7:48 PM, Bas van Dijk <v.dijk.bas at gmail.com>
> wrote:
> >> On Thu, May 6, 2010 at 3:24 PM, Michael Snoyman <michael at snoyman.com>
> wrote:
> >>>
> >>>
> >>> On Thu, May 6, 2010 at 9:13 AM, Bryan O'Sullivan <bos at serpentine.com>
> wrote:
> >>>>
> >>>> On Wed, May 5, 2010 at 10:51 PM, Michael Snoyman <michael at snoyman.com
> >
> >>>> wrote:
> >>>>>
> >>>>> * When a connection is released, is goes to the end of the pool, so
> >>>>> connections get used evenly (not sure if this actually matters in
> practice).
> >>>>
> >>>> In practice, you're better off letting idle connections stay that way,
> >>>> because then your DB server can close connections and free up
> resources. In
> >>>> other words, when you're done with a connection, put it at the front
> of the
> >>>> reuse queue, not the back.
> >>>> You'll also want to handle the possibility that a connection that you
> grab
> >>>> from the pool has been closed by the server. Many connection pooling
> >>>> implementations I've seen get this wrong in subtle or expensive ways.
> >>>
> >>> Thanks for the feedback. I've gone ahead and implemented a simple
> resource
> >>> pool module. Since I need it to work with monad transformer stacks,
> I've
> >>> built it on top of MonadCatchIO-transformers. I've put the code up in a
> gist
> >>> on github[1]. I would appreciate if anyone could review this,
> especially to
> >>> make sure the exception handling code is correct. block and unblock in
> >>> particular concern me.
> >>> Thanks,
> >>> Michael
> >>> [1] http://gist.github.com/392078
> >>
> >> I also have a suggestion for your design. (Note however that I don't
> >> have much experience with resource pools.)
> >>
> >> In your current design a Pool has a fixed maximum number of opened
> >> resources. I can imagine situations where the maximum number of opened
> >> resources can change dynamically. For example due to plugging in (or
> >> out) a new blade server at run-time which will increase (or decrease)
> >> the maximum number of resources that can be handled.
> >>
> >> So what about changing:
> >>
> >> createPool :: IO a -> Int -> IO (Pool a)
> >> to:
> >> createPool :: IO (Maybe a) -> IO (Pool a)
> >>
> >> so, instead of statically storing the maximum number of  opened
> >> resources (Int), the resource creation function will decide itself
> >> when it has created enough (Maybe a).
> >>
> >> Regards,
> >>
> >> Bas
> >>
> >
> > How about something like this:
> >
> >
> --------------------------------------------------------------------------------
> > {-# LANGUAGE NoImplicitPrelude #-} -- (I like to be explicit)
> >
> > module Pool (Pool, new, withPool) where
> >
> > import Data.Function               ( ($), (.) )
> > import Data.Maybe                  ( Maybe(Nothing,Just), maybe )
> > import Data.Functor                ( (<$>) )
> > import Control.Monad               ( return, (>>=), (>>), (=<<), fail,
> > join, liftM )
> > import Control.Monad.STM           ( atomically )
> > import Control.Concurrent.STM.TVar ( TVar, newTVarIO, readTVar, writeTVar
> )
> > import Control.Monad.CatchIO       ( MonadCatchIO, block, finally )
> > import Control.Monad.IO.Class      ( liftIO )
> >
> > newtype Pool r = Pool (TVar [r])
> >
> > new :: MonadCatchIO m => m (Pool r)
> > new = liftIO $ Pool <$> newTVarIO []
> >
> > withPool :: MonadCatchIO m => Pool r -> m (Maybe r) -> (r -> m a) -> m
> (Maybe a)
> > withPool (Pool tv) mk f = block $ join $ liftIO $ atomically $ do
> >  rrs <- readTVar tv
> >  case rrs of
> >    [] -> return $ mk >>= maybe (return Nothing) with
> >    r:rs -> writeTVar tv rs >> return (with r)
> >    where
> >      with r = liftM Just (f r)
> >                `finally`
> >                  liftIO (atomically $ writeTVar tv . (r:) =<< readTVar
> tv)
> >
> --------------------------------------------------------------------------------
> >
> > Note that I don't store the resource creation action (m (Maybe r))
> > inside the pool. It's just passed as an argument to withPool.
> >
> > Regards,
> >
> > Bas
> >
>
> Note that it's probably better to pass the resource creation action as
> the first argument to withPool:
>
> withPool :: MonadCatchIO m => m (Maybe r) -> Pool r -> (r -> m a) -> m
> (Maybe a)
>
> This way it's easier to create specialized withPool functions by
> partially applying a specific resource creation action to withPool as
> in:
>
> withDBConsPool :: MonadCatchIO m => Pool DBCon -> (DBCon -> m a) -> m
> (Maybe a)
> withDBConsPool = withPool connectWithDB
>
> Regards,
>
> Bas
>

Bas,

Thank you for all the very thorough comments. If I'm understanding
correctly, there are two categories of suggestion:

1) Make the resource exhaustion mechanism more extensible.
2) Avoid "wormholes"

Please tell me if I've missed something.

Regarding (1), I think your approach is definitely better for complex pools;
however, for the usually case, I think it would present a more difficult API
for users. I could definitely imagine wrapping an easier-to-use interface
around your final example.

Regarding (2), I was not aware of it, thank you for updating me on the
issue.

So, here's my idea of how to wrap your Pool module to provide a simple
maximum-resource-count exhaustion algorithm.

{-# LANGUAGE PackageImports #-}

module EasyPool
    ( EasyPool
    , withEasyPool
    , newEasyPool
    ) where

import Pool
import Control.Monad.STM
import Control.Concurrent.STM.TVar
import Control.Monad.IO.Class
import "MonadCatchIO-transformers" Control.Monad.CatchIO

data EasyPool r m = EasyPool
    { epPool :: Pool r
    , epMake :: m (Maybe r)
    }

withEasyPool :: MonadCatchIO m => EasyPool r m -> (r -> m a) -> m (Maybe a)
withEasyPool (EasyPool pool mk) = withPool pool mk

newEasyPool :: MonadCatchIO m => Int -> m r -> m (EasyPool r m)
newEasyPool count mk = do
    pool <- new
    texist <- liftIO $ newTVarIO 0
    return $ EasyPool pool $ mk' texist
  where
    mk' texist = do
        exist <- liftIO $ atomically $ readTVar texist
        if exist >= count
            then return Nothing
            else do
                r <- mk
                liftIO $ atomically $ do
                    exist <- readTVar texist
                    if exist >= count
                        then return Nothing
                        else return $ Just r
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://www.haskell.org/pipermail/haskell-cafe/attachments/20100507/0208ea3f/attachment.html


More information about the Haskell-Cafe mailing list