packages feed

shibuya-pgmq-adapter 0.18.0.0 → 0.19.0.0

raw patch · 10 files changed

+376/−17 lines, 10 filesdep ~shibuya-corePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: shibuya-core

API changes (from Hackage documentation)

+ Shibuya.Adapter.Pgmq: LinkOnly :: TraceCorrelation
+ Shibuya.Adapter.Pgmq: ParentAndLink :: TraceCorrelation
+ Shibuya.Adapter.Pgmq: [telemetry] :: PgmqAdapterConfig -> !AdapterTelemetry
+ Shibuya.Adapter.Pgmq: data AdapterTelemetry
+ Shibuya.Adapter.Pgmq: data TraceCorrelation
+ Shibuya.Adapter.Pgmq: defaultAdapterTelemetry :: AdapterTelemetry
+ Shibuya.Adapter.Pgmq.Config: [telemetry] :: PgmqAdapterConfig -> !AdapterTelemetry
- Shibuya.Adapter.Pgmq: PgmqAdapterConfig :: !QueueName -> !Int32 -> !Int32 -> !PollingConfig -> !PollRetryConfig -> !PollRetryConfig -> !Maybe DeadLetterConfig -> !Maybe Int32 -> !Int64 -> !Maybe FifoConfig -> !Maybe PrefetchConfig -> PgmqAdapterConfig
+ Shibuya.Adapter.Pgmq: PgmqAdapterConfig :: !QueueName -> !Int32 -> !Int32 -> !PollingConfig -> !PollRetryConfig -> !PollRetryConfig -> !Maybe DeadLetterConfig -> !Maybe Int32 -> !Int64 -> !Maybe FifoConfig -> !Maybe PrefetchConfig -> !AdapterTelemetry -> PgmqAdapterConfig
- Shibuya.Adapter.Pgmq.Config: PgmqAdapterConfig :: !QueueName -> !Int32 -> !Int32 -> !PollingConfig -> !PollRetryConfig -> !PollRetryConfig -> !Maybe DeadLetterConfig -> !Maybe Int32 -> !Int64 -> !Maybe FifoConfig -> !Maybe PrefetchConfig -> PgmqAdapterConfig
+ Shibuya.Adapter.Pgmq.Config: PgmqAdapterConfig :: !QueueName -> !Int32 -> !Int32 -> !PollingConfig -> !PollRetryConfig -> !PollRetryConfig -> !Maybe DeadLetterConfig -> !Maybe Int32 -> !Int64 -> !Maybe FifoConfig -> !Maybe PrefetchConfig -> !AdapterTelemetry -> PgmqAdapterConfig

Files

CHANGELOG.md view
@@ -1,5 +1,40 @@ # Changelog +## 0.19.0.0 — 2026-11-14++### Breaking Changes++- Require [`shibuya-core`](mori://shinzui/shibuya/packages/shibuya-core) and+  `shibuya-metrics` `^>=0.13.0.0`. Core 0.13 renames processing spans from+  `<processor> process` to `process <processor>` and adds+  `messaging.operation.name = "process"`; update trace queries, dashboards and+  alerts that match the old span name.+- Add the strict field `telemetry :: AdapterTelemetry` to `PgmqAdapterConfig`.+  Configurations built from `defaultConfig` with record update need no change;+  a full `PgmqAdapterConfig { .. }` literal must set it, normally to+  `defaultAdapterTelemetry`.++### New Features++- Choose how a processing span relates to the span that sent the message with+  `telemetry = defaultAdapterTelemetry {traceCorrelation = LinkOnly}`. The+  default, `ParentAndLink`, keeps the processing span in the sender's trace as+  its child and now also links to the sender. `LinkOnly` starts each delivery+  in its own trace, linked to the sender, which suits delayed sends and long+  retry cycles. Without a usable `traceparent` neither adds a link. Re-export+  `AdapterTelemetry (traceCorrelation)`, `defaultAdapterTelemetry` and+  `TraceCorrelation (..)` from `Shibuya.Adapter.Pgmq`.+- Under `LinkOnly`, a dead-lettered message's `traceparent` names the+  consumer's processing span in its own trace; the sender's original header is+  still preserved under `x-shibuya-upstream-traceparent`. Dead-letter header+  handling itself is unchanged.++### Other Changes++- Add PostgreSQL-backed correlation tests covering both policies, redelivery+  and the dead-letter headers, and let the example consumer select `LinkOnly`+  with `OTEL_TRACE_LINK_ONLY=true`.+ ## 0.18.0.0 — 2026-10-27  ### Breaking Changes
shibuya-pgmq-adapter.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.12 name: shibuya-pgmq-adapter-version: 0.18.0.0+version: 0.19.0.0 synopsis: PGMQ adapter for the Shibuya queue processing framework description:   A Shibuya adapter that integrates with pgmq (PostgreSQL Message Queue)@@ -51,7 +51,7 @@     pgmq-core ^>=0.7.0.0,     pgmq-effectful ^>=0.7.0.0,     pgmq-hasql ^>=0.7.0.0,-    shibuya-core ^>=0.12.0.0,+    shibuya-core ^>=0.13.0.0,     stm ^>=2.5,     streamly ^>=0.11,     streamly-core ^>=0.3,@@ -101,6 +101,7 @@     Shibuya.Adapter.Pgmq.ConfigSpec     Shibuya.Adapter.Pgmq.Convert     Shibuya.Adapter.Pgmq.ConvertSpec+    Shibuya.Adapter.Pgmq.CorrelationSpec     Shibuya.Adapter.Pgmq.IntegrationSpec     Shibuya.Adapter.Pgmq.Internal     Shibuya.Adapter.Pgmq.InternalSpec@@ -129,7 +130,7 @@     pgmq-migration ^>=0.7.0.0,     quickcheck-instances ^>=0.3,     random,-    shibuya-core ^>=0.12.0.0,+    shibuya-core ^>=0.13.0.0,     shibuya-pgmq-adapter,     stm,     streamly ^>=0.11,
src/Shibuya/Adapter/Pgmq.hs view
@@ -88,6 +88,11 @@     FifoReadStrategy (..),     validateConfig, +    -- * Telemetry+    AdapterTelemetry (traceCorrelation),+    defaultAdapterTelemetry,+    TraceCorrelation (..),+     -- * Smart Constructors     directDeadLetter,     topicDeadLetter,@@ -146,7 +151,7 @@     routingKeyToText,     topicPatternToText,   )-import Shibuya.Adapter (Adapter (..))+import Shibuya.Adapter (Adapter (..), AdapterTelemetry (traceCorrelation), TraceCorrelation (..), defaultAdapterTelemetry) import Shibuya.Adapter.Pgmq.Config   ( DeadLetterConfig (..),     DeadLetterTarget (..),@@ -216,7 +221,8 @@           Adapter             { adapterName = "pgmq:" <> queueNameToText validConfig.queueName,               source = messageSource,-              shutdown = liftIO $ atomically $ writeTVar shutdownVar True+              shutdown = liftIO $ atomically $ writeTVar shutdownVar True,+              telemetry = validConfig.telemetry             }  -- | Poll in chunks so a shutdown can release messages that were read from pgmq
src/Shibuya/Adapter/Pgmq/Config.hs view
@@ -42,6 +42,7 @@ import Pgmq.Effectful (PgmqRuntimeError) import Pgmq.Types (QueueName, RoutingKey) import Pgmq.Types qualified as Pgmq+import Shibuya.Adapter (AdapterTelemetry, defaultAdapterTelemetry)  -- | Runtime resources and callbacks used by the adapter. data PgmqAdapterEnv = PgmqAdapterEnv@@ -99,7 +100,18 @@     -- See 'PrefetchConfig' for the visibility-timeout trade-off and the bounded,     -- loss-free shutdown behaviour (buffered messages are redelivered after     -- their visibility timeout rather than released immediately).-    prefetchConfig :: !(Maybe PrefetchConfig)+    prefetchConfig :: !(Maybe PrefetchConfig),+    -- | Telemetry options copied unchanged into the 'Shibuya.Adapter.Adapter'.+    --+    -- The default is 'defaultAdapterTelemetry', whose @traceCorrelation@ is+    -- 'Shibuya.Adapter.ParentAndLink': the processing span is a child of the+    -- span that sent the message and also links to it, so a job appears in+    -- the sending request's trace. Choose 'Shibuya.Adapter.LinkOnly' with+    -- @defaultAdapterTelemetry {traceCorrelation = LinkOnly}@ when messages+    -- are sent with long delays or retried over long visibility timeouts:+    -- each delivery then starts its own trace and is related to the sender+    -- only by the link, instead of extending a trace that ended long ago.+    telemetry :: !AdapterTelemetry   }   deriving stock (Show, Eq, Generic) @@ -295,5 +307,6 @@       haltVisibilityTimeout = Nothing,       maxRetries = 3,       fifoConfig = Nothing,-      prefetchConfig = Nothing+      prefetchConfig = Nothing,+      telemetry = defaultAdapterTelemetry     }
test/Main.hs view
@@ -3,6 +3,7 @@ import Shibuya.Adapter.Pgmq.ChaosSpec qualified as ChaosSpec import Shibuya.Adapter.Pgmq.ConfigSpec qualified as ConfigSpec import Shibuya.Adapter.Pgmq.ConvertSpec qualified as ConvertSpec+import Shibuya.Adapter.Pgmq.CorrelationSpec qualified as CorrelationSpec import Shibuya.Adapter.Pgmq.DeadLetterSpec qualified as DeadLetterSpec import Shibuya.Adapter.Pgmq.IntegrationSpec qualified as IntegrationSpec import Shibuya.Adapter.Pgmq.InternalSpec qualified as InternalSpec@@ -19,4 +20,5 @@     describe "Shibuya.Adapter.Pgmq Properties" PropertySpec.spec   describe "Integration Tests" IntegrationSpec.spec   describe "Chaos Tests" ChaosSpec.spec+  describe "Trace Correlation Tests" CorrelationSpec.spec   DeadLetterSpec.spec
test/Shibuya/Adapter/Pgmq/ChaosSpec.hs view
@@ -40,7 +40,7 @@ import Pgmq.Hasql.Sessions qualified as Sessions import Pgmq.Hasql.Statements.Types (BindTopic (..), ReadMessage (..), SendMessage (..), SendMessageWithHeaders (..)) import Pgmq.Types (Message (..), MessageBody (..), MessageHeaders (..), QueueName, parseQueueName, parseRoutingKey, parseTopicPattern)-import Shibuya.Adapter (Adapter (..))+import Shibuya.Adapter (Adapter (..), defaultAdapterTelemetry) import Shibuya.Adapter.Pgmq   ( PgmqAcknowledgementException,     PgmqAdapterConfig (..),@@ -386,7 +386,7 @@         Vector.length messages `shouldBe` 1         spans <- readIORef spansRef         named <- mapM (\sp -> do hot <- readIORef (OTel.spanHot sp); pure (OTel.hotName hot, sp)) spans-        case (Vector.uncons messages, [sp | ("dlq-identity process", sp) <- named]) of+        case (Vector.uncons messages, [sp | ("process dlq-identity", sp) <- named]) of           (Just (msg, _), [consumer]) -> case msg.headers of             Just (Object hdrs) -> do               let stringHeader k = case KeyMap.lookup k hdrs of@@ -695,7 +695,7 @@           case ingestedResult of             Nothing -> liftIO (expectationFailure "message should not auto-DLQ") >> pure ""             Just ingested -> do-              let adapter = Adapter "pgmq:test-terminal-ack-failure" (Stream.fromList [ingested]) (pure ())+              let adapter = Adapter "pgmq:test-terminal-ack-failure" (Stream.fromList [ingested]) (pure ()) defaultAdapterTelemetry                   processor = mkProcessor adapter (\_ -> pure $ AckDeadLetter $ PoisonPill "fail loudly")               appResult <- runApp defaultAppConfig [(processorId, processor)]               case appResult of
test/Shibuya/Adapter/Pgmq/ConfigSpec.hs view
@@ -2,7 +2,7 @@  import Data.Either (isLeft, isRight) import Pgmq.Types (parseRoutingKey)-import Shibuya.Adapter.Pgmq (parseQueueName)+import Shibuya.Adapter.Pgmq (AdapterTelemetry (traceCorrelation), TraceCorrelation (..), defaultAdapterTelemetry, parseQueueName) import Shibuya.Adapter.Pgmq.Config import Test.Hspec @@ -55,6 +55,14 @@    it "sets fifoConfig to Nothing" $ do     config.fifoConfig `shouldBe` Nothing++  it "uses the default adapter telemetry, whose correlation is ParentAndLink" $ do+    config.telemetry `shouldBe` defaultAdapterTelemetry+    config.telemetry.traceCorrelation `shouldBe` ParentAndLink++  it "validates a LinkOnly override and keeps it" $ do+    let linkOnly = config {telemetry = defaultAdapterTelemetry {traceCorrelation = LinkOnly}}+    fmap (\c -> c.telemetry.traceCorrelation) (validateConfig linkOnly) `shouldBe` Right LinkOnly  -- | Tests for defaultPollingConfig defaultPollingConfigSpec :: Spec
+ test/Shibuya/Adapter/Pgmq/CorrelationSpec.hs view
@@ -0,0 +1,286 @@+-- | Trace correlation of PGMQ processing spans against a real PostgreSQL.+--+-- Each case sends a message carrying a known producer @traceparent@, runs a+-- traced Shibuya app over the adapter with an in-memory span exporter, and+-- inspects the exported Consumer spans: their trace, their parent and their+-- links. The dead-letter cases also read the dead-letter queue message's+-- headers, which 'Shibuya.Adapter.Pgmq.Internal.mergeDlqHeaders' writes from+-- the active processing span.+--+-- Run with: cabal test shibuya-pgmq-adapter-test --test-option=--match --test-option=Correlation+module Shibuya.Adapter.Pgmq.CorrelationSpec (spec) where++import Control.Concurrent (threadDelay)+import Control.Exception qualified as Exception+import Control.Monad (void)+import Data.Aeson (Value (..), object, (.=))+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Foldable (toList)+import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)+import Data.Maybe (isNothing)+import Data.Text (Text)+import Data.Text.Encoding qualified as Text+import Data.Vector qualified as Vector+import Effectful (Eff, IOE, liftIO, runEff, (:>))+import Effectful.Error.Static (Error, runErrorNoCallStack)+import Hasql.Pool qualified as Pool+import OpenTelemetry.Attributes (emptyAttributes)+import OpenTelemetry.Exporter.InMemory (inMemoryListExporter)+import OpenTelemetry.Trace.Core qualified as OTel+import OpenTelemetry.Trace.Id (Base (..))+import OpenTelemetry.Trace.Id qualified as OTel.Id+import OpenTelemetry.Trace.Id.Generator (IdGenerator (..))+import OpenTelemetry.Util (appendOnlyBoundedCollectionValues)+import Pgmq.Effectful (Pgmq, PgmqRuntimeError, runPgmq)+import Pgmq.Hasql.Sessions qualified as Sessions+import Pgmq.Hasql.Statements.Types (ReadMessage (..), SendMessageWithHeaders (..))+import Pgmq.Types (Message (..), MessageBody (..), MessageHeaders (..))+import Shibuya.Adapter (Adapter (..))+import Shibuya.Adapter.Pgmq+  ( AdapterTelemetry (traceCorrelation),+    PgmqAdapterConfig (..),+    PollingConfig (..),+    TraceCorrelation (..),+    defaultAdapterTelemetry,+    defaultConfig,+    directDeadLetter,+    mkPgmqAdapterEnv,+    pgmqAdapter,+  )+import Shibuya.App (ProcessorId (..), ShutdownConfig (..), defaultAppConfig, defaultShutdownConfig, mkProcessor, runApp, stopAppGracefully)+import Shibuya.Core.Ack (AckDecision (..), DeadLetterReason (..), RetryDelay (..))+import Shibuya.Core.Ingested (Message (..))+import Shibuya.Core.Types (Attempt (..), Envelope (..))+import Shibuya.Handler (Handler)+import Shibuya.Telemetry.Effect (Tracing, runTracing)+import Shibuya.Telemetry.Propagation (extractTraceContext)+import Shibuya.Telemetry.Semantic (processSpanName)+import System.Environment (lookupEnv)+import Test.Hspec+import TmpPostgres (TestFixture (..), runPgmqSession, withPgmqDb, withTestFixture)++spec :: Spec+spec = around withTempDbFixture $ describe "Correlation" $ do+  it "ParentAndLink: process span is a child of the producer with one link" $ \fixture -> do+    run <- runCorrelated fixture defaultAdapterTelemetry False (countingHandler AckOk) 1+    run.adapterPolicy `shouldBe` ParentAndLink+    [sp] <- pure run.spans+    sp.name `shouldBe` processSpanName processorName+    hexTrace sp.context `shouldBe` producerTraceHex+    fmap hexSpan sp.parent `shouldBe` Just producerSpanHex+    linkIds sp `shouldBe` [(producerTraceHex, producerSpanHex)]++  it "LinkOnly: process span is a linked root" $ \fixture -> do+    run <- runCorrelated fixture linkOnly False (countingHandler AckOk) 1+    run.adapterPolicy `shouldBe` LinkOnly+    [sp] <- pure run.spans+    hexTrace sp.context `shouldNotBe` producerTraceHex+    isNothing sp.parent `shouldBe` True+    linkIds sp `shouldBe` [(producerTraceHex, producerSpanHex)]++  it "ParentAndLink: every redelivery stays in the producer's trace" $ \fixture -> do+    run <- runCorrelated fixture defaultAdapterTelemetry False retryOnceHandler 2+    length run.spans `shouldBe` 2+    map (hexTrace . (.context)) run.spans `shouldBe` [producerTraceHex, producerTraceHex]+    map (fmap hexSpan . (.parent)) run.spans `shouldBe` [Just producerSpanHex, Just producerSpanHex]+    map linkIds run.spans `shouldBe` replicate 2 [(producerTraceHex, producerSpanHex)]++  it "LinkOnly: each redelivery is its own linked trace" $ \fixture -> do+    run <- runCorrelated fixture linkOnly False retryOnceHandler 2+    case run.spans of+      [first, second] -> do+        hexTrace first.context `shouldNotBe` hexTrace second.context+        mapM_ (\sp -> hexTrace sp.context `shouldNotBe` producerTraceHex) [first, second]+        mapM_ (\sp -> isNothing sp.parent `shouldBe` True) [first, second]+        mapM_ (\sp -> linkIds sp `shouldBe` [(producerTraceHex, producerSpanHex)]) [first, second]+      other -> expectationFailure ("expected two processing spans, got " <> show (length other))++  it "LinkOnly: DLQ traceparent is the consumer's, upstream preserved" $ \fixture -> do+    run <- runCorrelated fixture linkOnly True (countingHandler (AckDeadLetter (PoisonPill "correlation"))) 1+    [sp] <- pure run.spans+    isNothing sp.parent `shouldBe` True+    linkIds sp `shouldBe` [(producerTraceHex, producerSpanHex)]+    hdrs <- readDlqHeaders fixture+    let stringHeader :: Text -> Maybe Text+        stringHeader k = case KeyMap.lookup (Key.fromText k) hdrs of+          Just (String v) -> Just v+          _ -> Nothing+        active = [(Text.encodeUtf8 k, Text.encodeUtf8 v) | k <- ["traceparent", "tracestate"], Just v <- [stringHeader k]]+    case extractTraceContext active of+      Nothing -> expectationFailure ("expected a decodable consumer traceparent in the DLQ, got " <> show hdrs)+      Just ctx -> do+        hexTrace ctx `shouldBe` hexTrace sp.context+        hexSpan ctx `shouldBe` hexSpan sp.context+        hexTrace ctx `shouldNotBe` producerTraceHex+        hexSpan ctx `shouldNotBe` producerSpanHex+    stringHeader "x-shibuya-upstream-traceparent" `shouldBe` Just producerParent+    stringHeader "x-shibuya-upstream-tracestate" `shouldBe` Just producerState++--------------------------------------------------------------------------------+-- Fixtures+--------------------------------------------------------------------------------++producerParent :: Text+producerParent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"++producerState :: Text+producerState = "vendor=opaque"++producerTraceHex :: Text+producerTraceHex = "0af7651916cd43dd8448eb211c80319c"++producerSpanHex :: Text+producerSpanHex = "b7ad6b7169203331"++processorName :: Text+processorName = "pgmq-correlation"++linkOnly :: AdapterTelemetry+linkOnly = defaultAdapterTelemetry {traceCorrelation = LinkOnly}++data CorrelatedRun = CorrelatedRun+  { adapterPolicy :: TraceCorrelation,+    spans :: [ConsumerSpan]+  }++-- | Send one message with the producer headers, process it with the given+-- telemetry until the handler has been called @deliveries@ times, and return+-- the exported Consumer spans in the order they ended.+runCorrelated ::+  TestFixture ->+  AdapterTelemetry ->+  Bool ->+  (IORef Int -> Handler '[Tracing, Pgmq, Error PgmqRuntimeError, IOE] Value) ->+  Int ->+  IO CorrelatedRun+runCorrelated TestFixture {pool, queueName, dlqName} telemetry withDlq handler deliveries = do+  runPgmqSession pool $+    void $+      Sessions.sendMessageWithHeaders $+        SendMessageWithHeaders+          { queueName = queueName,+            messageBody = MessageBody (String "correlated"),+            messageHeaders = MessageHeaders (object ["traceparent" .= producerParent, "tracestate" .= producerState]),+            delay = Just 0+          }+  let config =+        (defaultConfig queueName)+          { visibilityTimeout = 5,+            batchSize = 1,+            polling = StandardPolling {pollInterval = 0.05},+            deadLetterConfig = if withDlq then Just (directDeadLetter dlqName True) else Nothing,+            telemetry = telemetry+          }+  calls <- newIORef (0 :: Int)+  (spanProcessor, spansRef) <- inMemoryListExporter+  policy <-+    Exception.bracket+      (OTel.createTracerProvider [spanProcessor] OTel.emptyTracerProviderOptions {OTel.tracerProviderOptionsIdGenerator = DefaultIdGenerator})+      (\p -> OTel.shutdownTracerProvider p (Just 5_000_000))+      $ \provider -> do+        let tracer = OTel.makeTracer provider (OTel.InstrumentationLibrary "pgmq-correlation" "" "" emptyAttributes) OTel.tracerOptions+        runAdapterIO pool $ runTracing tracer $ do+          adapter <- requireAdapter pool config+          result <- runApp defaultAppConfig [(ProcessorId processorName, mkProcessor adapter (handler calls))]+          case result of+            Left err -> liftIO (expectationFailure (show err))+            Right appHandle -> do+              liftIO $ waitForCalls calls deliveries 5_000_000+              drained <- stopAppGracefully defaultShutdownConfig {drainTimeout = 5} appHandle+              liftIO $ drained `shouldBe` True+          pure adapter.telemetry.traceCorrelation+  readIORef calls `shouldReturn` deliveries+  exported <- readIORef spansRef+  consumers <- consumerSpans exported+  pure CorrelatedRun {adapterPolicy = policy, spans = consumers}++countingHandler :: (IOE :> es) => AckDecision -> IORef Int -> Handler es Value+countingHandler decision calls _ = do+  liftIO $ atomicModifyIORef' calls (\n -> (n + 1, ()))+  pure decision++-- | Ask for an immediate redelivery on the first attempt, succeed afterwards.+retryOnceHandler :: (IOE :> es) => IORef Int -> Handler es Value+retryOnceHandler calls message = do+  liftIO $ atomicModifyIORef' calls (\n -> (n + 1, ()))+  pure $ case message.envelope.attempt of+    Just (Attempt n) | n > 0 -> AckOk+    _ -> AckRetry (RetryDelay 0)++readDlqHeaders :: TestFixture -> IO (KeyMap.KeyMap Value)+readDlqHeaders TestFixture {pool, dlqName} = do+  messages <- runPgmqSession pool $ Sessions.readMessage $ ReadMessage dlqName 30 (Just 10) Nothing+  case Vector.toList messages of+    [msg] -> case msg.headers of+      Just (Object hdrs) -> pure hdrs+      other -> expectationFailure ("expected DLQ headers, got " <> show other) >> pure KeyMap.empty+    other -> expectationFailure ("expected one DLQ message, got " <> show (length other)) >> pure KeyMap.empty++waitForCalls :: IORef Int -> Int -> Int -> IO ()+waitForCalls ref target = go+  where+    go remaining+      | remaining <= 0 = pure ()+      | otherwise = do+          n <- readIORef ref+          if n >= target then pure () else threadDelay 20_000 >> go (remaining - 20_000)++withTempDbFixture :: (TestFixture -> IO ()) -> IO ()+withTempDbFixture action = do+  skipDb <- lookupEnv "PGMQ_TEST_SKIP_DB"+  case skipDb of+    Just "1" -> pendingWith "Database tests skipped (PGMQ_TEST_SKIP_DB=1)"+    _ -> do+      result <- withPgmqDb $ \pool -> withTestFixture pool action+      case result of+        Left err -> expectationFailure $ "Failed to start temp database: " <> show err+        Right () -> pure ()++runAdapterIO :: Pool.Pool -> Eff '[Pgmq, Error PgmqRuntimeError, IOE] a -> IO a+runAdapterIO pool action = do+  result <- runEff $ runErrorNoCallStack $ runPgmq pool action+  either (\err -> error ("Pgmq error: " <> show err)) pure result++requireAdapter ::+  (Pgmq :> es, Error PgmqRuntimeError :> es, IOE :> es, Tracing :> es) =>+  Pool.Pool ->+  PgmqAdapterConfig ->+  Eff es (Adapter es Value)+requireAdapter pool config =+  pgmqAdapter (mkPgmqAdapterEnv pool) config+    >>= either (\err -> liftIO (error ("Invalid PGMQ adapter config: " <> show err))) pure++--------------------------------------------------------------------------------+-- Span inspection+--------------------------------------------------------------------------------++data ConsumerSpan = ConsumerSpan+  { name :: Text,+    context :: OTel.SpanContext,+    parent :: Maybe OTel.SpanContext,+    links :: [OTel.SpanContext]+  }++consumerSpans :: [OTel.ImmutableSpan] -> IO [ConsumerSpan]+consumerSpans exported = traverse describeSpan [sp | sp <- exported, OTel.spanKind sp == OTel.Consumer]+  where+    describeSpan sp = do+      hot <- readIORef (OTel.spanHot sp)+      parent <- traverse OTel.getSpanContext (OTel.spanParent sp)+      pure+        ConsumerSpan+          { name = OTel.hotName hot,+            context = OTel.spanContext sp,+            parent,+            links = map OTel.frozenLinkContext (toList (appendOnlyBoundedCollectionValues (OTel.hotLinks hot)))+          }++linkIds :: ConsumerSpan -> [(Text, Text)]+linkIds sp = [(hexTrace l, hexSpan l) | l <- sp.links]++hexTrace :: OTel.SpanContext -> Text+hexTrace ctx = OTel.Id.traceIdBaseEncodedText Base16 ctx.traceId++hexSpan :: OTel.SpanContext -> Text+hexSpan ctx = OTel.Id.spanIdBaseEncodedText Base16 ctx.spanId
test/Shibuya/Adapter/Pgmq/IntegrationSpec.hs view
@@ -34,6 +34,7 @@     VisibilityTimeoutQuery (..),   ) import Pgmq.Types (Message (..), MessageBody (..), MessageHeaders (..), MessageId, QueueName)+import Shibuya.Adapter (defaultAdapterTelemetry) import Shibuya.Adapter.Pgmq.Config   ( FifoConfig (..),     FifoReadStrategy (..),@@ -448,7 +449,8 @@       haltVisibilityTimeout = Nothing,       maxRetries = 3,       fifoConfig = Nothing,-      prefetchConfig = Nothing+      prefetchConfig = Nothing,+      telemetry = defaultAdapterTelemetry     }  -- | Create config with custom batch size@@ -465,5 +467,6 @@       haltVisibilityTimeout = Nothing,       maxRetries = 3,       fifoConfig = Nothing,-      prefetchConfig = Nothing+      prefetchConfig = Nothing,+      telemetry = defaultAdapterTelemetry     }
test/Shibuya/Adapter/Pgmq/InternalSpec.hs view
@@ -18,6 +18,7 @@ import Pgmq.Effectful.Effect qualified as PgmqEffect import Pgmq.Hasql.Statements.Types (ReadGrouped (..), ReadGroupedWithPoll (..), ReadMessage (..), ReadWithPollMessage (..)) import Pgmq.Types (Message (..), MessageBody (..), MessageId (..), parseQueueName, queueNameToText)+import Shibuya.Adapter (defaultAdapterTelemetry) import Shibuya.Adapter.Pgmq.Config   ( FifoConfig (..),     FifoReadStrategy (..),@@ -104,7 +105,8 @@             haltVisibilityTimeout = Nothing,             maxRetries = 3,             fifoConfig = Nothing,-            prefetchConfig = Nothing+            prefetchConfig = Nothing,+            telemetry = defaultAdapterTelemetry           }       ReadMessage         { queueName = queryQueueName,@@ -143,7 +145,8 @@             haltVisibilityTimeout = Nothing,             maxRetries = 3,             fifoConfig = Nothing,-            prefetchConfig = Nothing+            prefetchConfig = Nothing,+            telemetry = defaultAdapterTelemetry           }       ReadWithPollMessage         { queueName = queryQueueName,@@ -190,7 +193,8 @@             haltVisibilityTimeout = Nothing,             maxRetries = 3,             fifoConfig = Nothing,-            prefetchConfig = Nothing+            prefetchConfig = Nothing,+            telemetry = defaultAdapterTelemetry           }       ReadGrouped         { queueName = queryQueueName,@@ -503,7 +507,8 @@           haltVisibilityTimeout = Nothing,           maxRetries = 3,           fifoConfig = Nothing,-          prefetchConfig = Nothing+          prefetchConfig = Nothing,+          telemetry = defaultAdapterTelemetry         }  runStubPgmq ::