packages feed

keter-2.1.0: src/Keter/PortPool.hs

{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell     #-}
{-# LANGUAGE TypeApplications    #-}
-- | Manages a pool of available ports and allocates them.
module Keter.PortPool
    ( -- * Types
      PortPool
      -- * Actions
    , getPort
    , releasePort
      -- * Initialize
    , start
    ) where

import           Keter.Common
import           Keter.Context
import           Data.Text           (pack)
import           Control.Applicative     ((<$>))
import           Control.Concurrent.MVar
import           Control.Exception
import           Control.Monad.IO.Class (liftIO)
import           Control.Monad.IO.Unlift (withRunInIO)
import           Control.Monad.Logger
import           Keter.Config
import           Network.Socket
import           Prelude                 hiding (log)

data PPState = PPState
    { ppAvail    :: ![Port]
    , ppRecycled :: !([Port] -> [Port])
    }

newtype PortPool = PortPool (MVar PPState)

-- | Gets an unassigned port number.
getPort :: PortPool
        -> KeterM cfg (Either SomeException Port)
getPort (PortPool mstate) =
    withRunInIO $ \rio -> modifyMVar mstate (rio . loop)
  where
    removePortMsg p = pack $
        "Port in use, removing from port pool: "
        ++ show p

    loop :: PPState -> KeterM cfg (PPState, Either SomeException Port)
    loop PPState {..} =
        case ppAvail of
            p:ps -> do
                let next = PPState ps ppRecycled
                res <- liftIO $ try $ listenOn $ show p
                case res of
                    Left (_ :: SomeException) -> do
                        $logInfo $ removePortMsg p
                        loop next
                    Right socket' -> do
                        res' <- liftIO $ try @SomeException $ close socket'
                        case res' of
                            Left e -> do
                                $logError $ pack $ show e
                                $logInfo $ removePortMsg p
                                loop next
                            Right () -> return (next, Right p)
            [] ->
                case ppRecycled [] of
                    [] -> return (PPState [] id, Left $ toException NoPortsAvailable)
                    ps -> loop $ PPState ps id

    listenOn port = do
        let hints = defaultHints {
                addrFlags = [AI_PASSIVE]
              , addrSocketType = Stream
              }
        addr:_ <- getAddrInfo (Just hints) Nothing (Just port)
        bracketOnError
             (socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
             (close)
             (\sock -> do
                 setSocketOption sock ReuseAddr 1
                 bind sock (addrAddress addr)
                 listen sock maxListenQueue
                 return sock
             )

-- | Return a port to the recycled collection of the pool.  Note that recycling
-- puts the new ports at the end of the queue (FIFO), so that if an application
-- holds onto the port longer than expected, there should be no issues.
releasePort :: PortPool -> Port -> IO ()
releasePort (PortPool mstate) p =
    modifyMVar_ mstate $ \(PPState avail recycled) -> return $ PPState avail $ recycled . (p:)

start :: PortSettings -> IO PortPool
start PortSettings{..} =
    PortPool <$> newMVar freshState
  where
    freshState = PPState portRange id