consumer

package
v0.19.1108 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package consumer is the Kafka consumer runtime: consumer naming and selection, the shared ClickHouse-writing sink, and the dead-letter path. It is to `consumer` what internal/pkg/worker is to `worker` — none of it knows anything about a particular domain.

Domain consumers live with their domain (app/runners/consumer holds heartbeats, otel-logs and otel-traces) and consist of little more than a Sink plus a decode-and-insert handler. The one consumer that lives here is the DLQ (see dlq.go), because the dead-letter topic is shared infrastructure that every consumer produces to rather than any domain's data.

Index

Constants

View Source
const NameAll = "all"

NameAll selects every consumer, mirroring `worker --namespace=all`. Locally one process runs them all.

Variables

This section is empty.

Functions

func Decode

func Decode[T any](ctx context.Context, s *Sink, recs []*kgo.Record, typ string) []T

Decode unwraps a partition's records into T, skipping anything undecodable or of the wrong type. A record we can't parse is dropped rather than returned as an error: failing the batch would block the partition forever re-reading the same bad record, so it's counted, collected, and skipped instead — every failure in this fetch is sent to the dead-letter queue in one batch after the loop, not one at a time as they're found. A burst of bad records (a bad deploy, a version-skewed producer) would otherwise mean one synchronous produce round trip per record, and a large enough burst could make this handler call itself look stuck — the exact failure mode the dead-letter queue exists to avoid, self-inflicted.

func Insert

func Insert[T any](ctx context.Context, s *Sink, partition int32, recs []*kgo.Record, rows []T) error

Insert writes one partition's decoded batch to ClickHouse, keyed by the batch's offset range as an insert_deduplication_token. Offsets are only committed after this returns nil, so a failed insert is redelivered; the token is what makes that replay idempotent rather than duplicating rows.

Types

type DLQConsumer

type DLQConsumer struct {
	*Sink
}

DLQConsumer drains the dead-letter topic into the same app.DLQRecord table the other consumers write to directly when a produce to this topic fails.

It lives in the runtime package rather than with a domain because the topic isn't any domain's data — every consumer produces to it, and what it carries is a decode failure from some other topic.

Its own decode failures go straight to that direct write rather than back through the topic — a dead-letter-about-a-dead-letter stops here, one level deep.

func NewDLQConsumer

func NewDLQConsumer(params Params) (*DLQConsumer, error)

type Name

type Name string

Name identifies one consumer: it is an element of the `consumer --name` value, the suffix of the consumer group, and the prefix of the consumer's metrics. One Name owns exactly one topic.

A Name is NOT a deployment. A deployment (a pod, its resources, its SERVICE_DEPLOYMENT tag, and therefore its Kafka client id) may host more than one consumer — `--name=otel-logs,otel-traces` runs both in one pod under the deployment named `otel`, each still with its own group and its own client. Consumers sharing a pod share a client id, so they also share a Kafka client quota, and share a liveness probe: if one handler wedges, the restart takes the other down with it. Group them only when scaling and failing together is acceptable.

The names live here, in the runtime, rather than each with its domain: NewSelection runs before the fx graph is built (see cmd/consumer.go), so the set of valid names has to be statically known and can't be collected from the domains at wiring time.

const (
	NameHeartbeats Name = "heartbeats"
	NameOtelLogs   Name = "otel-logs"
	NameOtelTraces Name = "otel-traces"
	NameDLQ        Name = "dlq"
)

func Names

func Names() []Name

type Params

type Params struct {
	fx.In

	Cfg       *internal.Config
	L         *zap.Logger
	MW        metrics.Writer
	CHDB      *gorm.DB `name:"ch"`
	LC        fx.Lifecycle
	Selection Selection
	Producer  *kafka.Producer
}

type Selection

type Selection struct {
	// contains filtered or unexported fields
}

Selection is the parsed `--name` flag.

func NewSelection

func NewSelection(spec string) (Selection, error)

NewSelection parses a comma-separated list of consumer names, or "all".

Unknown names are an error rather than a no-op: a typo in a deployment's flag would otherwise produce a pod that starts, passes health checks, and silently consumes nothing, which is a much harder failure to spot than a crash loop.

func (Selection) Includes

func (s Selection) Includes(n Name) bool

type Sink

type Sink struct {
	// contains filtered or unexported fields
}

Sink is the shared half of every consumer that writes to ClickHouse: naming, metrics, the ClickHouse handle, and the poll-loop lifecycle. Each consumer embeds one and adds only its own decode-and-insert handler.

func NewSink

func NewSink(params Params, name Name, topic string) *Sink

NewSink returns nil when this consumer should not run in this process — either it wasn't selected, or Kafka is disabled entirely. Callers return a nil consumer in that case and whatever inline write path they replaced stays in effect.

func (*Sink) ConsumerName

func (s *Sink) ConsumerName() string

ConsumerName identifies which consumer this sink is, so a healthcheck covering several sinks in one pod can report which one is stuck rather than just that something is.

func (*Sink) Healthy

func (s *Sink) Healthy(max time.Duration) (bool, time.Duration)

Healthy reports whether this consumer's handler is not currently stuck past max, and for how long it's been running if it is. A nil sink (consumer not running in this process) always reports healthy — there's nothing to check.

func (*Sink) Start

func (s *Sink) Start(params Params, handler pkgkafka.Handler) error

Start builds the consumer client and binds its poll loop to the fx lifecycle.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL