consumer

package
v0.3.0-20260708152012-... Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

Consumer

The consumer package orchestrates queue message processing. It manages subscription lifecycle, message consumption, ack/nack, and graceful shutdown.

Architecture

Consumer
  ├── Controller A (topic: "request")
  │     └── consumeLoop
  │           ├── processPartition("part-1")  ← serial per partition
  │           ├── processPartition("part-2")
  │           └── processPartition("part-3")
  └── Controller B (topic: "build")
        └── consumeLoop
              └── processPartition("part-1")

The consumer spawns one consumeLoop goroutine per controller. Each consumeLoop dispatches deliveries to per-partition goroutines, preserving ordering within each partition while processing different partitions in parallel.

Interfaces

Consumer

The top-level orchestrator. Register controllers, start consuming, and stop gracefully.

registry, _ := consumer.NewTopicRegistry([]consumer.TopicConfig{
    {Key: topickey.TopicKeyStart, Name: "request", Queue: q, Subscription: subConfig},
})

c := consumer.New(logger, scope, registry,
    errs.NewClassifierProcessor(
        genericerrs.Classifier,
        mysqlerrs.Classifier,
    ),
)

c.Register(myController)
c.Start(ctx)

// On shutdown:
if err := c.Stop(30000); err != nil {
    logger.Errorw("consumer stop error", "error", err)
}

The fourth argument is the errs.ErrorProcessor the consumer runs over every non-nil controller error before deciding ack/nack/reject. See platform/errs/README.md for the contract; in short, a primary pipeline consumer takes errs.NewClassifierProcessor(...) with the project's standard classifiers, and a DLQ-reconciliation consumer takes errs.AlwaysRetryableProcessor.

Controller

Business logic for processing queue messages. Implement this interface to handle deliveries for a specific topic.

type Controller interface {
    Process(ctx context.Context, delivery Delivery) error
    Name() string
    TopicKey() TopicKey
    ConsumerGroup() string
}
Delivery

A restricted view of a queue delivery exposed to controllers. Hides Ack/Nack/Reject (handled automatically by Consumer) while exposing message data and ExtendVisibilityTimeout.

TopicRegistry

The TopicRegistry maps topic keys to queue backends, topic names, and subscription configs. This decouples controllers from infrastructure wiring.

registry, _ := consumer.NewTopicRegistry([]consumer.TopicConfig{
    {
        Key:          topickey.TopicKeyStart,
        Name:         "request",
        Queue:        q,
        Subscription: extqueue.DefaultSubscriptionConfig("worker-1", "orchestrator"),
    },
    {
        Key:   topickey.TopicKeyBuild,
        Name:  "build",
        Queue: q,
        // No Subscription — publish-only topic
    },
})

Topic keys are fixed identifiers for pipeline stages (e.g., TopicKeyStart, TopicKeyBuild). Constants live in each domain's core/topickey package; this package defines only the TopicKey type and registry machinery. The actual queue topic name is configured separately, so library consumers can use their own naming conventions.

Error Handling

The consumer passes every non-nil controller error through the configured errs.ErrorProcessor once and then uses errs.IsRetryable to decide the transport action:

  • return nil — success, message is acked.
  • non-nil, retryable after processing — message is nacked for redelivery (visibility timeout drives the retry delay).
  • non-nil, non-retryable after processing — message is rejected, which moves it to the DLQ if one is configured for the subscription, or simply acks-and-drops if not.

Controllers therefore have two equally valid ways to surface a transient failure:

  1. Return an unclassified error and let a classifier wired into the processor recognise it (e.g. a *gomysql.MySQLError with a deadlock code → mysqlerrs.Classifier → retryable).
  2. Return errs.NewRetryableError(...) (or NewUserError, NewDependencyError, ...) explicitly when the controller already knows the right verdict — these framework wraps short-circuit any classifier walk.
func (c *MyController) Process(ctx context.Context, delivery consumer.Delivery) error {
    msg := delivery.Message()

    result, err := c.service.Process(ctx, msg.Payload)
    if err != nil {
        if isUserCaused(err) {
            return errs.NewUserError(err)   // reject → DLQ, never retried
        }
        return err                          // let the processor classify; nack if retryable
    }

    return nil  // ack → done
}

When the consumer is wired with errs.AlwaysRetryableProcessor (DLQ reconciliation), the framework overrides this: every non-nil error is forced retryable so the DLQ message comes back for another attempt. See submitqueue/orchestrator/controller/dlq/README.md.

Lifecycle

  1. Register controllers before starting.
  2. Start subscribes to all topics and spawns consume loops. Startup is atomic — if any subscription fails, all started subscriptions are cleaned up.
  3. Stop cancels all subscriptions and waits for goroutines to finish (with timeout budget split across controllers).

Once stopped, the consumer cannot be restarted — Register() and Start() return errors.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ValidateTopicName

func ValidateTopicName(name string) error

ValidateTopicName ensures a topic name is valid. Topic names must be non-empty, at most 255 characters, and contain only lowercase letters, numbers, underscores, and hyphens.

Types

type Consumer

type Consumer interface {
	// Register adds a controller to the consumer. Must be called before Start().
	Register(controller Controller) error

	// Start subscribes to all registered controllers' topics and begins consuming messages.
	// Context is cancelled when the consumer is stopped, the implementation should propagate it to the controllers
	// running message processing. The implementation can react immediately to the context cancellation by returning `ctx.Err()` instead of starting the message processing,
	// but can also opt out to defer the cancellation after the message processing routine is set up.
	// Start() will only be called once at the application startup, so it does not need to be idempotent.
	Start(ctx context.Context) error

	// Stop gracefully shuts down all controllers with the specified timeout.
	// timeoutMs is the maximum time in milliseconds to wait for graceful shutdown.
	// Returns error if shutdown times out.
	// Stop() will only be called once at the application shutdown, so it does not need to be idempotent.
	Stop(timeoutMs int64) error
}

Consumer orchestrates multiple queue consumers. It handles subscription lifecycle, message consumption, ack/nack, and graceful shutdown for the entire pipeline. Start(), Register() and Stop() are always called in this order so they do not need to be concurrently-safe between one another, but the implementation must be thread-safe between message processing and Register()/Stop() operations.

func New

func New(logger *zap.SugaredLogger, scope tally.Scope, registry TopicRegistry, processor errs.ErrorProcessor) Consumer

New creates a new consumer.

registry provides queue and subscription config for topics. processor is the error-classification policy applied exactly once per failing controller return — typically errs.NewClassifierProcessor(...) for primary pipeline consumers (per-node classifier walk that preserves controller-attached framework wraps), or errs.AlwaysRetryableProcessor for narrowly-scoped consumers such as DLQ reconciliation that must redeliver on any failure. processor must not be nil; callers that genuinely want no transformation can pass errs.NewClassifierProcessor() with no classifiers.

type Controller

type Controller interface {
	Process(ctx context.Context, delivery Delivery) error

	// Name returns the controller name for logging and metrics.
	Name() string

	// TopicKey returns the topic key this controller subscribes to.
	TopicKey() TopicKey

	// ConsumerGroup returns the consumer group for offset tracking.
	// Multiple controllers can share a consumer group to load-balance across workers.
	// Different consumer groups consume independently.
	ConsumerGroup() string
}

Controller processes queue deliveries. Controllers contain business logic and are registered with the Consumer. The Controller interface enables clean separation of concerns: - Controller focuses on business logic (deserialize, process, return error status) - Consumer handles infrastructure (subscription, ack/nack, metrics, lifecycle) The implementation of the controller should be idempotent and stateless. The controller is expected to be retried for the same message multiple times and should process side effects gracefully. The implementation must be thread-safe.

type Delivery

type Delivery interface {
	// Message returns the delivered message.
	Message() entityqueue.Message

	// ExtendVisibilityTimeout extends the time before this message becomes
	// visible to other consumers. Use when processing takes longer than expected.
	ExtendVisibilityTimeout(ctx context.Context, durationMillis int64) error

	// DeliveryID returns a backend-specific identifier for this delivery.
	DeliveryID() string

	// Attempt returns how many times this message has been delivered.
	// Starts at 1 for first delivery.
	Attempt() int

	// ReceivedAt returns when this delivery was received (Unix milliseconds).
	ReceivedAt() int64

	// Metadata returns backend-specific delivery metadata.
	Metadata() map[string]string
}

Delivery is the consumer package's view of a queue delivery. It exists to hide Ack/Nack from controllers — the Consumer framework handles those automatically based on the error returned from Process(). Controllers only see message data, metadata, and ExtendVisibilityTimeout (a business-level concern for long-running processing).

To signal outcome from Process():

  • Return nil to ack the message (success).
  • Return an error to nack the message for retry.
  • Return a non-retryable error to reject a poison pill message (removes it from the queue).

type TopicConfig

type TopicConfig struct {
	// Key is the fixed pipeline stage identifier.
	Key TopicKey
	// Name is the actual queue topic name (e.g. "request", "my-custom-request").
	Name string
	// Queue is the queue backend for this topic.
	Queue extqueue.Queue
	// Subscription is the subscription configuration for this topic.
	// Leave at zero value for publish-only topics.
	Subscription extqueue.SubscriptionConfig
}

TopicConfig combines all configuration for a single pipeline topic: the fixed key, the actual queue topic name, the queue backend, and (optionally) subscription settings.

type TopicKey

type TopicKey string

TopicKey identifies a pipeline stage. It is a fixed key used to look up queue backends, topic names, and subscription configs in the TopicRegistry. The actual queue topic name is provided separately via TopicConfig.Name so that library consumers can choose their own naming conventions.

func (TopicKey) String

func (t TopicKey) String() string

String returns the topic key as a string.

type TopicRegistry

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

TopicRegistry provides queue, topic name, and subscription config for topics. Each topic can have a different queue backend and topic name.

func NewTopicRegistry

func NewTopicRegistry(configs []TopicConfig) (TopicRegistry, error)

NewTopicRegistry creates a new TopicRegistry from a list of TopicConfigs. Returns an error if any topic name is invalid.

func (TopicRegistry) Queue

func (r TopicRegistry) Queue(key TopicKey) (extqueue.Queue, bool)

Queue returns the queue backend for the given topic key. Returns ok=false if no queue is registered for this key.

func (TopicRegistry) SubscriptionConfig

func (r TopicRegistry) SubscriptionConfig(key TopicKey, consumerGroup string) (extqueue.SubscriptionConfig, bool)

SubscriptionConfig returns the subscription configuration for the given topic key and consumer group. Returns ok=false if no configuration is registered.

func (TopicRegistry) TopicName

func (r TopicRegistry) TopicName(key TopicKey) (string, bool)

TopicName returns the actual queue topic name for the given key. Returns ok=false if no topic is registered for this key.

Directories

Path Synopsis
Package mock is a generated GoMock package.
Package mock is a generated GoMock package.

Jump to

Keyboard shortcuts

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