cgnats

package module
v0.0.0-...-70a53f2 Latest Latest
Warning

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

Go to latest
Published: Dec 29, 2025 License: MIT Imports: 17 Imported by: 0

README

cgnats

Go Reference

Strictly ordered message processing for NATS JetStream with Kafka-style consumer groups.

[!WARNING] This project is highly experimental and production use is discouraged for now. The API is subject to breaking changes.

Installation

go get github.com/ramasauskas/cgnats

Features

  • Kafka-style consumer groups - Members join groups, receive partition assignments, and coordinate via a central coordinator
  • Strict ordering guarantees - Messages within a partition are processed in order with at-least-once delivery
  • Automatic rebalancing - Partitions are redistributed when consumers join or leave the group
  • Offset management - Durable offset storage using JetStream KV with automatic recovery on restart
  • Pluggable partition assignment - Built-in round-robin assigner with support for custom strategies
  • High availability - Coordinator leader election with automatic failover
  • Lifecycle hooks - Callbacks for rebalancing, partition assignment, commits, and heartbeats for observability

Motivation

NATS JetStream provides powerful streaming capabilities, but achieving total order processing across multiple consumers in a distributed system remains challenging:

  • OrderedConsumer guarantees ordering but is limited to a single consumer - no horizontal scaling or high availability
  • Solutions like orbit.go/pcgroups help with partitioning but still require MaxAckPending=1 for strict ordering, limiting throughput due to network I/O

cgnats solves this by implementing Kafka-style consumer group coordination on top of JetStream.

This package focuses solely on consumption coordination. It does not handle message publishing or schema design - you are responsible for designing your stream subjects, partitioning strategy, and message format.

Requirements

NATS Server with JetStream enabled

Example

Starting the Coordinator

The coordinator manages consumer group membership and partition assignments.

package main

import (
	"context"
	"log"
	"time"

	"github.com/nats-io/nats.go"
	"github.com/nats-io/nats.go/jetstream"
	"github.com/ramasauskas/cgnats"
)

func main() {
	ctx := context.Background()

	nc, _ := nats.Connect(nats.DefaultURL)
	defer nc.Close()

	js, _ := jetstream.New(nc)

	coordinator, err := cgnats.NewCoordinator(ctx, nc, js,
		cgnats.WithCoordinatorSessionTimeout(10*time.Second),
	)
	if err != nil {
		log.Fatal(err)
	}

	// Run blocks until context is cancelled
	coordinator.Run(ctx)
}

[!NOTE] A standalone coordinator binary is on the roadmap, so you won't need to run this in your own process.

Consuming Messages

Consumers join a group and receive partition assignments. The reader handles rebalancing automatically when consumers join or leave the group.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/nats-io/nats.go"
	"github.com/nats-io/nats.go/jetstream"
	"github.com/ramasauskas/cgnats"
)

func main() {
	ctx := context.Background()

	nc, _ := nats.Connect(nats.DefaultURL)
	defer nc.Close()

	js, _ := jetstream.New(nc)

	// Map partition numbers to stream and subject
	resolver := func(partition int) (stream, subject string) {
		return "ORDERS", fmt.Sprintf("ORDERS.%d", partition)
	}

	reader, err := cgnats.NewReader(
		nc, js,
		"order-processors", // consumer group name
		4,                  // number of partitions
		cgnats.WithPartitionResolver(resolver),
		cgnats.WithSessionTimeout(10*time.Second),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer reader.Close()

	for {
		msgs, err := reader.Fetch(ctx, 10)
		if err != nil {
			log.Printf("fetch error: %v", err)
			continue
		}

		for _, msg := range msgs {
			log.Printf("partition=%d seq=%d data=%s",
				msg.Partition, msg.Sequence, msg.Data)
		}

		if err := reader.Ack(ctx, msgs...); err != nil {
			log.Printf("ack error: %v", err)
		}
	}
}

Roadmap

  • Standalone coordinator binary (currently you need to run the coordinator in your own process)
  • More chaotic E2E tests to ensure consistency

Acknowledgements

The reader API is inspired by segmentio/kafka-go.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Coordinator

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

Coordinator is the NATS-based coordinator that manages consumer groups. It uses JetStream KV for leader election and offset storage.

func NewCoordinator

func NewCoordinator(ctx context.Context, nc *nats.Conn, js jetstream.JetStream, oo ...CoordinatorConfigOpt) (*Coordinator, error)

NewCoordinator creates a new coordinator instance.

func (*Coordinator) Run

func (c *Coordinator) Run(ctx context.Context)

Run starts the coordinator's main loop. It attempts to acquire leadership and runs as leader until context is cancelled.

type CoordinatorConfig

type CoordinatorConfig struct {
	// KVReplicas is the number of replicas for JetStream KV buckets.
	KVReplicas int

	// KVBucketCoordinator is the KV bucket name used for leader election.
	KVBucketCoordinator string

	// KVBucketOffsets is the KV bucket name used for offset storage.
	KVBucketOffsets string

	// KVKeyCoordinator is the key name within the coordinator KV bucket
	// used for leader election.
	KVKeyCoordinator string

	// CoordinatorSessionTimeout is the TTL for the coordinator leadership lease.
	// The coordinator must renew its lease before this timeout expires.
	CoordinatorSessionTimeout time.Duration

	// InitialRebalanceDelay is the delay before triggering the first rebalance
	// when a new group is created with no previous members.
	InitialRebalanceDelay time.Duration

	// SubjectPrefix is the NATS subject prefix for coordinator RPC communication.
	SubjectPrefix string

	// MaxConcurrentRequests is the worker pool size for handling concurrent
	// requests from consumers.
	MaxConcurrentRequests int

	// MaxConsumersPerGroup limits the maximum number of consumers allowed per group.
	// 0 means unlimited.
	MaxConsumersPerGroup int

	// Logger is used for logging coordinator events and errors.
	Logger *slog.Logger

	// Hooks holds optional callbacks for coordinator observability.
	Hooks CoordinatorHooks
}

CoordinatorConfig holds configuration for the coordinator.

type CoordinatorConfigOpt

type CoordinatorConfigOpt func(CoordinatorConfig) CoordinatorConfig

CoordinatorConfigOpt is a function that modifies a Config.

func WithCoordinatorHooks

func WithCoordinatorHooks(m CoordinatorHooks) CoordinatorConfigOpt

WithCoordinatorHooks sets the coordinator hooks callbacks.

func WithCoordinatorSessionTimeout

func WithCoordinatorSessionTimeout(timeout time.Duration) CoordinatorConfigOpt

WithCoordinatorSessionTimeout sets the session timeout for the coordinator.

func WithInitialRebalanceDelay

func WithInitialRebalanceDelay(delay time.Duration) CoordinatorConfigOpt

WithInitialRebalanceDelay sets the delay before triggering a rebalance after a member joins.

func WithKVCoordinatorBucket

func WithKVCoordinatorBucket(bucket string) CoordinatorConfigOpt

WithKVCoordinatorBucket sets the KV bucket name for coordinator state.

func WithKVKeyCoordinator

func WithKVKeyCoordinator(key string) CoordinatorConfigOpt

WithKVKeyCoordinator sets the key name used within the coordinator KV bucket for leader election.

func WithKVOffsetsBucket

func WithKVOffsetsBucket(bucket string) CoordinatorConfigOpt

WithKVOffsetsBucket sets the KV bucket name for offset storage.

func WithLogger

func WithLogger(logger *slog.Logger) CoordinatorConfigOpt

WithLogger sets the logger for the coordinator.

func WithMaxConcurrentRequestsPerGroup

func WithMaxConcurrentRequestsPerGroup(n int) CoordinatorConfigOpt

WithMaxConcurrentRequestsPerGroup sets the maximum number of concurrent requests per group.

func WithMaxConsumersPerGroup

func WithMaxConsumersPerGroup(n int) CoordinatorConfigOpt

WithMaxConsumersPerGroup sets the maximum number of consumers allowed per group.

func WithSubjectPrefix

func WithSubjectPrefix(prefix string) CoordinatorConfigOpt

WithSubjectPrefix sets the NATS subject prefix for coordinator communication.

type CoordinatorHooks

type CoordinatorHooks struct {
	// Elected is called when this coordinator becomes the leader.
	Elected func()
	// LostLeader is called when leadership is lost unexpectedly.
	LostLeader func()
	// SteppingDown is called when the coordinator voluntarily steps down.
	SteppingDown func()
	// RebalanceStarted is called when a group rebalance begins.
	RebalanceStarted func(group string, generation uint64)
}

CoordinatorHooks holds optional hooks for coordinator observability.

type Message

type Message struct {
	// Stream is the JetStream stream this message came from.
	Stream string
	// Subject is the NATS subject this message was published to.
	Subject string
	// Data is the message payload.
	Data []byte
	// Partition is the partition number this message belongs to.
	Partition int
	// Sequence is the message sequence number within the partition.
	Sequence uint64
	// Headers contains optional message headers.
	Headers map[string][]string
	// Timestamp is when the message was published.
	Timestamp time.Time
}

Message represents a message received from a partition.

type PartitionResolver

type PartitionResolver func(partition int) (stream string, subject string)

PartitionResolver maps a partition index to a stream and subject.

type Reader

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

Reader provides methods to read messages from assigned partitions.

func NewReader

func NewReader(nc *nats.Conn, js jetstream.JetStream, group string, partitions int, opts ...ReaderOption) (*Reader, error)

NewReader creates a new Reader for consuming messages from partitions. The reader handles partition assignment, rebalancing, and offset management.

func (*Reader) Ack

func (r *Reader) Ack(ctx context.Context, msgs ...Message) error

Ack acknowledges the given messages, committing their offsets. Messages from each partition up to and including the highest sequence are considered processed. Returns ErrReaderClosed if the reader has been closed.

func (*Reader) Close

func (r *Reader) Close()

Close closes the reader and releases resources.

func (*Reader) Fetch

func (r *Reader) Fetch(ctx context.Context, maxMessages int) ([]Message, error)

Fetch fetches up to maxMessages from the assigned partitions. If previous Fetch was not acknowledged, returns the same messages. Returns ErrReaderClosed if the reader has been closed.

type ReaderConfig

type ReaderConfig struct {
	// PartitionResolver maps partition index to stream and subject (required).
	PartitionResolver PartitionResolver

	// BufferSize is the size of the internal message buffer.
	BufferSize int

	// SessionTimeout is how long before the consumer is considered dead
	// if no heartbeat is received.
	SessionTimeout time.Duration

	// RebalanceTimeout is how long to wait for consumers to rejoin during rebalance.
	RebalanceTimeout time.Duration

	// HeartbeatInterval is how often to send heartbeats to the coordinator.
	HeartbeatInterval time.Duration

	// RequestTimeout is the timeout for coordinator RPC requests.
	RequestTimeout time.Duration

	// FetchWaitTime is how long to wait for messages when fetching.
	FetchWaitTime time.Duration

	// FetchBatchTimeout is the timeout for collecting additional messages after
	// the first message is received.
	FetchBatchTimeout time.Duration

	// SubjectPrefix is the prefix for coordinator RPC subjects.
	SubjectPrefix string

	// Assigner is the partition assignment strategy.
	Assigner cg.Assigner

	// Logger is used for logging errors.
	Logger *slog.Logger

	// Hooks holds optional callbacks for reader observability.
	Hooks ReaderHooks
}

ReaderConfig holds configuration options for a Reader.

type ReaderHooks

type ReaderHooks struct {
	// PartitionsAssigned is called when partitions are assigned to this reader.
	PartitionsAssigned func(partitions []int)
	// PartitionsRevoked is called when partitions are revoked from this reader.
	PartitionsRevoked func(partitions []int)
}

ReaderHooks holds optional callbacks for reader observability.

type ReaderOption

type ReaderOption func(*ReaderConfig)

ReaderOption is a function that modifies ReaderConfig.

func WithAssigner

func WithAssigner(a cg.Assigner) ReaderOption

WithAssigner sets a custom partition assigner.

func WithBufferSize

func WithBufferSize(size int) ReaderOption

WithBufferSize sets the internal message buffer size.

func WithFetchBatchTimeout

func WithFetchBatchTimeout(d time.Duration) ReaderOption

WithFetchBatchTimeout sets the timeout for collecting additional messages after the first message is received.

func WithFetchWaitTime

func WithFetchWaitTime(d time.Duration) ReaderOption

WithFetchWaitTime sets the fetch wait time.

func WithHeartbeatInterval

func WithHeartbeatInterval(d time.Duration) ReaderOption

WithHeartbeatInterval sets the heartbeat interval.

func WithPartitionResolver

func WithPartitionResolver(resolver PartitionResolver) ReaderOption

WithPartitionResolver sets the partition resolver function.

func WithReaderHooks

func WithReaderHooks(h ReaderHooks) ReaderOption

WithReaderHooks sets the hooks for the reader.

func WithReaderLogger

func WithReaderLogger(logger *slog.Logger) ReaderOption

WithReaderLogger sets the logger for the reader.

func WithReaderSubjectPrefix

func WithReaderSubjectPrefix(prefix string) ReaderOption

WithReaderSubjectPrefix sets the subject prefix for coordinator RPC.

func WithRebalanceTimeout

func WithRebalanceTimeout(d time.Duration) ReaderOption

WithRebalanceTimeout sets the rebalance timeout.

func WithRequestTimeout

func WithRequestTimeout(d time.Duration) ReaderOption

WithRequestTimeout sets the request timeout.

func WithSessionTimeout

func WithSessionTimeout(d time.Duration) ReaderOption

WithSessionTimeout sets the session timeout.

Directories

Path Synopsis
Package cg provides consumer group coordination for partitioned message consumption.
Package cg provides consumer group coordination for partitioned message consumption.

Jump to

Keyboard shortcuts

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