Documentation
¶
Overview ¶
Package consumer runs a typed Kafka consumer loop around a user-supplied handler.
A Handler declares the topic it reads, the consumer group it joins, how to process a message of type T, and its retry policy. New wires it to a client factory and a dispatcher:
c, err := consumer.New[OrderPlaced](d, factory, handler)
if err != nil {
return err
}
shutdown, err := c.Run()
Run starts the poll loop in the background and returns immediately. Calling the returned Shutdown drains the in-flight message and closes the client. A failure inside the loop - a broker error, a commit error - is reported on the channel from ListenShutdown, and the loop stops.
Offsets are committed by this package, not by librdkafka: the client factory disables auto-commit, and a message is committed only after the handler returns without error.
When the handler fails, the error is matched against the RetryableErrors of its ConfigRetry. A retryable error is retried with exponential backoff; a non-retryable one, or a retry sequence that exhausts MaxElapsedTime, sends the message to the dead letter topic, which is the source topic suffixed with "-dlt". Messages whose payload cannot be decoded at all go to the same topic as plain text.
Each message gets its own logger, carrying the consumer group, the topic, the partition, the offset and a freshly generated trace id. The handler reads it with log.FromContext.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client interface {
Subscribe(topic string, cb kafka.RebalanceCb) error
ReadMessage(timeout time.Duration) (*kafka.Message, error)
CommitMessage(m *kafka.Message) ([]kafka.TopicPartition, error)
Close() error
}
Client is the Kafka consumer driver a Consumer polls. It is the subset of the confluent client the loop uses, which is what makes the loop testable.
type ClientOption ¶
type ClientOption func(s clientSettings) clientSettings
ClientOption configures the Kafka client built by NewFactory.
func WithBootstrapServer ¶
func WithBootstrapServer(servers string) ClientOption
WithBootstrapServer sets the broker list, as a comma-separated "host:port" string.
type ConfigRetry ¶
type ConfigRetry struct {
RetryableErrors []error
InitialInterval time.Duration
MaxElapsedTime time.Duration
MaxInterval time.Duration
}
ConfigRetry is a handler retry policy.
Only errors matching RetryableErrors under errors.Is are retried; anything else goes straight to the dead letter topic. The remaining fields configure the exponential backoff, and a retry sequence that exceeds MaxElapsedTime is abandoned to the dead letter topic as well.
The zero value retries nothing. Build one with NewConfigRetry or as a literal.
func NewConfigRetry ¶
func NewConfigRetry(options ...ConfigRetryOption) ConfigRetry
NewConfigRetry builds a ConfigRetry from options.
type ConfigRetryOption ¶
type ConfigRetryOption func(retrySettings configRetrySettings) configRetrySettings
ConfigRetryOption configures a ConfigRetry built by NewConfigRetry.
func WithInitialInterval ¶
func WithInitialInterval(initialInterval time.Duration) ConfigRetryOption
WithInitialInterval sets the delay before the first retry.
func WithMaxElapsedTime ¶
func WithMaxElapsedTime(maxElapsedTime time.Duration) ConfigRetryOption
WithMaxElapsedTime caps how long retrying may go on before the message is sent to the dead letter topic.
The consumer does not poll while retrying, so a value beyond the broker session timeout gets the consumer evicted from its group.
func WithMaxInterval ¶
func WithMaxInterval(maxInterval time.Duration) ConfigRetryOption
WithMaxInterval caps the delay between retries as the backoff grows.
func WithRetryableErrors ¶
func WithRetryableErrors(errs ...error) ConfigRetryOption
WithRetryableErrors sets the errors worth retrying. They are matched with errors.Is, so sentinels survive wrapping.
type Consumer ¶
Consumer is a running Kafka consumer loop.
func New ¶
func New[T any]( dispatcher dispatcher.Dispatcher, factory Factory, handler Handler[T], options ...Option, ) (Consumer, error)
New builds a Consumer for messages decoded into T.
It creates a Kafka client through factory using the handler ID as the group, so it fails if the broker is unreachable. dispatcher is used to publish to the dead letter topic, and is therefore required even for a consumer that never fails.
New does not start anything - call Run.
Example ¶
Requires a Kafka broker, so this example is compiled but not run.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/diegodesousas/go-devkit/pkg/log"
"github.com/diegodesousas/go-devkit/pkg/stream/consumer"
"github.com/diegodesousas/go-devkit/pkg/stream/dispatcher"
"github.com/pkg/errors"
)
type orderPlaced struct {
ID string `json:"id"`
Total int `json:"total"`
}
// errChargeUnavailable stands for a downstream failure worth retrying, as
// opposed to a malformed message, which is not.
var errChargeUnavailable = errors.New("charge service unavailable")
type orderHandler struct{}
func (orderHandler) ID() string { return "billing" }
func (orderHandler) Topic() string { return "orders" }
// ShouldSkip runs before Handle and acknowledges the message without processing
// it, which is how a consumer ignores traffic it does not care about.
func (orderHandler) ShouldSkip(o orderPlaced) bool { return o.Total == 0 }
func (orderHandler) Handle(ctx context.Context, o orderPlaced) error {
log.Info(ctx, "charging order", log.NewField("order-id", o.ID))
return nil
}
// Only errors listed here are retried; anything else goes straight to the dead
// letter topic.
func (orderHandler) ConfigRetry() consumer.ConfigRetry {
return consumer.NewConfigRetry(
consumer.WithRetryableErrors(errChargeUnavailable),
consumer.WithInitialInterval(time.Second),
consumer.WithMaxInterval(5*time.Second),
consumer.WithMaxElapsedTime(30*time.Second),
)
}
// Requires a Kafka broker, so this example is compiled but not run.
func main() {
client, err := dispatcher.NewClient(dispatcher.WithBootstrapServers("localhost:9092"))
if err != nil {
panic(err)
}
// The dispatcher is what publishes to the dead letter topic, so a consumer
// needs one even when the handler never fails.
d := dispatcher.New(client)
defer d.Shutdown()
factory := consumer.NewFactory(consumer.WithBootstrapServer("localhost:9092"))
c, err := consumer.New[orderPlaced](d, factory, orderHandler{})
if err != nil {
panic(err)
}
shutdown, err := c.Run() // returns immediately; the loop runs in the background
if err != nil {
panic(err)
}
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
// The loop reports its own failures here, so a broker error stops the
// process rather than leaving it running and consuming nothing.
go func() {
if err := <-c.ListenShutdown(); err != nil {
fmt.Println("consumer stopped:", err)
interrupt <- syscall.SIGTERM
}
}()
<-interrupt
shutdown() // waits for the in-flight message, then closes the client
}
Output:
type Factory ¶
Factory builds a Client for a consumer group.
func NewFactory ¶
func NewFactory(options ...ClientOption) Factory
NewFactory returns a Factory producing confluent Kafka consumers.
Auto-commit is off: offsets are committed by the Consumer once a handler succeeds. Session timeout is 45s and the group starts from the earliest offset. Only the bootstrap servers are configurable, so a cluster requiring SASL or TLS cannot be reached.
type Handler ¶
type Handler[T any] interface { ID() string Topic() string ShouldSkip(content T) bool Handle(ctx context.Context, content T) error ConfigRetry() ConfigRetry }
Handler is the user code a Consumer drives.
ID names the consumer group, Topic the topic to read. ShouldSkip is consulted before Handle and lets a message be acknowledged without processing. ConfigRetry declares which errors are worth retrying and how long to keep trying; it is consulted on every failure, so it must be cheap.
Handle receives a context carrying a logger scoped to the message. Returning nil commits the offset.
type Option ¶
type Option func(s settings) settings
Option configures a Consumer built by New.
func WithLogger ¶
WithLogger sets the base logger. The Consumer derives from it per message, adding the group, topic, partition, offset and a trace id. Defaults to log.New().
func WithStringGenerator ¶
func WithStringGenerator(generator gen.StringGenerator) Option
WithStringGenerator sets the source of per-message trace ids. Defaults to gen.UUIDGenerator; inject gen.SequenceGenerator in tests for deterministic output.