Skip to content

StatefulSet ordinal mode can silently leave buckets unowned #3384

Description

@waveywaves

Expected Behavior

StatefulSet ordinal mode should not silently leave configured buckets without owners.

If the bucket count exceeds the number of StatefulSet replicas, Knative should either distribute all buckets across the available replicas or report an actionable configuration error.

Actual Behavior

Each StatefulSet replica owns only the bucket at its ordinal:

// NewStatefulSetBucketAndSet creates a BucketSet for StatefulSet controller with
// the given bucket size and the information from environment variables. Then uses
// the created BucketSet to create a Bucket for this StatefulSet Pod.
func NewStatefulSetBucketAndSet(buckets int) (reconciler.Bucket, *hash.BucketSet, error) {
ssc, err := newStatefulSetConfig()
if err != nil {
return nil, nil, err
}
if ssc.StatefulSetID.ordinal >= buckets {
return nil, nil, fmt.Errorf("ordinal %d is out of range [0, %d)",
ssc.StatefulSetID.ordinal, buckets)
}
names := make(sets.Set[string], buckets)
for i := range buckets {
names.Insert(statefulSetPodDNS(i, ssc))
}
bs := hash.NewBucketSet(names)
// Buckets is sorted in order of names so we can use ordinal to
// get the correct Bucket for this binary.
return bs.Buckets()[ssc.StatefulSetID.ordinal], bs, nil

The configuration provides the pod ordinal but not the StatefulSet replica count:

// statefulSetID is a envconfig Decodable controller ordinal and name.
type statefulSetID struct {
ssName string
ordinal int
}
func (ssID *statefulSetID) Decode(v string) error {
if i := strings.LastIndex(v, "-"); i != -1 {
ui, err := strconv.Atoi(v[i+1:])
ssID.ordinal = ui
ssID.ssName = v[:i]
return err
}
return fmt.Errorf("%q is not a valid stateful set controller ordinal", v)
}
var _ envconfig.Decoder = (*statefulSetID)(nil)
// statefulSetConfig represents the required information for a StatefulSet service.
type statefulSetConfig struct {
StatefulSetID statefulSetID `envconfig:"STATEFUL_CONTROLLER_ORDINAL" required:"true"`
ServiceName string `envconfig:"STATEFUL_SERVICE_NAME" required:"true"`
Port string `envconfig:"STATEFUL_SERVICE_PORT" default:"80"`
Protocol string `envconfig:"STATEFUL_SERVICE_PROTOCOL" default:"http"`
}
// newStatefulSetConfig builds a stateful set LE config.
func newStatefulSetConfig() (*statefulSetConfig, error) {
ssc := &statefulSetConfig{}
if err := envconfig.Process("", ssc); err != nil {
return nil, err
}
return ssc, nil
}

With eight buckets and four replicas, four buckets have no owner. All four pods start successfully, but keys hashing to the unowned buckets are never reconciled.

Steps to Reproduce the Problem

  1. Configure leader election with eight buckets.
  2. Run the controller in StatefulSet ordinal mode with four replicas.
  3. Enqueue keys covering the eight-bucket hash space.
  4. Observe that keys assigned to buckets without corresponding pod ordinals are never reconciled.

Additional Info

The original StatefulSet integration used matching counts—ten replicas and ten buckets:

#1451

A downstream occurrence and current validation workaround are tracked in:

One possible design is to supply the replica count explicitly and assign buckets deterministically:

owner(bucketIndex) = bucketIndex % replicaCount

This would require defining safe behavior for scaling and rolling updates. If multiple-bucket ownership is not desired, detecting and reporting the unsupported topology would still prevent silent loss of reconciliation.

/area API
/kind bug

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/APIkind/bugCategorizes issue or PR as related to a bug.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions