Documentation
¶
Overview ¶
Package subscription provides durable JetStream consumer management utilities.
Overview ¶
This package implements a single durable pull consumer per worker whose FilterSubjects list is hot-reloaded whenever the worker's assignment changes. This design minimizes JetStream control-plane churn while preserving precise subject filtering for partition isolation.
Key Types
- WorkerConsumer: Manages the single durable consumer lifecycle, calculates deterministic subject sets from partitions (template expansion, dedupe, sort), applies changes atomically via CreateOrUpdateConsumer, and runs a resilient pull loop with heartbeat tolerance.
- MessageHandler / MessageHandlerFunc: Minimal interface for user message processing, enabling dependency injection and straightforward testing.
WorkerConsumer Highlights
- Hot-reload semantics: Pull loop is started once and not restarted on subject changes, avoiding message processing gaps.
- Deterministic subject ordering: Subjects are deduped and sorted to prevent unnecessary consumer updates due to ordering differences.
- Best-effort old durable cleanup: When workerID changes (e.g., stable ID reallocation) the previous durable is deleted asynchronously to prevent orphaned consumers accumulating.
- Batched pulls: Uses Messages() iterator with configurable batch size and heartbeat interval for efficient streaming without busy polling.
- Minimal defaults: Sensible configuration defaults applied automatically.
Usage Pattern
helper, err := subscription.NewWorkerConsumer(js, subscription.WorkerConsumerConfig{
StreamName: "events-stream",
ConsumerPrefix: "worker",
SubjectTemplate: "events.{{.PartitionID}}",
}, subscription.MessageHandlerFunc(func(ctx context.Context, msg jetstream.Msg) error {
// process message
return msg.Ack()
}))
if err != nil { log.Fatal(err) }
defer helper.Close(context.Background())
// Apply initial assignment
_ = helper.UpdateWorkerConsumer(ctx, "worker-7", partitions)
// Later, after a rebalance shrinks assignment
_ = helper.UpdateWorkerConsumer(ctx, "worker-7", smallerPartitions)
Interface Assertions Internal compile-time assertions live in internal packages; public packages avoid importing root to prevent cycles. WorkerConsumer deliberately omits an interface assertion; users can add one in their own test code if desired.
Concurrency & Safety All exported WorkerConsumer methods are safe for concurrent use. UpdateWorkerConsumer employs fine-grained locking and performs diff checks before mutating state to minimize unnecessary JetStream calls.
See worker_consumer_extended_test.go for integration scenarios covering lifecycle, expansion, workerID switches, external deletion, and concurrency.
Index ¶
- Constants
- Variables
- func NewSimpleMessageTracker() *simpleTracker
- type AuditResult
- type BroadcastConsumer
- type BroadcastConsumerConfig
- type ClaimBasedResolver
- func (r *ClaimBasedResolver) ForceRefreshPartition(ctx context.Context, partitionID string) error
- func (r *ClaimBasedResolver) GetOwner(partitionID string) (string, types.HandoffState, int64, bool)
- func (r *ClaimBasedResolver) SetBatching(window time.Duration, maxItems int)
- func (r *ClaimBasedResolver) SetMetrics(m ResolverMetrics)
- func (r *ClaimBasedResolver) Start(ctx context.Context) error
- func (r *ClaimBasedResolver) Stop()
- type GateMetrics
- type MessageHandler
- type MessageHandlerFunc
- type MessageTracker
- type NAKReason
- type ProcessingGateConfig
- type ResolverConfig
- type ResolverMetrics
- type RetryConfig
- type TrackerPolicy
- type WorkerConsumer
- func (wc *WorkerConsumer) Close(ctx context.Context) error
- func (wc *WorkerConsumer) SetResolverMetrics(m ResolverMetrics)
- func (wc *WorkerConsumer) SubjectConsumerInfos(ctx context.Context, subjects []string) (map[string]*jetstream.ConsumerInfo, error)
- func (wc *WorkerConsumer) UpdateWorkerConsumer(ctx context.Context, workerID string, partitions []types.Partition) error
- func (wc *WorkerConsumer) WorkerConsumerInfo(ctx context.Context, subject string) (*jetstream.ConsumerInfo, error)
- func (wc *WorkerConsumer) WorkerSubjects() []string
- type WorkerConsumerConfig
Constants ¶
const ( // DefaultBatchSize is the default number of messages to fetch per pull request. DefaultBatchSize = 1 // DefaultMaxWaiting is the default maximum number of outstanding pull requests. // This is set conservatively to 2 to prevent "thundering herd" issues when // managing many partitions per worker. DefaultMaxWaiting = 2 // DefaultFetchTimeout is the default maximum duration to wait for messages. DefaultFetchTimeout = 5 * time.Second // DefaultMaxRetries is the default maximum number of retry attempts. DefaultMaxRetries = 3 // DefaultRetryBackoff is the default duration between retry attempts. DefaultRetryBackoff = 100 * time.Millisecond // DefaultRetryBase is the default base backoff for decorrelated jitter. DefaultRetryBase = 200 * time.Millisecond // DefaultRetryMultiplier is the default multiplier used to grow backoff. DefaultRetryMultiplier = 1.6 // DefaultRetryMax is the default maximum backoff cap for control-plane retries. DefaultRetryMax = 5 * time.Second // DefaultMaxControlRetries is the default maximum control-plane retry attempts. DefaultMaxControlRetries = 6 // DefaultAckWait is the default duration to wait for acknowledgment. DefaultAckWait = 30 * time.Second // DefaultMaxDeliver is the default maximum delivery attempts. DefaultMaxDeliver = -1 // DefaultInactiveThreshold is the default inactive consumer cleanup threshold. DefaultInactiveThreshold = 24 * time.Hour // DefaultMaxSubjects is the default cap for filter subjects applied to a worker consumer. DefaultMaxSubjects = 3000 // DefaultHealthFailureThreshold is the default consecutive failure threshold // before considering the worker consumer unhealthy. DefaultHealthFailureThreshold = 3 // DefaultIteratorEscalationWindow is the sliding time window used to detect // a burst of iterator failures warranting escalation (consumer refresh). DefaultIteratorEscalationWindow = 60 * time.Second // DefaultIteratorEscalationThreshold is the number of consecutive iterator // failures within the escalation window required to trigger a single // escalation event (refresh) per window. DefaultIteratorEscalationThreshold = 3 )
Default configuration values for WorkerConsumer.
Variables ¶
var ErrMaxSubjectsExceeded = errors.New("worker consumer subjects exceed MaxSubjects cap")
ErrMaxSubjectsExceeded indicates the requested subjects exceed MaxSubjects.
var ErrWorkerIDMutation = errors.New("workerID mutation is not allowed by configuration")
ErrWorkerIDMutation indicates workerID changed while AllowWorkerIDChange=false.
Functions ¶
func NewSimpleMessageTracker ¶
func NewSimpleMessageTracker() *simpleTracker
NewSimpleMessageTracker constructs the concrete tracker.
Types ¶
type AuditResult ¶
type AuditResult struct {
Partition string
LostMessages int
UnresolvedHoles int
Duration time.Duration
TimedOut bool
LateMessages int
}
AuditResult summarizes a drain audit outcome for a partition. LostMessages > 0 indicates invariant breach; UnresolvedHoles are sequences still missing at audit timeout.
type BroadcastConsumer ¶ added in v1.6.0
type BroadcastConsumer struct {
// contains filtered or unexported fields
}
BroadcastConsumer manages a single JetStream durable consumer per instance using a wildcard subject filter. All messages matching the filter are passed to the handler regardless of partition assignment.
Stream Requirement ¶
The underlying stream MUST use LimitsPolicy or InterestPolicy. WorkQueuePolicy is incompatible with broadcast pattern because it delivers each message to exactly one consumer, preventing fan-out.
Thread Safety ¶
- UpdateWorkerConsumer is serialized; concurrent calls block on internal mutex
- Close is safe to call concurrently with UpdateWorkerConsumer
Message Handling ¶
- All messages matching the WildcardFilter are passed to the handler
- Messages are ACKed after successful handling (or ManualAck)
Blocking Behavior ¶
- Close may block up to the context deadline waiting for the pull loop to stop
func NewBroadcastConsumer ¶ added in v1.6.0
func NewBroadcastConsumer(js jetstream.JetStream, cfg BroadcastConsumerConfig, handler MessageHandler) (*BroadcastConsumer, error)
NewBroadcastConsumer creates a new broadcast fan-out consumer.
The consumer uses a wildcard FilterSubject to receive all messages. On first UpdateWorkerConsumer call, creates the durable consumer and starts the pull loop. The durable name is derived from ConsumerID.
Stream Requirement ¶
The underlying stream MUST use LimitsPolicy or InterestPolicy. WorkQueuePolicy is incompatible because it delivers each message to exactly one consumer, defeating the fan-out purpose.
func (*BroadcastConsumer) Close ¶ added in v1.6.0
func (bc *BroadcastConsumer) Close(ctx context.Context) error
Close stops the consumer loop. Consumer is left for server GC via InactiveThreshold.
func (*BroadcastConsumer) UpdateWorkerConsumer ¶ added in v1.6.0
func (bc *BroadcastConsumer) UpdateWorkerConsumer(ctx context.Context, workerID string, partitions []types.Partition) error
UpdateWorkerConsumer implements WorkerConsumerUpdater interface. It ensures the consumer loop is started using ConsumerID for the durable name.
The partitions argument is IGNORED as this consumer receives all messages matching the wildcard. Partition updates take effect strictly by ensuring the consumer is active.
type BroadcastConsumerConfig ¶ added in v1.6.0
type BroadcastConsumerConfig struct {
// StreamName is the JetStream stream name where subjects live. Required.
StreamName string `validate:"required"`
// ConsumerPrefix is the prefix for the durable consumer name.
// It must contain only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), or underscores (_).
// Final name format: <ConsumerPrefix>_broadcast_<consumerID>
// Required.
ConsumerPrefix string `validate:"required"`
// ConsumerID controls the identity used in the broadcast durable name.
//
// Accepted formats:
// - "fixed-string": Uses the literal value as the identity
// - "env:ENV_NAME": Uses the value of the specified environment variable
//
// If empty, the consumer will attempt to derive an identity from
// HOSTNAME, then POD_NAME, and finally fall back to a generated short ID.
ConsumerID string
// WildcardFilter is the JetStream FilterSubject for the consumer.
// Use ">" for multi-token partition IDs (recommended).
// Use "*" only for single-token partition IDs.
//
// Examples:
// - "orders.>" for partition IDs like "region.us-east"
// - "orders.*.events" for single-token partition IDs like "p1"
//
// Required.
WildcardFilter string `validate:"required"`
// Logger provides structured logging. Defaults to a no-op logger when nil.
Logger types.Logger
// Metrics is the global metrics collector used across the library.
// If nil, no metrics are emitted from the consumer.
Metrics types.MetricsCollector
// ManualAck, when true, disables the helper's automatic Ack/Nak behavior.
// Handlers are responsible for calling msg.Ack/Nak/Term and optionally
// msg.InProgress() to extend AckWait. This enables handler-controlled
// backpressure. Defaults to false.
ManualAck bool
// AckWait is the time allowed for processing before redelivery.
// Default: 30s.
AckWait time.Duration `default:"30s" validate:"gt=0"`
// MaxDeliver is the maximum redelivery attempts before moving to DLQ (if configured).
// Default: -1 (unlimited attempts).
MaxDeliver int `default:"-1" validate:"gte=-1"`
// BatchSize is the max number of messages to pull per iterator request.
// Default: 1.
BatchSize int `default:"1" validate:"gt=0"`
// FetchTimeout is the max time to wait when pulling a batch.
// Default: 5s.
FetchTimeout time.Duration `default:"5s" validate:"gt=0"`
// MaxWaiting caps outstanding pull requests for the durable consumer.
// Default: 2.
MaxWaiting int `default:"2" validate:"gt=0"`
// MaxAckPending limits the number of in-flight unacknowledged messages the server
// will allow. If zero, the server default is used.
MaxAckPending int `validate:"gte=0"`
// InactiveThreshold is how long an idle consumer is kept by the server before cleanup.
// Default: 24h.
InactiveThreshold time.Duration `default:"24h" validate:"gt=0"`
// AckPolicy controls JetStream ack policy. Defaults to AckExplicitPolicy.
AckPolicy jetstream.AckPolicy
// Retry configures backoff behavior for control-plane operations.
Retry RetryConfig
// IteratorFactory optionally overrides the iterator creation logic for testing or
// advanced customization. When nil, a default factory is used.
IteratorFactory func(cons jetstream.Consumer, batch int, expiry time.Duration) (jetstream.MessagesContext, error)
}
BroadcastConsumerConfig configures the broadcast fan-out consumer.
The broadcast consumer creates a single JetStream durable consumer per instance using a wildcard FilterSubject. All workers receive all messages and process them locally without partition filtering.
Stream Requirement ¶
The stream MUST use LimitsPolicy or InterestPolicy. WorkQueuePolicy is incompatible because it delivers each message to exactly one consumer, defeating the fan-out purpose. With WorkQueuePolicy, only one worker would receive each message instead of all workers receiving and filtering locally.
Required Fields ¶
- StreamName: JetStream stream name containing the subjects
- ConsumerPrefix: Prefix for durable name; final name is "<ConsumerPrefix>_broadcast_<consumerID>"
- WildcardFilter: JetStream FilterSubject for the consumer, e.g., "orders.>" for multi-token partition IDs
Optional tuning fields are documented inline below. Zero values are replaced by sensible defaults via SetDefaults().
func DefaultBroadcastConsumerConfig ¶ added in v1.6.0
func DefaultBroadcastConsumerConfig() BroadcastConsumerConfig
DefaultBroadcastConsumerConfig returns a BroadcastConsumerConfig with sensible defaults. Note: Required fields (StreamName, ConsumerPrefix, WildcardFilter) must still be set by the user.
func (*BroadcastConsumerConfig) SetDefaults ¶ added in v1.6.0
func (c *BroadcastConsumerConfig) SetDefaults() error
SetDefaults sets default values for the configuration.
func (*BroadcastConsumerConfig) Validate ¶ added in v1.6.0
func (c *BroadcastConsumerConfig) Validate() error
Validate checks configuration constraints.
type ClaimBasedResolver ¶
type ClaimBasedResolver struct {
// contains filtered or unexported fields
}
ClaimBasedResolver provides fast ownership lookups via a watch-backed KV cache. It implements types.OwnershipResolver.
func NewClaimBasedResolver ¶
func NewClaimBasedResolver(kv jetstream.KeyValue, prefix string, logger types.Logger) *ClaimBasedResolver
NewClaimBasedResolver constructs a resolver on a KV bucket. prefix should match claims prefix used in ClaimStore. Example: prefix="claims/" will watch pattern "claims/*" and strip prefix from keys for partition IDs.
func (*ClaimBasedResolver) ForceRefreshPartition ¶
func (r *ClaimBasedResolver) ForceRefreshPartition(ctx context.Context, partitionID string) error
ForceRefreshPartition performs a best-effort on-demand refresh for a specific partition. It fetches the claim directly from KV and updates the local cache immediately. Errors are returned but safe to ignore by callers that treat this as an optimization.
func (*ClaimBasedResolver) GetOwner ¶
func (r *ClaimBasedResolver) GetOwner(partitionID string) (string, types.HandoffState, int64, bool)
GetOwner returns ownership data for a partition from the cache.
This is a lock-free O(1) operation that reads from the atomic cache pointer.
Parameters:
- partitionID: Partition identifier to look up
Returns:
- string: Owner worker ID
- HandoffState: Current handoff state (Stable, Prepare, Commit)
- int64: Claim epoch number
- bool: true if partition was found in cache, false otherwise
func (*ClaimBasedResolver) SetBatching ¶
func (r *ClaimBasedResolver) SetBatching(window time.Duration, maxItems int)
SetBatching overrides the default batching behavior. Zero or negative values are ignored. Safe to call before or after Start.
func (*ClaimBasedResolver) SetMetrics ¶
func (r *ClaimBasedResolver) SetMetrics(m ResolverMetrics)
SetMetrics configures optional metrics recording for the resolver. Safe to call before or after Start.
type GateMetrics ¶
type GateMetrics interface {
// IncGateNAK increments the NAK counter by reason
// (e.g., "unknown_ownership", "non_owner", "owner_disallowed_state").
IncGateNAK(reason string)
// ObserveGateNakDelay records the NAK delay applied by the gate.
ObserveGateNakDelay(d time.Duration)
}
GateMetrics captures optional Processing Gate metrics.
Implementations should be non-blocking. When nil, metrics are not recorded.
type MessageHandler ¶
type MessageHandler interface {
// Handle processes a single message.
//
// In default mode, return nil for the helper to ACK, or an error for the helper to NAK.
// In ManualAck mode, the handler must call msg.Ack/Nak/Term exactly once and may
// periodically call msg.InProgress() to extend AckWait while long-running work completes.
Handle(ctx context.Context, msg jetstream.Msg) error
}
MessageHandler defines the contract for processing JetStream messages yielded by the WorkerConsumer pull loop.
Behavior summary:
- The helper fetches a message and calls Handle once per message.
- By default, when Handle returns nil, the helper automatically ACKs the message. When Handle returns a non-nil error, the helper NAKs the message.
- If ManualAck is enabled in WorkerConsumerConfig, the helper does not perform any disposition; the handler is responsible for calling msg.Ack/Nak/Term and may call msg.InProgress() periodically to extend AckWait while processing.
Backpressure and concurrency:
- The pull loop is single-threaded: it does not call Handle for the next message until the current Handle returns. Handlers can implement backpressure by blocking (e.g., when an internal work queue is full). In ManualAck mode, returning quickly is safe only if the handler arranges for a background worker to take over and eventually ACK/Nak/Term.
- If you return quickly without ManualAck, the helper will immediately ACK on success, so calling InProgress() afterwards has no effect.
Redelivery semantics:
- With AckExplicit policy, failing to ACK within AckWait causes redelivery. Use msg.InProgress() to extend the deadline when work takes longer than AckWait.
- Exactly-once is not guaranteed; design handlers to be idempotent.
Parameters:
- ctx: Request-scoped context for cancellation and deadlines
- msg: The JetStream message to process
Returns:
- error: nil on success; non-nil to indicate failure. In default mode, the helper NAKs on error.
Example (default mode):
var h MessageHandler = MessageHandlerFunc(func(ctx context.Context, msg jetstream.Msg) error {
// process msg.Data(); return error to NAK
return nil // helper will ACK
})
Example (manual ack mode with background worker):
var h MessageHandler = MessageHandlerFunc(func(ctx context.Context, msg jetstream.Msg) error {
// enqueue to internal queue; if full, block to apply backpressure
if !enqueue(msg) { return context.DeadlineExceeded }
return nil // do not Ack here; background worker will Ack/Nak/Term
})
type MessageHandlerFunc ¶
MessageHandlerFunc is a function adapter for MessageHandler.
type MessageTracker ¶
type MessageTracker interface {
// OnDelivery records arrival of a message sequence for a partition.
// publishTime is the server publish timestamp or derived header time.
OnDelivery(partition string, sequence uint64, publishTime time.Time)
// OnNAK records a negative acknowledgement and its reason.
OnNAK(partition string, sequence uint64, reason NAKReason)
// OnAck records successful final processing of a message sequence.
OnAck(partition string, sequence uint64)
// DrainAudit performs a bounded audit to classify unresolved holes as lost.
DrainAudit(ctx context.Context, partition string) (AuditResult, error)
}
MessageTracker defines lifecycle hooks used to enforce and measure consumer stability invariants. Methods are invoked by the consumer loop and processing gate. Implementations must be concurrency-safe.
func NewNoopMessageTracker ¶
func NewNoopMessageTracker() MessageTracker
NewNoopMessageTracker creates a no-op tracker implementation.
type NAKReason ¶
type NAKReason string
NAKReason enumerates reasons for issuing a negative acknowledgement that triggers redelivery. Initial set aligns with stability plan; may expand as implementation evolves.
const ( NAKReasonAssignment NAKReason = "assignment" // Partition not yet assigned / ownership gating NAKReasonHandoff NAKReason = "handoff" // Controlled handoff / leadership or cooldown transition NAKReasonBackoff NAKReason = "backoff" // Transient downstream/backpressure condition NAKReasonHealth NAKReason = "health" // Consumer unhealthy / degraded state NAKReasonOrdering NAKReason = "ordering" // Ordering deferral (waiting for earlier sequence) NAKReasonInit NAKReason = "init" // Initialization / cold start gating )
type ProcessingGateConfig ¶
type ProcessingGateConfig struct {
// Enabled toggles the gate.
// Default: false (gate disabled).
Enabled bool
// AllowedStates defines which handoff states permit message processing.
// If empty, defaults to [StateStable].
//
// Common configurations:
// - [StateStable]: Strict consistency (default). Messages only processed when ownership is stable.
// - [StateStable, StateHandoff]: Higher availability. Allows processing during handoff.
AllowedStates []types.HandoffState `validate:"omitempty,min=1"` //nolint:revive
// WarmupDuration, when >0, enables a warm-up phase during which only
// WarmupAllowedStates are permitted. After the duration elapses, AllowedStates
// take effect for steady-state processing.
WarmupDuration time.Duration `validate:"gte=0"`
// WarmupAllowedStates defines which states are permitted during the warm-up phase.
// If empty and WarmupDuration>0, defaults to Stable-only.
WarmupAllowedStates []types.HandoffState
// NakDelay is the base delay for NAK when the worker is not the owner or
// the state is disallowed.
// Default: 100ms.
NakDelay time.Duration `default:"100ms" validate:"gte=0"`
// NakJitter is a fractional jitter in [0.0, 1.0] applied to NakDelay.
// Default: 0.0 (no jitter).
NakJitter float64 `validate:"gte=0,lte=1"`
// Debug enables verbose logging for NAK decisions (non-owner, disallowed state,
// unknown ownership). Only effective when a logger is configured.
Debug bool
// Metrics optionally records processing gate metrics (NAK reasons, delays).
// When nil, no gate metrics are emitted.
Metrics GateMetrics
}
ProcessingGateConfig configures the Processing Gate behavior.
type ResolverConfig ¶
type ResolverConfig struct {
// OwnershipResolver (advanced) supplies custom ownership lookups.
// When nil and ProcessingGate.Enabled is true, a claim-based resolver
// is auto-created using HandoffBucketName/Prefix. When non-nil, it
// overrides the automatic resolver creation.
OwnershipResolver types.OwnershipResolver
// HandoffBucketName specifies the KV bucket name for handoff claims.
// When ProcessingGate.Enabled is true and this is set, WorkerConsumer
// will automatically get/create the KV bucket and start a claim resolver.
// Defaults to "parti-handoff" if empty and gate is enabled.
// Should match the HandoffBucket in parti.Config.KVBuckets for consistency.
HandoffBucketName string `default:"parti-handoff"`
// HandoffClaimsPrefix is the key prefix for handoff claims in the KV bucket.
// Defaults to "claims/" if empty and gate is enabled.
HandoffClaimsPrefix string `default:"claims/"`
// BatchWindow sets the resolver KV watch coalescing window for the
// auto-created claim-based resolver when ProcessingGate is enabled.
// If zero, a default (5ms) is used. Ignored when a custom OwnershipResolver is provided.
BatchWindow time.Duration `default:"5ms" validate:"gte=0"`
// BatchMaxItems caps the number of unique partition updates coalesced
// into a single apply. If zero, a default (1024) is used. Ignored when a custom
// OwnershipResolver is provided.
BatchMaxItems int `default:"1024" validate:"gt=0"`
}
ResolverConfig groups ownership resolver settings.
type ResolverMetrics ¶
type ResolverMetrics interface {
// ObserveVisibilityLag records the lag between claim LastUpdated and
// when the resolver observes the update (watcher delivery time).
ObserveVisibilityLag(d time.Duration)
// SetCacheSize sets the current resolver cache size (number of partitions
// with visible claims).
SetCacheSize(n int)
// IncUpdate increments update counters by operation type
// (e.g., "upsert", "delete").
IncUpdate(op string)
// ObserveBatch records properties of an applied batch: the number of
// coalesced items and how long it took to apply to the cache.
ObserveBatch(size int, applyDuration time.Duration)
// IncBatchFlush increments a counter for batch flush events by reason
// (e.g., "timer", "maxitems").
IncBatchFlush(reason string)
}
ResolverMetrics captures optional ownership resolver metrics. Implementations should be non-blocking. When nil, metrics are not recorded.
type RetryConfig ¶
type RetryConfig struct {
// Backoff is the delay between retries for control-plane operations.
// Default: DefaultRetryBackoff (100ms).
Backoff time.Duration `default:"100ms" validate:"gte=0"`
// Max caps the jittered backoff.
// Default: DefaultRetryMax (5s).
Max time.Duration `default:"5s" validate:"gte=0,gtefield=Backoff"`
// Multiplier grows the backoff window for decorrelated jitter.
// Default: DefaultRetryMultiplier (1.6).
Multiplier float64 `default:"1.6" validate:"gte=1"`
// Base is the base backoff used for decorrelated jitter retries for control-plane ops.
// Default: DefaultRetryBase (200ms). If zero and Backoff is set, Base falls back to Backoff.
Base time.Duration `default:"200ms" validate:"gte=0"`
// Seed optionally seeds the jitter RNG for deterministic tests; when zero, a random seed is used.
Seed int64
}
RetryConfig groups retry backoff settings.
type TrackerPolicy ¶
type TrackerPolicy interface {
// ConfigurePolicy sets latency classification policy.
ConfigurePolicy(latencyThreshold time.Duration, strict bool, allowLateTail int)
}
TrackerPolicy allows configuring latency-related policy on a tracker implementation. Implementations that do not support policy configuration may ignore it.
type WorkerConsumer ¶
type WorkerConsumer struct {
// contains filtered or unexported fields
}
WorkerConsumer manages one JetStream durable pull consumer per subject (partition). It preserves per-subject cursors across reassignments and never deletes durables; idle consumers are reclaimed by JetStream via InactiveThreshold.
Thread Safety:
- UpdateWorkerConsumer is serialized; concurrent calls block on an internal mutex
- Close is safe to call concurrently with UpdateWorkerConsumer
- All other public methods are safe for concurrent use
Blocking Behavior:
- UpdateWorkerConsumer may block up to DrainOnRemoveTimeout (default 10s) when removing subjects with DrainOnRemove enabled
- Close may block up to the context deadline waiting for active pull loops to stop
func NewWorkerConsumer ¶
func NewWorkerConsumer(js jetstream.JetStream, cfg WorkerConsumerConfig, handler MessageHandler) (*WorkerConsumer, error)
NewWorkerConsumer creates a new per-subject durable consumer helper.
func (*WorkerConsumer) Close ¶
func (wc *WorkerConsumer) Close(ctx context.Context) error
Close cancels all subject loops. Consumers are left intact for server GC.
func (*WorkerConsumer) SetResolverMetrics ¶
func (wc *WorkerConsumer) SetResolverMetrics(m ResolverMetrics)
SetResolverMetrics sets optional metrics for the auto-created claim resolver. If called before the resolver is initialized, the metrics will be applied on creation.
func (*WorkerConsumer) SubjectConsumerInfos ¶
func (wc *WorkerConsumer) SubjectConsumerInfos(ctx context.Context, subjects []string) (map[string]*jetstream.ConsumerInfo, error)
SubjectConsumerInfos returns ConsumerInfo for each provided subject's durable. The call is fail-fast: if retrieving info for any subject fails, it returns the error and no partial map. Callers can split or retry specific subjects if needed.
func (*WorkerConsumer) UpdateWorkerConsumer ¶
func (wc *WorkerConsumer) UpdateWorkerConsumer(ctx context.Context, workerID string, partitions []types.Partition) error
UpdateWorkerConsumer applies the per-subject assignment set. It creates/binds durables for added subjects and starts pull loops; it cancels loops for removed subjects but does not delete the underlying consumer (server will GC by inactivity).
func (*WorkerConsumer) WorkerConsumerInfo ¶
func (wc *WorkerConsumer) WorkerConsumerInfo(ctx context.Context, subject string) (*jetstream.ConsumerInfo, error)
WorkerConsumerInfo returns the ConsumerInfo for a specific subject's durable. It manages one durable per subject, so callers must specify the subject. If the consumer isn't currently tracked in-memory, this will create/bind it to return info, matching contract that durables are never deleted proactively.
func (*WorkerConsumer) WorkerSubjects ¶
func (wc *WorkerConsumer) WorkerSubjects() []string
WorkerSubjects returns a sorted copy of the currently managed per-subject keys. Returns nil when no subjects are active.
type WorkerConsumerConfig ¶
type WorkerConsumerConfig struct {
// StreamName is the JetStream stream name where subjects live. Required.
StreamName string `validate:"required"`
// ConsumerPrefix is the prefix for the durable consumer name.
// It must contain only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), or underscores (_).
// Required.
ConsumerPrefix string `validate:"required"`
// SubjectTemplate is a text/template used to build subjects from a partition.
// Uses Go's text/template syntax.
//
// Available template fields:
// - {{.PartitionID}}: Partition keys joined with "." (equals partition.SubjectKey())
//
// Note: Only PartitionID is available. Other Partition fields (Keys, Weight) are not
// directly accessible in the template.
//
// Example: "orders.{{.PartitionID}}.events" with Keys=["region", "us-east"] produces
// "orders.region.us-east.events"
//
// Required.
SubjectTemplate string `validate:"required"`
// Logger provides structured logging. Defaults to a no-op logger when nil.
Logger types.Logger
// Metrics is the global metrics collector used across the library.
// If nil, no metrics are emitted from the worker consumer helper.
Metrics types.MetricsCollector
// ManualAck, when true, disables the helper's automatic Ack/Nak behavior.
// Handlers are responsible for calling msg.Ack/Nak/Term and optionally
// msg.InProgress() to extend AckWait. This enables handler-controlled
// backpressure via an internal work queue. Defaults to false.
//
// Recommendation: Enable this for any asynchronous processing (e.g. spawning goroutines).
// Keep false (default) for simple synchronous handlers to ensure safety.
ManualAck bool
// ProcessingGate configures optional exclusive processing enforcement.
// When enabled, WorkerConsumer automatically creates and manages a claim-based
// ownership resolver backed by the handoff KV bucket. The resolver lifecycle
// (start/stop) is handled internally - no manual management needed.
//
// Recommendation: Enable this for stateful workloads or when strict partition
// exclusivity is required. Without this, ownership transitions may be "loose",
// resulting in brief periods where a partition is processed by a worker that
// is no longer the assigned owner (though duplicates are rare).
ProcessingGate *ProcessingGateConfig `validate:"omitempty"` //nolint:revive
// Resolver configures the ownership resolver when ProcessingGate is enabled.
Resolver ResolverConfig
// PullGatingEnabled enables pre-pull ownership/state gating for per-subject consumers.
// When true, pulls are suppressed (not issued) if the current worker is not the owner or
// the handoff state is not Commit/Stable. Reduces NAK churn during handoffs.
PullGatingEnabled bool
// DrainOnRemove enables a graceful per-subject drain when a subject is removed
// from the worker's assignment. When true, the consumer helper will stop issuing
// new pulls for the subject and wait for pending acknowledgements to reach zero
// (or until DrainOnRemoveTimeout elapses) before cancelling the loop. This reduces
// NAK churn and minimizes gaps during scale-down and rebalancing.
// Default: false (immediate cancellation as before).
DrainOnRemove bool
// DrainOnRemoveTimeout caps the drain wait per subject when DrainOnRemove is enabled.
// Default: 10s when zero.
DrainOnRemoveTimeout time.Duration `default:"10s" validate:"gte=0"`
// AckWait is the time allowed for processing before redelivery.
// Default: DefaultAckWait (30s).
AckWait time.Duration `default:"30s" validate:"gt=0"`
// MaxDeliver is the maximum redelivery attempts before moving to DLQ (if configured).
// Default: DefaultMaxDeliver (-1 = unlimited attempts; relies on server default/unlimited behavior).
MaxDeliver int `default:"-1" validate:"gte=-1"`
// BatchSize is the max number of messages to pull per iterator request.
// Default: DefaultBatchSize (1).
BatchSize int `default:"1" validate:"gt=0"`
// FetchTimeout is the max time to wait when pulling a batch.
// Default: DefaultFetchTimeout (5s).
FetchTimeout time.Duration `default:"5s" validate:"gt=0"`
// MaxWaiting caps outstanding pull requests for each per-subject durable.
// Default: DefaultMaxWaiting (2).
MaxWaiting int `default:"2" validate:"gt=0"`
// MaxAckPending limits the number of in-flight unacknowledged messages the server will allow
// for each per-subject durable. If zero, the server default is used. This is most useful when using
// ManualAck and background processing to cap concurrent in-flight work at the server layer.
MaxAckPending int `validate:"gte=0"`
// MaxConcurrentSubjects caps the total number of per-subject consumers/loops.
// When exceeded, additional subjects are skipped with a warning and metric increment.
MaxConcurrentSubjects int `validate:"gte=0"`
// AckPolicy controls JetStream ack policy. Defaults to AckExplicitPolicy.
AckPolicy jetstream.AckPolicy
// InactiveThreshold is how long an idle consumer is kept by the server before cleanup.
// Default: DefaultInactiveThreshold (24h).
InactiveThreshold time.Duration `default:"24h" validate:"gt=0"`
// PartitionRefreshMinInterval sets the minimum interval between forced claim refreshes
// per partition when pull gating is enabled. This throttles KV hits when many
// subjects are suppressed. Default: 500ms when zero.
PartitionRefreshMinInterval time.Duration `default:"500ms" validate:"gt=0"`
// Retry configures backoff behavior for control-plane operations.
Retry RetryConfig
// IteratorEscalationWindow defines the sliding time window used to aggregate
// iterator failures for escalation detection. If zero, defaults to
// DefaultIteratorEscalationWindow.
IteratorEscalationWindow time.Duration `default:"60s" validate:"gt=0"`
// IteratorEscalationThreshold is the number of iterator failures within the
// escalation window that triggers a single escalation (consumer refresh).
// If zero, defaults to DefaultIteratorEscalationThreshold.
IteratorEscalationThreshold int `default:"3" validate:"gt=0"`
// AllowWorkerIDChange controls whether workerID changes are allowed after initialization.
// Default: false (immutable once set). Intended for controlled migrations only.
AllowWorkerIDChange bool
// IteratorFactory optionally overrides the iterator creation logic for testing or
// advanced customization. When nil, a default factory is used that configures
// heartbeat and expiry based on BatchSize and FetchTimeout.
IteratorFactory func(cons jetstream.Consumer, batch int, expiry time.Duration) (jetstream.MessagesContext, error)
}
WorkerConsumerConfig configures the single durable per-worker consumer helper.
Required fields:
- StreamName: JetStream stream name containing the subjects
- ConsumerPrefix: Prefix for durable name; final name is "<ConsumerPrefix>-<workerID>"
- SubjectTemplate: text/template expanded per partition with {{.PartitionID}}
Optional tuning fields are documented inline below. Zero values are replaced by sensible defaults via SetDefaults().
func DefaultWorkerConsumerConfig ¶ added in v1.1.0
func DefaultWorkerConsumerConfig() WorkerConsumerConfig
DefaultWorkerConsumerConfig returns a WorkerConsumerConfig with sensible defaults. Note: Required fields (StreamName, ConsumerPrefix, SubjectTemplate) must still be set by the user.
func (*WorkerConsumerConfig) SetDefaults ¶ added in v1.2.0
func (c *WorkerConsumerConfig) SetDefaults() error
SetDefaults sets default values for the configuration.
func (*WorkerConsumerConfig) Validate ¶ added in v1.2.0
func (c *WorkerConsumerConfig) Validate() error
Validate checks configuration constraints.
Source Files
¶
- backoff.go
- broadcast_config.go
- broadcast_consumer.go
- claim_resolver.go
- config.go
- constants.go
- doc.go
- errors.go
- gate_metrics.go
- handler.go
- message_tracker.go
- metrics.go
- partition_consumer.go
- processing_gate.go
- resolver_metrics.go
- subject_utils.go
- worker_consumer.go
- worker_consumer_info.go
- worker_consumer_test_helpers.go