cql-io-2.0.0: src/lib/Database/CQL/IO/Settings.hs
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
module Database.CQL.IO.Settings where
import Control.Exception (IOException)
import Control.Lens (makeLenses, set, over)
import Control.Monad.Catch
import Control.Retry hiding (retryPolicy)
import Data.List.NonEmpty (NonEmpty (..), (<|))
import Data.Semigroup ((<>))
import Data.Time
import Database.CQL.Protocol
import Database.CQL.IO.Cluster.Policies (Policy, random)
import Database.CQL.IO.Connection.Socket (PortNumber)
import Database.CQL.IO.Connection.Settings as C
import Database.CQL.IO.Exception
import Database.CQL.IO.Log
import Database.CQL.IO.Pool as P
import OpenSSL.Session (SSLContext, SomeSSLException)
import qualified Data.HashMap.Strict as HashMap
data Settings = Settings
{ _poolSettings :: PoolSettings
, _connSettings :: ConnectionSettings
, _retrySettings :: RetrySettings
, _logger :: Logger
, _minProtoVer :: Version
, _maxProtoVer :: Version
, _portnumber :: PortNumber
, _contacts :: NonEmpty String
, _policyMaker :: IO Policy
, _prepStrategy :: PrepareStrategy
, _maxControlConnections :: Int
}
-- | Strategy for the execution of 'PrepQuery's.
data PrepareStrategy
= EagerPrepare -- ^ cluster-wide preparation
| LazyPrepare -- ^ on-demand per node preparation
deriving (Eq, Ord, Show)
-- | Retry settings control if and how retries are performed
-- by the client upon encountering errors during query execution.
--
-- There are three aspects to the retry settings:
--
-- 1. /What/ to retry. Determined by the retry handlers ('setRetryHandlers').
-- 2. /How/ to perform the retries. Determined by the retry policy
-- ('setRetryPolicy').
-- 3. Configuration adjustments to be performed before retrying. Determined by
-- 'adjustConsistency', 'adjustSendTimeout' and 'adjustResponseTimeout'.
-- These adjustments are performed /once/ before the first retry and are
-- scoped to the retries only.
--
-- Retry settings can be scoped to a client action by 'Database.CQL.IO.Client.retry',
-- thus locally overriding the \"global\" retry settings configured by
-- 'setRetrySettings'.
data RetrySettings = RetrySettings
{ _retryPolicy :: forall m. Monad m => RetryPolicyM m
, _reducedConsistency :: (Maybe Consistency)
, _sendTimeoutChange :: Milliseconds
, _recvTimeoutChange :: Milliseconds
, _retryHandlers :: forall m. Monad m => [RetryStatus -> Handler m Bool]
}
makeLenses ''RetrySettings
makeLenses ''Settings
-- | Default settings:
--
-- * The initial contact point is \"localhost\" on port 9042.
--
-- * The load-balancing policy is 'random'.
--
-- * The minimum binary protocol version is 3.
--
-- * The preferred binary protocol version is 4. The initial connection is made
-- using V3. If the server supports V4, that connection is closed and a new
-- one opened using V4
--
-- * The connection idle timeout is 60s.
--
-- * The connection pool uses 4 stripes to mitigate thread contention.
--
-- * Connections use a connect timeout of 5s, a send timeout of 3s and
-- a receive timeout of 10s.
--
-- * 128 streams per connection are used.
--
-- * 16k receive buffer size.
--
-- * No compression is applied to frame bodies.
--
-- * No default keyspace is used.
--
-- * A single, immediate retry is performed for errors that are always safe to
-- retry and are known to have good chances of succeeding on a retry.
-- See 'defRetrySettings'.
--
-- * Query preparation is done lazily. See 'PrepareStrategy'.
defSettings :: Settings
defSettings = Settings
{ _poolSettings = P.defSettings
, _connSettings = C.defSettings
, _retrySettings = defRetrySettings
, _logger = nullLogger
, _minProtoVer = V3
, _maxProtoVer = V4
, _portnumber = 9042
, _contacts = "localhost" :| []
, _policyMaker = random
, _prepStrategy = LazyPrepare
, _maxControlConnections = 5
}
-----------------------------------------------------------------------------
-- Settings
-- | Set the minimum binary protocol version to use.
setMinProtocolVersion :: Version -> Settings -> Settings
setMinProtocolVersion = set minProtoVer
-- | Set the maximum binary protocol version to use.
setMaxProtocolVersion :: Version -> Settings -> Settings
setMaxProtocolVersion = set maxProtoVer
-- | Set the initial contact points (hosts) from which node discovery will
-- start.
setContacts :: String -> [String] -> Settings -> Settings
setContacts v vv = set contacts (v :| vv)
-- | Add an additional host to the contact list.
addContact :: String -> Settings -> Settings
addContact v = over contacts (v <|)
-- | Set the portnumber to use to connect on /every/ node of the cluster.
setPortNumber :: PortNumber -> Settings -> Settings
setPortNumber v = set portnumber v
-- | Set the load-balancing policy.
setPolicy :: IO Policy -> Settings -> Settings
setPolicy v = set policyMaker v
-- | Set strategy to use for preparing statements.
setPrepareStrategy :: PrepareStrategy -> Settings -> Settings
setPrepareStrategy v = set prepStrategy v
-- | Set the 'Logger' to use for processing log messages emitted by the client.
setLogger :: Logger -> Settings -> Settings
setLogger v = set logger v
-- | Set the max number of control connections
setMaxControlConnections :: Int -> Settings -> Settings
setMaxControlConnections = set maxControlConnections
-----------------------------------------------------------------------------
-- Pool Settings
-- | Set the connection idle timeout. Connections in a pool will be closed
-- if not in use for longer than this timeout.
setIdleTimeout :: NominalDiffTime -> Settings -> Settings
setIdleTimeout v = set (poolSettings.idleTimeout) v
-- | Maximum connections per pool /stripe/.
setMaxConnections :: Int -> Settings -> Settings
setMaxConnections v = set (poolSettings.maxConnections) v
-- | Set the number of pool stripes to use. A good setting is equal to the
-- number of CPU cores this codes is running on.
setPoolStripes :: Int -> Settings -> Settings
setPoolStripes v s
| v < 1 = error "cql-io settings: stripes must be greater than 0"
| otherwise = set (poolSettings.poolStripes) v s
-- | When receiving a response times out, we can no longer use the stream of the
-- connection that was used to make the request as it is uncertain if
-- a response will arrive later. Thus the bandwith of a connection will be
-- decreased. This settings defines a threshold after which we close the
-- connection to get a new one with all streams available.
setMaxTimeouts :: Int -> Settings -> Settings
setMaxTimeouts v = set (poolSettings.maxTimeouts) v
-----------------------------------------------------------------------------
-- Connection Settings
-- | Set the compression to use for frame body compression.
setCompression :: Compression -> Settings -> Settings
setCompression v = set (connSettings.compression) v
-- | Set the maximum number of streams per connection. In version 2 of the
-- binary protocol at most 128 streams can be used. Version 3 supports up
-- to 32768 streams.
setMaxStreams :: Int -> Settings -> Settings
setMaxStreams v s
| v < 1 || v > 32768 = error "cql-io settings: max. streams must be within [1, 32768]"
| otherwise = set (connSettings.maxStreams) v s
-- | Set the connect timeout of a connection.
setConnectTimeout :: NominalDiffTime -> Settings -> Settings
setConnectTimeout v = set (connSettings.connectTimeout) (Ms $ round (1000 * v))
-- | Set the send timeout of a connection. Requests exceeding the send
-- timeout will cause the connection to be closed and fail with a
-- 'ConnectionClosed' exception.
setSendTimeout :: NominalDiffTime -> Settings -> Settings
setSendTimeout v = set (connSettings.sendTimeout) (Ms $ round (1000 * v))
-- | Set the response timeout of a connection. Requests exceeding the
-- response timeout will fail with a 'ResponseTimeout' exception.
setResponseTimeout :: NominalDiffTime -> Settings -> Settings
setResponseTimeout v = set (connSettings.responseTimeout) (Ms $ round (1000 * v))
-- | Set the default keyspace to use. Every new connection will be
-- initialised to use this keyspace.
setKeyspace :: Keyspace -> Settings -> Settings
setKeyspace v = set (connSettings.defKeyspace) (Just v)
-- | Set the retry settings to use.
setRetrySettings :: RetrySettings -> Settings -> Settings
setRetrySettings v = set retrySettings v
-- | Set maximum receive buffer size.
--
-- The actual buffer size used will be the minimum of the CQL response size
-- and the value set here.
setMaxRecvBuffer :: Int -> Settings -> Settings
setMaxRecvBuffer v = set (connSettings.maxRecvBuffer) v
-- | Set a fully configured SSL context.
--
-- This will make client server queries use TLS.
setSSLContext :: SSLContext -> Settings -> Settings
setSSLContext v = set (connSettings.tlsContext) (Just v)
-- | Set the supported authentication mechanisms.
--
-- When a Cassandra server requests authentication on a connection,
-- it specifies the requested 'AuthMechanism'. The client 'Authenticator'
-- is chosen based that name. If no authenticator with a matching
-- name is configured, an 'AuthenticationError' is thrown.
setAuthentication :: [C.Authenticator] -> Settings -> Settings
setAuthentication = set (connSettings.authenticators)
. HashMap.fromList
. map (\a -> (authMechanism a, a))
-----------------------------------------------------------------------------
-- Retry Settings
-- | Never retry.
noRetry :: RetrySettings
noRetry = RetrySettings
{ _retryPolicy = RetryPolicyM $ const (return Nothing)
, _reducedConsistency = Nothing
, _sendTimeoutChange = Ms 0
, _recvTimeoutChange = Ms 0
, _retryHandlers = []
}
-- | Default retry settings, combining 'defRetryHandlers' with 'defRetryPolicy'.
-- Consistency is never reduced on retries and timeout values remain unchanged.
defRetrySettings :: RetrySettings
defRetrySettings = RetrySettings
{ _retryPolicy = defRetryPolicy
, _reducedConsistency = Nothing
, _sendTimeoutChange = Ms 0
, _recvTimeoutChange = Ms 0
, _retryHandlers = defRetryHandlers
}
-- | Eager retry settings, combining 'eagerRetryHandlers' with
-- 'eagerRetryPolicy'. Consistency is never reduced on retries and timeout
-- values remain unchanged.
eagerRetrySettings :: RetrySettings
eagerRetrySettings = RetrySettings
{ _retryPolicy = eagerRetryPolicy
, _reducedConsistency = Nothing
, _sendTimeoutChange = Ms 0
, _recvTimeoutChange = Ms 0
, _retryHandlers = eagerRetryHandlers
}
-- | The default retry policy permits a single, immediate retry.
defRetryPolicy :: RetryPolicy
defRetryPolicy = limitRetries 1
-- | The eager retry policy permits 5 retries with exponential
-- backoff (base-2) with an initial delay of 100ms, i.e. the
-- retries will be performed with 100ms, 200ms, 400ms, 800ms
-- and 1.6s delay, respectively, for a maximum delay of ~3s.
eagerRetryPolicy :: RetryPolicy
eagerRetryPolicy = limitRetries 5 <> exponentialBackoff 100000
-- | The default retry handlers permit a retry for the following errors:
--
-- * A 'HostError', since it always occurs before a query has been
-- sent to the server.
--
-- * A 'ConnectionError' that is a 'ConnectTimeout', since it always
-- occurs before a query has been sent to the server.
--
-- * A 'ResponseError' that is one of the following:
--
-- * 'Unavailable', since that is an error response from a coordinator
-- before the query is actually executed.
-- * A 'ReadTimeout' that indicates that the required consistency
-- level could be achieved but the data was unfortunately chosen
-- by the coordinator to be returned from a replica that turned
-- out to be unavailable. A retry has a good chance of getting the data
-- from one of the other replicas.
-- * A 'WriteTimeout' for a write to the batch log failed. The batch log
-- is written prior to execution of the statements of the batch and
-- hence these errors are safe to retry.
--
defRetryHandlers :: Monad m => [RetryStatus -> Handler m Bool]
defRetryHandlers =
[ const $ Handler $ \(e :: ConnectionError) -> case e of
ConnectTimeout {} -> return True
_ -> return False
, const $ Handler $ \(e :: ResponseError) -> return $ case reCause e of
Unavailable {} -> True
ReadTimeout {..} -> rTimeoutNumAck >= rTimeoutNumRequired &&
not rTimeoutDataPresent
WriteTimeout {..} -> wTimeoutWriteType == WriteBatchLog
_ -> False
, const $ Handler $ \(_ :: HostError) -> return True
, const $ Handler $ \(_ :: SomeSSLException) -> return True
]
-- | The eager retry handlers permit a superset of the errors
-- of 'defRetryHandlers', namely:
--
-- * Any 'ResponseError' that is a 'ReadTimeout', 'WriteTimeout',
-- 'Overloaded', 'Unavailable' or 'ServerError'.
--
-- * Any 'ConnectionError'.
--
-- * Any 'IOException'.
--
-- * Any 'HostError'.
--
-- * Any 'SomeSSLException' (if an SSL context is configured).
--
-- Notably, these retry handlers are only safe to use for idempotent
-- queries, or if a duplicate write has no severe consequences in
-- the context of the application's data model.
eagerRetryHandlers :: Monad m => [RetryStatus -> Handler m Bool]
eagerRetryHandlers =
[ const $ Handler $ \(e :: ResponseError) -> case reCause e of
ReadTimeout {} -> return True
WriteTimeout {} -> return True
Overloaded {} -> return True
Unavailable {} -> return True
ServerError {} -> return True
_ -> return False
, const $ Handler $ \(_ :: ConnectionError) -> return True
, const $ Handler $ \(_ :: IOException) -> return True
, const $ Handler $ \(_ :: HostError) -> return True
, const $ Handler $ \(_ :: SomeSSLException) -> return True
]
-- | Set the 'RetryPolicy' to apply on retryable exceptions,
-- which determines the number and distribution of retries over time,
-- i.e. /how/ retries are performed. Configuring a retry policy
-- does not specify /what/ errors should actually be retried.
-- See 'setRetryHandlers'.
setRetryPolicy :: RetryPolicy -> RetrySettings -> RetrySettings
setRetryPolicy v s = s { _retryPolicy = v }
-- | Set the exception handlers that decide whether a request can be
-- retried by the client, i.e. /what/ errors are permissible to retry.
-- For configuring /how/ the retries are performed, see 'setRetryPolicy'.
setRetryHandlers :: (forall m. Monad m => [RetryStatus -> Handler m Bool])
-> RetrySettings -> RetrySettings
setRetryHandlers v s = s { _retryHandlers = v }
-- | On retry, change the consistency to the given value.
adjustConsistency :: Consistency -> RetrySettings -> RetrySettings
adjustConsistency v = set reducedConsistency (Just v)
-- | On retry adjust the send timeout. See 'setSendTimeout'.
adjustSendTimeout :: NominalDiffTime -> RetrySettings -> RetrySettings
adjustSendTimeout v = set sendTimeoutChange (Ms $ round (1000 * v))
-- | On retry adjust the response timeout. See 'setResponseTimeout'.
adjustResponseTimeout :: NominalDiffTime -> RetrySettings -> RetrySettings
adjustResponseTimeout v = set recvTimeoutChange (Ms $ round (1000 * v))