franz-0.5: src/Database/Franz/Server.hs
{-# LANGUAGE DeriveGeneric, LambdaCase, OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeFamilies #-}
module Database.Franz.Server
( Settings(..)
, startServer
, FranzPrefix(..)
, defaultPort
) where
import Control.Concurrent
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Cont
import Control.Concurrent.STM
import Database.Franz.Internal.Fuse
import Database.Franz.Internal.IO
import Database.Franz.Internal.Protocol
import Database.Franz.Internal.Reader
import Data.ConcurrentResourceMap
import Data.Serialize
import qualified Data.IntMap.Strict as IM
import Data.IORef
import qualified Data.ByteString.Char8 as B
import qualified Data.HashMap.Strict as HM
import Data.Tuple (swap)
import qualified Network.Socket.SendFile.Handle as SF
import qualified Network.Socket.ByteString as SB
import qualified Network.Socket as S
import System.Directory
import System.IO
import System.Process (ProcessHandle, getPid)
data Env = Env
{ prefix :: FranzPrefix
, path :: FranzDirectory
, franzReader :: FranzReader
, refThreads :: IORef (IM.IntMap ThreadId) -- ^ thread pool of pending requests
, recvBuffer :: IORef B.ByteString -- ^ received but unconsumed bytes
, vConn :: MVar S.Socket -- ^ connection to the client
}
trySTM :: Exception e => STM a -> STM (Either e a)
trySTM m = fmap Right m `catchSTM` (pure . Left)
handleRaw :: Env -> RawRequest -> IO ()
handleRaw env@Env{..} (RawRequest reqId req) = do
let pop result = do
case result of
Left ex | Just e <- fromException ex -> sendHeader env $ ResponseError reqId e
_ -> pure ()
`finally` popThread env reqId
handleQuery prefix franzReader path req throwIO $ \stream query ->
atomically (trySTM query) >>= \case
Left e -> sendHeader env $ ResponseError reqId e
Right (ready, offsets)
| ready -> sendContents env (Response reqId) stream offsets
| otherwise -> do
tid <- flip forkFinally pop $ bracket_
(atomically $ addActivity stream)
(removeActivity stream) $ do
sendHeader env $ ResponseWait reqId
join $ atomically $ trySTM query >>= \case
Left e -> pure $ sendHeader env $ ResponseError reqId e
Right (ready', offsets') -> do
check ready'
pure $ sendContents env (Response reqId) stream offsets'
-- Store the thread ID of the thread yielding a future
-- response such that we can kill it mid-way if user
-- sends a cancel request or we're killed with an
-- exception.
atomicModifyIORef' refThreads $ \m -> (IM.insert reqId tid m, ())
`catch` \e -> sendHeader env $ ResponseError reqId e
handleRaw env (RawClean reqId) = do
tid <- popThread env reqId
mapM_ killThread tid
-- | Pick up a 'ThreadId' from a pool.
popThread :: Env -> Int -> IO (Maybe ThreadId)
popThread Env{..} reqId = atomicModifyIORef' refThreads
$ swap . IM.updateLookupWithKey (\_ _ -> Nothing) reqId
sendHeader :: Env -> ResponseHeader -> IO ()
sendHeader Env{..} x = withMVar vConn $ \conn -> SB.sendAll conn $ encode x
sendContents :: Env -> ResponseHeader -> Stream -> QueryResult -> IO ()
sendContents Env{..} header Stream{..} ((s0, p0), (s1, p1)) = withMVar vConn $ \conn -> do
SB.sendAll conn $ encode (header, PayloadHeader s0 s1 p0 indexNames)
-- byte offset + number of indices
let siz = 8 * (length indexNames + 1)
-- Send byte offsets and indices
SF.sendFile' conn indexHandle (fromIntegral $ siz * succ s0) (fromIntegral $ siz * (s1 - s0))
-- Send payloads
SF.sendFile' conn payloadHandle (fromIntegral p0) (fromIntegral $ p1 - p0)
respond :: Env -> IO ()
respond env@Env{..} = do
recvConn <- readMVar vConn
runGetRecv recvBuffer recvConn get >>= \case
Right raw -> handleRaw env raw
Left err -> throwIO $ MalformedRequest err
data Settings = Settings
{ reapInterval :: Double
, streamLifetime :: Double
, port :: S.PortNumber
, livePrefix :: FranzPrefix
, archivePrefix :: Maybe FranzPrefix
, mountPrefix :: FranzPrefix
}
newtype MountMap v = MountMap (HM.HashMap FilePath v)
instance ResourceMap MountMap where
type Key MountMap = FilePath
empty = MountMap mempty
delete k (MountMap m) = MountMap (HM.delete k m)
insert k v (MountMap m) = MountMap (HM.insert k v m)
lookup k (MountMap m) = HM.lookup k m
newMountMap :: IO (ConcurrentResourceMap MountMap ProcessHandle)
newMountMap = newResourceMap
startServer
:: Settings
-> IO ()
startServer Settings{..} = evalContT $ do
franzReader <- ContT withFranzReader
liftIO $ do
forM_ archivePrefix $ \(FranzPrefix path) -> do
e <- doesDirectoryExist path
unless e $ error $ "archive prefix " ++ path ++ " doesn't exist"
_ <- liftIO $ forkIO $ reaper reapInterval streamLifetime franzReader
hSetBuffering stderr LineBuffering
vMounts <- liftIO newMountMap
let hints = S.defaultHints { S.addrFlags = [S.AI_NUMERICHOST, S.AI_NUMERICSERV], S.addrSocketType = S.Stream }
addr:_ <- liftIO $ S.getAddrInfo (Just hints) (Just "0.0.0.0") (Just $ show port)
-- obtain a socket and start listening
sock <- ContT $ bracket (S.socket (S.addrFamily addr) S.Stream (S.addrProtocol addr)) S.close
liftIO $ do
S.setSocketOption sock S.ReuseAddr 1
S.setSocketOption sock S.NoDelay 1
S.bind sock $ S.addrAddress addr
S.listen sock S.maxListenQueue
logServer ["Listening on", show port]
liftIO $ forever $ do
(conn, connAddr) <- S.accept sock
void $ forkFinally (accept Settings{..} vMounts franzReader conn connAddr) $ \result -> do
case result of
Left ex -> case fromException ex of
Just e -> SB.sendAll conn $ encode $ ResponseError (-1) e
Nothing -> logServer [show ex]
Right _ -> return ()
S.close conn
logServer [show connAddr, "disconnected"]
accept :: Settings -> ConcurrentResourceMap MountMap ProcessHandle -> FranzReader -> S.Socket -> S.SockAddr -> IO ()
accept Settings{..} vMounts franzReader conn connAddr = do
-- buffer of received octets
recvBuffer <- newIORef B.empty
let respondLoop prefix path@(FranzDirectory dir) = do
SB.sendAll conn apiVersion
logServer [show connAddr, show dir]
refThreads <- newIORef IM.empty
vConn <- newMVar conn
forever (respond Env{..}) `finally` do
readIORef refThreads >>= mapM_ killThread
path <- liftIO $ runGetRecv recvBuffer conn get >>= \case
Left _ -> throwIO $ MalformedRequest "Expecting a path"
Right pathBS -> pure $ FranzDirectory $ B.unpack pathBS
-- when the final reader exits, close all the streams associated to the path
let closeGroup = do
streams <- atomically $ do
streams <- readTVar $ vStreams franzReader
writeTVar (vStreams franzReader) $ HM.delete path streams
pure streams
forM_ (HM.lookup path streams) $ mapM_ closeStream
case archivePrefix of
-- just start a session without thinking about archives
Nothing -> respondLoop livePrefix path
-- Mount a squashfs image and increment the counter
Just prefix | src <- getFranzDirectory prefix path -> do
-- check if an archive exists
exist <- doesFileExist src
if exist
then withFuse vMounts closeGroup src (getFranzDirectory mountPrefix path) $ respondLoop mountPrefix path
else do
logServer ["Archive", src, "doesn't exist; falling back to live streams"]
respondLoop livePrefix path
logServer :: MonadIO m => [String] -> m ()
logServer = liftIO . hPutStrLn stderr . unwords . (:) "[server]"
withFuse :: ConcurrentResourceMap MountMap ProcessHandle
-> IO () -- run when the final user is about to exit
-> FilePath
-> FilePath
-> IO a -> IO a
withFuse vMounts release src dst body = withSharedResource vMounts dst
(mountFuse logServer (throwIO . InternalError) src dst)
(\fuse -> do
release
killFuse logServer fuse dst `finally` do
pid <- getPid fuse
forM_ pid $ \p -> logServer ["Undead squashfuse detected:", show p])
(const body)