franz 0.4 → 0.5
raw patch · 20 files changed
+796/−737 lines, 20 filesdep ~basePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: base
API changes (from Hackage documentation)
- Database.Franz.Contents: getResponse :: Get Contents
- Database.Franz.Contents: readContents :: Stream -> QueryResult -> IO Contents
- Database.Franz.Internal: getInt64le :: Num a => Get a
- Database.Franz.Internal: hGetRange :: Handle -> Int -> Int -> IO ByteString
- Database.Franz.Internal: runGetRecv :: IORef ByteString -> Socket -> Get a -> IO (Either String a)
- Database.Franz.Protocol: AllItems :: RequestType
- Database.Franz.Protocol: ByIndex :: !IndexName -> !Int -> ItemRef
- Database.Franz.Protocol: BySeqNum :: !Int -> ItemRef
- Database.Franz.Protocol: ClientError :: !String -> FranzException
- Database.Franz.Protocol: IndexNotFound :: !IndexName -> ![IndexName] -> FranzException
- Database.Franz.Protocol: InternalError :: !String -> FranzException
- Database.Franz.Protocol: LastItem :: RequestType
- Database.Franz.Protocol: MalformedRequest :: !String -> FranzException
- Database.Franz.Protocol: PayloadHeader :: !Int -> !Int -> !Int -> !Vector IndexName -> PayloadHeader
- Database.Franz.Protocol: Query :: !StreamName -> !ItemRef -> !ItemRef -> !RequestType -> Query
- Database.Franz.Protocol: RawClean :: !ResponseId -> RawRequest
- Database.Franz.Protocol: RawRequest :: !ResponseId -> !Query -> RawRequest
- Database.Franz.Protocol: Response :: !ResponseId -> ResponseHeader
- Database.Franz.Protocol: ResponseError :: !ResponseId -> !FranzException -> ResponseHeader
- Database.Franz.Protocol: ResponseWait :: !ResponseId -> ResponseHeader
- Database.Franz.Protocol: StreamName :: ByteString -> StreamName
- Database.Franz.Protocol: StreamNotFound :: !FilePath -> FranzException
- Database.Franz.Protocol: [reqFrom] :: Query -> !ItemRef
- Database.Franz.Protocol: [reqStream] :: Query -> !StreamName
- Database.Franz.Protocol: [reqTo] :: Query -> !ItemRef
- Database.Franz.Protocol: [reqType] :: Query -> !RequestType
- Database.Franz.Protocol: [unStreamName] :: StreamName -> ByteString
- Database.Franz.Protocol: apiVersion :: ByteString
- Database.Franz.Protocol: data FranzException
- Database.Franz.Protocol: data ItemRef
- Database.Franz.Protocol: data PayloadHeader
- Database.Franz.Protocol: data Query
- Database.Franz.Protocol: data RawRequest
- Database.Franz.Protocol: data RequestType
- Database.Franz.Protocol: data ResponseHeader
- Database.Franz.Protocol: defaultPort :: PortNumber
- Database.Franz.Protocol: instance Data.Hashable.Class.Hashable Database.Franz.Protocol.StreamName
- Database.Franz.Protocol: instance Data.Serialize.Serialize Database.Franz.Protocol.FranzException
- Database.Franz.Protocol: instance Data.Serialize.Serialize Database.Franz.Protocol.ItemRef
- Database.Franz.Protocol: instance Data.Serialize.Serialize Database.Franz.Protocol.PayloadHeader
- Database.Franz.Protocol: instance Data.Serialize.Serialize Database.Franz.Protocol.Query
- Database.Franz.Protocol: instance Data.Serialize.Serialize Database.Franz.Protocol.RawRequest
- Database.Franz.Protocol: instance Data.Serialize.Serialize Database.Franz.Protocol.RequestType
- Database.Franz.Protocol: instance Data.Serialize.Serialize Database.Franz.Protocol.ResponseHeader
- Database.Franz.Protocol: instance Data.Serialize.Serialize Database.Franz.Protocol.StreamName
- Database.Franz.Protocol: instance Data.String.IsString Database.Franz.Protocol.StreamName
- Database.Franz.Protocol: instance GHC.Classes.Eq Database.Franz.Protocol.StreamName
- Database.Franz.Protocol: instance GHC.Classes.Ord Database.Franz.Protocol.StreamName
- Database.Franz.Protocol: instance GHC.Exception.Type.Exception Database.Franz.Protocol.FranzException
- Database.Franz.Protocol: instance GHC.Generics.Generic Database.Franz.Protocol.FranzException
- Database.Franz.Protocol: instance GHC.Generics.Generic Database.Franz.Protocol.ItemRef
- Database.Franz.Protocol: instance GHC.Generics.Generic Database.Franz.Protocol.Query
- Database.Franz.Protocol: instance GHC.Generics.Generic Database.Franz.Protocol.RawRequest
- Database.Franz.Protocol: instance GHC.Generics.Generic Database.Franz.Protocol.RequestType
- Database.Franz.Protocol: instance GHC.Generics.Generic Database.Franz.Protocol.ResponseHeader
- Database.Franz.Protocol: instance GHC.Show.Show Database.Franz.Protocol.FranzException
- Database.Franz.Protocol: instance GHC.Show.Show Database.Franz.Protocol.ItemRef
- Database.Franz.Protocol: instance GHC.Show.Show Database.Franz.Protocol.Query
- Database.Franz.Protocol: instance GHC.Show.Show Database.Franz.Protocol.RequestType
- Database.Franz.Protocol: instance GHC.Show.Show Database.Franz.Protocol.ResponseHeader
- Database.Franz.Protocol: instance GHC.Show.Show Database.Franz.Protocol.StreamName
- Database.Franz.Protocol: newtype StreamName
- Database.Franz.Protocol: streamNameToPath :: StreamName -> FilePath
- Database.Franz.Protocol: type IndexName = ByteString
- Database.Franz.Protocol: type ResponseId = Int
- Database.Franz.Reader: CaughtUp :: StreamStatus
- Database.Franz.Reader: FranzDirectory :: FilePath -> FranzDirectory
- Database.Franz.Reader: FranzPrefix :: FilePath -> FranzPrefix
- Database.Franz.Reader: FranzReader :: WatchManager -> TVar (HashMap FranzDirectory (HashMap StreamName Stream)) -> FranzReader
- Database.Franz.Reader: Gone :: StreamStatus
- Database.Franz.Reader: Outdated :: StreamStatus
- Database.Franz.Reader: ReaperState :: !Int -> !Int -> ReaperState
- Database.Franz.Reader: Stream :: FilePath -> !TVar (IntMap Int) -> !Vector IndexName -> !HashMap IndexName (TVar (IntMap Int)) -> !TVar Int -> !TVar StreamStatus -> !ThreadId -> !Handle -> !Handle -> !TVar Activity -> Stream
- Database.Franz.Reader: [followThread] :: Stream -> !ThreadId
- Database.Franz.Reader: [indexHandle] :: Stream -> !Handle
- Database.Franz.Reader: [indexNames] :: Stream -> !Vector IndexName
- Database.Franz.Reader: [indices] :: Stream -> !HashMap IndexName (TVar (IntMap Int))
- Database.Franz.Reader: [payloadHandle] :: Stream -> !Handle
- Database.Franz.Reader: [prunedStreams] :: ReaperState -> !Int
- Database.Franz.Reader: [streamPath] :: Stream -> FilePath
- Database.Franz.Reader: [totalStreams] :: ReaperState -> !Int
- Database.Franz.Reader: [unFranzPrefix] :: FranzPrefix -> FilePath
- Database.Franz.Reader: [vActivity] :: Stream -> !TVar Activity
- Database.Franz.Reader: [vCount] :: Stream -> !TVar Int
- Database.Franz.Reader: [vOffsets] :: Stream -> !TVar (IntMap Int)
- Database.Franz.Reader: [vStatus] :: Stream -> !TVar StreamStatus
- Database.Franz.Reader: [vStreams] :: FranzReader -> TVar (HashMap FranzDirectory (HashMap StreamName Stream))
- Database.Franz.Reader: [watchManager] :: FranzReader -> WatchManager
- Database.Franz.Reader: addActivity :: Stream -> STM ()
- Database.Franz.Reader: closeFranzReader :: FranzReader -> IO ()
- Database.Franz.Reader: closeStream :: Stream -> IO ()
- Database.Franz.Reader: createStream :: WatchManager -> FilePath -> IO Stream
- Database.Franz.Reader: data FranzReader
- Database.Franz.Reader: data ReaperState
- Database.Franz.Reader: data Stream
- Database.Franz.Reader: data StreamStatus
- Database.Franz.Reader: getFranzDirectory :: FranzPrefix -> FranzDirectory -> FilePath
- Database.Franz.Reader: getFranzStreamPath :: FranzPrefix -> FranzDirectory -> StreamName -> FilePath
- Database.Franz.Reader: handleQuery :: FranzPrefix -> FranzReader -> FranzDirectory -> Query -> (FranzException -> IO r) -> (Stream -> STM (Bool, QueryResult) -> IO r) -> IO r
- Database.Franz.Reader: instance Data.Hashable.Class.Hashable Database.Franz.Reader.FranzDirectory
- Database.Franz.Reader: instance Data.Hashable.Class.Hashable Database.Franz.Reader.FranzPrefix
- Database.Franz.Reader: instance GHC.Classes.Eq Database.Franz.Reader.FranzDirectory
- Database.Franz.Reader: instance GHC.Classes.Eq Database.Franz.Reader.FranzPrefix
- Database.Franz.Reader: instance GHC.Classes.Eq Database.Franz.Reader.StreamStatus
- Database.Franz.Reader: newFranzReader :: IO FranzReader
- Database.Franz.Reader: newtype FranzDirectory
- Database.Franz.Reader: newtype FranzPrefix
- Database.Franz.Reader: range :: Int -> Int -> RequestType -> IntMap Int -> (Bool, QueryResult)
- Database.Franz.Reader: reaper :: Double -> Double -> FranzReader -> IO ()
- Database.Franz.Reader: removeActivity :: Stream -> IO ()
- Database.Franz.Reader: splitR :: Int -> IntMap a -> (IntMap a, IntMap a)
- Database.Franz.Reader: type Activity = Either Double Int
- Database.Franz.Reader: type QueryResult = ((Int, Int), (Int, Int))
- Database.Franz.Reader: withFranzReader :: (FranzReader -> IO ()) -> IO ()
- Database.Franz.Reconnect: Reconnect :: Reconnect
- Database.Franz.Reconnect: atomicallyReconnecting :: Int -> STM a -> IO a
- Database.Franz.Reconnect: data Pool
- Database.Franz.Reconnect: data Reconnect
- Database.Franz.Reconnect: fetchWithPool :: Pool -> Query -> (STM Response -> IO r) -> IO r
- Database.Franz.Reconnect: instance GHC.Classes.Eq Database.Franz.Reconnect.Reconnect
- Database.Franz.Reconnect: instance GHC.Exception.Type.Exception Database.Franz.Reconnect.Reconnect
- Database.Franz.Reconnect: instance GHC.Show.Show Database.Franz.Reconnect.Reconnect
- Database.Franz.Reconnect: poolLogFunc :: Pool -> String -> IO ()
- Database.Franz.Reconnect: poolRetryPolicy :: Pool -> RetryPolicyM IO
- Database.Franz.Reconnect: withPool :: RetryPolicyM IO -> (String -> IO ()) -> FranzPath -> (Pool -> IO a) -> IO a
- Database.Franz.Reconnect: withReconnection :: Pool -> (Connection -> IO a) -> IO a
- Database.Franz.Server: killFuse :: ProcessHandle -> FilePath -> IO ()
- Database.Franz.Server: mountFuse :: FilePath -> FilePath -> IO ProcessHandle
- Database.Franz.URI: FranzPath :: !HostName -> !PortNumber -> !FilePath -> FranzPath
- Database.Franz.URI: LocalFranzPath :: !FilePath -> FranzPath
- Database.Franz.URI: [franzDir] :: FranzPath -> !FilePath
- Database.Franz.URI: [franzHost] :: FranzPath -> !HostName
- Database.Franz.URI: [franzPort] :: FranzPath -> !PortNumber
- Database.Franz.URI: data FranzPath
- Database.Franz.URI: fromFranzPath :: (Monoid a, IsString a) => FranzPath -> a
- Database.Franz.URI: instance GHC.Classes.Eq Database.Franz.URI.FranzPath
- Database.Franz.URI: instance GHC.Classes.Ord Database.Franz.URI.FranzPath
- Database.Franz.URI: instance GHC.Show.Show Database.Franz.URI.FranzPath
- Database.Franz.URI: toFranzPath :: String -> Either String FranzPath
+ Database.Franz.Client.Reconnect: Reconnect :: Reconnect
+ Database.Franz.Client.Reconnect: atomicallyReconnecting :: Int -> STM a -> IO a
+ Database.Franz.Client.Reconnect: data Pool
+ Database.Franz.Client.Reconnect: data Reconnect
+ Database.Franz.Client.Reconnect: fetchWithPool :: Pool -> Query -> (STM Response -> IO r) -> IO r
+ Database.Franz.Client.Reconnect: instance GHC.Classes.Eq Database.Franz.Client.Reconnect.Reconnect
+ Database.Franz.Client.Reconnect: instance GHC.Exception.Type.Exception Database.Franz.Client.Reconnect.Reconnect
+ Database.Franz.Client.Reconnect: instance GHC.Show.Show Database.Franz.Client.Reconnect.Reconnect
+ Database.Franz.Client.Reconnect: poolLogFunc :: Pool -> String -> IO ()
+ Database.Franz.Client.Reconnect: poolRetryPolicy :: Pool -> RetryPolicyM IO
+ Database.Franz.Client.Reconnect: withPool :: RetryPolicyM IO -> (String -> IO ()) -> FranzPath -> (Pool -> IO a) -> IO a
+ Database.Franz.Client.Reconnect: withReconnection :: Pool -> (Connection -> IO a) -> IO a
+ Database.Franz.Internal.Contents: Contents :: !Vector IndexName -> !ByteString -> !IndexVec -> !Int -> !Int -> !Int -> Contents
+ Database.Franz.Internal.Contents: [indexNames] :: Contents -> !Vector IndexName
+ Database.Franz.Internal.Contents: [indicess] :: Contents -> !IndexVec
+ Database.Franz.Internal.Contents: [length] :: Contents -> !Int
+ Database.Franz.Internal.Contents: [payloadOffset] :: Contents -> !Int
+ Database.Franz.Internal.Contents: [payloads] :: Contents -> !ByteString
+ Database.Franz.Internal.Contents: [seqnoOffset] :: Contents -> !Int
+ Database.Franz.Internal.Contents: data Contents
+ Database.Franz.Internal.Contents: getResponse :: Get Contents
+ Database.Franz.Internal.Contents: readContents :: Stream -> QueryResult -> IO Contents
+ Database.Franz.Internal.Contents: type IndexVec = Vector (Int, Vector Int64)
+ Database.Franz.Internal.Fuse: killFuse :: ([String] -> IO ()) -> ProcessHandle -> FilePath -> IO ()
+ Database.Franz.Internal.Fuse: mountFuse :: ([String] -> IO ()) -> (forall x. String -> IO x) -> FilePath -> FilePath -> IO ProcessHandle
+ Database.Franz.Internal.IO: getInt64le :: Num a => Get a
+ Database.Franz.Internal.IO: hGetRange :: Handle -> Int -> Int -> IO ByteString
+ Database.Franz.Internal.IO: runGetRecv :: IORef ByteString -> Socket -> Get a -> IO (Either String a)
+ Database.Franz.Internal.Protocol: AllItems :: RequestType
+ Database.Franz.Internal.Protocol: ByIndex :: !IndexName -> !Int -> ItemRef
+ Database.Franz.Internal.Protocol: BySeqNum :: !Int -> ItemRef
+ Database.Franz.Internal.Protocol: ClientError :: !String -> FranzException
+ Database.Franz.Internal.Protocol: IndexNotFound :: !IndexName -> ![IndexName] -> FranzException
+ Database.Franz.Internal.Protocol: InternalError :: !String -> FranzException
+ Database.Franz.Internal.Protocol: LastItem :: RequestType
+ Database.Franz.Internal.Protocol: MalformedRequest :: !String -> FranzException
+ Database.Franz.Internal.Protocol: PayloadHeader :: !Int -> !Int -> !Int -> !Vector IndexName -> PayloadHeader
+ Database.Franz.Internal.Protocol: Query :: !StreamName -> !ItemRef -> !ItemRef -> !RequestType -> Query
+ Database.Franz.Internal.Protocol: RawClean :: !ResponseId -> RawRequest
+ Database.Franz.Internal.Protocol: RawRequest :: !ResponseId -> !Query -> RawRequest
+ Database.Franz.Internal.Protocol: Response :: !ResponseId -> ResponseHeader
+ Database.Franz.Internal.Protocol: ResponseError :: !ResponseId -> !FranzException -> ResponseHeader
+ Database.Franz.Internal.Protocol: ResponseWait :: !ResponseId -> ResponseHeader
+ Database.Franz.Internal.Protocol: StreamName :: ByteString -> StreamName
+ Database.Franz.Internal.Protocol: StreamNotFound :: !FilePath -> FranzException
+ Database.Franz.Internal.Protocol: [reqFrom] :: Query -> !ItemRef
+ Database.Franz.Internal.Protocol: [reqStream] :: Query -> !StreamName
+ Database.Franz.Internal.Protocol: [reqTo] :: Query -> !ItemRef
+ Database.Franz.Internal.Protocol: [reqType] :: Query -> !RequestType
+ Database.Franz.Internal.Protocol: [unStreamName] :: StreamName -> ByteString
+ Database.Franz.Internal.Protocol: apiVersion :: ByteString
+ Database.Franz.Internal.Protocol: data FranzException
+ Database.Franz.Internal.Protocol: data ItemRef
+ Database.Franz.Internal.Protocol: data PayloadHeader
+ Database.Franz.Internal.Protocol: data Query
+ Database.Franz.Internal.Protocol: data RawRequest
+ Database.Franz.Internal.Protocol: data RequestType
+ Database.Franz.Internal.Protocol: data ResponseHeader
+ Database.Franz.Internal.Protocol: defaultPort :: PortNumber
+ Database.Franz.Internal.Protocol: instance Data.Hashable.Class.Hashable Database.Franz.Internal.Protocol.StreamName
+ Database.Franz.Internal.Protocol: instance Data.Serialize.Serialize Database.Franz.Internal.Protocol.FranzException
+ Database.Franz.Internal.Protocol: instance Data.Serialize.Serialize Database.Franz.Internal.Protocol.ItemRef
+ Database.Franz.Internal.Protocol: instance Data.Serialize.Serialize Database.Franz.Internal.Protocol.PayloadHeader
+ Database.Franz.Internal.Protocol: instance Data.Serialize.Serialize Database.Franz.Internal.Protocol.Query
+ Database.Franz.Internal.Protocol: instance Data.Serialize.Serialize Database.Franz.Internal.Protocol.RawRequest
+ Database.Franz.Internal.Protocol: instance Data.Serialize.Serialize Database.Franz.Internal.Protocol.RequestType
+ Database.Franz.Internal.Protocol: instance Data.Serialize.Serialize Database.Franz.Internal.Protocol.ResponseHeader
+ Database.Franz.Internal.Protocol: instance Data.Serialize.Serialize Database.Franz.Internal.Protocol.StreamName
+ Database.Franz.Internal.Protocol: instance Data.String.IsString Database.Franz.Internal.Protocol.StreamName
+ Database.Franz.Internal.Protocol: instance GHC.Classes.Eq Database.Franz.Internal.Protocol.StreamName
+ Database.Franz.Internal.Protocol: instance GHC.Classes.Ord Database.Franz.Internal.Protocol.StreamName
+ Database.Franz.Internal.Protocol: instance GHC.Exception.Type.Exception Database.Franz.Internal.Protocol.FranzException
+ Database.Franz.Internal.Protocol: instance GHC.Generics.Generic Database.Franz.Internal.Protocol.FranzException
+ Database.Franz.Internal.Protocol: instance GHC.Generics.Generic Database.Franz.Internal.Protocol.ItemRef
+ Database.Franz.Internal.Protocol: instance GHC.Generics.Generic Database.Franz.Internal.Protocol.Query
+ Database.Franz.Internal.Protocol: instance GHC.Generics.Generic Database.Franz.Internal.Protocol.RawRequest
+ Database.Franz.Internal.Protocol: instance GHC.Generics.Generic Database.Franz.Internal.Protocol.RequestType
+ Database.Franz.Internal.Protocol: instance GHC.Generics.Generic Database.Franz.Internal.Protocol.ResponseHeader
+ Database.Franz.Internal.Protocol: instance GHC.Show.Show Database.Franz.Internal.Protocol.FranzException
+ Database.Franz.Internal.Protocol: instance GHC.Show.Show Database.Franz.Internal.Protocol.ItemRef
+ Database.Franz.Internal.Protocol: instance GHC.Show.Show Database.Franz.Internal.Protocol.Query
+ Database.Franz.Internal.Protocol: instance GHC.Show.Show Database.Franz.Internal.Protocol.RequestType
+ Database.Franz.Internal.Protocol: instance GHC.Show.Show Database.Franz.Internal.Protocol.ResponseHeader
+ Database.Franz.Internal.Protocol: instance GHC.Show.Show Database.Franz.Internal.Protocol.StreamName
+ Database.Franz.Internal.Protocol: newtype StreamName
+ Database.Franz.Internal.Protocol: streamNameToPath :: StreamName -> FilePath
+ Database.Franz.Internal.Protocol: type IndexName = ByteString
+ Database.Franz.Internal.Protocol: type ResponseId = Int
+ Database.Franz.Internal.Reader: CaughtUp :: StreamStatus
+ Database.Franz.Internal.Reader: FranzDirectory :: FilePath -> FranzDirectory
+ Database.Franz.Internal.Reader: FranzPrefix :: FilePath -> FranzPrefix
+ Database.Franz.Internal.Reader: FranzReader :: WatchManager -> TVar (HashMap FranzDirectory (HashMap StreamName Stream)) -> FranzReader
+ Database.Franz.Internal.Reader: Gone :: StreamStatus
+ Database.Franz.Internal.Reader: Outdated :: StreamStatus
+ Database.Franz.Internal.Reader: ReaperState :: !Int -> !Int -> ReaperState
+ Database.Franz.Internal.Reader: Stream :: FilePath -> !TVar (IntMap Int) -> !Vector IndexName -> !HashMap IndexName (TVar (IntMap Int)) -> !TVar Int -> !TVar StreamStatus -> !ThreadId -> !Handle -> !Handle -> !TVar Activity -> Stream
+ Database.Franz.Internal.Reader: [followThread] :: Stream -> !ThreadId
+ Database.Franz.Internal.Reader: [indexHandle] :: Stream -> !Handle
+ Database.Franz.Internal.Reader: [indexNames] :: Stream -> !Vector IndexName
+ Database.Franz.Internal.Reader: [indices] :: Stream -> !HashMap IndexName (TVar (IntMap Int))
+ Database.Franz.Internal.Reader: [payloadHandle] :: Stream -> !Handle
+ Database.Franz.Internal.Reader: [prunedStreams] :: ReaperState -> !Int
+ Database.Franz.Internal.Reader: [streamPath] :: Stream -> FilePath
+ Database.Franz.Internal.Reader: [totalStreams] :: ReaperState -> !Int
+ Database.Franz.Internal.Reader: [unFranzPrefix] :: FranzPrefix -> FilePath
+ Database.Franz.Internal.Reader: [vActivity] :: Stream -> !TVar Activity
+ Database.Franz.Internal.Reader: [vCount] :: Stream -> !TVar Int
+ Database.Franz.Internal.Reader: [vOffsets] :: Stream -> !TVar (IntMap Int)
+ Database.Franz.Internal.Reader: [vStatus] :: Stream -> !TVar StreamStatus
+ Database.Franz.Internal.Reader: [vStreams] :: FranzReader -> TVar (HashMap FranzDirectory (HashMap StreamName Stream))
+ Database.Franz.Internal.Reader: [watchManager] :: FranzReader -> WatchManager
+ Database.Franz.Internal.Reader: addActivity :: Stream -> STM ()
+ Database.Franz.Internal.Reader: closeFranzReader :: FranzReader -> IO ()
+ Database.Franz.Internal.Reader: closeStream :: Stream -> IO ()
+ Database.Franz.Internal.Reader: createStream :: WatchManager -> FilePath -> IO Stream
+ Database.Franz.Internal.Reader: data FranzReader
+ Database.Franz.Internal.Reader: data ReaperState
+ Database.Franz.Internal.Reader: data Stream
+ Database.Franz.Internal.Reader: data StreamStatus
+ Database.Franz.Internal.Reader: getFranzDirectory :: FranzPrefix -> FranzDirectory -> FilePath
+ Database.Franz.Internal.Reader: getFranzStreamPath :: FranzPrefix -> FranzDirectory -> StreamName -> FilePath
+ Database.Franz.Internal.Reader: handleQuery :: FranzPrefix -> FranzReader -> FranzDirectory -> Query -> (FranzException -> IO r) -> (Stream -> STM (Bool, QueryResult) -> IO r) -> IO r
+ Database.Franz.Internal.Reader: instance Data.Hashable.Class.Hashable Database.Franz.Internal.Reader.FranzDirectory
+ Database.Franz.Internal.Reader: instance Data.Hashable.Class.Hashable Database.Franz.Internal.Reader.FranzPrefix
+ Database.Franz.Internal.Reader: instance GHC.Classes.Eq Database.Franz.Internal.Reader.FranzDirectory
+ Database.Franz.Internal.Reader: instance GHC.Classes.Eq Database.Franz.Internal.Reader.FranzPrefix
+ Database.Franz.Internal.Reader: instance GHC.Classes.Eq Database.Franz.Internal.Reader.StreamStatus
+ Database.Franz.Internal.Reader: newFranzReader :: IO FranzReader
+ Database.Franz.Internal.Reader: newtype FranzDirectory
+ Database.Franz.Internal.Reader: newtype FranzPrefix
+ Database.Franz.Internal.Reader: range :: Int -> Int -> RequestType -> IntMap Int -> (Bool, QueryResult)
+ Database.Franz.Internal.Reader: reaper :: Double -> Double -> FranzReader -> IO ()
+ Database.Franz.Internal.Reader: removeActivity :: Stream -> IO ()
+ Database.Franz.Internal.Reader: splitR :: Int -> IntMap a -> (IntMap a, IntMap a)
+ Database.Franz.Internal.Reader: type Activity = Either Double Int
+ Database.Franz.Internal.Reader: type QueryResult = ((Int, Int), (Int, Int))
+ Database.Franz.Internal.Reader: withFranzReader :: (FranzReader -> IO ()) -> IO ()
+ Database.Franz.Internal.URI: FranzPath :: !HostName -> !PortNumber -> !FilePath -> FranzPath
+ Database.Franz.Internal.URI: LocalFranzPath :: !FilePath -> FranzPath
+ Database.Franz.Internal.URI: [franzDir] :: FranzPath -> !FilePath
+ Database.Franz.Internal.URI: [franzHost] :: FranzPath -> !HostName
+ Database.Franz.Internal.URI: [franzPort] :: FranzPath -> !PortNumber
+ Database.Franz.Internal.URI: data FranzPath
+ Database.Franz.Internal.URI: fromFranzPath :: (Monoid a, IsString a) => FranzPath -> a
+ Database.Franz.Internal.URI: instance GHC.Classes.Eq Database.Franz.Internal.URI.FranzPath
+ Database.Franz.Internal.URI: instance GHC.Classes.Ord Database.Franz.Internal.URI.FranzPath
+ Database.Franz.Internal.URI: instance GHC.Show.Show Database.Franz.Internal.URI.FranzPath
+ Database.Franz.Internal.URI: toFranzPath :: String -> Either String FranzPath
+ Database.Franz.Server: FranzPrefix :: FilePath -> FranzPrefix
+ Database.Franz.Server: [unFranzPrefix] :: FranzPrefix -> FilePath
+ Database.Franz.Server: newtype FranzPrefix
Files
- ChangeLog.md +5/−0
- README.md +29/−0
- app/client.hs +1/−1
- app/server.hs +0/−1
- franz.cabal +12/−10
- src/Database/Franz/Client.hs +8/−8
- src/Database/Franz/Client/Reconnect.hs +96/−0
- src/Database/Franz/Contents.hs +3/−45
- src/Database/Franz/Internal.hs +0/−58
- src/Database/Franz/Internal/Contents.hs +56/−0
- src/Database/Franz/Internal/Fuse.hs +38/−0
- src/Database/Franz/Internal/IO.hs +58/−0
- src/Database/Franz/Internal/Protocol.hs +99/−0
- src/Database/Franz/Internal/Reader.hs +330/−0
- src/Database/Franz/Internal/URI.hs +53/−0
- src/Database/Franz/Protocol.hs +0/−99
- src/Database/Franz/Reader.hs +0/−330
- src/Database/Franz/Reconnect.hs +0/−96
- src/Database/Franz/Server.hs +8/−36
- src/Database/Franz/URI.hs +0/−53
ChangeLog.md view
@@ -1,3 +1,8 @@+# v0.5++* Renamed internal modules under `Internal` prefix+* Renamed `Database.Franz.Reconnect` to `Database.Franz.Client.Reconnect`+ # v0.4 * Added `StreamName`
README.md view
@@ -61,6 +61,8 @@ ## Client API +`Database.Franz.Client` exposes the client API.+ You can obtain a `Connection` to a remote franz file with `withConnection`. It tries to mount a squashfs image at `path`. This is shared between connections, and unmounts when the last client closes the connection. @@ -108,3 +110,30 @@ toList :: Contents -> [Item] ```++## Writer API++`Database.Franz.Writer` provides the writer interface.++```haskell+withWriter :: Foldable f+ => f String+ -> FilePath+ -> (WriterHandle f -> IO a)+ -> IO a+```++`withWriter` acquires a handle. The `f String` parameter represents a list of index names.++```haskell+write :: Foldable f+ => WriterHandle f+ -> f Int64 -- ^ index values+ -> Builder -- ^ payload+ -> IO Int+flush :: WriterHandle f -> IO ()+```++`write` appends a payload to the stream. `f Int64` is the list of index values, and it has to have the same length as the one you specified in `withWriter`. Changes will be written to disk whenever the buffer gets full or you call `flush`.++If you don't need the index mechanism, you can use `Database.Franz.Writer.Simple` instead.
app/client.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE ApplicativeDo #-} module Main where-import Database.Franz.URI+import Database.Franz.Internal.URI import Database.Franz.Client import qualified Database.Franz.Contents as C
app/server.hs view
@@ -1,6 +1,5 @@ module Main where -import Database.Franz.Reader (FranzPrefix(..)) import Database.Franz.Server import Options.Applicative
franz.cabal view
@@ -1,5 +1,5 @@ cabal-version: 2.0-version: 0.4+version: 0.5 name: franz description: Please see the README on GitHub at <https://github.com/tsurucapital/franz#readme> homepage: https://github.com/tsurucapital/franz#readme@@ -22,20 +22,22 @@ library exposed-modules:- Database.Franz.Writer- Database.Franz.Writer.Simple- Database.Franz.Internal+ Database.Franz.Client+ Database.Franz.Client.Reconnect Database.Franz.Contents- Database.Franz.Reader+ Database.Franz.Internal.Contents+ Database.Franz.Internal.Fuse+ Database.Franz.Internal.IO+ Database.Franz.Internal.Protocol+ Database.Franz.Internal.Reader+ Database.Franz.Internal.URI Database.Franz.Server- Database.Franz.Client- Database.Franz.Protocol- Database.Franz.Reconnect- Database.Franz.URI+ Database.Franz.Writer+ Database.Franz.Writer.Simple hs-source-dirs: src build-depends:- base >=4.7 && <5+ base >=4.13 && <5 , bytestring , cereal , containers
src/Database/Franz/Client.hs view
@@ -37,12 +37,12 @@ import Data.IORef.Unboxed import qualified Data.IntMap.Strict as IM import Data.Serialize hiding (getInt64le)-import Database.Franz.Contents-import Database.Franz.Internal-import Database.Franz.Protocol-import Database.Franz.Reader-import Database.Franz.Server-import Database.Franz.URI+import Database.Franz.Internal.Contents+import Database.Franz.Internal.Fuse+import Database.Franz.Internal.IO+import Database.Franz.Internal.Protocol+import Database.Franz.Internal.Reader+import Database.Franz.Internal.URI import qualified Network.Socket as S import qualified Network.Socket.ByteString as SB import System.Process (ProcessHandle)@@ -159,7 +159,7 @@ let tmpDir' = tmpDir </> "franz" createDirectoryIfMissing True tmpDir' dir <- createTempDirectory tmpDir' (takeBaseName path)- fuse <- mountFuse path dir+ fuse <- mountFuse mempty (throwIO . InternalError) path dir pure (dir, Just fuse) pure LocalConnection{..} @@ -169,7 +169,7 @@ withMVar connSocket S.close disconnect LocalConnection{..} = closeFranzReader connReader- `finally` mapM_ (\p -> killFuse p connDir) connFuse+ `finally` mapM_ (\p -> killFuse mempty p connDir) connFuse defQuery :: StreamName -> Query defQuery name = Query
+ src/Database/Franz/Client/Reconnect.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE LambdaCase #-}+module Database.Franz.Client.Reconnect+ ( Pool+ , poolLogFunc+ , poolRetryPolicy+ , withPool+ , withReconnection+ , Reconnect(..)+ , atomicallyReconnecting+ , fetchWithPool+ )+ where++import Control.Retry (recovering, RetryPolicyM)+import Control.Concurrent.MVar+import Control.Concurrent.STM+import Control.Exception (IOException)+import Control.Monad.Catch+import Database.Franz.Client++data Pool = Pool+ { poolPath :: FranzPath+ , poolRef :: MVar (Int {- Connection number -}, Maybe Connection)+ , poolRetryPolicy :: RetryPolicyM IO+ , poolLogFunc :: String -> IO ()+ }++-- | A wrapper of 'fetch' which calls 'withReconnection' internally+fetchWithPool+ :: Pool+ -> Query+ -> (STM Response -> IO r)+ -> IO r+fetchWithPool pool q cont = withReconnection pool $ \conn -> fetch conn q cont++-- | Run an action which takes a 'Connection', reconnecting whenever it throws an exception.+withReconnection :: Pool -> (Connection -> IO a) -> IO a+withReconnection Pool{..} cont = recovering+ poolRetryPolicy+ [const $ Handler $ \Reconnect -> pure True]+ body+ where++ handler ex+ | Just (ClientError err) <- fromException ex = Just err+ | Just e <- fromException ex = Just (show (e :: IOException))+ | Just Reconnect <- fromException ex = Just+ $ "Connection to " <> fromFranzPath poolPath <> " timed out"+ | otherwise = Nothing++ body _ = do+ (i, conn) <- modifyMVar poolRef $ \case+ (i, Nothing) -> do+ poolLogFunc $ unwords+ [ "Connnecting to"+ , fromFranzPath poolPath+ ]+ conn <- tryJust handler (connect poolPath)+ >>= either (\e -> poolLogFunc e >> throwM Reconnect) pure+ poolLogFunc $ "Connection #" <> show i <> " established"+ pure ((i, Just conn), (i, conn))+ v@(i, Just c) -> pure (v, (i, c))++ tryJust handler (cont conn) >>= \case+ Right a -> pure a+ Left msg -> do+ poolLogFunc msg+ modifyMVar_ poolRef $ \case+ -- Don't disconnect if the sequential number is different;+ -- another thread already established a new connection+ (j, Just _) | i == j -> (i + 1, Nothing) <$ disconnect conn+ x -> pure x+ throwM Reconnect++data Reconnect = Reconnect deriving (Show, Eq)+instance Exception Reconnect++withPool :: RetryPolicyM IO+ -> (String -> IO ()) -- ^ diagnostic output+ -> FranzPath+ -> (Pool -> IO a)+ -> IO a+withPool poolRetryPolicy poolLogFunc poolPath cont = do+ poolRef <- newMVar (0, Nothing)+ cont Pool{..} `finally` do+ (_, conn) <- takeMVar poolRef+ mapM_ disconnect conn++-- | Run an 'STM' action, throwing 'Reconnect' when it exceeds the given timeout.+atomicallyReconnecting :: Int -- ^ timeout in microseconds+ -> STM a -> IO a+atomicallyReconnecting timeout m = atomicallyWithin timeout m+ >>= maybe (throwM Reconnect) pure
src/Database/Franz/Contents.hs view
@@ -3,25 +3,20 @@ {-# LANGUAGE RecordWildCards #-} module Database.Franz.Contents ( Contents- , Database.Franz.Contents.indexNames+ , Database.Franz.Internal.Contents.indexNames , Item(..) , toList , last , length , index , lookupIndex- -- * Internal- , getResponse- , readContents ) where -import Data.Serialize hiding (getInt64le) import qualified Data.ByteString.Char8 as B import qualified Data.Vector as V import qualified Data.Vector.Unboxed as U-import Database.Franz.Internal-import Database.Franz.Protocol-import Database.Franz.Reader+import Database.Franz.Internal.Protocol+import Database.Franz.Internal.Contents import Data.Int import Prelude hiding (length, last) @@ -31,36 +26,9 @@ , payload :: !B.ByteString } deriving (Show, Eq) -data Contents = Contents- { indexNames :: !(V.Vector IndexName)- , payloads :: !B.ByteString- , indicess :: !IndexVec- , length :: !Int- , payloadOffset :: !Int- , seqnoOffset :: !Int- }- toList :: Contents -> [Item] toList contents = [unsafeIndex contents i | i <- [0..length contents - 1]] --- A vector containing file offsets and extra indices-type IndexVec = V.Vector (Int, U.Vector Int64)--getIndexVec :: V.Vector IndexName -> Int -> Get IndexVec-getIndexVec names len = V.replicateM len- $ (,) <$> getInt64le <*> U.convert `fmap` traverse (const getInt64le) names--getResponse :: Get Contents-getResponse = do- PayloadHeader seqnoOffset s1 payloadOffset indexNames <- get- let length = s1 - seqnoOffset- if length <= 0- then pure Contents{ payloads = B.empty, indicess = V.empty, ..}- else do- indicess <- getIndexVec indexNames length- payloads <- getByteString $ fst (V.unsafeLast indicess) - payloadOffset- pure Contents{..}- last :: Contents -> Maybe Item last contents | i >= 0 = Just $ unsafeIndex contents i@@ -84,13 +52,3 @@ lookupIndex :: Contents -> IndexName -> Maybe (Item -> Int64) lookupIndex Contents{indexNames} name = (\j Item{indices} -> indices U.! j) <$> V.elemIndex name indexNames--readContents :: Stream -> QueryResult -> IO Contents-readContents Stream{indexNames, payloadHandle, indexHandle} ((seqnoOffset, payloadOffset), (s1, p1)) = do- let length = s1 - seqnoOffset- -- byte offset + number of indices- let indexSize = 8 * (V.length indexNames + 1)- indexBS <- hGetRange indexHandle (indexSize * length) (indexSize * succ seqnoOffset)- payloads <- hGetRange payloadHandle (p1 - payloadOffset) payloadOffset- let indicess = either error id $ runGet (getIndexVec indexNames length) indexBS- pure Contents{..}
− src/Database/Franz/Internal.hs
@@ -1,58 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-module Database.Franz.Internal (getInt64le, runGetRecv, hGetRange) where--import Data.IORef-import Data.Serialize hiding (getInt64le)-import Data.Typeable (cast)-import Foreign.Ptr-import Foreign.ForeignPtr-import Foreign.C.Types-import GHC.IO.Exception-import GHC.IO.Handle.Internals (withHandle_)-import GHC.IO.Handle.Types (Handle__(..), Handle)-import Network.Socket as S-import Network.Socket.ByteString as SB-import System.Endian (fromLE64)-import System.IO.Error-import System.Posix.Types (Fd(..))-import qualified Data.ByteString as B-import qualified Data.ByteString.Internal as B-import qualified GHC.IO.FD as FD-import Data.Word (Word8)---- | Better implementation of 'Data.Serialize.getInt64le'-getInt64le :: Num a => Get a-getInt64le = fromIntegral . fromLE64 <$> getWord64host-{-# INLINE getInt64le #-}--runGetRecv :: IORef B.ByteString -> S.Socket -> Get a -> IO (Either String a)-runGetRecv refBuf sock m = do- lo <- readIORef refBuf- let go (Done a lo') = do- writeIORef refBuf lo'- return $ Right a- go (Partial cont) = SB.recv sock 4096 >>= go . cont- go (Fail str lo') = do- writeIORef refBuf lo'- return $ Left $ show sock ++ str- bs <- if B.null lo- then SB.recv sock 4096- else pure lo- go $ runGetPartial m bs--withFd :: Handle -> (Fd -> IO a) -> IO a-withFd h f = withHandle_ "withFd" h $ \ Handle__{..} -> do- case cast haDevice of- Nothing -> ioError (ioeSetErrorString (mkIOError IllegalOperation- "withFd" (Just h) Nothing)- "handle is not a file descriptor")- Just fd -> f (Fd (fromIntegral (FD.fdFD fd)))--foreign import ccall safe "pread"- c_pread :: Fd -> Ptr Word8 -> CSize -> CSize -> IO CSize--hGetRange :: Handle -> Int -> Int -> IO B.ByteString-hGetRange h len ofs = do- fptr <- B.mallocByteString len- count <- withFd h $ \fd -> withForeignPtr fptr $ \ptr -> c_pread fd ptr (fromIntegral len) (fromIntegral ofs)- pure $ B.PS fptr 0 $ fromIntegral count
+ src/Database/Franz/Internal/Contents.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE RecordWildCards #-}+module Database.Franz.Internal.Contents+ ( IndexVec+ , Contents(..)+ , getResponse+ , readContents+ )+ where++import Prelude hiding (length)+import Data.Serialize hiding (getInt64le)+import qualified Data.ByteString.Char8 as B+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as U+import Database.Franz.Internal.Reader+import Database.Franz.Internal.Protocol+import Database.Franz.Internal.IO+import Data.Int++-- A vector containing file offsets and extra indices+type IndexVec = V.Vector (Int, U.Vector Int64)++data Contents = Contents+ { indexNames :: !(V.Vector IndexName)+ , payloads :: !B.ByteString+ , indicess :: !IndexVec+ , length :: !Int+ , payloadOffset :: !Int+ , seqnoOffset :: !Int+ }++getIndexVec :: V.Vector IndexName -> Int -> Get IndexVec+getIndexVec names len = V.replicateM len+ $ (,) <$> getInt64le <*> U.convert `fmap` traverse (const getInt64le) names++getResponse :: Get Contents+getResponse = do+ PayloadHeader seqnoOffset s1 payloadOffset indexNames <- get+ let length = s1 - seqnoOffset+ if length <= 0+ then pure Contents{ payloads = B.empty, indicess = V.empty, ..}+ else do+ indicess <- getIndexVec indexNames length+ payloads <- getByteString $ fst (V.unsafeLast indicess) - payloadOffset+ pure Contents{..}++readContents :: Stream -> QueryResult -> IO Contents+readContents Stream{indexNames, payloadHandle, indexHandle} ((seqnoOffset, payloadOffset), (s1, p1)) = do+ let length = s1 - seqnoOffset+ -- byte offset + number of indices+ let indexSize = 8 * (V.length indexNames + 1)+ indexBS <- hGetRange indexHandle (indexSize * length) (indexSize * succ seqnoOffset)+ payloads <- hGetRange payloadHandle (p1 - payloadOffset) payloadOffset+ let indicess = either error id $ runGet (getIndexVec indexNames length) indexBS+ pure Contents{..}
+ src/Database/Franz/Internal/Fuse.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RankNTypes #-}+module Database.Franz.Internal.Fuse where++import Control.Exception hiding (throw)+import Control.Monad (when)+import Control.Retry+import System.Directory+import System.Process (ProcessHandle, spawnProcess, cleanupProcess,+ waitForProcess, getProcessExitCode)++mountFuse :: ([String] -> IO ()) -- logger+ -> (forall x. String -> IO x) -- throw an exception+ -> FilePath -> FilePath -> IO ProcessHandle+mountFuse logger throw src dest = do+ createDirectoryIfMissing True dest+ logger ["squashfuse", "-f", src, dest]+ bracketOnError (spawnProcess "squashfuse" ["-f", src, dest]) (flip (killFuse logger) dest) $ \fuse -> do+ -- It keeps process handles so that mounted directories are cleaned up+ -- but there's no easy way to tell when squashfuse finished mounting.+ -- Wait until the destination becomes non-empty.+ notMounted <- retrying (limitRetries 5 <> exponentialBackoff 100000) (const pure)+ $ \status -> getProcessExitCode fuse >>= \case+ Nothing -> do+ logger ["Waiting for squashfuse to mount", src, ":", show status]+ null <$> listDirectory dest+ Just e -> do+ removeDirectory dest+ throw $ "squashfuse exited with " <> show e+ when notMounted $ throw $ "Failed to mount " <> src+ return fuse++killFuse :: ([String] -> IO ()) -> ProcessHandle -> FilePath -> IO ()+killFuse logger fuse path = do+ cleanupProcess (Nothing, Nothing, Nothing, fuse)+ e <- waitForProcess fuse+ logger ["squashfuse:", show e]+ removeDirectory path
+ src/Database/Franz/Internal/IO.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE RecordWildCards #-}+module Database.Franz.Internal.IO (getInt64le, runGetRecv, hGetRange) where++import Data.IORef+import Data.Serialize hiding (getInt64le)+import Data.Typeable (cast)+import Foreign.Ptr+import Foreign.ForeignPtr+import Foreign.C.Types+import GHC.IO.Exception+import GHC.IO.Handle.Internals (withHandle_)+import GHC.IO.Handle.Types (Handle__(..), Handle)+import Network.Socket as S+import Network.Socket.ByteString as SB+import System.Endian (fromLE64)+import System.IO.Error+import System.Posix.Types (Fd(..))+import qualified Data.ByteString as B+import qualified Data.ByteString.Internal as B+import qualified GHC.IO.FD as FD+import Data.Word (Word8)++-- | Better implementation of 'Data.Serialize.getInt64le'+getInt64le :: Num a => Get a+getInt64le = fromIntegral . fromLE64 <$> getWord64host+{-# INLINE getInt64le #-}++runGetRecv :: IORef B.ByteString -> S.Socket -> Get a -> IO (Either String a)+runGetRecv refBuf sock m = do+ lo <- readIORef refBuf+ let go (Done a lo') = do+ writeIORef refBuf lo'+ return $ Right a+ go (Partial cont) = SB.recv sock 4096 >>= go . cont+ go (Fail str lo') = do+ writeIORef refBuf lo'+ return $ Left $ show sock ++ str+ bs <- if B.null lo+ then SB.recv sock 4096+ else pure lo+ go $ runGetPartial m bs++withFd :: Handle -> (Fd -> IO a) -> IO a+withFd h f = withHandle_ "withFd" h $ \ Handle__{..} -> do+ case cast haDevice of+ Nothing -> ioError (ioeSetErrorString (mkIOError IllegalOperation+ "withFd" (Just h) Nothing)+ "handle is not a file descriptor")+ Just fd -> f (Fd (fromIntegral (FD.fdFD fd)))++foreign import ccall safe "pread"+ c_pread :: Fd -> Ptr Word8 -> CSize -> CSize -> IO CSize++hGetRange :: Handle -> Int -> Int -> IO B.ByteString+hGetRange h len ofs = do+ fptr <- B.mallocByteString len+ count <- withFd h $ \fd -> withForeignPtr fptr $ \ptr -> c_pread fd ptr (fromIntegral len) (fromIntegral ofs)+ pure $ B.PS fptr 0 $ fromIntegral count
+ src/Database/Franz/Internal/Protocol.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralisedNewtypeDeriving #-}+module Database.Franz.Internal.Protocol+ ( apiVersion+ , defaultPort+ , IndexName+ , StreamName(..)+ , streamNameToPath+ , FranzException(..)+ , RequestType(..)+ , ItemRef(..)+ , Query(..)+ , RawRequest(..)+ , ResponseId+ , ResponseHeader(..)+ , PayloadHeader(..)) where++import Control.Exception (Exception)+import qualified Data.ByteString.Char8 as B+import Data.Serialize hiding (getInt64le)+import Database.Franz.Internal.IO (getInt64le)+import Data.Hashable (Hashable)+import Data.String+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.Socket (PortNumber)+import GHC.Generics (Generic)+import qualified Data.ByteString.FastBuilder as BB+import qualified Data.Vector as V++apiVersion :: B.ByteString+apiVersion = B.pack "0"++defaultPort :: PortNumber+defaultPort = 1886++type IndexName = B.ByteString++newtype StreamName = StreamName { unStreamName :: B.ByteString }+ deriving newtype (Show, Eq, Ord, Hashable, Serialize)++streamNameToPath :: StreamName -> FilePath+streamNameToPath = T.unpack . T.decodeUtf8 . unStreamName++-- | UTF-8 encoded+instance IsString StreamName where+ fromString = StreamName . BB.toStrictByteString . BB.stringUtf8++data FranzException = MalformedRequest !String+ | StreamNotFound !FilePath+ | IndexNotFound !IndexName ![IndexName]+ | InternalError !String+ | ClientError !String+ deriving (Show, Generic)+instance Serialize FranzException+instance Exception FranzException++data RequestType = AllItems | LastItem deriving (Show, Generic)+instance Serialize RequestType++data ItemRef = BySeqNum !Int -- ^ sequential number+ | ByIndex !IndexName !Int -- ^ index name and value+ deriving (Show, Generic)+instance Serialize ItemRef++data Query = Query+ { reqStream :: !StreamName+ , reqFrom :: !ItemRef -- ^ name of the index to search+ , reqTo :: !ItemRef -- ^ name of the index to search+ , reqType :: !RequestType+ } deriving (Show, Generic)+instance Serialize Query++data RawRequest+ = RawRequest !ResponseId !Query+ | RawClean !ResponseId deriving Generic+instance Serialize RawRequest++type ResponseId = Int++data ResponseHeader = Response !ResponseId+ -- ^ response ID, number of streams; there are items satisfying the query+ | ResponseWait !ResponseId -- ^ response ID; requested elements are not available right now+ | ResponseError !ResponseId !FranzException -- ^ something went wrong+ deriving (Show, Generic)+instance Serialize ResponseHeader++-- | Initial seqno, final seqno, base offset, index names+data PayloadHeader = PayloadHeader !Int !Int !Int !(V.Vector IndexName)++instance Serialize PayloadHeader where+ put (PayloadHeader s t u xs) = f s *> f t *> f u+ *> put (V.length xs) *> mapM_ put xs where+ f = putInt64le . fromIntegral+ get = PayloadHeader <$> getInt64le <*> getInt64le <*> getInt64le+ <*> do+ len <- get+ V.replicateM len get
+ src/Database/Franz/Internal/Reader.hs view
@@ -0,0 +1,330 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE GeneralisedNewtypeDeriving #-}+module Database.Franz.Internal.Reader where++import Control.Concurrent+import Control.Concurrent.STM+import Control.Exception+import Control.Monad+import Control.Monad.State.Strict+import Control.Monad.Trans.Maybe+import Data.Hashable (Hashable)+import Data.Serialize+import Database.Franz.Internal.Protocol+import qualified Data.ByteString.Char8 as B+import qualified Data.HashMap.Strict as HM+import qualified Data.IntMap.Strict as IM+import qualified Data.Vector.Unboxed as U+import qualified Data.Vector as V+import Data.Maybe (isJust)+import GHC.Clock (getMonotonicTime)+import System.Directory+import System.FilePath+import System.IO+import qualified System.FSNotify as FS++data StreamStatus = CaughtUp | Outdated | Gone deriving Eq++data Stream = Stream+ { streamPath :: FilePath+ , vOffsets :: !(TVar (IM.IntMap Int))+ , indexNames :: !(V.Vector IndexName)+ , indices :: !(HM.HashMap IndexName (TVar (IM.IntMap Int)))+ , vCount :: !(TVar Int)+ , vStatus :: !(TVar StreamStatus)+ , followThread :: !ThreadId+ , indexHandle :: !Handle+ , payloadHandle :: !Handle+ , vActivity :: !(TVar Activity)+ }++type Activity = Either Double Int++addActivity :: Stream -> STM ()+addActivity str = modifyTVar' (vActivity str) $ \case+ Left _ -> Right 0+ Right n -> Right (n + 1)++removeActivity :: Stream -> IO ()+removeActivity str = do+ now <- getMonotonicTime+ atomically $ modifyTVar' (vActivity str) $ \case+ Left _ -> Left now+ Right n+ | n <= 0 -> Left now+ | otherwise -> Right (n - 1)++closeStream :: Stream -> IO ()+closeStream Stream{..} = do+ killThread followThread+ hClose payloadHandle+ hClose indexHandle++createStream :: FS.WatchManager -> FilePath -> IO Stream+createStream man path = do+ let offsetPath = path </> "offsets"+ let payloadPath = path </> "payloads"+ exist <- doesFileExist offsetPath+ unless exist $ throwIO $ StreamNotFound offsetPath+ initialOffsetsBS <- B.readFile offsetPath+ payloadHandle <- openBinaryFile payloadPath ReadMode+ indexNames <- V.fromList . B.lines <$> B.readFile (path </> "indices")+ let icount = 1 + V.length indexNames+ let count = B.length initialOffsetsBS `div` (8 * icount)+ let getI = fromIntegral <$> getInt64le+ initialIndices <- either (throwIO . InternalError) pure+ $ runGet (V.replicateM count $ U.replicateM icount getI) initialOffsetsBS+ let initialOffsets = IM.fromList $ V.toList+ $ V.zip (V.enumFromN 0 count) $ V.map U.head initialIndices+ vOffsets <- newTVarIO $! initialOffsets+ vStatus <- newTVarIO Outdated+ vCount <- newTVarIO $! IM.size initialOffsets+ _ <- FS.watchDir man path (\case+ FS.Modified p _ _ | p == offsetPath -> True+ FS.Removed p _ _ | p == offsetPath -> True+ _ -> False)+ $ \case+ FS.Modified _ _ _ -> atomically $ writeTVar vStatus Outdated+ FS.Removed _ _ _ -> atomically $ writeTVar vStatus Gone+ _ -> pure ()++ vIndices <- forM [1..V.length indexNames] $ \i -> newTVarIO+ $ IM.fromList $ V.toList $ V.zip (V.map (U.! i) initialIndices) (V.enumFromN 0 count)++ indexHandle <- openFile offsetPath ReadMode++ let final :: Either SomeException () -> IO ()+ final (Left exc) | Just ThreadKilled <- fromException exc = pure ()+ final (Left exc) = logFollower [path, "terminated with", show exc]+ final (Right _) = logFollower [path, "has been removed"]++ -- TODO broadcast an exception if it exits?+ followThread <- flip forkFinally final $ do+ forM_ (IM.maxViewWithKey initialOffsets) $ \((i, _), _) ->+ hSeek indexHandle AbsoluteSeek $ fromIntegral $ succ i * icount * 8+ fix $ \self -> do+ bs <- B.hGet indexHandle (8 * icount)+ if B.null bs+ then do+ atomically $ modifyTVar' vStatus $ \case+ Outdated -> CaughtUp+ CaughtUp -> CaughtUp+ Gone -> Gone+ continue <- atomically $ readTVar vStatus >>= \case+ CaughtUp -> retry+ Outdated -> pure True+ Gone -> pure False+ when continue self+ else do+ ofs : indices <- either (throwIO . InternalError) pure $ runGet (replicateM icount getI) bs+ atomically $ do+ i <- readTVar vCount+ modifyTVar' vOffsets $ IM.insert i ofs+ forM_ (zip vIndices indices) $ \(v, x) -> modifyTVar' v $ IM.insert (fromIntegral x) i+ writeTVar vCount $! i + 1+ self++ let indices = HM.fromList $ zip (V.toList indexNames) vIndices++ vActivity <- getMonotonicTime >>= newTVarIO . Left+ return Stream{ streamPath = path, ..}+ where+ logFollower = hPutStrLn stderr . unwords . (:) "[follower]"++type QueryResult = ((Int, Int) -- starting SeqNo, byte offset+ , (Int, Int)) -- ending SeqNo, byte offset++range :: Int -- ^ from+ -> Int -- ^ to+ -> RequestType+ -> IM.IntMap Int -- ^ offsets+ -> (Bool, QueryResult)+range begin end rt allOffsets = case rt of+ AllItems -> (ready, (firstItem, maybe firstItem fst $ IM.maxViewWithKey body))+ LastItem -> case IM.maxViewWithKey body of+ Nothing -> (False, (zero, zero))+ Just (ofs', r) -> case IM.maxViewWithKey (IM.union left r) of+ Just (ofs, _) -> (ready, (ofs, ofs'))+ Nothing -> (ready, (zero, ofs'))+ where+ zero = (-1, 0)+ ready = isJust lastItem || not (null cont)+ (wing, lastItem, cont) = IM.splitLookup end allOffsets+ (left, body) = splitR begin $ maybe id (IM.insert end) lastItem wing+ firstItem = maybe zero fst $ IM.maxViewWithKey left++splitR :: Int -> IM.IntMap a -> (IM.IntMap a, IM.IntMap a)+splitR i m = let (l, p, r) = IM.splitLookup i m in (l, maybe id (IM.insert i) p r)++data FranzReader = FranzReader+ { watchManager :: FS.WatchManager+ , vStreams :: TVar (HM.HashMap FranzDirectory (HM.HashMap StreamName Stream))+ }++data ReaperState = ReaperState+ { -- | How many streams we pruned.+ prunedStreams :: !Int+ -- | How many streams we saw in total.+ , totalStreams :: !Int+ }++reaper :: Double -- interval+ -> Double -- lifetime+ -> FranzReader -> IO ()+reaper int life FranzReader{..} = forever $ do+ now <- getMonotonicTime+ -- Check if stream's activity indicates that we should prune it.+ let shouldPrune (Left t) = now - t >= life+ shouldPrune _ = False++ -- Try prunning stream at given filepath. Checks if stream+ -- should really be pruned first.+ tryPrune :: FranzDirectory -> StreamName -> STM (Maybe Stream)+ tryPrune mPath sPath = runMaybeT $ do+ currentAllStreams <- lift $ readTVar vStreams+ currentStreams <- MaybeT . pure $ HM.lookup mPath currentAllStreams+ currentStream <- MaybeT . pure $ HM.lookup sPath currentStreams+ currentAct <- lift $ readTVar (vActivity currentStream)+ guard $ shouldPrune currentAct+ let newStreams = HM.delete sPath currentStreams+ lift . writeTVar vStreams $ if HM.null newStreams+ -- Stream we're deleting was the+ -- last one around.+ then HM.delete mPath currentAllStreams+ -- Still have some other streams left for this mount path.+ -- Keep those only.+ else HM.insert mPath newStreams currentAllStreams+ pure currentStream++ -- Take a snapshots of all streams.+ allStreams <- readTVarIO vStreams+ -- Traverse the snapshot, looking for streams that currently seem+ -- out of date. If we find an out-of-date stream, take outer lock+ -- too, check again, delete it if necessary. Close stream promptly+ -- after deletion.+ --+ -- We could first traverse the whole snapshot, gather potential+ -- streams for deletion, take lock and delete all these streams from+ -- the map. However, on an assumption that we normally reaps streams+ -- and much lower rate than we use them and in favour of locking at+ -- as small time intervals as possible, we simply traverse the+ -- snapshot and if we find an out of date stream in the snapshot, we+ -- take the lock on the whole stream data structure and delete the+ -- stream. We also check that we aren't leaving an empty map entry+ -- behind and if we are, we delete it straight away. This stops us+ -- from having to traverse the whole map later to clean things up as+ -- well as ensuring that we never leave empty values in the map for+ -- others to see.+ --+ -- While doing all this, keep track of how many streams we saw and+ -- how many we have closed which is used later to report statistics+ -- to the user: this is in contrast to doing linear-time traversals+ -- just to get counts of things we've already traversed.+ stats <- flip execStateT (ReaperState 0 0) $+ forM_ (HM.toList allStreams) $ \(mPath, streams) ->+ forM_ (HM.toList streams) $ \(sPath, stream) -> do+ modify' $ \s -> s { totalStreams = totalStreams s + 1 }+ snapAct <- lift $ readTVarIO (vActivity stream)+ when (shouldPrune snapAct) $ do+ -- The stream indicates that it's old and should be pruned. Take+ -- a lock on the outer map, check again inside a transaction and+ -- amend the maps as necessary.+ deletedStream'm <- lift . atomically $ tryPrune mPath sPath+ forM_ deletedStream'm $ \prunedStream -> do+ lift $ closeStream prunedStream+ modify' $ \s -> s { prunedStreams = prunedStreams s + 1 }++ when (prunedStreams stats > 0) $ hPutStrLn stderr $ unwords+ [ "[reaper] closed"+ , show (prunedStreams stats)+ , "out of"+ , show (totalStreams stats)+ ]++ threadDelay $ floor $ int * 1e6++withFranzReader :: (FranzReader -> IO ()) -> IO ()+withFranzReader = bracket newFranzReader closeFranzReader++newFranzReader :: IO FranzReader+newFranzReader = do+ vStreams <- newTVarIO HM.empty+ watchManager <- FS.startManager+ pure FranzReader{..}++closeFranzReader :: FranzReader -> IO ()+closeFranzReader FranzReader{..} = do+ FS.stopManager watchManager+ readTVarIO vStreams >>= mapM_ (mapM_ closeStream)++-- | Globally-configured path which contains franz directories.+newtype FranzPrefix = FranzPrefix { unFranzPrefix :: FilePath } deriving (Eq, Hashable)++-- | Directory which contains franz streams.+-- Values of this type serve two purposes:+--+-- * Arbitrary prefix so that clients don't have to specify the full path +newtype FranzDirectory = FranzDirectory FilePath deriving (Eq, Hashable)++getFranzDirectory :: FranzPrefix -> FranzDirectory -> FilePath+getFranzDirectory (FranzPrefix prefix) (FranzDirectory dir) = prefix </> dir++getFranzStreamPath :: FranzPrefix -> FranzDirectory -> StreamName -> FilePath+getFranzStreamPath prefix dir name+ = getFranzDirectory prefix dir </> streamNameToPath name++handleQuery :: FranzPrefix+ -> FranzReader+ -> FranzDirectory+ -> Query+ -> (FranzException -> IO r)+ -> (Stream -> STM (Bool, QueryResult) -> IO r) -> IO r+handleQuery prefix FranzReader{..} dir (Query name begin_ end_ rt) onError cont+ = bracket (try acquire) (either mempty removeActivity) $ \case+ Left err -> onError err+ Right stream@Stream{..} -> cont stream $ do+ readTVar vStatus >>= \case+ Outdated -> retry+ CaughtUp -> pure ()+ Gone -> throwSTM $ StreamNotFound streamPath+ allOffsets <- readTVar vOffsets+ let finalOffset = case IM.maxViewWithKey allOffsets of+ Just ((k, _), _) -> k + 1+ Nothing -> 0+ let rotate i+ | i < 0 = finalOffset + i+ | otherwise = i+ begin <- case begin_ of+ BySeqNum i -> pure $ rotate i+ ByIndex index val -> case HM.lookup index indices of+ Nothing -> throwSTM $ IndexNotFound index $ HM.keys indices+ Just v -> do+ m <- readTVar v+ let (_, wing) = splitR val m+ return $! maybe maxBound fst $ IM.minView wing+ end <- case end_ of+ BySeqNum i -> pure $ rotate i+ ByIndex index val -> case HM.lookup index indices of+ Nothing -> throwSTM $ IndexNotFound index $ HM.keys indices+ Just v -> do+ m <- readTVar v+ let (body, lastItem, _) = IM.splitLookup val m+ let body' = maybe id (IM.insert val) lastItem body+ return $! maybe minBound fst $ IM.maxView body'+ return $! range begin end rt allOffsets+ where+ acquire = join $ atomically $ do+ allStreams <- readTVar vStreams+ let !streams = maybe mempty id $ HM.lookup dir allStreams+ case HM.lookup name streams of+ Nothing -> pure $ bracketOnError+ (createStream watchManager $ getFranzStreamPath prefix dir name)+ closeStream $ \s -> atomically $ do+ addActivity s+ modifyTVar' vStreams $ HM.insert dir $ HM.insert name s streams+ pure s+ Just s -> do+ addActivity s+ pure (pure s)
+ src/Database/Franz/Internal/URI.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings #-}+module Database.Franz.Internal.URI+ ( FranzPath(..)+ , toFranzPath+ , fromFranzPath+ ) where++import Data.List (stripPrefix)+import Data.String+import Network.Socket (HostName, PortNumber)+import Text.Read (readMaybe)++data FranzPath = FranzPath+ { franzHost :: !HostName+ , franzPort :: !PortNumber+ , franzDir :: !FilePath+ -- ^ Prefix of franz directories+ }+ | LocalFranzPath !FilePath+ deriving (Show, Eq, Ord)++localPrefix :: IsString a => a+localPrefix = "franz-local:"++remotePrefix :: IsString a => a+remotePrefix = "franz://"++-- | Parse a franz URI (franz://host:port/path or franz-local:path).+toFranzPath :: String -> Either String FranzPath+toFranzPath uri | Just path <- stripPrefix localPrefix uri = Right $ LocalFranzPath path+toFranzPath uri = do+ hostnamePath <- maybe (Left $ "Expecting " <> remotePrefix) Right $ stripPrefix remotePrefix uri+ (host, path) <- case break (== '/') hostnamePath of+ (h, '/' : p) -> Right (h, p)+ _ -> Left "Expecting /"+ case break (== ':') host of+ (hostname, ':' : portStr)+ | Just p <- readMaybe portStr -> Right $ FranzPath hostname p path+ | otherwise -> Left "Failed to parse the port number"+ _ -> Right $ FranzPath host 1886 path++-- | Render 'FranzPath' as a franz URI.+fromFranzPath :: (Monoid a, IsString a) => FranzPath -> a+fromFranzPath (FranzPath host port path) = mconcat+ [ remotePrefix+ , fromString host+ , ":"+ , fromString (show port)+ , "/"+ , fromString path+ ]++fromFranzPath (LocalFranzPath path) = localPrefix <> fromString path
− src/Database/Franz/Protocol.hs
@@ -1,99 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE GeneralisedNewtypeDeriving #-}-module Database.Franz.Protocol- ( apiVersion- , defaultPort- , IndexName- , StreamName(..)- , streamNameToPath- , FranzException(..)- , RequestType(..)- , ItemRef(..)- , Query(..)- , RawRequest(..)- , ResponseId- , ResponseHeader(..)- , PayloadHeader(..)) where--import Control.Exception (Exception)-import qualified Data.ByteString.Char8 as B-import Data.Serialize hiding (getInt64le)-import Database.Franz.Internal (getInt64le)-import Data.Hashable (Hashable)-import Data.String-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import Network.Socket (PortNumber)-import GHC.Generics (Generic)-import qualified Data.ByteString.FastBuilder as BB-import qualified Data.Vector as V--apiVersion :: B.ByteString-apiVersion = B.pack "0"--defaultPort :: PortNumber-defaultPort = 1886--type IndexName = B.ByteString--newtype StreamName = StreamName { unStreamName :: B.ByteString }- deriving newtype (Show, Eq, Ord, Hashable, Serialize)--streamNameToPath :: StreamName -> FilePath-streamNameToPath = T.unpack . T.decodeUtf8 . unStreamName---- | UTF-8 encoded-instance IsString StreamName where- fromString = StreamName . BB.toStrictByteString . BB.stringUtf8--data FranzException = MalformedRequest !String- | StreamNotFound !FilePath- | IndexNotFound !IndexName ![IndexName]- | InternalError !String- | ClientError !String- deriving (Show, Generic)-instance Serialize FranzException-instance Exception FranzException--data RequestType = AllItems | LastItem deriving (Show, Generic)-instance Serialize RequestType--data ItemRef = BySeqNum !Int -- ^ sequential number- | ByIndex !IndexName !Int -- ^ index name and value- deriving (Show, Generic)-instance Serialize ItemRef--data Query = Query- { reqStream :: !StreamName- , reqFrom :: !ItemRef -- ^ name of the index to search- , reqTo :: !ItemRef -- ^ name of the index to search- , reqType :: !RequestType- } deriving (Show, Generic)-instance Serialize Query--data RawRequest- = RawRequest !ResponseId !Query- | RawClean !ResponseId deriving Generic-instance Serialize RawRequest--type ResponseId = Int--data ResponseHeader = Response !ResponseId- -- ^ response ID, number of streams; there are items satisfying the query- | ResponseWait !ResponseId -- ^ response ID; requested elements are not available right now- | ResponseError !ResponseId !FranzException -- ^ something went wrong- deriving (Show, Generic)-instance Serialize ResponseHeader---- | Initial seqno, final seqno, base offset, index names-data PayloadHeader = PayloadHeader !Int !Int !Int !(V.Vector IndexName)--instance Serialize PayloadHeader where- put (PayloadHeader s t u xs) = f s *> f t *> f u- *> put (V.length xs) *> mapM_ put xs where- f = putInt64le . fromIntegral- get = PayloadHeader <$> getInt64le <*> getInt64le <*> getInt64le- <*> do- len <- get- V.replicateM len get
− src/Database/Franz/Reader.hs
@@ -1,330 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE GeneralisedNewtypeDeriving #-}-module Database.Franz.Reader where--import Control.Concurrent-import Control.Concurrent.STM-import Control.Exception-import Control.Monad-import Control.Monad.State.Strict-import Control.Monad.Trans.Maybe-import Data.Hashable (Hashable)-import Data.Serialize-import Database.Franz.Protocol-import qualified Data.ByteString.Char8 as B-import qualified Data.HashMap.Strict as HM-import qualified Data.IntMap.Strict as IM-import qualified Data.Vector.Unboxed as U-import qualified Data.Vector as V-import Data.Maybe (isJust)-import GHC.Clock (getMonotonicTime)-import System.Directory-import System.FilePath-import System.IO-import qualified System.FSNotify as FS--data StreamStatus = CaughtUp | Outdated | Gone deriving Eq--data Stream = Stream- { streamPath :: FilePath- , vOffsets :: !(TVar (IM.IntMap Int))- , indexNames :: !(V.Vector IndexName)- , indices :: !(HM.HashMap IndexName (TVar (IM.IntMap Int)))- , vCount :: !(TVar Int)- , vStatus :: !(TVar StreamStatus)- , followThread :: !ThreadId- , indexHandle :: !Handle- , payloadHandle :: !Handle- , vActivity :: !(TVar Activity)- }--type Activity = Either Double Int--addActivity :: Stream -> STM ()-addActivity str = modifyTVar' (vActivity str) $ \case- Left _ -> Right 0- Right n -> Right (n + 1)--removeActivity :: Stream -> IO ()-removeActivity str = do- now <- getMonotonicTime- atomically $ modifyTVar' (vActivity str) $ \case- Left _ -> Left now- Right n- | n <= 0 -> Left now- | otherwise -> Right (n - 1)--closeStream :: Stream -> IO ()-closeStream Stream{..} = do- killThread followThread- hClose payloadHandle- hClose indexHandle--createStream :: FS.WatchManager -> FilePath -> IO Stream-createStream man path = do- let offsetPath = path </> "offsets"- let payloadPath = path </> "payloads"- exist <- doesFileExist offsetPath- unless exist $ throwIO $ StreamNotFound offsetPath- initialOffsetsBS <- B.readFile offsetPath- payloadHandle <- openBinaryFile payloadPath ReadMode- indexNames <- V.fromList . B.lines <$> B.readFile (path </> "indices")- let icount = 1 + V.length indexNames- let count = B.length initialOffsetsBS `div` (8 * icount)- let getI = fromIntegral <$> getInt64le- initialIndices <- either (throwIO . InternalError) pure- $ runGet (V.replicateM count $ U.replicateM icount getI) initialOffsetsBS- let initialOffsets = IM.fromList $ V.toList- $ V.zip (V.enumFromN 0 count) $ V.map U.head initialIndices- vOffsets <- newTVarIO $! initialOffsets- vStatus <- newTVarIO Outdated- vCount <- newTVarIO $! IM.size initialOffsets- _ <- FS.watchDir man path (\case- FS.Modified p _ _ | p == offsetPath -> True- FS.Removed p _ _ | p == offsetPath -> True- _ -> False)- $ \case- FS.Modified _ _ _ -> atomically $ writeTVar vStatus Outdated- FS.Removed _ _ _ -> atomically $ writeTVar vStatus Gone- _ -> pure ()-- vIndices <- forM [1..V.length indexNames] $ \i -> newTVarIO- $ IM.fromList $ V.toList $ V.zip (V.map (U.! i) initialIndices) (V.enumFromN 0 count)-- indexHandle <- openFile offsetPath ReadMode-- let final :: Either SomeException () -> IO ()- final (Left exc) | Just ThreadKilled <- fromException exc = pure ()- final (Left exc) = logFollower [path, "terminated with", show exc]- final (Right _) = logFollower [path, "has been removed"]-- -- TODO broadcast an exception if it exits?- followThread <- flip forkFinally final $ do- forM_ (IM.maxViewWithKey initialOffsets) $ \((i, _), _) ->- hSeek indexHandle AbsoluteSeek $ fromIntegral $ succ i * icount * 8- fix $ \self -> do- bs <- B.hGet indexHandle (8 * icount)- if B.null bs- then do- atomically $ modifyTVar' vStatus $ \case- Outdated -> CaughtUp- CaughtUp -> CaughtUp- Gone -> Gone- continue <- atomically $ readTVar vStatus >>= \case- CaughtUp -> retry- Outdated -> pure True- Gone -> pure False- when continue self- else do- ofs : indices <- either (throwIO . InternalError) pure $ runGet (replicateM icount getI) bs- atomically $ do- i <- readTVar vCount- modifyTVar' vOffsets $ IM.insert i ofs- forM_ (zip vIndices indices) $ \(v, x) -> modifyTVar' v $ IM.insert (fromIntegral x) i- writeTVar vCount $! i + 1- self-- let indices = HM.fromList $ zip (V.toList indexNames) vIndices-- vActivity <- getMonotonicTime >>= newTVarIO . Left- return Stream{ streamPath = path, ..}- where- logFollower = hPutStrLn stderr . unwords . (:) "[follower]"--type QueryResult = ((Int, Int) -- starting SeqNo, byte offset- , (Int, Int)) -- ending SeqNo, byte offset--range :: Int -- ^ from- -> Int -- ^ to- -> RequestType- -> IM.IntMap Int -- ^ offsets- -> (Bool, QueryResult)-range begin end rt allOffsets = case rt of- AllItems -> (ready, (firstItem, maybe firstItem fst $ IM.maxViewWithKey body))- LastItem -> case IM.maxViewWithKey body of- Nothing -> (False, (zero, zero))- Just (ofs', r) -> case IM.maxViewWithKey (IM.union left r) of- Just (ofs, _) -> (ready, (ofs, ofs'))- Nothing -> (ready, (zero, ofs'))- where- zero = (-1, 0)- ready = isJust lastItem || not (null cont)- (wing, lastItem, cont) = IM.splitLookup end allOffsets- (left, body) = splitR begin $ maybe id (IM.insert end) lastItem wing- firstItem = maybe zero fst $ IM.maxViewWithKey left--splitR :: Int -> IM.IntMap a -> (IM.IntMap a, IM.IntMap a)-splitR i m = let (l, p, r) = IM.splitLookup i m in (l, maybe id (IM.insert i) p r)--data FranzReader = FranzReader- { watchManager :: FS.WatchManager- , vStreams :: TVar (HM.HashMap FranzDirectory (HM.HashMap StreamName Stream))- }--data ReaperState = ReaperState- { -- | How many streams we pruned.- prunedStreams :: !Int- -- | How many streams we saw in total.- , totalStreams :: !Int- }--reaper :: Double -- interval- -> Double -- lifetime- -> FranzReader -> IO ()-reaper int life FranzReader{..} = forever $ do- now <- getMonotonicTime- -- Check if stream's activity indicates that we should prune it.- let shouldPrune (Left t) = now - t >= life- shouldPrune _ = False-- -- Try prunning stream at given filepath. Checks if stream- -- should really be pruned first.- tryPrune :: FranzDirectory -> StreamName -> STM (Maybe Stream)- tryPrune mPath sPath = runMaybeT $ do- currentAllStreams <- lift $ readTVar vStreams- currentStreams <- MaybeT . pure $ HM.lookup mPath currentAllStreams- currentStream <- MaybeT . pure $ HM.lookup sPath currentStreams- currentAct <- lift $ readTVar (vActivity currentStream)- guard $ shouldPrune currentAct- let newStreams = HM.delete sPath currentStreams- lift . writeTVar vStreams $ if HM.null newStreams- -- Stream we're deleting was the- -- last one around.- then HM.delete mPath currentAllStreams- -- Still have some other streams left for this mount path.- -- Keep those only.- else HM.insert mPath newStreams currentAllStreams- pure currentStream-- -- Take a snapshots of all streams.- allStreams <- readTVarIO vStreams- -- Traverse the snapshot, looking for streams that currently seem- -- out of date. If we find an out-of-date stream, take outer lock- -- too, check again, delete it if necessary. Close stream promptly- -- after deletion.- --- -- We could first traverse the whole snapshot, gather potential- -- streams for deletion, take lock and delete all these streams from- -- the map. However, on an assumption that we normally reaps streams- -- and much lower rate than we use them and in favour of locking at- -- as small time intervals as possible, we simply traverse the- -- snapshot and if we find an out of date stream in the snapshot, we- -- take the lock on the whole stream data structure and delete the- -- stream. We also check that we aren't leaving an empty map entry- -- behind and if we are, we delete it straight away. This stops us- -- from having to traverse the whole map later to clean things up as- -- well as ensuring that we never leave empty values in the map for- -- others to see.- --- -- While doing all this, keep track of how many streams we saw and- -- how many we have closed which is used later to report statistics- -- to the user: this is in contrast to doing linear-time traversals- -- just to get counts of things we've already traversed.- stats <- flip execStateT (ReaperState 0 0) $- forM_ (HM.toList allStreams) $ \(mPath, streams) ->- forM_ (HM.toList streams) $ \(sPath, stream) -> do- modify' $ \s -> s { totalStreams = totalStreams s + 1 }- snapAct <- lift $ readTVarIO (vActivity stream)- when (shouldPrune snapAct) $ do- -- The stream indicates that it's old and should be pruned. Take- -- a lock on the outer map, check again inside a transaction and- -- amend the maps as necessary.- deletedStream'm <- lift . atomically $ tryPrune mPath sPath- forM_ deletedStream'm $ \prunedStream -> do- lift $ closeStream prunedStream- modify' $ \s -> s { prunedStreams = prunedStreams s + 1 }-- when (prunedStreams stats > 0) $ hPutStrLn stderr $ unwords- [ "[reaper] closed"- , show (prunedStreams stats)- , "out of"- , show (totalStreams stats)- ]-- threadDelay $ floor $ int * 1e6--withFranzReader :: (FranzReader -> IO ()) -> IO ()-withFranzReader = bracket newFranzReader closeFranzReader--newFranzReader :: IO FranzReader-newFranzReader = do- vStreams <- newTVarIO HM.empty- watchManager <- FS.startManager- pure FranzReader{..}--closeFranzReader :: FranzReader -> IO ()-closeFranzReader FranzReader{..} = do- FS.stopManager watchManager- readTVarIO vStreams >>= mapM_ (mapM_ closeStream)---- | Globally-configured path which contains franz directories.-newtype FranzPrefix = FranzPrefix { unFranzPrefix :: FilePath } deriving (Eq, Hashable)---- | Directory which contains franz streams.--- Values of this type serve two purposes:------ * Arbitrary prefix so that clients don't have to specify the full path -newtype FranzDirectory = FranzDirectory FilePath deriving (Eq, Hashable)--getFranzDirectory :: FranzPrefix -> FranzDirectory -> FilePath-getFranzDirectory (FranzPrefix prefix) (FranzDirectory dir) = prefix </> dir--getFranzStreamPath :: FranzPrefix -> FranzDirectory -> StreamName -> FilePath-getFranzStreamPath prefix dir name- = getFranzDirectory prefix dir </> streamNameToPath name--handleQuery :: FranzPrefix- -> FranzReader- -> FranzDirectory- -> Query- -> (FranzException -> IO r)- -> (Stream -> STM (Bool, QueryResult) -> IO r) -> IO r-handleQuery prefix FranzReader{..} dir (Query name begin_ end_ rt) onError cont- = bracket (try acquire) (either mempty removeActivity) $ \case- Left err -> onError err- Right stream@Stream{..} -> cont stream $ do- readTVar vStatus >>= \case- Outdated -> retry- CaughtUp -> pure ()- Gone -> throwSTM $ StreamNotFound streamPath- allOffsets <- readTVar vOffsets- let finalOffset = case IM.maxViewWithKey allOffsets of- Just ((k, _), _) -> k + 1- Nothing -> 0- let rotate i- | i < 0 = finalOffset + i- | otherwise = i- begin <- case begin_ of- BySeqNum i -> pure $ rotate i- ByIndex index val -> case HM.lookup index indices of- Nothing -> throwSTM $ IndexNotFound index $ HM.keys indices- Just v -> do- m <- readTVar v- let (_, wing) = splitR val m- return $! maybe maxBound fst $ IM.minView wing- end <- case end_ of- BySeqNum i -> pure $ rotate i- ByIndex index val -> case HM.lookup index indices of- Nothing -> throwSTM $ IndexNotFound index $ HM.keys indices- Just v -> do- m <- readTVar v- let (body, lastItem, _) = IM.splitLookup val m- let body' = maybe id (IM.insert val) lastItem body- return $! maybe minBound fst $ IM.maxView body'- return $! range begin end rt allOffsets- where- acquire = join $ atomically $ do- allStreams <- readTVar vStreams- let !streams = maybe mempty id $ HM.lookup dir allStreams- case HM.lookup name streams of- Nothing -> pure $ bracketOnError- (createStream watchManager $ getFranzStreamPath prefix dir name)- closeStream $ \s -> atomically $ do- addActivity s- modifyTVar' vStreams $ HM.insert dir $ HM.insert name s streams- pure s- Just s -> do- addActivity s- pure (pure s)
− src/Database/Franz/Reconnect.hs
@@ -1,96 +0,0 @@-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE LambdaCase #-}-module Database.Franz.Reconnect- ( Pool- , poolLogFunc- , poolRetryPolicy- , withPool- , withReconnection- , Reconnect(..)- , atomicallyReconnecting- , fetchWithPool- )- where--import Control.Retry (recovering, RetryPolicyM)-import Control.Concurrent.MVar-import Control.Concurrent.STM-import Control.Exception (IOException)-import Control.Monad.Catch-import Database.Franz.Client--data Pool = Pool- { poolPath :: FranzPath- , poolRef :: MVar (Int {- Connection number -}, Maybe Connection)- , poolRetryPolicy :: RetryPolicyM IO- , poolLogFunc :: String -> IO ()- }---- | A wrapper of 'fetch' which calls 'withReconnection' internally-fetchWithPool- :: Pool- -> Query- -> (STM Response -> IO r)- -> IO r-fetchWithPool pool q cont = withReconnection pool $ \conn -> fetch conn q cont---- | Run an action which takes a 'Connection', reconnecting whenever it throws an exception.-withReconnection :: Pool -> (Connection -> IO a) -> IO a-withReconnection Pool{..} cont = recovering- poolRetryPolicy- [const $ Handler $ \Reconnect -> pure True]- body- where-- handler ex- | Just (ClientError err) <- fromException ex = Just err- | Just e <- fromException ex = Just (show (e :: IOException))- | Just Reconnect <- fromException ex = Just- $ "Connection to " <> fromFranzPath poolPath <> " timed out"- | otherwise = Nothing-- body _ = do- (i, conn) <- modifyMVar poolRef $ \case- (i, Nothing) -> do- poolLogFunc $ unwords- [ "Connnecting to"- , fromFranzPath poolPath- ]- conn <- tryJust handler (connect poolPath)- >>= either (\e -> poolLogFunc e >> throwM Reconnect) pure- poolLogFunc $ "Connection #" <> show i <> " established"- pure ((i, Just conn), (i, conn))- v@(i, Just c) -> pure (v, (i, c))-- tryJust handler (cont conn) >>= \case- Right a -> pure a- Left msg -> do- poolLogFunc msg- modifyMVar_ poolRef $ \case- -- Don't disconnect if the sequential number is different;- -- another thread already established a new connection- (j, Just _) | i == j -> (i + 1, Nothing) <$ disconnect conn- x -> pure x- throwM Reconnect--data Reconnect = Reconnect deriving (Show, Eq)-instance Exception Reconnect--withPool :: RetryPolicyM IO- -> (String -> IO ()) -- ^ diagnostic output- -> FranzPath- -> (Pool -> IO a)- -> IO a-withPool poolRetryPolicy poolLogFunc poolPath cont = do- poolRef <- newMVar (0, Nothing)- cont Pool{..} `finally` do- (_, conn) <- takeMVar poolRef- mapM_ disconnect conn---- | Run an 'STM' action, throwing 'Reconnect' when it exceeds the given timeout.-atomicallyReconnecting :: Int -- ^ timeout in microseconds- -> STM a -> IO a-atomicallyReconnecting timeout m = atomicallyWithin timeout m- >>= maybe (throwM Reconnect) pure
src/Database/Franz/Server.hs view
@@ -5,9 +5,8 @@ module Database.Franz.Server ( Settings(..) , startServer+ , FranzPrefix(..) , defaultPort- , mountFuse- , killFuse ) where import Control.Concurrent@@ -15,11 +14,11 @@ import Control.Monad import Control.Monad.IO.Class import Control.Monad.Trans.Cont-import Control.Retry import Control.Concurrent.STM-import Database.Franz.Internal-import Database.Franz.Protocol-import Database.Franz.Reader+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@@ -32,8 +31,7 @@ import qualified Network.Socket as S import System.Directory import System.IO-import System.Process (ProcessHandle, spawnProcess, cleanupProcess,- waitForProcess, getProcessExitCode, getPid)+import System.Process (ProcessHandle, getPid) data Env = Env { prefix :: FranzPrefix@@ -214,36 +212,10 @@ -> FilePath -> IO a -> IO a withFuse vMounts release src dst body = withSharedResource vMounts dst- (mountFuse src dst)+ (mountFuse logServer (throwIO . InternalError) src dst) (\fuse -> do release- killFuse fuse dst `finally` do+ killFuse logServer fuse dst `finally` do pid <- getPid fuse forM_ pid $ \p -> logServer ["Undead squashfuse detected:", show p]) (const body)--mountFuse :: FilePath -> FilePath -> IO ProcessHandle-mountFuse src dest = do- createDirectoryIfMissing True dest- logServer ["squashfuse", "-f", src, dest]- bracketOnError (spawnProcess "squashfuse" ["-f", src, dest]) (flip killFuse dest) $ \fuse -> do- -- It keeps process handles so that mounted directories are cleaned up- -- but there's no easy way to tell when squashfuse finished mounting.- -- Wait until the destination becomes non-empty.- notMounted <- retrying (limitRetries 5 <> exponentialBackoff 100000) (const pure)- $ \status -> getProcessExitCode fuse >>= \case- Nothing -> do- logServer ["Waiting for squashfuse to mount", src, ":", show status]- null <$> listDirectory dest- Just e -> do- removeDirectory dest- throwIO $ InternalError $ "squashfuse exited with " <> show e- when notMounted $ throwIO $ InternalError $ "Failed to mount " <> src- return fuse--killFuse :: ProcessHandle -> FilePath -> IO ()-killFuse fuse path = do- cleanupProcess (Nothing, Nothing, Nothing, fuse)- e <- waitForProcess fuse- logServer ["squashfuse:", show e]- removeDirectory path
− src/Database/Franz/URI.hs
@@ -1,53 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module Database.Franz.URI- ( FranzPath(..)- , toFranzPath- , fromFranzPath- ) where--import Data.List (stripPrefix)-import Data.String-import Network.Socket (HostName, PortNumber)-import Text.Read (readMaybe)--data FranzPath = FranzPath- { franzHost :: !HostName- , franzPort :: !PortNumber- , franzDir :: !FilePath- -- ^ Prefix of franz directories- }- | LocalFranzPath !FilePath- deriving (Show, Eq, Ord)--localPrefix :: IsString a => a-localPrefix = "franz-local:"--remotePrefix :: IsString a => a-remotePrefix = "franz://"---- | Parse a franz URI (franz://host:port/path or franz-local:path).-toFranzPath :: String -> Either String FranzPath-toFranzPath uri | Just path <- stripPrefix localPrefix uri = Right $ LocalFranzPath path-toFranzPath uri = do- hostnamePath <- maybe (Left $ "Expecting " <> remotePrefix) Right $ stripPrefix remotePrefix uri- (host, path) <- case break (== '/') hostnamePath of- (h, '/' : p) -> Right (h, p)- _ -> Left "Expecting /"- case break (== ':') host of- (hostname, ':' : portStr)- | Just p <- readMaybe portStr -> Right $ FranzPath hostname p path- | otherwise -> Left "Failed to parse the port number"- _ -> Right $ FranzPath host 1886 path---- | Render 'FranzPath' as a franz URI.-fromFranzPath :: (Monoid a, IsString a) => FranzPath -> a-fromFranzPath (FranzPath host port path) = mconcat- [ remotePrefix- , fromString host- , ":"- , fromString (show port)- , "/"- , fromString path- ]--fromFranzPath (LocalFranzPath path) = localPrefix <> fromString path