natskell-1.0.0.1: jetstream/JetStream/Message.hs
{-# LANGUAGE LambdaCase #-}
module JetStream.Message
( messageAPI
, fetchMessages
, ackMessage
, nakMessage
, inProgressMessage
, termMessage
) where
import qualified Client.API as Nats
import Control.Concurrent.STM
import Control.Exception (bracket)
import Control.Monad (unless, void)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BC
import Data.Char (isAlphaNum)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Word (Word64)
import JetStream.Consumer.API
( ConsumerAPI
, consumerInfo
, deleteConsumer
, putConsumer
)
import JetStream.Consumer.Types
( ConsumerAction (ConsumerCreateOrUpdate)
, ConsumerConfigOption
, ConsumerInfo (consumerInfoName)
, ConsumerKind (PullConsumer)
, ConsumerTarget (NamedConsumer)
, withConsumerAckPolicy
, withConsumerDeliverPolicy
, withConsumerFilter
, withConsumerHeadersOnly
, withConsumerInactiveThreshold
, withConsumerMaxDeliver
, withConsumerMaxWaiting
, withConsumerMemoryStorage
, withConsumerReplayPolicy
, withConsumerReplicas
)
import JetStream.Error
( JetStreamError (JetStreamDecodeError, JetStreamNatsError, JetStreamNoReply, JetStreamTimeout)
)
import JetStream.Message.Types
import JetStream.Options
( JetStreamContext (..)
, requestTimeoutMicros
)
import qualified JetStream.Protocol.Subject as Subject
import JetStream.Types
( AckPolicy (AckNone)
, ConsumerName
, DeliverPolicy (DeliverByStartSequence)
, JetStreamRequestOption
, StreamName
, Subject
)
import System.Timeout (timeout)
-- | Build the message capability from the shared JetStream context.
messageAPI :: JetStreamContext -> ConsumerAPI -> MessageAPI
messageAPI context consumerAPI =
MessageAPI
{ fetch = \stream consumer options requestOptions ->
fetchMessages context stream consumer (pullRequest options) requestOptions
, consumePush = \deliverSubject options _ handler ->
consumePushMessages context deliverSubject (pushConsumeConfig options) handler
, createOrderedConsumer = \stream options requestOptions ->
createOrderedConsumerHandle context consumerAPI stream (orderedConsumerConfig options) requestOptions
, ack = \message _ -> ackMessage (contextClient context) message
, nak = \message _ -> nakMessage (contextClient context) message
, inProgress = \message _ -> inProgressMessage (contextClient context) message
, term = \message _ -> termMessage (contextClient context) message
}
consumePushMessages
:: JetStreamContext
-> Subject
-> PushConsumeConfig
-> (Message -> IO ())
-> IO (Either JetStreamError PushSubscription)
consumePushMessages context deliverSubject config handler = do
subscription <- Nats.subscribe natsClient deliverSubject subscribeOptions $ \msgView ->
let message = fromMsgView msgView
in unless (isStatusMessage message) (handler message)
pure $ case subscription of
Left err -> Left (JetStreamNatsError err)
Right handle -> Right PushSubscription
{ stopPushSubscription = mapNatsResult <$> Nats.unsubscribe natsClient handle []
}
where
natsClient = contextClient context
subscribeOptions =
maybe [] (\queueGroup -> [Nats.withQueueGroup queueGroup]) (pushConsumeQueueGroup config)
data OrderedState = OrderedState
{ orderedStateContext :: JetStreamContext
, orderedStateConsumers :: ConsumerAPI
, orderedStateStream :: StreamName
, orderedStateConfig :: OrderedConsumerConfig
, orderedStateNamePrefix :: ConsumerName
, orderedStateSerial :: TVar Int
, orderedStateNextSequence :: TVar (Maybe Word64)
, orderedStateCurrentName :: TVar (Maybe ConsumerName)
, orderedStateStopped :: TVar Bool
}
createOrderedConsumerHandle
:: JetStreamContext
-> ConsumerAPI
-> StreamName
-> OrderedConsumerConfig
-> [JetStreamRequestOption]
-> IO (Either JetStreamError OrderedConsumer)
createOrderedConsumerHandle context consumerAPI stream config requestOptions = do
namePrefix <- orderedNamePrefix context config
state <- OrderedState context consumerAPI stream config namePrefix
<$> newTVarIO 0
<*> newTVarIO Nothing
<*> newTVarIO Nothing
<*> newTVarIO False
created <- resetOrderedConsumer state requestOptions
pure $ do
void created
Right OrderedConsumer
{ orderedConsumerInfo = orderedInfo state
, fetchOrdered = fetchOrderedMessages state
, stopOrderedConsumer = stopOrdered state
}
orderedInfo :: OrderedState -> [JetStreamRequestOption] -> IO (Either JetStreamError ConsumerInfo)
orderedInfo state requestOptions = do
currentName <- readTVarIO (orderedStateCurrentName state)
case currentName of
Nothing ->
pure (Left JetStreamNoReply)
Just consumerName ->
consumerInfo
(orderedStateConsumers state)
(orderedStateStream state)
consumerName
requestOptions
fetchOrderedMessages :: OrderedState -> [FetchOption] -> [JetStreamRequestOption] -> IO (Either JetStreamError PullResponse)
fetchOrderedMessages state options requestOptions = do
resetResult <- resetOrderedConsumer state requestOptions
case resetResult of
Left err ->
pure (Left err)
Right info -> do
responseResult <- fetchMessages
(orderedStateContext state)
(orderedStateStream state)
(consumerInfoName info)
(pullRequest options)
requestOptions
case responseResult of
Left err ->
pure (Left err)
Right response ->
case orderedResponseNextSequence response of
Left err ->
pure (Left err)
Right Nothing ->
pure (Right response)
Right (Just nextSequence) -> do
atomically $
writeTVar (orderedStateNextSequence state) (Just nextSequence)
pure (Right response)
resetOrderedConsumer :: OrderedState -> [JetStreamRequestOption] -> IO (Either JetStreamError ConsumerInfo)
resetOrderedConsumer state requestOptions = do
stopped <- readTVarIO (orderedStateStopped state)
if stopped
then pure (Left JetStreamNoReply)
else do
previous <- readTVarIO (orderedStateCurrentName state)
case previous of
Nothing ->
pure ()
Just consumerName ->
void $
deleteConsumer
(orderedStateConsumers state)
(orderedStateStream state)
consumerName
requestOptions
(serial, nextSequence) <- atomically $ do
serial <- succ <$> readTVar (orderedStateSerial state)
writeTVar (orderedStateSerial state) serial
nextSequence <- readTVar (orderedStateNextSequence state)
pure (serial, nextSequence)
let consumerName = orderedConsumerName (orderedStateNamePrefix state) serial
options = orderedConsumerOptions (orderedStateConfig state) nextSequence
result <- putConsumer
(orderedStateConsumers state)
(orderedStateStream state)
ConsumerCreateOrUpdate
(NamedConsumer consumerName)
PullConsumer
options
requestOptions
case result of
Left err ->
pure (Left err)
Right info -> do
atomically $
writeTVar (orderedStateCurrentName state) (Just consumerName)
pure (Right info)
stopOrdered :: OrderedState -> [JetStreamRequestOption] -> IO (Either JetStreamError ())
stopOrdered state requestOptions = do
atomically $
writeTVar (orderedStateStopped state) True
currentName <- readTVarIO (orderedStateCurrentName state)
case currentName of
Nothing ->
pure (Right ())
Just consumerName ->
void <$>
deleteConsumer
(orderedStateConsumers state)
(orderedStateStream state)
consumerName
requestOptions
orderedConsumerOptions
:: OrderedConsumerConfig
-> Maybe Word64
-> [ConsumerConfigOption]
orderedConsumerOptions config nextSequence =
catMaybes
[ Just (withConsumerAckPolicy AckNone)
, Just (withConsumerDeliverPolicy deliverPolicy)
, Just (withConsumerMaxDeliver (-1))
, Just (withConsumerMaxWaiting 512)
, Just (withConsumerInactiveThreshold inactiveThreshold)
, Just (withConsumerReplicas 1)
, Just (withConsumerMemoryStorage True)
, withConsumerFilter <$> orderedConsumerFilter config
, withConsumerReplayPolicy <$> orderedConsumerReplayPolicy config
, withConsumerHeadersOnly <$> orderedConsumerHeadersOnly config
]
where
deliverPolicy =
case nextSequence of
Nothing ->
orderedConsumerDeliverPolicy config
Just sequenceNumber ->
DeliverByStartSequence sequenceNumber
inactiveThreshold =
fromMaybe 300 (orderedConsumerInactiveThreshold config)
orderedResponseNextSequence :: PullResponse -> Either JetStreamError (Maybe Word64)
orderedResponseNextSequence response =
case reverse (pullResponseMessages response) of
[] ->
Right Nothing
lastMessage:_ ->
case messageMetadata lastMessage of
Nothing ->
Left (JetStreamDecodeError "missing JetStream message metadata")
Just metadata ->
Right (Just (messageMetadataStreamSequence metadata + 1))
orderedNamePrefix :: JetStreamContext -> OrderedConsumerConfig -> IO ConsumerName
orderedNamePrefix context config =
case orderedConsumerNamePrefix config of
Just namePrefix ->
pure namePrefix
Nothing -> do
inbox <- Nats.newInbox (contextClient context)
pure (BS.append (BC.pack "ORDERED_") (sanitizeConsumerName inbox))
orderedConsumerName :: ConsumerName -> Int -> ConsumerName
orderedConsumerName namePrefix serial =
BS.concat [namePrefix, BC.pack "_", BC.pack (show serial)]
sanitizeConsumerName :: Subject -> ConsumerName
sanitizeConsumerName =
BC.map replace
where
replace char
| isAlphaNum char = char
| otherwise = '_'
-- | Fetch messages for a pull consumer.
fetchMessages :: JetStreamContext -> StreamName -> ConsumerName -> PullRequest -> [JetStreamRequestOption] -> IO (Either JetStreamError PullResponse)
fetchMessages context stream consumer request requestOptions
| pullRequestBatch request <= 0 = pure (Right (PullResponse [] Nothing))
| otherwise = do
responseQueue <- newTQueueIO
inbox <- Nats.newInbox natsClient
bracket
(Nats.subscribe natsClient inbox [] (atomically . writeTQueue responseQueue))
(\case
Left _ -> pure ()
Right handle -> void (Nats.unsubscribe natsClient handle []))
(\case
Left err ->
pure (Left (JetStreamNatsError err))
Right _ -> do
published <- Nats.publish natsClient requestSubject
(pullRequestPayload (pullRequestBatch request) request)
[Nats.withReplyTo inbox]
case published of
Left err ->
pure (Left (JetStreamNatsError err))
Right () ->
collectResponses waitMicros responseQueue (pullRequestBatch request) [])
where
natsClient = contextClient context
requestSubject = Subject.consumerNextSubject context stream consumer
waitMicros = min
(responseTimeoutMicros request)
(requestTimeoutMicros context requestOptions)
ackMessage :: Nats.Client -> Message -> IO (Either JetStreamError ())
ackMessage natsClient = publishAck natsClient Ack
nakMessage :: Nats.Client -> Message -> IO (Either JetStreamError ())
nakMessage natsClient = publishAck natsClient Nak
inProgressMessage :: Nats.Client -> Message -> IO (Either JetStreamError ())
inProgressMessage natsClient = publishAck natsClient InProgress
termMessage :: Nats.Client -> Message -> IO (Either JetStreamError ())
termMessage natsClient = publishAck natsClient Term
data PullResult = PullResultMessage Message
| PullResultStatus PullStatus
| PullResultTimeout
collectResponses :: Int -> TQueue Nats.MsgView -> Int -> [Message] -> IO (Either JetStreamError PullResponse)
collectResponses _ _ 0 messages =
pure (Right (PullResponse (reverse messages) Nothing))
collectResponses waitMicros responseQueue remaining messages = do
response <- timeout waitMicros (atomically (readTQueue responseQueue))
case classifyPullResult response of
PullResultMessage message ->
collectResponses waitMicros responseQueue (remaining - 1) (message:messages)
PullResultStatus status ->
pure (Right (PullResponse (reverse messages) (Just status)))
PullResultTimeout ->
pure (Left JetStreamTimeout)
classifyPullResult :: Maybe Nats.MsgView -> PullResult
classifyPullResult Nothing = PullResultTimeout
classifyPullResult (Just msgView) =
case messageStatus message of
Nothing -> PullResultMessage message
Just status -> PullResultStatus status
where
message = fromMsgView msgView
fromMsgView :: Nats.MsgView -> Message
fromMsgView msgView =
Message
{ messageSubject = Nats.subject msgView
, messagePayload = Nats.payload msgView
, messageHeaders = Nats.headers msgView
, messageReplyTo = Nats.replyTo msgView
, messageStatus = classifyStatusHeaders (Nats.headers msgView)
}
publishAck :: Nats.Client -> AckVerb -> Message -> IO (Either JetStreamError ())
publishAck natsClient verb message =
case messageReplyTo message of
Nothing ->
pure (Left JetStreamNoReply)
Just reply ->
mapNatsResult <$> Nats.publish natsClient reply (ackPayload verb) []
mapNatsResult :: Either Nats.NatsError () -> Either JetStreamError ()
mapNatsResult =
either (Left . JetStreamNatsError) Right
responseTimeoutMicros :: PullRequest -> Int
responseTimeoutMicros request =
case pullRequestWait request of
FetchNoWaitMicros timeoutMicros ->
max 1 timeoutMicros
FetchExpiresMicros timeoutMicros
| timeoutMicros <= 0 -> 1
| otherwise -> timeoutMicros + 100000