shibuya-kafka-adapter: Kafka adapter for the Shibuya queue processing framework

[ concurrency, library, mit, streaming ] [ Propose Tags ] [ Report a vulnerability ]

A Shibuya adapter that integrates with Apache Kafka via kafka-effectful and hw-kafka-client. Provides polling, offset commit semantics, partition awareness, and graceful shutdown.


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0, 0.2.0.0, 0.4.0.0, 0.5.0.0, 0.5.0.1, 0.6.0.0, 0.7.0.0, 0.8.0.1, 0.9.0.0, 0.9.0.1, 0.9.1.0, 0.12.0.0, 0.13.0.0
Change log CHANGELOG.md
Dependencies base (>=4.21.0.0 && <4.22), bytestring (>=0.12 && <0.13), containers (>=0.7 && <0.8), effectful-core (>=2.6.1 && <2.7 || >=2.7.1.1 && <2.8), hs-opentelemetry-api (>=1.0 && <1.1), hs-opentelemetry-semantic-conventions (>=1.40 && <1.41), hw-kafka-client (>=5.3 && <6), hw-kafka-streamly (>=0.2 && <0.3), kafka-effectful (>=0.3.1.0 && <0.4), shibuya-core (>=0.13.0.0 && <0.14), stm (>=2.5 && <2.6), streamly (>=0.11 && <0.12), streamly-core (>=0.3 && <0.4), text (>=2.1 && <2.2), time (>=1.14 && <1.15), unordered-containers (>=0.2 && <0.3) [details]
Tested with ghc ==9.12.2
License MIT
Copyright 2026 Nadeem Bitar
Author Nadeem Bitar
Maintainer nadeem@gmail.com
Uploaded by shinzui at 2026-11-14T18:53:17Z
Category Concurrency, Streaming
Home page https://github.com/shinzui/shibuya-kafka-adapter
Bug tracker https://github.com/shinzui/shibuya-kafka-adapter/issues
Source repo head: git clone https://github.com/shinzui/shibuya-kafka-adapter.git
Distributions
Downloads 167 total (36 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for shibuya-kafka-adapter-0.13.0.0

[back to package description]

shibuya-kafka-adapter

Consume Kafka messages through the Shibuya queue-processing framework, with explicit acknowledgements, partition-aware retries, offset management, and OpenTelemetry context propagation.

Release: 0.12.0.0 on Hackage · API documentation · Changelog

The adapter requires shibuya-core ^>=0.12.0.0 and GHC 9.12. The repository's Nix shell uses GHC 9.12.4. It builds on kafka-effectful for consumer operations and hw-kafka-streamly for poll-error classification.

Using the adapter

Add these dependencies to your application:

build-depends:
  base ^>=4.21,
  effectful-core,
  hw-kafka-client >=5.3 && <6,
  kafka-effectful ^>=0.3.1.0,
  shibuya-core ^>=0.12.0.0,
  shibuya-kafka-adapter ^>=0.12.0.0

The consumer scope owns the Kafka connection and subscription. Create the adapter inside that scope and use mkProcessor, which defaults to serial processing. Disable automatic offset storage so a record's offset is stored only after acknowledgement.

This example prints messages from orders on localhost:9092:

{-# LANGUAGE GHC2024 #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}

module Main (main) where

import Control.Monad.IO.Class (liftIO)
import Effectful (runEff)
import Effectful.Error.Static (runError)
import Kafka.Consumer.Types (OffsetReset (Earliest))
import Kafka.Effectful.Consumer qualified as Kafka
import Shibuya.Adapter.Kafka
  ( BrokerAddress (..), ConsumerGroupId (..), KafkaError, TopicName (..)
  , defaultConfig, kafkaAdapter
  )
import Shibuya.App (ProcessorId (..), defaultAppConfig, mkProcessor, runApp, waitApp)
import Shibuya.Core.Ack (AckDecision (AckOk))
import Shibuya.Core.Ingested (Message (..))
import Shibuya.Core.Types (Envelope (..))
import Shibuya.Telemetry.Effect (runTracingNoop)

main :: IO ()
main = do
  result <- runEff . runError @KafkaError . runTracingNoop $ do
    let props = Kafka.brokersList [BrokerAddress "localhost:9092"]
             <> Kafka.groupId (ConsumerGroupId "orders-consumer")
             <> Kafka.noAutoOffsetStore
        sub = Kafka.topics [TopicName "orders"] <> Kafka.offsetReset Earliest
    Kafka.runKafkaConsumer props sub $ do
      adapter <- kafkaAdapter (defaultConfig [TopicName "orders"])
      let handler message = do
            liftIO (print message.envelope.payload)
            pure AckOk
      app <- runApp defaultAppConfig
        [(ProcessorId "orders", mkProcessor adapter handler)]
      case app of
        Left err -> liftIO (fail (show err))
        Right handle -> waitApp handle
  case result of
    Left err -> fail (show err)
    Right () -> pure ()

defaultConfig polls with a 1,000 ms timeout and a maximum batch size of 100. Its topics field supplies adapter metadata and is checked against the live subscription. Subscription topics and offset-reset policy belong to runKafkaConsumer.

Acknowledgements and ordering

Use serial processing. The adapter does not support Shibuya's Async or Ahead policies. librdkafka stores the highest offset per partition without tracking gaps; concurrent finalization can commit past unresolved work. The adapter cannot inspect or enforce the processor's concurrency policy.

Decision Adapter behavior
AckOk Store the record's offset locally; auto-commit or consumer close flushes it to the broker.
AckRetry Keep the earliest unresolved delivery as the retry boundary, seek back to it, and leave its offset unstored.
AckDeadLetter Store the offset and log a dead-lettered message DROPPED warning. The adapter has no DLQ producer.
AckHalt Pause the originating partition without storing its offset; the processor stops.

With serial single-message processing, a retry blocks later messages in the same partition. Pre-seek records are discarded, already-buffered successors are skipped before handler entry, and replayed successors wait until the retried record resolves. Later callbacks cannot replace the earliest retry boundary or store offsets past it. A retry cannot undo messages already admitted together in the same handler batch.

If retention or compaction removed the retry offset, the adapter warns and resumes at the next available record. Kafka provides no delivery-attempt counter here: Envelope.attempt is always Nothing. Use an external store for bounded retry counts, or halt processing.

A dead-letter acknowledgement advances the group's position without publishing the message elsewhere. Arrange any required DLQ publication before returning AckDeadLetter.

Rebalances and shutdown

For assignment fencing, create state with newKafkaAdapterState, add Kafka.Consumer.setCallback (Kafka.Consumer.rebalanceCallback (kafkaRebalanceHandler state)) to the consumer properties before consumer creation, and construct the adapter with kafkaAdapterWith state config.

The callback logs assignment changes, clears retry state on revocation, and fences old delivery callbacks so they cannot store, seek, or pause a later assignment. Deliveries revoked before their admission check are skipped. A revocation after that check cannot cancel a handler already admitted. The helper is optional; kafkaAdapter alone does not provide this ownership fence.

Keep the consumer scope open while stopping the Shibuya application. Adapter shutdown signals the source to stop and commits offsets already stored. Let runKafkaConsumer close normally afterward so it can flush offsets stored during draining.

After AckHalt, polling stops. The consumer can lose its assignment when max.poll.interval.ms expires; a single-member group needs a restart to resume. Partition pause state belongs to the consumer session.

Errors and tracing

Non-fatal poll errors, including timeouts and partition EOFs, are filtered out. Fatal poll errors terminate the source through Error KafkaError. Exhausted acknowledgement operations throw KafkaAcknowledgementException at the finalizer boundary so Shibuya can retain the processor failure.

The adapter preserves Kafka headers, extracts W3C traceparent and tracestate, and adds messaging.system=kafka, messaging.kafka.destination.partition, and messaging.kafka.message.offset to the envelope. Shibuya's runner uses these attributes on its per-message span. The example above disables tracing with runTracingNoop; use the configured tracing interpreter to export spans.

Trace correlation

When a record carries a W3C traceparent header, Shibuya opens a Consumer span named process <processor> around the handler and always adds a link to the producer's span. The telemetry field of KafkaAdapterConfig decides whether that span is also the producer's child:

import Shibuya.Adapter.Kafka

-- Default (ParentAndLink): the handler runs inside the producer's trace.
config = defaultConfig [TopicName "orders"]

-- LinkOnly: each record's processing starts its own trace, linked to the producer.
replaying =
  (defaultConfig [TopicName "orders"])
    { telemetry = defaultAdapterTelemetry {traceCorrelation = LinkOnly}
    }

ParentAndLink suits records consumed close to real time: producer and consumer appear in one connected trace. Choose LinkOnly when the producer's trace has usually ended before processing starts, so the consumer span would only stretch it:

  • replay from an old offset, or a reprocessing job over retained data;
  • sustained consumer lag;
  • many consumer groups on one topic, each of which would otherwise add its processing to the same producer trace.

A LinkOnly trace is sampled on its own, independently of the producer's sampling decision, and a tail-sampling collector sees it as a new trace. The link still lets a trace backend navigate to the producer.

OpenTelemetry's messaging conventions allow creation-context parenting for a single message and recommend against it by default only when processing runs inside another span; Shibuya records its choice in its ADR 0014 (mori://shinzui/shibuya, docs/adr/0014-define-consumer-correlation-and-bounded-batch-link-accounting.md; artifact URI pending). Without a usable traceparent neither setting adds a link, and the span keeps whatever span is active on the processing thread as its parent.

Batch processors ignore the setting: a batch span is always a root with one link per record (the first 128 records with a valid context), each link carrying the record's messaging.message.id, and messaging.batch.message_count on the span.

Examples and development

See the repository README for runnable examples, the Nix toolchain, and live Kafka integration tests. Only the adapter library is published to Hackage.

License

MIT. See LICENSE.