diff --git a/changelog.d/0-release-notes/WPB-25325 b/changelog.d/0-release-notes/WPB-25325 new file mode 100644 index 00000000000..e9526beeb71 --- /dev/null +++ b/changelog.d/0-release-notes/WPB-25325 @@ -0,0 +1,3 @@ +The PostgreSQL connection pool implementation was switched to `hasql-resource-pool`. +The `agingTimeout` setting is now ignored and should be treated as deprecated. +Pool metrics now include acquisition/session latency. diff --git a/charts/elasticsearch-index/templates/migrate-data.yaml b/charts/elasticsearch-index/templates/migrate-data.yaml index ec23f91f8ca..bb59d83ff6d 100644 --- a/charts/elasticsearch-index/templates/migrate-data.yaml +++ b/charts/elasticsearch-index/templates/migrate-data.yaml @@ -61,8 +61,6 @@ spec: - {{ .Values.postgresqlPool.size | quote }} - --pg-pool-acquisition-timeout - {{ .Values.postgresqlPool.acquisitionTimeout | quote }} - - --pg-pool-aging-timeout - - {{ .Values.postgresqlPool.agingTimeout | quote }} - --pg-pool-idleness-timeout - {{ .Values.postgresqlPool.idlenessTimeout | quote }} {{- if hasKey $.Values.secrets "pgPassword" }} diff --git a/charts/elasticsearch-index/values.yaml b/charts/elasticsearch-index/values.yaml index 9aa145388fe..b12673ed181 100644 --- a/charts/elasticsearch-index/values.yaml +++ b/charts/elasticsearch-index/values.yaml @@ -46,7 +46,6 @@ postgresql: postgresqlPool: size: 100 acquisitionTimeout: 10s - agingTimeout: 1d idlenessTimeout: 10m postgresMigration: diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index 48a45ffd5aa..a6679954cc1 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -71,7 +71,6 @@ galley: postgresqlPool: size: 100 acquisitionTimeout: 10s - agingTimeout: 1d idlenessTimeout: 10m # Not used if enableFederation is false @@ -968,7 +967,6 @@ background-worker: postgresqlPool: size: 5 acquisitionTimeout: 10s - agingTimeout: 1d idlenessTimeout: 10m # Setting this to `true` will start conversation migration to postgresql. @@ -1132,7 +1130,6 @@ brig: postgresqlPool: size: 100 acquisitionTimeout: 10s - agingTimeout: 1d idlenessTimeout: 10m emailSMS: diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index ab4bbf32689..b1c6ffd490e 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -1939,15 +1939,6 @@ config: # Connection acquisition timeout. acquisitionTimeout: 10s - # Maximal connection lifetime. - # - # Determines how long is available for reuse. After the timeout passes and - # an active session is finished the connection will be closed releasing a - # slot in the pool for a fresh connection to be established. - # - # This is useful as a healthy measure for resetting the server-side caches. - agingTimeout: 1d - # Maximal connection idle time. idlenessTimeout: 10m secrets: @@ -1965,7 +1956,6 @@ postgresql: postgresqlPool: size: 100 acquisitionTimeout: 10s - agingTimeout: 1d idlenessTimeout: 10m postgresqlPassword: /path/to/pgPassword # refers to a PostgreSQL password file ``` @@ -2194,7 +2184,6 @@ postgresql: postgresqlPool: size: 5 acquisitionTimeout: 10s - agingTimeout: 1d idlenessTimeout: 10m # Start migration workers when true diff --git a/flake.lock b/flake.lock index ae1e0b1c673..f4c3b235f89 100644 --- a/flake.lock +++ b/flake.lock @@ -215,6 +215,23 @@ "type": "github" } }, + "hasql-resource-pool": { + "flake": false, + "locked": { + "lastModified": 1783610992, + "narHash": "sha256-xpnT7V2EddKVzK5cvaPeRYpCyRbWyqkQJrsBMpSbWXg=", + "owner": "wireapp", + "repo": "hasql-resource-pool", + "rev": "5b5d3df0fff81801986a0110acae5420215f01c5", + "type": "github" + }, + "original": { + "owner": "wireapp", + "repo": "hasql-resource-pool", + "rev": "5b5d3df0fff81801986a0110acae5420215f01c5", + "type": "github" + } + }, "hsaml2": { "flake": false, "locked": { @@ -356,6 +373,7 @@ "cryptostore": "cryptostore", "flake-utils": "flake-utils", "hasql-migration": "hasql-migration", + "hasql-resource-pool": "hasql-resource-pool", "hsaml2": "hsaml2", "hspec-wai": "hspec-wai", "http-client": "http-client", diff --git a/flake.nix b/flake.nix index bbc3c8826f9..5206ca874e8 100644 --- a/flake.nix +++ b/flake.nix @@ -101,6 +101,13 @@ url = "github:wireapp/hsaml2/use-crypton-asn1"; flake = false; }; + + hasql-resource-pool = { + # Update this to the upstream repo/rev once the PR is merged there. + # https://github.com/avanov/hasql-resource-pool/pull/6 + url = "github:wireapp/hasql-resource-pool?rev=5b5d3df0fff81801986a0110acae5420215f01c5"; + flake = false; + }; }; outputs = inputs@{ nixpkgs, nixpkgs_24_11, flake-utils, tom-bombadil, sbomnix, ... }: diff --git a/libs/extended/default.nix b/libs/extended/default.nix index 4dd312bbaa6..1813326fc12 100644 --- a/libs/extended/default.nix +++ b/libs/extended/default.nix @@ -19,7 +19,7 @@ , errors , exceptions , hasql -, hasql-pool +, hasql-resource-pool , hspec , hspec-discover , http-client @@ -49,7 +49,6 @@ , transformers , types-common , unliftio -, uuid , wai }: mkDerivation { @@ -72,7 +71,7 @@ mkDerivation { errors exceptions hasql - hasql-pool + hasql-resource-pool http-client http-client-tls http-types @@ -96,7 +95,6 @@ mkDerivation { transformers types-common unliftio - uuid wai ]; testHaskellDepends = [ diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index 0174d4b10cc..eee2d884228 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -103,7 +103,7 @@ library , errors , exceptions , hasql - , hasql-pool + , hasql-resource-pool , http-client , http-client-tls , http-types @@ -127,7 +127,6 @@ library , transformers , types-common , unliftio - , uuid , wai default-language: GHC2021 diff --git a/libs/extended/src/Hasql/Pool/Extended.hs b/libs/extended/src/Hasql/Pool/Extended.hs index cf6e6d0f4f1..0cd8e4b61c9 100644 --- a/libs/extended/src/Hasql/Pool/Extended.hs +++ b/libs/extended/src/Hasql/Pool/Extended.hs @@ -20,21 +20,22 @@ module Hasql.Pool.Extended where import Data.Aeson import Data.Map as Map import Data.Misc -import Data.Set qualified as Set -import Data.UUID +import Hasql.Connection qualified import Hasql.Connection.Settings qualified as HasqlConnSettings -import Hasql.Pool as HasqlPool -import Hasql.Pool.Config qualified as HasqlPool -import Hasql.Pool.Observation +import Hasql.Pool qualified as HasqlPool import Imports import PostgresqlConnectionString qualified import Prometheus +import UnliftIO.IO (getMonotonicTime) import Util.Options data PoolConfig = PoolConfig { size :: Int, + -- | Configured pool acquisition wait time. hasql-resource-pool only + -- accepts whole seconds here, so we round up to the nearest second and + -- pass it through as the pool acquisition timeout. acquisitionTimeout :: Duration, - agingTimeout :: Duration, + -- | Controls how long idle connections stay resident in the pool. idlenessTimeout :: Duration } deriving (Eq, Show) @@ -44,100 +45,127 @@ instance FromJSON PoolConfig where PoolConfig <$> o .: "size" <*> o .: "acquisitionTimeout" - <*> o .: "agingTimeout" <*> o .: "idlenessTimeout" --- | Creates a pool from postgres config params --- --- HasqlPool.staticConnectionSettings translates the connection settings to the pool settings --- HasqlPool.settings translates the pool settings into pool config --- HasqlPool.acquire creates the pool. --- ezpz. -initPostgresPool :: PoolConfig -> Map Text Text -> Maybe FilePathSecrets -> IO HasqlPool.Pool -initPostgresPool config pgConfig mFpSecrets = do - mPw <- for mFpSecrets initCredentials - let pgSettings = - HasqlConnSettings.connectionString (PostgresqlConnectionString.toUrl $ PostgresqlConnectionString.fromKeyValueParams pgConfig) - <> foldMap HasqlConnSettings.password mPw - metrics <- initHasqlPoolMetrics - connsRef <- newIORef $ Connections mempty mempty mempty - HasqlPool.acquire $ - HasqlPool.settings - [ HasqlPool.staticConnectionSettings pgSettings, - HasqlPool.size config.size, - HasqlPool.acquisitionTimeout config.acquisitionTimeout.duration, - HasqlPool.agingTimeout config.agingTimeout.duration, - HasqlPool.idlenessTimeout config.idlenessTimeout.duration, - HasqlPool.observationHandler (observationHandler connsRef metrics) - ] - data HasqlPoolMetrics = HasqlPoolMetrics { readyForUseGauge :: Gauge, inUseGauge :: Gauge, establishedCounter :: Counter, - terminationCounter :: Counter, + connectionFailureCounter :: Counter, + acquisitionTimeoutCounter :: Counter, sessionFailureCounter :: Counter, - sessionCounter :: Counter + sessionCounter :: Counter, + connectionAcquisitionDuration :: Histogram, + sessionDuration :: Histogram } -initHasqlPoolMetrics :: IO HasqlPoolMetrics -initHasqlPoolMetrics = do - HasqlPoolMetrics - <$> register (gauge $ Info "wire_hasql_pool_ready_for_use" "Number of hasql pool connections ready for use") - <*> register (gauge $ Info "wire_hasql_pool_in_use" "Number of hasql pool connections ready for use") - <*> register (counter $ Info "wire_hasql_pool_connection_established_count" "Number of established connections") - <*> register (counter $ Info "wire_hasql_pool_connection_terminated_count" "Number of terminated connections") - <*> register (counter $ Info "wire_hasql_pool_session_failure_count" "Number of times a session has failed") - <*> register (counter $ Info "wire_hasql_pool_session_count" "Number of times a session was created") - -data Connections = Connections - { connecting :: Set UUID, - inUse :: Set UUID, - readyForUse :: Set UUID +data Pool = Pool + { rawPool :: HasqlPool.Pool, + metrics :: HasqlPoolMetrics, + -- | Pool acquisition timeout in seconds, rounded up from the configured + -- duration. This is used by the session runner to bound waiting for an + -- available connection slot. + poolAcquisitionTimeout :: Duration } -observationHandler :: IORef Connections -> HasqlPoolMetrics -> Observation -> IO () -observationHandler connsRef metrics (ConnectionObservation connId status) = do - case status of - ConnectingConnectionStatus -> do - modifyIORef' connsRef (\conns -> conns {connecting = Set.insert connId conns.connecting}) - ReadyForUseConnectionStatus reason -> do - connsChange <- case reason of - SessionFailedConnectionReadyForUseReason _ -> do - incCounter metrics.sessionFailureCounter - pure $ \conns -> conns {inUse = Set.delete connId conns.inUse} - SessionSucceededConnectionReadyForUseReason -> do - pure $ \conns -> conns {inUse = Set.delete connId conns.inUse} - EstablishedConnectionReadyForUseReason -> do - incCounter metrics.establishedCounter - pure (\conns -> conns {connecting = Set.delete connId conns.connecting}) - - (inUseSize, readyForUseSize) <- atomicModifyIORef' connsRef $ \conns -> - let newConns = (connsChange conns) {readyForUse = Set.insert connId conns.readyForUse} - in (newConns, (Set.size newConns.inUse, Set.size newConns.readyForUse)) - - setGauge metrics.readyForUseGauge (fromIntegral readyForUseSize) - setGauge metrics.inUseGauge (fromIntegral inUseSize) - InUseConnectionStatus -> do - incCounter metrics.sessionCounter - (inUseSize, readyForUseSize) <- atomicModifyIORef' connsRef $ \conns -> - let newConns = - conns - { readyForUse = Set.delete connId conns.readyForUse, - inUse = Set.insert connId conns.inUse - } - in (newConns, (Set.size newConns.inUse, Set.size newConns.readyForUse)) - setGauge metrics.readyForUseGauge (fromIntegral readyForUseSize) - setGauge metrics.inUseGauge (fromIntegral inUseSize) - TerminatedConnectionStatus _ -> do - (inUseSize, readyForUseSize) <- atomicModifyIORef' connsRef $ \conns -> - let newConns = - conns - { connecting = Set.delete connId conns.connecting, - readyForUse = Set.delete connId conns.readyForUse, - inUse = Set.delete connId conns.inUse - } - in (newConns, (Set.size newConns.inUse, Set.size newConns.readyForUse)) - incCounter metrics.terminationCounter - setGauge metrics.readyForUseGauge (fromIntegral readyForUseSize) - setGauge metrics.inUseGauge (fromIntegral inUseSize) +recordHasqlPoolConnectionAcquisition :: HasqlPoolMetrics -> Double -> IO () +recordHasqlPoolConnectionAcquisition metrics secs = + observe metrics.connectionAcquisitionDuration secs + +recordHasqlPoolConnectionEstablished :: HasqlPoolMetrics -> IO () +recordHasqlPoolConnectionEstablished metrics = + void $ addCounter metrics.establishedCounter 1 + +recordHasqlPoolConnectionFailure :: HasqlPoolMetrics -> IO () +recordHasqlPoolConnectionFailure metrics = + void $ addCounter metrics.connectionFailureCounter 1 + +recordHasqlPoolSessionStarted :: HasqlPoolMetrics -> IO () +recordHasqlPoolSessionStarted metrics = + void $ addCounter metrics.sessionCounter 1 + +recordHasqlPoolSessionFailure :: HasqlPoolMetrics -> IO () +recordHasqlPoolSessionFailure metrics = + void $ addCounter metrics.sessionFailureCounter 1 + +recordHasqlPoolSessionDuration :: HasqlPoolMetrics -> Double -> IO () +recordHasqlPoolSessionDuration metrics secs = + observe metrics.sessionDuration secs + +recordHasqlPoolAcquisitionTimeout :: HasqlPoolMetrics -> IO () +recordHasqlPoolAcquisitionTimeout metrics = + void $ addCounter metrics.acquisitionTimeoutCounter 1 + +recordHasqlPoolStats :: Pool -> IO () +recordHasqlPoolStats pool = do + -- hasql-resource-pool does not expose per-acquire/release callbacks, so + -- these gauges are refreshed from the pool's current total connections stats instead. + poolStats <- HasqlPool.stats pool.rawPool + setGauge pool.metrics.readyForUseGauge (fromIntegral poolStats.available) + setGauge pool.metrics.inUseGauge (fromIntegral poolStats.currentUsage) + +startHasqlPoolStatsReporter :: Pool -> IO () +startHasqlPoolStatsReporter pool = void $ forkIO $ forever $ do + recordHasqlPoolStats pool + threadDelay (5 * 1_000_000) -- 5s + +-- | Creates a pool from postgres config params. +-- +-- 'acquisitionTimeout' is mapped to the pool acquisition timeout, +-- 'idlenessTimeout' controls how long idle connections stay resident. +initPostgresPool :: PoolConfig -> Map Text Text -> Maybe FilePathSecrets -> IO Pool +initPostgresPool config pgConfig mFpSecrets = do + mPw <- for mFpSecrets initCredentials + let pgSettings = + HasqlConnSettings.connectionString (PostgresqlConnectionString.toUrl $ PostgresqlConnectionString.fromKeyValueParams pgConfig) + <> foldMap HasqlConnSettings.password mPw + metrics <- mkHasqlPoolMetrics + rawPool <- + HasqlPool.acquireWith + (instrumentedConnectionGetter metrics (Hasql.Connection.acquire pgSettings)) + ( config.size, + realToFrac config.idlenessTimeout.duration, + unusedSettings + ) + let pool = Pool {rawPool, metrics, poolAcquisitionTimeout = config.acquisitionTimeout} + startHasqlPoolStatsReporter pool + pure pool + where + instrumentedConnectionGetter metrics getter = do + started <- getMonotonicTime + res <- getter + ended <- getMonotonicTime + recordHasqlPoolConnectionAcquisition metrics (ended - started) + case res of + Right _ -> recordHasqlPoolConnectionEstablished metrics + Left _ -> recordHasqlPoolConnectionFailure metrics + pure res + + mkHasqlPoolMetrics :: IO HasqlPoolMetrics + mkHasqlPoolMetrics = + HasqlPoolMetrics + <$> register (gauge $ Info "wire_hasql_pool_ready_for_use" "Number of hasql pool connections ready for use") + <*> register (gauge $ Info "wire_hasql_pool_in_use" "Number of hasql pool connections in use") + <*> register (counter $ Info "wire_hasql_pool_connection_established_count" "Number of established connections") + <*> register (counter $ Info "wire_hasql_pool_connection_failure_count" "Number of failed connection acquisition attempts") + <*> register (counter $ Info "wire_hasql_pool_acquisition_timeout_count" "Number of pool acquisition timeouts") + <*> register (counter $ Info "wire_hasql_pool_session_failure_count" "Number of times a session has failed") + <*> register (counter $ Info "wire_hasql_pool_session_count" "Number of times a session was created") + <*> register (histogram (Info "wire_hasql_pool_connection_acquisition_seconds" "Time spent establishing new PostgreSQL connections") defaultBuckets) + <*> register (histogram (Info "wire_hasql_pool_session_seconds" "Time spent using PostgreSQL sessions") defaultBuckets) + + unusedSettings = + -- The custom getter above performs the actual connection establishment. + -- The API forces us to pass this record, but it is actually not used in acquireWith + HasqlPool.ConnectionSettings + { host = "", + port = 5432, + user = "", + password = "", + dbName = "", + connAcqTimeout = 0, + txIdleTimeout = HasqlPool.TimeoutSetting 0 HasqlPool.Seconds, + stmtTimeout = HasqlPool.TimeoutSetting 0 HasqlPool.Seconds, + sslMode = "prefer", + sslRootCert = "" + } diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index 8f0129a62a9..63f949f4b2d 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -50,6 +50,7 @@ module Data.Misc mkDurationLiteral, unsafeDurationLiteral, durationToMicros, + durationToCeilingSeconds, -- * HttpsUrl HttpsUrl (..), @@ -330,6 +331,17 @@ durationToMicros :: Duration -> Int durationToMicros = fromInteger . flip div 1_000_000 . diffTimeToPicoseconds . duration +-- | Convert a 'Duration' to whole seconds, rounding up and clamping a bounded integral type. +-- Non-positive durations map to zero. +durationToCeilingSeconds :: forall a. (Bounded a, Integral a) => Duration -> a +durationToCeilingSeconds d + | duration d <= 0 = 0 + | otherwise = + fromInteger $ + min + (toInteger (maxBound :: a)) + (ceiling (duration d) :: Integer) + instance FromJSON Duration where parseJSON = withText "Duration" $ either fail pure . parseDuration diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index 50b26ec421f..cbda44f339a 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -53,7 +53,7 @@ , HaskellNet-SSL , hasql , hasql-migration -, hasql-pool +, hasql-resource-pool , hasql-th , hasql-transaction , hex @@ -192,7 +192,7 @@ mkDerivation { HaskellNet-SSL hasql hasql-migration - hasql-pool + hasql-resource-pool hasql-th hasql-transaction hex @@ -322,7 +322,7 @@ mkDerivation { HaskellNet-SSL hasql hasql-migration - hasql-pool + hasql-resource-pool hasql-th hasql-transaction hex diff --git a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs index 2f46d3ee28d..a7a37fdee80 100644 --- a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs @@ -24,22 +24,16 @@ where import Data.Id import Data.Range -import Hasql.Pool import Hasql.TH import Imports import Polysemy -import Polysemy.Error (Error) -import Polysemy.Input import Wire.API.PostgresMarshall import Wire.API.User qualified as User import Wire.AppStore import Wire.Postgres interpretAppStoreToPostgres :: - ( Member (Embed IO) r, - Member (Input Pool) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => InterpreterFor AppStore r interpretAppStoreToPostgres = interpret $ \case @@ -50,10 +44,7 @@ interpretAppStoreToPostgres = DeleteApp userId teamId -> deleteAppImpl userId teamId createAppImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => StoredApp -> Sem r () createAppImpl app = @@ -64,10 +55,7 @@ createAppImpl app = values ($1 :: uuid, $2 :: uuid, $3 :: json, $4 :: text, $5 :: text, $6 :: uuid) |] getAppImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => UserId -> TeamId -> Sem r (Maybe StoredApp) @@ -85,10 +73,7 @@ eraseMetadata :: StoredApp -> StoredApp eraseMetadata sap = sap {meta = mempty} getAppsImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => TeamId -> Sem r [StoredApp] getAppsImpl tid = @@ -99,10 +84,7 @@ getAppsImpl tid = from apps where team_id = ($1 :: uuid) |] updateAppImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => TeamId -> UserId -> StoredAppUpdate -> @@ -137,10 +119,7 @@ updateAppImpl (toUUID -> teamId) (toUUID -> appId) upd = do pure $ maybe (Left NotFound) (\_ -> Right ()) found deleteAppImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => UserId -> TeamId -> Sem r () diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs b/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs index 14decb92f63..93e22e9daa5 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs @@ -27,7 +27,7 @@ import Data.Id (ConvId) import Data.Misc (HttpsUrl) import Data.Text qualified as T import Data.Time -import Hasql.Pool qualified as Hasql +import Hasql.Pool.Extended qualified as Hasql import Imports import Polysemy import Polysemy.Async diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs index cdd72d0097c..8c5ddc11bd7 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs @@ -34,7 +34,8 @@ import Data.Time import Data.Time.Calendar.OrdinalDate (fromOrdinalDate) import Data.Vector (Vector) import Data.Vector qualified as Vector -import Hasql.Pool qualified as Hasql +import Hasql.Pool (UsageError) +import Hasql.Pool.Extended qualified as Hasql import Hasql.Statement qualified as Hasql import Hasql.TH import Hasql.Transaction qualified as Transaction @@ -196,7 +197,7 @@ migrateAllUsers migOpts migCounter migDuration = do select :: PrepQuery R () (Identity UserId) select = "select distinct user from user_remote_conv" -handleErrors :: (Member (State Int) r, Member TinyLog r) => (Id a -> Sem (Error MigrationLockError : Error Hasql.UsageError : r) b) -> ByteString -> Id a -> Sem r (Maybe b) +handleErrors :: (Member (State Int) r, Member TinyLog r) => (Id a -> Sem (Error MigrationLockError : Error UsageError : r) b) -> ByteString -> Id a -> Sem r (Maybe b) handleErrors action lockType id_ = join <$> handleError (handleError action lockType) lockType id_ diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs index 067cb970071..9dee5441e8e 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs @@ -34,7 +34,6 @@ import Data.Vector qualified as Vector import GHC.Records (HasField) import Hasql.Decoders qualified as HD import Hasql.Pipeline qualified as Pipeline -import Hasql.Pool qualified as Hasql import Hasql.Session qualified as HasqlSession import Hasql.Statement qualified as Hasql import Hasql.TH @@ -43,8 +42,6 @@ import Hasql.Transaction qualified as Transaction import Hasql.Transaction.Sessions (IsolationLevel (ReadCommitted), Mode (..)) import Imports import Polysemy -import Polysemy.Error -import Polysemy.Input import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.CellsState import Wire.API.Conversation.Pagination @@ -1317,10 +1314,7 @@ rawResultToSearchResult r = do } searchConversationsImpl :: - ( Member (Input Hasql.Pool) r, - Member (Error Hasql.UsageError) r, - Member (Embed IO) r - ) => + (PGConstraints r) => ConversationSearch -> Sem r [ConversationSearchResult] searchConversationsImpl req = diff --git a/libs/wire-subsystems/src/Wire/DomainRegistrationStore/Migration.hs b/libs/wire-subsystems/src/Wire/DomainRegistrationStore/Migration.hs index 26a7d5de941..5047d4b61e2 100644 --- a/libs/wire-subsystems/src/Wire/DomainRegistrationStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/DomainRegistrationStore/Migration.hs @@ -27,7 +27,8 @@ import Data.Conduit.List qualified as C import Data.Domain import Data.Id import Database.CQL.Protocol (Record (asRecord), TupleType) -import Hasql.Pool qualified as Hasql +import Hasql.Pool (UsageError) +import Hasql.Pool.Extended qualified as Hasql import Imports hiding (lookup) import Polysemy import Polysemy.Async @@ -180,7 +181,7 @@ handleRegistrationErrors :: Member TinyLog r ) => ByteString -> - (Sem (Error MigrationLockError : Error Hasql.UsageError : r) ()) -> + (Sem (Error MigrationLockError : Error UsageError : r) ()) -> Sem r () handleRegistrationErrors key action = do eithErr <- runError (runError action) diff --git a/libs/wire-subsystems/src/Wire/Error.hs b/libs/wire-subsystems/src/Wire/Error.hs index b526bb6c9da..cab57eb1887 100644 --- a/libs/wire-subsystems/src/Wire/Error.hs +++ b/libs/wire-subsystems/src/Wire/Error.hs @@ -35,7 +35,6 @@ import Network.HTTP.Types import Network.Wai import Network.Wai.Utilities import Network.Wai.Utilities.Error qualified as Wai -import Network.Wai.Utilities.Exception import Network.Wai.Utilities.JSONResponse import Network.Wai.Utilities.Server import Servant (ServerError (..)) @@ -82,14 +81,14 @@ httpErrorToJSONResponse e@(RichError werr _ headers) = postgresUsageErrorToHttpError :: UsageError -> HttpError postgresUsageErrorToHttpError err = case err of - SessionUsageError _se -> + SessionError _se -> -- FUTUREWORK: should this case should be more nuanced? eg., if a foreign key is dangling, should we -- return "404 not found", not "database crashed"? -- The problem is that the SessionError is not typed to easily be parsed -- To prevent foreign key errors we should check the foreign key constraints before inserting - StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> displayExceptionNoBacktrace err)) - ConnectionUsageError _ -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> displayExceptionNoBacktrace err)) - AcquisitionTimeoutUsageError -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> displayExceptionNoBacktrace err)) + StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err)) + ConnectionError _ -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err)) + AcquisitionTimeoutUsageError -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err)) -- | Extract the wai error from an HttpError and convert into a -- servant error. `RichError` extra data is discarded! diff --git a/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs b/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs index 30bfd2fb298..a4bb9ddec74 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs @@ -30,14 +30,11 @@ import Data.Range (Range, fromRange) import Data.Time.Clock import Data.UUID (UUID, nil) import Data.Vector qualified as V -import Hasql.Pool import Hasql.Session import Hasql.Statement import Hasql.TH import Imports import Polysemy -import Polysemy.Error (Error, throw) -import Polysemy.Input import Wire.API.Meeting (Recurrence) import Wire.API.PostgresMarshall (PostgresMarshall (..), PostgresUnmarshall (..), dimapPG) import Wire.API.User.Identity (EmailAddress, fromEmail) @@ -245,10 +242,7 @@ updateMeetingImpl meetingId mTitle mStartDate mEndDate mRecurrence = do -- * Delete deleteMeetingImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => MeetingId -> Sem r () deleteMeetingImpl meetingId = do @@ -403,17 +397,12 @@ replaceInvitedEmailsImpl meetingId emails = do |] getOldMeetingsImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => UTCTime -> Int -> Sem r [StoredMeeting] getOldMeetingsImpl cutoffTime batchSize = do - pool <- input - result <- liftIO $ use pool session - either throw pure result + runSession session where session :: Session [StoredMeeting] session = statement (cutoffTime, fromIntegral batchSize) $ V.toList <$> listStatement diff --git a/libs/wire-subsystems/src/Wire/MigrationLock.hs b/libs/wire-subsystems/src/Wire/MigrationLock.hs index 0c9eedf1260..3fd75598280 100644 --- a/libs/wire-subsystems/src/Wire/MigrationLock.hs +++ b/libs/wire-subsystems/src/Wire/MigrationLock.hs @@ -25,6 +25,7 @@ import Data.Id import Data.UUID qualified as UUID import Data.Vector (Vector) import Hasql.Pool qualified as Hasql +import Hasql.Pool.Extended qualified as HasqlPoolExt import Hasql.Session qualified as Session import Hasql.Statement qualified as Hasql import Hasql.TH @@ -97,7 +98,7 @@ withMigrationLocks lockType maxWait lockables action = do lockAcquired <- embed newEmptyMVar actionCompleted <- embed newEmptyMVar - pool <- input + pool <- (.rawPool) <$> input @HasqlPoolExt.Pool lockThread <- async . embed . Hasql.use pool $ do let lockIds = fmap lockKey lockables Session.statement lockIds acquireLocks diff --git a/libs/wire-subsystems/src/Wire/Postgres.hs b/libs/wire-subsystems/src/Wire/Postgres.hs index d18de5f10a8..f91f3637efe 100644 --- a/libs/wire-subsystems/src/Wire/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/Postgres.hs @@ -70,15 +70,17 @@ where import Control.Monad.Trans.State import Data.Functor.Contravariant import Data.Id +import Data.Misc import Data.Text qualified as Text import Data.Text.Encoding qualified as Text -import Data.Time.Clock +import Data.Time.Clock (UTCTime) import Hasql.Decoders qualified as Dec import Hasql.Encoders qualified as Enc import Hasql.Errors import Hasql.Pipeline (Pipeline) -import Hasql.Pool import Hasql.Pool qualified as Hasql +import Hasql.Pool.Extended qualified as HasqlPoolExt +import Hasql.Pool.Observer qualified as HasqlObserver import Hasql.Session import Hasql.Statement import Hasql.Transaction (Transaction) @@ -92,7 +94,7 @@ import PostgreSQL.ErrorCodes qualified as PostgreSQL import Wire.API.Pagination type PGConstraints r = - ( Member (Input Hasql.Pool) r, + ( Member (Input HasqlPoolExt.Pool) r, Member (Embed IO) r, Member (Error Hasql.UsageError) r ) @@ -100,8 +102,10 @@ type PGConstraints r = -- | Resets the pool if it detects server errors due to admin intervention. -- Things like server restart. Then retries the session. -- --- Inspired by https://github.com/nikita-volkov/hasql-pool/issues/27 -useWithResetAndRetry :: forall a. Pool -> Session a -> IO (Either UsageError a) +-- Inspired by https://github.com/nikita-volkov/hasql-pool/issues/27. +-- The old issue still describes the server-error retry pattern, even though +-- this module now uses hasql-resource-pool. +useWithResetAndRetry :: forall a. HasqlPoolExt.Pool -> Session a -> IO (Either Hasql.UsageError a) useWithResetAndRetry pool sess = go maxRetries where maxRetries :: Int @@ -110,19 +114,35 @@ useWithResetAndRetry pool sess = go maxRetries resettableErrors :: [ByteString] resettableErrors = [PostgreSQL.admin_shutdown, PostgreSQL.crash_shutdown, PostgreSQL.cannot_connect_now, PostgreSQL.database_dropped] - go :: Int -> IO (Either UsageError a) - go 0 = use pool sess + go :: Int -> IO (Either Hasql.UsageError a) + go 0 = useObservedNoRetry pool sess go n = do - eithRes <- use pool sess + eithRes <- useObservedNoRetry pool sess case eithRes of - Left (SessionUsageError (StatementSessionError _ _ _ _ _ (ServerStatementError (ServerError errCode _ _ _ _)))) -> do + Left (Hasql.SessionError (StatementSessionError _ _ _ _ _ (ServerStatementError (ServerError errCode _ _ _ _)))) -> do if (Text.encodeUtf8 errCode `elem` resettableErrors) then do - release pool + Hasql.release pool.rawPool go (n - 1) else pure eithRes _ -> pure eithRes + useObservedNoRetry :: HasqlPoolExt.Pool -> Session a -> IO (Either Hasql.UsageError a) + useObservedNoRetry p s = do + HasqlPoolExt.recordHasqlPoolSessionStarted p.metrics + result <- + Hasql.useWithObserverAndPoolAcquisitionTimeout + (Just \observed -> HasqlPoolExt.recordHasqlPoolSessionDuration p.metrics (realToFrac $ HasqlObserver.latency observed)) + (durationToCeilingSeconds p.poolAcquisitionTimeout) + p.rawPool + s + case result of + Left (Hasql.ConnectionError _) -> HasqlPoolExt.recordHasqlPoolConnectionFailure p.metrics + Left (Hasql.SessionError _) -> HasqlPoolExt.recordHasqlPoolSessionFailure p.metrics + Left Hasql.AcquisitionTimeoutUsageError -> HasqlPoolExt.recordHasqlPoolAcquisitionTimeout p.metrics + Right _ -> pure () + pure result + -- | Runs a 'Session' using the 'Hasql.Pool'. Retries on server errors due to -- admin intervention. Things like server restart. runSessionWithRetry :: @@ -142,7 +162,23 @@ runSessionWithRetry sess = do runSession :: (PGConstraints r) => Session a -> Sem r a runSession sess = do pool <- input - liftIO (use pool sess) >>= either throw pure + liftIO (useObserved pool sess) >>= either throw pure + +useObserved :: HasqlPoolExt.Pool -> Session a -> IO (Either Hasql.UsageError a) +useObserved pool sess = do + HasqlPoolExt.recordHasqlPoolSessionStarted pool.metrics + result <- + Hasql.useWithObserverAndPoolAcquisitionTimeout + (Just \observed -> HasqlPoolExt.recordHasqlPoolSessionDuration pool.metrics (realToFrac $ HasqlObserver.latency observed)) + (durationToCeilingSeconds pool.poolAcquisitionTimeout) + pool.rawPool + sess + case result of + Left (Hasql.ConnectionError _) -> HasqlPoolExt.recordHasqlPoolConnectionFailure pool.metrics + Left (Hasql.SessionError _) -> HasqlPoolExt.recordHasqlPoolSessionFailure pool.metrics + Left Hasql.AcquisitionTimeoutUsageError -> HasqlPoolExt.recordHasqlPoolAcquisitionTimeout pool.metrics + Right _ -> pure () + pure result -- | Runs a 'Statement' using the 'Hasql.Pool'. Always retries on server errors -- due to admin intervention. Things like server restart. diff --git a/libs/wire-subsystems/src/Wire/PostgresMigrations.hs b/libs/wire-subsystems/src/Wire/PostgresMigrations.hs index d8b022b1ffd..f3372235d28 100644 --- a/libs/wire-subsystems/src/Wire/PostgresMigrations.hs +++ b/libs/wire-subsystems/src/Wire/PostgresMigrations.hs @@ -44,7 +44,9 @@ allMigrations = map (\(name, contentBS) -> MigrationScript name (Text.decodeUtf8 nonTransactionMigrations :: Set ScriptName nonTransactionMigrations = Set.fromList ["20260428072649-create-conv-parent-index.sql"] -data PostgresMigrationError = PostgresMigrationError MigrationError +data PostgresMigrationError + = PostgresMigrationError MigrationError + | PostgresMigrationUsageError UsageError deriving (Show) instance Exception PostgresMigrationError @@ -71,7 +73,7 @@ runAllMigrations pool logger = do Just err -> throw $ PostgresMigrationError err Log.info logger $ Log.msg (Log.val "Migrations completed successfully") - either throwIO pure =<< use pool session + either (throwIO . PostgresMigrationUsageError) pure =<< use pool session where -- We must use `try` instead of blocking on the lock because running `CREATE -- INDEX CONCURRENTLY` requires all transactions to be complete and blocking @@ -120,4 +122,4 @@ resetSchema pool logger = do let session = do script "DROP SCHEMA IF EXISTS public CASCADE" script "CREATE SCHEMA IF NOT EXISTS public" - either throwIO pure =<< use pool session + either (throwIO . PostgresMigrationUsageError) pure =<< use pool session diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs index b454f1380d1..a6a1e968a72 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs @@ -29,21 +29,17 @@ import Data.Set import Data.Set qualified as Set import Data.UUID import Data.Vector hiding (mapM) -import Hasql.Pool import Hasql.Statement import Hasql.TH import Imports import Polysemy import Polysemy.Error (Error, throw) -import Polysemy.Input import Wire.API.Team.Collaborator import Wire.Postgres import Wire.TeamCollaboratorsStore interpretTeamCollaboratorsStoreToPostgres :: - ( Member (Embed IO) r, - Member (Input Pool) r, - Member (Error UsageError) r, + ( PGConstraints r, Member (Error TeamCollaboratorsError) r ) => InterpreterFor TeamCollaboratorsStore r @@ -101,10 +97,7 @@ createTeamCollaboratorImpl userId teamId permissions = do |] getAllTeamCollaboratorsImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => TeamId -> Sem r [TeamCollaborator] getAllTeamCollaboratorsImpl teamId = do @@ -118,10 +111,7 @@ getAllTeamCollaboratorsImpl teamId = do |] updateTeamCollaboratorImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => UserId -> TeamId -> Set CollaboratorPermission -> @@ -140,10 +130,7 @@ updateTeamCollaboratorImpl userId teamId permissions = do |] removeTeamCollaboratorImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => UserId -> TeamId -> Sem r () @@ -181,10 +168,7 @@ postgreslRepToCollaboratorPermission = (collaboratorPermissionMap Bimap.! {- `!` throws if the element isn't found -}) getTeamCollaborationsImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => UserId -> Sem r [TeamCollaborator] getTeamCollaborationsImpl teamId = do @@ -198,10 +182,7 @@ getTeamCollaborationsImpl teamId = do |] getTeamCollaboratorsWithIdsImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => Set TeamId -> Set UserId -> Sem r [TeamCollaborator] diff --git a/libs/wire-subsystems/src/Wire/TeamFeatureStore/Migration.hs b/libs/wire-subsystems/src/Wire/TeamFeatureStore/Migration.hs index 060fd86b38a..1edeb7daf2c 100644 --- a/libs/wire-subsystems/src/Wire/TeamFeatureStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/TeamFeatureStore/Migration.hs @@ -22,7 +22,8 @@ import Data.ByteString.Conversion import Data.Conduit import Data.Conduit.List qualified as C import Data.Id -import Hasql.Pool qualified as Hasql +import Hasql.Pool (UsageError) +import Hasql.Pool.Extended qualified as Hasql import Imports import Polysemy import Polysemy.Async @@ -137,7 +138,7 @@ handleErrors :: Member TinyLog r ) => ByteString -> - (Sem (Error MigrationLockError : Error Hasql.UsageError : r) ()) -> + (Sem (Error MigrationLockError : Error UsageError : r) ()) -> Sem r () handleErrors key action = do eithErr <- runError (runError action) diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs index 6702f5a41f4..bc034c009c4 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs @@ -32,7 +32,6 @@ import Data.UUID as UUID import Data.Vector (Vector) import Data.Vector qualified as V import Hasql.Decoders qualified as HD -import Hasql.Pool import Hasql.Session import Hasql.Statement import Hasql.TH @@ -40,7 +39,6 @@ import Hasql.Transaction qualified as Tx import Hasql.Transaction.Sessions qualified as TxSessions import Imports import Polysemy -import Polysemy.Error (Error) import Polysemy.Input import Wire.API.Pagination import Wire.API.PostgresMarshall @@ -52,10 +50,7 @@ import Wire.Postgres import Wire.UserGroupStore (UserGroupStore (..)) type UserGroupStorePostgresEffectConstraints r = - ( Member (Embed IO) r, - Member (Input Pool) r, - Member (Error UsageError) r - ) + PGConstraints r interpretUserGroupStoreToPostgres :: forall r. diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 680a8e71519..9ee955bcaf3 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -129,7 +129,7 @@ common common-all , HaskellNet-SSL , hasql , hasql-migration - , hasql-pool + , hasql-resource-pool , hasql-th , hasql-transaction , hex diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 40cd37a0110..17849cbc878 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -168,6 +168,12 @@ let src = inputs.postgresql-connection-string; }; + # Wire fork with pool acquisition timeout support. + # Update this to the upstream repo/rev once the PR is merged there. + hasql-resource-pool = { + src = inputs.hasql-resource-pool; + }; + cryptostore = { src = inputs.cryptostore; }; @@ -198,6 +204,7 @@ let version = "0.13"; sha256 = "sha256-m8Q1jwCyDrlEPbv2cZ/FIv/ey3dPjDVkmppzvi3Zjw4="; }; + }; # Name -> Source -> Maybe Subpath -> Drv mkGitDrv = name: src: subpath: diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index b176136ead1..59373e631c3 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -26,7 +26,9 @@ hself: hsuper: { # Tests require a running postgresql hasql = hlib.dontCheck hsuper.hasql_1_10_3; - hasql-pool = hlib.dontCheck hsuper.hasql-pool_1_4_2; + # The library builds with hasql-1.10.x, but its packaged test suite still + # uses older hasql APIs. + hasql-resource-pool = hlib.dontCheck hsuper.hasql-resource-pool; hasql-migration = hlib.markUnbroken (hlib.doJailbreak (hlib.dontCheck hsuper.hasql-migration)); hasql-transaction = hlib.dontCheck hsuper.hasql-transaction_1_2_2; postgresql-binary = hlib.dontCheck (hsuper.postgresql-binary_0_15_0_1); diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index a3943555eb7..af6b66a8ffe 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -47,7 +47,7 @@ library , extended , extra , galley-types - , hasql-pool + , hasql-resource-pool , HsOpenSSL , http-client , http2-manager diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index d794fbb0cc5..97734fccd3b 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -33,7 +33,6 @@ cassandraBrig: postgresqlPool: size: 5 acquisitionTimeout: 10s - agingTimeout: 1d idlenessTimeout: 10m rabbitmq: diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index 1b08c073784..28faab430e9 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -19,7 +19,7 @@ , extra , federator , galley-types -, hasql-pool +, hasql-resource-pool , HsOpenSSL , hspec , http-client @@ -78,7 +78,7 @@ mkDerivation { extended extra galley-types - hasql-pool + hasql-resource-pool HsOpenSSL http-client http2-manager diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index 6c514afb7de..b1e8a33d26c 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -30,8 +30,8 @@ import Data.Id (TeamId) import Data.Map.Strict qualified as Map import Data.Misc (HttpsUrl) import HTTP2.Client.Manager -import Hasql.Pool qualified as Hasql import Hasql.Pool.Extended +import Hasql.Pool.Extended qualified as Hasql import Imports import Network.AMQP qualified as Q import Network.AMQP.Extended diff --git a/services/background-worker/src/Wire/Effects.hs b/services/background-worker/src/Wire/Effects.hs index 09768fe3e17..70062a52986 100644 --- a/services/background-worker/src/Wire/Effects.hs +++ b/services/background-worker/src/Wire/Effects.hs @@ -35,7 +35,7 @@ import Data.Text qualified as T import Data.Text.Lazy qualified as TL import Galley.Types.Error (InternalError, internalErrorDescription, legalHoldServiceUnavailable) import Hasql.Pool (UsageError) -import Hasql.Pool qualified as Hasql +import Hasql.Pool.Extended qualified as HasqlPoolExt import Imports import Network.HTTP.Client qualified as Http import Network.Wai.Utilities.JSONResponse (JSONResponse (..)) @@ -228,7 +228,7 @@ type BackgroundWorkerEffects = Input ClientState, Input (FeatureDefaults LegalholdConfig), Input (Local ()), - Input Hasql.Pool, + Input HasqlPoolExt.Pool, P.TinyLog, Error RateLimitExceeded, Error UnreachableBackendsLegacy, @@ -301,7 +301,7 @@ runBackgroundWorkerEffects env extEnv requestId mJobId = . mapError @UnreachableBackendsLegacy (const ("Unreachable backends legacy" :: Text)) . mapError @RateLimitExceeded (const ("Rate limit exceeded" :: Text)) . interpretTinyLog - . runInputConst @Hasql.Pool env.hasqlPool + . runInputConst @HasqlPoolExt.Pool env.hasqlPool . runInputConst @(Local ()) (toLocalUnsafe env.federationDomain ()) . runInputConst @(FeatureDefaults LegalholdConfig) (env.conversationSubsystemConfig.legalholdDefaults) . runInputConst @ClientState env.cassandraGalley diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 2f7ab0b0789..7868d06447c 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -245,7 +245,7 @@ library , fsnotify >=0.4 , galley-types >=0.75.3 , hashable >=1.2 - , hasql-pool + , hasql-resource-pool , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk , HsOpenSSL >=0.10 diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 05ec68a0213..8be11f028bd 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -29,7 +29,6 @@ postgresql: postgresqlPool: size: 20 acquisitionTimeout: 10s - agingTimeout: 1d idlenessTimeout: 10m rabbitmq: diff --git a/services/brig/default.nix b/services/brig/default.nix index 4ccc141c029..e431f9c93db 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -47,7 +47,7 @@ , fsnotify , galley-types , hashable -, hasql-pool +, hasql-resource-pool , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk , hscim @@ -192,7 +192,7 @@ mkDerivation { fsnotify galley-types hashable - hasql-pool + hasql-resource-pool hs-opentelemetry-instrumentation-wai hs-opentelemetry-sdk HsOpenSSL diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 1e50c5179ed..9a4a59dda72 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -137,8 +137,8 @@ import Data.Text.IO qualified as Text import Data.Time.Clock import Database.Bloodhound qualified as ES import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) -import Hasql.Pool qualified as HasqlPool import Hasql.Pool.Extended +import Hasql.Pool.Extended qualified as HasqlPool import Imports import Network.AMQP qualified as Q import Network.AMQP.Extended qualified as Q diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index afa2ecc20e3..358aa19ea7a 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -40,7 +40,7 @@ import Data.Coerce (coerce) import Data.Qualified (Local, toLocalUnsafe) import Data.ZAuth.CryptoSign (CryptoSign, runCryptoSign) import Hasql.Pool (UsageError) -import Hasql.Pool qualified as Hasql +import Hasql.Pool.Extended qualified as HasqlPoolExt import Imports import Network.Wai.Utilities.Error qualified as Wai import Polysemy @@ -242,7 +242,7 @@ type BrigLowerLevelEffects = SFT, ConnectionStore InternalPaging, Input Cas.ClientState, - Input Hasql.Pool, + Input HasqlPoolExt.Pool, Input AppSubsystemConfig, Input UserSubsystemConfig, Input VerificationCodeThrottleTTL, diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index f3b8f1c83b4..ea72f9aeef5 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -39,9 +39,9 @@ import Data.Credentials (Credentials (..)) import Data.Id import Database.Bloodhound qualified as ES import Database.Bloodhound.Internal.Client (BHEnv (..)) -import Hasql.Pool -import Hasql.Pool qualified as Hasql +import Hasql.Pool (UsageError) import Hasql.Pool.Extended +import Hasql.Pool.Extended qualified as Hasql import Imports import Network.HTTP.Client (Manager) import Polysemy @@ -93,6 +93,11 @@ type BrigIndexEffectStack = type SemDeps = (Manager, ClientState, Hasql.Pool, BHEnv, IndexedUserStoreConfig, RequestId, IndexName) +newtype PostgresUsageException = PostgresUsageException UsageError + deriving (Show) + +instance Exception PostgresUsageException + mkSemDeps :: ESConnectionSettings -> CassandraSettings -> PostgresSettings -> Logger -> IO SemDeps mkSemDeps esConn cas pg logger = do mgr <- initHttpManagerWithTLSConfig esConn.esInsecureSkipVerifyTls esConn.esCaCert @@ -127,7 +132,7 @@ runSem (mgr, casClient, pgPool, bhEnv, indexedUserStoreConfig, reqId, migrationI runFinal . embedToFinal . throwErrorToIOFinal @ClientError - . throwErrorToIOFinal @UsageError + . throwPostgresUsageErrorToIOFinal . runInputConst pgPool . loggerToTinyLogReqId reqId logger . ignoreMetrics @@ -148,6 +153,12 @@ throwErrorToIOFinal action = do Left e -> embedFinal $ throwIO e Right a -> pure a +throwPostgresUsageErrorToIOFinal :: (Member (Final IO) r) => InterpreterFor (Error UsageError) r +throwPostgresUsageErrorToIOFinal action = do + runError action >>= \case + Left e -> embedFinal $ throwIO (PostgresUsageException e) + Right a -> pure a + runCommand :: Logger -> Command -> IO () runCommand l = \case Create es galley -> do diff --git a/services/brig/src/Brig/Index/Options.hs b/services/brig/src/Brig/Index/Options.hs index bbf0c9880ed..b19c1f3124f 100644 --- a/services/brig/src/Brig/Index/Options.hs +++ b/services/brig/src/Brig/Index/Options.hs @@ -367,13 +367,6 @@ poolConfigParser = <> help "Pool acquisition timeout in seconds" <> value (unsafeParseDuration "10s") ) - <*> option - (eitherReader (parseDuration . Text.pack)) - ( long "pg-pool-aging-timeout" - <> metavar "Duration" - <> help "Pool aging timeout in seconds" - <> value (unsafeParseDuration "1d") - ) <*> option (eitherReader (parseDuration . Text.pack)) ( long "pg-pool-idleness-timeout" diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 5b5d0f96818..6ef4967bcde 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -45,6 +45,7 @@ import Data.Metrics.AWS (gaugeTokenRemaing) import Data.Metrics.Servant qualified as Metrics import Data.Proxy (Proxy (Proxy)) import Data.Text (unpack) +import Hasql.Pool.Extended (rawPool) import Imports hiding (head) import Network.HTTP.Media qualified as HTTPMedia import Network.HTTP.Types qualified as HTTP @@ -82,7 +83,7 @@ import Wire.UserStore run :: Opts -> IO () run opts = withTracer \tracer -> do (app, e) <- mkApp opts - runAllMigrations e.hasqlPool e.appLogger + runAllMigrations e.hasqlPool.rawPool e.appLogger let s = Server.newSettings (server e) internalEventListener <- Async.async $ @@ -113,7 +114,7 @@ run opts = withTracer \tracer -> do migratePostgres :: Opts -> Bool -> IO () migratePostgres opts resetFirst = do logger <- initLogger opts - pool <- initPostgresPool opts.postgresqlPool opts.postgresql opts.postgresqlPassword + pool <- (.rawPool) <$> initPostgresPool opts.postgresqlPool opts.postgresql opts.postgresqlPassword when resetFirst $ resetSchema pool logger runAllMigrations pool logger flush logger diff --git a/services/galley/default.nix b/services/galley/default.nix index 8ab0f4c24c8..3f9eb5cb466 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -32,7 +32,7 @@ , federator , filepath , galley-types -, hasql-pool +, hasql-resource-pool , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk , HsOpenSSL @@ -134,7 +134,7 @@ mkDerivation { exceptions extended galley-types - hasql-pool + hasql-resource-pool hs-opentelemetry-instrumentation-wai hs-opentelemetry-sdk HsOpenSSL diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 2ecfe746462..5ca4fdc34c8 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -210,7 +210,7 @@ library , exceptions >=0.4 , extended , galley-types >=0.65.0 - , hasql-pool + , hasql-resource-pool , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk , HsOpenSSL >=0.11 diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index e31a28ce321..666b300d50d 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -19,7 +19,6 @@ postgresql: postgresqlPool: size: 20 acquisitionTimeout: 10s - agingTimeout: 1d idlenessTimeout: 10m brig: diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index b7e256db100..9aa6c2f6503 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -62,6 +62,7 @@ import Galley.Types.Error import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) import Hasql.Pool qualified as Hasql import Hasql.Pool.Extended (initPostgresPool) +import Hasql.Pool.Extended qualified as HasqlPoolExt import Imports hiding (forkIO) import Network.AMQP.Extended (mkRabbitMqChannelMVar) import Network.HTTP.Client (responseTimeoutMicro) @@ -266,7 +267,7 @@ type GalleyEffects = ErrorS 'InvalidOperation, Error RpcException, Input ClientState, - Input Hasql.Pool, + Input HasqlPoolExt.Pool, Input Env, Input ConversationSubsystemConfig, Error MigrationLockError, diff --git a/services/galley/src/Galley/Env.hs b/services/galley/src/Galley/Env.hs index 009ac952546..f753b0c8db3 100644 --- a/services/galley/src/Galley/Env.hs +++ b/services/galley/src/Galley/Env.hs @@ -50,7 +50,7 @@ import Data.Misc (HttpsUrl) import Data.Time.Clock.DiffTime (millisecondsToDiffTime) import Galley.Queue qualified as Q import HTTP2.Client.Manager (Http2Manager) -import Hasql.Pool +import Hasql.Pool.Extended import Imports import Network.AMQP qualified as Q import Network.HTTP.Client diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index be55ed36d83..9ea6b3870ae 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -45,6 +45,7 @@ import Galley.Cassandra import Galley.Env import Galley.Monad import Galley.Queue qualified as Q +import Hasql.Pool.Extended (rawPool) import Imports import Network.HTTP.Media.RenderHeader qualified as HTTPMedia import Network.HTTP.Types qualified as HTTP @@ -74,7 +75,7 @@ run :: Opts -> IO () run opts = lowerCodensity do tracer <- withTracerC (app, env) <- mkApp opts - lift $ runAllMigrations env._hasqlPool env._applog + lift $ runAllMigrations env._hasqlPool.rawPool env._applog let settings' = newSettings $ defaultServer