git.iko.soy/typeable/hazelcast-rest
on master help sign in
Public release
commitbd2cfe2dc2d1bc8ca5e6b44efe04ffb01b2b3d76 authorEvgenii Akentev <i@ak3n.com> dateWed 25 Sep 2019, 19:31 (7 years ago)
9 files changed+421
.gitignore+3 Setup.hs+2 benchmark/Benchmark.hs+90 hazelcast-rest.cabal+36 src/Database/Hazelcast/Rest.hs+7 src/Database/Hazelcast/Rest/Cluster.hs+29 src/Database/Hazelcast/Rest/Core.hs+110 src/Database/Hazelcast/Rest/Internal.hs+50 src/Database/Hazelcast/Rest/Map.hs+94
.gitignorenew file+3
@@ -0,0 +1,3 @@
1+dist
2+dist-newstyle
3+.ghc.environment.*
Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
benchmark/Benchmark.hsnew file+90
@@ -0,0 +1,90 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE PartialTypeSignatures #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+
5+module Main where
6+
7+import Control.Concurrent
8+import Control.Monad
9+import Control.Monad.Trans
10+import Data.List.NonEmpty as NE
11+import Data.Time
12+import Database.Hazelcast.Rest as Hazelcast
13+import Text.Printf
14+
15+nRequests, nClients :: Int
16+nRequests = 100000
17+nClients = 50
18+
19+
20+main :: IO ()
21+main = do
22+ ----------------------------------------------------------------------
23+ -- Preparation
24+ --
25+ let pingKey = mkMapPath "mapName" "PING"
26+
27+ let
28+ connInfo = Hazelcast.defaultConnectInfo
29+ { connectHosts = NE.fromList
30+ [ HazelcastHost "localhost" 5701 ]
31+ }
32+ conn <- connect connInfo
33+ runHazelcast conn $ do
34+ _ <- Hazelcast.insert pingKey "PONG"
35+ return ()
36+
37+ ----------------------------------------------------------------------
38+ -- Spawn clients
39+ --
40+ start <- newEmptyMVar
41+ done <- newEmptyMVar
42+ replicateM_ nClients $ forkIO $ do
43+ runHazelcast conn $ forever $ do
44+ action <- liftIO $ takeMVar start
45+ action
46+ liftIO $ putMVar done ()
47+
48+ let
49+ timeAction name nActions action = do
50+ startT <- getCurrentTime
51+ -- each clients runs ACTION nRepetitions times
52+ let nRepetitions = nRequests `div` nClients `div` nActions
53+ replicateM_ nClients $ putMVar start (replicateM_ nRepetitions action)
54+ replicateM_ nClients $ takeMVar done
55+ stopT <- getCurrentTime
56+ let
57+ deltaT = realToFrac $ diffUTCTime stopT startT
58+ -- the real # of reqs send. We might have lost some due to 'div'.
59+ actualReqs = nRepetitions * nActions * nClients
60+ rqsPerSec = fromIntegral actualReqs / deltaT :: Double
61+ putStrLn $ printf "%-15s %4dx%4dx%2d %10.2f Req/s"
62+ (name :: String) nActions nRepetitions nClients rqsPerSec
63+
64+ ----------------------------------------------------------------------
65+ -- Benchmarks
66+ --
67+ timeAction "get" 1 $ do
68+ r <- Hazelcast.lookup pingKey
69+ unless (r == ReplyOk "PONG") $ error $ "bad hazelcast reply: " ++ show r
70+ return ()
71+
72+ timeAction "get 1" 1 $ do
73+ _ <- Hazelcast.lookupMany (replicate 1 pingKey)
74+ return ()
75+
76+ timeAction "get 50" 50 $ do
77+ _ <- Hazelcast.lookupMany (replicate 50 pingKey)
78+ return ()
79+
80+ timeAction "get 100" 100 $ do
81+ _ <- Hazelcast.lookupMany (replicate 100 pingKey)
82+ return ()
83+
84+ timeAction "get 1000" 1000 $ do
85+ _ <- Hazelcast.lookupMany (replicate 1000 pingKey)
86+ return ()
87+
88+ timeAction "put" 1 $ do
89+ _ <- Hazelcast.insert pingKey "DONG"
90+ return ()
hazelcast-rest.cabalnew file+36
@@ -0,0 +1,36 @@
1+name: hazelcast-rest
2+version: 0.1.0.0
3+synopsis: Client library for the Hazelcast utilizing REST protocol
4+license: BSD3
5+author: Typeable.io contributors
6+maintainer: makeit@typeable.io
7+category: Database
8+build-type: Simple
9+cabal-version: >=1.10
10+
11+library
12+ build-depends: base >=4.10 && <5
13+ , bytestring >=0.10.8
14+ , base64-bytestring
15+ , http-client >=0.5
16+ , http-types >=0.9
17+ , mtl >=2.2
18+ hs-source-dirs: src
19+ default-language: Haskell2010
20+ exposed-modules: Database.Hazelcast.Rest
21+ , Database.Hazelcast.Rest.Cluster
22+ , Database.Hazelcast.Rest.Core
23+ , Database.Hazelcast.Rest.Internal
24+ , Database.Hazelcast.Rest.Map
25+ ghc-options: -Wall
26+
27+benchmark hazelcast-benchmark
28+ type: exitcode-stdio-1.0
29+ hs-source-dirs: benchmark
30+ default-language: Haskell2010
31+ main-is: Benchmark.hs
32+ build-depends: base >=4.10 && <5
33+ , hazelcast-rest
34+ , mtl >=2.2
35+ , time
36+ ghc-options: -O2 -Wall -rtsopts
src/Database/Hazelcast/Rest.hsnew file+7
@@ -0,0 +1,7 @@
1+module Database.Hazelcast.Rest
2+ ( module X
3+ ) where
4+
5+import Database.Hazelcast.Rest.Core as X
6+import Database.Hazelcast.Rest.Internal as X
7+import Database.Hazelcast.Rest.Map as X
src/Database/Hazelcast/Rest/Cluster.hsnew file+29
@@ -0,0 +1,29 @@
1+{-|
2+
3+Module provides type representing Hazelcast cluster and function to select a
4+host using Round-robin algorithm.
5+
6+-}
7+
8+module Database.Hazelcast.Rest.Cluster
9+ ( ClusterInfo(..)
10+ , roundRobinHost
11+ ) where
12+
13+import Data.IORef
14+import Database.Hazelcast.Rest.Internal
15+
16+-- | 'ClusterInfo' represents Hazelcast cluster.
17+newtype ClusterInfo = ClusterInfo (IORef [HazelcastHost])
18+
19+-- | Select host from cluster using Round-robin algorithm.
20+roundRobinHost :: ClusterInfo -> IO HazelcastHost
21+roundRobinHost (ClusterInfo ref) = atomicModifyIORef' ref rotateHosts
22+
23+-- | Return current head host in list and rotate list of hosts.
24+rotateHosts
25+ :: [HazelcastHost]
26+ -> ([HazelcastHost], HazelcastHost)
27+rotateHosts hosts = (rotate hosts, head hosts)
28+ where
29+ rotate l = zipWith const (drop 1 $ cycle l) l
src/Database/Hazelcast/Rest/Core.hsnew file+110
@@ -0,0 +1,110 @@
1+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE RecordWildCards #-}
4+{-|
5+
6+Module provides Hazelcast monad and functions for opening connection and
7+sending requests.
8+
9+-}
10+
11+
12+module Database.Hazelcast.Rest.Core
13+ ( connect
14+ , HazelcastEnv(..)
15+ , Hazelcast(..)
16+ , MonadHazelcast(..)
17+ , runHazelcast
18+ , sendRequest
19+ , sendRequests
20+ , defaultConnectInfo
21+ -- reexport
22+ , Internal.Reply(..)
23+ , Internal.ConnectInfo(..)
24+ , Internal.HazelcastHost(..)
25+ ) where
26+
27+import Control.Monad.Reader
28+import Data.IORef
29+import qualified Data.List.NonEmpty as NE
30+import Database.Hazelcast.Rest.Cluster
31+import Database.Hazelcast.Rest.Internal as Internal
32+import qualified Network.HTTP.Client as HTTP
33+
34+
35+-- | Open connection with Hazelcast using connection information.
36+connect :: ConnectInfo -> IO HazelcastEnv
37+connect ConnectInfo{..} = do
38+ let
39+ ss = HTTP.defaultManagerSettings
40+ { HTTP.managerConnCount = connectMaxConnections
41+ , HTTP.managerResponseTimeout = HTTP.responseTimeoutMicro
42+ $ connectTimeoutSeconds * 1000000
43+ }
44+ manager <- HTTP.newManager ss
45+ clusterInfo <- ClusterInfo <$> newIORef (NE.toList connectHosts)
46+ pure (HazelcastEnv manager clusterInfo)
47+
48+-- | 'HazelcastEnv' provides opened connection and cluster information.
49+data HazelcastEnv = HazelcastEnv
50+ { henvConnection :: HTTP.Manager
51+ , henvClusterInfo :: ClusterInfo
52+ }
53+
54+-- | 'Hazelcast' type is a wrapper around ReaderT monad contains 'HazelcastEnv'
55+-- type to read.
56+newtype Hazelcast a = Hazelcast
57+ { unHazelcast :: ReaderT HazelcastEnv IO a
58+ } deriving (Monad, MonadIO, Functor, Applicative)
59+
60+-- | 'MonadHazelcast' provides function to lift Hazelcast type to another.
61+class Monad m => MonadHazelcast m where
62+ liftHazelcast :: Hazelcast a -> m a
63+
64+instance MonadHazelcast Hazelcast where
65+ liftHazelcast = id
66+
67+-- | Hazelcast monad runner.
68+runHazelcast :: HazelcastEnv -> Hazelcast a -> IO a
69+runHazelcast hEnv (Hazelcast hazelcast) =
70+ runReaderT hazelcast hEnv
71+
72+-- | Send HTTP request to Hazelcast using 'MonadHazelcast'.
73+sendRequest
74+ :: (MonadHazelcast m)
75+ => HTTP.Request
76+ -> m Reply
77+sendRequest r =
78+ liftHazelcast . Hazelcast $ do
79+ m <- asks henvConnection
80+ c <- asks henvClusterInfo
81+ HazelcastHost{..} <- liftIO $ roundRobinHost c
82+ let
83+ req = r
84+ { HTTP.host = hostAddr
85+ , HTTP.port = fromIntegral hostPort }
86+ liftIO $ Internal.sendHttpRequest m req
87+
88+-- | Send many HTTP requests to Hazelcast using 'MonadHazelcast'.
89+sendRequests
90+ :: (MonadHazelcast m, Traversable t)
91+ => t HTTP.Request
92+ -> m (t Reply)
93+sendRequests rqs =
94+ liftHazelcast . Hazelcast $ do
95+ m <- asks henvConnection
96+ c <- asks henvClusterInfo
97+ HazelcastHost{..} <- liftIO $ roundRobinHost c
98+ let
99+ modifyReq r = r
100+ { HTTP.host = hostAddr
101+ , HTTP.port = fromIntegral hostPort }
102+ liftIO $ traverse (Internal.sendHttpRequest m . modifyReq) rqs
103+
104+-- | Default Hazelcast connection information.
105+defaultConnectInfo :: ConnectInfo
106+defaultConnectInfo = ConnectInfo
107+ { connectHosts = NE.fromList [HazelcastHost "localhost" 5701]
108+ , connectMaxConnections = 50
109+ , connectTimeoutSeconds = 10
110+ }
src/Database/Hazelcast/Rest/Internal.hsnew file+50
@@ -0,0 +1,50 @@
1+{-|
2+
3+Module contains types and functions required for connection with Hazelcast.
4+
5+-}
6+
7+module Database.Hazelcast.Rest.Internal where
8+
9+import qualified Data.ByteString as BS
10+import qualified Data.ByteString.Lazy as BS.L
11+import qualified Data.List.NonEmpty as NE
12+import Data.Word
13+import qualified Network.HTTP.Client as HTTP
14+import qualified Network.HTTP.Types as HTTP
15+
16+-- | Type represents Hazelcast host.
17+data HazelcastHost = HazelcastHost
18+ { hostAddr :: BS.ByteString
19+ , hostPort :: Word16
20+ }
21+
22+-- | Type represents connection information.
23+data ConnectInfo = ConnectInfo
24+ { connectHosts :: NE.NonEmpty HazelcastHost
25+ , connectMaxConnections :: Int
26+ , connectTimeoutSeconds :: Int
27+ }
28+
29+-- | Type provides possible Hazelcast responses.
30+data Reply
31+ = ReplyOk BS.L.ByteString
32+ | ReplyEmpty
33+ | ReplyError Int
34+ deriving (Eq, Show)
35+
36+-- | Send HTTP request to Hazelcast.
37+sendHttpRequest
38+ :: HTTP.Manager
39+ -> HTTP.Request
40+ -> IO Reply
41+sendHttpRequest m req =
42+ readReply <$> HTTP.httpLbs req m
43+
44+-- | Read status code from Hazelcast response and depending on status code
45+-- returns one of Reply constructor.
46+readReply :: HTTP.Response BS.L.ByteString -> Reply
47+readReply r = case HTTP.statusCode (HTTP.responseStatus r) of
48+ 204 -> ReplyEmpty
49+ 200 -> ReplyOk $ HTTP.responseBody r
50+ x -> ReplyError x
src/Database/Hazelcast/Rest/Map.hsnew file+94
@@ -0,0 +1,94 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-|
3+
4+Module contaions high-level functions for working with Hazelcast.
5+
6+-}
7+
8+
9+module Database.Hazelcast.Rest.Map
10+ ( Path
11+ , mkMapPath
12+ , insert
13+ , lookup
14+ , lookupMany
15+ , delete
16+ , getRq
17+ , postRq
18+ , deleteRq
19+ ) where
20+
21+import qualified Data.ByteString as BS
22+import Data.ByteString.Base64.URL
23+import Database.Hazelcast.Rest.Core as Core
24+import qualified Network.HTTP.Client as HTTP
25+import qualified Network.HTTP.Types as HTTP
26+import Prelude hiding (lookup)
27+
28+-- | Type represents url-safe (only valid url characters) path.
29+newtype Path = Path { unPath :: BS.ByteString }
30+ deriving (Show)
31+
32+-- | Construct url-safe path. There is no map name encoding because it
33+-- may be set in hazelcast config file and after encoding nonexistent map name
34+-- can be obtained.
35+mkMapPath
36+ :: BS.ByteString -- ^ map name should not contain invalid url characters
37+ -> BS.ByteString
38+ -> Path
39+mkMapPath mapName key = Path $ BS.concat
40+ [ "/hazelcast/rest/maps/"
41+ , mapName
42+ , "/"
43+ , encode key]
44+
45+-- | Insert value by path to Hazelcast.
46+insert :: MonadHazelcast m => Path -> BS.ByteString -> m Reply
47+insert (Path path) body = Core.sendRequest (postRq path body)
48+
49+-- | Find value by path in Hazelcast.
50+lookup :: MonadHazelcast m => Path -> m Reply
51+lookup (Path path) = Core.sendRequest (getRq path)
52+
53+-- | Find values by paths in Hazelcast.
54+lookupMany
55+ :: (MonadHazelcast m, Traversable t)
56+ => t Path
57+ -> m (t Reply)
58+lookupMany paths = Core.sendRequests qs
59+ where
60+ qs = fmap (getRq . unPath) paths
61+
62+-- | Delete value by path in Hazelcast.
63+delete :: MonadHazelcast m => Path -> m Reply
64+delete (Path path) = Core.sendRequest (deleteRq path)
65+
66+-- | Form GET request to Hazelcast.
67+getRq :: BS.ByteString -> HTTP.Request
68+getRq path = HTTP.defaultRequest
69+ { HTTP.method = HTTP.methodGet
70+ , HTTP.path = path
71+ , HTTP.requestVersion = HTTP.http11
72+ , HTTP.requestHeaders = [(HTTP.hAccept, "*/*")]
73+ }
74+
75+-- | Form POST request to Hazelcast.
76+postRq :: BS.ByteString -> BS.ByteString -> HTTP.Request
77+postRq path body = HTTP.defaultRequest
78+ { HTTP.method = HTTP.methodPost
79+ , HTTP.path = path
80+ , HTTP.requestVersion = HTTP.http11
81+ , HTTP.requestHeaders =
82+ [ (HTTP.hAccept, "*/*")
83+ , (HTTP.hContentType, "text/plain") ]
84+ , HTTP.requestBody = HTTP.RequestBodyBS body
85+ }
86+
87+-- | Form DELETE request to Hazelcast.
88+deleteRq :: BS.ByteString -> HTTP.Request
89+deleteRq path = HTTP.defaultRequest
90+ { HTTP.method = HTTP.methodDelete
91+ , HTTP.path = path
92+ , HTTP.requestVersion = HTTP.http11
93+ , HTTP.requestHeaders = [ (HTTP.hAccept, "*/*") ]
94+ }