packages feed

natskell-1.2.0.0: jetstream/JetStream/KeyValue/Types.hs

{-# LANGUAGE OverloadedStrings #-}

module JetStream.KeyValue.Types
  ( KeyValueAPI (..)
  , KeyValueBucket (..)
  , KeyValueBucketName
  , KeyValueKey
  , KeyValuePattern
  , KeyValueValue
  , KeyValueRevision
  , KeyValueOperation (..)
  , KeyValueEntry (..)
  , KeyValueConfig (..)
  , KeyValueConfigOption
  , KeyValueStatus (..)
  , KeyValueError (..)
  , KeyValueDeleteOption
  , KeyValueDeleteConfig (..)
  , KeyValueWatchOption
  , KeyValueWatchConfig (..)
  , KeyValueWatcher (..)
  , KeyValueWatchBatch (..)
  , KeyValuePurgeDeletesOption
  , KeyValuePurgeDeletesConfig (..)
  , keyValueConfig
  , validateKeyValueConfig
  , validateKeyValueBucketName
  , validateKeyValueKey
  , validateKeyValuePattern
  , keyValueStreamName
  , keyValueSubjectPrefix
  , keyValueSubject
  , keyValuePatternSubject
  , keyValueDuplicateWindow
  , keyValueStreamOptions
  , keyValueEntryFromStreamMessage
  , keyValueEntryFromMessage
  , keyValueDeleteConfig
  , keyValueWatchConfig
  , validateKeyValueWatchConfig
  , keyValuePurgeDeletesConfig
  , withKeyValueDescription
  , withKeyValueMaxValueSize
  , withKeyValueHistory
  , withKeyValueTTL
  , withKeyValueMaxBytes
  , withKeyValueStorage
  , withKeyValueReplicas
  , withKeyValueCompression
  , withKeyValueLastRevision
  , withKeyValueIncludeHistory
  , withKeyValueUpdatesOnly
  , withKeyValueIgnoreDeletes
  , withKeyValueMetadataOnly
  , withKeyValueDeleteMarkersOlderThan
  ) where

import           Control.Concurrent.STM  (TVar)
import qualified Data.ByteString         as BS
import qualified Data.ByteString.Char8   as BC
import           Data.Char
    ( isAsciiLower
    , isAsciiUpper
    , isDigit
    , toLower
    )
import           Data.Int                (Int32)
import           Data.Maybe              (catMaybes, fromMaybe)
import           Data.Time.Clock         (NominalDiffTime, UTCTime)
import           Data.Word               (Word64)
import           JetStream.Error         (JetStreamError)
import           JetStream.Message.Types
    ( FetchOption
    , Message
    , OrderedConsumer
    , PullStatus
    , messageHeaders
    , messageMetadata
    , messageMetadataNumPending
    , messageMetadataStreamSequence
    , messageMetadataTimestamp
    , messagePayload
    , messageSubject
    )
import           JetStream.Stream.Types
    ( StorageType (FileStorage)
    , StreamCompression (S2Compression)
    , StreamConfigOption
    , StreamMessage
    , streamMessageHeadersRaw
    , streamMessagePayload
    , streamMessageSequence
    , streamMessageSubject
    , streamMessageTime
    , withAllowDirect
    , withAllowRollup
    , withCompression
    , withDenyDelete
    , withDescription
    , withDiscard
    , withDuplicateWindow
    , withMaxAge
    , withMaxBytes
    , withMaxConsumers
    , withMaxMessageSize
    , withMaxMessages
    , withMaxMessagesPerSubject
    , withReplicas
    , withRetention
    , withStorage
    )
import           JetStream.Types
    ( CallOption
    , DiscardPolicy (DiscardNew)
    , JetStreamRequestOption
    , RetentionPolicy (LimitsPolicy)
    , applyCallOptions
    )
import           Parser.Attoparsec       (parseHeaderBlock)

type KeyValueBucketName = BS.ByteString
type KeyValueKey = BS.ByteString
type KeyValuePattern = BS.ByteString
type KeyValueValue = BS.ByteString
type KeyValueRevision = Word64

data KeyValueAPI = KeyValueAPI
                     { createKeyValueBucket :: KeyValueBucketName -> [KeyValueConfigOption] -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueBucket)
                     , updateKeyValueBucket :: KeyValueBucketName -> [KeyValueConfigOption] -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueBucket)
                     , createOrUpdateKeyValueBucket :: KeyValueBucketName -> [KeyValueConfigOption] -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueBucket)
                     , lookupKeyValueBucket :: KeyValueBucketName -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueBucket)
                     , deleteKeyValueBucket :: KeyValueBucketName -> [JetStreamRequestOption] -> IO (Either KeyValueError ())
                     , listKeyValueBuckets :: [JetStreamRequestOption] -> IO (Either KeyValueError [KeyValueBucket])
                     , listKeyValueStatuses :: [JetStreamRequestOption] -> IO (Either KeyValueError [KeyValueStatus])
                     , getKeyValueStatus :: KeyValueBucket -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueStatus)
                     , getKeyValueEntry :: KeyValueBucket -> KeyValueKey -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueEntry)
                     , getKeyValueEntryRevision :: KeyValueBucket -> KeyValueKey -> KeyValueRevision -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueEntry)
                     , putKeyValueEntry :: KeyValueBucket -> KeyValueKey -> KeyValueValue -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueRevision)
                     , createKeyValueEntry :: KeyValueBucket -> KeyValueKey -> KeyValueValue -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueRevision)
                     , updateKeyValueEntry :: KeyValueBucket -> KeyValueKey -> KeyValueValue -> KeyValueRevision -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueRevision)
                     , deleteKeyValueEntry :: KeyValueBucket -> KeyValueKey -> [KeyValueDeleteOption] -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueRevision)
                     , purgeKeyValueEntry :: KeyValueBucket -> KeyValueKey -> [KeyValueDeleteOption] -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueRevision)
                     , watchKeyValues :: KeyValueBucket -> [KeyValuePattern] -> [KeyValueWatchOption] -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueWatcher)
                     , fetchKeyValueWatch :: KeyValueWatcher -> [FetchOption] -> [JetStreamRequestOption] -> IO (Either KeyValueError KeyValueWatchBatch)
                     , stopKeyValueWatch :: KeyValueWatcher -> [JetStreamRequestOption] -> IO (Either KeyValueError ())
                     , listKeyValueKeys :: KeyValueBucket -> [JetStreamRequestOption] -> IO (Either KeyValueError [KeyValueKey])
                     , getKeyValueHistory :: KeyValueBucket -> KeyValueKey -> [JetStreamRequestOption] -> IO (Either KeyValueError [KeyValueEntry])
                     , purgeDeletedKeyValueEntries :: KeyValueBucket -> [KeyValuePurgeDeletesOption] -> [JetStreamRequestOption] -> IO (Either KeyValueError ())
                     }

newtype KeyValueBucket = KeyValueBucket { keyValueBucketName :: KeyValueBucketName }
  deriving (Eq, Ord, Show)

data KeyValueOperation = KeyValuePut | KeyValueDelete | KeyValuePurge
  deriving (Eq, Show)

data KeyValueEntry = KeyValueEntry
                       { keyValueEntryBucket    :: KeyValueBucketName
                       , keyValueEntryKey       :: KeyValueKey
                       , keyValueEntryValue     :: KeyValueValue
                       , keyValueEntryRevision  :: KeyValueRevision
                       , keyValueEntryCreated   :: UTCTime
                       , keyValueEntryDelta     :: Integer
                       , keyValueEntryOperation :: KeyValueOperation
                       }
  deriving (Eq, Show)

data KeyValueConfig = KeyValueConfig
                        { keyValueConfigBucket       :: KeyValueBucketName
                        , keyValueConfigDescription  :: Maybe BS.ByteString
                        , keyValueConfigMaxValueSize :: Int32
                        , keyValueConfigHistory      :: Int
                        , keyValueConfigTTL          :: NominalDiffTime
                        , keyValueConfigMaxBytes     :: Integer
                        , keyValueConfigStorage      :: StorageType
                        , keyValueConfigReplicas     :: Int
                        , keyValueConfigCompression  :: Bool
                        }
  deriving (Eq, Show)

type KeyValueConfigOption = CallOption KeyValueConfig

data KeyValueStatus = KeyValueStatus
                        { keyValueStatusBucket :: KeyValueBucketName
                        , keyValueStatusValues :: Integer
                        , keyValueStatusBytes  :: Integer
                        , keyValueStatusConfig :: KeyValueConfig
                        }
  deriving (Eq, Show)

data KeyValueError = KeyValueJetStreamError JetStreamError
                   | KeyValueInvalidBucketName KeyValueBucketName
                   | KeyValueInvalidKey KeyValueKey
                   | KeyValueInvalidPattern KeyValuePattern
                   | KeyValueInvalidHistory Int
                   | KeyValueInvalidMaxValueSize Int32
                   | KeyValueInvalidMaxBytes Integer
                   | KeyValueInvalidTTL NominalDiffTime
                   | KeyValueInvalidReplicas Int
                   | KeyValueInvalidWatchOptions
                   | KeyValueBucketNotFound KeyValueBucketName
                   | KeyValueBucketExists KeyValueBucketName
                   | KeyValueInvalidBucket KeyValueBucketName
                   | KeyValueKeyNotFound KeyValueBucketName KeyValueKey
                   | KeyValueKeyExists KeyValueBucketName KeyValueKey
                   | KeyValueRevisionMismatch KeyValueBucketName KeyValueKey KeyValueRevision
                   | KeyValueNoKeysFound KeyValueBucketName
                   | KeyValueDecodeError String
  deriving (Eq, Show)

newtype KeyValueDeleteConfig = KeyValueDeleteConfig { keyValueDeleteExpectedRevision :: Maybe KeyValueRevision }
  deriving (Eq, Show)

type KeyValueDeleteOption = CallOption KeyValueDeleteConfig

data KeyValueWatchConfig = KeyValueWatchConfig
                             { keyValueWatchIncludeHistory :: Bool
                             , keyValueWatchUpdatesOnly    :: Bool
                             , keyValueWatchIgnoreDeletes  :: Bool
                             , keyValueWatchMetadataOnly   :: Bool
                             }
  deriving (Eq, Show)

type KeyValueWatchOption = CallOption KeyValueWatchConfig

data KeyValueWatcher = KeyValueWatcher
                         { keyValueWatcherBucket           :: KeyValueBucket
                         , keyValueWatcherConsumer         :: OrderedConsumer
                         , keyValueWatcherIgnoreDeletes    :: Bool
                         , keyValueWatcherInitialRemaining :: TVar Integer
                         , keyValueWatcherInitialComplete  :: TVar Bool
                         }

data KeyValueWatchBatch = KeyValueWatchBatch
                            { keyValueWatchEntries         :: [KeyValueEntry]
                            , keyValueWatchInitialComplete :: Bool
                            , keyValueWatchStatus          :: Maybe PullStatus
                            }
  deriving (Eq, Show)

newtype KeyValuePurgeDeletesConfig = KeyValuePurgeDeletesConfig { keyValueDeleteMarkersOlderThan :: NominalDiffTime }
  deriving (Eq, Show)

type KeyValuePurgeDeletesOption = CallOption KeyValuePurgeDeletesConfig

keyValueConfig :: KeyValueBucketName -> [KeyValueConfigOption] -> KeyValueConfig
keyValueConfig bucket options =
  normalizeKeyValueConfig $ applyCallOptions options KeyValueConfig
    { keyValueConfigBucket = bucket
    , keyValueConfigDescription = Nothing
    , keyValueConfigMaxValueSize = -1
    , keyValueConfigHistory = 1
    , keyValueConfigTTL = 0
    , keyValueConfigMaxBytes = -1
    , keyValueConfigStorage = FileStorage
    , keyValueConfigReplicas = 1
    , keyValueConfigCompression = False
    }

normalizeKeyValueConfig :: KeyValueConfig -> KeyValueConfig
normalizeKeyValueConfig config =
  config
    { keyValueConfigMaxValueSize = defaultWhenZero (-1) (keyValueConfigMaxValueSize config)
    , keyValueConfigHistory = defaultWhenZero 1 (keyValueConfigHistory config)
    , keyValueConfigMaxBytes = defaultWhenZero (-1) (keyValueConfigMaxBytes config)
    , keyValueConfigReplicas = defaultWhenZero 1 (keyValueConfigReplicas config)
    }
  where
    defaultWhenZero fallback value
      | value == 0 = fallback
      | otherwise = value

validateKeyValueConfig :: KeyValueConfig -> Either KeyValueError ()
validateKeyValueConfig config = do
  validateKeyValueBucketName (keyValueConfigBucket config)
  if keyValueConfigHistory config < 1 || keyValueConfigHistory config > 64
    then Left (KeyValueInvalidHistory (keyValueConfigHistory config))
    else Right ()
  if keyValueConfigMaxValueSize config < (-1)
    then Left (KeyValueInvalidMaxValueSize (keyValueConfigMaxValueSize config))
    else Right ()
  if keyValueConfigMaxBytes config < (-1)
    then Left (KeyValueInvalidMaxBytes (keyValueConfigMaxBytes config))
    else Right ()
  if keyValueConfigTTL config < 0
    then Left (KeyValueInvalidTTL (keyValueConfigTTL config))
    else Right ()
  if keyValueConfigReplicas config < 1
    then Left (KeyValueInvalidReplicas (keyValueConfigReplicas config))
    else Right ()

validateKeyValueBucketName :: KeyValueBucketName -> Either KeyValueError ()
validateKeyValueBucketName bucket
  | BS.null bucket = Left (KeyValueInvalidBucketName bucket)
  | BC.all isBucketCharacter bucket = Right ()
  | otherwise = Left (KeyValueInvalidBucketName bucket)

validateKeyValueKey :: KeyValueKey -> Either KeyValueError ()
validateKeyValueKey key
  | validKeyLike isKeyCharacter key = Right ()
  | otherwise = Left (KeyValueInvalidKey key)

validateKeyValuePattern :: KeyValuePattern -> Either KeyValueError ()
validateKeyValuePattern pattern'
  | validKeyLike isPatternCharacter pattern' && validGreaterThan pattern' = Right ()
  | otherwise = Left (KeyValueInvalidPattern pattern')
  where
    validGreaterThan value =
      case BC.elemIndex '>' value of
        Nothing    -> True
        Just index -> index == BS.length value - 1

validKeyLike :: (Char -> Bool) -> BS.ByteString -> Bool
validKeyLike validCharacter value =
  not (BS.null value)
    && BC.head value /= '.'
    && BC.last value /= '.'
    && not (".." `BS.isInfixOf` value)
    && BC.all validCharacter value

isBucketCharacter :: Char -> Bool
isBucketCharacter char =
  isAsciiAlphaNumeric char || char == '_' || char == '-'

isKeyCharacter :: Char -> Bool
isKeyCharacter char =
  isAsciiAlphaNumeric char || char `elem` ("-/_=." :: String)

isPatternCharacter :: Char -> Bool
isPatternCharacter char =
  isKeyCharacter char || char == '*' || char == '>'

isAsciiAlphaNumeric :: Char -> Bool
isAsciiAlphaNumeric char =
  isAsciiLower char || isAsciiUpper char || isDigit char

keyValueStreamName :: KeyValueBucketName -> BS.ByteString
keyValueStreamName = BS.append "KV_"

keyValueSubjectPrefix :: KeyValueBucketName -> BS.ByteString
keyValueSubjectPrefix bucket = BS.concat ["$KV.", bucket, "."]

keyValueSubject :: KeyValueBucketName -> KeyValueKey -> BS.ByteString
keyValueSubject bucket = BS.append (keyValueSubjectPrefix bucket)

keyValuePatternSubject :: KeyValueBucketName -> KeyValuePattern -> BS.ByteString
keyValuePatternSubject = keyValueSubject

keyValueStreamOptions :: KeyValueConfig -> [StreamConfigOption]
keyValueStreamOptions config =
  catMaybes
    [ withDescription <$> keyValueConfigDescription config
    , Just (withRetention LimitsPolicy)
    , Just (withStorage (keyValueConfigStorage config))
    , Just (withDiscard DiscardNew)
    , Just (withMaxConsumers (-1))
    , Just (withMaxMessages (-1))
    , Just (withMaxMessagesPerSubject (toInteger (keyValueConfigHistory config)))
    , Just (withMaxBytes (keyValueConfigMaxBytes config))
    , Just (withMaxAge (keyValueConfigTTL config))
    , Just (withMaxMessageSize (keyValueConfigMaxValueSize config))
    , Just (withReplicas (keyValueConfigReplicas config))
    , Just (withDuplicateWindow (keyValueDuplicateWindow config))
    , Just (withDenyDelete True)
    , Just (withAllowRollup True)
    , Just (withAllowDirect True)
    , if keyValueConfigCompression config
        then Just (withCompression S2Compression)
        else Nothing
    ]

keyValueDuplicateWindow :: KeyValueConfig -> NominalDiffTime
keyValueDuplicateWindow config
  | keyValueConfigTTL config > 0 = min 120 (keyValueConfigTTL config)
  | otherwise = 120

keyValueEntryFromStreamMessage
  :: KeyValueBucket
  -> KeyValueKey
  -> StreamMessage
  -> Either KeyValueError KeyValueEntry
keyValueEntryFromStreamMessage bucket key message
  | streamMessageSubject message /= expectedSubject =
      Left (KeyValueKeyNotFound bucketName key)
  | otherwise = do
      operation <- operationFromRawHeaders (streamMessageHeadersRaw message)
      Right KeyValueEntry
        { keyValueEntryBucket = bucketName
        , keyValueEntryKey = key
        , keyValueEntryValue = fromMaybe BS.empty (streamMessagePayload message)
        , keyValueEntryRevision = streamMessageSequence message
        , keyValueEntryCreated = streamMessageTime message
        , keyValueEntryDelta = 0
        , keyValueEntryOperation = operation
        }
  where
    bucketName = keyValueBucketName bucket
    expectedSubject = keyValueSubject bucketName key

keyValueEntryFromMessage
  :: KeyValueBucket
  -> Message
  -> Either KeyValueError KeyValueEntry
keyValueEntryFromMessage bucket message = do
  metadata <- maybe
    (Left (KeyValueDecodeError "missing JetStream message metadata"))
    Right
    (messageMetadata message)
  key <- keyFromSubject bucket (messageSubject message)
  pure KeyValueEntry
    { keyValueEntryBucket = keyValueBucketName bucket
    , keyValueEntryKey = key
    , keyValueEntryValue = messagePayload message
    , keyValueEntryRevision = messageMetadataStreamSequence metadata
    , keyValueEntryCreated = messageMetadataTimestamp metadata
    , keyValueEntryDelta = messageMetadataNumPending metadata
    , keyValueEntryOperation = operationFromHeaders (messageHeaders message)
    }

keyFromSubject :: KeyValueBucket -> BS.ByteString -> Either KeyValueError KeyValueKey
keyFromSubject bucket subject =
  if prefix `BS.isPrefixOf` subject && BS.length subject > BS.length prefix
    then Right (BS.drop (BS.length prefix) subject)
    else Left (KeyValueDecodeError "key-value message subject does not match bucket")
  where
    prefix = keyValueSubjectPrefix (keyValueBucketName bucket)

operationFromRawHeaders :: Maybe BS.ByteString -> Either KeyValueError KeyValueOperation
operationFromRawHeaders Nothing =
  Right KeyValuePut
operationFromRawHeaders (Just rawHeaders) =
  case parseHeaderBlock rawHeaders of
    Left err      -> Left (KeyValueDecodeError err)
    Right headers -> Right (operationFromHeaders (Just headers))

operationFromHeaders :: Maybe [(BS.ByteString, BS.ByteString)] -> KeyValueOperation
operationFromHeaders headers =
  case lookupHeader "KV-Operation" (fromMaybe [] headers) of
    Just "DEL"   -> KeyValueDelete
    Just "PURGE" -> KeyValuePurge
    _ ->
      case lookupHeader "Nats-Marker-Reason" (fromMaybe [] headers) of
        Just "MaxAge" -> KeyValuePurge
        Just "Purge"  -> KeyValuePurge
        Just "Remove" -> KeyValueDelete
        _             -> KeyValuePut

lookupHeader
  :: BS.ByteString
  -> [(BS.ByteString, BS.ByteString)]
  -> Maybe BS.ByteString
lookupHeader name = go
  where
    normalizedName = normalizeHeaderName name
    go [] = Nothing
    go ((headerName, value):headers)
      | normalizeHeaderName headerName == normalizedName = Just value
      | otherwise = go headers

normalizeHeaderName :: BS.ByteString -> BS.ByteString
normalizeHeaderName = BC.map toLower

keyValueDeleteConfig :: [KeyValueDeleteOption] -> KeyValueDeleteConfig
keyValueDeleteConfig options =
  applyCallOptions options (KeyValueDeleteConfig Nothing)

keyValueWatchConfig :: [KeyValueWatchOption] -> KeyValueWatchConfig
keyValueWatchConfig options =
  applyCallOptions options KeyValueWatchConfig
    { keyValueWatchIncludeHistory = False
    , keyValueWatchUpdatesOnly = False
    , keyValueWatchIgnoreDeletes = False
    , keyValueWatchMetadataOnly = False
    }

validateKeyValueWatchConfig :: KeyValueWatchConfig -> Either KeyValueError ()
validateKeyValueWatchConfig config
  | keyValueWatchIncludeHistory config && keyValueWatchUpdatesOnly config =
      Left KeyValueInvalidWatchOptions
  | otherwise = Right ()

keyValuePurgeDeletesConfig
  :: [KeyValuePurgeDeletesOption]
  -> KeyValuePurgeDeletesConfig
keyValuePurgeDeletesConfig options =
  applyCallOptions options (KeyValuePurgeDeletesConfig 1800)

withKeyValueDescription :: BS.ByteString -> KeyValueConfigOption
withKeyValueDescription description config =
  config { keyValueConfigDescription = Just description }

withKeyValueMaxValueSize :: Int32 -> KeyValueConfigOption
withKeyValueMaxValueSize maxValueSize config =
  config { keyValueConfigMaxValueSize = maxValueSize }

withKeyValueHistory :: Int -> KeyValueConfigOption
withKeyValueHistory history config =
  config { keyValueConfigHistory = history }

withKeyValueTTL :: NominalDiffTime -> KeyValueConfigOption
withKeyValueTTL ttl config =
  config { keyValueConfigTTL = ttl }

withKeyValueMaxBytes :: Integer -> KeyValueConfigOption
withKeyValueMaxBytes maxBytes config =
  config { keyValueConfigMaxBytes = maxBytes }

withKeyValueStorage :: StorageType -> KeyValueConfigOption
withKeyValueStorage storage config =
  config { keyValueConfigStorage = storage }

withKeyValueReplicas :: Int -> KeyValueConfigOption
withKeyValueReplicas replicas config =
  config { keyValueConfigReplicas = replicas }

withKeyValueCompression :: Bool -> KeyValueConfigOption
withKeyValueCompression compression config =
  config { keyValueConfigCompression = compression }

withKeyValueLastRevision :: KeyValueRevision -> KeyValueDeleteOption
withKeyValueLastRevision revision config =
  config { keyValueDeleteExpectedRevision = Just revision }

withKeyValueIncludeHistory :: KeyValueWatchOption
withKeyValueIncludeHistory config =
  config { keyValueWatchIncludeHistory = True }

withKeyValueUpdatesOnly :: KeyValueWatchOption
withKeyValueUpdatesOnly config =
  config { keyValueWatchUpdatesOnly = True }

withKeyValueIgnoreDeletes :: KeyValueWatchOption
withKeyValueIgnoreDeletes config =
  config { keyValueWatchIgnoreDeletes = True }

withKeyValueMetadataOnly :: KeyValueWatchOption
withKeyValueMetadataOnly config =
  config { keyValueWatchMetadataOnly = True }

withKeyValueDeleteMarkersOlderThan
  :: NominalDiffTime
  -> KeyValuePurgeDeletesOption
withKeyValueDeleteMarkersOlderThan olderThan config =
  config { keyValueDeleteMarkersOlderThan = olderThan }