From ec764c1251a22f2d6fd6aaf39b353330db391512 Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Thu, 4 Jun 2026 17:10:32 +0800 Subject: [PATCH 01/10] feat(metadata): add canonical revision calculation for MetadataInfo Add three methods to replace CRC32-based revision with MD5 over deterministic ServiceInfo serialization, aligning with Java dubbo's MetadataInfo.calAndGetRevision(). - ServiceInfo.toDescString(): deterministic string representation in format (name|group|version|protocol|port|path|params|methods), with sorted params/methods keys for stable output. - CalRevision(app, services): computes MD5 hex digest from app name concatenated with sorted ServiceInfo.toDescString() outputs. Returns 0 for empty services (matches Java EMPTY_REVISION). - MetadataInfo.CalAndGetRevision(): updates info.Revision in-place. This replaces the coarse app+path+version+port+method CRC32 approach in resolveRevision, ensuring group/protocol/params changes are captured and revision is strongly bound to the serialized MetadataInfo content. --- metadata/info/metadata_info.go | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/metadata/info/metadata_info.go b/metadata/info/metadata_info.go index 1dd4ffc108..455b6f6e40 100644 --- a/metadata/info/metadata_info.go +++ b/metadata/info/metadata_info.go @@ -18,7 +18,10 @@ package info import ( + "crypto/md5" + "fmt" "net/url" + "sort" "strconv" "strings" ) @@ -174,6 +177,15 @@ func (info *MetadataInfo) ReplaceExportedServices(urls []*common.URL) { } } +// CalAndGetRevision calculates and updates the revision for this MetadataInfo. +// The revision is derived from the canonical ServiceInfo representation, +// ensuring strong binding between revision and serialized metadata content. +// Aligned with Java dubbo MetadataInfo.calAndGetRevision(). +func (info *MetadataInfo) CalAndGetRevision() string { + info.Revision = CalRevision(info.App, info.Services) + return info.Revision +} + func (info *MetadataInfo) findExportedServiceURL(matchKey string) *common.URL { for _, urls := range info.exportedServiceURLs { for _, serviceURL := range urls { @@ -283,3 +295,90 @@ func (si *ServiceInfo) GetServiceKey() string { si.ServiceKey = common.ServiceKey(si.Name, si.Group, si.Version) return si.ServiceKey } + +// toDescString returns a deterministic string representation of ServiceInfo +// for revision calculation. Aligned with Java dubbo ServiceInfo.toDescString(). +// +// Format: name|group|version|protocol|port|path|params|methods +// +// Empty fields use "" as placeholder to keep separator count stable. +// Params are sorted by key alphabetically, joined as k=v&k=v. +// The "methods" key is excluded from params and appended separately. +// Methods are sorted alphabetically and comma-joined. +// No escaping is performed on param values (aligned with Java behavior). +func (si *ServiceInfo) toDescString() string { + var b strings.Builder + + b.WriteString(si.Name) + b.WriteByte('|') + b.WriteString(si.Group) + b.WriteByte('|') + b.WriteString(si.Version) + b.WriteByte('|') + b.WriteString(si.Protocol) + b.WriteByte('|') + b.WriteString(strconv.Itoa(si.Port)) + b.WriteByte('|') + b.WriteString(si.Path) + b.WriteByte('|') + + // params: sorted keys, exclude methods key + keys := make([]string, 0, len(si.Params)) + for k := range si.Params { + if k == constant.MethodsKey { + continue + } + keys = append(keys, k) + } + sort.Strings(keys) + for i, k := range keys { + if i > 0 { + b.WriteByte('&') + } + b.WriteString(k) + b.WriteByte('=') + b.WriteString(si.Params[k]) + } + + b.WriteByte('|') + + // methods: sorted alphabetically, comma-joined + if methodsStr, ok := si.Params[constant.MethodsKey]; ok && len(methodsStr) > 0 { + methods := strings.Split(methodsStr, ",") + sort.Strings(methods) + for i, m := range methods { + if i > 0 { + b.WriteByte(',') + } + b.WriteString(m) + } + } + + return b.String() +} + +// CalRevision calculates a deterministic revision string from canonical ServiceInfo objects. +// Returns "0" if services is empty (aligned with Java EMPTY_REVISION). +// Services are sorted by matchKey before serialization to ensure deterministic output. +// The revision is an MD5 hex digest of: app + sorted toDescString of each ServiceInfo. +func CalRevision(app string, services map[string]*ServiceInfo) string { + if len(services) == 0 { + return "0" + } + + // collect and sort matchKeys for deterministic iteration + matchKeys := make([]string, 0, len(services)) + for mk := range services { + matchKeys = append(matchKeys, mk) + } + sort.Strings(matchKeys) + + var b strings.Builder + b.WriteString(app) + for _, mk := range matchKeys { + b.WriteString(services[mk].toDescString()) + } + + sum := md5.Sum([]byte(b.String())) + return fmt.Sprintf("%x", sum) +} From be0d5441f42b2f262779ef2ae0636463a5e908eb Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Thu, 4 Jun 2026 17:16:31 +0800 Subject: [PATCH 02/10] refactor(revision): replace CRC32 revision with canonical MD5-based calculation Refactor resolveRevision to derive revision from canonical ServiceInfo objects rather than raw URLs, aligning with Java dubbo's MetadataInfo.calAndGetRevision(). Changes in service_revision_customizer.go: - resolveRevision now converts URLs to ServiceInfo via NewServiceInfoWithURL, groups by MatchKey, and delegates to info.CalRevision (MD5 over sorted toDescString) instead of CRC32-summing app+path+version+port+method. - Customizers now source URLs from instance.GetServiceMetadata() rather than the global GetMetadataService(), so each instance's revision is scoped to its own MetadataInfo. This fixes cross-test state leakage. Changes in service_revision_customizer_test.go (new, 10 cases): - Group / protocol / params / methods / version change triggers revision change - Deterministic: same input always produces same revision - Empty URL list returns 0 - Ordering-independent: URL insertion order and param key order do not affect revision - Non-IncludeKeys params are ignored --- .../customizer/service_revision_customizer.go | 55 ++---- .../service_revision_customizer_test.go | 184 ++++++++++++++++++ 2 files changed, 204 insertions(+), 35 deletions(-) create mode 100644 registry/servicediscovery/customizer/service_revision_customizer_test.go diff --git a/registry/servicediscovery/customizer/service_revision_customizer.go b/registry/servicediscovery/customizer/service_revision_customizer.go index a9598d997c..c0733ac8f9 100644 --- a/registry/servicediscovery/customizer/service_revision_customizer.go +++ b/registry/servicediscovery/customizer/service_revision_customizer.go @@ -17,12 +17,6 @@ package customizer -import ( - "fmt" - "hash/crc32" - "sort" -) - import ( "github.com/dubbogo/gost/log/logger" ) @@ -31,7 +25,7 @@ import ( "dubbo.apache.org/dubbo-go/v3/common" "dubbo.apache.org/dubbo-go/v3/common/constant" "dubbo.apache.org/dubbo-go/v3/common/extension" - "dubbo.apache.org/dubbo-go/v3/metadata" + "dubbo.apache.org/dubbo-go/v3/metadata/info" "dubbo.apache.org/dubbo-go/v3/registry" ) @@ -51,12 +45,12 @@ func (e *exportedServicesRevisionMetadataCustomizer) GetPriority() int { // Customize calculate the revision for exported urls and then put it into instance metadata func (e *exportedServicesRevisionMetadataCustomizer) Customize(instance registry.ServiceInstance) { - urls, err := metadata.GetMetadataService().GetExportedServiceURLs() - if err != nil { - logger.Errorf("[Registry][ServiceDiscovery] get metadata service url is error, err=%v", err) + metaInfo := instance.GetServiceMetadata() + if metaInfo == nil { + logger.Warn("[Registry][ServiceDiscovery] exportedServicesRevision customizer: instance service metadata is nil") return } - revision := resolveRevision(urls) + revision := resolveRevision(metaInfo.GetExportedServiceURLs()) if len(revision) == 0 { revision = defaultRevision } @@ -72,45 +66,36 @@ func (e *subscribedServicesRevisionMetadataCustomizer) GetPriority() int { // Customize calculate the revision for subscribed urls and then put it into instance metadata func (e *subscribedServicesRevisionMetadataCustomizer) Customize(instance registry.ServiceInstance) { - urls, err := metadata.GetMetadataService().GetSubscribedURLs() - if err != nil { - logger.Errorf("[Registry][ServiceDiscovery] get metadata subscribed url is error, err=%v", err) + metaInfo := instance.GetServiceMetadata() + if metaInfo == nil { + logger.Warn("[Registry][ServiceDiscovery] subscribedServicesRevision customizer: instance service metadata is nil") return } - revision := resolveRevision(urls) + revision := resolveRevision(metaInfo.GetSubscribedURLs()) if len(revision) == 0 { revision = defaultRevision } instance.GetMetadata()[constant.SubscribedServicesRevisionPropertyName] = revision } -// resolveRevision provides the actual pattern to calculate the revision. -// please refer to dubbo-java's method, org.apache.dubbo.metadata.Metadata#calAndGetRevision +// resolveRevision calculates a deterministic revision from the given URLs. +// It converts URLs to canonical ServiceInfo objects and delegates to info.CalRevision, +// aligning with Java dubbo's MetadataInfo.calAndGetRevision(). func resolveRevision(urls []*common.URL) string { if len(urls) == 0 { return "0" } - candidates := make([]string, 0, len(urls)) + // build canonical ServiceInfo map from URLs, keyed by MatchKey + services := make(map[string]*info.ServiceInfo, len(urls)) + app := "" for _, u := range urls { - desc := u.GetParam(constant.ApplicationKey, "") + u.Path + u.GetParam(constant.VersionKey, "") + u.Port - - if len(u.Methods) == 0 { - candidates = append(candidates, desc) - } else { - for _, m := range u.Methods { - // methods are part of candidates - candidates = append(candidates, desc+constant.KeySeparator+m) - } + si := info.NewServiceInfoWithURL(u) + services[si.GetMatchKey()] = si + if app == "" { + app = u.GetParam(constant.ApplicationKey, "") } - } - sort.Strings(candidates) - // it's nearly impossible to be overflow - res := uint64(0) - for _, c := range candidates { - res += uint64(crc32.ChecksumIEEE([]byte(c))) - } - return fmt.Sprint(res) + return info.CalRevision(app, services) } diff --git a/registry/servicediscovery/customizer/service_revision_customizer_test.go b/registry/servicediscovery/customizer/service_revision_customizer_test.go new file mode 100644 index 0000000000..39db9c43be --- /dev/null +++ b/registry/servicediscovery/customizer/service_revision_customizer_test.go @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package customizer + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "dubbo.apache.org/dubbo-go/v3/common" + "dubbo.apache.org/dubbo-go/v3/common/constant" +) + +// helper to create a URL with common service discovery fields +func newTestURL(protocol string, port string, path string, application string, group string, version string, methods []string, extraParams map[string]string) *common.URL { + opts := []common.Option{ + common.WithProtocol(protocol), + common.WithPort(port), + common.WithPath(path), + common.WithParamsValue(constant.ApplicationKey, application), + common.WithParamsValue(constant.GroupKey, group), + common.WithParamsValue(constant.VersionKey, version), + common.WithParamsValue(constant.SideKey, constant.SideProvider), + } + if len(methods) > 0 { + opts = append(opts, common.WithMethods(methods)) + } + for k, v := range extraParams { + opts = append(opts, common.WithParamsValue(k, v)) + } + u, _ := common.NewURL(protocol+"://127.0.0.1:"+port+"/"+path, opts...) + return u +} + +// 1. group change → revision changes +func TestRevisionChangesOnGroupChange(t *testing.T) { + u1 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, nil) + u2 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupB", "1.0.0", []string{"sayHello"}, nil) + + r1 := resolveRevision([]*common.URL{u1}) + r2 := resolveRevision([]*common.URL{u2}) + + assert.NotEmpty(t, r1) + assert.NotEmpty(t, r2) + assert.NotEqual(t, r1, r2, "revision should change when group changes") +} + +// 2. protocol change → revision changes +func TestRevisionChangesOnProtocolChange(t *testing.T) { + u1 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, nil) + u2 := newTestURL("tri", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, nil) + + r1 := resolveRevision([]*common.URL{u1}) + r2 := resolveRevision([]*common.URL{u2}) + + assert.NotEmpty(t, r1) + assert.NotEmpty(t, r2) + assert.NotEqual(t, r1, r2, "revision should change when protocol changes") +} + +// 3. params change (timeout, loadbalance) → revision changes +func TestRevisionChangesOnParamsChange(t *testing.T) { + u1 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, + map[string]string{constant.TimeoutKey: "3000", constant.LoadbalanceKey: "random"}) + u2 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, + map[string]string{constant.TimeoutKey: "5000", constant.LoadbalanceKey: "roundrobin"}) + + r1 := resolveRevision([]*common.URL{u1}) + r2 := resolveRevision([]*common.URL{u2}) + + assert.NotEmpty(t, r1) + assert.NotEmpty(t, r2) + assert.NotEqual(t, r1, r2, "revision should change when params (timeout/loadbalance) change") +} + +// 4. methods change → revision changes +func TestRevisionChangesOnMethodChange(t *testing.T) { + u1 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, nil) + u2 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello", "sayGoodbye"}, nil) + + r1 := resolveRevision([]*common.URL{u1}) + r2 := resolveRevision([]*common.URL{u2}) + + assert.NotEmpty(t, r1) + assert.NotEmpty(t, r2) + assert.NotEqual(t, r1, r2, "revision should change when methods change") +} + +// 5. version change → revision changes +func TestRevisionChangesOnVersionChange(t *testing.T) { + u1 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, nil) + u2 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "2.0.0", []string{"sayHello"}, nil) + + r1 := resolveRevision([]*common.URL{u1}) + r2 := resolveRevision([]*common.URL{u2}) + + assert.NotEmpty(t, r1) + assert.NotEmpty(t, r2) + assert.NotEqual(t, r1, r2, "revision should change when version changes") +} + +// 6. same input → same revision (deterministic) +func TestRevisionStable(t *testing.T) { + u := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello", "sayGoodbye"}, + map[string]string{constant.TimeoutKey: "3000"}) + + r1 := resolveRevision([]*common.URL{u}) + r2 := resolveRevision([]*common.URL{u}) + r3 := resolveRevision([]*common.URL{u}) + + assert.Equal(t, r1, r2, "same input should produce same revision") + assert.Equal(t, r2, r3, "same input should produce same revision") +} + +// 7. empty URL list → "0" +func TestRevisionEmptyServices(t *testing.T) { + r := resolveRevision(nil) + assert.Equal(t, "0", r) + + r = resolveRevision([]*common.URL{}) + assert.Equal(t, "0", r) +} + +// 8. different insertion order → same revision +func TestRevisionOrderingIndependent(t *testing.T) { + u1 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, nil) + u2 := newTestURL("dubbo", "20881", "com.example.AnotherService", "test-app", "groupA", "1.0.0", []string{"getUser"}, nil) + + r1 := resolveRevision([]*common.URL{u1, u2}) + r2 := resolveRevision([]*common.URL{u2, u1}) + + assert.Equal(t, "0", "0") // dummy assertion for baseline + assert.Equal(t, r1, r2, "revision should be the same regardless of URL order") +} + +// 9. params key-value ordering → same revision +func TestRevisionParamsOrderStable(t *testing.T) { + // Params in different order within URL — NewURL handles param insertion, + // so we build two URLs that end up with the same effective params. + // The key test is that ServiceInfo.toDescString() sorts params by key. + u1 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, + map[string]string{constant.TimeoutKey: "3000", constant.ClusterKey: "failover"}) + u2 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, + map[string]string{constant.ClusterKey: "failover", constant.TimeoutKey: "3000"}) + + r1 := resolveRevision([]*common.URL{u1}) + r2 := resolveRevision([]*common.URL{u2}) + + assert.Equal(t, r1, r2, "revision should be the same regardless of param key-value ordering") +} + +// 10. non-IncludeKeys params → revision unchanged +func TestRevisionIgnoresNonIncludeKeys(t *testing.T) { + u1 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, + map[string]string{constant.TimeoutKey: "3000"}) + // add a param NOT in IncludeKeys (e.g., a custom arbitrary param) + u2 := newTestURL("dubbo", "20880", "com.example.TestService", "test-app", "groupA", "1.0.0", []string{"sayHello"}, + map[string]string{constant.TimeoutKey: "3000", "custom.arbitrary.key": "someValue"}) + + r1 := resolveRevision([]*common.URL{u1}) + r2 := resolveRevision([]*common.URL{u2}) + + // Note: custom.arbitrary.key is NOT in IncludeKeys, so it should be filtered by NewServiceInfoWithURL + assert.Equal(t, r1, r2, "revision should ignore params not in IncludeKeys") +} From 1a3718ed839c9d958115fc50190936a173271200 Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Thu, 4 Jun 2026 19:47:22 +0800 Subject: [PATCH 03/10] fix: remove useless assertion leftover from debugging --- .../customizer/service_revision_customizer_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/registry/servicediscovery/customizer/service_revision_customizer_test.go b/registry/servicediscovery/customizer/service_revision_customizer_test.go index 39db9c43be..34034996ab 100644 --- a/registry/servicediscovery/customizer/service_revision_customizer_test.go +++ b/registry/servicediscovery/customizer/service_revision_customizer_test.go @@ -148,7 +148,6 @@ func TestRevisionOrderingIndependent(t *testing.T) { r1 := resolveRevision([]*common.URL{u1, u2}) r2 := resolveRevision([]*common.URL{u2, u1}) - assert.Equal(t, "0", "0") // dummy assertion for baseline assert.Equal(t, r1, r2, "revision should be the same regardless of URL order") } From a279983ac7bda0ecfbc290f295fd690838547ad1 Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Thu, 4 Jun 2026 19:52:01 +0800 Subject: [PATCH 04/10] fix(metadata): replace MD5 with SHA-512 in revision calculation Switch the hash algorithm used in CalRevision from MD5 (32 hex chars) to SHA-512 (128 hex chars) for stronger collision resistance. --- metadata/info/metadata_info.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metadata/info/metadata_info.go b/metadata/info/metadata_info.go index 455b6f6e40..b7a43b4ec1 100644 --- a/metadata/info/metadata_info.go +++ b/metadata/info/metadata_info.go @@ -18,7 +18,7 @@ package info import ( - "crypto/md5" + "crypto/sha512" "fmt" "net/url" "sort" @@ -360,7 +360,7 @@ func (si *ServiceInfo) toDescString() string { // CalRevision calculates a deterministic revision string from canonical ServiceInfo objects. // Returns "0" if services is empty (aligned with Java EMPTY_REVISION). // Services are sorted by matchKey before serialization to ensure deterministic output. -// The revision is an MD5 hex digest of: app + sorted toDescString of each ServiceInfo. +// The revision is a SHA-512 hex digest of: app + sorted toDescString of each ServiceInfo. func CalRevision(app string, services map[string]*ServiceInfo) string { if len(services) == 0 { return "0" @@ -379,6 +379,6 @@ func CalRevision(app string, services map[string]*ServiceInfo) string { b.WriteString(services[mk].toDescString()) } - sum := md5.Sum([]byte(b.String())) + sum := sha512.Sum512([]byte(b.String())) return fmt.Sprintf("%x", sum) } From 8b25b7e6ee86ed62297b28ea998fcc058e065ff4 Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Fri, 5 Jun 2026 14:40:17 +0800 Subject: [PATCH 05/10] fix(metadata): exclude EnvironmentKey from ServiceInfo IncludeKeys Environment is instance-level routing metadata (per the consumer-side comment in service_instances_changed_listener_impl.go). The consumer always overrides it from the current instance metadata, regardless of what is cached by revision. Including it in IncludeKeys caused revision to change between instances with different environment tags, producing spurious revision updates with no routing impact. --- metadata/info/metadata_info.go | 8 +++++++- metadata/info/metadata_info_test.go | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/metadata/info/metadata_info.go b/metadata/info/metadata_info.go index b7a43b4ec1..500e7291c5 100644 --- a/metadata/info/metadata_info.go +++ b/metadata/info/metadata_info.go @@ -55,9 +55,15 @@ var IncludeKeys = gxset.NewSet( constant.VersionKey, constant.WarmupKey, constant.WeightKey, - constant.EnvironmentKey, constant.ReleaseKey) +// EnvironmentKey is intentionally excluded from IncludeKeys because +// environment is instance-level routing metadata, not service-level. +// Consumer code in service_instances_changed_listener_impl.go overrides +// environment from the current instance metadata, so including it in +// the revision would cause unnecessary revision changes between +// instances with different environment tags. + // MetadataInfo the metadata information of instance type MetadataInfo struct { App string `json:"app,omitempty" hessian:"app"` diff --git a/metadata/info/metadata_info_test.go b/metadata/info/metadata_info_test.go index f9a6d9a098..7af7dc73cf 100644 --- a/metadata/info/metadata_info_test.go +++ b/metadata/info/metadata_info_test.go @@ -164,7 +164,7 @@ func TestServiceInfoGetParams(t *testing.T) { assert.Equal(t, []string{"random"}, service.GetParams()["loadbalance"]) } -func TestServiceInfoGetParamsIncludesEnvironment(t *testing.T) { +func TestServiceInfoExcludesInstanceLevelParams(t *testing.T) { serviceURL, err := common.NewURL("tri://127.0.0.1:20000/org.apache.dubbo.samples.proto.GreetService", common.WithInterface("org.apache.dubbo.samples.proto.GreetService"), common.WithParamsValue(constant.EnvironmentKey, "pre"), @@ -174,7 +174,9 @@ func TestServiceInfoGetParamsIncludesEnvironment(t *testing.T) { service := NewServiceInfoWithURL(serviceURL) - assert.Equal(t, []string{"pre"}, service.GetParams()[constant.EnvironmentKey]) + // Environment is instance-level metadata, not service-level. + // It should NOT appear in ServiceInfo.Params and thus not affect revision. + assert.Empty(t, service.GetParams()[constant.EnvironmentKey]) } func TestServiceInfoGetMatchKey(t *testing.T) { From 0f00f009cc1930881061497f57cd18e5dda6a433 Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Fri, 5 Jun 2026 20:43:39 +0800 Subject: [PATCH 06/10] chore:delete useless comments --- metadata/info/metadata_info.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/metadata/info/metadata_info.go b/metadata/info/metadata_info.go index 500e7291c5..35bb664707 100644 --- a/metadata/info/metadata_info.go +++ b/metadata/info/metadata_info.go @@ -57,12 +57,6 @@ var IncludeKeys = gxset.NewSet( constant.WeightKey, constant.ReleaseKey) -// EnvironmentKey is intentionally excluded from IncludeKeys because -// environment is instance-level routing metadata, not service-level. -// Consumer code in service_instances_changed_listener_impl.go overrides -// environment from the current instance metadata, so including it in -// the revision would cause unnecessary revision changes between -// instances with different environment tags. // MetadataInfo the metadata information of instance type MetadataInfo struct { From f16e4be26cfc9c224383751eceb46d0c3af78287 Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Fri, 5 Jun 2026 20:58:22 +0800 Subject: [PATCH 07/10] chore: go fmt --- metadata/info/metadata_info.go | 1 - 1 file changed, 1 deletion(-) diff --git a/metadata/info/metadata_info.go b/metadata/info/metadata_info.go index 35bb664707..1fb9bc7633 100644 --- a/metadata/info/metadata_info.go +++ b/metadata/info/metadata_info.go @@ -57,7 +57,6 @@ var IncludeKeys = gxset.NewSet( constant.WeightKey, constant.ReleaseKey) - // MetadataInfo the metadata information of instance type MetadataInfo struct { App string `json:"app,omitempty" hessian:"app"` From 2c8620762ce996259c9921593ea35c53e7184bc4 Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Sat, 6 Jun 2026 17:19:52 +0800 Subject: [PATCH 08/10] refactor(metadata): use incremental hash and add CalAndGetRevision test --- metadata/info/metadata_info.go | 9 ++++----- metadata/info/metadata_info_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/metadata/info/metadata_info.go b/metadata/info/metadata_info.go index 1fb9bc7633..e482f249d5 100644 --- a/metadata/info/metadata_info.go +++ b/metadata/info/metadata_info.go @@ -372,12 +372,11 @@ func CalRevision(app string, services map[string]*ServiceInfo) string { } sort.Strings(matchKeys) - var b strings.Builder - b.WriteString(app) + h := sha512.New() + h.Write([]byte(app)) for _, mk := range matchKeys { - b.WriteString(services[mk].toDescString()) + h.Write([]byte(services[mk].toDescString())) } - sum := sha512.Sum512([]byte(b.String())) - return fmt.Sprintf("%x", sum) + return fmt.Sprintf("%x", h.Sum(nil)) } diff --git a/metadata/info/metadata_info_test.go b/metadata/info/metadata_info_test.go index 7af7dc73cf..01118abf1f 100644 --- a/metadata/info/metadata_info_test.go +++ b/metadata/info/metadata_info_test.go @@ -193,3 +193,27 @@ func TestServiceInfoGetMatchKey(t *testing.T) { func TestServiceInfoJavaClassName(t *testing.T) { assert.Equalf(t, "org.apache.dubbo.metadata.MetadataInfo", NewAppMetadataInfo("dubbo").JavaClassName(), "JavaClassName()") } + +func TestMetadataInfoCalAndGetRevision(t *testing.T) { + svcURL, err := common.NewURL( + "dubbo://127.0.0.1:20880/com.example.TestService", + common.WithParamsValue(constant.ApplicationKey, "test-app"), + common.WithParamsValue(constant.GroupKey, "groupA"), + common.WithParamsValue(constant.VersionKey, "1.0.0"), + common.WithMethods([]string{"sayHello"}), + ) + require.NoError(t, err) + + info := NewAppMetadataInfo("") + info.AddService(svcURL) + + // 1. Call CalAndGetRevision — should update info.Revision in-place + rev := info.CalAndGetRevision() + + assert.NotEmpty(t, rev) + assert.Equal(t, rev, info.Revision, "CalAndGetRevision should update info.Revision in-place") + + // 2. Result should match CalRevision for the same input + assert.Equal(t, CalRevision(info.App, info.Services), rev, + "CalAndGetRevision should return the same result as CalRevision") +} From 1c998e02f21735172bf87455a182bd373c527451 Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Thu, 11 Jun 2026 14:23:02 +0800 Subject: [PATCH 09/10] refactor(metadata): remove unused CalAndGetRevision method No callers in production code. Go's MetadataInfo.Revision is set externally by the registry/customizer path, unlike Java where calAndGetRevision() manages the revision internally. CalRevision remains as the public API for revision calculation. --- metadata/info/metadata_info.go | 9 --------- metadata/info/metadata_info_test.go | 24 ------------------------ 2 files changed, 33 deletions(-) diff --git a/metadata/info/metadata_info.go b/metadata/info/metadata_info.go index e482f249d5..4caed3bfe2 100644 --- a/metadata/info/metadata_info.go +++ b/metadata/info/metadata_info.go @@ -176,15 +176,6 @@ func (info *MetadataInfo) ReplaceExportedServices(urls []*common.URL) { } } -// CalAndGetRevision calculates and updates the revision for this MetadataInfo. -// The revision is derived from the canonical ServiceInfo representation, -// ensuring strong binding between revision and serialized metadata content. -// Aligned with Java dubbo MetadataInfo.calAndGetRevision(). -func (info *MetadataInfo) CalAndGetRevision() string { - info.Revision = CalRevision(info.App, info.Services) - return info.Revision -} - func (info *MetadataInfo) findExportedServiceURL(matchKey string) *common.URL { for _, urls := range info.exportedServiceURLs { for _, serviceURL := range urls { diff --git a/metadata/info/metadata_info_test.go b/metadata/info/metadata_info_test.go index 01118abf1f..7af7dc73cf 100644 --- a/metadata/info/metadata_info_test.go +++ b/metadata/info/metadata_info_test.go @@ -193,27 +193,3 @@ func TestServiceInfoGetMatchKey(t *testing.T) { func TestServiceInfoJavaClassName(t *testing.T) { assert.Equalf(t, "org.apache.dubbo.metadata.MetadataInfo", NewAppMetadataInfo("dubbo").JavaClassName(), "JavaClassName()") } - -func TestMetadataInfoCalAndGetRevision(t *testing.T) { - svcURL, err := common.NewURL( - "dubbo://127.0.0.1:20880/com.example.TestService", - common.WithParamsValue(constant.ApplicationKey, "test-app"), - common.WithParamsValue(constant.GroupKey, "groupA"), - common.WithParamsValue(constant.VersionKey, "1.0.0"), - common.WithMethods([]string{"sayHello"}), - ) - require.NoError(t, err) - - info := NewAppMetadataInfo("") - info.AddService(svcURL) - - // 1. Call CalAndGetRevision — should update info.Revision in-place - rev := info.CalAndGetRevision() - - assert.NotEmpty(t, rev) - assert.Equal(t, rev, info.Revision, "CalAndGetRevision should update info.Revision in-place") - - // 2. Result should match CalRevision for the same input - assert.Equal(t, CalRevision(info.App, info.Services), rev, - "CalAndGetRevision should return the same result as CalRevision") -} From 6669708a4dc1ae038c523b80e2b555383c350707 Mon Sep 17 00:00:00 2001 From: qyq316 <19606363088@163.com> Date: Thu, 11 Jun 2026 19:32:01 +0800 Subject: [PATCH 10/10] chore: retrigger CI