#53559·milvus

[Enhancement]: Generalize the streaming version gate into a declarative feature registry

Author: xaxysCreated Sep 17, 2026Updated Sep 17, 2026

Is there an existing issue for this?

  • I have searched the existing issues

What would you like to be added?

Generalize the streaming version gate into one declarative registry, instead of one hand-written method per gated capability.

Today the mechanism that makes a capability wait until every node that must understand it is upgraded (channel.StreamingVersion* markers) is copied per capability:

  • Balancer.WaitUntilWALbasedDDLReady and Balancer.WaitUntilSchemaDropReady both repeat "check the marker, satisfy the previous gate, wait for a role to be new enough, persist the marker", with the role and the version range hardcoded inside the method (balancer_impl.go, versionChecker265 / versionChecker300).
  • The caller side is feature-named too (rootcoord's waitUntilSchemaDropReady).

What I would like instead:

  • One VersionFeature enum of the gated capabilities (constants start at 1 so the zero value keeps meaning "no dependency").
  • One descriptor per feature: its name, the sticky marker to persist, the roles that must all be new enough (role + session version range), the gate it depends on, and an optional extra precondition that is neither of those (only the WAL-based DDL gate needs one: the streaming service must have been enabled at least once).
  • One Balancer.WaitUntilVersionFeatureReady(ctx, feature) that runs those steps in order, replacing the feature-named methods; rootcoord's helper becomes waitUntilVersionFeatureReady(ctx, feature).
  • Adding a capability becomes one constant + one descriptor + one call, and an unregistered constant is reported as an internal error instead of panicking on a request path.

Why is this needed?

The mechanism was introduced for the lossless streaming-architecture upgrade and later extended to the schema-drop DDL (DropField) by copying the four steps and hardcoding another role/version pair. Every new gated capability currently pays that copy again, adds permanent surface to the Balancer interface, and keeps the role/version pair inside a method body where it is hard to audit against the session version range it has to match.

Behaviour is intended to be unchanged: same markers, same wait order, same "one-time per cluster" semantics.

Anything else?

Implementation PR: see the comment below.