packages feed

clickhouse-migrate-0.2.0.0: src/ClickHouseMigrate/Internal/ServerPolicy.hs

{-# LANGUAGE MultilineStrings #-}

-- | Pinned-server admission before application/ledger work. No initialization.
module ClickHouseMigrate.Internal.ServerPolicy
  ( TargetCheckError (..),
    probeTarget,
    withTransport,
    withTransportChecked,
    supportedVersions,
  )
where

import ClickHouseMigrate.Internal.Checksum qualified as Checksum
import ClickHouseMigrate.Internal.Http (Http, probeConnection, submit, withHttpAdmissionChecked)
import ClickHouseMigrate.Internal.ServerProfile (TargetCheckError (..))
import ClickHouseMigrate.Internal.ServerProfile qualified as Profile
import ClickHouseMigrate.Internal.TargetTypes
import ClickHouseMigrate.Internal.Transport
import ClickHouseMigrate.Prelude
import Data.IORef (atomicModifyIORef', newIORef)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Encoding

-- Exact tested-patch policy. Keep coordinated with the checked-in candidate matrix.
supportedVersions :: [Text]
supportedVersions = ["26.8.28.14", "26.3.60.10001.altinitystable", "26.3.60.10002.altinitystable"]

probeTarget :: Http -> TargetConfig -> QueryId -> IO (Either TargetCheckError ServerIdentity)
probeTarget http config identity = validateOutcome config <$> submit http identity JsonRows (Profile.identityQuery (configTopology config))

validateOutcome :: TargetConfig -> TransportOutcome -> Either TargetCheckError ServerIdentity
validateOutcome config = Profile.validateOutcome supportedVersions (configTopology config)

-- The only production constructor for a checked transport. Its Http resources
-- close at scope exit; a capability used after exit cannot submit again.
withTransport :: TargetConfig -> QueryId -> (Transport -> IO a) -> IO (ScopeResult (Either TargetCheckError a))
withTransport config probeId = withTransportChecked config probeId (pure (Right ()))

withTransportChecked :: TargetConfig -> QueryId -> IO (Either TransportError ()) -> (Transport -> IO a) -> IO (ScopeResult (Either TargetCheckError a))
withTransportChecked config probeId@(QueryId rootId) check action = do
  counter <- newIORef (0 :: Int)
  let prefix = "chm-admission-" <> Checksum.digestHex (Checksum.digest (Encoding.encodeUtf8 rootId)) <> "-"
      admit connection = do
        n <- atomicModifyIORef' counter (\old -> (old + 1, old))
        outcome <- probeConnection config connection (QueryId (prefix <> Text.pack (show n))) (Profile.identityQuery (configTopology config))
        pure $ case validateOutcome config outcome of
          Right _ -> Right ()
          Left (TargetRejected failure) -> Left failure
          Left (ProbeFailure (NotSubmitted failure)) -> Left failure
          Left (ProbeFailure (SubmissionUncertain failure)) -> Left failure
          Left (ProbeFailure (ObservedServerFailure failure)) -> Left failure
          Left _ -> Left (TransportError UnexpectedResponse Nothing Nothing)
  withHttpAdmissionChecked config admit check $ \http -> do
    checked <- probeTarget http config probeId
    case checked of
      Left failure -> pure (Left failure)
      Right identity -> do
        membership <- case Profile.membershipQuery (configTopology config) of
          Nothing -> pure (Right ())
          Just sql -> Profile.validateMembership (configTopology config) <$> submit http (QueryId (rootId <> "-members")) JsonRows sql
        case membership of
          Left failure -> pure (Left failure)
          Right () -> Right <$> action (Transport (submit http) identity)