This is an automated email from the ASF dual-hosted git repository.
Similarityoung pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git
The following commit(s) were added to refs/heads/develop by this push:
new ac1ee2a84 refactor(cluster): consolidate duplicated endpoint ID suffix
algorithm (#969) (#972)
ac1ee2a84 is described below
commit ac1ee2a84770d56245cf818bd44deb16a098d17a
Author: 承潜 <[email protected]>
AuthorDate: Sun Jul 26 13:38:04 2026 +0800
refactor(cluster): consolidate duplicated endpoint ID suffix algorithm
(#969) (#972)
nextStableEndpointID (pkg/server) and uniqueSnapshotEndpointID (pkg/cluster)
were byte-for-byte identical implementations of the -2/-3 collision-suffix
algorithm, kept in sync only by convention. Since the endpoint ID is the
runtime health/cooldown key, a future edit to one and not the other would
silently assign different IDs to the same endpoint, splitting its health
state across a snapshot rebuild.
Lift the algorithm into model.StableUniqueEndpointID next to
GenerateEndpointID
(which both already call) and route all three call sites through it. No
behavior
change. Add a test asserting config assembly and snapshot rebuild produce
identical IDs for a duplicate-ID cluster, locking the two paths together.
---
pkg/cluster/cluster.go | 28 +---------------
pkg/model/cluster.go | 35 ++++++++++++++++++++
pkg/server/cluster_manager.go | 32 ++-----------------
pkg/server/cluster_manager_test.go | 65 ++++++++++++++++++++++++++++++++++++++
4 files changed, 104 insertions(+), 56 deletions(-)
diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go
index 4dc783820..6608e546b 100644
--- a/pkg/cluster/cluster.go
+++ b/pkg/cluster/cluster.go
@@ -18,7 +18,6 @@
package cluster
import (
- "fmt"
"sync"
"sync/atomic"
)
@@ -246,7 +245,7 @@ func newEndpointSnapshot(config *model.ClusterConfig,
previous *EndpointSnapshot
continue
}
snapshotEndpoint := model.CloneEndpoint(endpoint)
- snapshotEndpoint.ID = uniqueSnapshotEndpointID(clusterName,
snapshotEndpoint, endpointIDs)
+ snapshotEndpoint.ID = model.StableUniqueEndpointID(clusterName,
snapshotEndpoint, endpointIDs)
endpointIDs[snapshotEndpoint.ID] = struct{}{}
address := snapshotEndpoint.Address.GetAddress()
healthy := endpointSnapshotHealth(snapshotEndpoint, address,
previous, inheritRuntimeHealth)
@@ -268,31 +267,6 @@ func newEndpointSnapshotIndex(endpointCount int)
*EndpointSnapshot {
}
}
-// uniqueSnapshotEndpointID resolves a stable runtime ID for one endpoint in
-// the snapshot's per-cluster dedup set. The operator's explicit endpoint.ID
-// wins unless it collides; collisions append -2, -3, ... so an operator who
-// wrote id: foo twice sees foo and foo-2 (not generated-<hash>-2). When the
-// operator did not supply an ID, the deterministic hash from PR-2 is used as
-// the base and collisions on that synthesized base also append -2, -3, ...
-func uniqueSnapshotEndpointID(clusterName string, endpoint *model.Endpoint,
endpointIDs map[string]struct{}) string {
- id := ""
- if endpoint != nil {
- id = endpoint.ID
- }
- if id == "" {
- id = model.GenerateEndpointID(clusterName, endpoint)
- }
- if _, exists := endpointIDs[id]; !exists {
- return id
- }
- for suffix := 2; ; suffix++ {
- candidate := fmt.Sprintf("%s-%d", id, suffix)
- if _, exists := endpointIDs[candidate]; !exists {
- return candidate
- }
- }
-}
-
func endpointSnapshotHealth(
endpoint *model.Endpoint,
address string,
diff --git a/pkg/model/cluster.go b/pkg/model/cluster.go
index dda7e7983..6238f6001 100644
--- a/pkg/model/cluster.go
+++ b/pkg/model/cluster.go
@@ -216,6 +216,41 @@ func endpointIDMaterialField(name, value string) string {
return fmt.Sprintf("%s:%d:%s\n", name, len(value), value)
}
+// StableUniqueEndpointID resolves a stable runtime ID for one endpoint within
+// a per-cluster dedup set. The operator's explicit endpoint.ID wins unless it
+// collides with an entry already in taken; on collision it appends -2, -3, ...
+// so an operator who wrote id: foo twice sees foo and foo-2 (not
+// generated-<hash>-2). When the operator supplied no ID, GenerateEndpointID's
+// deterministic hash is the base and collisions on that base also append
+// -2, -3, ...
+//
+// The endpoint ID is the runtime health/cooldown key. Static/dynamic config
+// assembly (server.ClusterStore.assembleClusterEndpoints) and snapshot rebuild
+// (cluster.newEndpointSnapshot) must agree on it, or the same endpoint would
+// split its health state across a snapshot rebuild. Both call this so the two
+// paths can never drift.
+//
+// taken is read only: the caller records the returned ID before resolving the
+// next endpoint.
+func StableUniqueEndpointID(clusterName string, endpoint *Endpoint, taken
map[string]struct{}) string {
+ baseID := ""
+ if endpoint != nil {
+ baseID = endpoint.ID
+ }
+ if baseID == "" {
+ baseID = GenerateEndpointID(clusterName, endpoint)
+ }
+ if _, exists := taken[baseID]; !exists {
+ return baseID
+ }
+ for suffix := 2; ; suffix++ {
+ candidate := fmt.Sprintf("%s-%d", baseID, suffix)
+ if _, exists := taken[candidate]; !exists {
+ return candidate
+ }
+ }
+}
+
// CloneEndpoints returns a deep copy of endpoints. Nil input is preserved.
func CloneEndpoints(endpoints []*Endpoint) []*Endpoint {
if endpoints == nil {
diff --git a/pkg/server/cluster_manager.go b/pkg/server/cluster_manager.go
index fb3afb12b..31c907872 100644
--- a/pkg/server/cluster_manager.go
+++ b/pkg/server/cluster_manager.go
@@ -409,10 +409,10 @@ func (s *ClusterStore) assembleClusterEndpoints(c
*model.ClusterConfig) {
}
// Endpoint IDs are runtime health keys, so keep them unique
per cluster.
if endpoint.ID == "" {
- endpoint.ID = nextStableEndpointID(c.Name, endpoint,
endpointIDs)
+ endpoint.ID = model.StableUniqueEndpointID(c.Name,
endpoint, endpointIDs)
} else if _, exists := endpointIDs[endpoint.ID]; exists {
duplicateID := endpoint.ID
- endpoint.ID = nextStableEndpointID(c.Name, endpoint,
endpointIDs)
+ endpoint.ID = model.StableUniqueEndpointID(c.Name,
endpoint, endpointIDs)
logger.Warnf(
"[dubbo-go-pixiu] duplicate endpoint ID %s in
cluster %s, assigned endpoint ID %s",
duplicateID,
@@ -431,32 +431,6 @@ func (s *ClusterStore) assembleClusterEndpoints(c
*model.ClusterConfig) {
}
}
-// nextStableEndpointID returns a unique endpoint ID for the cluster's dedup
-// set. If endpoint.ID is set (operator-supplied) and only collides with a
-// sibling, it appends -2, -3, ... to preserve the operator's choice. If
-// endpoint.ID is empty, it derives a deterministic generated-* base via
-// model.GenerateEndpointID and suffixes that on collision. This matches
-// uniqueSnapshotEndpointID in pkg/cluster and keeps the same dashboard/log
-// identity post-rebuild.
-func nextStableEndpointID(clusterName string, endpoint *model.Endpoint,
endpointIDs map[string]struct{}) string {
- baseID := ""
- if endpoint != nil {
- baseID = endpoint.ID
- }
- if baseID == "" {
- baseID = model.GenerateEndpointID(clusterName, endpoint)
- }
- if _, exists := endpointIDs[baseID]; !exists {
- return baseID
- }
- for suffix := 2; ; suffix++ {
- candidate := fmt.Sprintf("%s-%d", baseID, suffix)
- if _, exists := endpointIDs[candidate]; !exists {
- return candidate
- }
- }
-}
-
// replaceClusterRuntime returns the old runtime so callers decide when to
stop it.
func (s *ClusterStore) replaceClusterRuntime(name string, config
*model.ClusterConfig) *cluster.Cluster {
var previous *cluster.EndpointSnapshot
@@ -818,7 +792,7 @@ func resolveSetEndpointSlotByHash(clusterName string,
incoming *model.Endpoint,
if endpointContentEqualForSet(e, incoming) {
return setEndpointOutcome{targetID: e.ID, action:
setEndpointIdempotent, replaceIdx: -1}
}
- suffixedID := nextStableEndpointID(clusterName, incoming,
existingEndpointIDs(existing))
+ suffixedID := model.StableUniqueEndpointID(clusterName,
incoming, existingEndpointIDs(existing))
logSetEndpointSuffix(clusterName, incomingHash, suffixedID)
return setEndpointOutcome{targetID: suffixedID, action:
setEndpointAppend, replaceIdx: -1}
}
diff --git a/pkg/server/cluster_manager_test.go
b/pkg/server/cluster_manager_test.go
index 87ece3de5..c7834aeb0 100644
--- a/pkg/server/cluster_manager_test.go
+++ b/pkg/server/cluster_manager_test.go
@@ -1111,6 +1111,71 @@ func TestAssembleEndpointsDeduplicatesExplicitID(t
*testing.T) {
"not the generated- hash, so the operator's choice
stays readable")
}
+// TestEndpointIDAssemblyAndSnapshotRebuildAgree locks issue #969: the config
+// assembly path (ClusterStore.assembleClusterEndpoints, used for
static/dynamic
+// config) and the snapshot-rebuild path (cluster.NewCluster ->
newEndpointSnapshot)
+// must assign byte-identical endpoint IDs for the same cluster. The endpoint
ID
+// is the runtime health/cooldown key, so if the two -2/-3 suffix algorithms
ever
+// drift, the same endpoint would get a different ID on a snapshot rebuild and
+// split its health state. Both paths now route through
model.StableUniqueEndpointID;
+// this test fails if a future change reintroduces a second, diverging copy.
+func TestEndpointIDAssemblyAndSnapshotRebuildAgree(t *testing.T) {
+ const clusterName = "id-agreement"
+
+ idsOf := func(endpoints []*model.Endpoint) []string {
+ ids := make([]string, len(endpoints))
+ for i, endpoint := range endpoints {
+ ids[i] = endpoint.ID
+ }
+ return ids
+ }
+
+ tests := []struct {
+ name string
+ endpoints func() []*model.Endpoint
+ }{
+ {
+ // Operator wrote the same id: twice — base is the
operator ID.
+ name: "explicit duplicate ID",
+ endpoints: func() []*model.Endpoint {
+ return []*model.Endpoint{
+ {ID: "foo", Address:
model.SocketAddress{Address: "127.0.0.1", Port: 22001}},
+ {ID: "foo", Address:
model.SocketAddress{Address: "127.0.0.1", Port: 22002}},
+ }
+ },
+ },
+ {
+ // No IDs and identical hash material — base is the
generated hash.
+ name: "anonymous endpoints with identical hash
material",
+ endpoints: func() []*model.Endpoint {
+ return []*model.Endpoint{
+ {Address: model.SocketAddress{Address:
"127.0.0.1", Port: 22010}},
+ {Address: model.SocketAddress{Address:
"127.0.0.1", Port: 22010}},
+ }
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assembled := &model.ClusterConfig{Name: clusterName,
Endpoints: tt.endpoints()}
+ (&ClusterStore{}).assembleClusterEndpoints(assembled)
+ assembledIDs := idsOf(assembled.Endpoints)
+
+ runtime :=
cluster.NewCluster(&model.ClusterConfig{Name: clusterName, Endpoints:
tt.endpoints()})
+ snapshotIDs :=
idsOf(runtime.EndpointSnapshot().AllEndpoints())
+
+ assert.Equal(t, assembledIDs, snapshotIDs,
+ "config assembly and snapshot rebuild must
assign identical endpoint IDs")
+
+ if assert.Len(t, assembledIDs, 2) {
+ assert.Equal(t, assembledIDs[0]+"-2",
assembledIDs[1],
+ "both paths must suffix the colliding
second endpoint with -2, not collapse it")
+ }
+ })
+ }
+}
+
func testClusterManager(clusters ...*model.ClusterConfig) *ClusterManager {
return CreateDefaultClusterManager(&model.Bootstrap{
StaticResources: model.StaticResources{