Use bucket region cache to minimize getLocation requests (#3)

This commit is contained in:
Krishnan Parthasarathi 2017-02-25 16:42:23 +05:30 committed by Aditya Manthramurthy
parent e4e2576c74
commit abdc9fe320
11 changed files with 231 additions and 31 deletions

24
examples/listBuckets.hs Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env stack
-- stack --resolver lts-6.27 runghc --package minio-hs
{-# Language OverloadedStrings #-}
import Network.Minio
import Prelude
-- | The following example uses minio's play server at
-- https://play.minio.io:9000. The endpoint and associated
-- credentials are provided via the libary constant,
--
-- > minioPlayCI :: ConnectInfo
--
-- This example list buckets that belongs to the user and returns
-- region of the first bucket returned.
main :: IO ()
main = do
firstRegionE <- runResourceT $ runMinio minioPlayCI $ do
buckets <- getService
getLocation $ biName $ head buckets
print firstRegionE

24
examples/makebucket.hs Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env stack
-- stack --resolver lts-6.27 runghc --package minio-hs
{-# Language OverloadedStrings #-}
import Network.Minio
import Prelude
-- | The following example uses minio's play server at
-- https://play.minio.io:9000. The endpoint and associated
-- credentials are provided via the libary constant,
--
-- > minioPlayCI :: ConnectInfo
--
main :: IO ()
main = do
let
bucket = "my-bucket"
res <- runResourceT $ runMinio minioPlayCI $ do
-- N B the region provided for makeBucket is optional.
makeBucket bucket (Just "us-east-1")
print res

23
examples/removebucket.hs Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env stack
-- stack --resolver lts-6.27 runghc --package minio-hs
{-# Language OverloadedStrings #-}
import Network.Minio
import Prelude
-- | The following example uses minio's play server at
-- https://play.minio.io:9000. The endpoint and associated
-- credentials are provided via the libary constant,
--
-- > minioPlayCI :: ConnectInfo
--
main :: IO ()
main = do
let
bucket = "my-bucket"
res <- runResourceT $ runMinio minioPlayCI $ do
removeBucket bucket
print res

View File

@ -3,7 +3,11 @@ module Network.Minio
ConnectInfo(..) ConnectInfo(..)
, awsCI , awsCI
, awsWithRegion
, minioPlayCI , minioPlayCI
, minioSimple
, minioSimpleTLS
, minioWithOpts
, Minio , Minio
, runMinio , runMinio
@ -34,6 +38,7 @@ module Network.Minio
, getService , getService
, getLocation , getLocation
, makeBucket , makeBucket
, removeBucket
, listObjects , listObjects
, listIncompleteUploads , listIncompleteUploads
@ -58,6 +63,7 @@ This module exports the high-level Minio API for object storage.
import Control.Monad.Trans.Resource (runResourceT) import Control.Monad.Trans.Resource (runResourceT)
import qualified Data.Conduit as C import qualified Data.Conduit as C
import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.Binary as CB
import qualified Data.Map as Map
import Lib.Prelude import Lib.Prelude
@ -108,7 +114,13 @@ makeBucket :: Bucket -> Maybe Region -> Minio ()
makeBucket bucket regionMay= do makeBucket bucket regionMay= do
region <- maybe (asks $ connectRegion . mcConnInfo) return regionMay region <- maybe (asks $ connectRegion . mcConnInfo) return regionMay
putBucket bucket region putBucket bucket region
modify (Map.insert bucket region)
-- | Get an object's metadata from the object store. -- | Get an object's metadata from the object store.
statObject :: Bucket -> Object -> Minio ObjectInfo statObject :: Bucket -> Object -> Minio ObjectInfo
statObject bucket object = headObject bucket object statObject bucket object = headObject bucket object
removeBucket :: Bucket -> Minio()
removeBucket bucket = do
deleteBucket bucket
modify (Map.delete bucket)

View File

@ -5,10 +5,13 @@ module Network.Minio.API
, runMinio , runMinio
, executeRequest , executeRequest
, mkStreamRequest , mkStreamRequest
, getLocation
) where ) where
import qualified Data.Conduit as C import qualified Data.Conduit as C
import Data.Conduit.Binary (sourceHandleRange) import Data.Conduit.Binary (sourceHandleRange)
import Data.Default (def)
import qualified Data.Map as Map
import Network.HTTP.Conduit (Response) import Network.HTTP.Conduit (Response)
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types as HT
@ -19,6 +22,7 @@ import Network.Minio.Data
import Network.Minio.Data.Crypto import Network.Minio.Data.Crypto
import Network.Minio.Sign.V4 import Network.Minio.Sign.V4
import Network.Minio.Utils import Network.Minio.Utils
import Network.Minio.XmlParser
sha256Header :: ByteString -> HT.Header sha256Header :: ByteString -> HT.Header
sha256Header = ("x-amz-content-sha256", ) sha256Header = ("x-amz-content-sha256", )
@ -38,17 +42,59 @@ getRequestBody (PayloadH h off size) =
(return . fromIntegral $ off) (return . fromIntegral $ off)
(return . fromIntegral $ size) (return . fromIntegral $ size)
buildRequest :: (MonadIO m, MonadReader MinioConn m)
=> RequestInfo -> m NC.Request -- | Fetch bucket location (region)
getLocation :: Bucket -> Minio Region
getLocation bucket = do
resp <- executeRequest $ def {
riBucket = Just bucket
, riQueryParams = [("location", Nothing)]
, riNeedsLocation = False
}
parseLocation $ NC.responseBody resp
-- | Looks for region in RegionMap and updates it using getLocation if
-- absent.
discoverRegion :: RequestInfo -> Minio (Maybe Region)
discoverRegion ri = runMaybeT $ do
bucket <- MaybeT $ return $ riBucket ri
regionMay <- gets (Map.lookup bucket)
maybe (do
l <- lift $ getLocation bucket
modify $ Map.insert bucket l
return l
) return regionMay
buildRequest :: RequestInfo -> Minio NC.Request
buildRequest ri = do buildRequest ri = do
{-
If ListBuckets/MakeBucket/GetLocation then use connectRegion ci
Else If discovery off use connectRegion ci
Else {
// Here discovery is on
Lookup region in regionMap
If present use that
Else getLocation
}
-}
ci <- asks mcConnInfo
region <- if | not $ riNeedsLocation ri -> -- getService/makeBucket/getLocation
-- don't need location
return $ Just $ connectRegion ci
| not $ connectAutoDiscoverRegion ci -> -- if autodiscovery of location is disabled by user
return $ Just $ connectRegion ci
| otherwise -> discoverRegion ri
sha256Hash <- getPayloadSHA256Hash (riPayload ri) sha256Hash <- getPayloadSHA256Hash (riPayload ri)
let newRi = ri { let newRi = ri {
riPayloadHash = sha256Hash riPayloadHash = sha256Hash
, riHeaders = sha256Header sha256Hash : (riHeaders ri) , riHeaders = sha256Header sha256Hash : (riHeaders ri)
, riRegion = region
} }
ci <- asks mcConnInfo
reqHeaders <- liftIO $ signV4 ci newRi reqHeaders <- liftIO $ signV4 ci newRi
return NC.defaultRequest { return NC.defaultRequest {

View File

@ -5,19 +5,43 @@ import Control.Monad.Base
import qualified Control.Monad.Catch as MC import qualified Control.Monad.Catch as MC
import Control.Monad.Trans.Control import Control.Monad.Trans.Control
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
import qualified Data.ByteString as B import qualified Data.ByteString as B
import Data.Default (Default(..)) import Data.Default (Default(..))
import qualified Data.Map as Map
import qualified Data.Text as T import qualified Data.Text as T
import Network.HTTP.Client (defaultManagerSettings) import Network.HTTP.Client (defaultManagerSettings)
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import Network.HTTP.Types (Method, Header, Query) import Network.HTTP.Types (Method, Header, Query)
import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types as HT
import Network.Minio.Errors
import Network.Minio.Utils
import Text.XML import Text.XML
import Lib.Prelude import Lib.Prelude
import Network.Minio.Errors
import Network.Minio.Utils -- TODO: Add a type which provides typed constants for region. this
-- type should have a IsString instance to infer the appropriate
-- constant.
-- | awsRegionMap - library constant
awsRegionMap :: Map.Map Text Text
awsRegionMap = Map.fromList [
("us-east-1", "s3.amazonaws.com")
, ("us-east-2", "s3-us-east-2.amazonaws.com")
, ("us-west-1", "s3-us-west-1.amazonaws.com")
, ("us-east-2", "s3-us-west-2.amazonaws.com")
, ("ca-central-1", "s3-ca-central-1.amazonaws.com")
, ("ap-south-1", "s3-ap-south-1.amazonaws.com")
, ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
, ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
, ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
, ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
, ("eu-west-1", "s3-eu-west-1.amazonaws.com")
, ("eu-west-2", "s3-eu-west-2.amazonaws.com")
, ("eu-central-1", "s3-eu-central-1.amazonaws.com")
, ("sa-east-1", "s3-sa-east-1.amazonaws.com")
]
-- | Connection Info data type. Use the Default instance to create -- | Connection Info data type. Use the Default instance to create
-- connection info for your service. -- connection info for your service.
@ -28,10 +52,11 @@ data ConnectInfo = ConnectInfo {
, connectSecretKey :: Text , connectSecretKey :: Text
, connectIsSecure :: Bool , connectIsSecure :: Bool
, connectRegion :: Region , connectRegion :: Region
, connectAutoDiscoverRegion :: Bool
} deriving (Eq, Show) } deriving (Eq, Show)
instance Default ConnectInfo where instance Default ConnectInfo where
def = ConnectInfo "localhost" 9000 "minio" "minio123" False "us-east-1" def = ConnectInfo "localhost" 9000 "minio" "minio123" False "us-east-1" True
-- | -- |
-- Default aws ConnectInfo. Credentials should be supplied before use. -- Default aws ConnectInfo. Credentials should be supplied before use.
@ -44,6 +69,21 @@ awsCI = def {
, connectIsSecure = True , connectIsSecure = True
} }
-- |
-- aws ConnectInfo with the specified region.
-- This is for users who don't want minio-hs discovering region of a
-- bucket if not known.
awsWithRegion :: Region -> Bool -> ConnectInfo
awsWithRegion region autoDiscoverRegion =
let host = maybe "s3.amazonaws.com" identity $
Map.lookup region awsRegionMap
in awsCI {
connectHost = host
, connectRegion = region
, connectAutoDiscoverRegion = autoDiscoverRegion
}
-- | -- |
-- Default minio play server ConnectInfo. Credentials are already filled. -- Default minio play server ConnectInfo. Credentials are already filled.
minioPlayCI :: ConnectInfo minioPlayCI :: ConnectInfo
@ -55,6 +95,38 @@ minioPlayCI = def {
, connectIsSecure = True , connectIsSecure = True
} }
-- |
-- ConnectInfo for minio server over HTTP.
minioSimple :: Text -> Int -> ConnectInfo
minioSimple host port = def {
connectHost = host
, connectPort = port
, connectRegion = "us-east-1"
, connectIsSecure = False
}
-- |
-- ConnectInfo for minio server over HTTPS.
minioSimpleTLS :: Text -> Int -> ConnectInfo
minioSimpleTLS host port = mSimple {
connectIsSecure = True
}
where
mSimple = minioSimple host port
-- |
-- ConnectInfo for minio server with no defaults.
-- This is for users who don't want minio-hs discovering region of a
-- bucket if not known.
minioWithOpts :: Text -> Int -> Region -> Bool -> Bool -> ConnectInfo
minioWithOpts host port region secure autoDiscoverRegion = def {
connectHost = host
, connectPort = port
, connectRegion = region
, connectIsSecure = secure
, connectAutoDiscoverRegion = autoDiscoverRegion
}
-- | -- |
-- Represents a bucket in the object store -- Represents a bucket in the object store
type Bucket = Text type Bucket = Text
@ -204,10 +276,11 @@ data RequestInfo = RequestInfo {
, riPayload :: Payload , riPayload :: Payload
, riPayloadHash :: ByteString , riPayloadHash :: ByteString
, riRegion :: Maybe Region , riRegion :: Maybe Region
, riNeedsLocation :: Bool
} }
instance Default RequestInfo where instance Default RequestInfo where
def = RequestInfo HT.methodGet def def def def def "" def def = RequestInfo HT.methodGet def def def def def "" def True
getPathFromRI :: RequestInfo -> ByteString getPathFromRI :: RequestInfo -> ByteString
getPathFromRI ri = B.concat $ parts getPathFromRI ri = B.concat $ parts
@ -215,11 +288,10 @@ getPathFromRI ri = B.concat $ parts
objPart = maybe [] (\o -> ["/", encodeUtf8 o]) $ riObject ri objPart = maybe [] (\o -> ["/", encodeUtf8 o]) $ riObject ri
parts = maybe ["/"] (\b -> "/" : encodeUtf8 b : objPart) $ riBucket ri parts = maybe ["/"] (\b -> "/" : encodeUtf8 b : objPart) $ riBucket ri
getRegionFromRI :: RequestInfo -> Text type RegionMap = Map.Map Bucket Region
getRegionFromRI ri = maybe "us-east-1" identity (riRegion ri)
newtype Minio a = Minio { newtype Minio a = Minio {
unMinio :: ReaderT MinioConn (ResourceT IO) a unMinio :: ReaderT MinioConn (StateT RegionMap (ResourceT IO)) a
} }
deriving ( deriving (
Functor Functor
@ -227,6 +299,7 @@ newtype Minio a = Minio {
, Monad , Monad
, MonadIO , MonadIO
, MonadReader MinioConn , MonadReader MinioConn
, MonadState RegionMap
, MonadThrow , MonadThrow
, MonadCatch , MonadCatch
, MonadBase IO , MonadBase IO
@ -234,7 +307,7 @@ newtype Minio a = Minio {
) )
instance MonadBaseControl IO Minio where instance MonadBaseControl IO Minio where
type StM Minio a = a type StM Minio a = (a, RegionMap)
liftBaseWith f = Minio $ liftBaseWith $ \q -> f (q . unMinio) liftBaseWith f = Minio $ liftBaseWith $ \q -> f (q . unMinio)
restoreM = Minio . restoreM restoreM = Minio . restoreM
@ -257,7 +330,7 @@ connect ci = do
runMinio :: ConnectInfo -> Minio a -> ResourceT IO (Either MinioErr a) runMinio :: ConnectInfo -> Minio a -> ResourceT IO (Either MinioErr a)
runMinio ci m = do runMinio ci m = do
conn <- liftIO $ connect ci conn <- liftIO $ connect ci
flip runReaderT conn . unMinio $ flip evalStateT Map.empty . flip runReaderT conn . unMinio $
(m >>= (return . Right)) `MC.catches` (m >>= (return . Right)) `MC.catches`
[MC.Handler handlerME, MC.Handler handlerHE, MC.Handler handlerFE] [MC.Handler handlerME, MC.Handler handlerHE, MC.Handler handlerFE]
where where

View File

@ -66,17 +66,11 @@ import Network.Minio.XmlParser
-- | Fetch all buckets from the service. -- | Fetch all buckets from the service.
getService :: Minio [BucketInfo] getService :: Minio [BucketInfo]
getService = do getService = do
resp <- executeRequest $ def resp <- executeRequest $ def {
riNeedsLocation = False
}
parseListBuckets $ NC.responseBody resp parseListBuckets $ NC.responseBody resp
-- | Fetch bucket location (region)
getLocation :: Bucket -> Minio Region
getLocation bucket = do
resp <- executeRequest $ def { riBucket = Just bucket
, riQueryParams = [("location", Nothing)]
}
parseLocation $ NC.responseBody resp
-- | GET an object from the service and return the response headers -- | GET an object from the service and return the response headers
-- and a conduit source for the object content -- and a conduit source for the object content
getObject' :: Bucket -> Object -> HT.Query -> [HT.Header] getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
@ -98,6 +92,7 @@ putBucket bucket location = do
def { riMethod = HT.methodPut def { riMethod = HT.methodPut
, riBucket = Just bucket , riBucket = Just bucket
, riPayload = PayloadBS $ mkCreateBucketConfig location , riPayload = PayloadBS $ mkCreateBucketConfig location
, riNeedsLocation = False
} }
-- | Single PUT object size. -- | Single PUT object size.

View File

@ -88,7 +88,9 @@ signV4AtTime ts ci ri =
authHeader = (mk "Authorization", authHeaderValue) authHeader = (mk "Authorization", authHeaderValue)
scope = getScope ts ri region = maybe (connectRegion ci) identity $ riRegion ri
scope = getScope ts region
authHeaderValue = B.concat [ authHeaderValue = B.concat [
"AWS4-HMAC-SHA256 Credential=", "AWS4-HMAC-SHA256 Credential=",
@ -105,7 +107,7 @@ signV4AtTime ts ci ri =
signingKey = hmacSHA256RawBS "aws4_request" signingKey = hmacSHA256RawBS "aws4_request"
. hmacSHA256RawBS "s3" . hmacSHA256RawBS "s3"
. hmacSHA256RawBS (encodeUtf8 $ getRegionFromRI ri) . hmacSHA256RawBS (encodeUtf8 region)
. hmacSHA256RawBS (awsDateFormatBS ts) . hmacSHA256RawBS (awsDateFormatBS ts)
$ (B.concat ["AWS4", encodeUtf8 $ connectSecretKey ci]) $ (B.concat ["AWS4", encodeUtf8 $ connectSecretKey ci])
@ -119,10 +121,10 @@ signV4AtTime ts ci ri =
canonicalRequest = getCanonicalRequest ri headersToSign canonicalRequest = getCanonicalRequest ri headersToSign
getScope :: UTCTime -> RequestInfo -> ByteString getScope :: UTCTime -> Region -> ByteString
getScope ts ri = B.intercalate "/" $ [ getScope ts region = B.intercalate "/" $ [
pack $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts, pack $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts,
encodeUtf8 $ getRegionFromRI ri, "s3", "aws4_request" encodeUtf8 region, "s3", "aws4_request"
] ]
getHeadersToSign :: [Header] -> [(ByteString, ByteString)] getHeadersToSign :: [Header] -> [(ByteString, ByteString)]

View File

@ -15,7 +15,7 @@ import qualified Data.Text as T
import Data.Text.Read (decimal) import Data.Text.Read (decimal)
import Data.Time import Data.Time
import Text.XML import Text.XML
import Text.XML.Cursor import Text.XML.Cursor hiding (bool)
import Lib.Prelude import Lib.Prelude
@ -65,7 +65,8 @@ parseListBuckets xmldata = do
parseLocation :: (MonadThrow m) => LByteString -> m Region parseLocation :: (MonadThrow m) => LByteString -> m Region
parseLocation xmldata = do parseLocation xmldata = do
r <- parseRoot xmldata r <- parseRoot xmldata
return $ T.concat $ r $/ content let region = T.concat $ r $/ content
return $ bool "us-east-1" region $ region /= ""
-- | Parse the response XML of an newMultipartUpload call. -- | Parse the response XML of an newMultipartUpload call.
parseNewMultipartUpload :: (MonadThrow m) parseNewMultipartUpload :: (MonadThrow m)

View File

@ -83,7 +83,7 @@ liveServerUnitTests = testGroup "Unit tests against a live server"
step "getLocation works" step "getLocation works"
region <- getLocation bucket region <- getLocation bucket
liftIO $ region == "" @? ("Got unexpected region => " ++ show region) liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
step "singlepart putObject works" step "singlepart putObject works"
fPutObject bucket "lsb-release" "/etc/lsb-release" fPutObject bucket "lsb-release" "/etc/lsb-release"

View File

@ -55,7 +55,7 @@ testParseLocation = do
, ,
-- 3. Test parsing of a valid, empty location xml. -- 3. Test parsing of a valid, empty location xml.
("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>", ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
"" "us-east-1"
) )
] ]