packages feed

hs-asapo-0.9.2: lib/Asapo/Either/Producer.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- |
-- Description : High-level interface for all producer-related functions
--
-- To implement an ASAP:O producer, you should only need this interface.
-- It exposes no memory-management functions (like free) or pointers, and
-- is thus safe to use.
module Asapo.Either.Producer
  ( Endpoint (..),
    ProcessingThreads (..),
    RequestHandlerType (..),
    Error (..),
    Metadata (..),
    SourceCredentials,
    DeletionFlags (..),
    Producer,
    LogLevel (..),
    FileName (..),
    DatasetSubstream (..),
    DatasetSize (..),
    VersionInfo (..),
    UpsertMode (..),
    MetadataIngestMode (..),
    AutoIdFlag (..),
    TransferFlag (..),
    StorageFlag (..),
    RequestResponse (..),
    Opcode (..),
    GenericRequestHeader (..),
    getRequestsQueueSize,
    getRequestsQueueVolumeMb,
    setRequestsQueueLimits,
    checkError,
    checkErrorWithGivenHandle,
    createProducer,
    freeProducer,
    withProducer,
    enableLocalLog,
    waitRequestsFinished,
    getVersionInfo,
    getStreamInfo,
    getStreamMeta,
    getBeamtimeMeta,
    deleteStream,
    getLastStream,
    send,
    sendFile,
    sendStreamFinishedFlag,
    sendBeamtimeMetadata,
    sendStreamMetadata,
    setLogLevel,
    enableRemoteLog,
    setCredentials,
  )
where

import Asapo.Either.Common (MessageId (MessageId), SourceCredentials, StreamInfo, StreamName (StreamName), nominalDiffToMillis, peekCStringText, retrieveStreamInfoFromC, stringHandleToText, stringHandleToTextUnsafe, withCStringNToText, withConstText, withCredentials, withPtr, withText)
import Asapo.Raw.Common (AsapoErrorHandle, AsapoStreamInfoHandle, AsapoStringHandle, asapo_error_explain, asapo_free_error_handle, asapo_free_stream_info_handle, asapo_free_string_handle, asapo_is_error, asapo_new_error_handle, asapo_new_string_handle)
import Asapo.Raw.Producer
  ( AsapoGenericRequestHeader (AsapoGenericRequestHeader),
    AsapoLogLevel,
    AsapoMessageHeaderHandle,
    AsapoOpcode,
    AsapoProducerHandle,
    AsapoRequestCallbackPayloadHandle,
    asapoLogLevelDebug,
    asapoLogLevelError,
    asapoLogLevelInfo,
    asapoLogLevelNone,
    asapoLogLevelWarning,
    asapo_create_message_header,
    asapo_create_producer,
    asapo_free_message_header_handle,
    asapo_free_producer_handle,
    asapo_producer_delete_stream,
    asapo_producer_enable_local_log,
    asapo_producer_enable_remote_log,
    asapo_producer_get_beamtime_meta,
    asapo_producer_get_last_stream,
    asapo_producer_get_requests_queue_size,
    asapo_producer_get_requests_queue_volume_mb,
    asapo_producer_get_stream_info,
    asapo_producer_get_stream_meta,
    asapo_producer_get_version_info,
    asapo_producer_send,
    asapo_producer_send_beamtime_metadata,
    asapo_producer_send_file,
    asapo_producer_send_stream_finished_flag,
    asapo_producer_send_stream_metadata,
    asapo_producer_set_credentials,
    asapo_producer_set_log_level,
    asapo_producer_set_requests_queue_limits,
    asapo_producer_wait_requests_finished,
    asapo_request_callback_payload_get_original_header,
    asapo_request_callback_payload_get_response,
    createRequestCallback,
    kFilesystem,
    kInsert,
    kOpcodeAuthorize,
    kOpcodeCount,
    kOpcodeDeleteStream,
    kOpcodeGetBufferData,
    kOpcodeGetMeta,
    kOpcodeLastStream,
    kOpcodeStreamInfo,
    kOpcodeTransferData,
    kOpcodeTransferDatasetData,
    kOpcodeTransferMetaData,
    kOpcodeUnknownOp,
    kReplace,
    kStoreInDatabase,
    kStoreInFilesystem,
    kTcp,
    kTransferData,
    kTransferMetaDataOnly,
    kUpdate,
  )
import Control.Applicative (Applicative (pure))
import Control.Exception (bracket)
import Data.Bits ((.|.))
import Data.Bool (Bool)
import Data.ByteString qualified as BS
import Data.ByteString.Internal qualified as BSI
import Data.Either (Either (Left, Right))
import Data.Eq (Eq ((==)))
import Data.Foldable (Foldable (elem))
import Data.Functor ((<$>))
import Data.Int (Int)
import Data.Maybe (Maybe (Just, Nothing))
import Data.Ord ((>))
import Data.Text (Text)
import Data.Time (NominalDiffTime)
import Data.Word (Word64, Word8)
import Foreign (Storable (peek), alloca, castPtr)
import Foreign.C.ConstPtr (ConstPtr (unConstPtr))
import Foreign.ForeignPtr (ForeignPtr, withForeignPtr)
import Foreign.Ptr (Ptr)
import System.IO (IO)
import Text.Show (Show)
import Prelude (fromIntegral)

-- | Wrapper around an ASAP:O producer error. Note that there is only
--  an error "explanation" here, no error code, since the C interface
--  does not expose this.
newtype Error = Error Text deriving (Show)

-- | Wrapper around an ASAP:O producer endpoint (usually something like "host:port")
newtype Endpoint = Endpoint Text

-- | Wrapper around the number of ASAP:O processing threads (simply to make call signatures mor readable)
newtype ProcessingThreads = ProcessingThreads Int

-- | This has no documentation in ASAP:O yet
data RequestHandlerType = TcpHandler | FilesystemHandler

-- | Opaque wrapper around an ASAP:O producer
newtype Producer = Producer AsapoProducerHandle

-- | Internal function to check and return either an error (if it's
-- present behind the given handle) or a "result" of some function
-- that produced the error handle
checkErrorWithGivenHandle :: AsapoErrorHandle -> b -> IO (Either Error b)
checkErrorWithGivenHandle errorHandle result = do
  isError <- asapo_is_error errorHandle
  if isError > 0
    then do
      let explanationLength = 1024
      explanation <- withCStringNToText explanationLength \explanationPtr ->
        asapo_error_explain
          errorHandle
          explanationPtr
          (fromIntegral explanationLength)
      pure (Left (Error explanation))
    else pure (Right result)

withErrorHandle :: (AsapoErrorHandle -> IO c) -> IO c
withErrorHandle = bracket asapo_new_error_handle asapo_free_error_handle

-- | Helper function since most ASAP:O functions receive a pointer to
-- an error handle as the last argument, so error checking becomes
-- "abstractable"
checkError :: (Ptr AsapoErrorHandle -> IO b) -> IO (Either Error b)
checkError f = do
  withErrorHandle \errorHandle -> do
    (errorHandlePtr, result) <- withPtr errorHandle f
    checkErrorWithGivenHandle errorHandlePtr result

-- | Create a producer and return a handle. The caller must call 'freeProducer' after finishing using the handle. See 'withProducer' for a safer version
createProducer :: Endpoint -> ProcessingThreads -> RequestHandlerType -> SourceCredentials -> NominalDiffTime -> IO (Either Error Producer)
createProducer (Endpoint endpoint) (ProcessingThreads processingThreads) handlerType sourceCredentials timeout = do
  withCredentials sourceCredentials \credentials' ->
    let convertHandlerType TcpHandler = kTcp
        convertHandlerType FilesystemHandler = kFilesystem
     in do
          withText endpoint \endpoint' -> do
            -- first <$> to go into IO, second <$> to go into the Either
            (Producer <$>)
              <$> checkError
                ( asapo_create_producer
                    endpoint'
                    (fromIntegral processingThreads)
                    (convertHandlerType handlerType)
                    credentials'
                    (nominalDiffToMillis timeout)
                )

-- | Free the producer handle. This function is only useful in tandem with 'createProducer'
freeProducer :: Producer -> IO ()
freeProducer (Producer p) = asapo_free_producer_handle p

-- | Create a producer and do something with it. This is the main entrypoint into the producer
withProducer ::
  forall a.
  Endpoint ->
  ProcessingThreads ->
  RequestHandlerType ->
  SourceCredentials ->
  -- | timeout
  NominalDiffTime ->
  (Error -> IO a) ->
  (Producer -> IO a) ->
  IO a
withProducer endpoint processingThreads handlerType sourceCredentials timeout onError onSuccess = bracket (createProducer endpoint processingThreads handlerType sourceCredentials timeout) freeProducer' handle
  where
    freeProducer' :: Either Error Producer -> IO ()
    freeProducer' (Left _) = pure ()
    freeProducer' (Right producerHandle) = freeProducer producerHandle
    handle :: Either Error Producer -> IO a
    handle (Left e) = onError e
    handle (Right v) = onSuccess v

withStringHandle :: (AsapoStringHandle -> IO c) -> IO c
withStringHandle = bracket asapo_new_string_handle asapo_free_string_handle

data VersionInfo = VersionInfo
  { versionClient :: Text,
    versionServer :: Text,
    versionSupported :: Bool
  }
  deriving (Show)

-- | Retrieve producer version info
getVersionInfo :: Producer -> IO (Either Error VersionInfo)
getVersionInfo (Producer producerHandle) =
  withStringHandle \clientInfo -> withStringHandle \serverInfo -> alloca \supportedPtr -> do
    result <- checkError (asapo_producer_get_version_info producerHandle clientInfo serverInfo supportedPtr)
    case result of
      Left e -> pure (Left e)
      -- The return value is a CInt which is unnecessary probably?
      Right _integerReturnCode -> do
        supported <- peek supportedPtr
        clientInfo' <- stringHandleToTextUnsafe clientInfo
        serverInfo' <- stringHandleToTextUnsafe serverInfo
        pure (Right (VersionInfo clientInfo' serverInfo' (supported > 0)))

-- | Retrieve info for a single stream
getStreamInfo ::
  Producer ->
  StreamName ->
  -- | Timeout
  NominalDiffTime ->
  IO (Either Error StreamInfo)
getStreamInfo (Producer producer) (StreamName stream) timeout = bracket init destroy f
  where
    init :: IO (Either Error AsapoStreamInfoHandle)
    init = withConstText stream \streamC -> checkError (asapo_producer_get_stream_info producer streamC (nominalDiffToMillis timeout))
    destroy :: Either Error AsapoStreamInfoHandle -> IO ()
    destroy (Right handle) = asapo_free_stream_info_handle handle
    destroy _ = pure ()
    f :: Either Error AsapoStreamInfoHandle -> IO (Either Error StreamInfo)
    f (Left e) = pure (Left e)
    f (Right streamInfoHandle) = Right <$> retrieveStreamInfoFromC streamInfoHandle

-- | Retrieve info for the latest stream
getLastStream ::
  Producer ->
  -- | Timeout
  NominalDiffTime ->
  IO (Either Error StreamInfo)
getLastStream (Producer producer) timeout = bracket init destroy f
  where
    init :: IO (Either Error AsapoStreamInfoHandle)
    init = checkError (asapo_producer_get_last_stream producer (nominalDiffToMillis timeout))
    destroy :: Either Error AsapoStreamInfoHandle -> IO ()
    destroy (Right handle) = asapo_free_stream_info_handle handle
    destroy _ = pure ()
    f :: Either Error AsapoStreamInfoHandle -> IO (Either Error StreamInfo)
    f (Left e) = pure (Left e)
    f (Right streamInfoHandle) = Right <$> retrieveStreamInfoFromC streamInfoHandle

-- | Retrieve metadata for the given stream (which might be missing, in which case @Nothing@ is returned)
getStreamMeta ::
  Producer ->
  StreamName ->
  -- | timeout
  NominalDiffTime ->
  IO (Either Error (Maybe Text))
getStreamMeta (Producer producer) (StreamName stream) timeout = bracket init destroy f
  where
    init :: IO (Either Error AsapoStringHandle)
    init = withConstText stream \streamC -> checkError (asapo_producer_get_stream_meta producer streamC (nominalDiffToMillis timeout))
    destroy :: Either Error AsapoStringHandle -> IO ()
    destroy (Right handle) = asapo_free_string_handle handle
    destroy _ = pure ()
    f :: Either Error AsapoStringHandle -> IO (Either Error (Maybe Text))
    f (Left e) = pure (Left e)
    f (Right string) = Right <$> stringHandleToText string

-- | Retrieve metadata for the given stream (which might be missing, in which case @Nothing@ is returned)
getBeamtimeMeta ::
  Producer ->
  -- | timeout
  NominalDiffTime ->
  IO (Either Error (Maybe Text))
getBeamtimeMeta (Producer producer) timeout = bracket init destroy f
  where
    init :: IO (Either Error AsapoStringHandle)
    init = checkError (asapo_producer_get_beamtime_meta producer (nominalDiffToMillis timeout))
    destroy :: Either Error AsapoStringHandle -> IO ()
    destroy (Right handle) = asapo_free_string_handle handle
    destroy _ = pure ()
    f :: Either Error AsapoStringHandle -> IO (Either Error (Maybe Text))
    f (Left e) = pure (Left e)
    f (Right string) = Right <$> stringHandleToText string

data DeletionFlags
  = -- | Delete metadata also
    DeleteMeta
  | -- | Don't throw an error if the data doesn't exist anyways
    DeleteErrorOnNotExist
  deriving (Eq)

-- | Delete the given stream
deleteStream ::
  Producer ->
  StreamName ->
  -- | timeout
  NominalDiffTime ->
  [DeletionFlags] ->
  IO (Either Error Int)
deleteStream (Producer producer) (StreamName stream) timeout deletionFlags = do
  result <- withConstText stream \streamC ->
    checkError
      ( asapo_producer_delete_stream
          producer
          streamC
          (nominalDiffToMillis timeout)
          (if DeleteMeta `elem` deletionFlags then 1 else 0)
          (if DeleteErrorOnNotExist `elem` deletionFlags then 1 else 0)
      )
  pure (fromIntegral <$> result)

-- | Wrapper around file name (dubious to use @Text@ here, but fine for now)
newtype FileName = FileName Text

-- | Wrapper around metadata to be produced
newtype Metadata = Metadata Text

-- | Wrapper around the substream to use
newtype DatasetSubstream = DatasetSubstream Int

-- | Wrapper around the dataset size to use
newtype DatasetSize = DatasetSize Int

-- | Anti-boolean-blindness for the "auto id" flag in the message header
data AutoIdFlag = UseAutoId | NoAutoId deriving (Eq)

-- | Which data to transfer
data TransferFlag = DataAndMetadata | MetadataOnly

-- | Where to store the data
data StorageFlag = Filesystem | Database | FilesystemAndDatabase

convertSendFlags :: TransferFlag -> StorageFlag -> Word64
convertSendFlags tf sf = convertTransferFlag tf .|. convertStorageFlag sf
  where
    convertTransferFlag :: TransferFlag -> Word64
    convertTransferFlag DataAndMetadata = fromIntegral kTransferData
    convertTransferFlag MetadataOnly = fromIntegral kTransferMetaDataOnly
    convertStorageFlag :: StorageFlag -> Word64
    convertStorageFlag Filesystem = fromIntegral kStoreInFilesystem
    convertStorageFlag Database = fromIntegral kStoreInDatabase
    convertStorageFlag FilesystemAndDatabase = fromIntegral kStoreInDatabase .|. fromIntegral kStoreInFilesystem

-- | Internal function to create a message header handle, which gets used to send data.
withMessageHeaderHandle ::
  MessageId ->
  FileName ->
  Metadata ->
  DatasetSubstream ->
  DatasetSize ->
  AutoIdFlag ->
  Int ->
  (AsapoMessageHeaderHandle -> IO b) ->
  IO b
withMessageHeaderHandle
  (MessageId messageId)
  (FileName fileName)
  (Metadata metadata)
  (DatasetSubstream datasetSubstream)
  (DatasetSize datasetSize)
  autoIdFlag
  dataSize = bracket init destroy
    where
      init :: IO AsapoMessageHeaderHandle
      init = withConstText fileName \fileNameC -> withConstText metadata \metadataC ->
        asapo_create_message_header
          messageId
          (fromIntegral dataSize)
          fileNameC
          metadataC
          (fromIntegral datasetSubstream)
          (fromIntegral datasetSize)
          (if autoIdFlag == UseAutoId then 1 else 0)
      destroy = asapo_free_message_header_handle

data Opcode
  = OpcodeUnknownOp
  | OpcodeTransferData
  | OpcodeTransferDatasetData
  | OpcodeStreamInfo
  | OpcodeLastStream
  | OpcodeGetBufferData
  | OpcodeAuthorize
  | OpcodeTransferMetaData
  | OpcodeDeleteStream
  | OpcodeGetMeta
  | OpcodeCount
  | OpcodePersistStream

convertOpcode :: AsapoOpcode -> Opcode
convertOpcode x | x == kOpcodeUnknownOp = OpcodeUnknownOp
convertOpcode x | x == kOpcodeTransferData = OpcodeTransferData
convertOpcode x | x == kOpcodeTransferDatasetData = OpcodeTransferDatasetData
convertOpcode x | x == kOpcodeStreamInfo = OpcodeStreamInfo
convertOpcode x | x == kOpcodeLastStream = OpcodeLastStream
convertOpcode x | x == kOpcodeGetBufferData = OpcodeGetBufferData
convertOpcode x | x == kOpcodeAuthorize = OpcodeAuthorize
convertOpcode x | x == kOpcodeTransferMetaData = OpcodeTransferMetaData
convertOpcode x | x == kOpcodeDeleteStream = OpcodeDeleteStream
convertOpcode x | x == kOpcodeGetMeta = OpcodeGetMeta
convertOpcode x | x == kOpcodeCount = OpcodeCount
convertOpcode _ = OpcodePersistStream

-- | Information about the send request, to be used in the ASAP:O send callback
data GenericRequestHeader = GenericRequestHeader
  { genericRequestHeaderOpCode :: Opcode,
    genericRequestHeaderDataId :: Int,
    genericRequestHeaderDataSize :: Int,
    genericRequestHeaderMetaSize :: Int,
    genericRequestHeaderCustomData :: [Int],
    genericRequestHeaderMessage :: BS.ByteString,
    genericRequestHeaderStream :: Text,
    genericRequestHeaderApiVersion :: Text
  }

convertRequestHeader :: AsapoGenericRequestHeader -> IO GenericRequestHeader
convertRequestHeader (AsapoGenericRequestHeader opcode dataId dataSize metaSize customData message stream apiVersion) = do
  streamText <- peekCStringText stream
  apiVersionText <- peekCStringText apiVersion
  messageAsBs <- BS.packCStringLen (message, fromIntegral dataSize)
  let customData' :: [Int]
      customData' = fromIntegral <$> customData
  pure
    ( GenericRequestHeader
        (convertOpcode opcode)
        (fromIntegral dataId)
        (fromIntegral dataSize)
        (fromIntegral metaSize)
        customData'
        messageAsBs
        streamText
        apiVersionText
    )

-- | Information about the request and its response, to be used in the ASAP:O send callback
data RequestResponse = RequestResponse
  { responsePayload :: Text,
    responseOriginalRequestHeader :: GenericRequestHeader,
    responseError :: Maybe Error
  }

sendRequestCallback ::
  (RequestResponse -> IO ()) ->
  Maybe (ForeignPtr Word8) ->
  Ptr () ->
  AsapoRequestCallbackPayloadHandle ->
  AsapoErrorHandle ->
  IO ()
sendRequestCallback simpleCallback _dataAsForeignPtr _data payloadHandle errorHandle = do
  payloadText <- bracket (asapo_request_callback_payload_get_response payloadHandle) asapo_free_string_handle stringHandleToTextUnsafe
  originalHeaderCPtr <- asapo_request_callback_payload_get_original_header payloadHandle
  originalHeaderC <- peek (unConstPtr originalHeaderCPtr)
  originalHeader <- convertRequestHeader originalHeaderC
  errorHandle' <- checkErrorWithGivenHandle errorHandle ()
  case errorHandle' of
    Left e -> simpleCallback (RequestResponse payloadText originalHeader (Just e))
    Right _ -> simpleCallback (RequestResponse payloadText originalHeader Nothing)

toJustForeignPtr :: (a, b, c) -> a
toJustForeignPtr (p, _offset, _length) = p

-- | Send a message containing raw data. Due to newtype and enum usage, all parameter should be self-explanatory
send ::
  Producer ->
  MessageId ->
  FileName ->
  Metadata ->
  DatasetSubstream ->
  DatasetSize ->
  AutoIdFlag ->
  BS.ByteString ->
  TransferFlag ->
  StorageFlag ->
  StreamName ->
  (RequestResponse -> IO ()) ->
  IO (Either Error Int)
send (Producer producer) messageId fileName metadata datasetSubstream datasetSize autoIdFlag data' transferFlag storageFlag (StreamName stream) callback =
  withMessageHeaderHandle
    messageId
    fileName
    metadata
    datasetSubstream
    datasetSize
    autoIdFlag
    (BS.length data')
    \messageHeaderHandle -> do
      withConstText stream \streamC -> do
        let word8Ptr :: ForeignPtr Word8
            word8Ptr = toJustForeignPtr (BSI.toForeignPtr data')
        requestCallback <-
          createRequestCallback
            ( sendRequestCallback
                callback
                -- We pass this ptr to the callback to keep the GC from collecting it
                (Just word8Ptr)
            )
        withForeignPtr word8Ptr \dataPtr ->
          ( fromIntegral
              <$>
          )
            <$> checkError
              ( asapo_producer_send
                  producer
                  messageHeaderHandle
                  (castPtr dataPtr)
                  (convertSendFlags transferFlag storageFlag)
                  streamC
                  requestCallback
              )

-- | Send a message containing a file. Due to newtype and enum usage, all parameter should be self-explanatory
sendFile ::
  Producer ->
  MessageId ->
  -- | File name to put into the message header
  FileName ->
  Metadata ->
  DatasetSubstream ->
  DatasetSize ->
  AutoIdFlag ->
  -- | Size
  Int ->
  -- | File to actually send
  FileName ->
  TransferFlag ->
  StorageFlag ->
  StreamName ->
  (RequestResponse -> IO ()) ->
  IO (Either Error Int)
sendFile (Producer producer) messageId fileName meta datasetSubstream datasetSize autoIdFlag size (FileName fileNameToSend) transferFlag storageFlag (StreamName stream) callback =
  withMessageHeaderHandle
    messageId
    fileName
    meta
    datasetSubstream
    datasetSize
    autoIdFlag
    size
    \messageHeaderHandle ->
      withConstText fileNameToSend \fileNameToSendC -> withConstText stream \streamC -> do
        requestCallback <- createRequestCallback (sendRequestCallback callback Nothing)
        ( fromIntegral
            <$>
          )
          <$> checkError
            ( asapo_producer_send_file
                producer
                messageHeaderHandle
                fileNameToSendC
                (convertSendFlags transferFlag storageFlag)
                streamC
                requestCallback
            )

-- | As the title says, send the "stream finished" flag
sendStreamFinishedFlag :: Producer -> StreamName -> MessageId -> StreamName -> (RequestResponse -> IO ()) -> IO (Either Error Int)
sendStreamFinishedFlag (Producer producer) (StreamName stream) (MessageId lastId) (StreamName nextStream) callback = do
  requestCallback <- createRequestCallback (sendRequestCallback callback Nothing)
  withConstText stream \streamC -> withConstText nextStream \nextStreamC ->
    (fromIntegral <$>)
      <$> checkError
        ( asapo_producer_send_stream_finished_flag
            producer
            streamC
            lastId
            nextStreamC
            requestCallback
        )

data MetadataIngestMode = Insert | Replace | Update

data UpsertMode = UseUpsert | NoUpsert

-- | Send or extend beamtime metadata
sendBeamtimeMetadata :: Producer -> Metadata -> MetadataIngestMode -> UpsertMode -> (RequestResponse -> IO ()) -> IO (Either Error Int)
sendBeamtimeMetadata (Producer producer) (Metadata metadata) ingestMode upsertMode callback = do
  requestCallback <- createRequestCallback (sendRequestCallback callback Nothing)
  (fromIntegral <$>)
    <$> withConstText metadata \metadataC ->
      checkError
        ( asapo_producer_send_beamtime_metadata
            producer
            metadataC
            ( case ingestMode of
                Insert -> kInsert
                Replace -> kReplace
                Update -> kUpdate
            )
            ( case upsertMode of
                UseUpsert -> 1
                _ -> 0
            )
            requestCallback
        )

-- | Send or extend stream metadata
sendStreamMetadata :: Producer -> Metadata -> MetadataIngestMode -> UpsertMode -> StreamName -> (RequestResponse -> IO ()) -> IO (Either Error Int)
sendStreamMetadata (Producer producer) (Metadata metadata) ingestMode upsertMode (StreamName stream) callback = do
  requestCallback <- createRequestCallback (sendRequestCallback callback Nothing)
  (fromIntegral <$>)
    <$> withConstText metadata \metadataC -> withConstText stream \streamC ->
      checkError
        ( asapo_producer_send_stream_metadata
            producer
            metadataC
            ( case ingestMode of
                Insert -> kInsert
                Replace -> kReplace
                Update -> kUpdate
            )
            ( case upsertMode of
                UseUpsert -> 1
                _ -> 0
            )
            streamC
            requestCallback
        )

data LogLevel
  = LogNone
  | LogError
  | LogInfo
  | LogDebug
  | LogWarning
  deriving (Eq)

convertLogLevel :: LogLevel -> AsapoLogLevel
convertLogLevel x | x == LogNone = asapoLogLevelNone
convertLogLevel x | x == LogError = asapoLogLevelError
convertLogLevel x | x == LogInfo = asapoLogLevelInfo
convertLogLevel x | x == LogDebug = asapoLogLevelDebug
convertLogLevel _ = asapoLogLevelWarning

-- | Set the log level
setLogLevel :: Producer -> LogLevel -> IO ()
setLogLevel (Producer producer) logLevel = asapo_producer_set_log_level producer (convertLogLevel logLevel)

-- | Enable/Disable logging to stdout
enableLocalLog :: Producer -> Bool -> IO ()
enableLocalLog (Producer producer) enable = asapo_producer_enable_local_log producer (if enable then 1 else 0)

-- | Enable/Disable logging to the central server
enableRemoteLog :: Producer -> Bool -> IO ()
enableRemoteLog (Producer producer) enable = asapo_producer_enable_remote_log producer (if enable then 1 else 0)

-- | Set a different set of credentials
setCredentials :: Producer -> SourceCredentials -> IO (Either Error Int)
setCredentials (Producer producer) credentials = withCredentials credentials \credentialsHandle ->
  (fromIntegral <$>) <$> checkError (asapo_producer_set_credentials producer credentialsHandle)

-- | Get current size of the requests queue (number of requests pending/being processed)
getRequestsQueueSize :: Producer -> IO Int
getRequestsQueueSize (Producer producer) = fromIntegral <$> asapo_producer_get_requests_queue_size producer

-- | Get current volume of the requests queue (total memory of occupied by pending/being processed requests)
getRequestsQueueVolumeMb :: Producer -> IO Int
getRequestsQueueVolumeMb (Producer producer) = fromIntegral <$> asapo_producer_get_requests_queue_volume_mb producer

-- | Set maximum size of the requests queue
setRequestsQueueLimits ::
  Producer ->
  -- | Size (0 for unlimited)
  Int ->
  -- | Volume (in MiB; 0 for unlimited)
  Int ->
  IO ()
setRequestsQueueLimits (Producer producer) size volume =
  asapo_producer_set_requests_queue_limits producer (fromIntegral size) (fromIntegral volume)

-- | Wait for all outstanding requests to finish
waitRequestsFinished :: Producer -> NominalDiffTime -> IO (Either Error Int)
waitRequestsFinished (Producer producer) timeout = do
  (fromIntegral <$>) <$> checkError (asapo_producer_wait_requests_finished producer (nominalDiffToMillis timeout))