packages feed

keiro-0.17.0.0: bench/ProducerIdentityBench.hs

-- | Compare the pre-plan-164 fresh-ID path with deterministic producer writes.
module ProducerIdentityBench (producerIdentityBenchmarks) where

import Control.Monad (forM, forM_)
import Data.Aeson qualified as Aeson
import Data.ByteString qualified as BS
import Data.IORef (IORef, atomicModifyIORef')
import Data.Time (UTCTime (..), secondsToDiffTime)
import Data.Time.Calendar (Day (ModifiedJulianDay))
import Data.UUID qualified as UUID
import Keiro.Integration.Event
import Keiro.Outbox
import Keiro.Prelude
import Kiroku.Store qualified as Store
import Kiroku.Store.Types (EventId (..), EventType (..), GlobalPosition (..), RecordedEvent (..), StreamId (..), StreamVersion (..))
import Test.Tasty.Bench (Benchmark, bcompareWithin, bench, bgroup, nf, nfIO)
import "hasql-transaction" Hasql.Transaction qualified as Tx

producerIdentityBenchmarks :: Store.KirokuStore -> IORef Int -> [Benchmark]
producerIdentityBenchmarks store sequenceRef =
  [ bgroup
      "producer-identity"
      [ bench "derive-v1" $ nf identityText (ProducerEventKey (EventId (UUID.fromWords 0 0 0 1)) 0),
        bench "legacy-fresh-1000" $ nfIO (run True False False),
        bcompareWithin 0 1.10 "$0 == \"All.producer-identity.legacy-fresh-1000\"" $ bench "deterministic-fresh-1000" $ nfIO (run True True False),
        bench "deterministic-fresh-and-replay-1000" $ nfIO (run True True True),
        bench "legacy-single-tx-per-event-1000" $ nfIO (run False False False),
        bcompareWithin 0 1.10 "$0 == \"All.producer-identity.legacy-single-tx-per-event-1000\"" $ bench "deterministic-single-tx-per-event-1000" $ nfIO (run False True False)
      ]
  ]
  where
    run batch deterministic replay = do
      generation <- atomicModifyIORef' sequenceRef (\n -> (n + 1, n + 1))
      result <- Store.runStoreIO store $ do
        Store.runTransaction (Tx.sql "TRUNCATE keiro.keiro_outbox")
        let recorded = [sourceEvent generation i | i <- [1 .. 1000]]
        if deterministic
          then do
            let actions = fmap (\event -> enqueueProducerEventTx producer event 0 draft) recorded
            if batch
              then do
                first <- Store.runTransaction (sequence actions)
                unless (all isInserted first) (error "producer benchmark failed to insert")
              else forM_ actions $ \action -> do
                outcome <- Store.runTransaction action
                unless (isInserted outcome) (error "producer benchmark failed to insert")
            when replay $ do
              second <- Store.runTransaction (sequence actions)
              unless (all isDuplicate second) (error "producer benchmark failed to replay")
          else do
            -- Match the old helper's preparation boundary: bulk preparation
            -- for bulk SQL, and preparation immediately before each individual
            -- transaction for the normal subscription path.
            let prepare event = do
                  oid <- freshOutboxId
                  envelope <- freshIntegrationEvent producer (draft & #sourceEventId ?~ event ^. #eventId & #sourceGlobalPosition ?~ event ^. #globalPosition)
                  pure (oid, envelope)
            if batch
              then do
                messages <- forM recorded prepare
                Store.runTransaction (traverse_ (uncurry enqueueIntegrationEventTx) messages)
              else forM_ recorded $ \event -> do
                message <- prepare event
                Store.runTransaction (uncurry enqueueIntegrationEventTx message)
      case result of
        Left err -> fail (show err)
        Right () -> pure ()
    isInserted ProducerInserted {} = True
    isInserted _ = False
    isDuplicate ProducerDuplicateIdentical {} = True
    isDuplicate _ = False

producer :: IntegrationProducer ()
producer = IntegrationProducer "bench-producer" "bench.outbox" "msg" (\_ _ -> Just draft)

draft :: IntegrationEventDraft
draft =
  IntegrationEventDraft
    { destination = "bench.outbox.events.v1",
      key = Just "key",
      eventType = "BenchEvent",
      schemaVersion = 1,
      contentType = ApplicationJson,
      schemaReference = Nothing,
      sourceEventId = Nothing,
      sourceGlobalPosition = Nothing,
      payloadBytes = BS.replicate 1024 65,
      occurredAt = UTCTime (ModifiedJulianDay 60000) (secondsToDiffTime 0),
      causationId = Nothing,
      correlationId = Nothing,
      traceContext = Nothing,
      attributes = Nothing
    }

sourceEvent :: Int -> Int -> RecordedEvent
sourceEvent generation i =
  RecordedEvent
    { eventId = EventId (UUID.fromWords 0 (fromIntegral generation) 0 (fromIntegral i)),
      eventType = EventType "BenchEvent",
      streamVersion = StreamVersion (fromIntegral i),
      globalPosition = GlobalPosition (fromIntegral i),
      originalStreamId = StreamId 1,
      originalVersion = StreamVersion (fromIntegral i),
      payload = Aeson.Null,
      metadata = Nothing,
      causationId = Nothing,
      correlationId = Nothing,
      createdAt = UTCTime (ModifiedJulianDay 60000) (secondsToDiffTime 0)
    }

-- Force both identifiers, including the UUID projection, in the pure benchmark.
identityText :: ProducerEventKey -> (Text, Text)
identityText key =
  let identity = deriveProducerIdentity producer key
   in (UUID.toText (unOutboxId (identity ^. #outboxId)), identity ^. #messageId)