Documentation
¶
Overview ¶
Package parti is a Go library for building partitioned workloads on NATS. It provides a complete toolkit for sharding work across workers: dynamic partitioning with leader-coordinated rebalancing, static partitioning for fixed-topology deployments (e.g. Kubernetes StatefulSets), and resilient JetStream consumers with auto-recovery from durable deletion.
Its headline capability is solving the coordination gap NATS leaves open when both the worker fleet and the partition set change at runtime — providing stable worker identities, cache-affinity-aware rebalancing, and adaptive stabilization without external coordination services. Static partitioning and consumer auto-recovery live in the partition and consumer subpackages respectively.
Quick Start ¶
Basic usage with default settings:
import (
"github.com/arloliu/parti/v2"
"github.com/arloliu/parti/v2/source"
"github.com/arloliu/parti/v2/strategy"
)
cfg := parti.Config{
WorkerIDPrefix: "worker",
WorkerIDMin: 0,
WorkerIDMax: 999,
}
partitions := []parti.Partition{{Keys: []string{"0"}}, {Keys: []string{"1"}}, {Keys: []string{"2"}}}
src := source.NewStatic(partitions)
// Connect with MaxReconnects(-1) so the client rides through transient
// NATS outages instead of going CLOSED. See docs/OPERATIONS.md
// "NATS Client Connection" for the full posture.
natsConn, _ := nats.Connect(natsURL, nats.MaxReconnects(-1), nats.RetryOnFailedConnect(true))
js, _ := jetstream.New(natsConn)
assignmentStrategy := strategy.NewConsistentHash()
mgr, _ := parti.NewManager(&cfg, js, src, assignmentStrategy)
if err := mgr.Start(ctx); err != nil {
log.Fatal(err)
}
// Start returns once the synchronous sanity-check phase succeeds.
// Wait for the background runner to apply the initial assignment.
if err := <-mgr.WaitState(parti.StateStable, 30*time.Second); err != nil {
log.Fatal(err)
}
defer mgr.Stop(context.Background())
Key Features ¶
- Stable Worker IDs: Workers claim stable IDs for consistent assignment during rolling updates
- Leader-Based Assignment: One worker calculates assignments without external coordination
- Adaptive Rebalancing: Different stabilization windows for cold start (30s) vs planned scale (10s)
- Cache Affinity: Preserves >80% partition locality during rebalancing
- Weighted Assignment: Supports partition weights for load balancing
- Static Partitioning: Zero-coordination routing for StatefulSet-style deployments (partition subpackage)
- Consumer Auto-Recovery: JetStream consumers detect and recreate deleted durables automatically (consumer subpackage)
Architecture ¶
Workers progress through a state machine:
INIT → CLAIMING_ID → ELECTION → WAITING_ASSIGNMENT → STABLE
The leader monitors heartbeats, detects topology changes, and publishes new assignments. All workers watch for assignment updates and trigger callbacks when their partitions change.
Advanced Usage ¶
Custom strategy with options:
import (
"github.com/arloliu/parti/v2"
"github.com/arloliu/parti/v2/source"
"github.com/arloliu/parti/v2/strategy"
)
assignmentStrategy := strategy.NewConsistentHash(
strategy.WithVirtualNodes(300),
)
hooks := &parti.Hooks{
OnAssignmentChanged: func(ctx context.Context, oldPartitions, newPartitions []parti.Partition) error {
// Handle full assignment change; derive added/removed by diffing old vs new if needed.
return nil
},
}
partitions := []parti.Partition{{Keys: []string{"0"}}, {Keys: []string{"1"}}, {Keys: []string{"2"}}}
src := source.NewStatic(partitions)
js, _ := jetstream.New(natsConn)
mgr, _ := parti.NewManager(&cfg, js, src, assignmentStrategy,
parti.WithHooks(hooks),
)
See the examples/ directory for complete working examples.
Index ¶
- Constants
- Variables
- func BuildControlPlaneKVConfig(bucket string, ttl time.Duration, storage jetstream.StorageType) jetstream.KeyValueConfig
- func SetDefaults(cfg *Config) error
- type AlertLevel
- type Assignment
- type AssignmentDivergenceMetricsRecorder
- type AssignmentMetrics
- type AssignmentStrategy
- type AuthorityChoice
- type CalculatorMetrics
- type CapabilityReporter
- type CompositeConsumerUpdater
- func (c *CompositeConsumerUpdater) Add(updaters ...WorkerConsumerUpdater)
- func (c *CompositeConsumerUpdater) Capabilities() uint32
- func (c *CompositeConsumerUpdater) Len() int
- func (c *CompositeConsumerUpdater) ObserveWorkerCount(n int)
- func (c *CompositeConsumerUpdater) SetOnStreamMissingError(fn func(streamName string, err error))
- func (c *CompositeConsumerUpdater) UpdateWorkerConsumer(ctx context.Context, workerID string, partitions []Partition) error
- type Config
- type DegradedAlertConfig
- type DegradedBehaviorConfig
- type ElectionAgent
- type FleetSizeObserver
- type HandoffClaim
- type HandoffClaimState
- type HandoffConfig
- type HandoffMetricsRecorder
- type HandoffSweepMetricsRecorder
- type Hooks
- type KVBucketConfig
- type LabelState
- type Logger
- type Manager
- func (m *Manager) Capabilities() uint32
- func (m *Manager) CurrentAssignment() Assignment
- func (m *Manager) InspectHandoffClaims(ctx context.Context) ([]HandoffClaim, error)
- func (m *Manager) IsInRecoveryGrace() bool
- func (m *Manager) IsLeader() bool
- func (m *Manager) LabelState() LabelState
- func (m *Manager) RefreshPartitions(ctx context.Context) error
- func (m *Manager) SetCapability(capBit uint32, active bool)
- func (m *Manager) Start(ctx context.Context) (startErr error)
- func (m *Manager) State() State
- func (m *Manager) Stop(ctx context.Context) error
- func (m *Manager) WaitState(expectedState State, timeout time.Duration) <-chan error
- func (m *Manager) WorkerID() string
- func (m *Manager) WorkerLabels() []string
- type ManagerMetrics
- type MetricsCollector
- type Option
- func WithBucketEpochProbeInterval(d time.Duration) Option
- func WithElectionAgent(agent ElectionAgent) Option
- func WithHandoffMetricsRecorder(mr HandoffMetricsRecorder) Option
- func WithHooks(hooks *Hooks) Option
- func WithLabelSpillGrace(d time.Duration) Option
- func WithLogger(logger Logger) Option
- func WithMetrics(metrics MetricsCollector) Option
- func WithWorkerConsumerUpdater(updater WorkerConsumerUpdater) Option
- func WithWorkerLabels(labels ...string) Option
- type Partition
- type PartitionSource
- type PartitionSourceUpdater
- type PartitionUpdater
- type State
- type StreamMissingHook
- type WatchablePartitionSource
- type WorkerConsumerMetrics
- type WorkerConsumerUpdater
- type WorkerMetrics
Examples ¶
Constants ¶
const ( // bucket reachable on the live connection but unable to serve ops because its // RAFT quorum is lost. Kept distinct from DegradeReasonKVErrorThreshold so the // operator surface distinguishes a quorum-loss op stall from a whole-bucket // wipe, and so the contract that whole-bucket loss is the ONLY path to the // threshold reason is preserved. Recovery-scoped: exit requires a heartbeat Put // stamped after the degrade. DegradeReasonKVUnavailable = "kv-unavailable" // DegradeReasonEnumerationStall is a sustained leader-side worker-enumeration // (heartbeat Keys scan) stall that the connectivity / degrading classifiers // miss. Kept distinct so the recovery exit can require an enumeration success // before exiting. Recovery-scoped (leader-gated). DegradeReasonEnumerationStall = "heartbeat-enumeration-stall" // DegradeReasonAssignmentWatcherExhausted is emitted by the assignment-watcher // retry envelope's permanent-failure callback when the watcher exhausts its // attempt budget. Distinct from DegradeReasonKVErrorThreshold so operators can // tell "assignment bucket is unrecoverable" from "accumulated transient errors". DegradeReasonAssignmentWatcherExhausted = "assignment-watcher-exhausted" // DegradeReasonKVErrorThreshold is the whole-bucket-loss reason from // recordKVError once the KV-error window crosses the threshold. Whole-bucket // loss (connectivity / degrading-JetStream) is the ONLY path to this reason — // transient kv-unavailable entries are clearable by a healthy op and degrade // with DegradeReasonKVUnavailable instead (the AGENTS.md contract). DegradeReasonKVErrorThreshold = "KV error threshold exceeded" // DegradeReasonNATSConnectionDown is set by the connection monitor once the // NATS connection has been down past the enter threshold. DegradeReasonNATSConnectionDown = "NATS connection down" // DegradeReasonStreamMissingRecoveryExhausted is the dynamic-consumer // stream-missing recovery path: routed through the stream-missing observer // rather than the generic KV-error threshold. DegradeReasonStreamMissingRecoveryExhausted = "stream-missing-recovery-exhausted" // DegradeReasonStartupTimeout is fired by the startup watchdog when the manager // is still not Stable after StartupTimeout. DegradeReasonStartupTimeout = "startup-timeout" // DegradeReasonStartupBackgroundPanic is fired when a background startup // goroutine panics. DegradeReasonStartupBackgroundPanic = "startup-background-panic" )
Degrade reasons — the single registry of every enterDegraded reason string.
These values are an operator-facing contract: they are passed verbatim to the OnDegraded hook (see [Hooks.OnDegraded]) and are documented in docs/API_REFERENCE.md. The string VALUES are frozen — changing one is an operator-visible break — so they are pinned literally by TestDegradeReason_LiteralValues in addition to being referenced by name here.
Centralizing them in one block (rather than scattered consts + inline literals across manager_degraded.go, manager_assignment.go, manager_setup.go, and manager_startup_async.go) keeps the taxonomy in one place and lets the recovery-exit gates branch on names instead of magic strings.
const ( StateInit = types.StateInit StateClaimingID = types.StateClaimingID StateElection = types.StateElection StateWaitingAssignment = types.StateWaitingAssignment StateStable = types.StateStable StateScaling = types.StateScaling StateRebalancing = types.StateRebalancing StateEmergency = types.StateEmergency StateDegraded = types.StateDegraded StateShutdown = types.StateShutdown )
Re-export State constants from the internal types package.
const ( HandoffStateUnknown = types.HandoffStateUnknown HandoffStateStable = types.HandoffStateStable HandoffStatePrepare = types.HandoffStatePrepare HandoffStateCommit = types.HandoffStateCommit )
Re-export HandoffState constants.
Variables ¶
var ( ErrInvalidConfig = types.ErrInvalidConfig ErrNATSConnectionRequired = types.ErrNATSConnectionRequired ErrPartitionSourceRequired = types.ErrPartitionSourceRequired ErrAssignmentStrategyRequired = types.ErrAssignmentStrategyRequired ErrAlreadyStarted = types.ErrAlreadyStarted ErrNotStarted = types.ErrNotStarted ErrNotImplemented = types.ErrNotImplemented ErrNoWorkersAvailable = types.ErrNoWorkersAvailable ErrInvalidWorkerID = types.ErrInvalidWorkerID ErrElectionFailed = types.ErrElectionFailed ErrConnectivity = types.ErrConnectivity ErrDegraded = types.ErrDegraded ErrIDClaimFailed = types.ErrIDClaimFailed ErrAssignmentFailed = types.ErrAssignmentFailed ErrInvalidPreset = types.ErrInvalidPreset // ErrStreamMissing is the operator-actionable umbrella error surfaced // when the dynamic partition consumer's recovery flow determines that // the underlying JetStream stream is absent (or post-hook recovery // fails for any reason — restored consumer config mismatch, etc.). // Callers branch via errors.Is(err, parti.ErrStreamMissing) inside // their Hooks.OnError handler. See [types.ErrStreamMissing] for the // full contract. ErrStreamMissing = types.ErrStreamMissing )
Re-export sentinel errors from the internal types package.
These are stable, comparable values intended for use with errors.Is(). Keeping them in the root package provides a convenient public API while allowing internal packages to depend on the `types` subpackage without importing the root package.
ErrKVUnavailable marks a KV operation that failed because its backing bucket is reachable on the live NATS connection but cannot serve the op (deadline / no-responders) — the quorum-loss condition the connection-status monitor and the connectivity/degrading-JetStream classifiers all miss.
It is applied ONLY at the manager's own periodic KV-op call sites via [markKVUnavailable], never added to any global predicate. That keeps an unwrapped deadline / no-responders from anywhere else (notably the peer-takeover claim path through onClaimerError) out of the degraded circuit, preserving the cross-feature classification contracts.
Functions ¶
func BuildControlPlaneKVConfig ¶ added in v2.4.1
func BuildControlPlaneKVConfig(bucket string, ttl time.Duration, storage jetstream.StorageType) jetstream.KeyValueConfig
BuildControlPlaneKVConfig returns the canonical jetstream.KeyValueConfig for a Parti control-plane KV bucket. It is a thin wrapper around internal/kvbuckets.BuildKeyValueConfig, re-exported here so that external packages (such as provision/) can construct byte-equivalent configurations without importing an internal package.
See internal/kvbuckets.BuildKeyValueConfig for the full contract, including the byte-equivalence guarantee with (*Manager).ensureKVBucket.
func SetDefaults ¶
SetDefaults applies default values to zero-valued configuration fields. If a field is zero-valued, it will be set to the corresponding default value.
Returns:
- error: Non-nil if the default tags on the Config struct are malformed
Example ¶
This example demonstrates using SetDefaults to fill in default values for a partially configured Config, such as one loaded from YAML.
package main
import (
"fmt"
"github.com/arloliu/parti/v2"
)
func main() {
// Simulate a Config loaded from YAML with only some fields set
cfg := parti.Config{
WorkerIDPrefix: "custom-worker",
WorkerIDMax: 50,
}
// Fill remaining fields with defaults
if err := parti.SetDefaults(&cfg); err != nil {
fmt.Printf("Failed to set defaults: %v\n", err)
return
}
fmt.Printf("Prefix: %s, HeartbeatInterval: %v\n", cfg.WorkerIDPrefix, cfg.HeartbeatInterval)
}
Output: Prefix: custom-worker, HeartbeatInterval: 5s
Types ¶
type AlertLevel ¶
type AlertLevel int
AlertLevel represents the severity level of degraded mode alerts.
const ( // AlertLevelInfo indicates informational alerts (least severe). AlertLevelInfo AlertLevel = iota // AlertLevelWarn indicates warning alerts. AlertLevelWarn // AlertLevelError indicates error alerts. AlertLevelError // AlertLevelCritical indicates critical alerts (most severe). AlertLevelCritical )
func (AlertLevel) String ¶
func (l AlertLevel) String() string
String returns the string representation of the alert level.
Returns:
- string: Alert level name ("Info", "Warn", "Error", "Critical", or "Unknown")
type Assignment ¶
type Assignment = types.Assignment
Re-export types from the internal types package.
This file provides a stable, backward-compatible public API for the library's core types and interfaces. It uses type aliases to re-export definitions from the `types` subpackage, which contains the actual implementations.
This pattern solves the "import cycle" problem by allowing internal packages to depend on `types` without depending on the root `parti` package, while still providing a convenient `parti.State`, `parti.Logger`, etc. for users.
type AssignmentDivergenceMetricsRecorder ¶ added in v2.10.2
type AssignmentDivergenceMetricsRecorder = types.AssignmentDivergenceMetricsRecorder
AssignmentDivergenceMetricsRecorder is the optional equal-version divergence observability capability a MetricsCollector may additionally implement; see types.AssignmentDivergenceMetricsRecorder for the label contract.
type AssignmentMetrics ¶
type AssignmentMetrics = types.AssignmentMetrics
Re-export interfaces from the internal types package for convenience.
type AssignmentStrategy ¶
type AssignmentStrategy = types.AssignmentStrategy
Re-export interfaces from the internal types package for convenience.
type AuthorityChoice ¶ added in v2.4.0
type AuthorityChoice int
AuthorityChoice is the result of the dual-read source-of-truth selection rule (§3.6). It tells the manager which channel — the commit watcher or the legacy alias watcher — should drive the next applyAssignment call for the observation in hand.
const ( // AuthorityNone indicates no usable authority is available; the manager // should wait for a fresher observation. AuthorityNone AuthorityChoice = iota // AuthorityCommit indicates the commit watcher's payload should drive // the apply. AuthorityCommit // AuthorityLegacyAlias indicates the legacy alias watcher's payload // should drive the apply (rolling-upgrade or new-leader→old-leader // handoff window). AuthorityLegacyAlias )
type CalculatorMetrics ¶
type CalculatorMetrics = types.CalculatorMetrics
Re-export interfaces from the internal types package for convenience.
type CapabilityReporter ¶ added in v2.4.0
type CapabilityReporter interface {
// Capabilities returns the OR of capability bits this reporter has
// successfully wired at runtime. Must be safe for concurrent use,
// non-blocking, and monotonic for runtime-wireup bits.
Capabilities() uint32
}
CapabilityReporter is an optional interface a WorkerConsumerUpdater MAY implement to report runtime capabilities back to the Manager.
When the registered updater (or any child of a composite updater) satisfies this interface, Manager queries Capabilities() after each handoff apply attempt and ORs the returned bits into its capability bitmask via SetCapability.
Implementations MUST be:
- Safe for concurrent calls. Capabilities() may be invoked from the manager-apply goroutine (which calls reportConsumerCapabilities after every handoffCoordinator.Apply attempt) and may race with the updater's own UpdateWorkerConsumer calls. The heartbeat publisher does NOT call Capabilities() directly — it reads Manager.Capabilities() (the live bitmask callback) — so the race surface is reporter ↔ updater, not reporter ↔ heartbeat.
- Non-blocking. Capabilities() is invoked on every apply attempt; must not perform I/O or acquire locks held by long operations. A simple atomic load is the expected shape.
- Monotonic for runtime-wire-up bits such as CapProcessingGate: once a capability has been successfully wired (e.g., a handler wrapped with the processing gate), the corresponding bit MUST remain set for the rest of the updater's lifetime even if a later per-subject create fails. The bit reflects "this updater has at least one wired component", not "all components are currently wired".
Manager integration semantics for the reporter integration are OR-only for runtime-wire-up bits: reportConsumerCapabilities calls SetCapability(bit, true) for known reported bits and never clears. (Manager.SetCapability itself supports clearing bits via active=false, used by other components — but the reporter pathway only sets.) Implementers should not rely on a returned-zero Capabilities() causing the manager to clear a previously-reported bit; it won't.
Returning 0 is always safe (no caps advertised).
type CompositeConsumerUpdater ¶
type CompositeConsumerUpdater struct {
// contains filtered or unexported fields
}
CompositeConsumerUpdater combines multiple WorkerConsumerUpdater instances. When UpdateWorkerConsumer is called, it fans out to all registered updaters.
Use cases:
- Register both WorkerConsumer (per-partition) and BroadcastConsumer (fan-out)
- Multiple BroadcastConsumer instances for different stream/subject patterns
Error handling:
- Calls all updaters even if some fail
- Returns a combined error with all failures
func NewCompositeConsumerUpdater ¶
func NewCompositeConsumerUpdater(updaters ...WorkerConsumerUpdater) *CompositeConsumerUpdater
NewCompositeConsumerUpdater creates a composite from multiple updaters. All provided updaters will receive partition updates when UpdateWorkerConsumer is called.
Example:
wc, _ := durable.NewWorkerConsumer(js, wcConfig, handler1)
bc, _ := durable.NewBroadcastConsumer(js, bcConfig, handler2)
composite := parti.NewCompositeConsumerUpdater(wc, bc)
mgr, _ := parti.NewManager(cfg, js, src, strategy,
parti.WithWorkerConsumerUpdater(composite),
)
Example ¶
This example demonstrates composing multiple WorkerConsumerUpdater instances using CompositeConsumerUpdater.
package main
import (
"fmt"
"github.com/arloliu/parti/v2"
)
func main() {
// In production, these would be real consumer instances (e.g., Dynamic, Broadcast).
// NewCompositeConsumerUpdater filters nil updaters, so only non-nil ones are kept.
composite := parti.NewCompositeConsumerUpdater( /* updater1, updater2 */ )
// Add updaters later via Add
fmt.Printf("Composite updater created, initial count: %d\n", composite.Len())
}
Output: Composite updater created, initial count: 0
func (*CompositeConsumerUpdater) Add ¶
func (c *CompositeConsumerUpdater) Add(updaters ...WorkerConsumerUpdater)
Add appends additional updaters to the composite. This is useful for dynamically registering consumers after creation.
Newly-added children inherit any state the composite has already received: an installed stream-missing observer (via SetOnStreamMissingError) and the last observed fleet worker-count (via ObserveWorkerCount), so a late registration does not silently miss either. Inherited-state forwarding runs outside c.mu so a slow child cannot serialize peers or deadlock a re-entrant Add.
func (*CompositeConsumerUpdater) Capabilities ¶ added in v2.4.0
func (c *CompositeConsumerUpdater) Capabilities() uint32
Capabilities implements CapabilityReporter by ORing the runtime capability bits reported by each child updater that implements CapabilityReporter. Children that do not implement the interface contribute zero bits.
Composition rule: OR. A capability is considered wired for the composite if any child updater has wired it.
Returns:
- uint32: OR of all child-reported capability bits, or 0 if no child implements CapabilityReporter or none has wired any bits.
func (*CompositeConsumerUpdater) Len ¶
func (c *CompositeConsumerUpdater) Len() int
Len returns the number of registered updaters.
func (*CompositeConsumerUpdater) ObserveWorkerCount ¶ added in v2.8.0
func (c *CompositeConsumerUpdater) ObserveWorkerCount(n int)
ObserveWorkerCount implements FleetSizeObserver by caching the worker-count and forwarding it to every child that implements FleetSizeObserver. The cache is replayed to children added later via Add.
func (*CompositeConsumerUpdater) SetOnStreamMissingError ¶ added in v2.5.0
func (c *CompositeConsumerUpdater) SetOnStreamMissingError(fn func(streamName string, err error))
SetOnStreamMissingError records the callback and forwards it to all current children that implement recovery.StreamMissingObserver. Children that do not implement the interface are silently skipped. Subsequent [Add] calls forward the recorded observer to newly-added observer-capable children.
Passing fn == nil clears the recorded observer and forwards the nil to current observer-capable children so they stop dispatching to the previous closure.
Implements recovery.StreamMissingObserver; the Parti manager type-asserts the composite during Manager.Start and bridges the stream-missing recovery exhaustion event to enterDegraded( "stream-missing-recovery-exhausted").
func (*CompositeConsumerUpdater) UpdateWorkerConsumer ¶
func (c *CompositeConsumerUpdater) UpdateWorkerConsumer(ctx context.Context, workerID string, partitions []Partition) error
UpdateWorkerConsumer implements WorkerConsumerUpdater. Calls UpdateWorkerConsumer on all registered updaters, collecting any errors. All updaters are called even if some fail.
type Config ¶
type Config struct {
// WorkerIDPrefix is the prefix for worker IDs (e.g., "worker" produces "worker-0", "worker-1").
WorkerIDPrefix string `yaml:"workerIdPrefix" default:"worker" validate:"required"`
// WorkerLabels is this worker's label set, fixed at process startup.
// Labeled partitions (Partition.Label) are assigned only to workers
// whose set contains the partition's label. Validated with
// partition-key charset rules; sorted and deduplicated. At most 16
// labels, each at most 64 bytes. Empty = unlabeled worker.
// WithWorkerLabels overrides this field when both are set.
WorkerLabels []string `yaml:"workerLabels"`
// UnlabeledPartitionPolicy controls which workers receive unlabeled
// partitions. "dedicated" (default): unlabeled workers only, falling
// back to all workers when no unlabeled worker is live. "shared":
// all workers. Leader-side; MUST be identical across every manager
// in the fleet (same contract as the AssignmentStrategy choice).
UnlabeledPartitionPolicy string `yaml:"unlabeledPartitionPolicy" default:"dedicated" validate:"oneof=dedicated shared"`
// LabelSpillGrace is how long a label's worker pool must be
// continuously empty before its partitions spill to the fallback
// ladder. 0 spills immediately. Leader-side; MUST be fleet-uniform.
LabelSpillGrace time.Duration `yaml:"labelSpillGrace" default:"60s" validate:"gte=0"`
// WorkerIDMin is the minimum stable ID number (inclusive).
// Set to 0 for most use cases.
WorkerIDMin int `yaml:"workerIdMin" default:"0" validate:"gte=0"`
// WorkerIDMax is the maximum stable ID number (inclusive).
// Determines the maximum number of concurrent workers: (WorkerIDMax - WorkerIDMin + 1).
// For example, WorkerIDMin=0 and WorkerIDMax=999 allows up to 1000 workers.
WorkerIDMax int `yaml:"workerIdMax" default:"999" validate:"gtefield=WorkerIDMin"`
// WorkerIDTTL is how long a worker ID claim remains valid in the key-value store.
// Must be >= HeartbeatTTL (validated via the `gtefield=HeartbeatTTL` tag below;
// setting WorkerIDTTL below HeartbeatTTL causes Manager.Start to fail
// validation). Renewal writes one KV op per worker every
// WorkerIDTTL/3, so longer values reduce steady-state IOPS at the cost of slower
// ID reclamation after a worker exits.
// Recommended: 3-5x HeartbeatTTL. Default 75s is 5x the default HeartbeatTTL (15s).
// The stableID KV bucket's MaxAge is reconciled to exactly WorkerIDTTL on
// Manager.Start: an existing bucket whose TTL differs (most dangerously 0,
// which leaks worker IDs on ungraceful restart) is corrected, and a
// deliberately-longer operator TTL is shortened to WorkerIDTTL.
WorkerIDTTL time.Duration `yaml:"workerIdTtl" default:"75s" validate:"gt=0,gtefield=HeartbeatTTL"`
// HeartbeatInterval is how often workers publish heartbeat messages.
// Shorter intervals provide faster failure detection but increase KV write load
// (one write per worker per interval, amplified by JetStream replication and fsync).
// Recommended: 3-10 seconds.
HeartbeatInterval time.Duration `yaml:"heartbeatInterval" default:"5s" validate:"gt=0"`
// HeartbeatTTL is how long heartbeat messages remain valid before a worker is considered failed.
// Must be greater than HeartbeatInterval.
// Recommended: 3x HeartbeatInterval.
HeartbeatTTL time.Duration `yaml:"heartbeatTtl" default:"15s" validate:"gt=0"`
// ColdStartWindow is the stabilization period when starting workers from zero.
// During this window, partition assignment is delayed to allow all initial workers to join.
// Recommended: 30 seconds.
ColdStartWindow time.Duration `yaml:"coldStartWindow" default:"30s" validate:"gt=0,gtefield=PlannedScaleWindow"`
// PlannedScaleWindow is the stabilization period during rolling updates or planned scaling.
// Shorter than ColdStartWindow to minimize disruption during controlled changes.
// Recommended: 10 seconds.
PlannedScaleWindow time.Duration `yaml:"plannedScaleWindow" default:"10s" validate:"gt=0"`
// EmergencyGracePeriod is the minimum time a worker must be missing before
// triggering emergency rebalance. Prevents false positives from transient
// network issues or brief connectivity loss.
//
// Default: 0 (auto-calculated as 1.5 * HeartbeatInterval)
// Recommended: 1.5-2.0 * HeartbeatInterval
// Constraint: Must be <= HeartbeatTTL
EmergencyGracePeriod time.Duration `yaml:"emergencyGracePeriod" validate:"ltefield=HeartbeatTTL"`
// RestartDetectionRatio historically classified a restart as cold start vs
// planned (if failed/total > ratio, treat as cold start).
//
// NOTE: this knob is currently a NO-OP. The cold-start-vs-planned-scale window
// selector that consumed it was removed as dead code, so the value is still
// accepted and validated for config compatibility but no longer affects
// behavior. Its removal is deferred to a future major version (public-API change).
RestartDetectionRatio float64 `yaml:"restartDetectionRatio" default:"0.5" validate:"gte=0,lte=1"`
// OperationTimeout is the timeout for KV operations (get, put, delete).
// Recommended: 10 seconds.
OperationTimeout time.Duration `yaml:"operationTimeout" default:"10s" validate:"gt=0"`
// BucketEpochProbeInterval is how often the bucket-epoch fence (see
// monitorBucketEpochs) polls each Parti-owned KV bucket for a
// wipe-and-recreate event. This was previously coupled to
// OperationTimeout — the same knob that bounds every individual KV
// operation's deadline — so changing one unintentionally changed the
// other. This field decouples the probe cadence from that deadline.
// Default 10s preserves the prior default behavior for deployments
// that left OperationTimeout at its own 10s default; deployments that
// TUNED OperationTimeout and relied on the old coupled cadence must set
// this field to their previous OperationTimeout value to preserve it.
// The per-probe deadline is still OperationTimeout (see
// probeBucketCreated); this field controls only how often a probe pass
// runs.
BucketEpochProbeInterval time.Duration `yaml:"bucketEpochProbeInterval" default:"10s" validate:"gt=0"`
// ElectionTimeout is the maximum time to wait for leader election to complete.
// Also drives leader lease renewal frequency (ElectionTimeout/3). Longer values
// reduce KV write load at the cost of slower failover detection.
// Recommended: 5-15 seconds.
ElectionTimeout time.Duration `yaml:"electionTimeout" default:"10s" validate:"gt=0"`
// StartupTimeout is the maximum time to wait for the manager to fully start.
// Includes worker ID claiming, leader election, and initial partition assignment.
// Must be at least ColdStartWindow + ElectionTimeout with some headroom.
// Recommended: 60 seconds.
StartupTimeout time.Duration `yaml:"startupTimeout" default:"60s" validate:"gt=0"`
// ShutdownTimeout is the maximum time to wait for graceful shutdown.
// Includes releasing worker ID, stopping heartbeats, and cleanup operations.
// Recommended: 10 seconds.
ShutdownTimeout time.Duration `yaml:"shutdownTimeout" default:"10s" validate:"gt=0"`
// RebalanceCooldown is the minimum time between rebalancing operations.
//
// Enforces rate limiting BEFORE stabilization windows to prevent thrashing
// during rapid topology changes. If a rebalance was completed <RebalanceCooldown
// ago, new topology changes are deferred until the interval expires.
//
// Default: 10 seconds
// Recommendation: Should be <= PlannedScaleWindow for proper coordination
//
// Note: This was renamed from MinRebalanceInterval in v0.x for semantic clarity.
RebalanceCooldown time.Duration `yaml:"rebalanceCooldown" default:"10s" validate:"gt=0,ltefield=PlannedScaleWindow"`
// KVBuckets controls NATS JetStream KV bucket configuration.
KVBuckets KVBucketConfig `yaml:"kvBuckets"`
// DegradedAlert controls alert emission during degraded mode operation.
DegradedAlert DegradedAlertConfig `yaml:"degradedAlert"`
// DegradedBehavior controls when the manager enters and exits degraded mode.
DegradedBehavior DegradedBehaviorConfig `yaml:"degradedBehavior"`
// EnableTwoPhaseHandoff gates the manager-side two-phase handoff coordinator.
//
// When false (default), assignment changes are applied directly via the
// WorkerConsumerUpdater in a simple "remove/add" sequence managed by the
// manager without KV claims. This keeps the control plane minimal but can
// permit a brief duplicate-consumption window during reassignment.
//
// When true, the manager initializes a modular handoff coordinator which
// can implement a prepare/commit protocol to make ownership transitions
// atomic, single-owner, auditable, and crash-resumable. This feature is
// wired behind a clean interface and can be enabled/disabled without
// scattering conditional logic throughout the manager code path.
EnableTwoPhaseHandoff bool `yaml:"enableTwoPhaseHandoff" default:"false"`
// Handoff controls two-phase handoff tuning knobs.
// Only applied when EnableTwoPhaseHandoff is true.
Handoff HandoffConfig `yaml:"handoff"`
// ApplyStartJitter, when > 0, randomly delays fresh-version applies
// (watcher / commit / alias / initial-bootstrap, all funneled through
// applyAssignmentWithPrev) by a uniformly distributed duration in
// [0, ApplyStartJitter) before taking applyStoreMu. This spreads
// JetStream consumer create/destroy load across a worker fleet that
// observed the same new assignment version simultaneously (e.g. after
// a leader re-election).
//
// Retries (scheduleApplyRetry) do NOT pay this jitter. The retry path
// has its own exponential-backoff envelope with decorrelated jitter;
// compounding apply-start jitter on top would inflate recovery latency
// without spreading any fleet because a retry is one worker, not a fleet.
//
// Default 0 disables jitter (backwards compatible). Recommended starting
// point for a 20-worker fleet creating 100 consumers each: 500ms.
//
// Hard-capped at 10s by Validate(): a larger value would dwarf the
// LSR-advancement bookkeeping and risk masking apply failures behind
// what would look like apply latency.
//
// Startup-runner consequence: the startup background runner's first
// apply (applyInitialAssignment, manager_startup_async.go) also funnels
// through applyAssignmentWithPrev for non-empty commits/aliases. With
// jitter enabled, the runner sleeps up to ApplyStartJitter before its
// first apply attempt. The soft watchdog measures StartupTimeout from
// Start invocation (AGENTS.md "Apply boundedness" section), so operators
// MUST configure ApplyStartJitter << StartupTimeout. Recommended:
// ApplyStartJitter <= StartupTimeout / 4. A focused test pins this
// (TestApplyStartJitter_StartupBudget).
ApplyStartJitter time.Duration `yaml:"applyStartJitter" default:"0" validate:"gte=0"`
// AssignmentWatcherDebounce is the idle-window duration used to coalesce
// rapid bursts of assignment delivery watcher events into a single apply.
// It applies to both the legacy per-worker assignment alias watcher and
// the assignment._commit watcher. When > 0, each watcher path keeps the
// latest observed assignment target in a pending slot and processes it
// after the stream has been idle for the full window.
//
// Early-flush conditions (commit watcher only): the pending commit is
// flushed BEFORE the idle window elapses when (a) the watcher channel
// closes mid-window, or (b) a same-or-higher commit arrives that
// changes this worker's effective partition set — either a Workers
// membership flip (this worker added or removed) or a
// Payloads[workerID].PayloadHash difference. Both shapes are treated
// as ownership transitions; debounce collapses only commits whose
// per-worker partition set is unchanged. This preserves the two-phase
// handoff invariant that every ownership transition for this worker is
// observed as a discrete apply. Stale lower-version commits arriving
// after a higher pending version are dropped without flushing pending.
// On context cancellation (Stop), the pending commit is dropped
// without flushing — see the alias watcher's matching shutdown
// semantics. The legacy alias watcher does NOT yet have the "flush on
// assignment change" rule; harmonization is a follow-up.
//
// Default 0 disables debouncing (preserves pre-PR-3 behavior). Recommended
// value derived from the apply-coalescing diagnostic
// (PARTI_RUN_HERD_DIAGNOSTIC=1) plus a 50 ms safety margin — typically
// in the 100–300 ms range.
//
// Hard-capped at 1 second by Validate(): a larger window would dwarf
// reasonable reassignment-latency budgets and risk masking real apply
// failures behind what looks like apply slowness.
AssignmentWatcherDebounce time.Duration `yaml:"assignmentWatcherDebounce" default:"0" validate:"gte=0"`
}
Config is the configuration for the Manager.
All duration fields accept standard Go duration strings like "30s", "5m", "1h".
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns a Config with sensible defaults.
This function panics only if the library's own struct tags are malformed, which indicates a programming error in Parti itself.
Returns:
- Config: Configuration with default values
Example ¶
This example demonstrates using DefaultConfig as a starting point and customizing specific fields.
package main
import (
"fmt"
"time"
"github.com/arloliu/parti/v2"
)
func main() {
cfg := parti.DefaultConfig()
// Override only what you need
cfg.WorkerIDPrefix = "my-service"
cfg.WorkerIDMax = 50
cfg.HeartbeatInterval = 5 * time.Second
cfg.HeartbeatTTL = 15 * time.Second
fmt.Printf("Prefix: %s, MaxWorkers: %d, HeartbeatInterval: %v, HeartbeatTTL: %v\n",
cfg.WorkerIDPrefix, cfg.WorkerIDMax+1, cfg.HeartbeatInterval, cfg.HeartbeatTTL)
}
Output: Prefix: my-service, MaxWorkers: 51, HeartbeatInterval: 5s, HeartbeatTTL: 15s
func TestConfig ¶
func TestConfig() Config
TestConfig returns a configuration optimized for fast test execution.
Test timings are 10-100x faster than production defaults to enable rapid iteration without sacrificing test coverage. Use DefaultConfig() for production deployments.
Returns:
- Config: Configuration with fast timings for tests
Example:
cfg := parti.TestConfig() cfg.WorkerIDPrefix = "test-worker" manager, err := parti.NewManager(nc, cfg)
func (*Config) Validate ¶
Validate checks configuration constraints and returns error for invalid values.
Hard Validation Rules:
- HeartbeatTTL >= 2 * HeartbeatInterval (allow 1 missed heartbeat)
- WorkerIDTTL >= 3 * HeartbeatInterval (stable ID renewal)
- WorkerIDTTL >= HeartbeatTTL (ID must outlive heartbeat)
- WorkerIDTTL >= 300ms (stable-ID renewal floor)
- RebalanceCooldown > 0 (prevent thrashing)
- ColdStartWindow >= PlannedScaleWindow (cold start is slower)
- RebalanceCooldown <= PlannedScaleWindow (rate limit coordination)
- RebalanceCooldown <= ColdStartWindow (rate limit coordination)
- EmergencyGracePeriod <= HeartbeatTTL (detection window)
Returns:
- error: Validation error with clear explanation, nil if valid
func (*Config) ValidateWithWarnings ¶
ValidateWithWarnings checks configuration and logs warnings for non-recommended values.
This is called after Validate() in NewManager() to provide operator guidance.
Parameters:
- logger: Logger instance for warning output
type DegradedAlertConfig ¶
type DegradedAlertConfig struct {
// InfoThreshold is the duration in degraded mode before emitting Info-level alerts.
// Default: 30 seconds.
InfoThreshold time.Duration `yaml:"infoThreshold" default:"30s" validate:"gt=0"`
// WarnThreshold is the duration in degraded mode before emitting Warn-level alerts.
// Default: 2 minutes.
WarnThreshold time.Duration `yaml:"warnThreshold" default:"2m" validate:"gt=0,gtefield=InfoThreshold"`
// ErrorThreshold is the duration in degraded mode before emitting Error-level alerts.
// Default: 5 minutes.
ErrorThreshold time.Duration `yaml:"errorThreshold" default:"5m" validate:"gt=0,gtefield=WarnThreshold"`
// CriticalThreshold is the duration in degraded mode before emitting Critical-level alerts.
// Default: 10 minutes.
CriticalThreshold time.Duration `yaml:"criticalThreshold" default:"10m" validate:"gt=0,gtefield=ErrorThreshold"`
// AlertInterval is the time between repeated alerts at the same severity level.
// Default: 1 minute.
AlertInterval time.Duration `yaml:"alertInterval" default:"1m" validate:"gt=0"`
}
DegradedAlertConfig controls alert emission during degraded mode operation.
type DegradedBehaviorConfig ¶
type DegradedBehaviorConfig struct {
// EnterThreshold is how long NATS connectivity errors must persist before entering degraded mode.
// Provides hysteresis to prevent flapping during transient issues.
// Default: 10 seconds.
EnterThreshold time.Duration `yaml:"enterThreshold" default:"10s" validate:"gte=0"`
// ExitThreshold is how long NATS connectivity must be stable before exiting degraded mode.
// Should be shorter than EnterThreshold to recover quickly.
// Default: 5 seconds.
ExitThreshold time.Duration `yaml:"exitThreshold" default:"5s" validate:"gte=0"`
// KVErrorThreshold is the number of KV operation errors within KVErrorWindow
// that trigger degraded mode. Whole-bucket-loss errors accumulate across the
// window; connected-but-KV-unavailable timeouts are reset by any successful
// periodic KV op while not degraded, so for that class it is a consecutive
// run with no intervening success rather than blips summed across the window.
// Default: 5 errors.
KVErrorThreshold int `yaml:"kvErrorThreshold" default:"5" validate:"gte=0"`
// KVErrorWindow is the time window for counting KV errors toward
// KVErrorThreshold. Errors outside this window are not counted.
// Default: 30 seconds.
KVErrorWindow time.Duration `yaml:"kvErrorWindow" default:"30s" validate:"gte=0"`
// RecoveryGracePeriod is the minimum time the leader must wait after recovering from
// degraded mode before declaring missing workers as failed (emergency rebalance).
// Prevents false emergencies when workers recover slightly slower than the leader.
// Default: 15 seconds.
RecoveryGracePeriod time.Duration `yaml:"recoveryGracePeriod" default:"15s" validate:"gte=0"`
}
DegradedBehaviorConfig controls when the manager enters and exits degraded mode.
func DegradedBehaviorPreset ¶
func DegradedBehaviorPreset(preset string) (DegradedBehaviorConfig, error)
DegradedBehaviorPreset returns a preconfigured DegradedBehaviorConfig based on the preset name.
Supported presets:
- "conservative": Slower to enter degraded, safer for production (30s enter, 10s exit, 10 errors)
- "balanced": Default behavior, good for most use cases (10s enter, 5s exit, 5 errors)
- "aggressive": Faster to enter degraded, better for development (5s enter, 3s exit, 3 errors)
Parameters:
- preset: Preset name ("conservative", "balanced", or "aggressive")
Returns:
- DegradedBehaviorConfig: Preconfigured behavior settings
- error: ErrInvalidPreset if preset name is not recognized
Example:
cfg, err := DegradedBehaviorPreset("conservative")
if err != nil {
log.Fatal(err)
}
type ElectionAgent ¶
type ElectionAgent = types.ElectionAgent
Re-export interfaces from the internal types package for convenience.
type FleetSizeObserver ¶ added in v2.8.0
type FleetSizeObserver interface {
// ObserveWorkerCount reports the current committed cluster worker-count.
ObserveWorkerCount(n int)
}
FleetSizeObserver is an optional interface a WorkerConsumerUpdater MAY implement to receive the observed cluster worker-count (N) for fleet-size- aware (adaptive) rate limiting.
When the registered updater (or any child of a composite updater) satisfies this interface, the Manager calls ObserveWorkerCount whenever the committed worker-set size changes — and once during startup, before the first apply — so the consumer can retune its consumer-create rate to min(perWorkerCeiling, clusterRate/N).
Implementations MUST be:
- Non-blocking. ObserveWorkerCount runs on the Manager's commit-watch goroutine while the Manager holds an internal lock; it must not perform I/O or block.
- Safe for concurrent use.
- Non-reentrant: it MUST NOT call back into the Manager or any apply/update path (mirrors the CapabilityReporter contract and the D5 lock-order rule).
n is always >= 1.
type HandoffClaim ¶
type HandoffClaim struct {
PartitionID string `json:"partition_id"`
Owner string `json:"owner"`
PendingOwner string `json:"pending_owner,omitempty"`
State HandoffClaimState `json:"state"`
Epoch int64 `json:"epoch"`
LastUpdated time.Time `json:"last_updated"`
TTLSeconds int64 `json:"ttl_seconds"`
ConflictCount int64 `json:"conflict_count,omitempty"`
}
HandoffClaim is a read-only view of a partition handoff claim stored in the two-phase handoff KV bucket. It is intentionally decoupled from internal implementation details and suitable for diagnostics and tests.
func InspectHandoffClaims ¶
func InspectHandoffClaims(ctx context.Context, js jetstream.JetStream, bucket string) ([]HandoffClaim, error)
InspectHandoffClaims opens the provided JetStream KV bucket and returns all current handoff claims stored under the "claims/" prefix.
This helper is public to allow tests to inspect claims without a Manager instance, given a JetStream context and bucket name.
Parameters:
- ctx: Context for cancellation
- js: JetStream context used to open the bucket
- bucket: KV bucket name where handoff claims are stored
Returns:
- []HandoffClaim: Decoded claims
- error: Error opening the bucket or reading/decoding entries
type HandoffClaimState ¶
type HandoffClaimState string
HandoffClaimState represents the logical state of a handoff claim.
Values:
- "stable": No handoff in progress
- "prepare": Target worker declared intent to take over
- "commit": Ownership switch in-progress, awaiting final stabilization
- "abort": Handoff aborted (future use)
- "unknown": Parsing fallback
const ( HandoffClaimStable HandoffClaimState = "stable" HandoffClaimPrepare HandoffClaimState = "prepare" HandoffClaimCommit HandoffClaimState = "commit" HandoffClaimAbort HandoffClaimState = "abort" HandoffClaimUnknown HandoffClaimState = "unknown" )
type HandoffConfig ¶
type HandoffConfig struct {
// SweepInterval controls how often stale/expired claims are opportunistically
// swept during Apply calls. If zero or negative, a sweep is attempted on every Apply.
SweepInterval time.Duration `yaml:"sweepInterval" default:"30s" validate:"gte=0"`
// MaxRetries controls bounded CAS retries for claim updates. Zero uses default (3).
MaxRetries int `yaml:"maxRetries" default:"3" validate:"gte=0"`
// BaseBackoff is the initial backoff for CAS retry with exponential backoff.
BaseBackoff time.Duration `yaml:"baseBackoff" default:"50ms" validate:"gte=0"`
// MaxBackoff caps the exponential backoff duration.
MaxBackoff time.Duration `yaml:"maxBackoff" default:"500ms" validate:"gte=0,gtefield=BaseBackoff"`
// Jitter is a fractional value [0.0, 1.0] to randomize backoff durations.
Jitter float64 `yaml:"jitter" default:"0.2" validate:"gte=0,lte=1"`
// DelayAfterPrepare introduces an artificial delay after the prepare phase completes
// and before the consumer updater is invoked. Useful for making intermediate states
// observable in tests and demonstrations. Ignored if <= 0.
DelayAfterPrepare time.Duration `yaml:"delayAfterPrepare" default:"0" validate:"gte=0"`
// DelayBeforeStable introduces an artificial delay after entering the commit state
// and before finalizing to stable. Useful for external observation of the commit state.
// Ignored if <= 0.
DelayBeforeStable time.Duration `yaml:"delayBeforeStable" default:"0" validate:"gte=0"`
// PhaseConcurrency caps the number of in-flight per-partition KV operations
// during each of the prepare, commit, and stabilize phases. Zero uses the
// default of 20. Note: 0 means "use default", not "no parallelism"; set 1
// for strictly serial execution.
PhaseConcurrency int `yaml:"phaseConcurrency" default:"0" validate:"gte=0,lte=256"`
// ClaimWritePerSec optionally enables a per-worker token-bucket rate limit
// on every physical handoff claim-write (the KV PutIfEpoch the coordinator
// and startup loops issue), measured in writes/second. It paces the
// otherwise-unbounded startup hygiene/resume loops (which walk every claim
// key sequentially) and the two-phase coordinator's per-partition CAS
// writes — including CAS retries — so a large-fleet restart or rapid
// rebalance cannot drive a back-to-back KV write burst that stresses the
// NATS cluster. PhaseConcurrency bounds simultaneity; this bounds rate.
//
// Opt-in: 0 (the default) disables rate limiting and leaves behaviour
// unchanged. A single budget is shared across all claim-write sites for
// this worker. Must be >= 0.
//
// Note: the startup hygiene pass runs under OperationTimeout on the
// synchronous Manager.Start path, so a low rate combined with many
// pre-existing stale claims can extend Start's blocking return latency up
// to OperationTimeout before the best-effort pass stops early; the
// background resume pass and the coordinator are not OperationTimeout-bounded
// and run off the Start path. Sizing guidance lives in docs/OPERATIONS.md.
ClaimWritePerSec float64 `yaml:"claimWritePerSec" default:"0" validate:"gte=0"`
// ClaimWriteBurst is the token-bucket burst size for ClaimWritePerSec. It
// must be >= 1 when ClaimWritePerSec > 0 (rejected at Validate otherwise);
// it is ignored when ClaimWritePerSec == 0. Recommended starting point:
// burst ≈ PhaseConcurrency so a single rebalance wave is absorbed without
// throttling and only sustained bursts are paced.
ClaimWriteBurst int `yaml:"claimWriteBurst" default:"0" validate:"gte=0"`
// ClaimWriteClusterRate, when > 0, makes claim-write rate limiting
// fleet-size-aware: each worker enforces
// min(ClaimWritePerSec, ClaimWriteClusterRate/N), where N is the observed
// committed worker count. It bounds the STEADY-STATE cluster-wide
// claim-write rate to ClaimWriteClusterRate instead of N*ClaimWritePerSec.
// Requires ClaimWritePerSec > 0 (the per-worker ceiling and burst source)
// and EnableTwoPhaseHandoff. Default 0 = static per-worker rate only.
ClaimWriteClusterRate float64 `yaml:"claimWriteClusterRate" default:"0" validate:"gte=0"`
}
HandoffConfig controls two-phase handoff coordinator behavior.
These settings are only used when EnableTwoPhaseHandoff is true.
type HandoffMetricsRecorder ¶
type HandoffMetricsRecorder = types.HandoffMetricsRecorder
Re-export interfaces from the internal types package for convenience.
type HandoffSweepMetricsRecorder ¶ added in v2.10.1
type HandoffSweepMetricsRecorder = types.HandoffSweepMetricsRecorder
HandoffSweepMetricsRecorder is the optional claim-sweep observability capability a HandoffMetricsRecorder may additionally implement; see types.HandoffSweepMetricsRecorder for the label contract.
type KVBucketConfig ¶
type KVBucketConfig struct {
// StableIDBucket is the bucket name for stable worker ID claims.
StableIDBucket string `yaml:"stableIdBucket" default:"parti-stableid" validate:"required"`
// ElectionBucket is the bucket name for leader election.
ElectionBucket string `yaml:"electionBucket" default:"parti-election" validate:"required"`
// HeartbeatBucket is the bucket name for worker heartbeats.
HeartbeatBucket string `yaml:"heartbeatBucket" default:"parti-heartbeat" validate:"required"`
// AssignmentBucket is the bucket name for partition assignments.
AssignmentBucket string `yaml:"assignmentBucket" default:"parti-assignment" validate:"required"`
// AssignmentTTL is how long assignments remain in KV (0 = no expiration).
// Assignments should persist across leader changes for version continuity.
// Recommended: 0 (no TTL) or very long (e.g., 1 hour).
AssignmentTTL time.Duration `yaml:"assignmentTtl" default:"0" validate:"gte=0"`
// HandoffBucket is the bucket name for two-phase handoff ownership claims.
// Used only when EnableTwoPhaseHandoff is true. Stores per-partition claim
// records that track prepare/commit/stable transitions to ensure atomic
// ownership changes and crash-resumable state. Kept separate from assignment
// data to allow independent TTL and operational policies (sweeps, compaction).
// Recommended: distinct bucket to isolate churn from stable assignment data.
HandoffBucket string `yaml:"handoffBucket" default:"parti-handoff"`
// HandoffTTL is the advisory staleness threshold the two-phase handoff
// coordinator uses to recover a STUCK in-flight handoff: the periodic claim
// sweep resets a prepare/commit claim back to stable once it has gone
// untouched for longer than HandoffTTL.
//
// It is NOT a KV bucket TTL. The handoff bucket is created with no MaxAge —
// a bucket-level TTL would also age out healthy stable ownership claims
// (which are written once and never refreshed), permanently suppressing
// pull-gated consumers. Stable claims therefore never expire.
//
// Must be longer than the expected multi-phase handoff duration (including
// retries). Recommended: 2-5 minutes in production; fast tests may use
// seconds.
HandoffTTL time.Duration `yaml:"handoffTtl" default:"2m"`
// Replicas is the JetStream stream-replication factor applied to
// Parti-owned KV buckets when the library creates them fresh. Zero
// (the default) leaves the field unset, which nats.go normalizes to 1
// server-side — equivalent to no replication, the legacy behavior.
//
// Not to be confused with worker-pod replicas: Parti does not
// configure pod counts (that is the orchestrator's concern). This
// setting controls JetStream-level data replication only.
//
// Applies uniformly to every Parti-owned bucket the library creates
// (stableID, election, heartbeat, assignment, handoff). Per-bucket
// precision requires pre-creating buckets with explicit configs (the
// provision package supports this) — pre-created buckets keep their
// existing replica count regardless of this setting, because the
// library's ensure path is get-first.
//
// Post-create modification: NATS JetStream DOES support changing the
// replica count on an existing bucket via `UpdateStream`, but Parti
// does NOT auto-reconcile Replicas. Unlike MaxAge (which is reconciled
// because it carries a correctness invariant), Replicas is a
// per-deployment HA-quality decision; silently rewriting an existing
// bucket's Replicas could trigger expensive cross-node replication
// the operator did not ask for, or mask a deliberate downsize. When
// Manager.Start observes a pre-existing bucket whose Replicas differs
// from this setting, it emits a WARN log line naming the divergence
// and pointing at the `nats stream update KV_<bucket> --replicas=<N>`
// command operators can run to apply the change themselves.
//
// Cluster topology must support the value at create time or bucket
// creation fails loudly at Manager.Start (e.g. Replicas=3 against a
// single-node NATS server returns a stream-creation error).
//
// Recommended values:
// - 0 / 1 — single-node NATS or test environments (default)
// - 3 — production multi-node cluster (minimum for quorum)
// - 5 — high-availability tier
Replicas int `yaml:"replicas,omitempty" default:"0" validate:"gte=0"`
}
KVBucketConfig configures NATS JetStream KV bucket names and TTLs.
type LabelState ¶ added in v2.10.1
type LabelState struct {
// PoolSizes maps each label present in the last published rebalance to
// the number of live workers eligible for it.
PoolSizes map[string]int
// Parked maps each such label to the number of partitions currently
// parked (intentionally left unassigned) for it.
Parked map[string]int
}
LabelState is a point-in-time, leader-computed snapshot of label-mode observability state, exposed pull-style so deployments without a MetricsCollector get baseline label visibility ("are any VIP partitions parked right now?") from a health endpoint or log line. It complements, and does not replace, the push types.LabelMetrics interface.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager coordinates workers in a distributed system for partition-based work distribution.
Manager is the main entry point of the Parti library. It handles:
- Stable worker ID claiming using NATS KV
- Leader election for assignment coordination
- Partition assignment calculation and distribution
- Heartbeat publishing and failure detection
- Graceful rebalancing during scaling events
Thread Safety:
- All public methods are safe for concurrent use
- State transitions are atomic and linearizable
- Assignment updates are copy-on-write
Lifecycle:
- Create with NewManager()
- Call Start() to claim ID and begin coordination
- Use hooks to react to assignment changes
- Call Stop() for graceful shutdown
Testing: Consumers can define minimal interfaces for mocking:
type WorkCoordinator interface {
Start(ctx context.Context) error
WorkerID() string
}
func NewManager ¶
func NewManager(cfg *Config, js jetstream.JetStream, source PartitionSource, strategy AssignmentStrategy, opts ...Option) (*Manager, error)
NewManager creates a new Manager instance with the provided configuration.
The Manager coordinates workers in a distributed system using NATS for:
- Stable worker ID claiming (via NATS KV)
- Leader election for assignment coordination
- Partition assignment distribution
- Heartbeat publication for health monitoring
Returns a concrete *Manager struct following the "accept interfaces, return structs" principle. Consumers can define their own interfaces for testing if needed.
Internal components (calculator, heartbeat, election, claimer) are initialized with NoOp implementations, ensuring they are never nil.
Parameters:
- cfg: Configuration for the manager
- js: JetStream context for NATS interaction
- source: Source of partitions to distribute
- strategy: Strategy for assigning partitions to workers
- opts: Optional configuration options
Returns:
- *Manager: Initialized manager instance
- error: Validation error if configuration is invalid
Example:
cfg := parti.Config{WorkerIDPrefix: "worker", WorkerIDMax: 999}
src := source.NewStatic(partitions)
curStrategy := strategy.NewConsistentHash()
js, _ := jetstream.New(natsConn)
mgr, err := parti.NewManager(&cfg, js, src, curStrategy)
if err != nil {
log.Fatal(err)
}
Example ¶
This example demonstrates the basic usage of NewManager with default settings. In production, obtain js from nats.Connect + jetstream.New.
package main
import (
"context"
"fmt"
"github.com/arloliu/parti/v2"
"github.com/arloliu/parti/v2/source"
"github.com/arloliu/parti/v2/strategy"
"github.com/nats-io/nats.go/jetstream"
)
func main() {
// In production, obtain from nats.Connect + jetstream.New
var js jetstream.JetStream
cfg := &parti.Config{
WorkerIDPrefix: "worker",
WorkerIDMin: 0,
WorkerIDMax: 999,
}
partitions := []parti.Partition{
{Keys: []string{"orders", "0"}},
{Keys: []string{"orders", "1"}},
{Keys: []string{"orders", "2"}},
}
src := source.NewStatic(partitions)
hashStrategy := strategy.NewConsistentHash()
mgr, err := parti.NewManager(cfg, js, src, hashStrategy)
if err != nil {
return // requires live NATS; see integration tests for full examples
}
defer func() { _ = mgr.Stop(context.Background()) }()
if err := mgr.Start(context.Background()); err != nil {
// Always call Stop on Start failure to release resources.
return
}
fmt.Printf("Worker started with ID: %s\n", mgr.WorkerID())
}
Output:
Example (WithHooks) ¶
This example demonstrates using NewManager with hooks for lifecycle notifications.
package main
import (
"context"
"fmt"
"github.com/arloliu/parti/v2"
"github.com/arloliu/parti/v2/source"
"github.com/arloliu/parti/v2/strategy"
"github.com/nats-io/nats.go/jetstream"
)
func main() {
var js jetstream.JetStream
hooks := &parti.Hooks{
OnAssignmentChanged: func(ctx context.Context, oldPartitions, newPartitions []parti.Partition) error {
fmt.Printf("Assignment changed: %d -> %d partitions\n",
len(oldPartitions), len(newPartitions))
return nil
},
OnStateChanged: func(ctx context.Context, from, to parti.State) error {
fmt.Printf("State: %s -> %s\n", from, to)
return nil
},
OnLeadershipChanged: func(ctx context.Context, isLeader bool) error {
fmt.Printf("Leadership changed: isLeader=%v\n", isLeader)
return nil
},
}
cfg := &parti.Config{
WorkerIDPrefix: "worker",
WorkerIDMax: 10,
}
src := source.NewStatic([]parti.Partition{{Keys: []string{"p0"}}})
hashStrategy := strategy.NewConsistentHash()
_, err := parti.NewManager(cfg, js, src, hashStrategy,
parti.WithHooks(hooks),
)
if err != nil {
return // requires live NATS; see integration tests for full examples
}
}
Output:
Example (WithStrategy) ¶
This example demonstrates using NewManager with a custom strategy.
package main
import (
"github.com/arloliu/parti/v2"
"github.com/arloliu/parti/v2/source"
"github.com/arloliu/parti/v2/strategy"
"github.com/nats-io/nats.go/jetstream"
)
func main() {
var js jetstream.JetStream
cfg := &parti.Config{
WorkerIDPrefix: "processor",
WorkerIDMax: 50,
}
partitions := []parti.Partition{
{Keys: []string{"shard-0"}, Weight: 100},
{Keys: []string{"shard-1"}, Weight: 200},
{Keys: []string{"shard-2"}, Weight: 150},
}
src := source.NewStatic(partitions)
// Use consistent hash with more virtual nodes for better distribution
hashStrategy := strategy.NewConsistentHash(
strategy.WithVirtualNodes(300),
)
_, err := parti.NewManager(cfg, js, src, hashStrategy)
if err != nil {
return // requires live NATS; see integration tests for full examples
}
}
Output:
func (*Manager) Capabilities ¶ added in v2.4.0
Capabilities returns the current capability bitmask as an atomic snapshot.
The heartbeat publisher calls this on every heartbeat composition to embed the current runtime wire-up state. Do not cache the result — always read via this method so the heartbeat reflects live state.
Returns:
- uint32: Current capability bitmask (OR of active types.CapXxx constants)
func (*Manager) CurrentAssignment ¶
func (m *Manager) CurrentAssignment() Assignment
CurrentAssignment returns the current partition assignment for this worker.
The returned Assignment shares its Partitions backing array with the Manager's internal state. Callers MUST NOT modify the returned slice or its elements. If mutation is needed, make a copy first.
Returns:
- Assignment: The current assignment. Returns empty assignment if none received.
func (*Manager) InspectHandoffClaims ¶
func (m *Manager) InspectHandoffClaims(ctx context.Context) ([]HandoffClaim, error)
InspectHandoffClaims returns all current handoff claims from the configured handoff KV bucket for this Manager instance.
The method is best-effort and intended for integration tests and operational diagnostics. It requires two-phase handoff to be enabled and the handoff bucket to exist.
Parameters:
- ctx: Context for cancellation
Returns:
- []HandoffClaim: Decoded claim entries under the claims/ prefix
- error: Any failure opening the bucket or listing/decoding entries
func (*Manager) IsInRecoveryGrace ¶
IsInRecoveryGrace returns true if currently in recovery grace period.
This is part of the StateProvider interface and allows components like Calculator to check recovery grace status without circular dependencies.
Returns:
- bool: true if in recovery grace period
func (*Manager) IsLeader ¶
IsLeader returns whether this manager is currently the leader.
Returns:
- bool: true if this manager is the leader, false otherwise.
func (*Manager) LabelState ¶ added in v2.10.1
func (m *Manager) LabelState() LabelState
LabelState returns the label snapshot from this node's last successfully published rebalance. Leader-only: non-leaders, leaders that have not yet published a rebalance, and stopped Managers return the zero value (nil maps). Use Manager.IsLeader to disambiguate "not the leader" from "leader with nothing labeled". The returned maps are fresh copies; the caller owns them and may retain or mutate them freely.
The snapshot is point-in-time: a leadership transition strictly concurrent with the call may race it, but once the transition is observable (IsLeader() reports false — the election loop flips the flag before any calculator teardown begins), the accessor returns the zero value.
Key-set parity with the push gauges: keys are exactly the labeled pools of the published decision (the unlabeled general pool is never a key), and a label with zero parked partitions is present with an explicit 0.
Returns:
- LabelState: The snapshot, or the zero value when this node has none.
func (*Manager) RefreshPartitions ¶
RefreshPartitions triggers partition discovery refresh.
This method forces the partition source to be re-queried and, if the worker is the leader, triggers an immediate rebalance with the updated partition list. Non-leader workers will receive the updated assignments automatically.
Use this when:
- Partitions are added/removed dynamically (e.g., Kafka topics, Redis shards)
- You want to redistribute work after manual partition changes
- Your partition source has changed but workers haven't detected it yet
Parameters:
- ctx: Context for operation timeout
Returns:
- error: Refresh error, or ErrNotStarted if manager isn't running
Example:
// After adding new partitions to your partition source
if err := manager.RefreshPartitions(ctx); err != nil {
log.Printf("Failed to refresh partitions: %v", err)
}
func (*Manager) SetCapability ¶ added in v2.4.0
SetCapability sets or clears a single capability bit in the manager's heartbeat capability bitmask.
Called by the component that actually wires the corresponding safety mechanism — not by config-reading code. Examples:
- The two-phase handoff coordinator calls SetCapability(types.CapTwoPhaseHandoff, true) after successfully starting, and (…, false) on Stop.
- The consumer/updater reports CapProcessingGate via the optional CapabilityReporter interface; Manager samples it through reportConsumerCapabilities after every handoffCoordinator.Apply attempt and ORs the bit in here. The reporter pathway never clears bits; only other components may call SetCapability with active=false to clear.
- The heartbeat publisher's CapAckV1 bit is set by startHeartbeat after the publisher starts successfully.
Parameters:
- capBit: Capability bit to set or clear (e.g., types.CapAckV1)
- active: true to set the bit, false to clear it
func (*Manager) Start ¶
Start runs the manager's synchronous sanity-check phase: claim a stable worker ID, ensure required KV buckets exist, participate in election, start the heartbeat publisher, and start the calculator if elected leader. On success it transitions the manager to StateWaitingAssignment and spawns a background runner that attempts to fetch the initial assignment and apply it via the unified pipeline; on apply success the runner CAS-transitions to StateStable. A soft watchdog enters StateDegraded ("startup-timeout") if StartupTimeout (measured from Start invocation) elapses without reaching StateStable, signaling the readiness probe to rotate the pod.
Start returns once the sanity-check phase succeeds. The state observed after Start may be WaitingAssignment, Stable, or any calculator-driven active state (Scaling, Rebalancing, Emergency) depending on race. Callers that need to know the manager is ready to process work should call mgr.WaitState(StateStable, timeout).
The background runner is best-effort: on assignment-fetch or apply failure it logs and continues to monitor startup. Existing recovery mechanisms handle subsequent retries — the assignment watcher redelivers when the leader publishes; applyAssignmentWithPrev's scheduleApplyRetry re-attempts on apply failure; monitorNATSConnection drives attemptRecoveryFromDegraded on reconnect.
Apply boundedness: applyInitialAssignment internally calls handoffCoordinator.Apply(m.ctx, ...) which is unbounded per attempt (identical to pre-refactor Start). A stuck consumer updater can block the runner inside one apply attempt until Stop. The soft watchdog still fires enterDegraded("startup-timeout") in that case for probe-driven rotation.
Start returns an error only for synchronous-phase failures (bucket creation, ID claim, election RPC). Auto-cleanup invokes Stop on a non-nil error so callers do not need to call Stop after a failed Start.
Parameters:
- ctx: Context for startup-phase timeout control (not manager lifetime)
Returns:
- error: Synchronous-phase startup error or context cancellation
Example usage:
mgr := parti.NewManager(cfg, js, source, strategy)
if err := mgr.Start(ctx); err != nil {
return err // no need to call Stop
}
if err := <-mgr.WaitState(parti.StateStable, 30*time.Second); err != nil {
return err
}
func (*Manager) State ¶
State returns the current state of the manager.
Returns:
- State: The current state (e.g., StateInit, StateStable).
func (*Manager) Stop ¶
Stop gracefully shuts down the manager.
Safe to call multiple times - subsequent calls will return ErrNotStarted.
Parameters:
- ctx: Context for shutdown timeout
Returns:
- error: Shutdown error or timeout
func (*Manager) WaitState ¶
WaitState waits for the manager to reach the expected state within the timeout period.
This method is useful for testing and synchronization scenarios where you need to wait for the manager to reach a specific state before proceeding.
The method returns a read-only channel that will receive exactly one value:
- nil if the expected state is reached within the timeout
- context.DeadlineExceeded if the timeout expires before reaching the state
The channel is closed after sending the result, allowing safe use in select statements.
Parameters:
- expectedState: The state to wait for
- timeout: Maximum duration to wait for the state
Returns:
- <-chan error: A channel that receives the result (nil on success, error on timeout)
Example:
// Wait for manager to reach Stable state
errCh := manager.WaitState(StateStable, 10*time.Second)
if err := <-errCh; err != nil {
log.Printf("Failed to reach Stable state: %v", err)
}
// Using with select for multiple operations
select {
case err := <-manager.WaitState(StateStable, 5*time.Second):
if err != nil {
return fmt.Errorf("timeout waiting for stable state: %w", err)
}
case <-ctx.Done():
return ctx.Err()
}
// Waiting for multiple managers
for i, mgr := range managers {
if err := <-mgr.WaitState(StateStable, 10*time.Second); err != nil {
return fmt.Errorf("manager %d failed: %w", i, err)
}
}
func (*Manager) WorkerID ¶
WorkerID returns the stable worker ID claimed by this manager.
Returns:
- string: The worker ID (e.g., "worker-0"), or empty string if not yet claimed.
func (*Manager) WorkerLabels ¶ added in v2.9.0
WorkerLabels returns this worker's resolved label set (sorted, deduplicated, normalized at construction — see NewManager and normalizeWorkerLabels). Each call returns a fresh clone of the manager's internal label slice, so the caller owns the result and may freely mutate it without affecting the manager's internal state.
Returns:
- []string: A fresh copy of this worker's label set, or nil if unlabeled.
type ManagerMetrics ¶
type ManagerMetrics = types.ManagerMetrics
Re-export interfaces from the internal types package for convenience.
type MetricsCollector ¶
type MetricsCollector = types.MetricsCollector
Re-export interfaces from the internal types package for convenience.
type Option ¶
type Option func(*managerOptions)
Option configures a Manager with optional dependencies.
func WithBucketEpochProbeInterval ¶ added in v2.10.0
WithBucketEpochProbeInterval sets how often the bucket-epoch fence probes each Parti-owned KV bucket for a wipe-and-recreate event, overriding Config.BucketEpochProbeInterval — see that field's doc for the full cadence-vs-deadline rationale (OperationTimeout still bounds each individual probe).
A non-positive duration causes NewManager to return an error wrapping types.ErrInvalidConfig, matching the config field's gt=0 rule.
Parameters:
- d: Probe interval (> 0)
Returns:
- Option: Functional option for NewManager
func WithElectionAgent ¶
func WithElectionAgent(agent ElectionAgent) Option
WithElectionAgent sets a custom election agent.
Parameters:
- agent: ElectionAgent implementation
Returns:
- Option: Functional option for NewManager
Example:
agent := myElectionAgent
js, _ := jetstream.New(conn)
mgr, err := parti.NewManager(&cfg, js, src, strategy.NewConsistentHash(), parti.WithElectionAgent(agent))
if err != nil { /* handle */ }
func WithHandoffMetricsRecorder ¶
func WithHandoffMetricsRecorder(mr HandoffMetricsRecorder) Option
WithHandoffMetricsRecorder sets a specialized metrics recorder for the internal two-phase handoff coordinator.
Intended primarily for tests to assert CAS conflicts, phase timings, and sweeper behavior. In production, leave unset to use the default no-op or a future global wiring.
A recorder that additionally implements the optional HandoffSweepMetricsRecorder capability also receives per-origin claim-sweep pass counts; the coordinator type-asserts for it, so a recorder without the capability loses nothing else.
Parameters:
- mr: HandoffMetricsRecorder implementation
Returns:
- Option: Functional option for NewManager
func WithHooks ¶
WithHooks sets lifecycle event hooks.
Parameters:
- hooks: Hooks structure with callback functions
Returns:
- Option: Functional option for NewManager
Example:
hooks := &parti.Hooks{
OnAssignmentChanged: func(ctx context.Context, old, new []parti.Partition) error {
// derive added/removed by diffing old vs new if needed
return nil
},
}
js, _ := jetstream.New(conn)
mgr, err := parti.NewManager(&cfg, js, src, strategy.NewConsistentHash(), parti.WithHooks(hooks))
if err != nil { /* handle */ }
func WithLabelSpillGrace ¶ added in v2.9.1
WithLabelSpillGrace sets how long a label's worker pool must be continuously empty before its partitions spill to unlabeled fallback workers, overriding Config.LabelSpillGrace.
Use this to reach an immediate spill (d == 0): Config.LabelSpillGrace is a non-pointer duration with a 60s default, so an explicit 0 in the config is silently re-defaulted to 60s and cannot be configured. This option preserves the difference between "unset" and "0", so WithLabelSpillGrace(0) yields a grace of 0 (spill on the first rebalance that finds the pool empty).
The option wins over Config.LabelSpillGrace, mirroring the WithWorkerLabels vs Config.WorkerLabels precedence. A negative duration causes NewManager to return an error wrapping types.ErrInvalidConfig, matching the config field's gte=0 rule.
Parameters:
- d: Spill grace duration (>= 0; 0 means immediate spill)
Returns:
- Option: Functional option for NewManager
func WithLogger ¶
WithLogger sets a logger.
Parameters:
- logger: Logger implementation (compatible with zap.SugaredLogger)
Returns:
- Option: Functional option for NewManager
Example:
logger := zap.NewExample().Sugar()
js, _ := jetstream.New(conn)
mgr, err := parti.NewManager(&cfg, js, src, strategy.NewConsistentHash(), parti.WithLogger(logger))
if err != nil { /* handle */ }
func WithMetrics ¶
func WithMetrics(metrics MetricsCollector) Option
WithMetrics sets a metrics collector.
A collector that additionally implements the optional AssignmentDivergenceMetricsRecorder capability also receives equal-version divergence counts; the manager type-asserts for it once at construction, so a collector without the capability loses nothing else.
Parameters:
- metrics: MetricsCollector implementation
Returns:
- Option: Functional option for NewManager
Example:
metrics := myPrometheusCollector
js, _ := jetstream.New(conn)
mgr, err := parti.NewManager(&cfg, js, src, strategy.NewConsistentHash(), parti.WithMetrics(metrics))
if err != nil { /* handle */ }
func WithWorkerConsumerUpdater ¶
func WithWorkerConsumerUpdater(updater WorkerConsumerUpdater) Option
WithWorkerConsumerUpdater injects a WorkerConsumerUpdater used by Manager to apply the worker's current assignment to a single durable JetStream consumer.
Invocation Points:
- Immediately after initial assignment (async, best-effort)
- After each subsequent assignment change
This option enables fully manager-driven consumer reconciliation; hooks.OnAssignmentChanged can then be reserved for metrics or side effects instead of subscription wiring.
Parameters:
- updater: Implementation that maps assignments to consumer FilterSubjects
Returns:
- Option: Functional option for NewManager
Example:
js, _ := jetstream.New(nc)
helper, _ := durable.NewWorkerConsumer(js, durable.WorkerConsumerConfig{ /* ... */ }, handler)
mgr, err := parti.NewManager(cfg, js, src, strategy, parti.WithWorkerConsumerUpdater(helper))
if err != nil { /* handle */ }
func WithWorkerLabels ¶ added in v2.9.0
WithWorkerLabels sets this worker's label set, overriding Config.WorkerLabels. Labels are fixed for the manager's lifetime, published in every heartbeat, and drive label-based partition assignment. Invalid labels cause NewManager to return an error.
Use this instead of Config.WorkerLabels when several workers share one Config value (e.g. test clusters) but need distinct labels.
Parameters:
- labels: Worker label set (validated, sorted, and deduplicated by NewManager)
Returns:
- Option: Functional option for NewManager
type Partition ¶
Re-export types from the internal types package.
This file provides a stable, backward-compatible public API for the library's core types and interfaces. It uses type aliases to re-export definitions from the `types` subpackage, which contains the actual implementations.
This pattern solves the "import cycle" problem by allowing internal packages to depend on `types` without depending on the root `parti` package, while still providing a convenient `parti.State`, `parti.Logger`, etc. for users.
type PartitionSource ¶
type PartitionSource = types.PartitionSource
Re-export interfaces from the internal types package for convenience.
type PartitionSourceUpdater ¶
type PartitionSourceUpdater = types.PartitionSourceUpdater
Re-export interfaces from the internal types package for convenience.
type PartitionUpdater ¶
type PartitionUpdater = types.PartitionUpdater
Re-export interfaces from the internal types package for convenience.
type State ¶
Re-export types from the internal types package.
This file provides a stable, backward-compatible public API for the library's core types and interfaces. It uses type aliases to re-export definitions from the `types` subpackage, which contains the actual implementations.
This pattern solves the "import cycle" problem by allowing internal packages to depend on `types` without depending on the root `parti` package, while still providing a convenient `parti.State`, `parti.Logger`, etc. for users.
type StreamMissingHook ¶ added in v2.5.0
type StreamMissingHook = types.StreamMissingHook
StreamMissingHook is the operator-supplied escalation invoked when the dynamic partition consumer's recovery flow detects the underlying JetStream stream is absent. See types.StreamMissingHook for the operator contract — same-durable- name preservation, compatible-config reconciliation, and the post-hook checkpoint-reset / epoch-fence semantics.
type WatchablePartitionSource ¶
type WatchablePartitionSource = types.WatchablePartitionSource
Re-export interfaces from the internal types package for convenience.
type WorkerConsumerMetrics ¶
type WorkerConsumerMetrics = types.WorkerConsumerMetrics
Re-export interfaces from the internal types package for convenience.
type WorkerConsumerUpdater ¶
type WorkerConsumerUpdater interface {
// UpdateWorkerConsumer applies the given partition assignment to the worker's durable consumer.
//
// See interface documentation for semantics and concurrency guarantees.
//
// Parameters:
// - ctx: Context for cancellation and deadline
// - workerID: Stable worker ID claimed by Manager
// - partitions: Complete assignment slice (may be empty for zero subjects)
//
// Returns:
// - error: Non-nil only on unrecoverable configuration or API failure after retries
UpdateWorkerConsumer(ctx context.Context, workerID string, partitions []Partition) error
}
WorkerConsumerUpdater applies partition assignments to a worker-level durable JetStream consumer.
Semantics:
- Single durable consumer per worker (named <ConsumerPrefix>-<workerID>)
- Complete partition set provided each call (NOT a delta)
- Must be idempotent: identical subject set re-applied => no change
- SHOULD implement internal retries/backoff for transient JetStream errors
- MUST return error only for unrecoverable misconfiguration (e.g., invalid stream)
Concurrency: Implementations SHOULD be safe for concurrent calls.
type WorkerMetrics ¶
type WorkerMetrics = types.WorkerMetrics
Re-export interfaces from the internal types package for convenience.
Source Files
¶
- capability_reporter.go
- composite_updater.go
- config.go
- degrade_reasons.go
- doc.go
- errors.go
- fleet_size_observer.go
- kv_error_classify.go
- kvbuckets.go
- manager.go
- manager_assignment.go
- manager_degraded.go
- manager_election.go
- manager_handoff.go
- manager_handoff_ratelimit.go
- manager_select_authority.go
- manager_setup.go
- manager_startup_async.go
- manager_state.go
- options.go
- types.go
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
partictl
command
Package main is the cmd/partictl CLI for Parti environment provisioning.
|
Package main is the cmd/partictl CLI for Parti environment provisioning. |
|
Package consumer provides unified JetStream consumer types for different consumption patterns.
|
Package consumer provides unified JetStream consumer types for different consumption patterns. |
|
examples
|
|
|
basic
command
Package main demonstrates basic Parti usage with default settings.
|
Package main demonstrates basic Parti usage with default settings. |
|
degraded-readiness
command
Package main demonstrates wiring Parti's OnDegraded hook to a k8s readiness probe.
|
Package main demonstrates wiring Parti's OnDegraded hook to a k8s readiness probe. |
|
kv-watcher
command
|
|
|
internal
|
|
|
assignment
Package assignment provides partition assignment calculation and distribution.
|
Package assignment provides partition assignment calculation and distribution. |
|
durable
Package subscription provides durable JetStream consumer management utilities.
|
Package subscription provides durable JetStream consumer management utilities. |
|
dynamicbuild
Package dynamicbuild contains pure helpers shared by the runtime dynamic-consumer code path in internal/durable and the provision SDK's dynamic-consumer alignment builder.
|
Package dynamicbuild contains pure helpers shared by the runtime dynamic-consumer code path in internal/durable and the provision SDK's dynamic-consumer alignment builder. |
|
election
Package election provides leader election implementations for parti.
|
Package election provides leader election implementations for parti. |
|
heartbeat
Package heartbeat provides periodic health monitoring for workers through NATS KV.
|
Package heartbeat provides periodic health monitoring for workers through NATS KV. |
|
hooks
Package hooks provides hook implementations for the parti library.
|
Package hooks provides hook implementations for the parti library. |
|
kvbuckets
Package kvbuckets provides shared builders for JetStream KeyValue bucket configurations used by the Parti control plane.
|
Package kvbuckets provides shared builders for JetStream KeyValue bucket configurations used by the Parti control plane. |
|
logging
Package logging provides logging utilities for the Parti library.
|
Package logging provides logging utilities for the Parti library. |
|
partcodec
Package partcodec implements the wire-format codec for partition tables stored in NATS JetStream KV.
|
Package partcodec implements the wire-format codec for partition tables stored in NATS JetStream KV. |
|
partutil
Package partutil provides shared partition utilities used by both the public partition package and its internal implementation packages.
|
Package partutil provides shared partition utilities used by both the public partition package and its internal implementation packages. |
|
ratelimit
Package ratelimit provides a minimal token-bucket rate limiter abstraction for bounding per-worker JetStream consumer-create RPC rates.
|
Package ratelimit provides a minimal token-bucket rate limiter abstraction for bounding per-worker JetStream consumer-create RPC rates. |
|
retry
Package retry implements the bounded-retry envelope used to wrap the long-lived retry loops in the Parti runtime (source watcher restart, handoff watcher restart, assignment watcher, dynamic consumer recovery).
|
Package retry implements the bounded-retry envelope used to wrap the long-lived retry loops in the Parti runtime (source watcher restart, handoff watcher restart, assignment watcher, dynamic consumer recovery). |
|
testutil
Package testutil provides shared test utilities and fixtures for integration tests.
|
Package testutil provides shared test utilities and fixtures for integration tests. |
|
Package jsutil provides utilities for working with NATS JetStream streams and consumers.
|
Package jsutil provides utilities for working with NATS JetStream streams and consumers. |
|
Package kvutil provides utilities for working with NATS JetStream KeyValue stores.
|
Package kvutil provides utilities for working with NATS JetStream KeyValue stores. |
|
Package partitest provides test utilities for the Parti library.
|
Package partitest provides test utilities for the Parti library. |
|
Package partition provides static partition-based NATS publishing and subscribing.
|
Package partition provides static partition-based NATS publishing and subscribing. |
|
Package provision manages the NATS resources Parti's runtime depends on: control-plane KV buckets, the partition-source bucket, and the partition table stored inside the partition-source key.
|
Package provision manages the NATS resources Parti's runtime depends on: control-plane KV buckets, the partition-source bucket, and the partition table stored inside the partition-source key. |
|
scripts
|
|
|
gap_timeline
command
|
|
|
inspect_consumers
command
|
|
|
trace_visualizer
command
|
|
|
Package source provides built-in partition source implementations.
|
Package source provides built-in partition source implementations. |
|
Package strategy provides built-in assignment strategy implementations.
|
Package strategy provides built-in assignment strategy implementations. |
|
test
|
|
|
cmd/nats-server
command
This server runs in a separate process to isolate NATS server memory overhead from parti library measurements.
|
This server runs in a separate process to isolate NATS server memory overhead from parti library measurements. |
|
simulation/cmd/simulation
command
label_chaos.go implements the label_heartbeat_takeover chaos primitive: see coordinator.LabelHeartbeatTakeoverEvent's doc comment for the full rationale (why this replaces a kill+respawn approach).
|
label_chaos.go implements the label_heartbeat_takeover chaos primitive: see coordinator.LabelHeartbeatTakeoverEvent's doc comment for the full rationale (why this replaces a kill+respawn approach). |
|
simulation/internal/logging
Package logging provides logging utilities for the simulation.
|
Package logging provides logging utilities for the simulation. |
|
simulation/internal/worker
Package worker's ChaosProofMetrics gives label chaos scenarios a race-free way to positively prove four code paths actually fired, rather than inferring it from the absence of oracle violations (which can pass vacuously if the chaos primitive silently took a different, already-tested path — e.g.
|
Package worker's ChaosProofMetrics gives label chaos scenarios a race-free way to positively prove four code paths actually fired, rather than inferring it from the absence of oracle violations (which can pass vacuously if the chaos primitive silently took a different, already-tested path — e.g. |
|
Package types provides core type definitions and interfaces for the Parti library.
|
Package types provides core type definitions and interfaces for the Parti library. |