diff --git a/.gitignore b/.gitignore index 568e9f24541dd6f02dd8670436fd48db481b7f21..f7622f8ac9cc1ae42ea5203df70c5327f09bf300 100644 --- a/.gitignore +++ b/.gitignore @@ -20,15 +20,13 @@ classes # go mod, go test vendor/ -coverage.txt - logs/ .vscode/ -coverage.txt # unit test remoting/zookeeper/zookeeper-4unittest/ config_center/zookeeper/zookeeper-4unittest/ registry/zookeeper/zookeeper-4unittest/ +metadata/report/zookeeper/zookeeper-4unittest/ registry/consul/agent* config_center/apollo/mockDubbog.properties.json diff --git a/CHANGE.md b/CHANGE.md index 00b074d284971d779c84792951262879098fc18b..3e48db1d3bfe3f0222dab7a222519aff70dbc27f 100644 --- a/CHANGE.md +++ b/CHANGE.md @@ -1,6 +1,55 @@ # Release Notes --- +## 1.5.0 + +### New Features +- [Application-Level Registry Model](https://github.com/apache/dubbo-go/pull/604) + - [DelegateMetadataReport & RemoteMetadataService](https://github.com/apache/dubbo-go/pull/505) + - [Nacos MetadataReport implementation](https://github.com/apache/dubbo-go/pull/522) + - [Nacos service discovery](https://github.com/apache/dubbo-go/blob/9a5990d9a9c3d5e6633c0d7d926c156416bcb931/registry/nacos/service_discovery.go) + - [Zk metadata service](https://github.com/apache/dubbo-go/pull/633) + - [Zk service discovery](https://github.com/apache/dubbo-go/blob/9a5990d9a9c3d5e6633c0d7d926c156416bcb931/registry/zookeeper/service_discovery.go) + - [Etcd metadata report](https://github.com/apache/dubbo-go/blob/9a5990d9a9c3d5e6633c0d7d926c156416bcb931/metadata/report/etcd/report.go) + - [Etcd metadata service discovery](https://github.com/apache/dubbo-go/blob/9a5990d9a9c3d5e6633c0d7d926c156416bcb931/registry/etcdv3/service_discovery.go) +- [Support grpc json protocol](https://github.com/apache/dubbo-go/pull/582) + +### Enhancement +- [Optimize err handling ](https://github.com/apache/dubbo-go/pull/536/) +- [Add attribute method into Invocation and RpcInvocation](https://github.com/apache/dubbo-go/pull/537) +- [Optimize lock for zookeeper registry](https://github.com/apache/dubbo-go/pull/578) +- [Improve code coverage of zookeeper config center](https://github.com/apache/dubbo-go/pull/549) +- [Improve code coverage of nacos config center and configuration parser](https://github.com/apache/dubbo-go/pull/587) +- [Kubernetes as registry enhance](https://github.com/apache/dubbo-go/pull/577) +- [Optimize zk client's lock and tests](https://github.com/apache/dubbo-go/pull/601) +- [Add setInvoker method for invocation](https://github.com/apache/dubbo-go/pull/612) +- [Upgrade getty & hessian2](https://github.com/apache/dubbo-go/pull/626) +- [Optimize router design: Extract priority router](https://github.com/apache/dubbo-go/pull/630) +- [NamespaceId config for nacos](https://github.com/apache/dubbo-go/pull/641) + + +### Bugfixes +- [Fix Gitee problem](https://github.com/apache/dubbo-go/pull/590) +- [Gitee quality analyses -- common](https://github.com/apache/dubbo-go/issues/616) +- [Nacos client logDir path seperator for Windows](https://github.com/apache/dubbo-go/pull/591) +- [Fix various linter warnings](https://github.com/apache/dubbo-go/pull/624) +- [Fixed some issues in config folder that reported by sonar-qube](https://github.com/apache/dubbo-go/pull/634) +- [Zk disconnected, dubbo-go panic when subscribe](https://github.com/apache/dubbo-go/pull/613) +- [Enhancement cluster code analysis](https://github.com/apache/dubbo-go/pull/632) + +### Document & Comment +- [Add comment for common directory](https://github.com/apache/dubbo-go/pull/530) +- [Add comments for config_center](https://github.com/apache/dubbo-go/pull/545) +- [Update the comments in metrics](https://github.com/apache/dubbo-go/pull/547) +- [Add comments for config](https://github.com/apache/dubbo-go/pull/579) +- [Updated the dubbo-go-ext image](https://github.com/apache/dubbo-go/pull/581) +- [Add comment for cluster](https://github.com/apache/dubbo-go/pull/584) +- [Update the comments in filter directory](https://github.com/apache/dubbo-go/pull/586) +- [Add comment for metadata](https://github.com/apache/dubbo-go/pull/588) +- [Update the comments in protocol directory](https://github.com/apache/dubbo-go/pull/602) +- [Add comments for remoting](https://github.com/apache/dubbo-go/pull/605) +- [Update the comments in registy directory](https://github.com/apache/dubbo-go/pull/589) + ## 1.4.0 ### New Features diff --git a/before_ut.bat b/before_ut.bat index dc51008dadaad21af6fcb6021863ff4102b0afa2..5d2b9e4682d0ac24efe4ae68be8d977933db103f 100644 --- a/before_ut.bat +++ b/before_ut.bat @@ -34,4 +34,7 @@ md cluster\router\chain\zookeeper-4unittest\contrib\fatjar xcopy /f "%zkJar%" "cluster/router/chain/zookeeper-4unittest/contrib/fatjar/" md cluster\router\condition\zookeeper-4unittest\contrib\fatjar -xcopy /f "%zkJar%" "cluster/router/condition/zookeeper-4unittest/contrib/fatjar/" \ No newline at end of file +xcopy /f "%zkJar%" "cluster/router/condition/zookeeper-4unittest/contrib/fatjar/" + +md metadata\report\zookeeper\zookeeper-4unittest\contrib\fatjar +xcopy /f "%zkJar%" "metadata/report/zookeeper/zookeeper-4unittest/contrib/fatjar/" \ No newline at end of file diff --git a/before_ut.sh b/before_ut.sh index 7ee92e57a26cbdbb1d1a0b3e792726ad5e1954f8..210e9e723ba9e2118cf642729359808b78fddb8d 100755 --- a/before_ut.sh +++ b/before_ut.sh @@ -25,13 +25,16 @@ if [ ! -f "${zkJar}" ]; then fi mkdir -p config_center/zookeeper/zookeeper-4unittest/contrib/fatjar -cp ${zkJar} config_center/zookeeper/zookeeper-4unittest/contrib/fatjar/ +cp ${zkJar} config_center/zookeeper/zookeeper-4unittest/contrib/fatjar mkdir -p registry/zookeeper/zookeeper-4unittest/contrib/fatjar -cp ${zkJar} registry/zookeeper/zookeeper-4unittest/contrib/fatjar/ +cp ${zkJar} registry/zookeeper/zookeeper-4unittest/contrib/fatjar mkdir -p cluster/router/chain/zookeeper-4unittest/contrib/fatjar cp ${zkJar} cluster/router/chain/zookeeper-4unittest/contrib/fatjar mkdir -p cluster/router/condition/zookeeper-4unittest/contrib/fatjar -cp ${zkJar} cluster/router/condition/zookeeper-4unittest/contrib/fatjar \ No newline at end of file +cp ${zkJar} cluster/router/condition/zookeeper-4unittest/contrib/fatjar + +mkdir -p metadata/report/zookeeper/zookeeper-4unittest/contrib/fatjar +cp ${zkJar} metadata/report/zookeeper/zookeeper-4unittest/contrib/fatjar \ No newline at end of file diff --git a/cluster/directory/base_directory.go b/cluster/directory/base_directory.go index 7865b807ad4bedfd045aadffbfdbf2f19186e293..8a7e0c0e8359c69faf0e504adeb1778c38a08e04 100644 --- a/cluster/directory/base_directory.go +++ b/cluster/directory/base_directory.go @@ -22,7 +22,6 @@ import ( ) import ( - "github.com/dubbogo/gost/container/set" "go.uber.org/atomic" ) @@ -35,8 +34,6 @@ import ( "github.com/apache/dubbo-go/common/logger" ) -var routerURLSet = gxset.NewSet() - // BaseDirectory Abstract implementation of Directory: Invoker list returned from this Directory's list method have been filtered by Routers type BaseDirectory struct { url *common.URL @@ -120,14 +117,3 @@ func (dir *BaseDirectory) Destroy(doDestroy func()) { func (dir *BaseDirectory) IsAvailable() bool { return !dir.destroyed.Load() } - -// GetRouterURLSet Return router URL -func GetRouterURLSet() *gxset.HashSet { - return routerURLSet -} - -// AddRouterURLSet Add router URL -// Router URL will init in config/config_loader.go -func AddRouterURLSet(url *common.URL) { - routerURLSet.Add(url) -} diff --git a/common/constant/default.go b/common/constant/default.go index c69989b4fbc3e95cb42c7f5e403989b9cff9215b..c1c404e089ea90899d2b599b01cd5980c3e92ab1 100644 --- a/common/constant/default.go +++ b/common/constant/default.go @@ -76,3 +76,12 @@ const ( const ( COMMA_SPLIT_PATTERN = "\\s*[,]+\\s*" ) + +const ( + SIMPLE_METADATA_SERVICE_NAME = "MetadataService" + DEFAULT_REVIESION = "N/A" +) + +const ( + SERVICE_DISCOVERY_DEFAULT_GROUP = "DEFAULT_GROUP" +) diff --git a/common/constant/key.go b/common/constant/key.go index 8123d66557157a0cd3a267df36ee8a9d56df1aab..cd23dd0f1ad3e000fe54c251d9563bcf12ba86c7 100644 --- a/common/constant/key.go +++ b/common/constant/key.go @@ -22,10 +22,12 @@ const ( ) const ( + PORT_KEY = "port" GROUP_KEY = "group" VERSION_KEY = "version" INTERFACE_KEY = "interface" PATH_KEY = "path" + PROTOCOL_KEY = "protocol" SERVICE_KEY = "service" METHODS_KEY = "methods" TIMEOUT_KEY = "timeout" @@ -40,6 +42,7 @@ const ( TOKEN_KEY = "token" LOCAL_ADDR = "local-addr" REMOTE_ADDR = "remote-addr" + PATH_SEPARATOR = "/" DUBBO_KEY = "dubbo" RELEASE_KEY = "release" ANYHOST_KEY = "anyhost" @@ -77,6 +80,11 @@ const ( EXECUTE_REJECTED_EXECUTION_HANDLER_KEY = "execute.limit.rejected.handler" PROVIDER_SHUTDOWN_FILTER = "pshutdown" CONSUMER_SHUTDOWN_FILTER = "cshutdown" + PID_KEY = "pid" + SYNC_REPORT_KEY = "sync.report" + RETRY_PERIOD_KEY = "retry.period" + RETRY_TIMES_KEY = "retry.times" + CYCLE_REPORT_KEY = "cycle.report" ) const ( @@ -155,6 +163,14 @@ const ( NACOS_USERNAME = "username" ) +const ( + ZOOKEEPER_KEY = "zookeeper" +) + +const ( + ETCDV3_KEY = "etcdv3" +) + const ( TRACING_REMOTE_SPAN_CTX = "tracing.remote.span.ctx" ) @@ -230,7 +246,6 @@ const ( KEY_SEPARATOR = ":" DEFAULT_PATH_TAG = "metadata" KEY_REVISON_PREFIX = "revision" - PATH_SEPARATOR = "/" // metadata service METADATA_SERVICE_NAME = "org.apache.dubbo.metadata.MetadataService" @@ -259,7 +274,19 @@ const ( ) // service discovery - const ( - NACOS_GROUP = "nacos.group" + SUBSCRIBED_SERVICE_NAMES_KEY = "subscribed-services" + PROVIDER_BY = "provided-by" + EXPORTED_SERVICES_REVISION_PROPERTY_NAME = "dubbo.exported-services.revision" + SUBSCRIBED_SERVICES_REVISION_PROPERTY_NAME = "dubbo.subscribed-services.revision" + SERVICE_INSTANCE_SELECTOR = "service-instance-selector" + METADATA_STORAGE_TYPE_PROPERTY_NAME = "dubbo.metadata.storage-type" + DEFAULT_METADATA_STORAGE_TYPE = "local" + SERVICE_INSTANCE_ENDPOINTS = "dubbo.endpoints" + METADATA_SERVICE_PREFIX = "dubbo.metadata-service." + METADATA_SERVICE_URL_PARAMS_PROPERTY_NAME = METADATA_SERVICE_PREFIX + "url-params" + METADATA_SERVICE_URLS_PROPERTY_NAME = METADATA_SERVICE_PREFIX + "urls" + + // SERVICE_DISCOVERY_KEY indicate which service discovery instance will be used + SERVICE_DISCOVERY_KEY = "service_discovery" ) diff --git a/common/extension/event_dispatcher.go b/common/extension/event_dispatcher.go new file mode 100644 index 0000000000000000000000000000000000000000..f0503e05422844e129a81212beed6af414612b6b --- /dev/null +++ b/common/extension/event_dispatcher.go @@ -0,0 +1,77 @@ +/* + * 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 extension + +import ( + "sync" +) + +import ( + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/common/observer" +) + +var ( + globalEventDispatcher observer.EventDispatcher + initEventListeners []func() observer.EventListener + initEventOnce sync.Once +) + +var ( + dispatchers = make(map[string]func() observer.EventDispatcher, 8) +) + +// SetEventDispatcher, actually, it doesn't really init the global dispatcher +func SetEventDispatcher(name string, v func() observer.EventDispatcher) { + dispatchers[name] = v +} + +// SetAndInitGlobalDispatcher will actually init the global dispatcher +// if there is already a global dispatcher, +// it will be override +// if the dispatcher with the name not found, it will panic +func SetAndInitGlobalDispatcher(name string) { + if len(name) == 0 { + name = "direct" + } + if globalEventDispatcher != nil { + logger.Warnf("EventDispatcher has been initialized. It will be replaced") + } + + if dp, ok := dispatchers[name]; !ok || dp == nil { + panic("EventDispatcher for " + name + " is not found, make sure you have import the package, " + + "like import _ github.com/apache/dubbo-go/common/observer/dispatcher ") + } + globalEventDispatcher = dispatchers[name]() +} + +// GetGlobalDispatcher will init all listener and then return dispatcher +func GetGlobalDispatcher() observer.EventDispatcher { + initEventOnce.Do(func() { + // we should delay to add the listeners to avoid some listeners left + for _, l := range initEventListeners { + globalEventDispatcher.AddEventListener(l()) + } + }) + return globalEventDispatcher +} + +// AddEventListener it will be added in global event dispatcher +func AddEventListener(creator func() observer.EventListener) { + initEventListeners = append(initEventListeners, creator) +} diff --git a/common/extension/event_dispatcher_test.go b/common/extension/event_dispatcher_test.go new file mode 100644 index 0000000000000000000000000000000000000000..472360cea5a04c2cd70f0df6ea4db23f6be88f1a --- /dev/null +++ b/common/extension/event_dispatcher_test.go @@ -0,0 +1,111 @@ +/* + * 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 extension + +import ( + "reflect" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) +import ( + "github.com/apache/dubbo-go/common/observer" +) + +func TestSetAndInitGlobalDispatcher(t *testing.T) { + mock := &mockEventDispatcher{} + SetEventDispatcher("mock", func() observer.EventDispatcher { + return mock + }) + + SetAndInitGlobalDispatcher("mock") + dispatcher := GetGlobalDispatcher() + assert.NotNil(t, dispatcher) + assert.Equal(t, mock, dispatcher) + + mock1 := &mockEventDispatcher{} + + SetEventDispatcher("mock1", func() observer.EventDispatcher { + return mock1 + }) + + SetAndInitGlobalDispatcher("mock1") + dispatcher = GetGlobalDispatcher() + assert.NotNil(t, dispatcher) + assert.Equal(t, mock1, dispatcher) +} + +func TestAddEventListener(t *testing.T) { + AddEventListener(func() observer.EventListener { + return &mockEventListener{} + }) + + AddEventListener(func() observer.EventListener { + return &mockEventListener{} + }) + + assert.Equal(t, 2, len(initEventListeners)) +} + +type mockEventListener struct { +} + +func (m mockEventListener) GetPriority() int { + panic("implement me") +} + +func (m mockEventListener) OnEvent(e observer.Event) error { + panic("implement me") +} + +func (m mockEventListener) GetEventType() reflect.Type { + panic("implement me") +} + +type mockEventDispatcher struct { +} + +func (m mockEventDispatcher) AddEventListener(listener observer.EventListener) { + panic("implement me") +} + +func (m mockEventDispatcher) AddEventListeners(listenersSlice []observer.EventListener) { + panic("implement me") +} + +func (m mockEventDispatcher) RemoveEventListener(listener observer.EventListener) { + panic("implement me") +} + +func (m mockEventDispatcher) RemoveEventListeners(listenersSlice []observer.EventListener) { + panic("implement me") +} + +func (m mockEventDispatcher) GetAllEventListeners() []observer.EventListener { + panic("implement me") +} + +func (m mockEventDispatcher) RemoveAllEventListeners() { + panic("implement me") +} + +func (m mockEventDispatcher) Dispatch(event observer.Event) { + panic("implement me") +} diff --git a/common/extension/metadata_report_factory.go b/common/extension/metadata_report_factory.go index c55f8617fadd9d09d68547b05341d127716ce73c..593318d01beec1f89d8194c6d4bd18a15c798a0e 100644 --- a/common/extension/metadata_report_factory.go +++ b/common/extension/metadata_report_factory.go @@ -18,20 +18,20 @@ package extension import ( - "github.com/apache/dubbo-go/metadata" + "github.com/apache/dubbo-go/metadata/report/factory" ) var ( - metaDataReportFactories = make(map[string]func() metadata.MetadataReportFactory, 8) + metaDataReportFactories = make(map[string]func() factory.MetadataReportFactory, 8) ) // SetMetadataReportFactory sets the MetadataReportFactory with @name -func SetMetadataReportFactory(name string, v func() metadata.MetadataReportFactory) { +func SetMetadataReportFactory(name string, v func() factory.MetadataReportFactory) { metaDataReportFactories[name] = v } // GetMetadataReportFactory finds the MetadataReportFactory with @name -func GetMetadataReportFactory(name string) metadata.MetadataReportFactory { +func GetMetadataReportFactory(name string) factory.MetadataReportFactory { if metaDataReportFactories[name] == nil { panic("metadata report for " + name + " is not existing, make sure you have import the package.") } diff --git a/common/extension/metadata_service.go b/common/extension/metadata_service.go new file mode 100644 index 0000000000000000000000000000000000000000..1823273b8f7f86b2d96abf990359e14b569abddb --- /dev/null +++ b/common/extension/metadata_service.go @@ -0,0 +1,47 @@ +/* + * 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 extension + +import ( + "fmt" +) + +import ( + "github.com/apache/dubbo-go/metadata/service" +) + +var ( + // there will be two types: local or remote + metadataServiceInsMap = make(map[string]func() (service.MetadataService, error), 2) +) + +// SetMetadataService will store the msType => creator pair +func SetMetadataService(msType string, creator func() (service.MetadataService, error)) { + metadataServiceInsMap[msType] = creator +} + +// GetMetadataService will create a MetadataService instance +// it will panic if msType not found +func GetMetadataService(msType string) (service.MetadataService, error) { + if creator, ok := metadataServiceInsMap[msType]; ok { + return creator() + } + panic(fmt.Sprintf("could not find the metadata service creator for metadataType: %s, please check whether you have imported relative packages, \n"+ + "local - github.com/apache/dubbo-go/metadata/service/inmemory, \n"+ + "remote - github.com/apache/dubbo-go/metadata/service/remote", msType)) +} diff --git a/common/extension/metadata_service_proxy_factory.go b/common/extension/metadata_service_proxy_factory.go new file mode 100644 index 0000000000000000000000000000000000000000..2b88d37c9a5145ddca81930cd76ccac1a7184318 --- /dev/null +++ b/common/extension/metadata_service_proxy_factory.go @@ -0,0 +1,48 @@ +/* + * 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 extension + +import ( + "fmt" +) + +import ( + "github.com/apache/dubbo-go/metadata/service" +) + +var ( + metadataServiceProxyFactoryMap = make(map[string]func() service.MetadataServiceProxyFactory, 2) +) + +type MetadataServiceProxyFactoryFunc func() service.MetadataServiceProxyFactory + +// SetMetadataServiceProxyFactory store the name-creator pair +func SetMetadataServiceProxyFactory(name string, creator MetadataServiceProxyFactoryFunc) { + metadataServiceProxyFactoryMap[name] = creator +} + +// GetMetadataServiceProxyFactory will create an instance. +// it will panic if the factory with name not found +func GetMetadataServiceProxyFactory(name string) service.MetadataServiceProxyFactory { + if f, ok := metadataServiceProxyFactoryMap[name]; ok { + return f() + } + panic(fmt.Sprintf("could not find the metadata service factory creator for name: %s, please check whether you have imported relative packages, \n"+ + "local - github.com/apache/dubbo-go/metadata/service/inmemory, \n"+ + "remote - github.com/apache/dubbo-go/metadata/service/remote", name)) +} diff --git a/common/extension/service_discovery.go b/common/extension/service_discovery.go index 1d891c2189e55fbd6fa03e4c6e5ea6ec5b654e46..0227920dc64a7b5f6ad1939fcccbb7384c43f68d 100644 --- a/common/extension/service_discovery.go +++ b/common/extension/service_discovery.go @@ -21,25 +21,28 @@ import ( perrors "github.com/pkg/errors" ) import ( - "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/registry" ) var ( - discoveryCreatorMap = make(map[string]func(url *common.URL) (registry.ServiceDiscovery, error), 4) + discoveryCreatorMap = make(map[string]func(name string) (registry.ServiceDiscovery, error), 4) ) // SetServiceDiscovery will store the @creator and @name -func SetServiceDiscovery(name string, creator func(_ *common.URL) (registry.ServiceDiscovery, error)) { - discoveryCreatorMap[name] = creator +// protocol indicate the implementation, like nacos +// the name like nacos-1... +func SetServiceDiscovery(protocol string, creator func(name string) (registry.ServiceDiscovery, error)) { + discoveryCreatorMap[protocol] = creator } // GetServiceDiscovery will return the registry.ServiceDiscovery +// protocol indicate the implementation, like nacos +// the name like nacos-1... // if not found, or initialize instance failed, it will return error. -func GetServiceDiscovery(name string, url *common.URL) (registry.ServiceDiscovery, error) { - creator, ok := discoveryCreatorMap[name] +func GetServiceDiscovery(protocol string, name string) (registry.ServiceDiscovery, error) { + creator, ok := discoveryCreatorMap[protocol] if !ok { - return nil, perrors.New("Could not find the service discovery with name: " + name) + return nil, perrors.New("Could not find the service discovery with discovery protocol: " + protocol) } - return creator(url) + return creator(name) } diff --git a/common/extension/service_instance_customizer.go b/common/extension/service_instance_customizer.go new file mode 100644 index 0000000000000000000000000000000000000000..3ebb3e40f5851ad4799e7cee5966edd77cea2f9e --- /dev/null +++ b/common/extension/service_instance_customizer.go @@ -0,0 +1,56 @@ +/* + * 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 extension + +import ( + "sort" +) + +import ( + "github.com/apache/dubbo-go/registry" +) + +var ( + customizers = make([]registry.ServiceInstanceCustomizer, 0, 8) +) + +// AddCustomizers will put the customizer into slices and then sort them; +// this method will be invoked several time, so we sort them here. +func AddCustomizers(cus registry.ServiceInstanceCustomizer) { + customizers = append(customizers, cus) + sort.Stable(customizerSlice(customizers)) +} + +// GetCustomizers will return the sorted customizer +// the result won't be nil +func GetCustomizers() []registry.ServiceInstanceCustomizer { + return customizers +} + +type customizerSlice []registry.ServiceInstanceCustomizer + +// nolint +func (c customizerSlice) Len() int { + return len(c) +} + +// nolint +func (c customizerSlice) Swap(i, j int) { c[i], c[j] = c[j], c[i] } + +// nolint +func (c customizerSlice) Less(i, j int) bool { return c[i].GetPriority() < c[j].GetPriority() } diff --git a/common/extension/service_instance_selector_factory.go b/common/extension/service_instance_selector_factory.go new file mode 100644 index 0000000000000000000000000000000000000000..66d3e7646e6c26639901fe0a9c8994233aa7c567 --- /dev/null +++ b/common/extension/service_instance_selector_factory.go @@ -0,0 +1,46 @@ +/* + * 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 extension + +import ( + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/registry/servicediscovery/instance" +) + +var ( + serviceInstanceSelectorMappings = make(map[string]func() instance.ServiceInstanceSelector, 2) +) + +// nolint +func SetServiceInstanceSelector(name string, f func() instance.ServiceInstanceSelector) { + serviceInstanceSelectorMappings[name] = f +} + +// GetServiceInstanceSelector will create an instance +// it will panic if selector with the @name not found +func GetServiceInstanceSelector(name string) (instance.ServiceInstanceSelector, error) { + serviceInstanceSelector, ok := serviceInstanceSelectorMappings[name] + if !ok { + return nil, perrors.New("Could not find service instance selector with" + + "name:" + name) + } + return serviceInstanceSelector(), nil +} diff --git a/common/extension/service_name_mapping.go b/common/extension/service_name_mapping.go new file mode 100644 index 0000000000000000000000000000000000000000..99fd4c25e93d6f3d085a8bcab50482535e8c9019 --- /dev/null +++ b/common/extension/service_name_mapping.go @@ -0,0 +1,36 @@ +/* + * 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 extension + +import ( + "github.com/apache/dubbo-go/metadata/mapping" +) + +type ServiceNameMappingCreator func() mapping.ServiceNameMapping + +var ( + globalNameMappingCreator ServiceNameMappingCreator +) + +func SetGlobalServiceNameMapping(nameMappingCreator ServiceNameMappingCreator) { + globalNameMappingCreator = nameMappingCreator +} + +func GetGlobalServiceNameMapping() mapping.ServiceNameMapping { + return globalNameMappingCreator() +} diff --git a/common/observer/dispatcher/direct_event_dispatcher.go b/common/observer/dispatcher/direct_event_dispatcher.go new file mode 100644 index 0000000000000000000000000000000000000000..a2d334ce2ca9461a8e14e28bb0f3ecf21971751f --- /dev/null +++ b/common/observer/dispatcher/direct_event_dispatcher.go @@ -0,0 +1,69 @@ +/* + * 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 dispatcher + +import ( + "reflect" +) + +import ( + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/common/observer" +) + +func init() { + extension.SetEventDispatcher("direct", NewDirectEventDispatcher) +} + +// DirectEventDispatcher is align with DirectEventDispatcher interface in Java. +// it's the top abstraction +// Align with 2.7.5 +// Dispatcher event to listener direct +type DirectEventDispatcher struct { + observer.BaseListener +} + +// NewDirectEventDispatcher ac constructor of DirectEventDispatcher +func NewDirectEventDispatcher() observer.EventDispatcher { + return &DirectEventDispatcher{ + BaseListener: observer.NewBaseListener(), + } +} + +// Dispatch event directly +// it lookup the listener by event's type. +// if listener not found, it just return and do nothing +func (ded *DirectEventDispatcher) Dispatch(event observer.Event) { + if event == nil { + logger.Warnf("[DirectEventDispatcher] dispatch event nil") + return + } + eventType := reflect.TypeOf(event).Elem() + ded.Mutex.RLock() + defer ded.Mutex.RUnlock() + listenersSlice, loaded := ded.ListenersCache[eventType] + if !loaded { + return + } + for _, listener := range listenersSlice { + if err := listener.OnEvent(event); err != nil { + logger.Warnf("[DirectEventDispatcher] dispatch event error:%v", err) + } + } +} diff --git a/common/observer/dispatcher/direct_event_dispatcher_test.go b/common/observer/dispatcher/direct_event_dispatcher_test.go new file mode 100644 index 0000000000000000000000000000000000000000..12facbb9c4bd1b33fa071a50beae87b77e1be968 --- /dev/null +++ b/common/observer/dispatcher/direct_event_dispatcher_test.go @@ -0,0 +1,77 @@ +/* + * 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 dispatcher + +import ( + "fmt" + "reflect" + "testing" +) + +import ( + "github.com/apache/dubbo-go/common/observer" +) + +func TestDirectEventDispatcher_Dispatch(t *testing.T) { + ded := NewDirectEventDispatcher() + ded.AddEventListener(&TestEventListener{ + BaseListener: observer.NewBaseListener(), + }) + ded.AddEventListener(&TestEventListener1{}) + ded.Dispatch(&TestEvent{}) + ded.Dispatch(nil) +} + +type TestEvent struct { + observer.BaseEvent +} + +type TestEventListener struct { + observer.BaseListener + observer.EventListener +} + +func (tel *TestEventListener) OnEvent(e observer.Event) error { + fmt.Println("TestEventListener") + return nil +} + +func (tel *TestEventListener) GetPriority() int { + return -1 +} + +func (tel *TestEventListener) GetEventType() reflect.Type { + return reflect.TypeOf(&TestEvent{}) +} + +type TestEventListener1 struct { + observer.EventListener +} + +func (tel *TestEventListener1) OnEvent(e observer.Event) error { + fmt.Println("TestEventListener1") + return nil +} + +func (tel *TestEventListener1) GetPriority() int { + return 1 +} + +func (tel *TestEventListener1) GetEventType() reflect.Type { + return reflect.TypeOf(TestEvent{}) +} diff --git a/common/observer/dispatcher/mock_event_dispatcher.go b/common/observer/dispatcher/mock_event_dispatcher.go new file mode 100644 index 0000000000000000000000000000000000000000..45cdaa71a257ff78293e1a1bdc15232c36a9aa26 --- /dev/null +++ b/common/observer/dispatcher/mock_event_dispatcher.go @@ -0,0 +1,58 @@ +/* + * 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 dispatcher + +import ( + "github.com/apache/dubbo-go/common/observer" +) + +// MockEventDispatcher will do nothing. +// It is only used by tests +// Now the implementation doing nothing, +// But you can modify this if needed +type MockEventDispatcher struct { +} + +// AddEventListener do nothing +func (m MockEventDispatcher) AddEventListener(listener observer.EventListener) { +} + +// AddEventListeners do nothing +func (m MockEventDispatcher) AddEventListeners(listenersSlice []observer.EventListener) { +} + +// RemoveEventListener do nothing +func (m MockEventDispatcher) RemoveEventListener(listener observer.EventListener) { +} + +// RemoveEventListeners do nothing +func (m MockEventDispatcher) RemoveEventListeners(listenersSlice []observer.EventListener) { +} + +// GetAllEventListeners return empty list +func (m MockEventDispatcher) GetAllEventListeners() []observer.EventListener { + return make([]observer.EventListener, 0) +} + +// RemoveAllEventListeners do nothing +func (m MockEventDispatcher) RemoveAllEventListeners() { +} + +// Dispatch do nothing +func (m MockEventDispatcher) Dispatch(event observer.Event) { +} diff --git a/common/observer/event.go b/common/observer/event.go new file mode 100644 index 0000000000000000000000000000000000000000..209a50c78a36c13d789f6f9fbc81a73a5d9a535f --- /dev/null +++ b/common/observer/event.go @@ -0,0 +1,68 @@ +/* + * 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 observer + +import ( + "fmt" + "math/rand" + "time" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// Event is align with Event interface in Java. +// it's the top abstraction +// Align with 2.7.5 +type Event interface { + fmt.Stringer + GetSource() interface{} + GetTimestamp() time.Time +} + +// BaseEvent is the base implementation of Event +// You should never use it directly +type BaseEvent struct { + Source interface{} + Timestamp time.Time +} + +// GetSource return the source +func (b *BaseEvent) GetSource() interface{} { + return b.Source +} + +// GetTimestamp return the Timestamp when the event is created +func (b *BaseEvent) GetTimestamp() time.Time { + return b.Timestamp +} + +// String return a human readable string representing this event +func (b *BaseEvent) String() string { + return fmt.Sprintf("BaseEvent[source = %#v]", b.Source) +} + +// NewBaseEvent create an BaseEvent instance +// and the Timestamp will be current timestamp +func NewBaseEvent(source interface{}) *BaseEvent { + return &BaseEvent{ + Source: source, + Timestamp: time.Now(), + } +} diff --git a/common/observer/event_dispatcher.go b/common/observer/event_dispatcher.go new file mode 100644 index 0000000000000000000000000000000000000000..17745e68c07065f954d456914fb276fadf6d975d --- /dev/null +++ b/common/observer/event_dispatcher.go @@ -0,0 +1,27 @@ +/* + * 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 observer + +// EventDispatcher is align with EventDispatcher interface in Java. +// it's the top abstraction +// Align with 2.7.5 +type EventDispatcher interface { + Listenable + // Dispatch event + Dispatch(event Event) +} diff --git a/common/observer/event_listener.go b/common/observer/event_listener.go new file mode 100644 index 0000000000000000000000000000000000000000..faa8705a4292b1441b9605852017256a9305f624 --- /dev/null +++ b/common/observer/event_listener.go @@ -0,0 +1,50 @@ +/* + * 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 observer + +import ( + "reflect" +) + +import ( + gxsort "github.com/dubbogo/gost/sort" +) + +// EventListener is an new interface used to align with dubbo 2.7.5 +// It contains the Prioritized means that the listener has its priority +// Usually the priority of your custom implementation should be between [100, 9000] +// the number outside the range will be though as system reserve number +// usually implementation should be singleton +type EventListener interface { + gxsort.Prioritizer + // OnEvent handle this event + OnEvent(e Event) error + // GetEventType listen which event type + GetEventType() reflect.Type +} + +// ConditionalEventListener only handle the event which it can handle +type ConditionalEventListener interface { + EventListener + // Accept will make the decision whether it should handle this event + Accept(e Event) bool +} + +type ChangedNotify interface { + Notify(e Event) +} diff --git a/common/observer/listenable.go b/common/observer/listenable.go new file mode 100644 index 0000000000000000000000000000000000000000..bf6ae7219bfd3bd981cf5cd2c87a57c57d7df5d8 --- /dev/null +++ b/common/observer/listenable.go @@ -0,0 +1,142 @@ +/* + * 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 observer + +import ( + "reflect" + "sort" + "sync" +) + +// Listenable could add and remove the event listener +type Listenable interface { + AddEventListener(listener EventListener) + AddEventListeners(listenersSlice []EventListener) + RemoveEventListener(listener EventListener) + RemoveEventListeners(listenersSlice []EventListener) + GetAllEventListeners() []EventListener + RemoveAllEventListeners() +} + +// BaseListener base listenable +type BaseListener struct { + Listenable + ListenersCache map[reflect.Type][]EventListener + Mutex sync.RWMutex +} + +// NewBaseListener a constructor of base listenable +func NewBaseListener() BaseListener { + return BaseListener{ + ListenersCache: make(map[reflect.Type][]EventListener, 8), + } +} + +// AddEventListener add event listener +func (bl *BaseListener) AddEventListener(listener EventListener) { + eventType := listener.GetEventType() + if eventType.Kind() == reflect.Ptr { + eventType = eventType.Elem() + } + bl.Mutex.Lock() + defer bl.Mutex.Unlock() + listenersSlice, loaded := bl.ListenersCache[eventType] + if !loaded { + listenersSlice = make([]EventListener, 0, 8) + } + // return if listenersSlice already has this listener + if loaded && containListener(listenersSlice, listener) { + return + } + listenersSlice = append(listenersSlice, listener) + sort.Slice(listenersSlice, func(i, j int) bool { + return listenersSlice[i].GetPriority() < listenersSlice[j].GetPriority() + }) + bl.ListenersCache[eventType] = listenersSlice +} + +// AddEventListeners add the slice of event listener +func (bl *BaseListener) AddEventListeners(listenersSlice []EventListener) { + for _, listener := range listenersSlice { + bl.AddEventListener(listener) + } +} + +// RemoveEventListener remove the event listener +func (bl *BaseListener) RemoveEventListener(listener EventListener) { + eventType := listener.GetEventType() + if eventType.Kind() == reflect.Ptr { + eventType = eventType.Elem() + } + bl.Mutex.Lock() + defer bl.Mutex.Unlock() + listenersSlice, loaded := bl.ListenersCache[eventType] + if !loaded { + return + } + for i, l := range listenersSlice { + if l == listener { + listenersSlice = append(listenersSlice[:i], listenersSlice[i+1:]...) + } + } + bl.ListenersCache[eventType] = listenersSlice +} + +// RemoveEventListeners remove the slice of event listener +// it will iterate all listener and remove it one by one +func (bl *BaseListener) RemoveEventListeners(listenersSlice []EventListener) { + for _, listener := range listenersSlice { + bl.RemoveEventListener(listener) + } +} + +// RemoveAllEventListeners remove all +// using Lock +func (bl *BaseListener) RemoveAllEventListeners() { + bl.Mutex.Lock() + defer bl.Mutex.Unlock() + bl.ListenersCache = make(map[reflect.Type][]EventListener, 8) +} + +// GetAllEventListeners get all listener +// using RLock +func (bl *BaseListener) GetAllEventListeners() []EventListener { + allListenersSlice := make([]EventListener, 0, 16) + + bl.Mutex.RLock() + defer bl.Mutex.RUnlock() + for _, listenersSlice := range bl.ListenersCache { + allListenersSlice = append(allListenersSlice, listenersSlice...) + } + sort.Slice(allListenersSlice, func(i, j int) bool { + return allListenersSlice[i].GetPriority() < allListenersSlice[j].GetPriority() + }) + return allListenersSlice +} + +// containListener true if contain listener +// it's not thread safe +// usually it should be use in lock scope +func containListener(listenersSlice []EventListener, listener EventListener) bool { + for _, loadListener := range listenersSlice { + if loadListener == listener { + return true + } + } + return false +} diff --git a/common/observer/listenable_test.go b/common/observer/listenable_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5a03382a937fe925b6e17b495d066b86c8d2161d --- /dev/null +++ b/common/observer/listenable_test.go @@ -0,0 +1,65 @@ +/* + * 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 observer + +import ( + "reflect" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +func TestListenable(t *testing.T) { + el := &TestEventListener{} + bl := NewBaseListener() + b := &bl + b.AddEventListener(el) + b.AddEventListener(el) + al := b.GetAllEventListeners() + assert.Equal(t, len(al), 1) + assert.Equal(t, al[0].GetEventType(), reflect.TypeOf(TestEvent{})) + b.RemoveEventListener(el) + assert.Equal(t, len(b.GetAllEventListeners()), 0) + var ts []EventListener + ts = append(ts, el) + b.AddEventListeners(ts) + assert.Equal(t, len(al), 1) + +} + +type TestEvent struct { + BaseEvent +} + +type TestEventListener struct { + EventListener +} + +func (tel *TestEventListener) OnEvent(e Event) error { + return nil +} + +func (tel *TestEventListener) GetPriority() int { + return -1 +} + +func (tel *TestEventListener) GetEventType() reflect.Type { + return reflect.TypeOf(TestEvent{}) +} diff --git a/common/rpc_service.go b/common/rpc_service.go index f8b8e0761e86a3038d90e4cf457ac49df66bc92a..9ef2b956aa955f4fc79c6f75bd060ccfee2d02ca 100644 --- a/common/rpc_service.go +++ b/common/rpc_service.go @@ -131,6 +131,11 @@ func (s *Service) Method() map[string]*MethodType { return s.methods } +// Name will return service name +func (s *Service) Name() string { + return s.name +} + // RcvrType gets @s.rcvrType. func (s *Service) RcvrType() reflect.Type { return s.rcvrType @@ -337,6 +342,16 @@ func suiteMethod(method reflect.Method) *MethodType { argsType []reflect.Type ) + // this method is in RPCService + // we force users must implement RPCService interface in their provider + // and RPCService has only one method "Reference" + // In general, this method should not be exported to client + // so we ignore this method + // see RPCService + if mname == "Reference" { + return nil + } + if outNum != 1 && outNum != 2 { logger.Warnf("method %s of mtype %v has wrong number of in out parameters %d; needs exactly 1/2", mname, mtype.String(), outNum) diff --git a/common/url.go b/common/url.go index d5b9792f537fd92e60beff72e6c2fa294e64b483..807d0ed5eff4ecb70d3adeb8524b841d0ec92a58 100644 --- a/common/url.go +++ b/common/url.go @@ -36,6 +36,7 @@ import ( import ( "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/logger" ) // /////////////////////////////// @@ -178,10 +179,12 @@ func WithToken(token string) option { if len(token) > 0 { value := token if strings.ToLower(token) == "true" || strings.ToLower(token) == "default" { - UUID, err := uuid.NewV4() - if err == nil { - value = UUID.String() + u, err := uuid.NewV4() + if err != nil { + logger.Errorf("could not generator UUID: %v", err) + return } + value = u.String() } url.SetParam(constant.TOKEN_KEY, value) } @@ -654,3 +657,22 @@ func mergeNormalParam(mergedUrl *URL, referenceUrl *URL, paramKeys []string) []f } return methodConfigMergeFcn } + +// URLSlice will be used to sort URL instance +// Instances will be order by URL.String() +type URLSlice []URL + +// nolint +func (s URLSlice) Len() int { + return len(s) +} + +// nolint +func (s URLSlice) Less(i, j int) bool { + return s[i].String() < s[j].String() +} + +// nolint +func (s URLSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/config/application_config.go b/config/application_config.go index 16e841792c7bffaa6f847d73c336c6a43431bc49..ef99664fa298c28365ed7acc54d0c18a88c9b5c2 100644 --- a/config/application_config.go +++ b/config/application_config.go @@ -33,7 +33,8 @@ type ApplicationConfig struct { Version string `yaml:"version" json:"version,omitempty" property:"version"` Owner string `yaml:"owner" json:"owner,omitempty" property:"owner"` Environment string `yaml:"environment" json:"environment,omitempty" property:"environment"` - MetadataType string `default:"local" yaml:"metadataType" json:"metadataType,omitempty" property:"metadataType"` //field for metadata report + // the metadata type. remote or local + MetadataType string `default:"local" yaml:"metadataType" json:"metadataType,omitempty" property:"metadataType"` } // nolint diff --git a/config/base_config.go b/config/base_config.go index fd749a934b8344a5ed00343f1b18f10bc41fd820..0ba5bc7ef98cb30a13890b93a659c467adcbf73b 100644 --- a/config/base_config.go +++ b/config/base_config.go @@ -43,13 +43,33 @@ type multiConfiger interface { // BaseConfig is the common configuration for provider and consumer type BaseConfig struct { ConfigCenterConfig *ConfigCenterConfig `yaml:"config_center" json:"config_center,omitempty"` - configCenterUrl *common.URL - prefix string - fatherConfig interface{} - MetricConfig *MetricConfig `yaml:"metrics" json:"metrics,omitempty"` + // since 1.5.0 version + Remotes map[string]*RemoteConfig `yaml:"remote" json:"remote,omitempty"` + ServiceDiscoveries map[string]*ServiceDiscoveryConfig `yaml:"service_discovery" json:"service_discovery,omitempty"` + MetadataReportConfig *MetadataReportConfig `yaml:"metadata_report" json:"metadata_report,omitempty" property:"metadata_report"` - fileStream *bytes.Buffer + // application config + ApplicationConfig *ApplicationConfig `yaml:"application" json:"application,omitempty" property:"application"` + + configCenterUrl *common.URL + prefix string + fatherConfig interface{} + EventDispatcherType string `default:"direct" yaml:"event_dispatcher_type" json:"event_dispatcher_type,omitempty"` + MetricConfig *MetricConfig `yaml:"metrics" json:"metrics,omitempty"` + fileStream *bytes.Buffer +} + +// nolint +func (c *BaseConfig) GetServiceDiscoveries(name string) (config *ServiceDiscoveryConfig, ok bool) { + config, ok = c.ServiceDiscoveries[name] + return +} + +// GetRemoteConfig will return the remote's config with the name if found +func (c *BaseConfig) GetRemoteConfig(name string) (config *RemoteConfig, ok bool) { + config, ok = c.Remotes[name] + return } // startConfigCenter will start the config center. @@ -64,7 +84,7 @@ func (c *BaseConfig) startConfigCenter() error { if c.prepareEnvironment() != nil { return perrors.WithMessagef(err, "start config center error!") } - //c.fresh() + // c.fresh() return err } @@ -101,14 +121,14 @@ func (c *BaseConfig) prepareEnvironment() error { return perrors.WithStack(err) } } - //global config file + // global config file mapContent, err := dynamicConfig.Parser().Parse(content) if err != nil { return perrors.WithStack(err) } config.GetEnvInstance().UpdateExternalConfigMap(mapContent) - //appGroup config file + // appGroup config file if len(appContent) != 0 { appMapConent, err := dynamicConfig.Parser().Parse(appContent) if err != nil { @@ -263,7 +283,7 @@ func setFieldValue(val reflect.Value, id reflect.Value, config *config.InmemoryC if f.Kind() == reflect.Map { if f.Type().Elem().Kind() == reflect.Ptr { - //initiate config + // initiate config s := reflect.New(f.Type().Elem().Elem()) prefix := s.MethodByName("Prefix").Call(nil)[0].String() for _, pfx := range strings.Split(prefix, "|") { @@ -278,7 +298,7 @@ func setFieldValue(val reflect.Value, id reflect.Value, config *config.InmemoryC } - //iter := f.MapRange() + // iter := f.MapRange() for _, k := range f.MapKeys() { v := f.MapIndex(k) @@ -313,7 +333,7 @@ func (c *BaseConfig) fresh() { } func (c *BaseConfig) freshInternalConfig(config *config.InmemoryConfiguration) { - //reflect to init struct + // reflect to init struct tp := reflect.ValueOf(c.fatherConfig).Elem().Type() initializeStruct(tp, reflect.ValueOf(c.fatherConfig).Elem()) diff --git a/config/base_config_test.go b/config/base_config_test.go index ca2875fb95e9bdabcc430edc15bd4a56c440bc45..15b468753ddfd99e77b3d99c0994c0599c649793 100644 --- a/config/base_config_test.go +++ b/config/base_config_test.go @@ -116,10 +116,12 @@ func TestRefresh(t *testing.T) { config.GetEnvInstance().UpdateExternalConfigMap(mockMap) father := &ConsumerConfig{ - Check: &[]bool{true}[0], - ApplicationConfig: baseAppConfig, - Registries: baseRegistries, - References: baseMockRef, + Check: &[]bool{true}[0], + BaseConfig: BaseConfig{ + ApplicationConfig: baseAppConfig, + }, + Registries: baseRegistries, + References: baseMockRef, ShutdownConfig: &ShutdownConfig{ Timeout: "12s", StepTimeout: "2s", @@ -148,10 +150,12 @@ func TestAppExternalRefresh(t *testing.T) { mockMap["dubbo.consumer.check"] = "true" config.GetEnvInstance().UpdateExternalConfigMap(mockMap) father := &ConsumerConfig{ - Check: &[]bool{true}[0], - ApplicationConfig: baseAppConfig, - Registries: baseRegistries, - References: baseMockRef, + Check: &[]bool{true}[0], + BaseConfig: BaseConfig{ + ApplicationConfig: baseAppConfig, + }, + Registries: baseRegistries, + References: baseMockRef, } c.SetFatherConfig(father) @@ -174,10 +178,12 @@ func TestAppExternalWithoutIDRefresh(t *testing.T) { mockMap["dubbo.consumer.check"] = "true" config.GetEnvInstance().UpdateExternalConfigMap(mockMap) father := &ConsumerConfig{ - Check: &[]bool{true}[0], - ApplicationConfig: baseAppConfig, - Registries: baseRegistries, - References: baseMockRef, + Check: &[]bool{true}[0], + BaseConfig: BaseConfig{ + ApplicationConfig: baseAppConfig, + }, + Registries: baseRegistries, + References: baseMockRef, } c.SetFatherConfig(father) @@ -202,11 +208,13 @@ func TestRefreshSingleRegistry(t *testing.T) { config.GetEnvInstance().UpdateExternalConfigMap(mockMap) father := &ConsumerConfig{ - Check: &[]bool{true}[0], - ApplicationConfig: baseAppConfig, - Registries: map[string]*RegistryConfig{}, - Registry: &RegistryConfig{}, - References: baseMockRef, + Check: &[]bool{true}[0], + BaseConfig: BaseConfig{ + ApplicationConfig: baseAppConfig, + }, + Registries: map[string]*RegistryConfig{}, + Registry: &RegistryConfig{}, + References: baseMockRef, } c.SetFatherConfig(father) @@ -231,8 +239,10 @@ func TestRefreshProvider(t *testing.T) { config.GetEnvInstance().UpdateExternalConfigMap(mockMap) father := &ProviderConfig{ - ApplicationConfig: baseAppConfig, - Registries: baseRegistries, + BaseConfig: BaseConfig{ + ApplicationConfig: baseAppConfig, + }, + Registries: baseRegistries, Services: map[string]*ServiceConfig{ "MockService": { InterfaceName: "com.MockService", @@ -299,7 +309,7 @@ func TestInitializeStruct(t *testing.T) { reflect.ValueOf(testConsumerConfig).Elem().Set(v.Elem()) assert.Condition(t, func() (success bool) { - return testConsumerConfig.ApplicationConfig != nil + return testConsumerConfig.Registry != nil }) assert.Condition(t, func() (success bool) { return testConsumerConfig.Registries != nil diff --git a/config/config_loader.go b/config/config_loader.go index f8f83533d4796cc6df3e2d4c2d77f42be56e5933..d5f8c68c1bf35c40c09d7d15bae4b6b9f161e9e7 100644 --- a/config/config_loader.go +++ b/config/config_loader.go @@ -21,6 +21,7 @@ import ( "fmt" "log" "os" + "sync" "time" ) @@ -33,15 +34,21 @@ import ( "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/extension" "github.com/apache/dubbo-go/common/logger" + _ "github.com/apache/dubbo-go/common/observer/dispatcher" ) var ( - consumerConfig *ConsumerConfig - providerConfig *ProviderConfig - metricConfig *MetricConfig - applicationConfig *ApplicationConfig - maxWait = 3 - confRouterFile string + consumerConfig *ConsumerConfig + providerConfig *ProviderConfig + // baseConfig = providerConfig.BaseConfig or consumerConfig + baseConfig *BaseConfig + + // configAccessMutex is used to make sure that xxxxConfig will only be created once if needed. + // it should be used combine with double-check to avoid the race condition + configAccessMutex sync.Mutex + + maxWait = 3 + confRouterFile string ) // loaded consumer & provider config from xxx.yml, and log config from xxx.xml @@ -59,11 +66,19 @@ func init() { if errCon := ConsumerInit(confConFile); errCon != nil { log.Printf("[consumerInit] %#v", errCon) consumerConfig = nil + } else { + // Even though baseConfig has been initialized, we override it + // because we think read from config file is correct config + baseConfig = &consumerConfig.BaseConfig } if errPro := ProviderInit(confProFile); errPro != nil { log.Printf("[providerInit] %#v", errPro) providerConfig = nil + } else { + // Even though baseConfig has been initialized, we override it + // because we think read from config file is correct config + baseConfig = &providerConfig.BaseConfig } } @@ -99,9 +114,6 @@ func loadConsumerConfig() { } } - metricConfig = consumerConfig.MetricConfig - applicationConfig = consumerConfig.ApplicationConfig - checkApplicationName(consumerConfig.ApplicationConfig) if err := configCenterRefreshConsumer(); err != nil { logger.Errorf("[consumer config center refresh] %#v", err) @@ -122,14 +134,14 @@ func loadConsumerConfig() { ref.Implement(rpcService) } - //wait for invoker is available, if wait over default 3s, then panic + // wait for invoker is available, if wait over default 3s, then panic var count int checkok := true for { for _, refconfig := range consumerConfig.References { if (refconfig.Check != nil && *refconfig.Check) || (refconfig.Check == nil && consumerConfig.Check != nil && *consumerConfig.Check) || - (refconfig.Check == nil && consumerConfig.Check == nil) { //default to true + (refconfig.Check == nil && consumerConfig.Check == nil) { // default to true if refconfig.invoker != nil && !refconfig.invoker.IsAvailable() { @@ -174,15 +186,12 @@ func loadProviderConfig() { } } - // so, you should know that the consumer's config will be override - metricConfig = providerConfig.MetricConfig - applicationConfig = providerConfig.ApplicationConfig - checkApplicationName(providerConfig.ApplicationConfig) if err := configCenterRefreshProvider(); err != nil { logger.Errorf("[provider config center refresh] %#v", err) } checkRegistries(providerConfig.Registries, providerConfig.Registry) + for key, svs := range providerConfig.Services { rpcService := GetProviderService(key) if rpcService == nil { @@ -191,6 +200,7 @@ func loadProviderConfig() { } svs.id = key svs.Implement(rpcService) + svs.Protocols = providerConfig.Protocols if err := svs.Export(); err != nil { panic(fmt.Sprintf("service %s export failed! err: %#v", key, err)) } @@ -211,6 +221,15 @@ func Load() { // init router initRouter() + // init the global event dispatcher + extension.SetAndInitGlobalDispatcher(GetBaseConfig().EventDispatcherType) + + // start the metadata report if config set + if err := startMetadataReport(GetApplicationConfig().MetadataType, GetBaseConfig().MetadataReportConfig); err != nil { + logger.Errorf("Provider starts metadata report error, and the error is {%#v}", err) + return + } + // reference config loadConsumerConfig() @@ -233,39 +252,79 @@ func RPCService(service common.RPCService) { // GetMetricConfig find the MetricConfig // if it is nil, create a new one +// we use double-check to reduce race condition +// In general, it will be locked 0 or 1 time. +// So you don't need to worry about the race condition func GetMetricConfig() *MetricConfig { - if metricConfig == nil { - metricConfig = &MetricConfig{} + if GetBaseConfig().MetricConfig == nil { + configAccessMutex.Lock() + defer configAccessMutex.Unlock() + if GetBaseConfig().MetricConfig == nil { + GetBaseConfig().MetricConfig = &MetricConfig{} + } } - return metricConfig + return GetBaseConfig().MetricConfig } // GetApplicationConfig find the application config // if not, we will create one // Usually applicationConfig will be initialized when system start +// we use double-check to reduce race condition +// In general, it will be locked 0 or 1 time. +// So you don't need to worry about the race condition func GetApplicationConfig() *ApplicationConfig { - if applicationConfig == nil { - applicationConfig = &ApplicationConfig{} + if GetBaseConfig().ApplicationConfig == nil { + configAccessMutex.Lock() + defer configAccessMutex.Unlock() + if GetBaseConfig().ApplicationConfig == nil { + GetBaseConfig().ApplicationConfig = &ApplicationConfig{} + } } - return applicationConfig + return GetBaseConfig().ApplicationConfig } // GetProviderConfig find the provider config // if not found, create new one func GetProviderConfig() ProviderConfig { if providerConfig == nil { - logger.Warnf("providerConfig is nil!") - return ProviderConfig{} + if providerConfig == nil { + return ProviderConfig{} + } } return *providerConfig } // GetConsumerConfig find the consumer config // if not found, create new one +// we use double-check to reduce race condition +// In general, it will be locked 0 or 1 time. +// So you don't need to worry about the race condition func GetConsumerConfig() ConsumerConfig { if consumerConfig == nil { - logger.Warnf("consumerConfig is nil!") - return ConsumerConfig{} + if consumerConfig == nil { + return ConsumerConfig{} + } } return *consumerConfig } + +func GetBaseConfig() *BaseConfig { + if baseConfig == nil { + configAccessMutex.Lock() + defer configAccessMutex.Unlock() + if baseConfig == nil { + baseConfig = &BaseConfig{ + MetricConfig: &MetricConfig{}, + ConfigCenterConfig: &ConfigCenterConfig{}, + Remotes: make(map[string]*RemoteConfig, 0), + ApplicationConfig: &ApplicationConfig{}, + ServiceDiscoveries: make(map[string]*ServiceDiscoveryConfig, 0), + } + } + } + return baseConfig +} + +func IsProvider() bool { + return providerConfig != nil +} diff --git a/config/config_loader_test.go b/config/config_loader_test.go index a21a4998aae9eb6e0bd0632c7248cc39e7bbd9fa..2cbf526a70b52f184f500bad98edb01b97d239ec 100644 --- a/config/config_loader_test.go +++ b/config/config_loader_test.go @@ -24,6 +24,7 @@ import ( import ( "github.com/stretchr/testify/assert" + "go.uber.org/atomic" ) import ( @@ -93,7 +94,7 @@ func TestLoad(t *testing.T) { func TestLoadWithSingleReg(t *testing.T) { doInitConsumerWithSingleRegistry() - doInitProviderWithSingleRegistry() + mockInitProviderWithSingleRegistry() ms := &MockService{} SetConsumerService(ms) @@ -236,3 +237,66 @@ func TestConfigLoaderWithConfigCenterSingleRegistry(t *testing.T) { assert.Equal(t, "mock://127.0.0.1:2182", consumerConfig.Registries[constant.DEFAULT_KEY].Address) } + +func TestGetBaseConfig(t *testing.T) { + bc := GetBaseConfig() + assert.NotNil(t, bc) + _, found := bc.GetRemoteConfig("mock") + assert.False(t, found) +} + +// mockInitProviderWithSingleRegistry will init a mocked providerConfig +func mockInitProviderWithSingleRegistry() { + providerConfig = &ProviderConfig{ + BaseConfig: BaseConfig{ + ApplicationConfig: &ApplicationConfig{ + Organization: "dubbo_org", + Name: "dubbo", + Module: "module", + Version: "1.0.0", + Owner: "dubbo", + Environment: "test"}, + }, + + Registry: &RegistryConfig{ + Address: "mock://127.0.0.1:2181", + Username: "user1", + Password: "pwd1", + }, + Registries: map[string]*RegistryConfig{}, + + Services: map[string]*ServiceConfig{ + "MockService": { + InterfaceName: "com.MockService", + Protocol: "mock", + Cluster: "failover", + Loadbalance: "random", + Retries: "3", + Group: "huadong_idc", + Version: "1.0.0", + Methods: []*MethodConfig{ + { + Name: "GetUser", + Retries: "2", + Loadbalance: "random", + Weight: 200, + }, + { + Name: "GetUser1", + Retries: "2", + Loadbalance: "random", + Weight: 200, + }, + }, + exported: new(atomic.Bool), + }, + }, + Protocols: map[string]*ProtocolConfig{ + "mock": { + Name: "mock", + Ip: "127.0.0.1", + Port: "20000", + }, + }, + } +} diff --git a/config/consumer_config.go b/config/consumer_config.go index 1453628ab5e24a3607183c349d44e9440868ab60..f8b671bf3cf8304d57211f2f8b7c7c35f2aa6b9e 100644 --- a/config/consumer_config.go +++ b/config/consumer_config.go @@ -42,20 +42,18 @@ import ( type ConsumerConfig struct { BaseConfig `yaml:",inline"` Filter string `yaml:"filter" json:"filter,omitempty" property:"filter"` - // application - ApplicationConfig *ApplicationConfig `yaml:"application" json:"application,omitempty" property:"application"` - // client Connect_Timeout string `default:"100ms" yaml:"connect_timeout" json:"connect_timeout,omitempty" property:"connect_timeout"` ConnectTimeout time.Duration + Registry *RegistryConfig `yaml:"registry" json:"registry,omitempty" property:"registry"` + Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` + Request_Timeout string `yaml:"request_timeout" default:"5s" json:"request_timeout,omitempty" property:"request_timeout"` RequestTimeout time.Duration ProxyFactory string `yaml:"proxy_factory" default:"default" json:"proxy_factory,omitempty" property:"proxy_factory"` Check *bool `yaml:"check" json:"check,omitempty" property:"check"` - Registry *RegistryConfig `yaml:"registry" json:"registry,omitempty" property:"registry"` - Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` References map[string]*ReferenceConfig `yaml:"references" json:"references,omitempty" property:"references"` ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf"` FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` diff --git a/config/instance/metadata_report.go b/config/instance/metadata_report.go index 4c935d733283e6a79523dd0f35be60f092351dbb..8e833dd70bcc0db8e65cd8703f2bc1859432a887 100644 --- a/config/instance/metadata_report.go +++ b/config/instance/metadata_report.go @@ -24,18 +24,35 @@ import ( import ( "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/common/extension" - "github.com/apache/dubbo-go/metadata" + "github.com/apache/dubbo-go/metadata/report" ) var ( - instance metadata.MetadataReport - once sync.Once + instance report.MetadataReport + reportUrl common.URL + once sync.Once ) -// GetMetadataReportInstance gets metadata report instance by @url -func GetMetadataReportInstance(url *common.URL) metadata.MetadataReport { +// GetMetadataReportInstance will return the instance in lazy mode. Be careful the instance create will only +// execute once. +func GetMetadataReportInstance(selectiveUrl ...*common.URL) report.MetadataReport { once.Do(func() { - instance = extension.GetMetadataReportFactory(url.Protocol).CreateMetadataReport(url) + var url *common.URL + if len(selectiveUrl) > 0 { + url = selectiveUrl[0] + instance = extension.GetMetadataReportFactory(url.Protocol).CreateMetadataReport(url) + reportUrl = *url + } }) return instance } + +// GetMetadataReportUrl will return the report instance url +func GetMetadataReportUrl() common.URL { + return reportUrl +} + +// SetMetadataReportUrl will only can be used by unit test to mock url +func SetMetadataReportUrl(url common.URL) { + reportUrl = url +} diff --git a/config/instance/metadata_report_test.go b/config/instance/metadata_report_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d489af048055b362f2fa68a8963dd2cdf9632945 --- /dev/null +++ b/config/instance/metadata_report_test.go @@ -0,0 +1,85 @@ +/* + * 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 instance + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" + "github.com/apache/dubbo-go/metadata/report/factory" +) + +func TestGetMetadataReportInstance(t *testing.T) { + extension.SetMetadataReportFactory("mock", func() factory.MetadataReportFactory { + return &mockMetadataReportFactory{} + }) + u, _ := common.NewURL("mock://127.0.0.1") + rpt := GetMetadataReportInstance(&u) + assert.NotNil(t, rpt) +} + +type mockMetadataReportFactory struct { +} + +func (m *mockMetadataReportFactory) CreateMetadataReport(*common.URL) report.MetadataReport { + return &mockMetadataReport{} +} + +type mockMetadataReport struct { +} + +func (m mockMetadataReport) StoreProviderMetadata(*identifier.MetadataIdentifier, string) error { + panic("implement me") +} + +func (m mockMetadataReport) StoreConsumerMetadata(*identifier.MetadataIdentifier, string) error { + panic("implement me") +} + +func (m mockMetadataReport) SaveServiceMetadata(*identifier.ServiceMetadataIdentifier, common.URL) error { + panic("implement me") +} + +func (m mockMetadataReport) RemoveServiceMetadata(*identifier.ServiceMetadataIdentifier) error { + panic("implement me") +} + +func (m mockMetadataReport) GetExportedURLs(*identifier.ServiceMetadataIdentifier) ([]string, error) { + panic("implement me") +} + +func (m mockMetadataReport) SaveSubscribedData(*identifier.SubscriberMetadataIdentifier, string) error { + panic("implement me") +} + +func (m mockMetadataReport) GetSubscribedURLs(*identifier.SubscriberMetadataIdentifier) ([]string, error) { + panic("implement me") +} + +func (m mockMetadataReport) GetServiceDefinition(*identifier.MetadataIdentifier) (string, error) { + panic("implement me") +} diff --git a/config/metadata_report_config.go b/config/metadata_report_config.go index 11fb0cd65cf5500c09c51268115e5f7c60916657..6d319e5ecb8007e06dcf790fff145bfab754df3d 100644 --- a/config/metadata_report_config.go +++ b/config/metadata_report_config.go @@ -34,13 +34,10 @@ import ( // MethodConfig is method level configuration type MetadataReportConfig struct { - Protocol string `required:"true" yaml:"protocol" json:"protocol,omitempty"` - Address string `yaml:"address" json:"address,omitempty" property:"address"` - Username string `yaml:"username" json:"username,omitempty" property:"username"` - Password string `yaml:"password" json:"password,omitempty" property:"password"` - Params map[string]string `yaml:"params" json:"params,omitempty" property:"params"` - TimeoutStr string `yaml:"timeout" default:"5s" json:"timeout,omitempty" property:"timeout"` // unit: second - Group string `yaml:"group" json:"group,omitempty" property:"group"` + Protocol string `required:"true" yaml:"protocol" json:"protocol,omitempty"` + RemoteRef string `required:"true" yaml:"remote_ref" json:"remote_ref,omitempty"` + Params map[string]string `yaml:"params" json:"params,omitempty" property:"params"` + Group string `yaml:"group" json:"group,omitempty" property:"group"` } // nolint @@ -70,18 +67,24 @@ func (c *MetadataReportConfig) ToUrl() (*common.URL, error) { } } - url, err := common.NewURL(c.Address, + rc, ok := GetBaseConfig().GetRemoteConfig(c.RemoteRef) + + if !ok { + return nil, perrors.New("Could not find out the remote ref config, name: " + c.RemoteRef) + } + + res, err := common.NewURL(rc.Address, common.WithParams(urlMap), - common.WithUsername(c.Username), - common.WithPassword(c.Password), - common.WithLocation(c.Address), + common.WithUsername(rc.Username), + common.WithPassword(rc.Password), + common.WithLocation(rc.Address), common.WithProtocol(c.Protocol), ) - if err != nil || len(url.Protocol) == 0 { + if err != nil || len(res.Protocol) == 0 { return nil, perrors.New("Invalid MetadataReportConfig.") } - url.SetParam("metadata", url.Protocol) - return &url, nil + res.SetParam("metadata", res.Protocol) + return &res, nil } func (c *MetadataReportConfig) IsValid() bool { @@ -90,14 +93,12 @@ func (c *MetadataReportConfig) IsValid() bool { // StartMetadataReport: The entry of metadata report start func startMetadataReport(metadataType string, metadataReportConfig *MetadataReportConfig) error { - if metadataReportConfig == nil || metadataReportConfig.IsValid() { + if metadataReportConfig == nil || !metadataReportConfig.IsValid() { return nil } - if metadataType == constant.METACONFIG_REMOTE { - return perrors.New("No MetadataConfig found, you must specify the remote Metadata Center address when 'metadata=remote' is enabled.") - } else if metadataType == constant.METACONFIG_REMOTE && len(metadataReportConfig.Address) == 0 { - return perrors.New("MetadataConfig address can not be empty.") + if metadataType == constant.METACONFIG_REMOTE && len(metadataReportConfig.RemoteRef) == 0 { + return perrors.New("MetadataConfig remote ref can not be empty.") } if url, err := metadataReportConfig.ToUrl(); err == nil { diff --git a/config/metadata_report_config_test.go b/config/metadata_report_config_test.go index b80a77657568d0821fddca2fc16a0ccc90d7f96e..1c585ee79d58826b227df52574b3403639856306 100644 --- a/config/metadata_report_config_test.go +++ b/config/metadata_report_config_test.go @@ -23,13 +23,16 @@ import ( "github.com/stretchr/testify/assert" ) -func TestMetadataReportConfigToUrl(t *testing.T) { - metadataReportConfig := MetadataReportConfig{ - Protocol: "mock", +func TestMetadataReportConfig_ToUrl(t *testing.T) { + GetBaseConfig().Remotes["mock"] = &RemoteConfig{ Address: "127.0.0.1:2181", Username: "test", Password: "test", TimeoutStr: "3s", + } + metadataReportConfig := MetadataReportConfig{ + Protocol: "mock", + RemoteRef: "mock", Params: map[string]string{ "k": "v", }, diff --git a/config/provider_config.go b/config/provider_config.go index 97d037ede008b5fc8cab2febb2177a4e8c3dbd1b..9d8a2429d2fc1d88b01c436ae96d55bcd1c729aa 100644 --- a/config/provider_config.go +++ b/config/provider_config.go @@ -28,7 +28,6 @@ import ( import ( "github.com/apache/dubbo-go/common/constant" - "github.com/apache/dubbo-go/common/logger" "github.com/apache/dubbo-go/common/yaml" ) @@ -38,21 +37,18 @@ import ( // ProviderConfig is the default configuration of service provider type ProviderConfig struct { - BaseConfig `yaml:",inline"` - Filter string `yaml:"filter" json:"filter,omitempty" property:"filter"` - ProxyFactory string `yaml:"proxy_factory" default:"default" json:"proxy_factory,omitempty" property:"proxy_factory"` - // metadata-report - MetadataReportConfig *MetadataReportConfig `yaml:"metadata_report" json:"metadata_report,omitempty" property:"metadata_report"` + BaseConfig `yaml:",inline"` + Filter string `yaml:"filter" json:"filter,omitempty" property:"filter"` + ProxyFactory string `yaml:"proxy_factory" default:"default" json:"proxy_factory,omitempty" property:"proxy_factory"` + Services map[string]*ServiceConfig `yaml:"services" json:"services,omitempty" property:"services"` + Protocols map[string]*ProtocolConfig `yaml:"protocols" json:"protocols,omitempty" property:"protocols"` + ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf" ` + FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` + ShutdownConfig *ShutdownConfig `yaml:"shutdown_conf" json:"shutdown_conf,omitempty" property:"shutdown_conf" ` + ConfigType map[string]string `yaml:"config_type" json:"config_type,omitempty" property:"config_type"` - ApplicationConfig *ApplicationConfig `yaml:"application" json:"application,omitempty" property:"application"` - Registry *RegistryConfig `yaml:"registry" json:"registry,omitempty" property:"registry"` - Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` - Services map[string]*ServiceConfig `yaml:"services" json:"services,omitempty" property:"services"` - Protocols map[string]*ProtocolConfig `yaml:"protocols" json:"protocols,omitempty" property:"protocols"` - ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf" ` - FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` - ShutdownConfig *ShutdownConfig `yaml:"shutdown_conf" json:"shutdown_conf,omitempty" property:"shutdown_conf" ` - ConfigType map[string]string `yaml:"config_type" json:"config_type,omitempty" property:"config_type"` + Registry *RegistryConfig `yaml:"registry" json:"registry,omitempty" property:"registry"` + Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` } // UnmarshalYAML unmarshals the ProviderConfig by @unmarshal function @@ -89,25 +85,20 @@ func ProviderInit(confProFile string) error { } providerConfig.fileStream = bytes.NewBuffer(fileStream) - //set method interfaceId & interfaceName + // set method interfaceId & interfaceName for k, v := range providerConfig.Services { - //set id for reference + // set id for reference for _, n := range providerConfig.Services[k].Methods { n.InterfaceName = v.InterfaceName n.InterfaceId = k } } - //start the metadata report if config set - if err := startMetadataReport(providerConfig.ApplicationConfig.MetadataType, providerConfig.MetadataReportConfig); err != nil { - return perrors.WithMessagef(err, "Provider starts metadata report error, and the error is {%#v}", err) - } - logger.Debugf("provider config{%#v}\n", providerConfig) return nil } func configCenterRefreshProvider() error { - //fresh it + // fresh it if providerConfig.ConfigCenterConfig != nil { providerConfig.fatherConfig = providerConfig if err := providerConfig.startConfigCenter(); err != nil { diff --git a/config/reference_config.go b/config/reference_config.go index 5b7a8e9eac676e10276775cab327ae4de1eddf86..748b2d403fe315f879c817c4e0a4cf3197d807da 100644 --- a/config/reference_config.go +++ b/config/reference_config.go @@ -55,6 +55,7 @@ type ReferenceConfig struct { Retries string `yaml:"retries" json:"retries,omitempty" property:"retries"` Group string `yaml:"group" json:"group,omitempty" property:"group"` Version string `yaml:"version" json:"version,omitempty" property:"version"` + ProvideBy string `yaml:"provide_by" json:"provide_by,omitempty" property:"provide_by"` Methods []*MethodConfig `yaml:"methods" json:"methods,omitempty" property:"methods"` Async bool `yaml:"async" json:"async,omitempty" property:"async"` Params map[string]string `yaml:"params" json:"params,omitempty" property:"params"` @@ -144,6 +145,8 @@ func (c *ReferenceConfig) Refer(_ interface{}) { regUrl = u } } + + // TODO(decouple from directory, config should not depend on directory module) if regUrl != nil { cluster := extension.GetCluster("registryAware") c.invoker = cluster.Join(directory.NewStaticDirectory(invokers)) @@ -188,6 +191,7 @@ func (c *ReferenceConfig) getUrlMap() url.Values { urlMap.Set(constant.VERSION_KEY, c.Version) urlMap.Set(constant.GENERIC_KEY, strconv.FormatBool(c.Generic)) urlMap.Set(constant.ROLE_KEY, strconv.Itoa(common.CONSUMER)) + urlMap.Set(constant.PROVIDER_BY, c.ProvideBy) urlMap.Set(constant.RELEASE_KEY, "dubbo-golang-"+constant.Version) urlMap.Set(constant.SIDE_KEY, (common.RoleType(common.CONSUMER)).Role()) diff --git a/config/reference_config_test.go b/config/reference_config_test.go index e43f5aa40af84b9f15a5595ce23696b6a1bae9a4..3fbf8da44ca7d00e335260cf107e99dae3a2fa8a 100644 --- a/config/reference_config_test.go +++ b/config/reference_config_test.go @@ -38,13 +38,16 @@ var regProtocol protocol.Protocol func doInitConsumer() { consumerConfig = &ConsumerConfig{ - ApplicationConfig: &ApplicationConfig{ - Organization: "dubbo_org", - Name: "dubbo", - Module: "module", - Version: "2.6.0", - Owner: "dubbo", - Environment: "test"}, + BaseConfig: BaseConfig{ + ApplicationConfig: &ApplicationConfig{ + Organization: "dubbo_org", + Name: "dubbo", + Module: "module", + Version: "2.6.0", + Owner: "dubbo", + Environment: "test"}, + }, + Registries: map[string]*RegistryConfig{ "shanghai_reg1": { Protocol: "mock", @@ -79,6 +82,7 @@ func doInitConsumer() { Password: "pwd1", }, }, + References: map[string]*ReferenceConfig{ "MockService": { id: "MockProvider", @@ -136,19 +140,23 @@ func doInitConsumerAsync() { func doInitConsumerWithSingleRegistry() { consumerConfig = &ConsumerConfig{ - ApplicationConfig: &ApplicationConfig{ - Organization: "dubbo_org", - Name: "dubbo", - Module: "module", - Version: "2.6.0", - Owner: "dubbo", - Environment: "test"}, + BaseConfig: BaseConfig{ + ApplicationConfig: &ApplicationConfig{ + Organization: "dubbo_org", + Name: "dubbo", + Module: "module", + Version: "2.6.0", + Owner: "dubbo", + Environment: "test"}, + }, + Registry: &RegistryConfig{ Address: "mock://27.0.0.1:2181", Username: "user1", Password: "pwd1", }, Registries: map[string]*RegistryConfig{}, + References: map[string]*ReferenceConfig{ "MockService": { Params: map[string]string{ diff --git a/config/remote_config.go b/config/remote_config.go new file mode 100644 index 0000000000000000000000000000000000000000..5e0330c571715d99e63688ee944c61f8e48117bb --- /dev/null +++ b/config/remote_config.go @@ -0,0 +1,58 @@ +/* + * 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 config + +import ( + "time" +) + +import ( + "github.com/apache/dubbo-go/common/logger" +) + +// RemoteConfig: usually we need some middleware, including nacos, zookeeper +// this represents an instance of this middleware +// so that other module, like config center, registry could reuse the config +// but now, only metadata report, metadata service, service discovery use this structure +type RemoteConfig struct { + Address string `yaml:"address" json:"address,omitempty"` + TimeoutStr string `default:"5s" yaml:"timeout" json:"timeout,omitempty"` + Username string `yaml:"username" json:"username,omitempty" property:"username"` + Password string `yaml:"password" json:"password,omitempty" property:"password"` + Params map[string]string `yaml:"params" json:"address,omitempty"` +} + +// Timeout return timeout duration. +// if the configure is invalid, or missing, the default value 5s will be returned +func (rc *RemoteConfig) Timeout() time.Duration { + if res, err := time.ParseDuration(rc.TimeoutStr); err == nil { + return res + } + logger.Errorf("Could not parse the timeout string to Duration: %s, the default value will be returned", rc.TimeoutStr) + return 5 * time.Second +} + +// GetParam will return the value of the key. If not found, def will be return; +// def => default value +func (rc *RemoteConfig) GetParam(key string, def string) string { + param, ok := rc.Params[key] + if !ok { + return def + } + return param +} diff --git a/config/remote_config_test.go b/config/remote_config_test.go new file mode 100644 index 0000000000000000000000000000000000000000..82535fd60b932aecb7c6c3ee8206130fad9e7161 --- /dev/null +++ b/config/remote_config_test.go @@ -0,0 +1,42 @@ +/* + * 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 config + +import ( + "testing" +) +import ( + "github.com/stretchr/testify/assert" +) + +func TestRemoteConfig_GetParam(t *testing.T) { + rc := &RemoteConfig{ + Params: make(map[string]string, 1), + } + + def := "default value" + key := "key" + value := rc.GetParam(key, def) + assert.Equal(t, def, value) + + actualVal := "actual value" + rc.Params[key] = actualVal + + value = rc.GetParam(key, def) + assert.Equal(t, actualVal, value) +} diff --git a/config/router_config.go b/config/router_config.go index 0670ee9c20f618021d1d574344a0df85d837bd66..16943d96be76f93c2d540e2ccf16670b7424298f 100644 --- a/config/router_config.go +++ b/config/router_config.go @@ -18,16 +18,20 @@ package config import ( + gxset "github.com/dubbogo/gost/container/set" perrors "github.com/pkg/errors" ) import ( - "github.com/apache/dubbo-go/cluster/directory" "github.com/apache/dubbo-go/common/extension" "github.com/apache/dubbo-go/common/logger" "github.com/apache/dubbo-go/common/yaml" ) +var ( + routerURLSet = gxset.NewSet() +) + // RouterInit Load config file to init router config func RouterInit(confRouterFile string) error { fileRouterFactories := extension.GetFileRouterFactories() @@ -40,10 +44,14 @@ func RouterInit(confRouterFile string) error { r, e := factory.NewFileRouter(bytes) if e == nil { url := r.URL() - directory.AddRouterURLSet(&url) + routerURLSet.Add(url) return nil } logger.Warnf("router config type %s create fail {%v}\n", k, e) } return perrors.Errorf("no file router exists for parse %s , implement router.FIleRouterFactory please.", confRouterFile) } + +func GetRouterURLSet() *gxset.HashSet { + return routerURLSet +} diff --git a/config/router_config_test.go b/config/router_config_test.go index 2f0a38b2fdf59578c77076680c05b3eca5c26a1c..bf189b600f1135e4059c8833a3de042bba5427ff 100644 --- a/config/router_config_test.go +++ b/config/router_config_test.go @@ -27,7 +27,6 @@ import ( ) import ( - "github.com/apache/dubbo-go/cluster/directory" _ "github.com/apache/dubbo-go/cluster/router/condition" ) @@ -53,15 +52,3 @@ func TestString(t *testing.T) { assert.Equal(t, n2[0], "a1") assert.Equal(t, n2[1], "") } - -func TestRouterInit(t *testing.T) { - errPro := RouterInit(errorTestYML) - assert.Error(t, errPro) - - assert.Equal(t, 0, directory.GetRouterURLSet().Size()) - - errPro = RouterInit(testYML) - assert.NoError(t, errPro) - - assert.Equal(t, 1, directory.GetRouterURLSet().Size()) -} diff --git a/config/service_config.go b/config/service_config.go index 70a344c7b8c858a25e9a556ac6f4e899b1626ab4..57fce028fa59893979fc6f50671fe8681770a2b8 100644 --- a/config/service_config.go +++ b/config/service_config.go @@ -73,14 +73,18 @@ type ServiceConfig struct { ParamSign string `yaml:"param.sign" json:"param.sign,omitempty" property:"param.sign"` Tag string `yaml:"tag" json:"tag,omitempty" property:"tag"` + Protocols map[string]*ProtocolConfig unexported *atomic.Bool exported *atomic.Bool rpcService common.RPCService - cacheProtocol protocol.Protocol cacheMutex sync.Mutex + cacheProtocol protocol.Protocol + + exportersLock sync.Mutex + exporters []protocol.Exporter } -// nolint +// Prefix returns dubbo.service.${interface}. func (c *ServiceConfig) Prefix() string { return constant.ServiceConfigPrefix + c.InterfaceName + "." } @@ -94,6 +98,8 @@ func (c *ServiceConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal((*plain)(c)); err != nil { return err } + c.exported = atomic.NewBool(false) + c.unexported = atomic.NewBool(false) return nil } @@ -107,6 +113,16 @@ func NewServiceConfig(id string, context context.Context) *ServiceConfig { } } +// InitExported will set exported as false atom bool +func (c *ServiceConfig) InitExported() { + c.exported = atomic.NewBool(false) +} + +// IsExport will return whether the service config is exported or not +func (c *ServiceConfig) IsExport() bool { + return c.exported.Load() +} + // Get Random Port func getRandomPort(protocolConfigs []*ProtocolConfig) *list.List { ports := list.New() @@ -125,7 +141,7 @@ func getRandomPort(protocolConfigs []*ProtocolConfig) *list.List { return ports } -// Export ... +// Export exports the service func (c *ServiceConfig) Export() error { // TODO: config center start here @@ -142,7 +158,7 @@ func (c *ServiceConfig) Export() error { regUrls := loadRegistries(c.Registry, providerConfig.Registries, common.PROVIDER) urlMap := c.getUrlMap() - protocolConfigs := loadProtocol(c.Protocol, providerConfig.Protocols) + protocolConfigs := loadProtocol(c.Protocol, c.Protocols) if len(protocolConfigs) == 0 { logger.Warnf("The service %v's '%v' protocols don't has right protocolConfigs ", c.InterfaceName, c.Protocol) return nil @@ -178,6 +194,9 @@ func (c *ServiceConfig) Export() error { if len(c.Tag) > 0 { ivkURL.AddParam(constant.Tagkey, c.Tag) } + + var exporter protocol.Exporter + if len(regUrls) > 0 { for _, regUrl := range regUrls { regUrl.SubURL = ivkURL @@ -190,23 +209,47 @@ func (c *ServiceConfig) Export() error { c.cacheMutex.Unlock() invoker := extension.GetProxyFactory(providerConfig.ProxyFactory).GetInvoker(*regUrl) - exporter := c.cacheProtocol.Export(invoker) + exporter = c.cacheProtocol.Export(invoker) if exporter == nil { panic(perrors.New(fmt.Sprintf("Registry protocol new exporter error,registry is {%v},url is {%v}", regUrl, ivkURL))) } } } else { invoker := extension.GetProxyFactory(providerConfig.ProxyFactory).GetInvoker(*ivkURL) - exporter := extension.GetProtocol(protocolwrapper.FILTER).Export(invoker) + exporter = extension.GetProtocol(protocolwrapper.FILTER).Export(invoker) if exporter == nil { panic(perrors.New(fmt.Sprintf("Filter protocol without registry new exporter error,url is {%v}", ivkURL))) } } + c.exporters = append(c.exporters, exporter) } + c.exported.Store(true) return nil } -// Implement ... +// Unexport will call unexport of all exporters service config exported +func (c *ServiceConfig) Unexport() { + if !c.exported.Load() { + return + } + if c.unexported.Load() { + return + } + + func() { + c.exportersLock.Lock() + defer c.exportersLock.Unlock() + for _, exporter := range c.exporters { + exporter.Unexport() + } + c.exporters = nil + }() + + c.exported.Store(false) + c.unexported.Store(true) +} + +// Implement only store the @s and return func (c *ServiceConfig) Implement(s common.RPCService) { c.rpcService = s } @@ -275,3 +318,16 @@ func (c *ServiceConfig) getUrlMap() url.Values { return urlMap } + +// GetExportedUrls will return the url in service config's exporter +func (c *ServiceConfig) GetExportedUrls() []*common.URL { + if c.exported.Load() { + var urls []*common.URL + for _, exporter := range c.exporters { + url := exporter.GetInvoker().GetUrl() + urls = append(urls, &url) + } + return urls + } + return nil +} diff --git a/config/service_config_test.go b/config/service_config_test.go index 798984874a7983f4236a3e60d369b0dc9b255357..e7d55077beb56b0ccf8da833f5aeebfac07a9e3f 100644 --- a/config/service_config_test.go +++ b/config/service_config_test.go @@ -24,6 +24,7 @@ import ( import ( gxnet "github.com/dubbogo/gost/net" "github.com/stretchr/testify/assert" + "go.uber.org/atomic" ) import ( @@ -32,46 +33,14 @@ import ( func doInitProvider() { providerConfig = &ProviderConfig{ - ApplicationConfig: &ApplicationConfig{ - Organization: "dubbo_org", - Name: "dubbo", - Module: "module", - Version: "2.6.0", - Owner: "dubbo", - Environment: "test"}, - Registries: map[string]*RegistryConfig{ - "shanghai_reg1": { - Protocol: "mock", - TimeoutStr: "2s", - Group: "shanghai_idc", - Address: "127.0.0.1:2181", - Username: "user1", - Password: "pwd1", - }, - "shanghai_reg2": { - Protocol: "mock", - TimeoutStr: "2s", - Group: "shanghai_idc", - Address: "127.0.0.2:2181", - Username: "user1", - Password: "pwd1", - }, - "hangzhou_reg1": { - Protocol: "mock", - TimeoutStr: "2s", - Group: "hangzhou_idc", - Address: "127.0.0.3:2181", - Username: "user1", - Password: "pwd1", - }, - "hangzhou_reg2": { - Protocol: "mock", - TimeoutStr: "2s", - Group: "hangzhou_idc", - Address: "127.0.0.4:2181", - Username: "user1", - Password: "pwd1", - }, + BaseConfig: BaseConfig{ + ApplicationConfig: &ApplicationConfig{ + Organization: "dubbo_org", + Name: "dubbo", + Module: "module", + Version: "2.6.0", + Owner: "dubbo", + Environment: "test"}, }, Services: map[string]*ServiceConfig{ "MockService": { @@ -97,6 +66,7 @@ func doInitProvider() { Weight: 200, }, }, + exported: new(atomic.Bool), }, "MockServiceNoRightProtocol": { InterfaceName: "com.MockService", @@ -121,58 +91,45 @@ func doInitProvider() { Weight: 200, }, }, + exported: new(atomic.Bool), }, }, - Protocols: map[string]*ProtocolConfig{ - "mock": { - Name: "mock", - Ip: "127.0.0.1", - Port: "20000", - }, - }, - } -} -func doInitProviderWithSingleRegistry() { - providerConfig = &ProviderConfig{ - ApplicationConfig: &ApplicationConfig{ - Organization: "dubbo_org", - Name: "dubbo", - Module: "module", - Version: "2.6.0", - Owner: "dubbo", - Environment: "test"}, - Registry: &RegistryConfig{ - Address: "mock://127.0.0.1:2181", - Username: "user1", - Password: "pwd1", - }, - Registries: map[string]*RegistryConfig{}, - Services: map[string]*ServiceConfig{ - "MockService": { - InterfaceName: "com.MockService", - Protocol: "mock", - Cluster: "failover", - Loadbalance: "random", - Retries: "3", - Group: "huadong_idc", - Version: "1.0.0", - Methods: []*MethodConfig{ - { - Name: "GetUser", - Retries: "2", - Loadbalance: "random", - Weight: 200, - }, - { - Name: "GetUser1", - Retries: "2", - Loadbalance: "random", - Weight: 200, - }, - }, + Registries: map[string]*RegistryConfig{ + "shanghai_reg1": { + Protocol: "mock", + TimeoutStr: "2s", + Group: "shanghai_idc", + Address: "127.0.0.1:2181", + Username: "user1", + Password: "pwd1", + }, + "shanghai_reg2": { + Protocol: "mock", + TimeoutStr: "2s", + Group: "shanghai_idc", + Address: "127.0.0.2:2181", + Username: "user1", + Password: "pwd1", + }, + "hangzhou_reg1": { + Protocol: "mock", + TimeoutStr: "2s", + Group: "hangzhou_idc", + Address: "127.0.0.3:2181", + Username: "user1", + Password: "pwd1", + }, + "hangzhou_reg2": { + Protocol: "mock", + TimeoutStr: "2s", + Group: "hangzhou_idc", + Address: "127.0.0.4:2181", + Username: "user1", + Password: "pwd1", }, }, + Protocols: map[string]*ProtocolConfig{ "mock": { Name: "mock", @@ -190,6 +147,7 @@ func TestExport(t *testing.T) { for i := range providerConfig.Services { service := providerConfig.Services[i] service.Implement(&MockService{}) + service.Protocols = providerConfig.Protocols service.Export() } providerConfig = nil diff --git a/config/service_discovery_config.go b/config/service_discovery_config.go new file mode 100644 index 0000000000000000000000000000000000000000..d3dc697a9b31e6949375d74aa92234093da22378 --- /dev/null +++ b/config/service_discovery_config.go @@ -0,0 +1,30 @@ +/* + * 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 config + +// ServiceDiscoveryConfig will be used to create +type ServiceDiscoveryConfig struct { + // Protocol indicate which implementation will be used. + // for example, if the Protocol is nacos, it means that we will use nacosServiceDiscovery + Protocol string `yaml:"protocol" json:"protocol,omitempty"` + // Group, usually you don't need to config this field. + // you can use this to do some isolation + Group string `yaml:"group" json:"group,omitempty"` + // RemoteRef is the reference point to RemoteConfig which will be used to create remotes instances. + RemoteRef string `yaml:"remote_ref" json:"remote_ref,omitempty"` +} diff --git a/config_center/nacos/client_test.go b/config_center/nacos/client_test.go index 856522587d97f8be7a51c59ae36721ad88db04e8..01319f362b956a0a15be8d5d4dde2a8b2be57c89 100644 --- a/config_center/nacos/client_test.go +++ b/config_center/nacos/client_test.go @@ -66,7 +66,7 @@ func TestSetNacosClient(t *testing.T) { client = &NacosClient{ name: nacosClientName, NacosAddrs: []string{nacosURL}, - Timeout: 15, + Timeout: 15 * time.Second, exit: make(chan struct{}), onceClose: func() { close(client.exit) diff --git a/config_center/nacos/impl.go b/config_center/nacos/impl.go index 007b8be142274b63ceb56dd00399cdaf29c3746d..bbf707b93811663d0a259c6704e1008bfa91c5c1 100644 --- a/config_center/nacos/impl.go +++ b/config_center/nacos/impl.go @@ -36,8 +36,16 @@ import ( "github.com/apache/dubbo-go/config_center/parser" ) -const nacosClientName = "nacos config_center" +const ( + nacosClientName = "nacos config_center" + // the number is a little big tricky + // it will be used in query which looks up all keys with the target group + // now, one key represents one application + // so only a group has more than 9999 applications will failed + maxKeysNum = 9999 +) +// nacosDynamicConfiguration is the implementation of DynamicConfiguration based on nacos type nacosDynamicConfiguration struct { url *common.URL rootPath string @@ -108,9 +116,23 @@ func (n *nacosDynamicConfiguration) PublishConfig(key string, group string, valu // GetConfigKeysByGroup will return all keys with the group func (n *nacosDynamicConfiguration) GetConfigKeysByGroup(group string) (*gxset.HashSet, error) { - // TODO (the golang client of nacos does not support batch API) - // we should build a issue and then think about how to resolve this problem - return nil, perrors.New("unsupport operation, wait for implement") + group = n.resolvedGroup(group) + page, err := (*n.client.Client()).SearchConfig(vo.SearchConfigParm{ + Search: "accurate", + Group: group, + PageNo: 1, + // actually it's impossible for user to create 9999 application under one group + PageSize: maxKeysNum, + }) + + result := gxset.NewSet() + if err != nil { + return result, perrors.WithMessage(err, "can not find the client config") + } + for _, itm := range page.PageItems { + result.Add(itm.DataId) + } + return result, nil } // GetRule Get router rule diff --git a/config_center/nacos/impl_test.go b/config_center/nacos/impl_test.go index 453fa11f955c83ae2925a959e7a2465a2a0e7796..88d200edc927c62967975dff28e19c03743125f0 100644 --- a/config_center/nacos/impl_test.go +++ b/config_center/nacos/impl_test.go @@ -89,6 +89,34 @@ func TestGetConfig(t *testing.T) { assert.NoError(t, err) } +func TestNacosDynamicConfiguration_GetConfigKeysByGroup(t *testing.T) { + data := ` +{ + "PageItems": [ + { + "dataId": "application" + } + ] +} +` + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(data)) + })) + + nacosURL := strings.ReplaceAll(ts.URL, "http", "registry") + regurl, _ := common.NewURL(nacosURL) + nacosConfiguration, err := newNacosDynamicConfiguration(®url) + assert.NoError(t, err) + + nacosConfiguration.SetParser(&parser.DefaultConfigurationParser{}) + + configs, err := nacosConfiguration.GetConfigKeysByGroup("dubbo") + assert.Nil(t, err) + assert.Equal(t, 1, configs.Size()) + assert.True(t, configs.Contains("application")) + +} + func TestNacosDynamicConfigurationPublishConfig(t *testing.T) { nacos, err := initNacosData(t) assert.Nil(t, err) @@ -105,8 +133,6 @@ func TestAddListener(t *testing.T) { listener := &mockDataListener{} time.Sleep(time.Second * 2) nacos.AddListener("dubbo.properties", listener) - listener.wg.Add(1) - listener.wg.Wait() } func TestRemoveListener(_ *testing.T) { diff --git a/config_center/nacos/listener.go b/config_center/nacos/listener.go index 4c995389d38e1a39670aff26025f030bd4bfb1ec..3118a9d0529bf8762dc7ee864af32b044b74fd49 100644 --- a/config_center/nacos/listener.go +++ b/config_center/nacos/listener.go @@ -46,7 +46,9 @@ func (n *nacosDynamicConfiguration) addListener(key string, listener config_cent go callback(listener, namespace, group, dataId, data) }, }) - logger.Errorf("nacos : listen config fail, error:%v ", err) + if err != nil { + logger.Errorf("nacos : listen config fail, error:%v ", err) + } newListener := make(map[config_center.ConfigurationListener]context.CancelFunc) newListener[listener] = cancel n.keyListeners.Store(key, newListener) diff --git a/go.mod b/go.mod index 76fb862b00bd757080c7effd2b7ae0ba813b39a1..9e98e4ef0989e828be13fcb72b7ddd64f05d9df0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/apache/dubbo-go require ( github.com/Workiva/go-datastructures v1.0.50 github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 + github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5 // indirect github.com/apache/dubbo-go-hessian2 v1.6.1-0.20200623062814-707fde850279 + github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect github.com/coreos/bbolt v1.3.3 // indirect github.com/coreos/etcd v3.3.13+incompatible github.com/coreos/go-semver v0.3.0 // indirect @@ -11,9 +13,10 @@ require ( github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect github.com/creasty/defaults v1.3.0 github.com/dubbogo/getty v1.3.7 - github.com/dubbogo/go-zookeeper v1.0.0 + github.com/dubbogo/go-zookeeper v1.0.1 github.com/dubbogo/gost v1.9.0 github.com/emicklei/go-restful/v3 v3.0.0 + github.com/go-co-op/gocron v0.1.1 github.com/go-resty/resty/v2 v2.1.0 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect github.com/golang/mock v1.3.1 @@ -25,13 +28,14 @@ require ( github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 github.com/hashicorp/consul v1.5.3 github.com/hashicorp/consul/api v1.1.0 + github.com/hashicorp/vault v0.10.3 github.com/jinzhu/copier v0.0.0-20190625015134-976e0346caa8 github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b // indirect github.com/magiconair/properties v1.8.1 github.com/mitchellh/mapstructure v1.1.2 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd - github.com/nacos-group/nacos-sdk-go v0.3.2 + github.com/nacos-group/nacos-sdk-go v0.3.3-0.20200617023039-50c7537d6a5f github.com/opentracing/opentracing-go v1.1.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.1.0 diff --git a/go.sum b/go.sum index d426cc60b553a56c195c831834fff7ce8afab88b..eaeae0a22622a65dcf95b9b65c36289035aa3afd 100644 --- a/go.sum +++ b/go.sum @@ -7,7 +7,6 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX github.com/Azure/go-autorest v10.7.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v10.15.3+incompatible h1:nhKI/bvazIs3C3TFGoSqKY6hZ8f5od5mb5/UcS6HVIY= github.com/Azure/go-autorest v10.15.3+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v2.2.0+incompatible h1:V5BKkxACZLjzHjSgBbr2gvLA2Ae49yhc6CSY7MLy5k4= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -36,8 +35,14 @@ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vaj github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190802083043-4cd0c391755e/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ= github.com/aliyun/alibaba-cloud-sdk-go v1.61.18 h1:zOVTBdCKFd9JbCKz9/nt+FovbjPFmb7mUnp8nH9fQBA= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.18 h1:zOVTBdCKFd9JbCKz9/nt+FovbjPFmb7mUnp8nH9fQBA= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk= github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk= +github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/apache/dubbo-go-hessian2 v1.5.0 h1:fzulDG5G7nX0ccgKdiN9XipJ7tZ4WXKgmk4stdlDS6s= +github.com/apache/dubbo-go-hessian2 v1.5.0/go.mod h1:VwEnsOMidkM1usya2uPfGpSLO9XUF//WQcWn3y+jFz8= github.com/apache/dubbo-go-hessian2 v1.6.1-0.20200623062814-707fde850279 h1:1g3IJdaUjXWs++NA9Ail8+r6WgrkfhjS6hD/YXvRzjk= github.com/apache/dubbo-go-hessian2 v1.6.1-0.20200623062814-707fde850279/go.mod h1:7rEw9guWABQa6Aqb8HeZcsYPHsOS7XT1qtJvkmI6c5w= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= @@ -51,6 +56,7 @@ github.com/asaskevich/govalidator v0.0.0-20180319081651-7d2e70ef918f h1:/8NcnxL6 github.com/asaskevich/govalidator v0.0.0-20180319081651-7d2e70ef918f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.15.24 h1:xLAdTA/ore6xdPAljzZRed7IGqQgC+nY+ERS5vaj4Ro= github.com/aws/aws-sdk-go v1.15.24/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -105,10 +111,13 @@ github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dubbogo/getty v1.3.5 h1:xJxdDj9jm7wlrRSsVZSk2TDNxJbbac5GpxV0QpjO+Tw= +github.com/dubbogo/getty v1.3.5/go.mod h1:T55vN8Q6tZjf2AQZiGmkujneD3LfqYbv2b3QjacwYOY= github.com/dubbogo/getty v1.3.7 h1:xlkYD2/AH34iGteuLMsGjLl2PwBVrbIhHjf3tlUsv1M= github.com/dubbogo/getty v1.3.7/go.mod h1:XWO4+wAaMqgnBN9Ykv2YxxOAkGxymg6LGO9RK+EiCDY= -github.com/dubbogo/go-zookeeper v1.0.0 h1:RsYdlGwhDW+iKXM3eIIcvt34P2swLdmQfuIJxsHlGoM= -github.com/dubbogo/go-zookeeper v1.0.0/go.mod h1:fn6n2CAEer3novYgk9ULLwAjuV8/g4DdC2ENwRb6E+c= +github.com/dubbogo/go-zookeeper v1.0.1 h1:irLzvOsDOTNsN8Sv9tvYYxVu6DCQfLtziZQtUHmZgz8= +github.com/dubbogo/go-zookeeper v1.0.1/go.mod h1:fn6n2CAEer3novYgk9ULLwAjuV8/g4DdC2ENwRb6E+c= +github.com/dubbogo/gost v1.5.1/go.mod h1:pPTjVyoJan3aPxBPNUX0ADkXjPibLo+/Ib0/fADXSG8= github.com/dubbogo/gost v1.9.0 h1:UT+dWwvLyJiDotxJERO75jB3Yxgsdy10KztR5ycxRAk= github.com/dubbogo/gost v1.9.0/go.mod h1:pPTjVyoJan3aPxBPNUX0ADkXjPibLo+/Ib0/fADXSG8= github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 h1:2MIhn2R6oXQbgW5yHfS+d6YqyMfXiu2L55rFZC4UD/M= @@ -134,6 +143,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-co-op/gocron v0.1.1 h1:OfDmkqkCguFtFMsm6Eaayci3DADLa8pXvdmOlPU/JcU= +github.com/go-co-op/gocron v0.1.1/go.mod h1:Y9PWlYqDChf2Nbgg7kfS+ZsXHDTZbMZYPEQ0MILqH+M= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= @@ -149,6 +160,7 @@ github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+ github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-resty/resty/v2 v2.1.0 h1:Z6IefCpUMfnvItVJaJXWv/pMiiD11So35QgwEELsldE= github.com/go-resty/resty/v2 v2.1.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-sql-driver/mysql v0.0.0-20180618115901-749ddf1598b4 h1:1LlmVz15APoKz9dnm5j2ePptburJlwEH+/v/pUuoxck= @@ -203,6 +215,8 @@ github.com/gophercloud/gophercloud v0.0.0-20180828235145-f29afc2cceca/go.mod h1: github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI= @@ -388,6 +402,8 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nacos-group/nacos-sdk-go v0.3.2 h1:q+ukmIImL6u0zBtbceMZl2frgeAc45QT6cIrTZZz50c= github.com/nacos-group/nacos-sdk-go v0.3.2/go.mod h1:4TdsN7eZnnVCDlOlBa61b0gsRnvNJI74m9+2+OKZkcw= +github.com/nacos-group/nacos-sdk-go v0.3.3-0.20200617023039-50c7537d6a5f h1:gid5/0AkHvINWK69Fgbidb3BVIXqlf1YEm7wO0NVPsw= +github.com/nacos-group/nacos-sdk-go v0.3.3-0.20200617023039-50c7537d6a5f/go.mod h1:fti1GlX/EB6RDKvzK/P7Vuibqj0JMPJHQwrcTU1tLXk= github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2 h1:BQ1HW7hr4IVovMwWg0E0PYcyW8CzqDcVmaew9cujU4s= github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2/go.mod h1:TLb2Sg7HQcgGdloNxkrmtgDNR9uVYF3lfdFIN4Ro6Sk= github.com/oklog/run v0.0.0-20180308005104-6934b124db28 h1:Hbr3fbVPXea52oPQeP7KLSxP52g6SFaNY1IqAmUyEW0= @@ -395,10 +411,14 @@ github.com/oklog/run v0.0.0-20180308005104-6934b124db28/go.mod h1:dlhp/R75TPv97u github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= @@ -452,7 +472,10 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735 h1:7YvPJVmEeFHR1Tj9sZEYsmarJEQfMVYpd/Vyy/A8dqE= github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= +github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= +github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -506,12 +529,17 @@ github.com/zouyx/agollo v0.0.0-20191114083447-dde9fc9f35b8 h1:k8TV7Gz7cpWpOw/dz7 github.com/zouyx/agollo v0.0.0-20191114083447-dde9fc9f35b8/go.mod h1:S1cAa98KMFv4Sa8SbJ6ZtvOmf0VlgH0QJ1gXI0lBfBY= go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -523,7 +551,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -540,6 +567,8 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20170807180024-9a379c6b3e95/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -548,6 +577,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -581,7 +612,6 @@ golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.0.0-20180829000535-087779f1d2c9 h1:z1TeLUmxf9ws9KLICfmX+KGXTs+rjm+aGWzfsv7MZ9w= @@ -626,7 +656,6 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= istio.io/gogo-genproto v0.0.0-20190124151557-6d926a6e6feb/go.mod h1:eIDJ6jNk/IeJz6ODSksHl5Aiczy5JUq6vFhJWI5OtiI= k8s.io/api v0.0.0-20180806132203-61b11ee65332/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= diff --git a/metadata/definition/definition.go b/metadata/definition/definition.go index ead984345efde1ddd1d54b7599fd9d5584947ea2..dbbc0c8f1685edf1a26ab1fe4ad091c501e76f5f 100644 --- a/metadata/definition/definition.go +++ b/metadata/definition/definition.go @@ -17,6 +17,24 @@ package definition +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" +) + +// ServiceDefiner is a interface of service's definition +type ServiceDefiner interface { + ToBytes() ([]byte, error) +} + +// ServiceDefinition is the describer of service definition type ServiceDefinition struct { CanonicalName string CodeSource string @@ -24,6 +42,39 @@ type ServiceDefinition struct { Types []TypeDefinition } +// ToBytes convert ServiceDefinition to json string +func (def *ServiceDefinition) ToBytes() ([]byte, error) { + return json.Marshal(def) +} + +// String will iterate all methods and parameters and convert them to json string +func (def *ServiceDefinition) String() string { + var methodStr strings.Builder + for _, m := range def.Methods { + var paramType strings.Builder + for _, p := range m.ParameterTypes { + paramType.WriteString(fmt.Sprintf("{type:%v}", p)) + } + var param strings.Builder + for _, d := range m.Parameters { + param.WriteString(fmt.Sprintf("{id:%v,type:%v,builderName:%v}", d.Id, d.Type, d.TypeBuilderName)) + } + methodStr.WriteString(fmt.Sprintf("{name:%v,parameterTypes:[%v],returnType:%v,params:[%v] }", m.Name, paramType.String(), m.ReturnType, param.String())) + } + var types strings.Builder + for _, d := range def.Types { + types.WriteString(fmt.Sprintf("{id:%v,type:%v,builderName:%v}", d.Id, d.Type, d.TypeBuilderName)) + } + return fmt.Sprintf("{canonicalName:%v, codeSource:%v, methods:[%v], types:[%v]}", def.CanonicalName, def.CodeSource, methodStr.String(), types.String()) +} + +// FullServiceDefinition is the describer of service definition with parameters +type FullServiceDefinition struct { + ServiceDefinition + Params map[string]string +} + +// MethodDefinition is the describer of method definition type MethodDefinition struct { Name string ParameterTypes []string @@ -31,6 +82,7 @@ type MethodDefinition struct { Parameters []TypeDefinition } +// TypeDefinition is the describer of type definition type TypeDefinition struct { Id string Type string @@ -39,3 +91,47 @@ type TypeDefinition struct { Properties map[string]TypeDefinition TypeBuilderName string } + +// BuildServiceDefinition can build service definition which will be used to describe a service +func BuildServiceDefinition(service common.Service, url common.URL) *ServiceDefinition { + sd := &ServiceDefinition{} + sd.CanonicalName = url.Service() + + for k, m := range service.Method() { + var paramTypes []string + if len(m.ArgsType()) > 0 { + for _, t := range m.ArgsType() { + paramTypes = append(paramTypes, t.Kind().String()) + } + } + + var returnType string + if m.ReplyType() != nil { + returnType = m.ReplyType().Kind().String() + } + + methodD := MethodDefinition{ + Name: k, + ParameterTypes: paramTypes, + ReturnType: returnType, + } + sd.Methods = append(sd.Methods, methodD) + } + + return sd +} + +// ServiceDescriperBuild: build the service key, format is `group/serviceName:version` which be same as URL's service key +func ServiceDescriperBuild(serviceName string, group string, version string) string { + buf := &bytes.Buffer{} + if group != "" { + buf.WriteString(group) + buf.WriteString(constant.PATH_SEPARATOR) + } + buf.WriteString(serviceName) + if version != "" && version != "0.0.0" { + buf.WriteString(constant.KEY_SEPARATOR) + buf.WriteString(version) + } + return buf.String() +} diff --git a/metadata/definition/definition_test.go b/metadata/definition/definition_test.go new file mode 100644 index 0000000000000000000000000000000000000000..958f9324d0f9f4a5027792bd8e54b238a5f56feb --- /dev/null +++ b/metadata/definition/definition_test.go @@ -0,0 +1,52 @@ +/* + * 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 definition + +import ( + "fmt" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" +) + +func TestBuildServiceDefinition(t *testing.T) { + serviceName := "com.ikurento.user.UserProvider" + group := "group1" + version := "0.0.1" + protocol := "dubbo" + beanName := "UserProvider" + url, err := common.NewURL(fmt.Sprintf( + "%v://127.0.0.1:20000/com.ikurento.user.UserProvider1?anyhost=true&"+ + "application=BDTService&category=providers&default.timeout=10000&dubbo=dubbo-provider-golang-1.0.0&"+ + "environment=dev&interface=%v&ip=192.168.56.1&methods=GetUser&module=dubbogo+user-info+server&org=ikurento.com&"+ + "owner=ZX&pid=1447&revision=0.0.1&side=provider&timeout=3000×tamp=1556509797245&group=%v&version=%v&bean.name=%v", + protocol, serviceName, group, version, beanName)) + assert.NoError(t, err) + _, err = common.ServiceMap.Register(serviceName, protocol, &UserProvider{}) + assert.NoError(t, err) + service := common.ServiceMap.GetService(url.Protocol, url.GetParam(constant.BEAN_NAME_KEY, url.Service())) + sd := BuildServiceDefinition(*service, url) + assert.Equal(t, "{canonicalName:com.ikurento.user.UserProvider, codeSource:, methods:[{name:GetUser,parameterTypes:[{type:slice}],returnType:ptr,params:[] }], types:[]}", sd.String()) +} diff --git a/metadata/definition/mock.go b/metadata/definition/mock.go new file mode 100644 index 0000000000000000000000000000000000000000..ca9e125a7480c2b6ff57d0b7cc820b537eb908f2 --- /dev/null +++ b/metadata/definition/mock.go @@ -0,0 +1,46 @@ +/* + * 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 definition + +import ( + "context" + "time" +) + +type User struct { + Id string + Name string + Age int32 + Time time.Time +} + +type UserProvider struct { +} + +func (u *UserProvider) GetUser(ctx context.Context, req []interface{}) (*User, error) { + rsp := User{"A001", "Alex Stocks", 18, time.Now()} + return &rsp, nil +} + +func (u *UserProvider) Reference() string { + return "UserProvider" +} + +func (u User) JavaClassName() string { + return "com.ikurento.user.User" +} diff --git a/metadata/identifier/base_metadata_identifier.go b/metadata/identifier/base_metadata_identifier.go index 5f3df4c607e69d2b56e1258d081c148524cd7aca..2371f7ca02f403a11251b9b0cbb23369b27683e2 100644 --- a/metadata/identifier/base_metadata_identifier.go +++ b/metadata/identifier/base_metadata_identifier.go @@ -25,21 +25,21 @@ import ( "github.com/apache/dubbo-go/common/constant" ) -// BaseMetadataIdentifier defined for description the Metadata base identify -type BaseMetadataIdentifier interface { - getFilePathKey(params ...string) string - getIdentifierKey(params ...string) string +// BaseMetadataIdentifier defined for describe the Metadata base identify +type IMetadataIdentifier interface { + GetFilePathKey() string + GetIdentifierKey() string } // BaseMetadataIdentifier is the base implement of BaseMetadataIdentifier interface -type BaseServiceMetadataIdentifier struct { - serviceInterface string - version string - group string - side string +type BaseMetadataIdentifier struct { + ServiceInterface string + Version string + Group string + Side string } -// joinParams will join the specified char in slice, and return a string +// joinParams will join the specified char in slice, and build as string func joinParams(joinChar string, params []string) string { var joinedStr string for _, param := range params { @@ -50,27 +50,28 @@ func joinParams(joinChar string, params []string) string { } // getIdentifierKey returns string that format is service:Version:Group:Side:param1:param2... -func (mdi *BaseServiceMetadataIdentifier) getIdentifierKey(params ...string) string { - return mdi.serviceInterface + - constant.KEY_SEPARATOR + mdi.version + - constant.KEY_SEPARATOR + mdi.group + - constant.KEY_SEPARATOR + mdi.side + +func (mdi *BaseMetadataIdentifier) getIdentifierKey(params ...string) string { + return mdi.ServiceInterface + + constant.KEY_SEPARATOR + mdi.Version + + constant.KEY_SEPARATOR + mdi.Group + + constant.KEY_SEPARATOR + mdi.Side + joinParams(constant.KEY_SEPARATOR, params) } // getFilePathKey returns string that format is metadata/path/Version/Group/Side/param1/param2... -func (mdi *BaseServiceMetadataIdentifier) getFilePathKey(params ...string) string { - path := serviceToPath(mdi.serviceInterface) +func (mdi *BaseMetadataIdentifier) getFilePathKey(params ...string) string { + path := serviceToPath(mdi.ServiceInterface) return constant.DEFAULT_PATH_TAG + withPathSeparator(path) + - withPathSeparator(mdi.version) + - withPathSeparator(mdi.group) + - withPathSeparator(mdi.side) + + withPathSeparator(mdi.Version) + + withPathSeparator(mdi.Group) + + withPathSeparator(mdi.Side) + joinParams(constant.PATH_SEPARATOR, params) } +// serviceToPath uss URL encode to decode the @serviceInterface func serviceToPath(serviceInterface string) string { if serviceInterface == constant.ANY_VALUE { return "" @@ -84,6 +85,7 @@ func serviceToPath(serviceInterface string) string { } +// withPathSeparator return "/" + @path func withPathSeparator(path string) string { if len(path) != 0 { path = constant.PATH_SEPARATOR + path diff --git a/metadata/identifier/base_metadata_identifier_test.go b/metadata/identifier/base_metadata_identifier_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5b60992ab6132ecb306245af31bba7e3d0f09117 --- /dev/null +++ b/metadata/identifier/base_metadata_identifier_test.go @@ -0,0 +1,41 @@ +/* + * 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 identifier + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +var baseId = &BaseMetadataIdentifier{ + ServiceInterface: "org.apache.pkg.mockService", + Version: "1.0.0", + Group: "Group", + Side: "provider", +} + +func TestBaseGetFilePathKey(t *testing.T) { + assert.Equal(t, "metadata/1.0.0/Group/provider/a/b/c", baseId.getFilePathKey("a", "b", "c")) +} + +func TestBaseGetIdentifierKey(t *testing.T) { + assert.Equal(t, "org.apache.pkg.mockService:1.0.0:Group:provider:a:b:c", baseId.getIdentifierKey("a", "b", "c")) +} diff --git a/metadata/identifier/metadata_identifier.go b/metadata/identifier/metadata_identifier.go index 7e72c10da9c088ca167fa4fbc4dcb57f44b8c06d..7e50c4c6b9427bd9d439daa7464d96a2ea94fd39 100644 --- a/metadata/identifier/metadata_identifier.go +++ b/metadata/identifier/metadata_identifier.go @@ -19,16 +19,16 @@ package identifier // MetadataIdentifier is inherit baseMetaIdentifier with Application name type MetadataIdentifier struct { - application string + Application string BaseMetadataIdentifier } // GetIdentifierKey returns string that format is service:Version:Group:Side:Application -func (mdi *MetadataIdentifier) getIdentifierKey(params ...string) string { - return mdi.BaseMetadataIdentifier.getIdentifierKey(mdi.application) +func (mdi *MetadataIdentifier) GetIdentifierKey() string { + return mdi.BaseMetadataIdentifier.getIdentifierKey(mdi.Application) } // GetFilePathKey returns string that format is metadata/path/Version/Group/Side/Application -func (mdi *MetadataIdentifier) getFilePathKey(params ...string) string { - return mdi.BaseMetadataIdentifier.getFilePathKey(mdi.application) +func (mdi *MetadataIdentifier) GetFilePathKey() string { + return mdi.BaseMetadataIdentifier.getFilePathKey(mdi.Application) } diff --git a/metadata/identifier/metadata_identifier_test.go b/metadata/identifier/metadata_identifier_test.go new file mode 100644 index 0000000000000000000000000000000000000000..cba3c0dd76a01f2125b87db4478f99501bf2c284 --- /dev/null +++ b/metadata/identifier/metadata_identifier_test.go @@ -0,0 +1,44 @@ +/* + * 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 identifier + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +var metadataId = &MetadataIdentifier{ + Application: "app", + BaseMetadataIdentifier: BaseMetadataIdentifier{ + ServiceInterface: "org.apache.pkg.mockService", + Version: "1.0.0", + Group: "Group", + Side: "provider", + }, +} + +func TestGetFilePathKey(t *testing.T) { + assert.Equal(t, "metadata/1.0.0/Group/provider/app", metadataId.GetFilePathKey()) +} + +func TestGetIdentifierKey(t *testing.T) { + assert.Equal(t, "org.apache.pkg.mockService:1.0.0:Group:provider:app", metadataId.GetIdentifierKey()) +} diff --git a/metadata/identifier/service_metadata_identifier.go b/metadata/identifier/service_metadata_identifier.go index ccc149f7306c125b19a25373d4da660a154cc84e..b9e65967e0f707a6efcc9f8ded2ce5dec4f058b8 100644 --- a/metadata/identifier/service_metadata_identifier.go +++ b/metadata/identifier/service_metadata_identifier.go @@ -18,22 +18,38 @@ package identifier import ( + "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/common/constant" ) // ServiceMetadataIdentifier is inherit baseMetaIdentifier with service params: Revision and Protocol type ServiceMetadataIdentifier struct { - revision string - protocol string + Revision string + Protocol string BaseMetadataIdentifier } +// NewServiceMetadataIdentifier create instance. +// The ServiceInterface is the @url.Service() +// other parameters are read from @url +func NewServiceMetadataIdentifier(url common.URL) *ServiceMetadataIdentifier { + return &ServiceMetadataIdentifier{ + BaseMetadataIdentifier: BaseMetadataIdentifier{ + ServiceInterface: url.Service(), + Version: url.GetParam(constant.VERSION_KEY, ""), + Group: url.GetParam(constant.GROUP_KEY, ""), + Side: url.GetParam(constant.SIDE_KEY, ""), + }, + Protocol: url.Protocol, + } +} + // GetIdentifierKey returns string that format is service:Version:Group:Side:Protocol:"revision"+Revision -func (mdi *ServiceMetadataIdentifier) getIdentifierKey(params ...string) string { - return mdi.BaseMetadataIdentifier.getIdentifierKey(mdi.protocol + constant.KEY_REVISON_PREFIX + mdi.revision) +func (mdi *ServiceMetadataIdentifier) GetIdentifierKey() string { + return mdi.BaseMetadataIdentifier.getIdentifierKey(mdi.Protocol, constant.KEY_REVISON_PREFIX+mdi.Revision) } // GetFilePathKey returns string that format is metadata/path/Version/Group/Side/Protocol/"revision"+Revision -func (mdi *ServiceMetadataIdentifier) getFilePathKey(params ...string) string { - return mdi.BaseMetadataIdentifier.getFilePathKey(mdi.protocol + constant.KEY_REVISON_PREFIX + mdi.revision) +func (mdi *ServiceMetadataIdentifier) GetFilePathKey() string { + return mdi.BaseMetadataIdentifier.getFilePathKey(mdi.Protocol, constant.KEY_REVISON_PREFIX+mdi.Revision) } diff --git a/metadata/identifier/service_metadata_identifier_test.go b/metadata/identifier/service_metadata_identifier_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d7ef44a4bbc7611b6391122f8f5841db349eb036 --- /dev/null +++ b/metadata/identifier/service_metadata_identifier_test.go @@ -0,0 +1,45 @@ +/* + * 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 identifier + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +var serviceMetadataId = &ServiceMetadataIdentifier{ + Revision: "1.0", + Protocol: "dubbo", + BaseMetadataIdentifier: BaseMetadataIdentifier{ + ServiceInterface: "org.apache.pkg.mockService", + Version: "1.0.0", + Group: "Group", + Side: "provider", + }, +} + +func TestServiceGetFilePathKey(t *testing.T) { + assert.Equal(t, "metadata/1.0.0/Group/provider/dubbo/revision1.0", serviceMetadataId.GetFilePathKey()) +} + +func TestServiceGetIdentifierKey(t *testing.T) { + assert.Equal(t, "org.apache.pkg.mockService:1.0.0:Group:provider:dubbo:revision1.0", serviceMetadataId.GetIdentifierKey()) +} diff --git a/metadata/identifier/subscribe_metadata_identifier.go b/metadata/identifier/subscribe_metadata_identifier.go index 38f3ebbd462338b581d83cd19403a00a5064b5a4..b1e37db971ada56a77bc3b716606b6fc8d137d34 100644 --- a/metadata/identifier/subscribe_metadata_identifier.go +++ b/metadata/identifier/subscribe_metadata_identifier.go @@ -19,16 +19,16 @@ package identifier // SubscriberMetadataIdentifier is inherit baseMetaIdentifier with service params: Revision type SubscriberMetadataIdentifier struct { - revision string - BaseMetadataIdentifier + Revision string + MetadataIdentifier } // GetIdentifierKey returns string that format is service:Version:Group:Side:Revision -func (mdi *SubscriberMetadataIdentifier) getIdentifierKey(params ...string) string { - return mdi.BaseMetadataIdentifier.getIdentifierKey(mdi.revision) +func (mdi *SubscriberMetadataIdentifier) GetIdentifierKey() string { + return mdi.BaseMetadataIdentifier.getIdentifierKey(mdi.Revision) } // GetFilePathKey returns string that format is metadata/path/Version/Group/Side/Revision -func (mdi *SubscriberMetadataIdentifier) getFilePathKey(params ...string) string { - return mdi.BaseMetadataIdentifier.getFilePathKey(mdi.revision) +func (mdi *SubscriberMetadataIdentifier) GetFilePathKey() string { + return mdi.BaseMetadataIdentifier.getFilePathKey(mdi.Revision) } diff --git a/metadata/identifier/subscribe_metadata_identifier_test.go b/metadata/identifier/subscribe_metadata_identifier_test.go new file mode 100644 index 0000000000000000000000000000000000000000..215aa3c5691f20d7790029093372389ce620398c --- /dev/null +++ b/metadata/identifier/subscribe_metadata_identifier_test.go @@ -0,0 +1,46 @@ +/* + * 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 identifier + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +var subscribeMetadataId = &SubscriberMetadataIdentifier{ + Revision: "1.0", + MetadataIdentifier: MetadataIdentifier{ + BaseMetadataIdentifier: BaseMetadataIdentifier{ + ServiceInterface: "org.apache.pkg.mockService", + Version: "1.0.0", + Group: "Group", + Side: "provider", + }, + }, +} + +func TestSubscribeGetFilePathKey(t *testing.T) { + assert.Equal(t, "metadata/1.0.0/Group/provider/1.0", subscribeMetadataId.GetFilePathKey()) +} + +func TestSubscribeGetIdentifierKey(t *testing.T) { + assert.Equal(t, "org.apache.pkg.mockService:1.0.0:Group:provider:1.0", subscribeMetadataId.GetIdentifierKey()) +} diff --git a/metadata/namemapping/dynamic/service_name_mapping.go b/metadata/mapping/dynamic/service_name_mapping.go similarity index 75% rename from metadata/namemapping/dynamic/service_name_mapping.go rename to metadata/mapping/dynamic/service_name_mapping.go index e93c256fe093b4a3e3c431e1d012038b2bb7976b..84039ace9a2d56eca96bf36afc46d28e2a5ebe60 100644 --- a/metadata/namemapping/dynamic/service_name_mapping.go +++ b/metadata/mapping/dynamic/service_name_mapping.go @@ -19,6 +19,7 @@ package dynamic import ( "strconv" + "sync" "time" ) @@ -28,18 +29,26 @@ import ( ) import ( + commonCfg "github.com/apache/dubbo-go/common/config" "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" "github.com/apache/dubbo-go/config" "github.com/apache/dubbo-go/config_center" - "github.com/apache/dubbo-go/metadata" + "github.com/apache/dubbo-go/metadata/mapping" ) const ( - defaultGroup = config_center.DEFAULT_GROUP + defaultGroup = "mapping" slash = "/" ) +func init() { + extension.SetGlobalServiceNameMapping(GetNameMappingInstance) +} + // DynamicConfigurationServiceNameMapping is the implementation based on config center +// it's a singleton type DynamicConfigurationServiceNameMapping struct { dc config_center.DynamicConfiguration } @@ -48,7 +57,8 @@ type DynamicConfigurationServiceNameMapping struct { func (d *DynamicConfigurationServiceNameMapping) Map(serviceInterface string, group string, version string, protocol string) error { // metadata service is admin service, should not be mapped if constant.METADATA_SERVICE_NAME == serviceInterface { - return perrors.New("try to map the metadata service, will be ignored") + logger.Info("try to map the metadata service, will be ignored") + return nil } appName := config.GetApplicationConfig().Name @@ -76,7 +86,16 @@ func (d *DynamicConfigurationServiceNameMapping) buildGroup(serviceInterface str return defaultGroup + slash + serviceInterface } -// NewServiceNameMapping will create an instance of DynamicConfigurationServiceNameMapping -func NewServiceNameMapping(dc config_center.DynamicConfiguration) metadata.ServiceNameMapping { - return &DynamicConfigurationServiceNameMapping{dc: dc} +var ( + serviceNameMappingInstance *DynamicConfigurationServiceNameMapping + serviceNameMappingOnce sync.Once +) + +// GetNameMappingInstance return an instance, if not found, it creates one +func GetNameMappingInstance() mapping.ServiceNameMapping { + serviceNameMappingOnce.Do(func() { + dc := commonCfg.GetEnvInstance().GetDynamicConfiguration() + serviceNameMappingInstance = &DynamicConfigurationServiceNameMapping{dc: dc} + }) + return serviceNameMappingInstance } diff --git a/metadata/namemapping/dynamic/service_name_mapping_test.go b/metadata/mapping/dynamic/service_name_mapping_test.go similarity index 95% rename from metadata/namemapping/dynamic/service_name_mapping_test.go rename to metadata/mapping/dynamic/service_name_mapping_test.go index e3d620cd738421c256d8fd232b1afcfd425ca989..2896b0fd4aa4fb6bada132c276c70a1653e59f99 100644 --- a/metadata/namemapping/dynamic/service_name_mapping_test.go +++ b/metadata/mapping/dynamic/service_name_mapping_test.go @@ -41,14 +41,14 @@ func TestDynamicConfigurationServiceNameMapping(t *testing.T) { }).GetDynamicConfiguration(nil) config.GetApplicationConfig().Name = appName - mapping := NewServiceNameMapping(dc) + mapping := &DynamicConfigurationServiceNameMapping{dc: dc} intf := constant.METADATA_SERVICE_NAME group := "myGroup" version := "myVersion" protocol := "myProtocol" err = mapping.Map(intf, group, version, protocol) - assert.NotNil(t, err) + assert.Nil(t, err) intf = "MyService" err = mapping.Map(intf, group, version, protocol) assert.Nil(t, err) diff --git a/metadata/mapping/memory/service_name_mapping.go b/metadata/mapping/memory/service_name_mapping.go new file mode 100644 index 0000000000000000000000000000000000000000..0965d52d91e047215abd9b7d14523ecaa833f0ed --- /dev/null +++ b/metadata/mapping/memory/service_name_mapping.go @@ -0,0 +1,55 @@ +/* + * 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 memory + +import ( + "sync" +) + +import ( + gxset "github.com/dubbogo/gost/container/set" +) +import ( + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/metadata/mapping" +) + +func init() { + extension.SetGlobalServiceNameMapping(GetNameMappingInstance) +} + +type InMemoryServiceNameMapping struct{} + +func (i *InMemoryServiceNameMapping) Map(serviceInterface string, group string, version string, protocol string) error { + return nil +} + +func (i *InMemoryServiceNameMapping) Get(serviceInterface string, group string, version string, protocol string) (*gxset.HashSet, error) { + return gxset.NewSet(config.GetApplicationConfig().Name), nil +} + +var serviceNameMappingInstance *InMemoryServiceNameMapping +var serviceNameMappingOnce sync.Once + +func GetNameMappingInstance() mapping.ServiceNameMapping { + serviceNameMappingOnce.Do(func() { + serviceNameMappingInstance = &InMemoryServiceNameMapping{} + }) + return serviceNameMappingInstance +} diff --git a/metadata/service_name_mapping.go b/metadata/mapping/service_name_mapping.go similarity index 98% rename from metadata/service_name_mapping.go rename to metadata/mapping/service_name_mapping.go index c14e8ce2e7c40d1573897dfd6ba64c16e18acac7..6caed9f0b48c1bb9c2f0f1026eb642f69bb31113 100644 --- a/metadata/service_name_mapping.go +++ b/metadata/mapping/service_name_mapping.go @@ -15,7 +15,7 @@ * limitations under the License. */ -package metadata +package mapping import ( gxset "github.com/dubbogo/gost/container/set" diff --git a/metadata/report/consul/report.go b/metadata/report/consul/report.go new file mode 100644 index 0000000000000000000000000000000000000000..eb2bdc25ecec596a8f89abb80856b8d6e7be70a4 --- /dev/null +++ b/metadata/report/consul/report.go @@ -0,0 +1,125 @@ +/* + * 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 consul + +import ( + consul "github.com/hashicorp/consul/api" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" + "github.com/apache/dubbo-go/metadata/report/factory" +) + +var ( + emptyStrSlice = make([]string, 0) +) + +func init() { + mf := &consulMetadataReportFactory{} + extension.SetMetadataReportFactory("consul", func() factory.MetadataReportFactory { + return mf + }) +} + +// consulMetadataReport is the implementation of +// MetadataReport based on consul. +type consulMetadataReport struct { + client *consul.Client +} + +// StoreProviderMetadata stores the metadata. +func (m *consulMetadataReport) StoreProviderMetadata(providerIdentifier *identifier.MetadataIdentifier, serviceDefinitions string) error { + kv := &consul.KVPair{Key: providerIdentifier.GetIdentifierKey(), Value: []byte(serviceDefinitions)} + _, err := m.client.KV().Put(kv, nil) + return err +} + +// StoreConsumerMetadata stores the metadata. +func (m *consulMetadataReport) StoreConsumerMetadata(consumerMetadataIdentifier *identifier.MetadataIdentifier, serviceParameterString string) error { + kv := &consul.KVPair{Key: consumerMetadataIdentifier.GetIdentifierKey(), Value: []byte(serviceParameterString)} + _, err := m.client.KV().Put(kv, nil) + return err +} + +// SaveServiceMetadata saves the metadata. +func (m *consulMetadataReport) SaveServiceMetadata(metadataIdentifier *identifier.ServiceMetadataIdentifier, url common.URL) error { + kv := &consul.KVPair{Key: metadataIdentifier.GetIdentifierKey(), Value: []byte(url.String())} + _, err := m.client.KV().Put(kv, nil) + return err +} + +// RemoveServiceMetadata removes the metadata. +func (m *consulMetadataReport) RemoveServiceMetadata(metadataIdentifier *identifier.ServiceMetadataIdentifier) error { + k := metadataIdentifier.GetIdentifierKey() + _, err := m.client.KV().Delete(k, nil) + return err +} + +// GetExportedURLs gets the urls. +func (m *consulMetadataReport) GetExportedURLs(metadataIdentifier *identifier.ServiceMetadataIdentifier) ([]string, error) { + k := metadataIdentifier.GetIdentifierKey() + kv, _, err := m.client.KV().Get(k, nil) + if err != nil || kv == nil { + return emptyStrSlice, err + } + return []string{string(kv.Value)}, nil +} + +// SaveSubscribedData saves the urls. +func (m *consulMetadataReport) SaveSubscribedData(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier, urls string) error { + kv := &consul.KVPair{Key: subscriberMetadataIdentifier.GetIdentifierKey(), Value: []byte(urls)} + _, err := m.client.KV().Put(kv, nil) + return err +} + +// GetSubscribedURLs gets the urls. +func (m *consulMetadataReport) GetSubscribedURLs(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier) ([]string, error) { + k := subscriberMetadataIdentifier.GetIdentifierKey() + kv, _, err := m.client.KV().Get(k, nil) + if err != nil || kv == nil { + return emptyStrSlice, err + } + return []string{string(kv.Value)}, nil +} + +// GetServiceDefinition gets the service definition. +func (m *consulMetadataReport) GetServiceDefinition(metadataIdentifier *identifier.MetadataIdentifier) (string, error) { + k := metadataIdentifier.GetIdentifierKey() + kv, _, err := m.client.KV().Get(k, nil) + if err != nil || kv == nil { + return "", err + } + return string(kv.Value), nil +} + +type consulMetadataReportFactory struct { +} + +// nolint +func (mf *consulMetadataReportFactory) CreateMetadataReport(url *common.URL) report.MetadataReport { + config := &consul.Config{Address: url.Location} + client, err := consul.NewClient(config) + if err != nil { + panic(err) + } + return &consulMetadataReport{client: client} +} diff --git a/metadata/report/consul/report_test.go b/metadata/report/consul/report_test.go new file mode 100644 index 0000000000000000000000000000000000000000..34ee29de945f2b9ac6978a55008048e62f4c6812 --- /dev/null +++ b/metadata/report/consul/report_test.go @@ -0,0 +1,165 @@ +/* + * 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 consul + +import ( + "encoding/json" + "net/url" + "strconv" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" + "github.com/apache/dubbo-go/remoting/consul" +) + +func newProviderRegistryUrl(host string, port int) *common.URL { + return common.NewURLWithOptions( + common.WithIp(host), + common.WithPort(strconv.Itoa(port)), + common.WithParams(url.Values{}), + common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER)), + ) +} + +func newBaseMetadataIdentifier(side string) *identifier.BaseMetadataIdentifier { + return &identifier.BaseMetadataIdentifier{ + ServiceInterface: "org.apache.HelloWorld", + Version: "1.0.0", + Group: "group", + Side: side, + } +} + +func newMetadataIdentifier(side string) *identifier.MetadataIdentifier { + return &identifier.MetadataIdentifier{ + Application: "application", + BaseMetadataIdentifier: *newBaseMetadataIdentifier(side), + } +} + +func newServiceMetadataIdentifier(side string) *identifier.ServiceMetadataIdentifier { + return &identifier.ServiceMetadataIdentifier{ + Revision: "1.0", + Protocol: "dubbo", + BaseMetadataIdentifier: *newBaseMetadataIdentifier(side), + } +} + +func newSubscribeMetadataIdentifier(side string) *identifier.SubscriberMetadataIdentifier { + return &identifier.SubscriberMetadataIdentifier{ + Revision: "1.0", + MetadataIdentifier: *newMetadataIdentifier(side), + } +} + +type consulMetadataReportTestSuite struct { + t *testing.T + m report.MetadataReport +} + +func newConsulMetadataReportTestSuite(t *testing.T, m report.MetadataReport) *consulMetadataReportTestSuite { + return &consulMetadataReportTestSuite{t: t, m: m} +} + +func (suite *consulMetadataReportTestSuite) testStoreProviderMetadata() { + providerMi := newMetadataIdentifier("provider") + providerMeta := "provider" + err := suite.m.StoreProviderMetadata(providerMi, providerMeta) + assert.NoError(suite.t, err) +} + +func (suite *consulMetadataReportTestSuite) testStoreConsumerMetadata() { + consumerMi := newMetadataIdentifier("consumer") + consumerMeta := "consumer" + err := suite.m.StoreProviderMetadata(consumerMi, consumerMeta) + assert.NoError(suite.t, err) +} + +func (suite *consulMetadataReportTestSuite) testSaveServiceMetadata(url common.URL) { + serviceMi := newServiceMetadataIdentifier("provider") + err := suite.m.SaveServiceMetadata(serviceMi, url) + assert.NoError(suite.t, err) +} + +func (suite *consulMetadataReportTestSuite) testRemoveServiceMetadata() { + serviceMi := newServiceMetadataIdentifier("provider") + err := suite.m.RemoveServiceMetadata(serviceMi) + assert.NoError(suite.t, err) +} + +func (suite *consulMetadataReportTestSuite) testGetExportedURLs() { + serviceMi := newServiceMetadataIdentifier("provider") + urls, err := suite.m.GetExportedURLs(serviceMi) + assert.Equal(suite.t, 1, len(urls)) + assert.NoError(suite.t, err) +} + +func (suite *consulMetadataReportTestSuite) testSaveSubscribedData(url common.URL) { + subscribeMi := newSubscribeMetadataIdentifier("provider") + urls := []string{url.String()} + bytes, _ := json.Marshal(urls) + err := suite.m.SaveSubscribedData(subscribeMi, string(bytes)) + assert.Nil(suite.t, err) +} + +func (suite *consulMetadataReportTestSuite) testGetSubscribedURLs() { + subscribeMi := newSubscribeMetadataIdentifier("provider") + urls, err := suite.m.GetSubscribedURLs(subscribeMi) + assert.Equal(suite.t, 1, len(urls)) + assert.NoError(suite.t, err) +} + +func (suite *consulMetadataReportTestSuite) testGetServiceDefinition() { + providerMi := newMetadataIdentifier("provider") + providerMeta, err := suite.m.GetServiceDefinition(providerMi) + assert.Equal(suite.t, "provider", providerMeta) + assert.NoError(suite.t, err) +} + +func test1(t *testing.T) { + consulAgent := consul.NewConsulAgent(t, 8500) + defer consulAgent.Close() + + url := newProviderRegistryUrl("localhost", 8500) + mf := extension.GetMetadataReportFactory("consul") + m := mf.CreateMetadataReport(url) + + suite := newConsulMetadataReportTestSuite(t, m) + suite.testStoreProviderMetadata() + suite.testStoreConsumerMetadata() + suite.testSaveServiceMetadata(*url) + suite.testGetExportedURLs() + suite.testRemoveServiceMetadata() + suite.testSaveSubscribedData(*url) + suite.testGetSubscribedURLs() + suite.testGetServiceDefinition() +} + +func TestConsulMetadataReport(t *testing.T) { + t.Run("test1", test1) +} diff --git a/metadata/report/delegate/delegate_report.go b/metadata/report/delegate/delegate_report.go new file mode 100644 index 0000000000000000000000000000000000000000..cdd29ab2e483647ed90f37032e10d174f7583e39 --- /dev/null +++ b/metadata/report/delegate/delegate_report.go @@ -0,0 +1,287 @@ +/* + * 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 delegate + +import ( + "encoding/json" + "runtime/debug" + "sync" + "time" +) + +import ( + "github.com/go-co-op/gocron" + perrors "github.com/pkg/errors" + "go.uber.org/atomic" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config/instance" + "github.com/apache/dubbo-go/metadata/definition" + "github.com/apache/dubbo-go/metadata/identifier" +) + +const ( + // defaultMetadataReportRetryTimes is defined for max times to retry + defaultMetadataReportRetryTimes int64 = 100 + // defaultMetadataReportRetryPeriod is defined for cycle interval to retry, the unit is second + defaultMetadataReportRetryPeriod int64 = 3 + // defaultMetadataReportRetryPeriod is defined for cycle report or not + defaultMetadataReportCycleReport bool = true +) + +// metadataReportRetry is a scheduler for retrying task +type metadataReportRetry struct { + retryPeriod int64 + retryLimit int64 + scheduler *gocron.Scheduler + job *gocron.Job + retryCounter *atomic.Int64 + // if no failed report, wait how many times to run retry task. + retryTimesIfNonFail int64 +} + +// newMetadataReportRetry will create a scheduler for retry task +func newMetadataReportRetry(retryPeriod int64, retryLimit int64, retryFunc func() bool) (*metadataReportRetry, error) { + s1 := gocron.NewScheduler(time.UTC) + + mrr := &metadataReportRetry{ + retryPeriod: retryPeriod, + retryLimit: retryLimit, + scheduler: s1, + retryCounter: atomic.NewInt64(0), + retryTimesIfNonFail: 600, + } + + newJob, err := mrr.scheduler.Every(uint64(mrr.retryPeriod)).Seconds().Do( + func() { + mrr.retryCounter.Inc() + logger.Infof("start to retry task for metadata report. retry times: %v", mrr.retryCounter.Load()) + if mrr.retryCounter.Load() > mrr.retryLimit { + mrr.scheduler.Clear() + } else if retryFunc() && mrr.retryCounter.Load() > mrr.retryTimesIfNonFail { + mrr.scheduler.Clear() // may not interrupt the running job + } + }) + + mrr.job = newJob + return mrr, err +} + +// startRetryTask will make scheduler with retry task run +func (mrr *metadataReportRetry) startRetryTask() { + mrr.scheduler.StartAt(time.Now().Add(500 * time.Millisecond)) + mrr.scheduler.Start() +} + +// MetadataReport is a absolute delegate for MetadataReport +type MetadataReport struct { + reportUrl common.URL + syncReport bool + metadataReportRetry *metadataReportRetry + + failedReports map[*identifier.MetadataIdentifier]interface{} + failedReportsLock sync.RWMutex + + // allMetadataReports store all the metdadata reports records in memory + allMetadataReports map[*identifier.MetadataIdentifier]interface{} + allMetadataReportsLock sync.RWMutex +} + +// NewMetadataReport will create a MetadataReport with initiation +func NewMetadataReport() (*MetadataReport, error) { + url := instance.GetMetadataReportUrl() + bmr := &MetadataReport{ + reportUrl: url, + syncReport: url.GetParamBool(constant.SYNC_REPORT_KEY, false), + failedReports: make(map[*identifier.MetadataIdentifier]interface{}, 4), + allMetadataReports: make(map[*identifier.MetadataIdentifier]interface{}, 4), + } + + mrr, err := newMetadataReportRetry( + url.GetParamInt(constant.RETRY_PERIOD_KEY, defaultMetadataReportRetryPeriod), + url.GetParamInt(constant.RETRY_TIMES_KEY, defaultMetadataReportRetryTimes), + bmr.retry, + ) + + if err != nil { + return nil, err + } + + bmr.metadataReportRetry = mrr + if url.GetParamBool(constant.CYCLE_REPORT_KEY, defaultMetadataReportCycleReport) { + scheduler := gocron.NewScheduler(time.UTC) + _, err := scheduler.Every(1).Day().Do( + func() { + logger.Infof("start to publish all metadata in metadata report %s.", url.String()) + bmr.allMetadataReportsLock.RLock() + bmr.doHandlerMetadataCollection(bmr.allMetadataReports) + bmr.allMetadataReportsLock.RUnlock() + + }) + if err != nil { + return nil, err + } + scheduler.StartAt(time.Now().Add(500 * time.Millisecond)) + scheduler.Start() + } + return bmr, nil +} + +// retry will do metadata failed reports collection by call metadata report sdk +func (mr *MetadataReport) retry() bool { + mr.failedReportsLock.RLock() + defer mr.failedReportsLock.RUnlock() + return mr.doHandlerMetadataCollection(mr.failedReports) +} + +// StoreProviderMetadata will delegate to call remote metadata's sdk to store provider service definition +func (mr *MetadataReport) StoreProviderMetadata(identifier *identifier.MetadataIdentifier, definer definition.ServiceDefiner) { + if mr.syncReport { + mr.storeMetadataTask(common.PROVIDER, identifier, definer) + } + go mr.storeMetadataTask(common.PROVIDER, identifier, definer) +} + +// storeMetadataTask will delegate to call remote metadata's sdk to store +func (mr *MetadataReport) storeMetadataTask(role int, identifier *identifier.MetadataIdentifier, definer interface{}) { + logger.Infof("store provider metadata. Identifier :%v ; definition: %v .", identifier, definer) + mr.allMetadataReportsLock.Lock() + mr.allMetadataReports[identifier] = definer + mr.allMetadataReportsLock.Unlock() + + mr.failedReportsLock.Lock() + delete(mr.failedReports, identifier) + mr.failedReportsLock.Unlock() + // data is store the json marshaled definition + var ( + data []byte + err error + ) + + defer func() { + if r := recover(); r != nil { + mr.failedReportsLock.Lock() + mr.failedReports[identifier] = definer + mr.failedReportsLock.Unlock() + mr.metadataReportRetry.startRetryTask() + logger.Errorf("Failed to put provider metadata %v in %v, cause: %v\n%s\n", + identifier, string(data), r, string(debug.Stack())) + } + }() + + data, err = json.Marshal(definer) + if err != nil { + logger.Errorf("storeProviderMetadataTask error in stage json.Marshal, msg is %+v", err) + panic(err) + } + report := instance.GetMetadataReportInstance() + if role == common.PROVIDER { + err = report.StoreProviderMetadata(identifier, string(data)) + } else if role == common.CONSUMER { + err = report.StoreConsumerMetadata(identifier, string(data)) + } + + if err != nil { + logger.Errorf("storeProviderMetadataTask error in stage call metadata report to StoreProviderMetadata, msg is %+v", err) + panic(err) + } +} + +// StoreConsumerMetadata will delegate to call remote metadata's sdk to store consumer side service definition +func (mr *MetadataReport) StoreConsumerMetadata(identifier *identifier.MetadataIdentifier, definer map[string]string) { + if mr.syncReport { + mr.storeMetadataTask(common.CONSUMER, identifier, definer) + } + go mr.storeMetadataTask(common.CONSUMER, identifier, definer) +} + +// SaveServiceMetadata will delegate to call remote metadata's sdk to save service metadata +func (mr *MetadataReport) SaveServiceMetadata(identifier *identifier.ServiceMetadataIdentifier, url common.URL) error { + report := instance.GetMetadataReportInstance() + if mr.syncReport { + return report.SaveServiceMetadata(identifier, url) + } + go report.SaveServiceMetadata(identifier, url) + return nil +} + +// RemoveServiceMetadata will delegate to call remote metadata's sdk to remove service metadata +func (mr *MetadataReport) RemoveServiceMetadata(identifier *identifier.ServiceMetadataIdentifier) error { + report := instance.GetMetadataReportInstance() + if mr.syncReport { + return report.RemoveServiceMetadata(identifier) + } + go report.RemoveServiceMetadata(identifier) + return nil +} + +// GetExportedURLs will delegate to call remote metadata's sdk to get exported urls +func (mr *MetadataReport) GetExportedURLs(identifier *identifier.ServiceMetadataIdentifier) ([]string, error) { + report := instance.GetMetadataReportInstance() + return report.GetExportedURLs(identifier) +} + +// SaveSubscribedData will delegate to call remote metadata's sdk to save subscribed data +func (mr *MetadataReport) SaveSubscribedData(identifier *identifier.SubscriberMetadataIdentifier, urls []common.URL) error { + urlStrList := make([]string, 0, len(urls)) + for _, url := range urls { + urlStrList = append(urlStrList, url.String()) + } + bytes, err := json.Marshal(urlStrList) + if err != nil { + return perrors.WithMessage(err, "Could not convert the array to json") + } + + report := instance.GetMetadataReportInstance() + if mr.syncReport { + return report.SaveSubscribedData(identifier, string(bytes)) + } + go report.SaveSubscribedData(identifier, string(bytes)) + return nil +} + +// GetSubscribedURLs will delegate to call remote metadata's sdk to get subscribed urls +func (MetadataReport) GetSubscribedURLs(identifier *identifier.SubscriberMetadataIdentifier) ([]string, error) { + report := instance.GetMetadataReportInstance() + return report.GetSubscribedURLs(identifier) +} + +// GetServiceDefinition will delegate to call remote metadata's sdk to get service definitions +func (MetadataReport) GetServiceDefinition(identifier *identifier.MetadataIdentifier) (string, error) { + report := instance.GetMetadataReportInstance() + return report.GetServiceDefinition(identifier) +} + +// doHandlerMetadataCollection will store metadata to metadata support with given metadataMap +func (mr *MetadataReport) doHandlerMetadataCollection(metadataMap map[*identifier.MetadataIdentifier]interface{}) bool { + if len(metadataMap) == 0 { + return true + } + for e := range metadataMap { + if common.RoleType(common.PROVIDER).Role() == e.Side { + mr.StoreProviderMetadata(e, metadataMap[e].(*definition.ServiceDefinition)) + } else if common.RoleType(common.CONSUMER).Role() == e.Side { + mr.StoreConsumerMetadata(e, metadataMap[e].(map[string]string)) + } + } + return false +} diff --git a/metadata/report/delegate/delegate_report_test.go b/metadata/report/delegate/delegate_report_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3dfca577ba06598b90c553048777951c8823b256 --- /dev/null +++ b/metadata/report/delegate/delegate_report_test.go @@ -0,0 +1,123 @@ +/* + * 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 delegate + +import ( + "fmt" + "testing" + "time" +) + +import ( + "github.com/stretchr/testify/assert" + "go.uber.org/atomic" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config/instance" + "github.com/apache/dubbo-go/metadata/definition" + "github.com/apache/dubbo-go/metadata/identifier" +) + +func TestMetadataReport_MetadataReportRetry(t *testing.T) { + counter := atomic.NewInt64(1) + + retry, err := newMetadataReportRetry(1, 10, func() bool { + counter.Add(1) + return true + }) + assert.NoError(t, err) + retry.startRetryTask() + itsTime := time.After(2500 * time.Millisecond) + select { + case <-itsTime: + retry.scheduler.Clear() + assert.Equal(t, counter.Load(), int64(3)) + logger.Info("over") + } +} + +func TestMetadataReport_MetadataReportRetryWithLimit(t *testing.T) { + counter := atomic.NewInt64(1) + + retry, err := newMetadataReportRetry(1, 1, func() bool { + counter.Add(1) + return true + }) + assert.NoError(t, err) + retry.startRetryTask() + itsTime := time.After(2500 * time.Millisecond) + select { + case <-itsTime: + retry.scheduler.Clear() + assert.Equal(t, counter.Load(), int64(2)) + logger.Info("over") + } +} + +func mockNewMetadataReport(t *testing.T) *MetadataReport { + syncReportKey := "false" + retryPeriodKey := "3" + retryTimesKey := "100" + cycleReportKey := "true" + + url, err := common.NewURL(fmt.Sprintf( + "test://127.0.0.1:20000/?"+constant.SYNC_REPORT_KEY+"=%v&"+constant.RETRY_PERIOD_KEY+"=%v&"+ + constant.RETRY_TIMES_KEY+"=%v&"+constant.CYCLE_REPORT_KEY+"=%v", + syncReportKey, retryPeriodKey, retryTimesKey, cycleReportKey)) + assert.NoError(t, err) + instance.SetMetadataReportUrl(url) + mtr, err := NewMetadataReport() + assert.NoError(t, err) + assert.NotNil(t, mtr) + return mtr +} + +func TestMetadataReport_StoreProviderMetadata(t *testing.T) { + mtr := mockNewMetadataReport(t) + var metadataId = &identifier.MetadataIdentifier{ + Application: "app", + BaseMetadataIdentifier: identifier.BaseMetadataIdentifier{ + ServiceInterface: "com.ikurento.user.UserProvider", + Version: "0.0.1", + Group: "group1", + Side: "provider", + }, + } + + mtr.StoreProviderMetadata(metadataId, getMockDefinition(metadataId, t)) +} + +func getMockDefinition(id *identifier.MetadataIdentifier, t *testing.T) *definition.ServiceDefinition { + protocol := "dubbo" + beanName := "UserProvider" + url, err := common.NewURL(fmt.Sprintf( + "%v://127.0.0.1:20000/com.ikurento.user.UserProvider1?anyhost=true&"+ + "application=BDTService&category=providers&default.timeout=10000&dubbo=dubbo-provider-golang-1.0.0&"+ + "environment=dev&interface=%v&ip=192.168.56.1&methods=GetUser&module=dubbogo+user-info+server&org=ikurento.com&"+ + "owner=ZX&pid=1447&revision=0.0.1&side=provider&timeout=3000×tamp=1556509797245&group=%v&version=%v&bean.name=%v", + protocol, id.ServiceInterface, id.Group, id.Version, beanName)) + assert.NoError(t, err) + _, err = common.ServiceMap.Register(id.ServiceInterface, protocol, &definition.UserProvider{}) + assert.NoError(t, err) + service := common.ServiceMap.GetService(url.Protocol, url.GetParam(constant.BEAN_NAME_KEY, url.Service())) + return definition.BuildServiceDefinition(*service, url) +} diff --git a/metadata/report/etcd/report.go b/metadata/report/etcd/report.go new file mode 100644 index 0000000000000000000000000000000000000000..097835c986962850d137255ec807b417de1484ad --- /dev/null +++ b/metadata/report/etcd/report.go @@ -0,0 +1,142 @@ +/* + * 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 etcd + +import ( + "strings" + "time" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" + "github.com/apache/dubbo-go/metadata/report/factory" + "github.com/apache/dubbo-go/remoting/etcdv3" +) + +const DEFAULT_ROOT = "dubbo" + +func init() { + extension.SetMetadataReportFactory(constant.ETCDV3_KEY, func() factory.MetadataReportFactory { + return &etcdMetadataReportFactory{} + }) +} + +// etcdMetadataReport is the implementation of MetadataReport based etcd +type etcdMetadataReport struct { + client *etcdv3.Client + root string +} + +// StoreProviderMetadata will store the metadata +// metadata including the basic info of the server, provider info, and other user custom info +func (e *etcdMetadataReport) StoreProviderMetadata(providerIdentifier *identifier.MetadataIdentifier, serviceDefinitions string) error { + key := e.getNodeKey(providerIdentifier) + return e.client.Create(key, serviceDefinitions) +} + +// StoreConsumerMetadata will store the metadata +// metadata including the basic info of the server, consumer info, and other user custom info +func (e *etcdMetadataReport) StoreConsumerMetadata(consumerMetadataIdentifier *identifier.MetadataIdentifier, serviceParameterString string) error { + key := e.getNodeKey(consumerMetadataIdentifier) + return e.client.Create(key, serviceParameterString) +} + +// SaveServiceMetadata will store the metadata +// metadata including the basic info of the server, service info, and other user custom info +func (e *etcdMetadataReport) SaveServiceMetadata(metadataIdentifier *identifier.ServiceMetadataIdentifier, url common.URL) error { + key := e.getNodeKey(metadataIdentifier) + return e.client.Create(key, url.String()) +} + +// RemoveServiceMetadata will remove the service metadata +func (e *etcdMetadataReport) RemoveServiceMetadata(metadataIdentifier *identifier.ServiceMetadataIdentifier) error { + return e.client.Delete(e.getNodeKey(metadataIdentifier)) +} + +// GetExportedURLs will look up the exported urls. +// if not found, an empty list will be returned. +func (e *etcdMetadataReport) GetExportedURLs(metadataIdentifier *identifier.ServiceMetadataIdentifier) ([]string, error) { + content, err := e.client.Get(e.getNodeKey(metadataIdentifier)) + if err != nil { + logger.Errorf("etcdMetadataReport GetExportedURLs err:{%v}", err.Error()) + return []string{}, err + } + if content == "" { + return []string{}, nil + } + return []string{content}, nil +} + +// SaveSubscribedData will convert the urlList to json array and then store it +func (e *etcdMetadataReport) SaveSubscribedData(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier, urls string) error { + key := e.getNodeKey(subscriberMetadataIdentifier) + return e.client.Create(key, urls) +} + +// GetSubscribedURLs will lookup the url +// if not found, an empty list will be returned +func (e *etcdMetadataReport) GetSubscribedURLs(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier) ([]string, error) { + content, err := e.client.Get(e.getNodeKey(subscriberMetadataIdentifier)) + if err != nil { + logger.Errorf("etcdMetadataReport GetSubscribedURLs err:{%v}", err.Error()) + return nil, err + } + return []string{content}, nil +} + +// GetServiceDefinition will lookup the service definition +func (e *etcdMetadataReport) GetServiceDefinition(metadataIdentifier *identifier.MetadataIdentifier) (string, error) { + key := e.getNodeKey(metadataIdentifier) + content, err := e.client.Get(key) + if err != nil { + logger.Errorf("etcdMetadataReport GetServiceDefinition err:{%v}", err.Error()) + return "", err + } + return content, nil +} + +type etcdMetadataReportFactory struct{} + +// CreateMetadataReport get the MetadataReport instance of etcd +func (e *etcdMetadataReportFactory) CreateMetadataReport(url *common.URL) report.MetadataReport { + timeout, _ := time.ParseDuration(url.GetParam(constant.REGISTRY_TIMEOUT_KEY, constant.DEFAULT_REG_TIMEOUT)) + addresses := strings.Split(url.Location, ",") + client, err := etcdv3.NewClient(etcdv3.MetadataETCDV3Client, addresses, timeout, 1) + if err != nil { + logger.Errorf("Could not create etcd metadata report. URL: %s,error:{%v}", url.String(), err) + return nil + } + group := url.GetParam(constant.GROUP_KEY, DEFAULT_ROOT) + group = constant.PATH_SEPARATOR + strings.TrimPrefix(group, constant.PATH_SEPARATOR) + return &etcdMetadataReport{client: client, root: group} +} + +func (e *etcdMetadataReport) getNodeKey(MetadataIdentifier identifier.IMetadataIdentifier) string { + var rootDir string + if e.root == constant.PATH_SEPARATOR { + rootDir = e.root + } else { + rootDir = e.root + constant.PATH_SEPARATOR + } + return rootDir + MetadataIdentifier.GetFilePathKey() +} diff --git a/metadata/report/etcd/report_test.go b/metadata/report/etcd/report_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5dd8780b48a741a8eb8735b059bc3617a100f63d --- /dev/null +++ b/metadata/report/etcd/report_test.go @@ -0,0 +1,133 @@ +/* + * 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 etcd + +import ( + "encoding/json" + "net/url" + "strconv" + "testing" +) + +import ( + "github.com/coreos/etcd/embed" + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/metadata/identifier" +) + +const defaultEtcdV3WorkDir = "/tmp/default-dubbo-go-registry.etcd" + +func initEtcd(t *testing.T) *embed.Etcd { + DefaultListenPeerURLs := "http://localhost:2380" + DefaultListenClientURLs := "http://localhost:2379" + lpurl, _ := url.Parse(DefaultListenPeerURLs) + lcurl, _ := url.Parse(DefaultListenClientURLs) + cfg := embed.NewConfig() + cfg.LPUrls = []url.URL{*lpurl} + cfg.LCUrls = []url.URL{*lcurl} + cfg.Dir = defaultEtcdV3WorkDir + e, err := embed.StartEtcd(cfg) + if err != nil { + t.Fatal(err) + } + return e +} + +func TestEtcdMetadataReportFactory_CreateMetadataReport(t *testing.T) { + e := initEtcd(t) + url, err := common.NewURL("registry://127.0.0.1:2379", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER))) + if err != nil { + t.Fatal(err) + } + metadataReportFactory := &etcdMetadataReportFactory{} + metadataReport := metadataReportFactory.CreateMetadataReport(&url) + assert.NotNil(t, metadataReport) + e.Close() +} + +func TestEtcdMetadataReport_CRUD(t *testing.T) { + e := initEtcd(t) + url, err := common.NewURL("registry://127.0.0.1:2379", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER))) + if err != nil { + t.Fatal(err) + } + metadataReportFactory := &etcdMetadataReportFactory{} + metadataReport := metadataReportFactory.CreateMetadataReport(&url) + assert.NotNil(t, metadataReport) + + err = metadataReport.StoreConsumerMetadata(newMetadataIdentifier("consumer"), "consumer metadata") + assert.Nil(t, err) + + err = metadataReport.StoreProviderMetadata(newMetadataIdentifier("provider"), "provider metadata") + assert.Nil(t, err) + + serviceMi := newServiceMetadataIdentifier() + serviceUrl, _ := common.NewURL("registry://localhost:8848", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER))) + metadataReport.SaveServiceMetadata(serviceMi, serviceUrl) + assert.Nil(t, err) + + subMi := newSubscribeMetadataIdentifier() + urlList := make([]string, 0, 1) + urlList = append(urlList, serviceUrl.String()) + urls, _ := json.Marshal(urlList) + err = metadataReport.SaveSubscribedData(subMi, string(urls)) + assert.Nil(t, err) + + err = metadataReport.RemoveServiceMetadata(serviceMi) + assert.Nil(t, err) + + e.Close() +} + +func newSubscribeMetadataIdentifier() *identifier.SubscriberMetadataIdentifier { + return &identifier.SubscriberMetadataIdentifier{ + Revision: "subscribe", + MetadataIdentifier: *newMetadataIdentifier("provider"), + } + +} + +func newServiceMetadataIdentifier() *identifier.ServiceMetadataIdentifier { + return &identifier.ServiceMetadataIdentifier{ + Protocol: "nacos", + Revision: "a", + BaseMetadataIdentifier: identifier.BaseMetadataIdentifier{ + ServiceInterface: "com.test.MyTest", + Version: "1.0.0", + Group: "test_group", + Side: "service", + }, + } +} + +func newMetadataIdentifier(side string) *identifier.MetadataIdentifier { + return &identifier.MetadataIdentifier{ + Application: "test", + BaseMetadataIdentifier: identifier.BaseMetadataIdentifier{ + ServiceInterface: "com.test.MyTest", + Version: "1.0.0", + Group: "test_group", + Side: side, + }, + } +} diff --git a/metadata/report_factory.go b/metadata/report/factory/report_factory.go similarity index 79% rename from metadata/report_factory.go rename to metadata/report/factory/report_factory.go index 19b1004eee57073acec13c7f114179c47c73f145..9f00007cefbd5737c9c53d69924eba1d556c0023 100644 --- a/metadata/report_factory.go +++ b/metadata/report/factory/report_factory.go @@ -15,16 +15,17 @@ * limitations under the License. */ -package metadata +package factory import ( "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/metadata/report" ) -var ( - MetadataReportInstance MetadataReport -) - +// MetadataReportFactory interface will create metadata report type MetadataReportFactory interface { - CreateMetadataReport(*common.URL) MetadataReport + CreateMetadataReport(*common.URL) report.MetadataReport +} + +type BaseMetadataReportFactory struct { } diff --git a/metadata/report/nacos/report.go b/metadata/report/nacos/report.go new file mode 100644 index 0000000000000000000000000000000000000000..d69913bd8fbb04da2d50770c1196917cb1efdaa5 --- /dev/null +++ b/metadata/report/nacos/report.go @@ -0,0 +1,187 @@ +/* + * 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 nacos + +import ( + "net/url" +) + +import ( + "github.com/nacos-group/nacos-sdk-go/clients/config_client" + "github.com/nacos-group/nacos-sdk-go/vo" + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" + "github.com/apache/dubbo-go/metadata/report/factory" + "github.com/apache/dubbo-go/remoting/nacos" +) + +func init() { + mf := &nacosMetadataReportFactory{} + extension.SetMetadataReportFactory("nacos", func() factory.MetadataReportFactory { + return mf + }) +} + +// nacosMetadataReport is the implementation +// of MetadataReport based on nacos. +type nacosMetadataReport struct { + client config_client.IConfigClient +} + +// StoreProviderMetadata stores the metadata. +func (n *nacosMetadataReport) StoreProviderMetadata(providerIdentifier *identifier.MetadataIdentifier, serviceDefinitions string) error { + return n.storeMetadata(vo.ConfigParam{ + DataId: providerIdentifier.GetIdentifierKey(), + Group: providerIdentifier.Group, + Content: serviceDefinitions, + }) +} + +// StoreConsumerMetadata stores the metadata. +func (n *nacosMetadataReport) StoreConsumerMetadata(consumerMetadataIdentifier *identifier.MetadataIdentifier, serviceParameterString string) error { + return n.storeMetadata(vo.ConfigParam{ + DataId: consumerMetadataIdentifier.GetIdentifierKey(), + Group: consumerMetadataIdentifier.Group, + Content: serviceParameterString, + }) +} + +// SaveServiceMetadata saves the metadata. +func (n *nacosMetadataReport) SaveServiceMetadata(metadataIdentifier *identifier.ServiceMetadataIdentifier, url common.URL) error { + return n.storeMetadata(vo.ConfigParam{ + DataId: metadataIdentifier.GetIdentifierKey(), + Group: metadataIdentifier.Group, + Content: url.String(), + }) +} + +// RemoveServiceMetadata removes the metadata. +func (n *nacosMetadataReport) RemoveServiceMetadata(metadataIdentifier *identifier.ServiceMetadataIdentifier) error { + return n.deleteMetadata(vo.ConfigParam{ + DataId: metadataIdentifier.GetIdentifierKey(), + Group: metadataIdentifier.Group, + }) +} + +// GetExportedURLs gets the urls. +func (n *nacosMetadataReport) GetExportedURLs(metadataIdentifier *identifier.ServiceMetadataIdentifier) ([]string, error) { + return n.getConfigAsArray(vo.ConfigParam{ + DataId: metadataIdentifier.GetIdentifierKey(), + Group: metadataIdentifier.Group, + }) +} + +// SaveSubscribedData saves the urls. +func (n *nacosMetadataReport) SaveSubscribedData(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier, urls string) error { + return n.storeMetadata(vo.ConfigParam{ + DataId: subscriberMetadataIdentifier.GetIdentifierKey(), + Group: subscriberMetadataIdentifier.Group, + Content: urls, + }) +} + +// GetSubscribedURLs gets the urls. +func (n *nacosMetadataReport) GetSubscribedURLs(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier) ([]string, error) { + return n.getConfigAsArray(vo.ConfigParam{ + DataId: subscriberMetadataIdentifier.GetIdentifierKey(), + Group: subscriberMetadataIdentifier.Group, + }) +} + +// GetServiceDefinition gets the service definition. +func (n *nacosMetadataReport) GetServiceDefinition(metadataIdentifier *identifier.MetadataIdentifier) (string, error) { + return n.getConfig(vo.ConfigParam{ + DataId: metadataIdentifier.GetIdentifierKey(), + Group: metadataIdentifier.Group, + }) +} + +// storeMetadata will publish the metadata to Nacos +// if failed or error is not nil, error will be returned +func (n *nacosMetadataReport) storeMetadata(param vo.ConfigParam) error { + res, err := n.client.PublishConfig(param) + if err != nil { + return perrors.WithMessage(err, "Could not publish the metadata") + } + if !res { + return perrors.New("Publish the metadata failed.") + } + return nil +} + +// deleteMetadata will delete the metadata +func (n *nacosMetadataReport) deleteMetadata(param vo.ConfigParam) error { + res, err := n.client.DeleteConfig(param) + if err != nil { + return perrors.WithMessage(err, "Could not delete the metadata") + } + if !res { + return perrors.New("Deleting the metadata failed.") + } + return nil +} + +// getConfigAsArray will read the config and then convert it as an one-element array +// error or config not found, an empty list will be returned. +func (n *nacosMetadataReport) getConfigAsArray(param vo.ConfigParam) ([]string, error) { + res := make([]string, 0, 1) + + cfg, err := n.getConfig(param) + if err != nil || len(cfg) == 0 { + return res, err + } + + decodeCfg, err := url.QueryUnescape(cfg) + if err != nil { + logger.Errorf("The config is invalid: %s", cfg) + return res, err + } + + res = append(res, decodeCfg) + return res, nil +} + +// getConfig will read the config +func (n *nacosMetadataReport) getConfig(param vo.ConfigParam) (string, error) { + cfg, err := n.client.GetConfig(param) + if err != nil { + logger.Errorf("Finding the configuration failed: %v", param) + return "", err + } + return cfg, nil +} + +type nacosMetadataReportFactory struct { +} + +// nolint +func (n *nacosMetadataReportFactory) CreateMetadataReport(url *common.URL) report.MetadataReport { + client, err := nacos.NewNacosConfigClient(url) + if err != nil { + logger.Errorf("Could not create nacos metadata report. URL: %s", url.String()) + return nil + } + return &nacosMetadataReport{client: client} +} diff --git a/metadata/report/nacos/report_test.go b/metadata/report/nacos/report_test.go new file mode 100644 index 0000000000000000000000000000000000000000..be01eb22f7e95966c3bf816fdf648629b64380a3 --- /dev/null +++ b/metadata/report/nacos/report_test.go @@ -0,0 +1,116 @@ +/* + * 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 nacos + +import ( + "encoding/json" + "strconv" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" +) + +func TestNacosMetadataReport_CRUD(t *testing.T) { + rpt := newTestReport() + assert.NotNil(t, rpt) + + providerMi := newMetadataIdentifier("server") + providerMeta := "provider" + err := rpt.StoreProviderMetadata(providerMi, providerMeta) + assert.Nil(t, err) + + consumerMi := newMetadataIdentifier("client") + consumerMeta := "consumer" + err = rpt.StoreConsumerMetadata(consumerMi, consumerMeta) + assert.Nil(t, err) + + serviceMi := newServiceMetadataIdentifier() + serviceUrl, _ := common.NewURL("registry://console.nacos.io:80", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER))) + err = rpt.SaveServiceMetadata(serviceMi, serviceUrl) + assert.Nil(t, err) + + exportedUrls, err := rpt.GetExportedURLs(serviceMi) + assert.Equal(t, 1, len(exportedUrls)) + assert.Nil(t, err) + + subMi := newSubscribeMetadataIdentifier() + urls := []string{serviceUrl.String()} + bytes, _ := json.Marshal(urls) + err = rpt.SaveSubscribedData(subMi, string(bytes)) + assert.Nil(t, err) + + subscribeUrl, err := rpt.GetSubscribedURLs(subMi) + assert.Equal(t, 1, len(subscribeUrl)) + assert.Nil(t, err) + + err = rpt.RemoveServiceMetadata(serviceMi) + assert.Nil(t, err) +} + +func newSubscribeMetadataIdentifier() *identifier.SubscriberMetadataIdentifier { + return &identifier.SubscriberMetadataIdentifier{ + Revision: "subscribe", + MetadataIdentifier: *newMetadataIdentifier("provider"), + } +} + +func newServiceMetadataIdentifier() *identifier.ServiceMetadataIdentifier { + return &identifier.ServiceMetadataIdentifier{ + Protocol: "nacos", + Revision: "a", + BaseMetadataIdentifier: identifier.BaseMetadataIdentifier{ + ServiceInterface: "com.test.MyTest", + Version: "1.0.0", + Group: "test_group", + Side: "service", + }, + } +} + +func newMetadataIdentifier(side string) *identifier.MetadataIdentifier { + return &identifier.MetadataIdentifier{ + Application: "test", + BaseMetadataIdentifier: identifier.BaseMetadataIdentifier{ + ServiceInterface: "com.test.MyTest", + Version: "1.0.0", + Group: "test_group", + Side: side, + }, + } +} + +func TestNacosMetadataReportFactory_CreateMetadataReport(t *testing.T) { + res := newTestReport() + assert.NotNil(t, res) +} + +func newTestReport() report.MetadataReport { + regurl, _ := common.NewURL("registry://console.nacos.io:80", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER))) + res := extension.GetMetadataReportFactory("nacos").CreateMetadataReport(®url) + return res +} diff --git a/metadata/report/report.go b/metadata/report/report.go new file mode 100644 index 0000000000000000000000000000000000000000..62a9055e843297bd0d69ad94cb09ece64efda85f --- /dev/null +++ b/metadata/report/report.go @@ -0,0 +1,60 @@ +/* + * 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 report + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/metadata/identifier" +) + +// MetadataReport is an interface of +// remote metadata report. +type MetadataReport interface { + // StoreProviderMetadata stores the metadata. + // Metadata includes the basic info of the server, + // provider info, and other user custom info. + StoreProviderMetadata(*identifier.MetadataIdentifier, string) error + + // StoreConsumerMetadata stores the metadata. + // Metadata includes the basic info of the server, + // consumer info, and other user custom info. + StoreConsumerMetadata(*identifier.MetadataIdentifier, string) error + + // SaveServiceMetadata saves the metadata. + // Metadata includes the basic info of the server, + // service info, and other user custom info. + SaveServiceMetadata(*identifier.ServiceMetadataIdentifier, common.URL) error + + // RemoveServiceMetadata removes the metadata. + RemoveServiceMetadata(*identifier.ServiceMetadataIdentifier) error + + // GetExportedURLs gets the urls. + // If not found, an empty list will be returned. + GetExportedURLs(*identifier.ServiceMetadataIdentifier) ([]string, error) + + // SaveSubscribedData saves the urls. + // If not found, an empty str will be returned. + SaveSubscribedData(*identifier.SubscriberMetadataIdentifier, string) error + + // GetSubscribedURLs gets the urls. + // If not found, an empty list will be returned. + GetSubscribedURLs(*identifier.SubscriberMetadataIdentifier) ([]string, error) + + // GetServiceDefinition gets the service definition. + GetServiceDefinition(*identifier.MetadataIdentifier) (string, error) +} diff --git a/metadata/report/zookeeper/report.go b/metadata/report/zookeeper/report.go new file mode 100644 index 0000000000000000000000000000000000000000..8f46bb023054ec27ca0dbff2c249dc0135af07cd --- /dev/null +++ b/metadata/report/zookeeper/report.go @@ -0,0 +1,133 @@ +/* + * 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 zookeeper + +import ( + "strings" + "time" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" + "github.com/apache/dubbo-go/metadata/report/factory" + "github.com/apache/dubbo-go/remoting/zookeeper" +) + +var ( + emptyStrSlice = make([]string, 0) +) + +func init() { + mf := &zookeeperMetadataReportFactory{} + extension.SetMetadataReportFactory("zookeeper", func() factory.MetadataReportFactory { + return mf + }) +} + +// zookeeperMetadataReport is the implementation of +// MetadataReport based on zookeeper. +type zookeeperMetadataReport struct { + client *zookeeper.ZookeeperClient + rootDir string +} + +// StoreProviderMetadata stores the metadata. +func (m *zookeeperMetadataReport) StoreProviderMetadata(providerIdentifier *identifier.MetadataIdentifier, serviceDefinitions string) error { + k := m.rootDir + providerIdentifier.GetFilePathKey() + return m.client.CreateWithValue(k, []byte(serviceDefinitions)) +} + +// StoreConsumerMetadata stores the metadata. +func (m *zookeeperMetadataReport) StoreConsumerMetadata(consumerMetadataIdentifier *identifier.MetadataIdentifier, serviceParameterString string) error { + k := m.rootDir + consumerMetadataIdentifier.GetFilePathKey() + return m.client.CreateWithValue(k, []byte(serviceParameterString)) +} + +// SaveServiceMetadata saves the metadata. +func (m *zookeeperMetadataReport) SaveServiceMetadata(metadataIdentifier *identifier.ServiceMetadataIdentifier, url common.URL) error { + k := m.rootDir + metadataIdentifier.GetFilePathKey() + return m.client.CreateWithValue(k, []byte(url.String())) +} + +// RemoveServiceMetadata removes the metadata. +func (m *zookeeperMetadataReport) RemoveServiceMetadata(metadataIdentifier *identifier.ServiceMetadataIdentifier) error { + k := m.rootDir + metadataIdentifier.GetFilePathKey() + return m.client.Delete(k) +} + +// GetExportedURLs gets the urls. +func (m *zookeeperMetadataReport) GetExportedURLs(metadataIdentifier *identifier.ServiceMetadataIdentifier) ([]string, error) { + k := m.rootDir + metadataIdentifier.GetFilePathKey() + v, _, err := m.client.GetContent(k) + if err != nil || len(v) == 0 { + return emptyStrSlice, err + } + return []string{string(v)}, nil +} + +// SaveSubscribedData saves the urls. +func (m *zookeeperMetadataReport) SaveSubscribedData(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier, urls string) error { + k := m.rootDir + subscriberMetadataIdentifier.GetFilePathKey() + return m.client.CreateWithValue(k, []byte(urls)) +} + +// GetSubscribedURLs gets the urls. +func (m *zookeeperMetadataReport) GetSubscribedURLs(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier) ([]string, error) { + k := m.rootDir + subscriberMetadataIdentifier.GetFilePathKey() + v, _, err := m.client.GetContent(k) + if err != nil || len(v) == 0 { + return emptyStrSlice, err + } + return []string{string(v)}, nil +} + +// GetServiceDefinition gets the service definition. +func (m *zookeeperMetadataReport) GetServiceDefinition(metadataIdentifier *identifier.MetadataIdentifier) (string, error) { + k := m.rootDir + metadataIdentifier.GetFilePathKey() + v, _, err := m.client.GetContent(k) + return string(v), err +} + +type zookeeperMetadataReportFactory struct { +} + +// nolint +func (mf *zookeeperMetadataReportFactory) CreateMetadataReport(url *common.URL) report.MetadataReport { + client, err := zookeeper.NewZookeeperClient( + "zookeeperMetadataReport", + strings.Split(url.Location, ","), + 15*time.Second, + ) + if err != nil { + panic(err) + } + + rootDir := url.GetParam(constant.GROUP_KEY, "dubbo") + if !strings.HasPrefix(rootDir, constant.PATH_SEPARATOR) { + rootDir = constant.PATH_SEPARATOR + rootDir + } + if rootDir != constant.PATH_SEPARATOR { + rootDir = rootDir + constant.PATH_SEPARATOR + } + + return &zookeeperMetadataReport{client: client, rootDir: rootDir} +} diff --git a/metadata/report/zookeeper/report_test.go b/metadata/report/zookeeper/report_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a1e46e2e8d019c0415699ee409833b392a85b504 --- /dev/null +++ b/metadata/report/zookeeper/report_test.go @@ -0,0 +1,166 @@ +/* + * 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 zookeeper + +import ( + "encoding/json" + "net/url" + "strconv" + "testing" +) + +import ( + "github.com/dubbogo/go-zookeeper/zk" + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" +) + +func newProviderRegistryUrl(host string, port int) *common.URL { + return common.NewURLWithOptions( + common.WithIp(host), + common.WithPort(strconv.Itoa(port)), + common.WithParams(url.Values{}), + common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER)), + ) +} + +func newBaseMetadataIdentifier(side string) *identifier.BaseMetadataIdentifier { + return &identifier.BaseMetadataIdentifier{ + ServiceInterface: "org.apache.HelloWorld", + Version: "1.0.0", + Group: "group", + Side: side, + } +} + +func newMetadataIdentifier(side string) *identifier.MetadataIdentifier { + return &identifier.MetadataIdentifier{ + Application: "application", + BaseMetadataIdentifier: *newBaseMetadataIdentifier(side), + } +} + +func newServiceMetadataIdentifier(side string) *identifier.ServiceMetadataIdentifier { + return &identifier.ServiceMetadataIdentifier{ + Revision: "1.0", + Protocol: "dubbo", + BaseMetadataIdentifier: *newBaseMetadataIdentifier(side), + } +} + +func newSubscribeMetadataIdentifier(side string) *identifier.SubscriberMetadataIdentifier { + return &identifier.SubscriberMetadataIdentifier{ + Revision: "1.0", + MetadataIdentifier: *newMetadataIdentifier(side), + } +} + +type zookeeperMetadataReportTestSuite struct { + t *testing.T + m report.MetadataReport +} + +func newZookeeperMetadataReportTestSuite(t *testing.T, m report.MetadataReport) *zookeeperMetadataReportTestSuite { + return &zookeeperMetadataReportTestSuite{t: t, m: m} +} + +func (suite *zookeeperMetadataReportTestSuite) testStoreProviderMetadata() { + providerMi := newMetadataIdentifier("provider") + providerMeta := "provider" + err := suite.m.StoreProviderMetadata(providerMi, providerMeta) + assert.NoError(suite.t, err) +} + +func (suite *zookeeperMetadataReportTestSuite) testStoreConsumerMetadata() { + consumerMi := newMetadataIdentifier("consumer") + consumerMeta := "consumer" + err := suite.m.StoreProviderMetadata(consumerMi, consumerMeta) + assert.NoError(suite.t, err) +} + +func (suite *zookeeperMetadataReportTestSuite) testSaveServiceMetadata(url common.URL) { + serviceMi := newServiceMetadataIdentifier("provider") + err := suite.m.SaveServiceMetadata(serviceMi, url) + assert.NoError(suite.t, err) +} + +func (suite *zookeeperMetadataReportTestSuite) testRemoveServiceMetadata() { + serviceMi := newServiceMetadataIdentifier("provider") + err := suite.m.RemoveServiceMetadata(serviceMi) + assert.NoError(suite.t, err) +} + +func (suite *zookeeperMetadataReportTestSuite) testGetExportedURLs() { + serviceMi := newServiceMetadataIdentifier("provider") + urls, err := suite.m.GetExportedURLs(serviceMi) + assert.Equal(suite.t, 1, len(urls)) + assert.NoError(suite.t, err) +} + +func (suite *zookeeperMetadataReportTestSuite) testSaveSubscribedData(url common.URL) { + subscribeMi := newSubscribeMetadataIdentifier("provider") + urls := []string{url.String()} + bytes, _ := json.Marshal(urls) + err := suite.m.SaveSubscribedData(subscribeMi, string(bytes)) + assert.Nil(suite.t, err) +} + +func (suite *zookeeperMetadataReportTestSuite) testGetSubscribedURLs() { + subscribeMi := newSubscribeMetadataIdentifier("provider") + urls, err := suite.m.GetSubscribedURLs(subscribeMi) + assert.Equal(suite.t, 1, len(urls)) + assert.NoError(suite.t, err) +} + +func (suite *zookeeperMetadataReportTestSuite) testGetServiceDefinition() { + providerMi := newMetadataIdentifier("provider") + providerMeta, err := suite.m.GetServiceDefinition(providerMi) + assert.Equal(suite.t, "provider", providerMeta) + assert.NoError(suite.t, err) +} + +func test1(t *testing.T) { + testCluster, err := zk.StartTestCluster(1, nil, nil) + assert.NoError(t, err) + defer testCluster.Stop() + + url := newProviderRegistryUrl("127.0.0.1", testCluster.Servers[0].Port) + mf := extension.GetMetadataReportFactory("zookeeper") + m := mf.CreateMetadataReport(url) + + suite := newZookeeperMetadataReportTestSuite(t, m) + suite.testStoreProviderMetadata() + suite.testStoreConsumerMetadata() + suite.testSaveServiceMetadata(*url) + suite.testGetExportedURLs() + suite.testRemoveServiceMetadata() + suite.testSaveSubscribedData(*url) + suite.testGetSubscribedURLs() + suite.testGetServiceDefinition() +} + +func TestZookeeperMetadataReport(t *testing.T) { + t.Run("test1", test1) +} diff --git a/metadata/service.go b/metadata/service.go deleted file mode 100644 index 89df68fb313b1abe63082c0c220b0114c11fca19..0000000000000000000000000000000000000000 --- a/metadata/service.go +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 metadata - -import ( - "github.com/apache/dubbo-go/common" - gxset "github.com/dubbogo/gost/container/set" -) - -// Metadata service is a built-in service around the metadata of Dubbo services, -// whose interface is provided by Dubbo Framework and exported automatically before subscription after other services exporting, -// which may be used for Dubbo subscribers and admin. -type MetadataService interface { - ServiceName() string - ExportURL(url *common.URL) bool - UnexportURL(url *common.URL) bool - RefreshMetadata(exportedRevision string, subscribedRevision string) bool - SubscribeURL(url *common.URL) bool - UnsubscribeURL(url *common.URL) bool - PublishServiceDefinition(url *common.URL) - - GetExportedURLs(serviceInterface string, group string, version string, protocol string) gxset.HashSet - GetServiceDefinition(interfaceName string, version string, group string) string - GetServiceDefinitionByServiceKey(serviceKey string) string -} diff --git a/metadata/service/exporter/configurable/exporter.go b/metadata/service/exporter/configurable/exporter.go new file mode 100644 index 0000000000000000000000000000000000000000..f8b4b0c0174cb0e5a8753b814f89ed4d332e2fbe --- /dev/null +++ b/metadata/service/exporter/configurable/exporter.go @@ -0,0 +1,106 @@ +/* + * 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 configurable + +import ( + "context" + "sync" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/metadata/service/exporter" +) + +// MetadataServiceExporter is the ConfigurableMetadataServiceExporter which implement MetadataServiceExporter interface +type MetadataServiceExporter struct { + ServiceConfig *config.ServiceConfig + lock sync.RWMutex + metadataService service.MetadataService +} + +// NewMetadataServiceExporter will return a service_exporter.MetadataServiceExporter with the specified metadata service +func NewMetadataServiceExporter(metadataService service.MetadataService) exporter.MetadataServiceExporter { + return &MetadataServiceExporter{ + metadataService: metadataService, + } +} + +// Export will export the metadataService +func (exporter *MetadataServiceExporter) Export() error { + if !exporter.IsExported() { + + serviceConfig := config.NewServiceConfig(constant.SIMPLE_METADATA_SERVICE_NAME, context.Background()) + serviceConfig.Protocol = constant.DEFAULT_PROTOCOL + serviceConfig.Protocols = map[string]*config.ProtocolConfig{ + constant.DEFAULT_PROTOCOL: generateMetadataProtocol(), + } + serviceConfig.InterfaceName = constant.METADATA_SERVICE_NAME + // identify this is a golang server + serviceConfig.Params = map[string]string{} + serviceConfig.Group = config.GetApplicationConfig().Name + // now the error will always be nil + serviceConfig.Version, _ = exporter.metadataService.Version() + + var err error + func() { + exporter.lock.Lock() + defer exporter.lock.Unlock() + exporter.ServiceConfig = serviceConfig + exporter.ServiceConfig.Implement(exporter.metadataService) + err = exporter.ServiceConfig.Export() + }() + + logger.Infof("The MetadataService exports urls : %v ", exporter.ServiceConfig.GetExportedUrls()) + return err + } + logger.Warnf("The MetadataService has been exported : %v ", exporter.ServiceConfig.GetExportedUrls()) + return nil +} + +// Unexport will unexport the metadataService +func (exporter *MetadataServiceExporter) Unexport() { + if exporter.IsExported() { + exporter.ServiceConfig.Unexport() + } +} + +// GetExportedURLs will return the urls that export use. +// Noticeï¼The exported url is not same as url in registry , for example it lack the ip. +func (exporter *MetadataServiceExporter) GetExportedURLs() []*common.URL { + return exporter.ServiceConfig.GetExportedUrls() +} + +// isExported will return is metadataServiceExporter exported or not +func (exporter *MetadataServiceExporter) IsExported() bool { + exporter.lock.RLock() + defer exporter.lock.RUnlock() + return exporter.ServiceConfig != nil && exporter.ServiceConfig.IsExport() +} + +// generateMetadataProtocol will return a default ProtocolConfig +func generateMetadataProtocol() *config.ProtocolConfig { + return &config.ProtocolConfig{ + Name: constant.DEFAULT_PROTOCOL, + Port: "20000", + } +} diff --git a/metadata/service/exporter/configurable/exporter_test.go b/metadata/service/exporter/configurable/exporter_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4689c6660b7da78609501c5e98f0dd309e4bce7f --- /dev/null +++ b/metadata/service/exporter/configurable/exporter_test.go @@ -0,0 +1,122 @@ +/* + * 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 configurable + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + _ "github.com/apache/dubbo-go/common/proxy/proxy_factory" + "github.com/apache/dubbo-go/config" + _ "github.com/apache/dubbo-go/filter/filter_impl" + "github.com/apache/dubbo-go/metadata/service/inmemory" + "github.com/apache/dubbo-go/protocol/dubbo" + _ "github.com/apache/dubbo-go/protocol/dubbo" +) + +func TestConfigurableExporter(t *testing.T) { + dubbo.SetServerConfig(dubbo.ServerConfig{ + SessionNumber: 700, + SessionTimeout: "20s", + GettySessionParam: dubbo.GettySessionParam{ + CompressEncoding: false, + TcpNoDelay: true, + TcpKeepAlive: true, + KeepAlivePeriod: "120s", + TcpRBufSize: 262144, + TcpWBufSize: 65536, + PkgWQSize: 512, + TcpReadTimeout: "1s", + TcpWriteTimeout: "5s", + WaitTimeout: "1s", + MaxMsgLen: 10240000000, + SessionName: "server", + }}) + mockInitProviderWithSingleRegistry() + metadataService, _ := inmemory.NewMetadataService() + exported := NewMetadataServiceExporter(metadataService) + assert.Equal(t, false, exported.IsExported()) + assert.NoError(t, exported.Export()) + assert.Equal(t, true, exported.IsExported()) + assert.Regexp(t, "dubbo://:20000/MetadataService*", exported.GetExportedURLs()[0].String()) + exported.Unexport() + assert.Equal(t, false, exported.IsExported()) +} + +// mockInitProviderWithSingleRegistry will init a mocked providerConfig +func mockInitProviderWithSingleRegistry() { + providerConfig := &config.ProviderConfig{ + + BaseConfig: config.BaseConfig{ + ApplicationConfig: &config.ApplicationConfig{ + Organization: "dubbo_org", + Name: "dubbo", + Module: "module", + Version: "1.0.0", + Owner: "dubbo", + Environment: "test"}, + }, + + Registry: &config.RegistryConfig{ + Address: "mock://127.0.0.1:2181", + Username: "user1", + Password: "pwd1", + }, + Registries: map[string]*config.RegistryConfig{}, + + Services: map[string]*config.ServiceConfig{ + "MockService": { + InterfaceName: "com.MockService", + Protocol: "mock", + Cluster: "failover", + Loadbalance: "random", + Retries: "3", + Group: "huadong_idc", + Version: "1.0.0", + Methods: []*config.MethodConfig{ + { + Name: "GetUser", + Retries: "2", + Loadbalance: "random", + Weight: 200, + }, + { + Name: "GetUser1", + Retries: "2", + Loadbalance: "random", + Weight: 200, + }, + }, + }, + }, + Protocols: map[string]*config.ProtocolConfig{ + "mock": { + Name: "mock", + Ip: "127.0.0.1", + Port: "20000", + }, + }, + } + providerConfig.Services["MockService"].InitExported() + config.SetProviderConfig(*providerConfig) +} diff --git a/metadata/exporter.go b/metadata/service/exporter/exporter.go similarity index 81% rename from metadata/exporter.go rename to metadata/service/exporter/exporter.go index 5d47f8bd808ec802ba73c7db73d22c78c675d12a..cfdef3a0e79d29ce31717c0fc3c575e9e4ba1759 100644 --- a/metadata/exporter.go +++ b/metadata/service/exporter/exporter.go @@ -15,15 +15,16 @@ * limitations under the License. */ -package metadata +package exporter import ( "github.com/apache/dubbo-go/common" ) -type MetadataExporter interface { - Export() MetadataExporter - Unexport() MetadataExporter +// MetadataServiceExporter will export & unexport the metadata service, get exported url, and return is exported or not +type MetadataServiceExporter interface { + Export() error + Unexport() GetExportedURLs() []*common.URL IsExported() bool } diff --git a/metadata/service/inmemory/metadata_service_proxy_factory.go b/metadata/service/inmemory/metadata_service_proxy_factory.go new file mode 100644 index 0000000000000000000000000000000000000000..1f8eeaa55f4a0240746508fee2ff088e3a653ca5 --- /dev/null +++ b/metadata/service/inmemory/metadata_service_proxy_factory.go @@ -0,0 +1,97 @@ +/* + * 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 inmemory + +import ( + "encoding/json" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/registry" +) + +func init() { + factory := service.NewBaseMetadataServiceProxyFactory(createProxy) + extension.SetMetadataServiceProxyFactory(local, func() service.MetadataServiceProxyFactory { + return factory + }) +} + +// createProxy creates an instance of MetadataServiceProxy +// we read the metadata from ins.Metadata() +// and then create an Invoker instance +// also we will mark this proxy as golang's proxy +func createProxy(ins registry.ServiceInstance) service.MetadataService { + urls := buildStandardMetadataServiceURL(ins) + if len(urls) == 0 { + logger.Errorf("metadata service urls not found, %v", ins) + return nil + } + + u := urls[0] + p := extension.GetProtocol(u.Protocol) + invoker := p.Refer(*u) + return &MetadataServiceProxy{ + invkr: invoker, + } +} + +// buildStandardMetadataServiceURL will use standard format to build the metadata service url. +func buildStandardMetadataServiceURL(ins registry.ServiceInstance) []*common.URL { + ps := getMetadataServiceUrlParams(ins) + res := make([]*common.URL, 0, len(ps)) + sn := ins.GetServiceName() + host := ins.GetHost() + for protocol, params := range ps { + + convertedParams := make(map[string][]string, len(params)) + for k, v := range params { + convertedParams[k] = []string{v} + } + + u := common.NewURLWithOptions(common.WithIp(host), + common.WithPath(constant.METADATA_SERVICE_NAME), + common.WithProtocol(protocol), + common.WithPort(params[constant.PORT_KEY]), + common.WithParams(convertedParams), + common.WithParamsValue(constant.GROUP_KEY, sn)) + res = append(res, u) + } + return res +} + +// getMetadataServiceUrlParams this will convert the metadata service url parameters to map structure +// it looks like: +// {"dubbo":{"timeout":"10000","version":"1.0.0","dubbo":"2.0.2","release":"2.7.6","port":"20880"}} +func getMetadataServiceUrlParams(ins registry.ServiceInstance) map[string]map[string]string { + ps := ins.GetMetadata() + res := make(map[string]map[string]string, 2) + if str, ok := ps[constant.METADATA_SERVICE_URL_PARAMS_PROPERTY_NAME]; ok && len(str) > 0 { + + err := json.Unmarshal([]byte(str), &res) + if err != nil { + logger.Errorf("could not parse the metadata service url parameters to map", err) + } + } + return res +} diff --git a/metadata/service/inmemory/metadata_service_proxy_factory_test.go b/metadata/service/inmemory/metadata_service_proxy_factory_test.go new file mode 100644 index 0000000000000000000000000000000000000000..96020e1eb762442f946ccf8b368d6ebe9429d05e --- /dev/null +++ b/metadata/service/inmemory/metadata_service_proxy_factory_test.go @@ -0,0 +1,100 @@ +/* + * 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 inmemory + +import ( + "context" + "encoding/json" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/protocol" + "github.com/apache/dubbo-go/registry" +) + +func TestMetadataService_GetMetadataServiceUrlParams(t *testing.T) { + str := `{"dubbo":{"timeout":"10000","version":"1.0.0","dubbo":"2.0.2","release":"2.7.6","port":"20880"}}` + tmp := make(map[string]map[string]string) + err := json.Unmarshal([]byte(str), &tmp) + assert.Nil(t, err) +} + +func TestCreateProxy(t *testing.T) { + extension.SetProtocol("mock", func() protocol.Protocol { + return &mockProtocol{} + }) + ins := ®istry.DefaultServiceInstance{ + Id: "test-id", + ServiceName: "com.dubbo", + Host: "localhost", + Port: 8080, + Enable: true, + Healthy: true, + } + + pxy := createProxy(ins) + assert.Nil(t, pxy) + + ins.Metadata = map[string]string{constant.METADATA_SERVICE_URL_PARAMS_PROPERTY_NAME: `{"mock":{"timeout":"10000","version":"1.0.0","dubbo":"2.0.2","release":"2.7.6","port":"20880"}}`} + pxy = createProxy(ins) + assert.NotNil(t, pxy) +} + +type mockProtocol struct { +} + +func (m mockProtocol) Export(invoker protocol.Invoker) protocol.Exporter { + panic("implement me") +} + +func (m mockProtocol) Refer(url common.URL) protocol.Invoker { + return &mockInvoker{} +} + +func (m mockProtocol) Destroy() { + panic("implement me") +} + +type mockInvoker struct { +} + +func (m *mockInvoker) GetUrl() common.URL { + panic("implement me") +} + +func (m *mockInvoker) IsAvailable() bool { + panic("implement me") +} + +func (m *mockInvoker) Destroy() { + panic("implement me") +} + +func (m *mockInvoker) Invoke(context.Context, protocol.Invocation) protocol.Result { + return &protocol.RPCResult{ + Rest: &[]interface{}{"dubbo://localhost"}, + } +} diff --git a/metadata/service/inmemory/service.go b/metadata/service/inmemory/service.go new file mode 100644 index 0000000000000000000000000000000000000000..8269e691f1794fd9ac4b6091c157539e39ad7072 --- /dev/null +++ b/metadata/service/inmemory/service.go @@ -0,0 +1,249 @@ +/* + * 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 inmemory + +import ( + "sort" + "sync" +) + +import ( + cm "github.com/Workiva/go-datastructures/common" + "github.com/Workiva/go-datastructures/slice/skip" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/metadata/definition" + "github.com/apache/dubbo-go/metadata/service" +) + +// version will be used by Version func +const ( + version = "1.0.0" + local = "local" +) + +func init() { + extension.SetMetadataService(local, NewMetadataService) +} + +// MetadataService is store and query the metadata info in memory when each service registry +type MetadataService struct { + service.BaseMetadataService + exportedServiceURLs *sync.Map + subscribedServiceURLs *sync.Map + serviceDefinitions *sync.Map + lock *sync.RWMutex +} + +var ( + metadataServiceInstance *MetadataService + metadataServiceInitOnce sync.Once +) + +// NewMetadataService: initiate a metadata service +// it should be singleton +func NewMetadataService() (service.MetadataService, error) { + metadataServiceInitOnce.Do(func() { + metadataServiceInstance = &MetadataService{ + BaseMetadataService: service.NewBaseMetadataService(config.GetApplicationConfig().Name), + exportedServiceURLs: &sync.Map{}, + subscribedServiceURLs: &sync.Map{}, + serviceDefinitions: &sync.Map{}, + lock: &sync.RWMutex{}, + } + }) + return metadataServiceInstance, nil +} + +// Comparator is defined as Comparator for skip list to compare the URL +type Comparator common.URL + +// Compare is defined as Comparator for skip list to compare the URL +func (c Comparator) Compare(comp cm.Comparator) int { + a := common.URL(c).String() + b := common.URL(comp.(Comparator)).String() + switch { + case a > b: + return 1 + case a < b: + return -1 + default: + return 0 + } +} + +// addURL will add URL in memory +func (mts *MetadataService) addURL(targetMap *sync.Map, url *common.URL) bool { + var ( + urlSet interface{} + loaded bool + ) + logger.Debug(url.ServiceKey()) + if urlSet, loaded = targetMap.LoadOrStore(url.ServiceKey(), skip.New(uint64(0))); loaded { + mts.lock.RLock() + wantedUrl := urlSet.(*skip.SkipList).Get(Comparator(*url)) + if len(wantedUrl) > 0 && wantedUrl[0] != nil { + mts.lock.RUnlock() + return false + } + mts.lock.RUnlock() + } + mts.lock.Lock() + // double chk + wantedUrl := urlSet.(*skip.SkipList).Get(Comparator(*url)) + if len(wantedUrl) > 0 && wantedUrl[0] != nil { + mts.lock.Unlock() + return false + } + urlSet.(*skip.SkipList).Insert(Comparator(*url)) + mts.lock.Unlock() + return true +} + +// removeURL is used to remove specified url +func (mts *MetadataService) removeURL(targetMap *sync.Map, url *common.URL) { + if value, loaded := targetMap.Load(url.ServiceKey()); loaded { + mts.lock.Lock() + value.(*skip.SkipList).Delete(Comparator(*url)) + mts.lock.Unlock() + mts.lock.RLock() + defer mts.lock.RUnlock() + if value.(*skip.SkipList).Len() == 0 { + targetMap.Delete(url.ServiceKey()) + } + } +} + +// getAllService can return all the exportedUrlString except for metadataService +func (mts *MetadataService) getAllService(services *sync.Map) []common.URL { + // using skip list to dedup and sorting + res := make([]common.URL, 0) + services.Range(func(key, value interface{}) bool { + urls := value.(*skip.SkipList) + for i := uint64(0); i < urls.Len(); i++ { + url := common.URL(urls.ByPosition(i).(Comparator)) + if url.GetParam(constant.INTERFACE_KEY, url.Path) != constant.METADATA_SERVICE_NAME { + res = append(res, url) + } + } + return true + }) + sort.Sort(common.URLSlice(res)) + return res +} + +// getSpecifiedService can return specified service url by serviceKey +func (mts *MetadataService) getSpecifiedService(services *sync.Map, serviceKey string, protocol string) []common.URL { + res := make([]common.URL, 0) + serviceList, loaded := services.Load(serviceKey) + if loaded { + urls := serviceList.(*skip.SkipList) + for i := uint64(0); i < urls.Len(); i++ { + url := common.URL(urls.ByPosition(i).(Comparator)) + if len(protocol) == 0 || protocol == constant.ANY_VALUE || url.Protocol == protocol || url.GetParam(constant.PROTOCOL_KEY, "") == protocol { + res = append(res, url) + } + } + sort.Stable(common.URLSlice(res)) + } + return res +} + +// ExportURL can store the in memory +func (mts *MetadataService) ExportURL(url common.URL) (bool, error) { + return mts.addURL(mts.exportedServiceURLs, &url), nil +} + +// UnexportURL can remove the url store in memory +func (mts *MetadataService) UnexportURL(url common.URL) error { + mts.removeURL(mts.exportedServiceURLs, &url) + return nil +} + +// SubscribeURL can store the in memory +func (mts *MetadataService) SubscribeURL(url common.URL) (bool, error) { + return mts.addURL(mts.subscribedServiceURLs, &url), nil +} + +// UnsubscribeURL can remove the url store in memory +func (mts *MetadataService) UnsubscribeURL(url common.URL) error { + mts.removeURL(mts.subscribedServiceURLs, &url) + return nil +} + +// PublishServiceDefinition: publish url's service metadata info, and write into memory +func (mts *MetadataService) PublishServiceDefinition(url common.URL) error { + interfaceName := url.GetParam(constant.INTERFACE_KEY, "") + isGeneric := url.GetParamBool(constant.GENERIC_KEY, false) + if len(interfaceName) > 0 && !isGeneric { + service := common.ServiceMap.GetService(url.Protocol, url.GetParam(constant.BEAN_NAME_KEY, url.Service())) + sd := definition.BuildServiceDefinition(*service, url) + data, err := sd.ToBytes() + if err != nil { + logger.Errorf("publishProvider getServiceDescriptor error. providerUrl:%v , error:%v ", url, err) + return nil + } + mts.serviceDefinitions.Store(url.ServiceKey(), string(data)) + return nil + } + logger.Errorf("publishProvider interfaceName is empty . providerUrl:%v ", url) + return nil +} + +// GetExportedURLs get all exported urls +func (mts *MetadataService) GetExportedURLs(serviceInterface string, group string, version string, protocol string) ([]interface{}, error) { + if serviceInterface == constant.ANY_VALUE { + return service.ConvertURLArrToIntfArr(mts.getAllService(mts.exportedServiceURLs)), nil + } else { + serviceKey := definition.ServiceDescriperBuild(serviceInterface, group, version) + return service.ConvertURLArrToIntfArr(mts.getSpecifiedService(mts.exportedServiceURLs, serviceKey, protocol)), nil + } +} + +// GetSubscribedURLs get all subscribedUrl +func (mts *MetadataService) GetSubscribedURLs() ([]common.URL, error) { + return mts.getAllService(mts.subscribedServiceURLs), nil +} + +// GetServiceDefinition can get service definition by interfaceName, group and version +func (mts *MetadataService) GetServiceDefinition(interfaceName string, group string, version string) (string, error) { + serviceKey := definition.ServiceDescriperBuild(interfaceName, group, version) + v, _ := mts.serviceDefinitions.Load(serviceKey) + return v.(string), nil +} + +// GetServiceDefinition can get service definition by serviceKey +func (mts *MetadataService) GetServiceDefinitionByServiceKey(serviceKey string) (string, error) { + v, _ := mts.serviceDefinitions.Load(serviceKey) + return v.(string), nil +} + +// RefreshMetadata will always return true because it will be implement by remote service +func (mts *MetadataService) RefreshMetadata(exportedRevision string, subscribedRevision string) (bool, error) { + return true, nil +} + +// Version will return the version of metadata service +func (mts *MetadataService) Version() (string, error) { + return version, nil +} diff --git a/metadata/service/inmemory/service_proxy.go b/metadata/service/inmemory/service_proxy.go new file mode 100644 index 0000000000000000000000000000000000000000..7e01439f042a2046559188ec9df6924da0236cb1 --- /dev/null +++ b/metadata/service/inmemory/service_proxy.go @@ -0,0 +1,139 @@ +/* + * 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 inmemory + +import ( + "context" + "reflect" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/protocol" + "github.com/apache/dubbo-go/protocol/invocation" +) + +// actually it's RPC stub +// this will only be used by client-side +// if the metadata service is "local" metadata service in server side, +// which means that metadata service is RPC service too. +// so in client-side, if we want to get the metadata information, +// we must call metadata service +// this is the stub, or proxy +// for now, only GetExportedURLs need to be implemented +type MetadataServiceProxy struct { + invkr protocol.Invoker + golangServer bool +} + +func (m *MetadataServiceProxy) GetExportedURLs(serviceInterface string, group string, version string, protocol string) ([]interface{}, error) { + + siV := reflect.ValueOf(serviceInterface) + gV := reflect.ValueOf(group) + vV := reflect.ValueOf(version) + pV := reflect.ValueOf(protocol) + + const methodName = "getExportedURLs" + + inv := invocation.NewRPCInvocationWithOptions(invocation.WithMethodName(methodName), + invocation.WithArguments([]interface{}{siV.Interface(), gV.Interface(), vV.Interface(), pV.Interface()}), + invocation.WithReply(reflect.ValueOf(&[]interface{}{}).Interface()), + invocation.WithAttachments(map[string]string{constant.ASYNC_KEY: "false"}), + invocation.WithParameterValues([]reflect.Value{siV, gV, vV, pV})) + + res := m.invkr.Invoke(context.Background(), inv) + if res.Error() != nil { + logger.Errorf("could not get the metadata service from remote provider: %v", res.Error()) + return []interface{}{}, nil + } + + urlStrs := res.Result().(*[]interface{}) + + ret := make([]interface{}, 0, len(*urlStrs)) + + for _, s := range *urlStrs { + ret = append(ret, s) + } + return ret, nil +} + +func (m *MetadataServiceProxy) MethodMapper() map[string]string { + return map[string]string{} +} + +func (m *MetadataServiceProxy) Reference() string { + logger.Error("you should never invoke this implementation") + return constant.METADATA_SERVICE_NAME +} + +func (m *MetadataServiceProxy) ServiceName() (string, error) { + logger.Error("you should never invoke this implementation") + return "", nil +} + +func (m *MetadataServiceProxy) ExportURL(url common.URL) (bool, error) { + logger.Error("you should never invoke this implementation") + return false, nil +} + +func (m *MetadataServiceProxy) UnexportURL(url common.URL) error { + logger.Error("you should never invoke this implementation") + return nil +} + +func (m *MetadataServiceProxy) SubscribeURL(url common.URL) (bool, error) { + logger.Error("you should never invoke this implementation") + return false, nil +} + +func (m *MetadataServiceProxy) UnsubscribeURL(url common.URL) error { + logger.Error("you should never invoke this implementation") + return nil +} + +func (m *MetadataServiceProxy) PublishServiceDefinition(url common.URL) error { + logger.Error("you should never invoke this implementation") + return nil +} + +func (m *MetadataServiceProxy) GetSubscribedURLs() ([]common.URL, error) { + logger.Error("you should never invoke this implementation") + return []common.URL{}, nil +} + +func (m *MetadataServiceProxy) GetServiceDefinition(interfaceName string, group string, version string) (string, error) { + logger.Error("you should never invoke this implementation") + return "", nil +} + +func (m *MetadataServiceProxy) GetServiceDefinitionByServiceKey(serviceKey string) (string, error) { + logger.Error("you should never invoke this implementation") + return "", nil +} + +func (m *MetadataServiceProxy) RefreshMetadata(exportedRevision string, subscribedRevision string) (bool, error) { + logger.Error("you should never invoke this implementation") + return false, nil +} + +func (m *MetadataServiceProxy) Version() (string, error) { + logger.Error("you should never invoke this implementation") + return "", nil +} diff --git a/metadata/service/inmemory/service_proxy_test.go b/metadata/service/inmemory/service_proxy_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0d75517e418133ffbf3804ec96f061dda09b9e5e --- /dev/null +++ b/metadata/service/inmemory/service_proxy_test.go @@ -0,0 +1,82 @@ +/* + * 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 inmemory + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/protocol" + "github.com/apache/dubbo-go/registry" +) + +func TestMetadataServiceProxy_GetExportedURLs(t *testing.T) { + + pxy := createPxy() + assert.NotNil(t, pxy) + res, err := pxy.GetExportedURLs(constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE) + assert.Nil(t, err) + assert.Len(t, res, 1) + +} + +// TestNewMetadataService: those methods are not implemented +// when we implement them, adding UT +func TestNewMetadataService(t *testing.T) { + pxy := createPxy() + pxy.ServiceName() + pxy.PublishServiceDefinition(common.URL{}) + pxy.GetServiceDefinition(constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE) + pxy.Version() + pxy.GetSubscribedURLs() + pxy.UnsubscribeURL(common.URL{}) + pxy.GetServiceDefinitionByServiceKey("any") + pxy.ExportURL(common.URL{}) + pxy.SubscribeURL(common.URL{}) + pxy.MethodMapper() + pxy.UnexportURL(common.URL{}) + pxy.RefreshMetadata(constant.ANY_VALUE, constant.ANY_VALUE) + +} + +func createPxy() service.MetadataService { + extension.SetProtocol("mock", func() protocol.Protocol { + return &mockProtocol{} + }) + + ins := ®istry.DefaultServiceInstance{ + Id: "test-id", + ServiceName: "com.dubbo", + Host: "localhost", + Port: 8080, + Enable: true, + Healthy: true, + Metadata: map[string]string{constant.METADATA_SERVICE_URL_PARAMS_PROPERTY_NAME: `{"mock":{"timeout":"10000","version":"1.0.0","dubbo":"2.0.2","release":"2.7.6","port":"20880"}}`}, + } + + return extension.GetMetadataServiceProxyFactory(local).GetProxy(ins) +} diff --git a/metadata/service/inmemory/service_test.go b/metadata/service/inmemory/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..048c286fdf28fba6a15a86164df0789d421f0797 --- /dev/null +++ b/metadata/service/inmemory/service_test.go @@ -0,0 +1,95 @@ +/* + * 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 inmemory + +import ( + "fmt" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/metadata/definition" +) + +func TestMetadataService(t *testing.T) { + mts, _ := NewMetadataService() + serviceName := "com.ikurento.user.UserProvider" + group := "group1" + version := "0.0.1" + protocol := "dubbo" + beanName := "UserProvider" + + u2, err := common.NewURL(fmt.Sprintf( + "%v://127.0.0.1:20000/com.ikurento.user.UserProvider2?anyhost=true&"+ + "application=BDTService&category=providers&default.timeout=10000&dubbo=dubbo-provider-golang-1.0.0&"+ + "environment=dev&interface=%v&ip=192.168.56.1&methods=GetUser&module=dubbogo+user-info+server&org=ikurento.com&"+ + "owner=ZX&pid=1447&revision=0.0.1&side=provider&timeout=3000×tamp=1556509797245&group=%v&version=%v&bean.name=%v", + protocol, serviceName, group, version, beanName)) + assert.NoError(t, err) + mts.ExportURL(u2) + + u3, err := common.NewURL(fmt.Sprintf( + "%v://127.0.0.1:20000/com.ikurento.user.UserProvider3?anyhost=true&"+ + "application=BDTService&category=providers&default.timeout=10000&dubbo=dubbo-provider-golang-1.0.0&"+ + "environment=dev&interface=%v&ip=192.168.56.1&methods=GetUser&module=dubbogo+user-info+server&org=ikurento.com&"+ + "owner=ZX&pid=1447&revision=0.0.1&side=provider&timeout=3000×tamp=1556509797245&group=%v&version=%v&bean.name=%v", + protocol, serviceName, group, version, beanName)) + assert.NoError(t, err) + mts.ExportURL(u3) + + u, err := common.NewURL(fmt.Sprintf( + "%v://127.0.0.1:20000/com.ikurento.user.UserProvider1?anyhost=true&"+ + "application=BDTService&category=providers&default.timeout=10000&dubbo=dubbo-provider-golang-1.0.0&"+ + "environment=dev&interface=%v&ip=192.168.56.1&methods=GetUser&module=dubbogo+user-info+server&org=ikurento.com&"+ + "owner=ZX&pid=1447&revision=0.0.1&side=provider&timeout=3000×tamp=1556509797245&group=%v&version=%v&bean.name=%v", + protocol, serviceName, group, version, beanName)) + assert.NoError(t, err) + mts.ExportURL(u) + list, _ := mts.GetExportedURLs(serviceName, group, version, protocol) + assert.Equal(t, 3, len(list)) + mts.SubscribeURL(u) + + mts.SubscribeURL(u) + list2, _ := mts.GetSubscribedURLs() + assert.Equal(t, 1, len(list2)) + + mts.UnexportURL(u) + list3, _ := mts.GetExportedURLs(serviceName, group, version, protocol) + assert.Equal(t, 2, len(list3)) + + mts.UnsubscribeURL(u) + list4, _ := mts.GetSubscribedURLs() + assert.Equal(t, 0, len(list4)) + + userProvider := &definition.UserProvider{} + common.ServiceMap.Register(serviceName, protocol, userProvider) + mts.PublishServiceDefinition(u) + expected := "{\"CanonicalName\":\"com.ikurento.user.UserProvider\",\"CodeSource\":\"\"," + + "\"Methods\":[{\"Name\":\"GetUser\",\"ParameterTypes\":[\"slice\"],\"ReturnType\":\"ptr\"," + + "\"Parameters\":null}],\"Types\":null}" + def1, _ := mts.GetServiceDefinition(serviceName, group, version) + assert.Equal(t, expected, def1) + serviceKey := definition.ServiceDescriperBuild(serviceName, group, version) + def2, _ := mts.GetServiceDefinitionByServiceKey(serviceKey) + assert.Equal(t, expected, def2) +} diff --git a/metadata/service/remote/metadata_service_proxy_factory.go b/metadata/service/remote/metadata_service_proxy_factory.go new file mode 100644 index 0000000000000000000000000000000000000000..a1a8594282c581913d97586630f3e5e74305642d --- /dev/null +++ b/metadata/service/remote/metadata_service_proxy_factory.go @@ -0,0 +1,30 @@ +/* + * 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 remote + +import ( + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/metadata/service" +) + +func init() { + factory := service.NewBaseMetadataServiceProxyFactory(newMetadataServiceProxy) + extension.SetMetadataServiceProxyFactory(remote, func() service.MetadataServiceProxyFactory { + return factory + }) +} diff --git a/metadata/service/remote/service.go b/metadata/service/remote/service.go new file mode 100644 index 0000000000000000000000000000000000000000..ae83a69bef0af1614352c99c1e512a63770a0eff --- /dev/null +++ b/metadata/service/remote/service.go @@ -0,0 +1,208 @@ +/* + * 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 remote + +import ( + "sync" +) + +import ( + "go.uber.org/atomic" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/metadata/definition" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report/delegate" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/metadata/service/inmemory" +) + +// version will be used by Version func +const ( + version = "1.0.0" + remote = "remote" +) + +func init() { + extension.SetMetadataService(remote, newMetadataService) +} + +// MetadataService is a implement of metadata service which will delegate the remote metadata report +// This is singleton +type MetadataService struct { + service.BaseMetadataService + inMemoryMetadataService *inmemory.MetadataService + exportedRevision atomic.String + subscribedRevision atomic.String + delegateReport *delegate.MetadataReport +} + +var ( + metadataServiceOnce sync.Once + metadataServiceInstance *MetadataService +) + +// newMetadataService will create a new remote MetadataService instance +func newMetadataService() (service.MetadataService, error) { + var err error + metadataServiceOnce.Do(func() { + var mr *delegate.MetadataReport + mr, err = delegate.NewMetadataReport() + if err != nil { + return + } + // it will never return error + inms, _ := inmemory.NewMetadataService() + metadataServiceInstance = &MetadataService{ + BaseMetadataService: service.NewBaseMetadataService(config.GetApplicationConfig().Name), + inMemoryMetadataService: inms.(*inmemory.MetadataService), + delegateReport: mr, + } + }) + return metadataServiceInstance, err +} + +// setInMemoryMetadataService will replace the in memory metadata service by the specific param +func (mts *MetadataService) setInMemoryMetadataService(metadata *inmemory.MetadataService) { + mts.inMemoryMetadataService = metadata +} + +// ExportURL will be implemented by in memory service +func (mts *MetadataService) ExportURL(url common.URL) (bool, error) { + return mts.inMemoryMetadataService.ExportURL(url) +} + +// UnexportURL remove @url's metadata +func (mts *MetadataService) UnexportURL(url common.URL) error { + smi := identifier.NewServiceMetadataIdentifier(url) + smi.Revision = mts.exportedRevision.Load() + return mts.delegateReport.RemoveServiceMetadata(smi) +} + +// SubscribeURL will be implemented by in memory service +func (mts *MetadataService) SubscribeURL(url common.URL) (bool, error) { + return mts.inMemoryMetadataService.SubscribeURL(url) +} + +// UnsubscribeURL will be implemented by in memory service +func (mts *MetadataService) UnsubscribeURL(url common.URL) error { + return mts.UnsubscribeURL(url) +} + +// PublishServiceDefinition will call remote metadata's StoreProviderMetadata to store url info and service definition +func (mts *MetadataService) PublishServiceDefinition(url common.URL) error { + interfaceName := url.GetParam(constant.INTERFACE_KEY, "") + isGeneric := url.GetParamBool(constant.GENERIC_KEY, false) + if len(interfaceName) > 0 && !isGeneric { + sv := common.ServiceMap.GetService(url.Protocol, url.GetParam(constant.BEAN_NAME_KEY, url.Service())) + sd := definition.BuildServiceDefinition(*sv, url) + id := &identifier.MetadataIdentifier{ + BaseMetadataIdentifier: identifier.BaseMetadataIdentifier{ + ServiceInterface: interfaceName, + Version: url.GetParam(constant.VERSION_KEY, ""), + // Group: url.GetParam(constant.GROUP_KEY, constant.SERVICE_DISCOVERY_DEFAULT_GROUP), + Group: url.GetParam(constant.GROUP_KEY, constant.DUBBO), + Side: url.GetParam(constant.SIDE_KEY, "provider"), + }, + } + mts.delegateReport.StoreProviderMetadata(id, sd) + return nil + } + logger.Errorf("publishProvider interfaceName is empty . providerUrl:%v ", url) + return nil +} + +// GetExportedURLs will be implemented by in memory service +func (mts *MetadataService) GetExportedURLs(serviceInterface string, group string, version string, protocol string) ([]interface{}, error) { + return mts.inMemoryMetadataService.GetExportedURLs(serviceInterface, group, version, protocol) +} + +// GetSubscribedURLs will be implemented by in memory service +func (mts *MetadataService) GetSubscribedURLs() ([]common.URL, error) { + return mts.inMemoryMetadataService.GetSubscribedURLs() +} + +// GetServiceDefinition will be implemented by in memory service +func (mts *MetadataService) GetServiceDefinition(interfaceName string, group string, version string) (string, error) { + return mts.inMemoryMetadataService.GetServiceDefinition(interfaceName, group, version) +} + +// GetServiceDefinitionByServiceKey will be implemented by in memory service +func (mts *MetadataService) GetServiceDefinitionByServiceKey(serviceKey string) (string, error) { + return mts.inMemoryMetadataService.GetServiceDefinitionByServiceKey(serviceKey) +} + +// RefreshMetadata will refresh the exported & subscribed metadata to remote metadata report from the inmemory metadata service +func (mts *MetadataService) RefreshMetadata(exportedRevision string, subscribedRevision string) (bool, error) { + if len(exportedRevision) != 0 && exportedRevision != mts.exportedRevision.Load() { + mts.exportedRevision.Store(exportedRevision) + urls, err := mts.inMemoryMetadataService.GetExportedURLs(constant.ANY_VALUE, "", "", "") + if err != nil { + logger.Errorf("Error occur when execute remote.MetadataService.RefreshMetadata, error message is %+v", err) + return false, err + } + logger.Infof("urls length = %v", len(urls)) + for _, ui := range urls { + + u, err := common.NewURL(ui.(string)) + if err != nil { + logger.Errorf("this is not valid url string: %s ", ui.(string)) + continue + } + id := identifier.NewServiceMetadataIdentifier(u) + id.Revision = mts.exportedRevision.Load() + if err := mts.delegateReport.SaveServiceMetadata(id, u); err != nil { + logger.Errorf("Error occur when execute remote.MetadataService.RefreshMetadata, error message is %+v", err) + return false, err + } + } + } + + if len(subscribedRevision) != 0 && subscribedRevision != mts.subscribedRevision.Load() { + mts.subscribedRevision.Store(subscribedRevision) + urls, err := mts.inMemoryMetadataService.GetSubscribedURLs() + if err != nil { + logger.Errorf("Error occur when execute remote.MetadataService.RefreshMetadata, error message is %v+", err) + return false, err + } + if urls != nil && len(urls) > 0 { + id := &identifier.SubscriberMetadataIdentifier{ + MetadataIdentifier: identifier.MetadataIdentifier{ + Application: config.GetApplicationConfig().Name, + }, + Revision: subscribedRevision, + } + if err := mts.delegateReport.SaveSubscribedData(id, urls); err != nil { + logger.Errorf("Error occur when execute remote.MetadataService.RefreshMetadata, error message is %+v", err) + return false, err + } + } + } + return true, nil +} + +// Version will return the remote service version +func (MetadataService) Version() (string, error) { + return version, nil +} diff --git a/metadata/service/remote/service_proxy.go b/metadata/service/remote/service_proxy.go new file mode 100644 index 0000000000000000000000000000000000000000..eaf7a02f4a0f3a8280835940bd8da720a0bde9f5 --- /dev/null +++ b/metadata/service/remote/service_proxy.go @@ -0,0 +1,162 @@ +/* + * 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 remote + +import ( + "strings" +) +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config/instance" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/registry" +) + +type metadataServiceProxy struct { + serviceName string + revision string + report report.MetadataReport +} + +func (m *metadataServiceProxy) Reference() string { + return constant.METADATA_SERVICE_NAME +} + +func (m *metadataServiceProxy) ServiceName() (string, error) { + return m.serviceName, nil +} + +func (m *metadataServiceProxy) ExportURL(url common.URL) (bool, error) { + logger.Error("you should never invoke this implementation") + return true, nil +} + +func (m *metadataServiceProxy) UnexportURL(url common.URL) error { + logger.Error("you should never invoke this implementation") + return nil +} + +func (m *metadataServiceProxy) SubscribeURL(url common.URL) (bool, error) { + logger.Error("you should never invoke this implementation") + return true, nil +} + +func (m *metadataServiceProxy) UnsubscribeURL(url common.URL) error { + logger.Error("you should never invoke this implementation") + return nil +} + +func (m *metadataServiceProxy) PublishServiceDefinition(url common.URL) error { + logger.Error("you should never invoke this implementation") + return nil +} + +func (m *metadataServiceProxy) GetExportedURLs(serviceInterface string, group string, version string, protocol string) ([]interface{}, error) { + urls, err := m.report.GetExportedURLs(&identifier.ServiceMetadataIdentifier{ + BaseMetadataIdentifier: identifier.BaseMetadataIdentifier{ + ServiceInterface: serviceInterface, + Version: version, + Group: group, + Side: constant.PROVIDER_PROTOCOL, + }, + Revision: m.revision, + Protocol: protocol, + }) + + if err != nil { + return []interface{}{}, nil + } + res := make([]common.URL, 0, len(urls)) + for _, s := range urls { + u, err := common.NewURL(s) + if err != nil { + logger.Errorf("could not parse the url string to URL structure", err) + continue + } + res = append(res, u) + } + return service.ConvertURLArrToIntfArr(res), nil +} + +func (m *metadataServiceProxy) MethodMapper() map[string]string { + return map[string]string{} +} + +func (m *metadataServiceProxy) GetSubscribedURLs() ([]common.URL, error) { + logger.Error("you should never invoke this implementation") + return []common.URL{}, nil +} + +func (m *metadataServiceProxy) GetServiceDefinition(interfaceName string, group string, version string) (string, error) { + return m.report.GetServiceDefinition(&identifier.MetadataIdentifier{ + BaseMetadataIdentifier: identifier.BaseMetadataIdentifier{ + ServiceInterface: interfaceName, + Group: group, + Version: version, + Side: constant.PROVIDER_PROTOCOL, + }, + Application: m.serviceName, + }) +} + +func (m *metadataServiceProxy) GetServiceDefinitionByServiceKey(serviceKey string) (string, error) { + params := parse(serviceKey) + return m.GetServiceDefinition(params[0], params[1], params[2]) +} + +func (m *metadataServiceProxy) RefreshMetadata(exportedRevision string, subscribedRevision string) (bool, error) { + logger.Error("you should never invoke this implementation") + return true, nil +} + +func (m metadataServiceProxy) Version() (string, error) { + return version, nil +} + +func newMetadataServiceProxy(ins registry.ServiceInstance) service.MetadataService { + revision := ins.GetMetadata()[constant.EXPORTED_SERVICES_REVISION_PROPERTY_NAME] + if len(revision) == 0 { + revision = constant.DEFAULT_REVIESION + } + + return &metadataServiceProxy{ + serviceName: ins.GetServiceName(), + revision: revision, + report: instance.GetMetadataReportInstance(), + } +} + +func parse(key string) []string { + arr := make([]string, 3, 3) + tmp := strings.SplitN(key, "/", 2) + if len(tmp) > 1 { + arr[0] = tmp[0] + key = tmp[1] + } + tmp = strings.SplitN(key, "/", 2) + if len(tmp) > 1 { + arr[2] = tmp[1] + key = tmp[0] + } + arr[1] = key + return arr +} diff --git a/metadata/service/remote/service_proxy_test.go b/metadata/service/remote/service_proxy_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c284bb22123e731f3b8905f70508856bc767ace6 --- /dev/null +++ b/metadata/service/remote/service_proxy_test.go @@ -0,0 +1,135 @@ +/* + * 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 remote + +import ( + "testing" +) +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/config/instance" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" + "github.com/apache/dubbo-go/metadata/report/factory" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/registry" +) + +func TestMetadataServiceProxy_GetExportedURLs(t *testing.T) { + pxy := createProxy() + res, err := pxy.GetExportedURLs(constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE) + assert.Nil(t, err) + assert.Len(t, res, 2) +} + +func TestMetadataServiceProxy_GetServiceDefinition(t *testing.T) { + pxy := createProxy() + res, err := pxy.GetServiceDefinition(constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE) + assert.Nil(t, err) + assert.Equal(t, "definition", res) +} + +// TestMetadataServiceProxy test those unimportant method +// in fact, we don't use them +func TestMetadataServiceProxy(t *testing.T) { + pxy := createProxy() + pxy.ServiceName() + pxy.PublishServiceDefinition(common.URL{}) + pxy.Version() + pxy.GetSubscribedURLs() + pxy.UnsubscribeURL(common.URL{}) + pxy.GetServiceDefinitionByServiceKey("any") + pxy.ExportURL(common.URL{}) + pxy.SubscribeURL(common.URL{}) + pxy.MethodMapper() + pxy.UnexportURL(common.URL{}) + pxy.Reference() + pxy.RefreshMetadata(constant.ANY_VALUE, constant.ANY_VALUE) +} + +func createProxy() service.MetadataService { + + prepareTest() + + ins := ®istry.DefaultServiceInstance{ + Id: "test-id", + ServiceName: "com.dubbo", + Host: "localhost", + Port: 8080, + Enable: true, + Healthy: true, + Metadata: map[string]string{constant.METADATA_SERVICE_URL_PARAMS_PROPERTY_NAME: `{"mock":{"timeout":"10000","version":"1.0.0","dubbo":"2.0.2","release":"2.7.6","port":"20880"}}`}, + } + return newMetadataServiceProxy(ins) +} + +func prepareTest() { + extension.SetMetadataReportFactory("mock", func() factory.MetadataReportFactory { + return &mockMetadataReportFactory{} + }) + u, _ := common.NewURL("mock://localhost") + instance.GetMetadataReportInstance(&u) +} + +type mockMetadataReportFactory struct { +} + +func (m *mockMetadataReportFactory) CreateMetadataReport(*common.URL) report.MetadataReport { + return &mockMetadataReport{} +} + +type mockMetadataReport struct { +} + +func (m mockMetadataReport) StoreProviderMetadata(*identifier.MetadataIdentifier, string) error { + panic("implement me") +} + +func (m mockMetadataReport) StoreConsumerMetadata(*identifier.MetadataIdentifier, string) error { + panic("implement me") +} + +func (m mockMetadataReport) SaveServiceMetadata(*identifier.ServiceMetadataIdentifier, common.URL) error { + return nil +} + +func (m mockMetadataReport) RemoveServiceMetadata(*identifier.ServiceMetadataIdentifier) error { + panic("implement me") +} + +func (m mockMetadataReport) GetExportedURLs(*identifier.ServiceMetadataIdentifier) ([]string, error) { + return []string{"mock://localhost1", "mock://localhost2"}, nil +} + +func (m mockMetadataReport) SaveSubscribedData(*identifier.SubscriberMetadataIdentifier, string) error { + return nil +} + +func (m mockMetadataReport) GetSubscribedURLs(*identifier.SubscriberMetadataIdentifier) ([]string, error) { + panic("implement me") +} + +func (m mockMetadataReport) GetServiceDefinition(*identifier.MetadataIdentifier) (string, error) { + return "definition", nil +} diff --git a/metadata/service/remote/service_test.go b/metadata/service/remote/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..734f0989ab06ef17caeef241cd067c678fb8b2ad --- /dev/null +++ b/metadata/service/remote/service_test.go @@ -0,0 +1,143 @@ +/* + * 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 remote + +import ( + "fmt" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config/instance" + "github.com/apache/dubbo-go/metadata/definition" + "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/metadata/report" + "github.com/apache/dubbo-go/metadata/report/factory" + "github.com/apache/dubbo-go/metadata/service/inmemory" +) + +var ( + serviceMetadata = make(map[*identifier.ServiceMetadataIdentifier]common.URL, 4) + subscribedMetadata = make(map[*identifier.SubscriberMetadataIdentifier]string, 4) +) + +func getMetadataReportFactory() factory.MetadataReportFactory { + return &metadataReportFactory{} +} + +type metadataReportFactory struct { +} + +func (mrf *metadataReportFactory) CreateMetadataReport(*common.URL) report.MetadataReport { + return &metadataReport{} +} + +type metadataReport struct { +} + +func (metadataReport) StoreProviderMetadata(*identifier.MetadataIdentifier, string) error { + return nil +} + +func (metadataReport) StoreConsumerMetadata(*identifier.MetadataIdentifier, string) error { + return nil +} + +func (mr *metadataReport) SaveServiceMetadata(id *identifier.ServiceMetadataIdentifier, url common.URL) error { + logger.Infof("SaveServiceMetadata , url is %v", url) + serviceMetadata[id] = url + return nil +} + +func (metadataReport) RemoveServiceMetadata(*identifier.ServiceMetadataIdentifier) error { + return nil +} + +func (metadataReport) GetExportedURLs(*identifier.ServiceMetadataIdentifier) ([]string, error) { + return nil, nil +} + +func (mr *metadataReport) SaveSubscribedData(id *identifier.SubscriberMetadataIdentifier, urls string) error { + logger.Infof("SaveSubscribedData, , url is %v", urls) + subscribedMetadata[id] = urls + return nil +} + +func (metadataReport) GetSubscribedURLs(*identifier.SubscriberMetadataIdentifier) ([]string, error) { + return nil, nil +} + +func (metadataReport) GetServiceDefinition(*identifier.MetadataIdentifier) (string, error) { + return "", nil +} + +func TestMetadataService(t *testing.T) { + extension.SetMetadataReportFactory("mock", getMetadataReportFactory) + u, err := common.NewURL(fmt.Sprintf("mock://127.0.0.1:20000/?sync.report=true")) + assert.NoError(t, err) + instance.GetMetadataReportInstance(&u) + mts, err := newMetadataService() + assert.NoError(t, err) + mts.(*MetadataService).setInMemoryMetadataService(mockInmemoryProc(t)) + _, _ = mts.RefreshMetadata("0.0.1", "0.0.1") +} + +func mockInmemoryProc(t *testing.T) *inmemory.MetadataService { + mts, _ := inmemory.NewMetadataService() + serviceName := "com.ikurento.user.UserProvider" + group := "group1" + version := "0.0.1" + protocol := "dubbo" + beanName := "UserProvider" + userProvider := &definition.UserProvider{} + + u, err := common.NewURL(fmt.Sprintf( + "%v://127.0.0.1:20000/com.ikurento.user.UserProvider1?anyhost=true&"+ + "application=BDTService&category=providers&default.timeout=10000&dubbo=dubbo-provider-golang-1.0.0&"+ + "environment=dev&interface=%v&ip=192.168.56.1&methods=GetUser&module=dubbogo+user-info+server&org=ikurento.com&"+ + "owner=ZX&pid=1447&revision=0.0.1&side=provider&timeout=3000×tamp=1556509797245&group=%v&version=%v&bean.name=%v", + protocol, serviceName, group, version, beanName)) + assert.NoError(t, err) + + _, err = mts.ExportURL(u) + assert.NoError(t, err) + _, err = mts.SubscribeURL(u) + assert.NoError(t, err) + + _, err = common.ServiceMap.Register(serviceName, protocol, userProvider) + assert.NoError(t, err) + err = mts.PublishServiceDefinition(u) + assert.NoError(t, err) + + expected := "{\"CanonicalName\":\"com.ikurento.user.UserProvider\",\"CodeSource\":\"\"," + + "\"Methods\":[{\"Name\":\"GetUser\",\"ParameterTypes\":[\"slice\"],\"ReturnType\":\"ptr\"," + + "\"Parameters\":null}],\"Types\":null}" + def1, _ := mts.GetServiceDefinition(serviceName, group, version) + assert.Equal(t, expected, def1) + serviceKey := definition.ServiceDescriperBuild(serviceName, group, version) + def2, _ := mts.GetServiceDefinitionByServiceKey(serviceKey) + assert.Equal(t, expected, def2) + return mts.(*inmemory.MetadataService) +} diff --git a/metadata/service/service.go b/metadata/service/service.go new file mode 100644 index 0000000000000000000000000000000000000000..f6509d0a72eb26e488dfb4fdeef5f4bbfd6b1bea --- /dev/null +++ b/metadata/service/service.go @@ -0,0 +1,135 @@ +/* + * 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 service + +import ( + "sync" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/registry" +) + +// MetadataService is used to define meta data related behaviors +// usually the implementation should be singleton +type MetadataService interface { + common.RPCService + // ServiceName will get the service's name in meta service , which is application name + ServiceName() (string, error) + // ExportURL will store the exported url in metadata + ExportURL(url common.URL) (bool, error) + // UnexportURL will delete the exported url in metadata + UnexportURL(url common.URL) error + // SubscribeURL will store the subscribed url in metadata + SubscribeURL(url common.URL) (bool, error) + // UnsubscribeURL will delete the subscribed url in metadata + UnsubscribeURL(url common.URL) error + // PublishServiceDefinition will generate the target url's code info + PublishServiceDefinition(url common.URL) error + // GetExportedURLs will get the target exported url in metadata + // the url should be unique + // due to dubbo-go only support return array []interface{} in RPCService, so we should declare the return type as []interface{} + // actually, it's []String + GetExportedURLs(serviceInterface string, group string, version string, protocol string) ([]interface{}, error) + + MethodMapper() map[string]string + + // GetExportedURLs will get the target subscribed url in metadata + // the url should be unique + GetSubscribedURLs() ([]common.URL, error) + // GetServiceDefinition will get the target service info store in metadata + GetServiceDefinition(interfaceName string, group string, version string) (string, error) + // GetServiceDefinition will get the target service info store in metadata by service key + GetServiceDefinitionByServiceKey(serviceKey string) (string, error) + // RefreshMetadata will refresh the metadata + RefreshMetadata(exportedRevision string, subscribedRevision string) (bool, error) + // Version will return the metadata service version + Version() (string, error) +} + +// BaseMetadataService is used for the event logic for struct who will implement interface MetadataService +type BaseMetadataService struct { + serviceName string +} + +func NewBaseMetadataService(serviceName string) BaseMetadataService { + return BaseMetadataService{ + serviceName: serviceName, + } +} + +func (mts *BaseMetadataService) MethodMapper() map[string]string { + return map[string]string{ + "GetExportedURLs": "getExportedURLs", + } +} + +// ServiceName can get the service's name in meta service , which is application name +func (mts *BaseMetadataService) ServiceName() (string, error) { + return mts.serviceName, nil +} + +// Version will return the version of metadata service +func (mts *BaseMetadataService) Reference() string { + return constant.SIMPLE_METADATA_SERVICE_NAME +} + +type MetadataServiceProxyFactory interface { + GetProxy(ins registry.ServiceInstance) MetadataService +} + +type MetadataServiceProxyCreator func(ins registry.ServiceInstance) MetadataService + +type BaseMetadataServiceProxyFactory struct { + proxies sync.Map + creator MetadataServiceProxyCreator +} + +func NewBaseMetadataServiceProxyFactory(creator MetadataServiceProxyCreator) *BaseMetadataServiceProxyFactory { + return &BaseMetadataServiceProxyFactory{ + creator: creator, + } +} + +func (b *BaseMetadataServiceProxyFactory) GetProxy(ins registry.ServiceInstance) MetadataService { + key := ins.GetServiceName() + "##" + getExportedServicesRevision(ins) + if proxy, ok := b.proxies.Load(key); ok { + return proxy.(MetadataService) + } + v, _ := b.proxies.LoadOrStore(key, b.creator(ins)) + return v.(MetadataService) +} + +func getExportedServicesRevision(serviceInstance registry.ServiceInstance) string { + metaData := serviceInstance.GetMetadata() + return metaData[constant.EXPORTED_SERVICES_REVISION_PROPERTY_NAME] +} + +func ConvertURLArrToIntfArr(urls []common.URL) []interface{} { + if len(urls) == 0 { + return []interface{}{} + } + + res := make([]interface{}, 0, len(urls)) + for _, u := range urls { + res = append(res, u.String()) + } + return res +} diff --git a/registry/base_registry.go b/registry/base_registry.go index 3e1bddf233310871182544b6415c10c8df27e622..ad1a3b61741e003625612ad58409eb8615271a84 100644 --- a/registry/base_registry.go +++ b/registry/base_registry.go @@ -56,6 +56,8 @@ func init() { localIP, _ = gxnet.GetLocalIP() } +type createPathFunc func(dubboPath string) error + /* * -----------------------------------NOTICE--------------------------------------------- * If there is no special case, you'd better inherit BaseRegistry and implement the @@ -74,8 +76,12 @@ type FacadeBasedRegistry interface { CreatePath(string) error // DoRegister actually do the register job DoRegister(string, string) error + // DoUnregister do the unregister job + DoUnregister(string, string) error // DoSubscribe actually subscribe the URL DoSubscribe(conf *common.URL) (Listener, error) + // DoUnsubscribe does unsubscribe the URL + DoUnsubscribe(conf *common.URL) (Listener, error) // CloseAndNilClient close the client and then reset the client in registry to nil // you should notice that this method will be invoked inside a lock. // So you should implement this method as light weighted as you can. @@ -94,7 +100,7 @@ type BaseRegistry struct { birth int64 // time of file birth, seconds since Epoch; 0 if unknown wg sync.WaitGroup // wg+done for zk restart done chan struct{} - cltLock sync.Mutex //ctl lock is a lock for services map + cltLock sync.RWMutex //ctl lock is a lock for services map services map[string]common.URL // service name + protocol -> service config, for store the service registered } @@ -154,6 +160,43 @@ func (r *BaseRegistry) Register(conf common.URL) error { return nil } +// UnRegister implement interface registry to unregister +func (r *BaseRegistry) UnRegister(conf common.URL) error { + var ( + ok bool + err error + oldURL common.URL + ) + + func() { + r.cltLock.Lock() + defer r.cltLock.Unlock() + oldURL, ok = r.services[conf.Key()] + + if !ok { + err = perrors.Errorf("Path{%s} has not registered", conf.Key()) + } + + delete(r.services, conf.Key()) + }() + + if err != nil { + return err + } + + err = r.unregister(conf) + if err != nil { + func() { + r.cltLock.Lock() + defer r.cltLock.Unlock() + r.services[conf.Key()] = oldURL + }() + return perrors.WithMessagef(err, "register(conf:%+v)", conf) + } + + return nil +} + // service is for getting service path stored in url func (r *BaseRegistry) service(c common.URL) string { return url.QueryEscape(c.Service()) @@ -189,6 +232,18 @@ func (r *BaseRegistry) RestartCallBack() bool { // register for register url to registry, include init params func (r *BaseRegistry) register(c common.URL) error { + return r.processURL(c, r.facadeBasedRegistry.DoRegister, r.createPath) +} + +// unregister for unregister url to registry, include init params +func (r *BaseRegistry) unregister(c common.URL) error { + return r.processURL(c, r.facadeBasedRegistry.DoUnregister, nil) +} + +func (r *BaseRegistry) processURL(c common.URL, f func(string, string) error, cpf createPathFunc) error { + if f == nil { + panic(" Must provide a `function(string, string) error` to process URL. ") + } var ( err error //revision string @@ -213,15 +268,15 @@ func (r *BaseRegistry) register(c common.URL) error { switch role { case common.PROVIDER: - dubboPath, rawURL, err = r.providerRegistry(c, params) + dubboPath, rawURL, err = r.providerRegistry(c, params, cpf) case common.CONSUMER: - dubboPath, rawURL, err = r.consumerRegistry(c, params) + dubboPath, rawURL, err = r.consumerRegistry(c, params, cpf) default: return perrors.Errorf("@c{%v} type is not referencer or provider", c) } encodedURL = url.QueryEscape(rawURL) dubboPath = strings.ReplaceAll(dubboPath, "$", "%24") - err = r.facadeBasedRegistry.DoRegister(dubboPath, encodedURL) + err = f(dubboPath, encodedURL) if err != nil { return perrors.WithMessagef(err, "register Node(path:%s, url:%s)", dubboPath, rawURL) @@ -229,8 +284,15 @@ func (r *BaseRegistry) register(c common.URL) error { return nil } +// createPath will create dubbo path in register +func (r *BaseRegistry) createPath(dubboPath string) error { + r.cltLock.Lock() + defer r.cltLock.Unlock() + return r.facadeBasedRegistry.CreatePath(dubboPath) +} + // providerRegistry for provider role do -func (r *BaseRegistry) providerRegistry(c common.URL, params url.Values) (string, string, error) { +func (r *BaseRegistry) providerRegistry(c common.URL, params url.Values, f createPathFunc) (string, string, error) { var ( dubboPath string rawURL string @@ -240,11 +302,9 @@ func (r *BaseRegistry) providerRegistry(c common.URL, params url.Values) (string return "", "", perrors.Errorf("conf{Path:%s, Methods:%s}", c.Path, c.Methods) } dubboPath = fmt.Sprintf("/dubbo/%s/%s", r.service(c), common.DubboNodes[common.PROVIDER]) - func() { - r.cltLock.Lock() - defer r.cltLock.Unlock() - err = r.facadeBasedRegistry.CreatePath(dubboPath) - }() + if f != nil { + err = f(dubboPath) + } if err != nil { logger.Errorf("facadeBasedRegistry.CreatePath(path{%s}) = error{%#v}", dubboPath, perrors.WithStack(err)) return "", "", perrors.WithMessagef(err, "facadeBasedRegistry.CreatePath(path:%s)", dubboPath) @@ -274,7 +334,7 @@ func (r *BaseRegistry) providerRegistry(c common.URL, params url.Values) (string } // consumerRegistry for consumer role do -func (r *BaseRegistry) consumerRegistry(c common.URL, params url.Values) (string, string, error) { +func (r *BaseRegistry) consumerRegistry(c common.URL, params url.Values, f createPathFunc) (string, string, error) { var ( dubboPath string rawURL string @@ -282,23 +342,18 @@ func (r *BaseRegistry) consumerRegistry(c common.URL, params url.Values) (string ) dubboPath = fmt.Sprintf("/dubbo/%s/%s", r.service(c), common.DubboNodes[common.CONSUMER]) - func() { - r.cltLock.Lock() - defer r.cltLock.Unlock() - err = r.facadeBasedRegistry.CreatePath(dubboPath) - - }() + if f != nil { + err = f(dubboPath) + } if err != nil { logger.Errorf("facadeBasedRegistry.CreatePath(path{%s}) = error{%v}", dubboPath, perrors.WithStack(err)) return "", "", perrors.WithStack(err) } dubboPath = fmt.Sprintf("/dubbo/%s/%s", r.service(c), common.DubboNodes[common.PROVIDER]) - func() { - r.cltLock.Lock() - defer r.cltLock.Unlock() - err = r.facadeBasedRegistry.CreatePath(dubboPath) - }() + if f != nil { + err = f(dubboPath) + } if err != nil { logger.Errorf("facadeBasedRegistry.CreatePath(path{%s}) = error{%v}", dubboPath, perrors.WithStack(err)) @@ -323,20 +378,20 @@ func sleepWait(n int) { } // Subscribe :subscribe from registry, event will notify by notifyListener -func (r *BaseRegistry) Subscribe(url *common.URL, notifyListener NotifyListener) { +func (r *BaseRegistry) Subscribe(url *common.URL, notifyListener NotifyListener) error { n := 0 for { n++ if !r.IsAvailable() { logger.Warnf("event listener game over.") - return + return perrors.New("BaseRegistry is not available.") } listener, err := r.facadeBasedRegistry.DoSubscribe(url) if err != nil { if !r.IsAvailable() { logger.Warnf("event listener game over.") - return + return err } logger.Warnf("getListener() = err:%v", perrors.WithStack(err)) time.Sleep(time.Duration(RegistryConnDelay) * time.Second) @@ -358,6 +413,37 @@ func (r *BaseRegistry) Subscribe(url *common.URL, notifyListener NotifyListener) } } +// UnSubscribe URL +func (r *BaseRegistry) UnSubscribe(url *common.URL, notifyListener NotifyListener) error { + if !r.IsAvailable() { + logger.Warnf("event listener game over.") + return perrors.New("BaseRegistry is not available.") + } + + listener, err := r.facadeBasedRegistry.DoUnsubscribe(url) + if err != nil { + if !r.IsAvailable() { + logger.Warnf("event listener game over.") + return perrors.New("BaseRegistry is not available.") + } + logger.Warnf("getListener() = err:%v", perrors.WithStack(err)) + return perrors.WithStack(err) + } + + for { + if serviceEvent, err := listener.Next(); err != nil { + logger.Warnf("Selector.watch() = error{%v}", perrors.WithStack(err)) + listener.Close() + break + } else { + logger.Infof("update begin, service event: %v", serviceEvent.String()) + notifyListener.Notify(serviceEvent) + } + + } + return nil +} + // closeRegisters close and remove registry client and reset services map func (r *BaseRegistry) closeRegisters() { logger.Infof("begin to close provider client") diff --git a/registry/consul/registry.go b/registry/consul/registry.go index 4ef87394687aecc8804b2cebedd58fc0e72e8e6e..c425c5ec20d36be02c00499340f13b13c9aa2655 100644 --- a/registry/consul/registry.go +++ b/registry/consul/registry.go @@ -73,7 +73,8 @@ func newConsulRegistry(url *common.URL) (registry.Registry, error) { return r, nil } -// Register service to consul registry center +// Register register @url +// it delegate the job to register() method func (r *consulRegistry) Register(url common.URL) error { var err error @@ -87,6 +88,7 @@ func (r *consulRegistry) Register(url common.URL) error { return nil } +// register actually register the @url func (r *consulRegistry) register(url common.URL) error { service, err := buildService(url) if err != nil { @@ -95,8 +97,9 @@ func (r *consulRegistry) register(url common.URL) error { return r.client.Agent().ServiceRegister(service) } -// Unregister service from consul registry center -func (r *consulRegistry) Unregister(url common.URL) error { +// UnRegister unregister the @url +// it delegate the job to unregister() method +func (r *consulRegistry) UnRegister(url common.URL) error { var err error role, _ := strconv.Atoi(r.URL.GetParam(constant.ROLE_KEY, "")) @@ -109,18 +112,27 @@ func (r *consulRegistry) Unregister(url common.URL) error { return nil } +// unregister actually unregister the @url func (r *consulRegistry) unregister(url common.URL) error { return r.client.Agent().ServiceDeregister(buildId(url)) } -// Subscribe service from consul registry center -func (r *consulRegistry) Subscribe(url *common.URL, notifyListener registry.NotifyListener) { +// Subscribe subscribe the @url with the @notifyListener +func (r *consulRegistry) Subscribe(url *common.URL, notifyListener registry.NotifyListener) error { role, _ := strconv.Atoi(r.URL.GetParam(constant.ROLE_KEY, "")) if role == common.CONSUMER { r.subscribe(url, notifyListener) } + return nil +} + +// UnSubscribe is not supported yet +func (r *consulRegistry) UnSubscribe(url *common.URL, notifyListener registry.NotifyListener) error { + return perrors.New("UnSubscribe not support in consulRegistry") } +// subscribe actually subscribe the @url +// it loops forever until success func (r *consulRegistry) subscribe(url *common.URL, notifyListener registry.NotifyListener) { for { if !r.IsAvailable() { diff --git a/registry/consul/registry_test.go b/registry/consul/registry_test.go index bb6842cd8fb67dd2cc70b1a7530fbb94f618a9b0..94718f5ab657c198882f065a50e5d5a2c9d4bc6f 100644 --- a/registry/consul/registry_test.go +++ b/registry/consul/registry_test.go @@ -44,7 +44,7 @@ func (suite *consulRegistryTestSuite) testRegister() { func (suite *consulRegistryTestSuite) testUnregister() { consulProviderRegistry, _ := suite.providerRegistry.(*consulRegistry) - err := consulProviderRegistry.Unregister(suite.providerUrl) + err := consulProviderRegistry.UnRegister(suite.providerUrl) assert.NoError(suite.t, err) } diff --git a/registry/consul/utils_test.go b/registry/consul/utils_test.go index d66600b773ee78b43ac3da4edf8849d0019c744d..327dd95f7181907f6635c7fe89ef726bdcef5204 100644 --- a/registry/consul/utils_test.go +++ b/registry/consul/utils_test.go @@ -19,24 +19,19 @@ package consul import ( "fmt" - "io/ioutil" "net" "net/url" - "os" "strconv" "sync" "testing" ) -import ( - "github.com/hashicorp/consul/agent" -) - import ( "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/registry" "github.com/apache/dubbo-go/remoting" + "github.com/apache/dubbo-go/remoting/consul" ) var ( @@ -51,71 +46,39 @@ var ( ) func newProviderRegistryUrl(host string, port int) *common.URL { - url1 := common.NewURLWithOptions( + return common.NewURLWithOptions( common.WithIp(host), common.WithPort(strconv.Itoa(port)), common.WithParams(url.Values{}), common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER)), ) - return url1 } func newConsumerRegistryUrl(host string, port int) *common.URL { - url1 := common.NewURLWithOptions( + return common.NewURLWithOptions( common.WithIp(host), common.WithPort(strconv.Itoa(port)), common.WithParams(url.Values{}), common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.CONSUMER)), ) - return url1 } func newProviderUrl(host string, port int, service string, protocol string) common.URL { - url1 := common.NewURLWithOptions( + return *common.NewURLWithOptions( common.WithIp(host), common.WithPort(strconv.Itoa(port)), common.WithPath(service), common.WithProtocol(protocol), ) - return *url1 } func newConsumerUrl(host string, port int, service string, protocol string) common.URL { - url1 := common.NewURLWithOptions( + return *common.NewURLWithOptions( common.WithIp(host), common.WithPort(strconv.Itoa(port)), common.WithPath(service), common.WithProtocol(protocol), ) - return *url1 -} - -type testConsulAgent struct { - dataDir string - testAgent *agent.TestAgent -} - -func newConsulAgent(t *testing.T, port int) *testConsulAgent { - dataDir, _ := ioutil.TempDir("./", "agent") - hcl := ` - ports { - http = ` + strconv.Itoa(port) + ` - } - data_dir = "` + dataDir + `" - ` - testAgent := &agent.TestAgent{Name: t.Name(), DataDir: dataDir, HCL: hcl} - testAgent.Start(t) - - consulAgent := &testConsulAgent{ - dataDir: dataDir, - testAgent: testAgent, - } - return consulAgent -} - -func (consulAgent *testConsulAgent) close() { - consulAgent.testAgent.Shutdown() - os.RemoveAll(consulAgent.dataDir) } type testServer struct { @@ -184,8 +147,8 @@ func (suite *consulRegistryTestSuite) close() { // register -> subscribe -> unregister func test1(t *testing.T) { - consulAgent := newConsulAgent(t, registryPort) - defer consulAgent.close() + consulAgent := consul.NewConsulAgent(t, registryPort) + defer consulAgent.Close() server := newServer(providerHost, providerPort) defer server.close() @@ -204,8 +167,8 @@ func test1(t *testing.T) { // subscribe -> register func test2(t *testing.T) { - consulAgent := newConsulAgent(t, registryPort) - defer consulAgent.close() + consulAgent := consul.NewConsulAgent(t, registryPort) + defer consulAgent.Close() server := newServer(providerHost, providerPort) defer server.close() diff --git a/registry/directory/directory.go b/registry/directory/directory.go index e845db01f1b8f76897f2beeaee45a84537c96d83..2fbf9410f76c473362964c9ef148e3c581d3d045 100644 --- a/registry/directory/directory.go +++ b/registry/directory/directory.go @@ -54,7 +54,7 @@ type RegistryDirectory struct { listenerLock sync.Mutex serviceType string registry registry.Registry - cacheInvokersMap *sync.Map //use sync.map + cacheInvokersMap *sync.Map // use sync.map cacheOriginUrl *common.URL configurators []config_center.Configurator consumerConfigurationListener *consumerConfigurationListener @@ -108,15 +108,15 @@ func (dir *RegistryDirectory) refreshInvokers(res *registry.ServiceEvent) { url *common.URL oldInvoker protocol.Invoker = nil ) - //judge is override or others + // judge is override or others if res != nil { url = &res.Service - //1.for override url in 2.6.x + // 1.for override url in 2.6.x if url.Protocol == constant.OVERRIDE_PROTOCOL || url.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY) == constant.CONFIGURATORS_CATEGORY { dir.configurators = append(dir.configurators, extension.GetDefaultConfigurator(url)) url = nil - } else if url.Protocol == constant.ROUTER_PROTOCOL || //2.for router + } else if url.Protocol == constant.ROUTER_PROTOCOL || // 2.for router url.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY) == constant.ROUTER_CATEGORY { url = nil @@ -126,7 +126,7 @@ func (dir *RegistryDirectory) refreshInvokers(res *registry.ServiceEvent) { logger.Infof("selector add service url{%s}", res.Service) var urls []*common.URL - for _, v := range directory.GetRouterURLSet().Values() { + for _, v := range config.GetRouterURLSet().Values() { urls = append(urls, v.(*common.URL)) } @@ -177,7 +177,7 @@ func (dir *RegistryDirectory) toGroupInvokers() []protocol.Invoker { } groupInvokersList := make([]protocol.Invoker, 0, len(groupInvokersMap)) if len(groupInvokersMap) == 1 { - //len is 1 it means no group setting ,so do not need cluster again + // len is 1 it means no group setting ,so do not need cluster again for _, invokers := range groupInvokersMap { groupInvokersList = invokers } @@ -221,7 +221,7 @@ func (dir *RegistryDirectory) cacheInvoker(url *common.URL) protocol.Invoker { logger.Error("URL is nil ,pls check if service url is subscribe successfully!") return nil } - //check the url's protocol is equal to the protocol which is configured in reference config or referenceUrl is not care about protocol + // check the url's protocol is equal to the protocol which is configured in reference config or referenceUrl is not care about protocol if url.Protocol == referenceUrl.Protocol || referenceUrl.Protocol == "" { newUrl := common.MergeUrl(url, referenceUrl) dir.overrideUrl(newUrl) @@ -271,7 +271,7 @@ func (dir *RegistryDirectory) IsAvailable() bool { // Destroy method func (dir *RegistryDirectory) Destroy() { - //TODO:unregister & unsubscribe + // TODO:unregister & unsubscribe dir.BaseDirectory.Destroy(func() { invokers := dir.cacheInvokers dir.cacheInvokers = []protocol.Invoker{} diff --git a/registry/directory/directory_test.go b/registry/directory/directory_test.go index ac3f7124c12788959f529193b871652085fe6303..f2b2f8edd2d46950d2e74733b1d869e0de282ec0 100644 --- a/registry/directory/directory_test.go +++ b/registry/directory/directory_test.go @@ -45,7 +45,11 @@ import ( ) func init() { - config.SetConsumerConfig(config.ConsumerConfig{ApplicationConfig: &config.ApplicationConfig{Name: "test-application"}}) + config.SetConsumerConfig(config.ConsumerConfig{ + BaseConfig: config.BaseConfig{ + ApplicationConfig: &config.ApplicationConfig{Name: "test-application"}, + }, + }) } func TestSubscribe(t *testing.T) { @@ -60,7 +64,7 @@ func TestSubscribe(t *testing.T) { // registryDirectory, mockRegistry := normalRegistryDir() // time.Sleep(1e9) // assert.Len(t, registryDirectory.cacheInvokers, 3) -// mockRegistry.MockEvent(®istry.ServiceEvent{Action: remoting.EventTypeDel, Service: *common.NewURLWithOptions(common.WithPath("TEST0"), common.WithProtocol("dubbo"))}) +// mockRegistry.MockEvent(®istry.ServiceEvent{Action: remoting.EventTypeDel, Service: *event.NewURLWithOptions(event.WithPath("TEST0"), event.WithProtocol("dubbo"))}) // time.Sleep(1e9) // assert.Len(t, registryDirectory.cacheInvokers, 2) //} diff --git a/registry/etcdv3/registry.go b/registry/etcdv3/registry.go index f3df78177bda2b068d0ad88156b593ab3d48c5d7..2fec8eaad25e716fc5ed5ee33775d8898cb212e2 100644 --- a/registry/etcdv3/registry.go +++ b/registry/etcdv3/registry.go @@ -116,6 +116,11 @@ func (r *etcdV3Registry) DoRegister(root string, node string) error { return r.client.Create(path.Join(root, node), "") } +// nolint +func (r *etcdV3Registry) DoUnregister(root string, node string) error { + return perrors.New("DoUnregister is not support in etcdV3Registry") +} + // CloseAndNilClient closes listeners and clear client func (r *etcdV3Registry) CloseAndNilClient() { r.client.Close() @@ -174,3 +179,7 @@ func (r *etcdV3Registry) DoSubscribe(svc *common.URL) (registry.Listener, error) return configListener, nil } + +func (r *etcdV3Registry) DoUnsubscribe(conf *common.URL) (registry.Listener, error) { + return nil, perrors.New("DoUnsubscribe is not support in etcdV3Registry") +} diff --git a/registry/etcdv3/service_discovery.go b/registry/etcdv3/service_discovery.go new file mode 100644 index 0000000000000000000000000000000000000000..10396049fb6bb7a5a5935ce21639dc5a78a56b0b --- /dev/null +++ b/registry/etcdv3/service_discovery.go @@ -0,0 +1,322 @@ +/* + * 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 etcdv3 + +import ( + "fmt" + "sync" + "time" +) + +import ( + gxset "github.com/dubbogo/gost/container/set" + gxpage "github.com/dubbogo/gost/page" + "github.com/hashicorp/vault/helper/jsonutil" + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/registry" + "github.com/apache/dubbo-go/remoting" + "github.com/apache/dubbo-go/remoting/etcdv3" +) + +const ( + ROOT = "/services" +) + +var ( + initLock sync.Mutex +) + +func init() { + extension.SetServiceDiscovery(constant.ETCDV3_KEY, newEtcdV3ServiceDiscovery) +} + +// new etcd service discovery struct +type etcdV3ServiceDiscovery struct { + // descriptor is a short string about the basic information of this instance + descriptor string + // client is current Etcdv3 client + client *etcdv3.Client + // serviceInstance is current serviceInstance + serviceInstance *registry.ServiceInstance + // services is when register or update will add service name + services *gxset.HashSet + // child listener + childListenerMap map[string]*etcdv3.EventListener +} + +// basic information of this instance +func (e *etcdV3ServiceDiscovery) String() string { + return e.descriptor +} + +// Destory service discovery +func (e *etcdV3ServiceDiscovery) Destroy() error { + if e.client != nil { + e.client.Close() + } + return nil +} + +// Register will register an instance of ServiceInstance to registry +func (e *etcdV3ServiceDiscovery) Register(instance registry.ServiceInstance) error { + + e.serviceInstance = &instance + + path := toPath(instance) + + if nil != e.client { + ins, err := jsonutil.EncodeJSON(instance) + if err == nil { + err = e.client.RegisterTemp(path, string(ins)) + if err != nil { + logger.Errorf("cannot register the instance: %s", string(ins), err) + } else { + e.services.Add(instance.GetServiceName()) + } + } + } + + return nil +} + +// Update will update the data of the instance in registry +func (e *etcdV3ServiceDiscovery) Update(instance registry.ServiceInstance) error { + path := toPath(instance) + + if nil != e.client { + ins, err := jsonutil.EncodeJSON(instance) + if nil == err { + e.client.RegisterTemp(path, string(ins)) + e.services.Add(instance.GetServiceName()) + } + } + + return nil +} + +// Unregister will unregister this instance from registry +func (e *etcdV3ServiceDiscovery) Unregister(instance registry.ServiceInstance) error { + path := toPath(instance) + + if nil != e.client { + err := e.client.Delete(path) + e.services.Remove(instance.GetServiceName()) + e.serviceInstance = nil + return err + } + + return nil +} + +// ----------------- discovery ------------------- +// GetDefaultPageSize will return the default page size +func (e *etcdV3ServiceDiscovery) GetDefaultPageSize() int { + return registry.DefaultPageSize +} + +// GetServices will return the all service names. +func (e *etcdV3ServiceDiscovery) GetServices() *gxset.HashSet { + return e.services +} + +// GetInstances will return all service instances with serviceName +func (e *etcdV3ServiceDiscovery) GetInstances(serviceName string) []registry.ServiceInstance { + + if nil != e.client { + // get keys and values + _, vList, err := e.client.GetChildrenKVList(toParentPath(serviceName)) + if nil == err { + serviceInstances := make([]registry.ServiceInstance, 0, len(vList)) + for _, v := range vList { + instance := ®istry.DefaultServiceInstance{} + err = jsonutil.DecodeJSON([]byte(v), &instance) + if nil == err { + serviceInstances = append(serviceInstances, instance) + } + } + return serviceInstances + } + logger.Infof("could not getChildrenKVList the err is:%v", err) + } + + return make([]registry.ServiceInstance, 0, 0) +} + +// GetInstancesByPage will return a page containing instances of ServiceInstance with the serviceName +// the page will start at offset +func (e *etcdV3ServiceDiscovery) GetInstancesByPage(serviceName string, offset int, pageSize int) gxpage.Pager { + + all := e.GetInstances(serviceName) + + res := make([]interface{}, 0, pageSize) + + for i := offset; i < len(all) && i < offset+pageSize; i++ { + res = append(res, all[i]) + } + + return gxpage.New(offset, pageSize, res, len(all)) +} + +// GetHealthyInstancesByPage will return a page containing instances of ServiceInstance. +// The param healthy indices that the instance should be healthy or not. +// The page will start at offset +func (e *etcdV3ServiceDiscovery) GetHealthyInstancesByPage(serviceName string, offset int, pageSize int, healthy bool) gxpage.Pager { + all := e.GetInstances(serviceName) + res := make([]interface{}, 0, pageSize) + + var ( + i = offset + count = 0 + ) + for i < len(all) && count < pageSize { + ins := all[i] + if ins.IsHealthy() == healthy { + res = append(res, all[i]) + count++ + } + i++ + } + return gxpage.New(offset, pageSize, res, len(all)) +} + +// Batch get all instances by the specified service names +func (e *etcdV3ServiceDiscovery) GetRequestInstances(serviceNames []string, offset int, requestedSize int) map[string]gxpage.Pager { + res := make(map[string]gxpage.Pager, len(serviceNames)) + for _, name := range serviceNames { + res[name] = e.GetInstancesByPage(name, offset, requestedSize) + } + return res +} + +// ----------------- event ---------------------- +// AddListener adds a new ServiceInstancesChangedListener +// see addServiceInstancesChangedListener in Java +func (e *etcdV3ServiceDiscovery) AddListener(listener *registry.ServiceInstancesChangedListener) error { + return e.registerSreviceWatcher(listener.ServiceName) +} + +// DispatchEventByServiceName dispatches the ServiceInstancesChangedEvent to service instance whose name is serviceName +func (e *etcdV3ServiceDiscovery) DispatchEventByServiceName(serviceName string) error { + return e.DispatchEventForInstances(serviceName, e.GetInstances(serviceName)) +} + +// DispatchEventForInstances dispatches the ServiceInstancesChangedEvent to target instances +func (e *etcdV3ServiceDiscovery) DispatchEventForInstances(serviceName string, instances []registry.ServiceInstance) error { + return e.DispatchEvent(registry.NewServiceInstancesChangedEvent(serviceName, instances)) +} + +// DispatchEvent dispatches the event +func (e *etcdV3ServiceDiscovery) DispatchEvent(event *registry.ServiceInstancesChangedEvent) error { + extension.GetGlobalDispatcher().Dispatch(event) + return nil +} + +// Convert instance to dubbo path +func toPath(instance registry.ServiceInstance) string { + if instance == nil { + return "" + } + // like: /services/servicename1/host(127.0.0.1)/8080 + return fmt.Sprintf("%s%d", ROOT+constant.PATH_SEPARATOR+instance.GetServiceName()+constant.PATH_SEPARATOR+instance.GetHost()+constant.KEY_SEPARATOR, instance.GetPort()) +} + +// to dubbo service path +func toParentPath(serviceName string) string { + return ROOT + constant.PATH_SEPARATOR + serviceName +} + +// register service watcher +func (e *etcdV3ServiceDiscovery) registerSreviceWatcher(serviceName string) error { + + initLock.Lock() + defer initLock.Unlock() + + path := toParentPath(serviceName) + + listener, found := e.childListenerMap[serviceName] + + if !found { + listener = etcdv3.NewEventListener(e.client) + e.childListenerMap[serviceName] = listener + } + + listener.ListenServiceEvent(path, e) + + return nil +} + +// when child data change should DispatchEventByServiceName +func (e *etcdV3ServiceDiscovery) DataChange(eventType remoting.Event) bool { + + if eventType.Action == remoting.EventTypeUpdate { + instance := ®istry.DefaultServiceInstance{} + err := jsonutil.DecodeJSON([]byte(eventType.Content), &instance) + if err != nil { + instance.ServiceName = "" + } + + if err := e.DispatchEventByServiceName(instance.ServiceName); err != nil { + return false + } + } + + return true +} + +// netEcdv3ServiceDiscovery +func newEtcdV3ServiceDiscovery(name string) (registry.ServiceDiscovery, error) { + + initLock.Lock() + defer initLock.Unlock() + + sdc, ok := config.GetBaseConfig().GetServiceDiscoveries(name) + if !ok || len(sdc.RemoteRef) == 0 { + return nil, perrors.New("could not init the etcd service instance because the config is invalid") + } + + remoteConfig, ok := config.GetBaseConfig().GetRemoteConfig(sdc.RemoteRef) + if !ok { + return nil, perrors.New("could not find the remote config for name: " + sdc.RemoteRef) + } + + // init etcdv3 client + timeout, err := time.ParseDuration(remoteConfig.TimeoutStr) + if err != nil { + logger.Errorf("timeout config %v is invalid,err is %v", remoteConfig.TimeoutStr, err.Error()) + return nil, perrors.WithMessagef(err, "new etcd service discovery(address:%v)", remoteConfig.Address) + } + + logger.Infof("etcd address is: %v,timeout is:%s", remoteConfig.Address, timeout.String()) + + client := etcdv3.NewServiceDiscoveryClient( + etcdv3.WithName(etcdv3.RegistryETCDV3Client), + etcdv3.WithTimeout(timeout), + etcdv3.WithEndpoints(remoteConfig.Address), + ) + + descriptor := fmt.Sprintf("etcd-service-discovery[%s]", remoteConfig.Address) + + return &etcdV3ServiceDiscovery{descriptor, client, nil, gxset.NewSet(), make(map[string]*etcdv3.EventListener, 0)}, nil +} diff --git a/registry/etcdv3/service_discovery_test.go b/registry/etcdv3/service_discovery_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d8e3f1a2864150cc1f1e8996bc7c53e115dbef45 --- /dev/null +++ b/registry/etcdv3/service_discovery_test.go @@ -0,0 +1,80 @@ +/* + * 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 etcdv3 + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/registry" +) + +var testName = "test" + +func setUp() { + config.GetBaseConfig().ServiceDiscoveries[testName] = &config.ServiceDiscoveryConfig{ + Protocol: "etcdv3", + RemoteRef: testName, + } + + config.GetBaseConfig().Remotes[testName] = &config.RemoteConfig{ + Address: "localhost:2379", + TimeoutStr: "10s", + } +} + +func Test_newEtcdV3ServiceDiscovery(t *testing.T) { + name := constant.ETCDV3_KEY + _, err := newEtcdV3ServiceDiscovery(name) + + // warn: log configure file name is nil + assert.NotNil(t, err) + + sdc := &config.ServiceDiscoveryConfig{ + Protocol: "etcdv3", + RemoteRef: "mock", + } + config.GetBaseConfig().ServiceDiscoveries[name] = sdc + + _, err = newEtcdV3ServiceDiscovery(name) + + // RemoteConfig not found + assert.NotNil(t, err) + + config.GetBaseConfig().Remotes["mock"] = &config.RemoteConfig{ + Address: "localhost:2379", + TimeoutStr: "10s", + } + + res, err := newEtcdV3ServiceDiscovery(name) + assert.Nil(t, err) + assert.NotNil(t, res) +} + +func TestEtcdV3ServiceDiscovery_GetDefaultPageSize(t *testing.T) { + setUp() + serviceDiscovry := &etcdV3ServiceDiscovery{} + assert.Equal(t, registry.DefaultPageSize, serviceDiscovry.GetDefaultPageSize()) +} diff --git a/registry/event.go b/registry/event.go index 5fe6df6a379e1de8662917fb76c6d16fa9a17f37..39fb00c740ef2e70e2cd6768fa4a4bb3226f832c 100644 --- a/registry/event.go +++ b/registry/event.go @@ -25,6 +25,7 @@ import ( import ( "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/observer" "github.com/apache/dubbo-go/remoting" ) @@ -47,47 +48,9 @@ func (e ServiceEvent) String() string { return fmt.Sprintf("ServiceEvent{Action{%s}, Path{%s}}", e.Action, e.Service) } -// Event is align with Event interface in Java. -// it's the top abstraction -// Align with 2.7.5 -type Event interface { - fmt.Stringer - GetSource() interface{} - GetTimestamp() time.Time -} - -// baseEvent is the base implementation of Event -// You should never use it directly -type baseEvent struct { - source interface{} - timestamp time.Time -} - -// GetSource return the source -func (b *baseEvent) GetSource() interface{} { - return b.source -} - -// GetTimestamp return the timestamp when the event is created -func (b *baseEvent) GetTimestamp() time.Time { - return b.timestamp -} - -// String return a human readable string representing this event -func (b *baseEvent) String() string { - return fmt.Sprintf("baseEvent[source = %#v]", b.source) -} - -func newBaseEvent(source interface{}) *baseEvent { - return &baseEvent{ - source: source, - timestamp: time.Now(), - } -} - // ServiceInstancesChangedEvent represents service instances make some changing type ServiceInstancesChangedEvent struct { - baseEvent + observer.BaseEvent ServiceName string Instances []ServiceInstance } @@ -100,9 +63,9 @@ func (s *ServiceInstancesChangedEvent) String() string { // NewServiceInstancesChangedEvent will create the ServiceInstanceChangedEvent instance func NewServiceInstancesChangedEvent(serviceName string, instances []ServiceInstance) *ServiceInstancesChangedEvent { return &ServiceInstancesChangedEvent{ - baseEvent: baseEvent{ - source: serviceName, - timestamp: time.Now(), + BaseEvent: observer.BaseEvent{ + Source: serviceName, + Timestamp: time.Now(), }, ServiceName: serviceName, Instances: instances, diff --git a/registry/event/customizable_service_instance_listener.go b/registry/event/customizable_service_instance_listener.go new file mode 100644 index 0000000000000000000000000000000000000000..07e84c1454df91d2038beb08abddbc46274623c9 --- /dev/null +++ b/registry/event/customizable_service_instance_listener.go @@ -0,0 +1,73 @@ +/* + * 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 event + +import ( + "reflect" + "sync" +) + +import ( + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/observer" +) + +func init() { + extension.AddEventListener(GetCustomizableServiceInstanceListener) +} + +// customizableServiceInstanceListener is singleton +type customizableServiceInstanceListener struct { +} + +// GetPriority return priority 9999, +// 9999 is big enough to make sure it will be last invoked +func (c *customizableServiceInstanceListener) GetPriority() int { + return 9999 +} + +// OnEvent if the event is ServiceInstancePreRegisteredEvent +// it will iterate all ServiceInstanceCustomizer instances +// or it will do nothing +func (c *customizableServiceInstanceListener) OnEvent(e observer.Event) error { + if preRegEvent, ok := e.(*ServiceInstancePreRegisteredEvent); ok { + for _, cus := range extension.GetCustomizers() { + cus.Customize(preRegEvent.serviceInstance) + } + } + return nil +} + +// GetEventType will return ServiceInstancePreRegisteredEvent +func (c *customizableServiceInstanceListener) GetEventType() reflect.Type { + return reflect.TypeOf(&ServiceInstancePreRegisteredEvent{}) +} + +var ( + customizableServiceInstanceListenerInstance *customizableServiceInstanceListener + customizableServiceInstanceListenerOnce sync.Once +) + +// GetCustomizableServiceInstanceListener returns an instance +// if the instance was not initialized, we create one +func GetCustomizableServiceInstanceListener() observer.EventListener { + customizableServiceInstanceListenerOnce.Do(func() { + customizableServiceInstanceListenerInstance = &customizableServiceInstanceListener{} + }) + return customizableServiceInstanceListenerInstance +} diff --git a/registry/event/customizable_service_instance_listener_test.go b/registry/event/customizable_service_instance_listener_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1c81ece498b4864c3ea7f586d90052f3022627fc --- /dev/null +++ b/registry/event/customizable_service_instance_listener_test.go @@ -0,0 +1,76 @@ +/* + * 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 event + +import ( + "testing" + "time" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/registry" +) + +func TestGetCustomizableServiceInstanceListener(t *testing.T) { + + prepareMetadataServiceForTest() + + cus := GetCustomizableServiceInstanceListener() + + assert.Equal(t, 9999, cus.GetPriority()) + + extension.AddCustomizers(&mockCustomizer{}) + + err := cus.OnEvent(&mockEvent{}) + assert.Nil(t, err) + err = cus.OnEvent(NewServiceInstancePreRegisteredEvent("hello", createInstance())) + assert.Nil(t, err) + + tp := cus.GetEventType() + assert.NotNil(t, tp) +} + +type mockEvent struct { +} + +func (m *mockEvent) String() string { + panic("implement me") +} + +func (m *mockEvent) GetSource() interface{} { + panic("implement me") +} + +func (m *mockEvent) GetTimestamp() time.Time { + panic("implement me") +} + +type mockCustomizer struct { +} + +func (m *mockCustomizer) GetPriority() int { + return 0 +} + +func (m *mockCustomizer) Customize(instance registry.ServiceInstance) { +} diff --git a/registry/event/event_publishing_service_deiscovery_test.go b/registry/event/event_publishing_service_deiscovery_test.go new file mode 100644 index 0000000000000000000000000000000000000000..54752c03c0de598226270b27c8d7d0f3621d07d1 --- /dev/null +++ b/registry/event/event_publishing_service_deiscovery_test.go @@ -0,0 +1,190 @@ +/* + * 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 event + +import ( + "reflect" + "testing" +) + +import ( + gxset "github.com/dubbogo/gost/container/set" + gxpage "github.com/dubbogo/gost/page" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +import ( + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/observer" + dispatcher2 "github.com/apache/dubbo-go/common/observer/dispatcher" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/metadata/mapping" + _ "github.com/apache/dubbo-go/metadata/service/inmemory" + "github.com/apache/dubbo-go/registry" +) + +func TestEventPublishingServiceDiscovery_DispatchEvent(t *testing.T) { + + // extension.SetMetadataService("local", inmemory.NewMetadataService) + + config.GetApplicationConfig().MetadataType = "local" + + extension.SetGlobalServiceNameMapping(func() mapping.ServiceNameMapping { + return &mockServiceNameMapping{} + }) + + dc := NewEventPublishingServiceDiscovery(&ServiceDiscoveryA{}) + tsd := &TestServiceDiscoveryDestroyingEventListener{ + BaseListener: observer.NewBaseListener(), + } + tsd.SetT(t) + tsi := &TestServiceInstancePreRegisteredEventListener{} + tsi.SetT(t) + extension.AddEventListener(func() observer.EventListener { + return tsd + }) + extension.AddEventListener(func() observer.EventListener { + return tsi + }) + extension.SetEventDispatcher("direct", dispatcher2.NewDirectEventDispatcher) + extension.SetAndInitGlobalDispatcher("direct") + err := dc.Destroy() + assert.Nil(t, err) + si := ®istry.DefaultServiceInstance{Id: "testServiceInstance"} + err = dc.Register(si) + assert.Nil(t, err) + +} + +type TestServiceDiscoveryDestroyingEventListener struct { + suite.Suite + observer.BaseListener +} + +func (tel *TestServiceDiscoveryDestroyingEventListener) OnEvent(e observer.Event) error { + e1, ok := e.(*ServiceDiscoveryDestroyingEvent) + assert.Equal(tel.T(), ok, true) + assert.Equal(tel.T(), "testServiceDiscovery", e1.GetOriginal().String()) + assert.Equal(tel.T(), "testServiceDiscovery", e1.GetServiceDiscovery().String()) + return nil +} + +func (tel *TestServiceDiscoveryDestroyingEventListener) GetPriority() int { + return -1 +} + +func (tel *TestServiceDiscoveryDestroyingEventListener) GetEventType() reflect.Type { + return reflect.TypeOf(ServiceDiscoveryDestroyingEvent{}) +} + +type TestServiceInstancePreRegisteredEventListener struct { + suite.Suite + observer.BaseListener +} + +func (tel *TestServiceInstancePreRegisteredEventListener) OnEvent(e observer.Event) error { + e1, ok := e.(*ServiceInstancePreRegisteredEvent) + assert.Equal(tel.T(), ok, true) + assert.Equal(tel.T(), "testServiceInstance", e1.getServiceInstance().GetId()) + return nil +} + +func (tel *TestServiceInstancePreRegisteredEventListener) GetPriority() int { + return -1 +} + +func (tel *TestServiceInstancePreRegisteredEventListener) GetEventType() reflect.Type { + return reflect.TypeOf(ServiceInstancePreRegisteredEvent{}) +} + +type ServiceDiscoveryA struct { +} + +// String return mockServiceDiscovery +func (msd *ServiceDiscoveryA) String() string { + return "testServiceDiscovery" +} + +// Destroy do nothing +func (msd *ServiceDiscoveryA) Destroy() error { + return nil +} + +func (msd *ServiceDiscoveryA) Register(instance registry.ServiceInstance) error { + return nil +} + +func (msd *ServiceDiscoveryA) Update(instance registry.ServiceInstance) error { + return nil +} + +func (msd *ServiceDiscoveryA) Unregister(instance registry.ServiceInstance) error { + return nil +} + +func (msd *ServiceDiscoveryA) GetDefaultPageSize() int { + return 1 +} + +func (msd *ServiceDiscoveryA) GetServices() *gxset.HashSet { + return nil +} + +func (msd *ServiceDiscoveryA) GetInstances(serviceName string) []registry.ServiceInstance { + return nil +} + +func (msd *ServiceDiscoveryA) GetInstancesByPage(serviceName string, offset int, pageSize int) gxpage.Pager { + return nil +} + +func (msd *ServiceDiscoveryA) GetHealthyInstancesByPage(serviceName string, offset int, pageSize int, healthy bool) gxpage.Pager { + return nil +} + +func (msd *ServiceDiscoveryA) GetRequestInstances(serviceNames []string, offset int, requestedSize int) map[string]gxpage.Pager { + return nil +} + +func (msd *ServiceDiscoveryA) AddListener(listener *registry.ServiceInstancesChangedListener) error { + return nil +} + +func (msd *ServiceDiscoveryA) DispatchEventByServiceName(serviceName string) error { + return nil +} + +func (msd *ServiceDiscoveryA) DispatchEventForInstances(serviceName string, instances []registry.ServiceInstance) error { + return nil +} + +func (msd *ServiceDiscoveryA) DispatchEvent(event *registry.ServiceInstancesChangedEvent) error { + return nil +} + +type mockServiceNameMapping struct { +} + +func (m *mockServiceNameMapping) Map(serviceInterface string, group string, version string, protocol string) error { + return nil +} + +func (m *mockServiceNameMapping) Get(serviceInterface string, group string, version string, protocol string) (*gxset.HashSet, error) { + return gxset.NewSet("dubbo"), nil +} diff --git a/registry/event/event_publishing_service_discovery.go b/registry/event/event_publishing_service_discovery.go new file mode 100644 index 0000000000000000000000000000000000000000..3ee2f4a44946065cdf7489abc391df41f251d810 --- /dev/null +++ b/registry/event/event_publishing_service_discovery.go @@ -0,0 +1,157 @@ +/* + * 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 event + +import ( + gxset "github.com/dubbogo/gost/container/set" + gxpage "github.com/dubbogo/gost/page" +) + +import ( + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/registry" +) + +// EventPublishingServiceDiscovery will enhance Service Discovery +// Publish some event about service discovery +type EventPublishingServiceDiscovery struct { + serviceDiscovery registry.ServiceDiscovery +} + +// NewEventPublishingServiceDiscovery is a constructor +func NewEventPublishingServiceDiscovery(serviceDiscovery registry.ServiceDiscovery) *EventPublishingServiceDiscovery { + return &EventPublishingServiceDiscovery{ + serviceDiscovery: serviceDiscovery, + } +} + +// String returns serviceDiscovery.String() +func (epsd *EventPublishingServiceDiscovery) String() string { + return epsd.serviceDiscovery.String() +} + +// Destroy delegate function +func (epsd *EventPublishingServiceDiscovery) Destroy() error { + f := func() error { + return epsd.serviceDiscovery.Destroy() + } + return epsd.executeWithEvents(NewServiceDiscoveryDestroyingEvent(epsd, epsd.serviceDiscovery), + f, NewServiceDiscoveryDestroyedEvent(epsd, epsd.serviceDiscovery)) +} + +// Register delegate function +func (epsd *EventPublishingServiceDiscovery) Register(instance registry.ServiceInstance) error { + f := func() error { + return epsd.serviceDiscovery.Register(instance) + } + return epsd.executeWithEvents(NewServiceInstancePreRegisteredEvent(epsd.serviceDiscovery, instance), + f, NewServiceInstanceRegisteredEvent(epsd.serviceDiscovery, instance)) + +} + +// Update returns the result of serviceDiscovery.Update +func (epsd *EventPublishingServiceDiscovery) Update(instance registry.ServiceInstance) error { + f := func() error { + return epsd.serviceDiscovery.Update(instance) + } + return epsd.executeWithEvents(nil, f, nil) +} + +// Unregister unregister the instance and drop ServiceInstancePreUnregisteredEvent and ServiceInstanceUnregisteredEvent +func (epsd *EventPublishingServiceDiscovery) Unregister(instance registry.ServiceInstance) error { + f := func() error { + return epsd.serviceDiscovery.Unregister(instance) + } + return epsd.executeWithEvents(NewServiceInstancePreUnregisteredEvent(epsd.serviceDiscovery, instance), + f, NewServiceInstanceUnregisteredEvent(epsd.serviceDiscovery, instance)) +} + +// GetDefaultPageSize returns the result of serviceDiscovery.GetDefaultPageSize +func (epsd *EventPublishingServiceDiscovery) GetDefaultPageSize() int { + return epsd.serviceDiscovery.GetDefaultPageSize() +} + +// GetServices returns the result of serviceDiscovery.GetServices +func (epsd *EventPublishingServiceDiscovery) GetServices() *gxset.HashSet { + return epsd.serviceDiscovery.GetServices() +} + +// GetInstances returns the result of serviceDiscovery.GetInstances +func (epsd *EventPublishingServiceDiscovery) GetInstances(serviceName string) []registry.ServiceInstance { + return epsd.serviceDiscovery.GetInstances(serviceName) +} + +// GetInstancesByPage returns the result of serviceDiscovery.GetInstancesByPage +func (epsd *EventPublishingServiceDiscovery) GetInstancesByPage(serviceName string, offset int, pageSize int) gxpage.Pager { + return epsd.serviceDiscovery.GetInstancesByPage(serviceName, offset, pageSize) +} + +// GetHealthyInstancesByPage returns the result of serviceDiscovery.GetHealthyInstancesByPage +func (epsd *EventPublishingServiceDiscovery) GetHealthyInstancesByPage(serviceName string, offset int, pageSize int, healthy bool) gxpage.Pager { + return epsd.serviceDiscovery.GetHealthyInstancesByPage(serviceName, offset, pageSize, healthy) +} + +// GetRequestInstances returns result from serviceDiscovery.GetRequestInstances +func (epsd *EventPublishingServiceDiscovery) GetRequestInstances(serviceNames []string, offset int, requestedSize int) map[string]gxpage.Pager { + return epsd.serviceDiscovery.GetRequestInstances(serviceNames, offset, requestedSize) +} + +// AddListener add event listener +func (epsd *EventPublishingServiceDiscovery) AddListener(listener *registry.ServiceInstancesChangedListener) error { + extension.GetGlobalDispatcher().AddEventListener(listener) + return epsd.serviceDiscovery.AddListener(listener) +} + +// DispatchEventByServiceName pass serviceName to serviceDiscovery +func (epsd *EventPublishingServiceDiscovery) DispatchEventByServiceName(serviceName string) error { + return epsd.serviceDiscovery.DispatchEventByServiceName(serviceName) +} + +// DispatchEventForInstances pass params to serviceDiscovery +func (epsd *EventPublishingServiceDiscovery) DispatchEventForInstances(serviceName string, instances []registry.ServiceInstance) error { + return epsd.serviceDiscovery.DispatchEventForInstances(serviceName, instances) +} + +// DispatchEvent pass the event to serviceDiscovery +func (epsd *EventPublishingServiceDiscovery) DispatchEvent(event *registry.ServiceInstancesChangedEvent) error { + return epsd.serviceDiscovery.DispatchEvent(event) +} + +// executeWithEvents dispatch before event and after event if return error will dispatch exception event +func (epsd *EventPublishingServiceDiscovery) executeWithEvents(beforeEvent observer.Event, f func() error, afterEvent observer.Event) error { + globalDispatcher := extension.GetGlobalDispatcher() + if beforeEvent != nil { + globalDispatcher.Dispatch(beforeEvent) + } + if err := f(); err != nil { + globalDispatcher.Dispatch(NewServiceDiscoveryExceptionEvent(epsd, epsd.serviceDiscovery, err)) + return err + } + if afterEvent != nil { + globalDispatcher.Dispatch(afterEvent) + } + return nil +} + +// getMetadataService returns metadata service instance +func getMetadataService() (service.MetadataService, error) { + return extension.GetMetadataService(config.GetApplicationConfig().MetadataType) +} diff --git a/registry/event/log_event_listener.go b/registry/event/log_event_listener.go new file mode 100644 index 0000000000000000000000000000000000000000..0781a6d6db303ba3a1eb99b6b4c6d0743f9066b3 --- /dev/null +++ b/registry/event/log_event_listener.go @@ -0,0 +1,60 @@ +/* + * 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 event + +import ( + "reflect" + "sync" +) + +import ( + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/common/observer" +) + +func init() { + extension.AddEventListener(GetLogEventListener) +} + +// logEventListener is singleton +type logEventListener struct { +} + +func (l *logEventListener) GetPriority() int { + return 0 +} + +func (l *logEventListener) OnEvent(e observer.Event) error { + logger.Info("Event happen: " + e.String()) + return nil +} + +func (l *logEventListener) GetEventType() reflect.Type { + return reflect.TypeOf(&observer.BaseEvent{}) +} + +var logEventListenerInstance *logEventListener +var logEventListenerOnce sync.Once + +func GetLogEventListener() observer.EventListener { + logEventListenerOnce.Do(func() { + logEventListenerInstance = &logEventListener{} + }) + return logEventListenerInstance +} diff --git a/registry/event/log_event_listener_test.go b/registry/event/log_event_listener_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3136564687f74e4c5cebd13d135e097234b21284 --- /dev/null +++ b/registry/event/log_event_listener_test.go @@ -0,0 +1,32 @@ +/* + * 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 event + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +func TestLogEventListener(t *testing.T) { + l := &logEventListener{} + assert.Equal(t, 0, l.GetPriority()) + assert.Nil(t, l.OnEvent(&ServiceDiscoveryDestroyedEvent{})) +} diff --git a/registry/event/metadata_service_url_params_customizer.go b/registry/event/metadata_service_url_params_customizer.go new file mode 100644 index 0000000000000000000000000000000000000000..6d8f99b327363c9a2d636079ef1f74e78d4e0184 --- /dev/null +++ b/registry/event/metadata_service_url_params_customizer.go @@ -0,0 +1,105 @@ +/* + * 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 event + +import ( + "encoding/json" +) + +import ( + gxset "github.com/dubbogo/gost/container/set" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/registry" +) + +func init() { + exceptKeys := gxset.NewSet( + // remove APPLICATION_KEY because service name must be present + constant.APPLICATION_KEY, + // remove GROUP_KEY, always uses service name. + constant.GROUP_KEY, + // remove TIMESTAMP_KEY because it's nonsense + constant.TIMESTAMP_KEY) + extension.AddCustomizers(&metadataServiceURLParamsMetadataCustomizer{exceptKeys: exceptKeys}) + +} + +type metadataServiceURLParamsMetadataCustomizer struct { + exceptKeys *gxset.HashSet +} + +// GetPriority will return 0 so that it will be invoked in front of user defining Customizer +func (m *metadataServiceURLParamsMetadataCustomizer) GetPriority() int { + return 0 +} + +func (m *metadataServiceURLParamsMetadataCustomizer) Customize(instance registry.ServiceInstance) { + ms, err := getMetadataService() + if err != nil { + logger.Errorf("could not find the metadata service", err) + return + } + serviceName := constant.METADATA_SERVICE_NAME + // error always is nil + version, _ := ms.Version() + group := instance.GetServiceName() + urls, err := ms.GetExportedURLs(serviceName, group, version, constant.ANY_VALUE) + if err != nil || len(urls) == 0 { + logger.Info("could not find the exported urls", err) + return + } + ps := m.convertToParams(urls) + str, err := json.Marshal(ps) + if err != nil { + logger.Errorf("could not transfer the map to json", err) + return + } + instance.GetMetadata()[constant.METADATA_SERVICE_URL_PARAMS_PROPERTY_NAME] = string(str) +} + +func (m *metadataServiceURLParamsMetadataCustomizer) convertToParams(urls []interface{}) map[string]map[string]string { + + // usually there will be only one protocol + res := make(map[string]map[string]string, 1) + // those keys are useless + + for _, ui := range urls { + u, err := common.NewURL(ui.(string)) + if err != nil { + logger.Errorf("could not parse the string to url: %s", ui.(string), err) + continue + } + p := make(map[string]string, len(u.GetParams())) + for k, v := range u.GetParams() { + // we will ignore that + if m.exceptKeys.Contains(k) || len(v) == 0 || len(v[0]) == 0 { + continue + } + p[k] = v[0] + } + p[constant.PORT_KEY] = u.Port + res[u.Protocol] = p + } + return res +} diff --git a/registry/event/metadata_service_url_params_customizer_test.go b/registry/event/metadata_service_url_params_customizer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..98ae2df883f590f4c3e4b379bb5a0fcbe46d946c --- /dev/null +++ b/registry/event/metadata_service_url_params_customizer_test.go @@ -0,0 +1,124 @@ +/* + * 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 event + +import ( + "testing" +) + +import ( + gxset "github.com/dubbogo/gost/container/set" + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/registry" +) + +func prepareMetadataServiceForTest() { + config.GetApplicationConfig().MetadataType = "mock" + extension.SetMetadataService("mock", func() (service.MetadataService, error) { + return &mockMetadataService{ + urls: []interface{}{"mock://localhost:8080?a=b"}, + }, nil + }) +} + +func TestMetadataServiceURLParamsMetadataCustomizer(t *testing.T) { + + prepareMetadataServiceForTest() + + msup := &metadataServiceURLParamsMetadataCustomizer{exceptKeys: gxset.NewSet()} + assert.Equal(t, 0, msup.GetPriority()) + + msup.Customize(createInstance()) +} + +func createInstance() registry.ServiceInstance { + ins := ®istry.DefaultServiceInstance{} + return ins +} + +type mockMetadataService struct { + urls []interface{} +} + +func (m *mockMetadataService) Reference() string { + panic("implement me") +} + +func (m *mockMetadataService) ServiceName() (string, error) { + panic("implement me") +} + +func (m *mockMetadataService) ExportURL(url common.URL) (bool, error) { + panic("implement me") +} + +func (m *mockMetadataService) UnexportURL(url common.URL) error { + panic("implement me") +} + +func (m *mockMetadataService) SubscribeURL(url common.URL) (bool, error) { + panic("implement me") +} + +func (m *mockMetadataService) UnsubscribeURL(url common.URL) error { + panic("implement me") +} + +func (m *mockMetadataService) PublishServiceDefinition(url common.URL) error { + panic("implement me") +} + +func (m *mockMetadataService) GetExportedURLs(serviceInterface string, group string, version string, protocol string) ([]interface{}, error) { + return m.urls, nil +} + +func (m *mockMetadataService) MethodMapper() map[string]string { + panic("implement me") +} + +func (m *mockMetadataService) GetSubscribedURLs() ([]common.URL, error) { + res := make([]common.URL, 0, len(m.urls)) + for _, ui := range m.urls { + u, _ := common.NewURL(ui.(string)) + res = append(res, u) + } + return res, nil +} + +func (m *mockMetadataService) GetServiceDefinition(interfaceName string, group string, version string) (string, error) { + panic("implement me") +} + +func (m *mockMetadataService) GetServiceDefinitionByServiceKey(serviceKey string) (string, error) { + panic("implement me") +} + +func (m *mockMetadataService) RefreshMetadata(exportedRevision string, subscribedRevision string) (bool, error) { + panic("implement me") +} + +func (m *mockMetadataService) Version() (string, error) { + return "1.0.0", nil +} diff --git a/registry/event/protocol_ports_metadata_customizer.go b/registry/event/protocol_ports_metadata_customizer.go new file mode 100644 index 0000000000000000000000000000000000000000..a58471c2bd5a2e1b7b4211e02f605763b2e72c9c --- /dev/null +++ b/registry/event/protocol_ports_metadata_customizer.go @@ -0,0 +1,106 @@ +/* + * 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 event + +import ( + "encoding/json" + "strconv" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/registry" +) + +func init() { + extension.AddCustomizers(&ProtocolPortsMetadataCustomizer{}) +} + +// ProtocolPortsMetadataCustomizer will update the endpoints +type ProtocolPortsMetadataCustomizer struct { +} + +// GetPriority will return 0, which means it will be invoked at the beginning +func (p *ProtocolPortsMetadataCustomizer) GetPriority() int { + return 0 +} + +// Customize put the the string like [{"protocol": "dubbo", "port": 123}] into instance's metadata +func (p *ProtocolPortsMetadataCustomizer) Customize(instance registry.ServiceInstance) { + metadataService, err := getMetadataService() + if err != nil { + logger.Errorf("Could not init the MetadataService", err) + return + } + + // 4 is enough... we don't have many protocol + protocolMap := make(map[string]int, 4) + + list, err := metadataService.GetExportedURLs(constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE) + if err != nil || len(list) == 0 { + logger.Debugf("Could not find exported urls", err) + return + } + + for _, ui := range list { + u, err := common.NewURL(ui.(string)) + if err != nil || len(u.Protocol) == 0 { + logger.Errorf("the url string is invalid: %s", ui.(string), err) + continue + } + + port, err := strconv.Atoi(u.Port) + if err != nil { + logger.Errorf("Could not customize the metadata of port. ", err) + } + protocolMap[u.Protocol] = port + } + + instance.GetMetadata()[constant.SERVICE_INSTANCE_ENDPOINTS] = endpointsStr(protocolMap) +} + +// endpointsStr convert the map to json like [{"protocol": "dubbo", "port": 123}] +func endpointsStr(protocolMap map[string]int) string { + if len(protocolMap) == 0 { + return "" + } + + endpoints := make([]endpoint, 0, len(protocolMap)) + for k, v := range protocolMap { + endpoints = append(endpoints, endpoint{ + Port: v, + Protocol: k, + }) + } + + str, err := json.Marshal(endpoints) + if err != nil { + logger.Errorf("could not convert the endpoints to json", err) + return "" + } + return string(str) +} + +// nolint +type endpoint struct { + Port int `json:"port, omitempty"` + Protocol string `json:"protocol, omitempty"` +} diff --git a/registry/event/service_config_exported_event.go b/registry/event/service_config_exported_event.go new file mode 100644 index 0000000000000000000000000000000000000000..5ec027da3178f7aba066cdb1d684798e611953ea --- /dev/null +++ b/registry/event/service_config_exported_event.go @@ -0,0 +1,44 @@ +/* + * 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 event + +import ( + "time" +) + +import ( + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/config" +) + +// ServiceConfigExportedEvent represents an service was exported +type ServiceConfigExportedEvent struct { + observer.BaseEvent + ServiceConfig *config.ServiceConfig +} + +// NewServiceConfigExportedEvent create an instance +func NewServiceConfigExportedEvent(serviceConfig *config.ServiceConfig) *ServiceConfigExportedEvent { + return &ServiceConfigExportedEvent{ + BaseEvent: observer.BaseEvent{ + Source: serviceConfig, + Timestamp: time.Now(), + }, + ServiceConfig: serviceConfig, + } +} diff --git a/registry/event/service_discovery_event.go b/registry/event/service_discovery_event.go new file mode 100644 index 0000000000000000000000000000000000000000..13afa1a6aa63a8ad0721692d7e969d3af882b8f5 --- /dev/null +++ b/registry/event/service_discovery_event.go @@ -0,0 +1,103 @@ +/* + * 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 event + +import ( + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/registry" +) + +// ServiceDiscoveryEvent means that something happens to service discovery instance +type ServiceDiscoveryEvent struct { + observer.BaseEvent + original registry.ServiceDiscovery +} + +// NewServiceDiscoveryEvent returns an instance +func NewServiceDiscoveryEvent(discovery registry.ServiceDiscovery, original registry.ServiceDiscovery) *ServiceDiscoveryEvent { + return &ServiceDiscoveryEvent{ + BaseEvent: *observer.NewBaseEvent(discovery), + original: original, + } +} + +// GetServiceDiscovery returns the event source +func (sde *ServiceDiscoveryEvent) GetServiceDiscovery() registry.ServiceDiscovery { + return sde.GetSource().(registry.ServiceDiscovery) +} + +// GetOriginal actually I think we can remove this method. +func (sde *ServiceDiscoveryEvent) GetOriginal() registry.ServiceDiscovery { + return sde.original +} + +// ServiceDiscoveryDestroyingEvent +// this event will be dispatched before service discovery be destroyed +type ServiceDiscoveryDestroyingEvent struct { + ServiceDiscoveryEvent +} + +// ServiceDiscoveryExceptionEvent +// this event will be dispatched when the error occur in service discovery +type ServiceDiscoveryExceptionEvent struct { + ServiceDiscoveryEvent + err error +} + +// ServiceDiscoveryInitializedEvent +// this event will be dispatched after service discovery initialize +type ServiceDiscoveryInitializedEvent struct { + ServiceDiscoveryEvent +} + +// ServiceDiscoveryInitializingEvent +// this event will be dispatched before service discovery initialize +type ServiceDiscoveryInitializingEvent struct { + ServiceDiscoveryEvent +} + +// ServiceDiscoveryDestroyedEvent +// this event will be dispatched after service discovery be destroyed +type ServiceDiscoveryDestroyedEvent struct { + ServiceDiscoveryEvent +} + +// NewServiceDiscoveryDestroyingEvent create a ServiceDiscoveryDestroyingEvent +func NewServiceDiscoveryDestroyingEvent(discovery registry.ServiceDiscovery, original registry.ServiceDiscovery) *ServiceDiscoveryDestroyingEvent { + return &ServiceDiscoveryDestroyingEvent{*NewServiceDiscoveryEvent(discovery, original)} +} + +// NewServiceDiscoveryExceptionEvent create a ServiceDiscoveryExceptionEvent +func NewServiceDiscoveryExceptionEvent(discovery registry.ServiceDiscovery, original registry.ServiceDiscovery, err error) *ServiceDiscoveryExceptionEvent { + return &ServiceDiscoveryExceptionEvent{*NewServiceDiscoveryEvent(discovery, original), err} +} + +// NewServiceDiscoveryInitializedEvent create a ServiceDiscoveryInitializedEvent +func NewServiceDiscoveryInitializedEvent(discovery registry.ServiceDiscovery, original registry.ServiceDiscovery) *ServiceDiscoveryInitializedEvent { + return &ServiceDiscoveryInitializedEvent{*NewServiceDiscoveryEvent(discovery, original)} +} + +// NewServiceDiscoveryInitializingEvent create a ServiceDiscoveryInitializingEvent +func NewServiceDiscoveryInitializingEvent(discovery registry.ServiceDiscovery, original registry.ServiceDiscovery) *ServiceDiscoveryInitializingEvent { + return &ServiceDiscoveryInitializingEvent{*NewServiceDiscoveryEvent(discovery, original)} +} + +// NewServiceDiscoveryDestroyedEvent create a ServiceDiscoveryDestroyedEvent +func NewServiceDiscoveryDestroyedEvent(discovery registry.ServiceDiscovery, original registry.ServiceDiscovery) *ServiceDiscoveryDestroyedEvent { + return &ServiceDiscoveryDestroyedEvent{*NewServiceDiscoveryEvent(discovery, original)} +} diff --git a/registry/event/service_instance_event.go b/registry/event/service_instance_event.go new file mode 100644 index 0000000000000000000000000000000000000000..d4f23c299a844f4aab25e7d656a2cb99692861d7 --- /dev/null +++ b/registry/event/service_instance_event.go @@ -0,0 +1,87 @@ +/* + * 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 event + +import ( + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/registry" +) + +// ServiceInstanceEvent means something happen to this ServiceInstance +// like register this service instance +type ServiceInstanceEvent struct { + observer.BaseEvent + serviceInstance registry.ServiceInstance +} + +// NewServiceInstanceEvent create a ServiceInstanceEvent +func NewServiceInstanceEvent(source interface{}, instance registry.ServiceInstance) *ServiceInstanceEvent { + return &ServiceInstanceEvent{ + BaseEvent: *observer.NewBaseEvent(source), + serviceInstance: instance, + } +} + +// getServiceInstance return the service instance +func (sie *ServiceInstanceEvent) getServiceInstance() registry.ServiceInstance { + return sie.serviceInstance +} + +// ServiceInstancePreRegisteredEvent +// this event will be dispatched before service instance be registered +type ServiceInstancePreRegisteredEvent struct { + ServiceInstanceEvent +} + +// ServiceInstancePreUnregisteredEvent +// this event will be dispatched before service instance be unregistered +type ServiceInstancePreUnregisteredEvent struct { + ServiceInstanceEvent +} + +// ServiceInstanceRegisteredEvent +// this event will be dispatched after service instance be registered +type ServiceInstanceRegisteredEvent struct { + ServiceInstanceEvent +} + +// ServiceInstanceRegisteredEvent +// this event will be dispatched after service instance be unregistered +type ServiceInstanceUnregisteredEvent struct { + ServiceInstanceEvent +} + +// NewServiceInstancePreRegisteredEvent create a ServiceInstancePreRegisteredEvent +func NewServiceInstancePreRegisteredEvent(source interface{}, instance registry.ServiceInstance) *ServiceInstancePreRegisteredEvent { + return &ServiceInstancePreRegisteredEvent{*NewServiceInstanceEvent(source, instance)} +} + +// NewServiceInstancePreUnregisteredEvent create a ServiceInstancePreUnregisteredEvent +func NewServiceInstancePreUnregisteredEvent(source interface{}, instance registry.ServiceInstance) *ServiceInstancePreUnregisteredEvent { + return &ServiceInstancePreUnregisteredEvent{*NewServiceInstanceEvent(source, instance)} +} + +// NewServiceInstanceRegisteredEvent create a ServiceInstanceRegisteredEvent +func NewServiceInstanceRegisteredEvent(source interface{}, instance registry.ServiceInstance) *ServiceInstanceRegisteredEvent { + return &ServiceInstanceRegisteredEvent{*NewServiceInstanceEvent(source, instance)} +} + +// NewServiceInstanceUnregisteredEvent create a ServiceInstanceUnregisteredEvent +func NewServiceInstanceUnregisteredEvent(source interface{}, instance registry.ServiceInstance) *ServiceInstanceUnregisteredEvent { + return &ServiceInstanceUnregisteredEvent{*NewServiceInstanceEvent(source, instance)} +} diff --git a/registry/event/service_name_mapping_listener.go b/registry/event/service_name_mapping_listener.go new file mode 100644 index 0000000000000000000000000000000000000000..a4ac8b28db5a3778cf39eef98886e1825521aa44 --- /dev/null +++ b/registry/event/service_name_mapping_listener.go @@ -0,0 +1,89 @@ +/* + * 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 event + +import ( + "reflect" + "sync" +) + +import ( + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/metadata/mapping" +) + +func init() { + extension.AddEventListener(GetServiceNameMappingListener) +} + +// serviceNameMappingListener listen to service name mapping event +// usually it means that we exported some service +// it's a singleton +type serviceNameMappingListener struct { + nameMapping mapping.ServiceNameMapping +} + +// GetPriority return 3, which ensure that this listener will be invoked after log listener +func (s *serviceNameMappingListener) GetPriority() int { + return 3 +} + +// OnEvent only handle ServiceConfigExportedEvent +func (s *serviceNameMappingListener) OnEvent(e observer.Event) error { + if ex, ok := e.(*ServiceConfigExportedEvent); ok { + sc := ex.ServiceConfig + urls := sc.GetExportedUrls() + + for _, u := range urls { + err := s.nameMapping.Map(u.GetParam(constant.INTERFACE_KEY, ""), + u.GetParam(constant.GROUP_KEY, ""), + u.GetParam(constant.Version, ""), + u.Protocol) + if err != nil { + return perrors.WithMessage(err, "could not map the service: "+u.String()) + } + } + } + return nil +} + +// GetEventType return ServiceConfigExportedEvent +func (s *serviceNameMappingListener) GetEventType() reflect.Type { + return reflect.TypeOf(&ServiceConfigExportedEvent{}) +} + +var ( + serviceNameMappingListenerInstance *serviceNameMappingListener + serviceNameMappingListenerOnce sync.Once +) + +// GetServiceNameMappingListener returns an instance +func GetServiceNameMappingListener() observer.EventListener { + serviceNameMappingListenerOnce.Do(func() { + serviceNameMappingListenerInstance = &serviceNameMappingListener{ + nameMapping: extension.GetGlobalServiceNameMapping(), + } + }) + return serviceNameMappingListenerInstance +} diff --git a/registry/event/service_revision_customizer.go b/registry/event/service_revision_customizer.go new file mode 100644 index 0000000000000000000000000000000000000000..fd21e8f4c7a71cedfe1de7e9c836e7cee278182e --- /dev/null +++ b/registry/event/service_revision_customizer.go @@ -0,0 +1,137 @@ +/* + * 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 event + +import ( + "fmt" + "hash/crc32" + "sort" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/registry" +) + +const defaultRevision = "N/A" + +func init() { + extension.AddCustomizers(&exportedServicesRevisionMetadataCustomizer{}) + extension.AddCustomizers(&subscribedServicesRevisionMetadataCustomizer{}) +} + +type exportedServicesRevisionMetadataCustomizer struct { +} + +// GetPriority will return 1 so that it will be invoked in front of user defining Customizer +func (e *exportedServicesRevisionMetadataCustomizer) GetPriority() int { + return 1 +} + +// Customize calculate the revision for exported urls and then put it into instance metadata +func (e *exportedServicesRevisionMetadataCustomizer) Customize(instance registry.ServiceInstance) { + ms, err := getMetadataService() + if err != nil { + logger.Errorf("could not get metadata service", err) + return + } + + urls, err := ms.GetExportedURLs(constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE) + + if err != nil { + logger.Errorf("could not find the exported url", err) + } + + revision := resolveRevision(urls) + if len(revision) == 0 { + revision = defaultRevision + } + instance.GetMetadata()[constant.EXPORTED_SERVICES_REVISION_PROPERTY_NAME] = revision +} + +type subscribedServicesRevisionMetadataCustomizer struct { +} + +// GetPriority will return 2 so that it will be invoked in front of user defining Customizer +func (e *subscribedServicesRevisionMetadataCustomizer) GetPriority() int { + return 2 +} + +// Customize calculate the revision for subscribed urls and then put it into instance metadata +func (e *subscribedServicesRevisionMetadataCustomizer) Customize(instance registry.ServiceInstance) { + ms, err := getMetadataService() + if err != nil { + logger.Errorf("could not get metadata service", err) + return + } + + urls, err := ms.GetSubscribedURLs() + + if err != nil { + logger.Errorf("could not find the subscribed url", err) + } + + revision := resolveRevision(service.ConvertURLArrToIntfArr(urls)) + if len(revision) == 0 { + revision = defaultRevision + } + instance.GetMetadata()[constant.SUBSCRIBED_SERVICES_REVISION_PROPERTY_NAME] = revision +} + +// resolveRevision is different from Dubbo because golang doesn't support overload +// so that we could use interface + method name as identifier and ignore the method params +// per my understanding, it's enough because Dubbo actually ignore the url params. +// please refer org.apache.dubbo.common.URL#toParameterString(java.lang.String...) +func resolveRevision(urls []interface{}) string { + if len(urls) == 0 { + return "" + } + candidates := make([]string, 0, len(urls)) + + for _, ui := range urls { + u, err := common.NewURL(ui.(string)) + if err != nil { + logger.Errorf("could not parse the string to URL structure") + continue + } + sk := u.GetParam(constant.INTERFACE_KEY, "") + + if len(u.Methods) == 0 { + candidates = append(candidates, sk) + } else { + for _, m := range u.Methods { + // methods are part of candidates + candidates = append(candidates, sk+constant.KEY_SEPARATOR+m) + } + } + + // append url params if we need it + } + sort.Sort(sort.StringSlice(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) +} diff --git a/registry/event_listener.go b/registry/event_listener.go index 1805f2833c96bd08c4cf9c92337d7d221e8829e9..9e9ec2d5d4bcb8d1af90fff73db1c6708427f7f7 100644 --- a/registry/event_listener.go +++ b/registry/event_listener.go @@ -18,26 +18,39 @@ package registry import ( - gxsort "github.com/dubbogo/gost/sort" + "reflect" ) -// EventListener is an new interface used to align with dubbo 2.7.5 -// It contains the Prioritized means that the listener has its priority -type EventListener interface { - gxsort.Prioritizer - // OnEvent handle this event - OnEvent(e Event) error +import ( + "github.com/apache/dubbo-go/common/observer" +) + +// The Service Discovery Changed Event Listener +type ServiceInstancesChangedListener struct { + ServiceName string + ChangedNotify observer.ChangedNotify } -// ConditionalEventListener only handle the event which it can handle -type ConditionalEventListener interface { - EventListener - // Accept will make the decision whether it should handle this event - Accept(e Event) bool +// OnEvent on ServiceInstancesChangedEvent the service instances change event +func (lstn *ServiceInstancesChangedListener) OnEvent(e observer.Event) error { + lstn.ChangedNotify.Notify(e) + return nil } -// ServiceInstancesChangedListener is used when the Service Discovery Changed -// TODO (implement ConditionalEventListener) -type ServiceInstancesChangedListener struct { - ServiceName string +// Accept return true if the name is the same +func (lstn *ServiceInstancesChangedListener) Accept(e observer.Event) bool { + if ce, ok := e.(*ServiceInstancesChangedEvent); ok { + return ce.ServiceName == lstn.ServiceName + } + return false +} + +// GetPriority returns -1, it will be the first invoked listener +func (lstn *ServiceInstancesChangedListener) GetPriority() int { + return -1 +} + +// GetEventType returns ServiceInstancesChangedEvent +func (lstn *ServiceInstancesChangedListener) GetEventType() reflect.Type { + return reflect.TypeOf(&ServiceInstancesChangedEvent{}) } diff --git a/registry/kubernetes/registry.go b/registry/kubernetes/registry.go index f06d80124b7a627954ca3f4de0ae189e424708fd..7c5162670d85f661fb8460cc69537ac9b7a12a23 100644 --- a/registry/kubernetes/registry.go +++ b/registry/kubernetes/registry.go @@ -114,6 +114,10 @@ func (r *kubernetesRegistry) DoRegister(root string, node string) error { return r.client.Create(path.Join(root, node), "") } +func (r *kubernetesRegistry) DoUnregister(root string, node string) error { + return perrors.New("DoUnregister is not support in kubernetesRegistry") +} + // DoSubscribe actually subscribe the provider URL func (r *kubernetesRegistry) DoSubscribe(svc *common.URL) (registry.Listener, error) { @@ -147,6 +151,11 @@ func (r *kubernetesRegistry) DoSubscribe(svc *common.URL) (registry.Listener, er return configListener, nil } +// nolint +func (r *kubernetesRegistry) DoUnsubscribe(conf *common.URL) (registry.Listener, error) { + return nil, perrors.New("DoUnsubscribe is not support in kubernetesRegistry") +} + // InitListeners init listeners of kubernetes registry center func (r *kubernetesRegistry) InitListeners() { r.listener = kubernetes.NewEventListener(r.client) diff --git a/registry/mock_registry.go b/registry/mock_registry.go index 2b83d5ab8892f673e1123cd01fa74e48e3d2dc22..10561d0f49e995c94c93fa0463fc0b0421ff6e20 100644 --- a/registry/mock_registry.go +++ b/registry/mock_registry.go @@ -51,6 +51,11 @@ func (*MockRegistry) Register(url common.URL) error { return nil } +// nolint +func (r *MockRegistry) UnRegister(conf common.URL) error { + return nil +} + // nolint func (r *MockRegistry) Destroy() { if r.destroyed.CAS(false, true) { @@ -72,7 +77,7 @@ func (r *MockRegistry) subscribe(*common.URL) (Listener, error) { } // nolint -func (r *MockRegistry) Subscribe(url *common.URL, notifyListener NotifyListener) { +func (r *MockRegistry) Subscribe(url *common.URL, notifyListener NotifyListener) error { go func() { for { if !r.IsAvailable() { @@ -104,6 +109,12 @@ func (r *MockRegistry) Subscribe(url *common.URL, notifyListener NotifyListener) } } }() + return nil +} + +// UnSubscribe : +func (r *MockRegistry) UnSubscribe(url *common.URL, notifyListener NotifyListener) error { + return nil } type listener struct { diff --git a/registry/nacos/registry.go b/registry/nacos/registry.go index 3eeb7680abb3da98f5ed08f1aea57d490b2caf85..51d3e2f56abac8e4ab8b966870f1ff5bb79c4171 100644 --- a/registry/nacos/registry.go +++ b/registry/nacos/registry.go @@ -19,6 +19,7 @@ package nacos import ( "bytes" + "net" "strconv" "strings" "time" @@ -26,6 +27,9 @@ import ( import ( gxnet "github.com/dubbogo/gost/net" + "github.com/nacos-group/nacos-sdk-go/clients" + "github.com/nacos-group/nacos-sdk-go/clients/naming_client" + nacosConstant "github.com/nacos-group/nacos-sdk-go/common/constant" "github.com/nacos-group/nacos-sdk-go/vo" perrors "github.com/pkg/errors" ) @@ -43,7 +47,7 @@ var ( ) const ( - //RegistryConnDelay registry connection delay + // RegistryConnDelay registry connection delay RegistryConnDelay = 3 ) @@ -53,18 +57,8 @@ func init() { } type nacosRegistry struct { - nacosBaseRegistry -} - -// newNacosRegistry will create an instance -func newNacosRegistry(url *common.URL) (registry.Registry, error) { - base, err := newBaseRegistry(url) - if err != nil { - return nil, perrors.WithStack(err) - } - return &nacosRegistry{ - base, - }, nil + *common.URL + namingClient naming_client.INamingClient } func getCategory(url common.URL) string { @@ -137,23 +131,28 @@ func (nr *nacosRegistry) Register(url common.URL) error { return nil } +// UnRegister +func (nr *nacosRegistry) UnRegister(conf common.URL) error { + return perrors.New("UnRegister is not support in nacosRegistry") +} + func (nr *nacosRegistry) subscribe(conf *common.URL) (registry.Listener, error) { return NewNacosListener(*conf, nr.namingClient) } // subscribe from registry -func (nr *nacosRegistry) Subscribe(url *common.URL, notifyListener registry.NotifyListener) { +func (nr *nacosRegistry) Subscribe(url *common.URL, notifyListener registry.NotifyListener) error { for { if !nr.IsAvailable() { logger.Warnf("event listener game over.") - return + return perrors.New("nacosRegistry is not available.") } listener, err := nr.subscribe(url) if err != nil { if !nr.IsAvailable() { logger.Warnf("event listener game over.") - return + return err } logger.Warnf("getListener() = err:%v", perrors.WithStack(err)) time.Sleep(time.Duration(RegistryConnDelay) * time.Second) @@ -165,7 +164,7 @@ func (nr *nacosRegistry) Subscribe(url *common.URL, notifyListener registry.Noti if err != nil { logger.Warnf("Selector.watch() = error{%v}", perrors.WithStack(err)) listener.Close() - return + return err } logger.Infof("update begin, service event: %v", serviceEvent.String()) @@ -173,6 +172,12 @@ func (nr *nacosRegistry) Subscribe(url *common.URL, notifyListener registry.Noti } } + return nil +} + +// UnSubscribe : +func (nr *nacosRegistry) UnSubscribe(url *common.URL, notifyListener registry.NotifyListener) error { + return perrors.New("UnSubscribe not support in nacosRegistry") } // GetUrl gets its registration URL @@ -182,6 +187,7 @@ func (nr *nacosRegistry) GetUrl() common.URL { // IsAvailable determines nacos registry center whether it is available func (nr *nacosRegistry) IsAvailable() bool { + // TODO return true } @@ -189,3 +195,62 @@ func (nr *nacosRegistry) IsAvailable() bool { func (nr *nacosRegistry) Destroy() { return } + +// newNacosRegistry will create new instance +func newNacosRegistry(url *common.URL) (registry.Registry, error) { + nacosConfig, err := getNacosConfig(url) + if err != nil { + return &nacosRegistry{}, err + } + client, err := clients.CreateNamingClient(nacosConfig) + if err != nil { + return &nacosRegistry{}, err + } + registry := &nacosRegistry{ + URL: url, + namingClient: client, + } + return registry, nil +} + +// getNacosConfig will return the nacos config +// TODO support RemoteRef +func getNacosConfig(url *common.URL) (map[string]interface{}, error) { + if url == nil { + return nil, perrors.New("url is empty!") + } + if len(url.Location) == 0 { + return nil, perrors.New("url.location is empty!") + } + configMap := make(map[string]interface{}, 2) + + addresses := strings.Split(url.Location, ",") + serverConfigs := make([]nacosConstant.ServerConfig, 0, len(addresses)) + for _, addr := range addresses { + ip, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, perrors.WithMessagef(err, "split [%s] ", addr) + } + port, _ := strconv.Atoi(portStr) + serverConfigs = append(serverConfigs, nacosConstant.ServerConfig{ + IpAddr: ip, + Port: uint64(port), + }) + } + configMap["serverConfigs"] = serverConfigs + + var clientConfig nacosConstant.ClientConfig + timeout, err := time.ParseDuration(url.GetParam(constant.REGISTRY_TIMEOUT_KEY, constant.DEFAULT_REG_TIMEOUT)) + if err != nil { + return nil, err + } + clientConfig.TimeoutMs = uint64(timeout.Seconds() * 1000) + clientConfig.ListenInterval = 2 * clientConfig.TimeoutMs + clientConfig.CacheDir = url.GetParam(constant.NACOS_CACHE_DIR_KEY, "") + clientConfig.LogDir = url.GetParam(constant.NACOS_LOG_DIR_KEY, "") + clientConfig.Endpoint = url.GetParam(constant.NACOS_ENDPOINT, "") + clientConfig.NotLoadCacheAtStart = true + configMap["clientConfig"] = clientConfig + + return configMap, nil +} diff --git a/registry/nacos/service_discovery.go b/registry/nacos/service_discovery.go index 2611a8dc58d2a45e578d90aa6a5d1aeb7e7f4f63..63d92d70fd5e1a00f0ce1ca95b1926fb9c36c84b 100644 --- a/registry/nacos/service_discovery.go +++ b/registry/nacos/service_discovery.go @@ -17,26 +17,32 @@ package nacos +import ( + "fmt" + "sync" +) + import ( "github.com/dubbogo/gost/container/set" "github.com/dubbogo/gost/page" + "github.com/nacos-group/nacos-sdk-go/clients/naming_client" "github.com/nacos-group/nacos-sdk-go/model" "github.com/nacos-group/nacos-sdk-go/vo" perrors "github.com/pkg/errors" ) import ( - "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/extension" "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config" "github.com/apache/dubbo-go/registry" + "github.com/apache/dubbo-go/remoting/nacos" ) const ( - defaultGroup = "DEFAULT_GROUP" - idKey = "id" - defaultPageSize = 100 + defaultGroup = constant.SERVICE_DISCOVERY_DEFAULT_GROUP + idKey = "id" ) // init will put the service discovery into extension @@ -48,8 +54,12 @@ func init() { // There is a problem, the go client for nacos does not support the id field. // we will use the metadata to store the id of ServiceInstance type nacosServiceDiscovery struct { - nacosBaseRegistry group string + // descriptor is a short string about the basic information of this instance + descriptor string + + // namingClient is the Nacos' client + namingClient naming_client.INamingClient } // Destroy will close the service discovery. @@ -93,7 +103,7 @@ func (n *nacosServiceDiscovery) Unregister(instance registry.ServiceInstance) er // GetDefaultPageSize will return the constant registry.DefaultPageSize func (n *nacosServiceDiscovery) GetDefaultPageSize() int { - return defaultPageSize + return registry.DefaultPageSize } // GetServices will return the all services @@ -238,7 +248,7 @@ func (n *nacosServiceDiscovery) DispatchEventForInstances(serviceName string, in // DispatchEvent will dispatch the event func (n *nacosServiceDiscovery) DispatchEvent(event *registry.ServiceInstancesChangedEvent) error { - // TODO(waiting for event dispatcher, another task) + extension.GetGlobalDispatcher().Dispatch(event) return nil } @@ -255,10 +265,12 @@ func (n *nacosServiceDiscovery) toRegisterInstance(instance registry.ServiceInst Ip: instance.GetHost(), Port: uint64(instance.GetPort()), Metadata: metadata, - Enable: instance.IsEnable(), - Healthy: instance.IsHealthy(), - GroupName: n.group, - Ephemeral: true, + // We must specify the weight since Java nacos client will ignore the instance whose weight is 0 + Weight: 1, + Enable: instance.IsEnable(), + Healthy: instance.IsHealthy(), + GroupName: n.group, + Ephemeral: true, } } @@ -272,15 +284,58 @@ func (n *nacosServiceDiscovery) toDeregisterInstance(instance registry.ServiceIn } } -// toDeregisterInstance will create new service discovery instance -func newNacosServiceDiscovery(url *common.URL) (registry.ServiceDiscovery, error) { +func (n *nacosServiceDiscovery) String() string { + return n.descriptor +} + +var ( + // 16 would be enough. We won't use concurrentMap because in most cases, there are not race condition + instanceMap = make(map[string]registry.ServiceDiscovery, 16) + initLock sync.Mutex +) + +// newNacosServiceDiscovery will create new service discovery instance +// use double-check pattern to reduce race condition +func newNacosServiceDiscovery(name string) (registry.ServiceDiscovery, error) { + + instance, ok := instanceMap[name] + if ok { + return instance, nil + } + + initLock.Lock() + defer initLock.Unlock() + + // double check + instance, ok = instanceMap[name] + if ok { + return instance, nil + } + + sdc, ok := config.GetBaseConfig().GetServiceDiscoveries(name) + if !ok || len(sdc.RemoteRef) == 0 { + return nil, perrors.New("could not init the instance because the config is invalid") + } - base, err := newBaseRegistry(url) + remoteConfig, ok := config.GetBaseConfig().GetRemoteConfig(sdc.RemoteRef) + if !ok { + return nil, perrors.New("could not find the remote config for name: " + sdc.RemoteRef) + } + group := sdc.Group + if len(group) == 0 { + group = defaultGroup + } + + client, err := nacos.NewNacosClient(remoteConfig) if err != nil { - return nil, perrors.WithStack(err) + return nil, perrors.WithMessage(err, "create nacos client failed.") } + + descriptor := fmt.Sprintf("nacos-service-discovery[%s]", remoteConfig.Address) + return &nacosServiceDiscovery{ - nacosBaseRegistry: base, - group: url.GetParam(constant.NACOS_GROUP, defaultGroup), + group: group, + namingClient: client, + descriptor: descriptor, }, nil } diff --git a/registry/nacos/service_discovery_test.go b/registry/nacos/service_discovery_test.go index 04431a614b40288b2a21f75d69c4be313bd7721f..720c44a6f98e4693bb2395a538b2f5e679196647 100644 --- a/registry/nacos/service_discovery_test.go +++ b/registry/nacos/service_discovery_test.go @@ -18,7 +18,6 @@ package nacos import ( - "strconv" "testing" ) @@ -27,22 +26,65 @@ import ( ) import ( - "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/common/observer/dispatcher" + "github.com/apache/dubbo-go/config" "github.com/apache/dubbo-go/registry" ) +var ( + testName = "test" +) + +func Test_newNacosServiceDiscovery(t *testing.T) { + name := "nacos1" + _, err := newNacosServiceDiscovery(name) + + // the ServiceDiscoveryConfig not found + assert.NotNil(t, err) + + sdc := &config.ServiceDiscoveryConfig{ + Protocol: "nacos", + RemoteRef: "mock", + } + config.GetBaseConfig().ServiceDiscoveries[name] = sdc + + _, err = newNacosServiceDiscovery(name) + + // RemoteConfig not found + assert.NotNil(t, err) + + config.GetBaseConfig().Remotes["mock"] = &config.RemoteConfig{ + Address: "console.nacos.io:80", + TimeoutStr: "10s", + } + + res, err := newNacosServiceDiscovery(name) + assert.Nil(t, err) + assert.NotNil(t, res) + +} + func TestNacosServiceDiscovery_Destroy(t *testing.T) { - serviceDiscovry, err := extension.GetServiceDiscovery(constant.NACOS_KEY, mockUrl()) + prepareData() + serviceDiscovery, err := extension.GetServiceDiscovery(constant.NACOS_KEY, testName) assert.Nil(t, err) - assert.NotNil(t, serviceDiscovry) - err = serviceDiscovry.Destroy() + assert.NotNil(t, serviceDiscovery) + err = serviceDiscovery.Destroy() assert.Nil(t, err) - assert.Nil(t, serviceDiscovry.(*nacosServiceDiscovery).namingClient) + assert.Nil(t, serviceDiscovery.(*nacosServiceDiscovery).namingClient) } func TestNacosServiceDiscovery_CRUD(t *testing.T) { + prepareData() + extension.SetEventDispatcher("mock", func() observer.EventDispatcher { + return &dispatcher.MockEventDispatcher{} + }) + + extension.SetAndInitGlobalDispatcher("mock") + serviceName := "service-name" id := "id" host := "host" @@ -59,7 +101,7 @@ func TestNacosServiceDiscovery_CRUD(t *testing.T) { // clean data - serviceDiscovry, _ := extension.GetServiceDiscovery(constant.NACOS_KEY, mockUrl()) + serviceDiscovry, _ := extension.GetServiceDiscovery(constant.NACOS_KEY, testName) // clean data for local test serviceDiscovry.Unregister(®istry.DefaultServiceInstance{ @@ -112,11 +154,19 @@ func TestNacosServiceDiscovery_CRUD(t *testing.T) { } func TestNacosServiceDiscovery_GetDefaultPageSize(t *testing.T) { - serviceDiscovry, _ := extension.GetServiceDiscovery(constant.NACOS_KEY, mockUrl()) - assert.Equal(t, defaultPageSize, serviceDiscovry.GetDefaultPageSize()) + prepareData() + serviceDiscovry, _ := extension.GetServiceDiscovery(constant.NACOS_KEY, testName) + assert.Equal(t, registry.DefaultPageSize, serviceDiscovry.GetDefaultPageSize()) } -func mockUrl() *common.URL { - regurl, _ := common.NewURL("registry://console.nacos.io:80", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER))) - return ®url +func prepareData() { + config.GetBaseConfig().ServiceDiscoveries[testName] = &config.ServiceDiscoveryConfig{ + Protocol: "nacos", + RemoteRef: testName, + } + + config.GetBaseConfig().Remotes[testName] = &config.RemoteConfig{ + Address: "console.nacos.io:80", + TimeoutStr: "10s", + } } diff --git a/registry/protocol/protocol_test.go b/registry/protocol/protocol_test.go index 15fd3cacfacad36309e0ad4deb3c7c7441e47e26..2d6e0248fddb88bfa5ce19546fb7aed703b0fd3c 100644 --- a/registry/protocol/protocol_test.go +++ b/registry/protocol/protocol_test.go @@ -42,7 +42,9 @@ import ( ) func init() { - config.SetProviderConfig(config.ProviderConfig{ApplicationConfig: &config.ApplicationConfig{Name: "test-application"}}) + config.SetProviderConfig(config.ProviderConfig{BaseConfig: config.BaseConfig{ + ApplicationConfig: &config.ApplicationConfig{Name: "test-application"}, + }}) } func referNormal(t *testing.T, regProtocol *registryProtocol) { @@ -66,8 +68,9 @@ func referNormal(t *testing.T, regProtocol *registryProtocol) { func TestRefer(t *testing.T) { config.SetConsumerConfig( - config.ConsumerConfig{ - ApplicationConfig: &config.ApplicationConfig{Name: "test-application"}}) + config.ConsumerConfig{BaseConfig: config.BaseConfig{ + ApplicationConfig: &config.ApplicationConfig{Name: "test-application"}, + }}) regProtocol := newRegistryProtocol() referNormal(t, regProtocol) } diff --git a/registry/registry.go b/registry/registry.go index 5b37aa684ca90d1f18898b9f62f27d86a2c0fba3..bb09ead7ef2af6707345086f8695b35286d76a10 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -34,15 +34,28 @@ type Registry interface { //And it is also used for service consumer calling , register services cared about ,for dubbo's admin monitoring. Register(url common.URL) error + // UnRegister is required to support the contract: + // 1. If it is the persistent stored data of dynamic=false, the registration data can not be found, then the IllegalStateException is thrown, otherwise it is ignored. + // 2. Unregister according to the full url match. + // url Registration information , is not allowed to be empty, e.g: dubbo://10.20.153.10/org.apache.dubbo.foo.BarService?version=1.0.0&application=kylin + UnRegister(url common.URL) error + //When creating new registry extension,pls select one of the following modes. //Will remove in dubbogo version v1.1.0 //mode1 : return Listener with Next function which can return subscribe service event from registry //Deprecated! - //subscribe(common.URL) (Listener, error) + //subscribe(event.URL) (Listener, error) //Will relace mode1 in dubbogo version v1.1.0 //mode2 : callback mode, subscribe with notify(notify listener). - Subscribe(*common.URL, NotifyListener) + Subscribe(*common.URL, NotifyListener) error + + // UnSubscribe is required to support the contract: + // 1. If don't subscribe, ignore it directly. + // 2. Unsubscribe by full URL match. + // url Subscription condition, not allowed to be empty, e.g. consumer://10.20.153.10/org.apache.dubbo.foo.BarService?version=1.0.0&application=kylin + // listener A listener of the change event, not allowed to be empty + UnSubscribe(*common.URL, NotifyListener) error } // nolint diff --git a/registry/service_discovery.go b/registry/service_discovery.go index 1d5a3593e392083d2115222e131974b941a391c3..cb7a3c0182ff88995ab9dd6c920523225c3cb36c 100644 --- a/registry/service_discovery.go +++ b/registry/service_discovery.go @@ -26,6 +26,8 @@ import ( gxpage "github.com/dubbogo/gost/page" ) +const DefaultPageSize = 100 + // ServiceDiscovery is the common operations of Service Discovery type ServiceDiscovery interface { fmt.Stringer diff --git a/registry/service_instance.go b/registry/service_instance.go index 247c8567659d1d512a6685ddb0404fecd9968bcd..dbb458284d48aa350f2d5d3408b187b437ac81cd 100644 --- a/registry/service_instance.go +++ b/registry/service_instance.go @@ -17,6 +17,10 @@ package registry +import ( + gxsort "github.com/dubbogo/gost/sort" +) + // ServiceInstance is the model class of an instance of a service, which is used for service registration and discovery. type ServiceInstance interface { @@ -84,7 +88,19 @@ func (d *DefaultServiceInstance) IsHealthy() bool { return d.Healthy } -// GetMetadata will return the metadata +// GetMetadata will return the metadata, it will never return nil func (d *DefaultServiceInstance) GetMetadata() map[string]string { + if d.Metadata == nil { + d.Metadata = make(map[string]string, 0) + } return d.Metadata } + +// ServiceInstanceCustomizer is an extension point which allow user using custom logic to modify instance +// Be careful of priority. Usually you should use number between [100, 9000] +// other number will be thought as system reserve number +type ServiceInstanceCustomizer interface { + gxsort.Prioritizer + + Customize(instance ServiceInstance) +} diff --git a/registry/servicediscovery/instance/random/random_service_instance_selector.go b/registry/servicediscovery/instance/random/random_service_instance_selector.go new file mode 100644 index 0000000000000000000000000000000000000000..3f8f30dc8e9e91f9c75f8ff0611c98bb2f0c7b85 --- /dev/null +++ b/registry/servicediscovery/instance/random/random_service_instance_selector.go @@ -0,0 +1,55 @@ +/* + * 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 random + +import ( + "math/rand" + "time" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/registry" + "github.com/apache/dubbo-go/registry/servicediscovery/instance" +) + +func init() { + extension.SetServiceInstanceSelector("random", NewRandomServiceInstanceSelector) +} + +//the ServiceInstanceSelector implementation based on Random algorithm +type RandomServiceInstanceSelector struct { +} + +func NewRandomServiceInstanceSelector() instance.ServiceInstanceSelector { + return &RandomServiceInstanceSelector{} +} + +func (r *RandomServiceInstanceSelector) Select(url common.URL, serviceInstances []registry.ServiceInstance) registry.ServiceInstance { + if len(serviceInstances) == 0 { + return nil + } + if len(serviceInstances) == 1 { + return serviceInstances[0] + } + rand.Seed(time.Now().UnixNano()) + index := rand.Intn(len(serviceInstances)) + return serviceInstances[index] + +} diff --git a/registry/servicediscovery/instance/random/random_service_instance_selector_test.go b/registry/servicediscovery/instance/random/random_service_instance_selector_test.go new file mode 100644 index 0000000000000000000000000000000000000000..cddeb42c904131cdc6a62e5142de850410a3ec5a --- /dev/null +++ b/registry/servicediscovery/instance/random/random_service_instance_selector_test.go @@ -0,0 +1,56 @@ +/* + * 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 random + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/registry" +) + +func TestRandomServiceInstanceSelector_Select(t *testing.T) { + selector := NewRandomServiceInstanceSelector() + serviceInstances := []registry.ServiceInstance{ + ®istry.DefaultServiceInstance{ + Id: "1", + ServiceName: "test1", + Host: "127.0.0.1:80", + Port: 0, + Enable: false, + Healthy: false, + Metadata: nil, + }, + ®istry.DefaultServiceInstance{ + Id: "2", + ServiceName: "test2", + Host: "127.0.0.1:80", + Port: 0, + Enable: false, + Healthy: false, + Metadata: nil, + }, + } + assert.NotNil(t, selector.Select(common.URL{}, serviceInstances)) +} diff --git a/metadata/report.go b/registry/servicediscovery/instance/service_instance_selector.go similarity index 54% rename from metadata/report.go rename to registry/servicediscovery/instance/service_instance_selector.go index f2380f50cd0eb15182c137f02e5f78b4ba8e4fd2..82fb3458be2838e9a5780e95be71aa89039b664f 100644 --- a/metadata/report.go +++ b/registry/servicediscovery/instance/service_instance_selector.go @@ -15,21 +15,14 @@ * limitations under the License. */ -package metadata +package instance import ( "github.com/apache/dubbo-go/common" - "github.com/apache/dubbo-go/metadata/definition" - "github.com/apache/dubbo-go/metadata/identifier" + "github.com/apache/dubbo-go/registry" ) -type MetadataReport interface { - StoreProviderMetadata(*identifier.MetadataIdentifier, *definition.ServiceDefinition) - StoreConsumerMetadata(*identifier.MetadataIdentifier, map[string]string) - SaveServiceMetadata(*identifier.ServiceMetadataIdentifier, *common.URL) - RemoveServiceMetadata(*identifier.ServiceMetadataIdentifier) - GetExportedURLs(*identifier.ServiceMetadataIdentifier) []string - SaveSubscribedData(*identifier.SubscriberMetadataIdentifier, []*common.URL) - GetSubscribedURLs(*identifier.SubscriberMetadataIdentifier) []string - GetServiceDefinition(*identifier.MetadataIdentifier) +type ServiceInstanceSelector interface { + //Select an instance of ServiceInstance by the specified ServiceInstance service instances + Select(url common.URL, serviceInstances []registry.ServiceInstance) registry.ServiceInstance } diff --git a/registry/servicediscovery/service_discovery_registry.go b/registry/servicediscovery/service_discovery_registry.go new file mode 100644 index 0000000000000000000000000000000000000000..061d832b0328a5e1754c7804bf40cf83ac216a8b --- /dev/null +++ b/registry/servicediscovery/service_discovery_registry.go @@ -0,0 +1,713 @@ +/* + * 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 servicediscovery + +import ( + "bytes" + "encoding/json" + "strconv" + "strings" + "sync" +) + +import ( + cm "github.com/Workiva/go-datastructures/common" + gxset "github.com/dubbogo/gost/container/set" + gxnet "github.com/dubbogo/gost/net" + perrors "github.com/pkg/errors" + "go.uber.org/atomic" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/metadata/mapping" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/metadata/service/exporter/configurable" + "github.com/apache/dubbo-go/registry" + "github.com/apache/dubbo-go/registry/event" + "github.com/apache/dubbo-go/registry/servicediscovery/synthesizer" + "github.com/apache/dubbo-go/remoting" +) + +const ( + protocolName = "service-discovery" +) + +func init() { + extension.SetRegistry(protocolName, newServiceDiscoveryRegistry) +} + +// serviceDiscoveryRegistry is the implementation of application-level registry. +// It's completely different from other registry implementations +// This implementation is based on ServiceDiscovery abstraction and ServiceNameMapping +// In order to keep compatible with interface-level registry, +// this implementation is +type serviceDiscoveryRegistry struct { + lock sync.RWMutex + url *common.URL + serviceDiscovery registry.ServiceDiscovery + subscribedServices *gxset.HashSet + serviceNameMapping mapping.ServiceNameMapping + metaDataService service.MetadataService + registeredListeners *gxset.HashSet + subscribedURLsSynthesizers []synthesizer.SubscribedURLsSynthesizer + serviceRevisionExportedURLsCache map[string]map[string][]common.URL +} + +func newServiceDiscoveryRegistry(url *common.URL) (registry.Registry, error) { + + tryInitMetadataService() + + serviceDiscovery, err := creatServiceDiscovery(url) + if err != nil { + return nil, err + } + subscribedServices := parseServices(url.GetParam(constant.SUBSCRIBED_SERVICE_NAMES_KEY, "")) + subscribedURLsSynthesizers := synthesizer.GetAllSynthesizer() + serviceNameMapping := extension.GetGlobalServiceNameMapping() + metaDataService, err := extension.GetMetadataService(config.GetApplicationConfig().MetadataType) + if err != nil { + return nil, perrors.WithMessage(err, "could not init metadata service") + } + return &serviceDiscoveryRegistry{ + url: url, + serviceDiscovery: serviceDiscovery, + subscribedServices: subscribedServices, + subscribedURLsSynthesizers: subscribedURLsSynthesizers, + registeredListeners: gxset.NewSet(), + serviceRevisionExportedURLsCache: make(map[string]map[string][]common.URL, 8), + serviceNameMapping: serviceNameMapping, + metaDataService: metaDataService, + }, nil +} + +func (s *serviceDiscoveryRegistry) UnRegister(url common.URL) error { + if !shouldRegister(url) { + return nil + } + return s.metaDataService.UnexportURL(url) +} + +func (s *serviceDiscoveryRegistry) UnSubscribe(url *common.URL, listener registry.NotifyListener) error { + if !shouldSubscribe(*url) { + return nil + } + return s.metaDataService.UnsubscribeURL(*url) +} + +func creatServiceDiscovery(url *common.URL) (registry.ServiceDiscovery, error) { + sdcName := url.GetParam(constant.SERVICE_DISCOVERY_KEY, "") + sdc, ok := config.GetBaseConfig().GetServiceDiscoveries(sdcName) + if !ok { + return nil, perrors.Errorf("The service discovery with name: %s is not found", sdcName) + } + originServiceDiscovery, err := extension.GetServiceDiscovery(sdc.Protocol, sdcName) + if err != nil { + return nil, perrors.WithMessage(err, "Create service discovery fialed") + } + return event.NewEventPublishingServiceDiscovery(originServiceDiscovery), nil +} + +func parseServices(literalServices string) *gxset.HashSet { + set := gxset.NewSet() + if len(literalServices) == 0 { + return set + } + var splitServices = strings.Split(literalServices, ",") + for _, s := range splitServices { + if len(s) != 0 { + set.Add(s) + } + } + return set +} + +func (s *serviceDiscoveryRegistry) GetServiceDiscovery() registry.ServiceDiscovery { + return s.serviceDiscovery +} + +func (s *serviceDiscoveryRegistry) GetUrl() common.URL { + return *s.url +} + +func (s *serviceDiscoveryRegistry) IsAvailable() bool { + // TODO(whether available depends on metadata service and service discovery) + return true +} + +func (s *serviceDiscoveryRegistry) Destroy() { + err := s.serviceDiscovery.Destroy() + if err != nil { + logger.Errorf("destroy serviceDiscovery catch error:%s", err.Error()) + } +} + +func (s *serviceDiscoveryRegistry) Register(url common.URL) error { + if !shouldRegister(url) { + return nil + } + ok, err := s.metaDataService.ExportURL(url) + + if err != nil { + logger.Errorf("The URL[%s] registry catch error:%s!", url.String(), err.Error()) + return err + } + if !ok { + logger.Warnf("The URL[%s] has been registry!", url.String()) + } + + // we try to register this instance. Dubbo do this in org.apache.dubbo.config.bootstrap.DubboBootstrap + // But we don't want to design a similar bootstrap class. + ins, err := createInstance(url) + if err != nil { + return perrors.WithMessage(err, "could not create servcie instance, please check your service url") + } + + err = s.serviceDiscovery.Register(ins) + if err != nil { + return perrors.WithMessage(err, "register the service failed") + } + + err = s.metaDataService.PublishServiceDefinition(url) + if err != nil { + return perrors.WithMessage(err, "publish the service definition failed. ") + } + return s.serviceNameMapping.Map(url.GetParam(constant.INTERFACE_KEY, ""), + url.GetParam(constant.GROUP_KEY, ""), + url.GetParam(constant.Version, ""), + url.Protocol) +} + +func createInstance(url common.URL) (registry.ServiceInstance, error) { + appConfig := config.GetApplicationConfig() + port, err := strconv.ParseInt(url.Port, 10, 32) + if err != nil { + return nil, perrors.WithMessage(err, "invalid port: "+url.Port) + } + + host := url.Ip + if len(host) == 0 { + host, err = gxnet.GetLocalIP() + if err != nil { + return nil, perrors.WithMessage(err, "could not get the local Ip") + } + } + + // usually we will add more metadata + metadata := make(map[string]string, 8) + metadata[constant.METADATA_STORAGE_TYPE_PROPERTY_NAME] = appConfig.MetadataType + + return ®istry.DefaultServiceInstance{ + ServiceName: appConfig.Name, + Host: host, + Port: int(port), + Id: host + constant.KEY_SEPARATOR + url.Port, + Enable: true, + Healthy: true, + Metadata: metadata, + }, nil +} + +func shouldRegister(url common.URL) bool { + side := url.GetParam(constant.SIDE_KEY, "") + if side == constant.PROVIDER_PROTOCOL { + return true + } + logger.Debugf("The URL should not be register.", url.String()) + return false +} + +func (s *serviceDiscoveryRegistry) Subscribe(url *common.URL, notify registry.NotifyListener) error { + if !shouldSubscribe(*url) { + return nil + } + _, err := s.metaDataService.SubscribeURL(*url) + if err != nil { + return perrors.WithMessage(err, "subscribe url error: "+url.String()) + } + services := s.getServices(*url) + if services.Empty() { + return perrors.Errorf("Should has at least one way to know which services this interface belongs to, "+ + "subscription url:%s", url.String()) + } + for _, srv := range services.Values() { + serviceName := srv.(string) + serviceInstances := s.serviceDiscovery.GetInstances(serviceName) + s.subscribe(url, notify, serviceName, serviceInstances) + listener := ®istry.ServiceInstancesChangedListener{ + ServiceName: serviceName, + ChangedNotify: &InstanceChangeNotify{ + notify: notify, + serviceDiscoveryRegistry: s, + }, + } + s.registerServiceInstancesChangedListener(*url, listener) + } + return nil +} + +func (s *serviceDiscoveryRegistry) registerServiceInstancesChangedListener(url common.URL, listener *registry.ServiceInstancesChangedListener) { + listenerId := listener.ServiceName + ":" + getUrlKey(url) + if !s.subscribedServices.Contains(listenerId) { + err := s.serviceDiscovery.AddListener(listener) + if err != nil { + logger.Errorf("add listener[%s] catch error,url:%s err:%s", listenerId, url.String(), err.Error()) + } + } + +} + +func getUrlKey(url common.URL) string { + var bf bytes.Buffer + if len(url.Protocol) != 0 { + bf.WriteString(url.Protocol) + bf.WriteString("://") + } + if len(url.Location) != 0 { + bf.WriteString(url.Location) + bf.WriteString(":") + bf.WriteString(url.Port) + } + if len(url.Path) != 0 { + bf.WriteString("/") + bf.WriteString(url.Path) + } + bf.WriteString("?") + appendParam(bf, constant.VERSION_KEY, url) + appendParam(bf, constant.GROUP_KEY, url) + appendParam(bf, constant.NACOS_PROTOCOL_KEY, url) + return bf.String() +} + +func appendParam(buffer bytes.Buffer, paramKey string, url common.URL) { + buffer.WriteString(paramKey) + buffer.WriteString("=") + buffer.WriteString(url.GetParam(paramKey, "")) +} + +func (s *serviceDiscoveryRegistry) subscribe(url *common.URL, notify registry.NotifyListener, + serviceName string, serviceInstances []registry.ServiceInstance) { + if len(serviceInstances) == 0 { + logger.Warnf("here is no instance in service[name : %s]", serviceName) + return + } + var subscribedURLs []common.URL + subscribedURLs = append(subscribedURLs, s.getExportedUrls(*url, serviceInstances)...) + if len(subscribedURLs) == 0 { + subscribedURLs = s.synthesizeSubscribedURLs(url, serviceInstances) + } + for _, url := range subscribedURLs { + notify.Notify(®istry.ServiceEvent{ + Action: remoting.EventTypeAdd, + Service: url, + }) + } + +} + +func (s *serviceDiscoveryRegistry) synthesizeSubscribedURLs(subscribedURL *common.URL, serviceInstances []registry.ServiceInstance) []common.URL { + var urls []common.URL + for _, syn := range s.subscribedURLsSynthesizers { + if syn.Support(subscribedURL) { + urls = append(urls, syn.Synthesize(subscribedURL, serviceInstances)...) + } + } + return urls +} + +func shouldSubscribe(url common.URL) bool { + return !shouldRegister(url) +} + +func (s *serviceDiscoveryRegistry) getServices(url common.URL) *gxset.HashSet { + services := gxset.NewSet() + serviceNames := url.GetParam(constant.PROVIDER_BY, "") + if len(serviceNames) > 0 { + services = parseServices(serviceNames) + } + if services.Empty() { + services = s.findMappedServices(url) + if services.Empty() { + return s.subscribedServices + } + } + return services +} + +func (s *serviceDiscoveryRegistry) findMappedServices(url common.URL) *gxset.HashSet { + serviceInterface := url.GetParam(constant.INTERFACE_KEY, url.Path) + group := url.GetParam(constant.GROUP_KEY, "") + version := url.GetParam(constant.VERSION_KEY, "") + protocol := url.Protocol + serviceNames, err := s.serviceNameMapping.Get(serviceInterface, group, version, protocol) + if err != nil { + logger.Errorf("get serviceInterface:[%s] group:[%s] version:[%s] protocol:[%s] from "+ + "serviceNameMap error:%s", err.Error()) + return gxset.NewSet() + } + return serviceNames +} + +func (s *serviceDiscoveryRegistry) getExportedUrls(subscribedURL common.URL, serviceInstances []registry.ServiceInstance) []common.URL { + var filterInstances []registry.ServiceInstance + for _, s := range serviceInstances { + if !s.IsEnable() || !s.IsHealthy() { + continue + } + metaData := s.GetMetadata() + _, ok1 := metaData[constant.METADATA_SERVICE_URL_PARAMS_PROPERTY_NAME] + _, ok2 := metaData[constant.METADATA_SERVICE_URLS_PROPERTY_NAME] + if !ok1 && !ok2 { + continue + } + filterInstances = append(filterInstances, s) + } + if len(filterInstances) == 0 { + return []common.URL{} + } + s.prepareServiceRevisionExportedURLs(filterInstances) + subscribedURLs := s.cloneExportedURLs(subscribedURL, filterInstances) + return subscribedURLs +} + +// comparator is defined as Comparator for skip list to compare the URL +type comparator common.URL + +// Compare is defined as Comparator for skip list to compare the URL +func (c comparator) Compare(comp cm.Comparator) int { + a := common.URL(c).String() + b := common.URL(comp.(comparator)).String() + switch { + case a > b: + return 1 + case a < b: + return -1 + default: + return 0 + } +} + +func (s *serviceDiscoveryRegistry) getExportedUrlsByInst(serviceInstance registry.ServiceInstance) []common.URL { + var urls []common.URL + metadataStorageType := getExportedStoreType(serviceInstance) + proxyFactory := extension.GetMetadataServiceProxyFactory(metadataStorageType) + if proxyFactory == nil { + return urls + } + metadataService := proxyFactory.GetProxy(serviceInstance) + if metadataService == nil { + return urls + } + result, err := metadataService.GetExportedURLs(constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE, constant.ANY_VALUE) + if err != nil { + logger.Errorf("get exported urls catch error:%s,instance:%+v", err.Error(), serviceInstance) + return urls + } + + ret := make([]common.URL, 0, len(result)) + for _, ui := range result { + + u, err := common.NewURL(ui.(string)) + + if err != nil { + logger.Errorf("could not parse the url string to URL structure: %s", ui.(string), err) + continue + } + ret = append(ret, u) + } + return ret +} + +func (s *serviceDiscoveryRegistry) prepareServiceRevisionExportedURLs(serviceInstances []registry.ServiceInstance) { + s.lock.Lock() + // 1. expunge stale + s.expungeStaleRevisionExportedURLs(serviceInstances) + // 2. Initialize + s.initRevisionExportedURLs(serviceInstances) + s.lock.Unlock() +} + +func (s *serviceDiscoveryRegistry) expungeStaleRevisionExportedURLs(serviceInstances []registry.ServiceInstance) { + serviceName := serviceInstances[0].GetServiceName() + revisionExportedURLsMap, exist := s.serviceRevisionExportedURLsCache[serviceName] + if !exist { + return + } + existRevision := gxset.NewSet() + for k := range revisionExportedURLsMap { + existRevision.Add(k) + } + currentRevision := gxset.NewSet() + for _, s := range serviceInstances { + rv := getExportedServicesRevision(s) + if len(rv) != 0 { + currentRevision.Add(rv) + } + } + // staleRevisions = existedRevisions(copy) - currentRevisions + staleRevision := gxset.NewSet(existRevision.Values()...) + staleRevision.Remove(currentRevision.Values()...) + // remove exported URLs if staled + for _, s := range staleRevision.Values() { + delete(revisionExportedURLsMap, s.(string)) + } +} + +func (s *serviceDiscoveryRegistry) initRevisionExportedURLs(serviceInstances []registry.ServiceInstance) { + // initialize the revision exported URLs that the selected service instance exported + s.initSelectedRevisionExportedURLs(serviceInstances) + // initialize the revision exported URLs that other service instances exported + for _, serviceInstance := range serviceInstances { + s.initRevisionExportedURLsByInst(serviceInstance) + } +} + +func (s *serviceDiscoveryRegistry) initSelectedRevisionExportedURLs(serviceInstances []registry.ServiceInstance) { + for range serviceInstances { + selectServiceInstance := s.selectServiceInstance(serviceInstances) + revisionExportedURLs := s.initRevisionExportedURLsByInst(selectServiceInstance) + if len(revisionExportedURLs) > 0 { + // If the result is valid,break + break + } + } +} + +func (s *serviceDiscoveryRegistry) selectServiceInstance(serviceInstances []registry.ServiceInstance) registry.ServiceInstance { + size := len(serviceInstances) + if size == 0 { + return nil + } + if size == 1 { + return serviceInstances[0] + } + selectorName := s.url.GetParam(constant.SERVICE_INSTANCE_SELECTOR, "random") + selector, err := extension.GetServiceInstanceSelector(selectorName) + if err != nil { + logger.Errorf("get service instance selector cathe error:%s", err.Error()) + return nil + } + return selector.Select(*s.url, serviceInstances) +} + +func (s *serviceDiscoveryRegistry) initRevisionExportedURLsByInst(serviceInstance registry.ServiceInstance) []common.URL { + if serviceInstance == nil { + return []common.URL{} + } + serviceName := serviceInstance.GetServiceName() + revision := getExportedServicesRevision(serviceInstance) + revisionExportedURLsMap := s.serviceRevisionExportedURLsCache[serviceName] + if revisionExportedURLsMap == nil { + revisionExportedURLsMap = make(map[string][]common.URL, 4) + s.serviceRevisionExportedURLsCache[serviceName] = revisionExportedURLsMap + } + revisionExportedURLs := revisionExportedURLsMap[revision] + firstGet := false + if revisionExportedURLs == nil || len(revisionExportedURLs) == 0 { + if len(revisionExportedURLsMap) > 0 { + // The case is that current ServiceInstance with the different revision + logger.Warnf("The ServiceInstance[id: %s, host : %s , port : %s] has different revision : %s"+ + ", please make sure the service [name : %s] is changing or not.", serviceInstance.GetId(), + serviceInstance.GetHost(), serviceInstance.GetPort(), revision, serviceInstance.GetServiceName()) + } else { + firstGet = true + } + revisionExportedURLs = s.getExportedUrlsByInst(serviceInstance) + if revisionExportedURLs != nil { + revisionExportedURLsMap[revision] = revisionExportedURLs + logger.Debugf("Get the exported URLs[size : %s, first : %s] from the target service "+ + "instance [id: %s , service : %s , host : %s , port : %s , revision : %s]", + len(revisionExportedURLs), firstGet, serviceInstance.GetId(), serviceInstance.GetServiceName(), + serviceInstance.GetHost(), serviceInstance.GetPort(), revision) + } + } else { + // Else, The cache is hit + logger.Debugf("Get the exported URLs[size : %s] from cache, the instance"+ + "[id: %s , service : %s , host : %s , port : %s , revision : %s]", len(revisionExportedURLs), firstGet, + serviceInstance.GetId(), serviceInstance.GetServiceName(), serviceInstance.GetHost(), + serviceInstance.GetPort(), revision) + } + return revisionExportedURLs +} + +func getExportedServicesRevision(serviceInstance registry.ServiceInstance) string { + metaData := serviceInstance.GetMetadata() + return metaData[constant.EXPORTED_SERVICES_REVISION_PROPERTY_NAME] +} + +func getExportedStoreType(serviceInstance registry.ServiceInstance) string { + metaData := serviceInstance.GetMetadata() + result, ok := metaData[constant.METADATA_STORAGE_TYPE_PROPERTY_NAME] + if !ok { + return constant.DEFAULT_METADATA_STORAGE_TYPE + } + return result +} + +func (s *serviceDiscoveryRegistry) cloneExportedURLs(url common.URL, serviceInsances []registry.ServiceInstance) []common.URL { + if len(serviceInsances) == 0 { + return []common.URL{} + } + var clonedExportedURLs []common.URL + removeParamSet := gxset.NewSet() + removeParamSet.Add(constant.PID_KEY) + removeParamSet.Add(constant.TIMESTAMP_KEY) + for _, serviceInstance := range serviceInsances { + templateExportURLs := s.getTemplateExportedURLs(url, serviceInstance) + host := serviceInstance.GetHost() + for _, u := range templateExportURLs { + port := strconv.Itoa(getProtocolPort(serviceInstance, u.Protocol)) + if u.Location != host || u.Port != port { + u.Port = port // reset port + u.Location = host + ":" + port // reset host + } + + cloneUrl := u.CloneExceptParams(removeParamSet) + clonedExportedURLs = append(clonedExportedURLs, *cloneUrl) + } + } + return clonedExportedURLs + +} + +type endpoint struct { + Port int `json:"port, omitempty"` + Protocol string `json:"protocol, omitempty"` +} + +func getProtocolPort(serviceInstance registry.ServiceInstance, protocol string) int { + md := serviceInstance.GetMetadata() + rawEndpoints := md[constant.SERVICE_INSTANCE_ENDPOINTS] + if len(rawEndpoints) == 0 { + return -1 + } + var endpoints []endpoint + err := json.Unmarshal([]byte(rawEndpoints), &endpoints) + if err != nil { + logger.Errorf("json umarshal rawEndpoints[%s] catch error:%s", rawEndpoints, err.Error()) + return -1 + } + for _, e := range endpoints { + if e.Protocol == protocol { + return e.Port + } + } + return -1 +} +func (s *serviceDiscoveryRegistry) getTemplateExportedURLs(url common.URL, serviceInstance registry.ServiceInstance) []common.URL { + exportedURLs := s.getRevisionExportedURLs(serviceInstance) + if len(exportedURLs) == 0 { + return []common.URL{} + } + return filterSubscribedURLs(url, exportedURLs) +} + +func (s *serviceDiscoveryRegistry) getRevisionExportedURLs(serviceInstance registry.ServiceInstance) []common.URL { + if serviceInstance == nil { + return []common.URL{} + } + serviceName := serviceInstance.GetServiceName() + revision := getExportedServicesRevision(serviceInstance) + s.lock.RLock() + revisionExportedURLsMap, exist := s.serviceRevisionExportedURLsCache[serviceName] + if !exist { + return []common.URL{} + } + exportedURLs, exist := revisionExportedURLsMap[revision] + if !exist { + return []common.URL{} + } + s.lock.RUnlock() + // Get a copy from source in order to prevent the caller trying to change the cached data + cloneExportedURLs := make([]common.URL, len(exportedURLs)) + copy(cloneExportedURLs, exportedURLs) + return cloneExportedURLs +} + +func filterSubscribedURLs(subscribedURL common.URL, exportedURLs []common.URL) []common.URL { + var filterExportedURLs []common.URL + for _, url := range exportedURLs { + if url.GetParam(constant.INTERFACE_KEY, url.Path) != subscribedURL.GetParam(constant.INTERFACE_KEY, url.Path) { + break + } + if url.GetParam(constant.VERSION_KEY, "") != subscribedURL.GetParam(constant.VERSION_KEY, "") { + break + } + if url.GetParam(constant.GROUP_KEY, "") != subscribedURL.GetParam(constant.GROUP_KEY, "") { + break + } + if len(subscribedURL.Protocol) != 0 { + if subscribedURL.Protocol != url.Protocol { + break + } + } + filterExportedURLs = append(filterExportedURLs, url) + } + return filterExportedURLs +} + +type InstanceChangeNotify struct { + notify registry.NotifyListener + serviceDiscoveryRegistry *serviceDiscoveryRegistry +} + +func (icn *InstanceChangeNotify) Notify(event observer.Event) { + + if se, ok := event.(*registry.ServiceInstancesChangedEvent); ok { + sdr := icn.serviceDiscoveryRegistry + sdr.subscribe(sdr.url, icn.notify, se.ServiceName, se.Instances) + } +} + +var ( + exporting = &atomic.Bool{} +) + +// tryInitMetadataService will try to initialize metadata service +// TODO (move to somewhere) +func tryInitMetadataService() { + + ms, err := extension.GetMetadataService(config.GetApplicationConfig().MetadataType) + if err != nil { + logger.Errorf("could not init metadata service", err) + } + + if !config.IsProvider() || exporting.Load() { + return + } + + // In theory, we can use sync.Once + // But sync.Once is not reentrant. + // Now the invocation chain is createRegistry -> tryInitMetadataService -> metadataServiceExporter.export + // -> createRegistry -> initMetadataService... + // So using sync.Once will result in dead lock + exporting.Store(true) + + expt := configurable.NewMetadataServiceExporter(ms) + + err = expt.Export() + if err != nil { + logger.Errorf("could not export the metadata service", err) + } + extension.GetGlobalDispatcher().Dispatch(event.NewServiceConfigExportedEvent(expt.(*configurable.MetadataServiceExporter).ServiceConfig)) +} diff --git a/registry/servicediscovery/service_discovery_registry_test.go b/registry/servicediscovery/service_discovery_registry_test.go new file mode 100644 index 0000000000000000000000000000000000000000..53eb86507e635be32eb362519922f7042f945519 --- /dev/null +++ b/registry/servicediscovery/service_discovery_registry_test.go @@ -0,0 +1,246 @@ +/* + * 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 servicediscovery + +import ( + "testing" +) + +import ( + "github.com/dubbogo/gost/container/set" + "github.com/dubbogo/gost/page" + "github.com/stretchr/testify/assert" +) +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/metadata/mapping" + "github.com/apache/dubbo-go/metadata/service" + "github.com/apache/dubbo-go/registry" +) + +var ( + serviceInterface = "org.apache.dubbo.metadata.MetadataService" + group = "dubbo-provider" + version = "1.0.0" +) + +func TestServiceDiscoveryRegistry_Register(t *testing.T) { + config.GetApplicationConfig().MetadataType = "mock" + extension.SetMetadataService("mock", func() (service service.MetadataService, err error) { + service = &mockMetadataService{} + return + }) + + extension.SetServiceDiscovery("mock", func(name string) (discovery registry.ServiceDiscovery, err error) { + return &mockServiceDiscovery{}, nil + }) + + extension.SetGlobalServiceNameMapping(func() mapping.ServiceNameMapping { + return &mockServiceNameMapping{} + }) + + extension.SetEventDispatcher("mock", func() observer.EventDispatcher { + return &mockEventDispatcher{} + }) + extension.SetAndInitGlobalDispatcher("mock") + + config.GetBaseConfig().ServiceDiscoveries["mock"] = &config.ServiceDiscoveryConfig{ + Protocol: "mock", + } + registryURL, _ := common.NewURL("service-discovery://localhost:12345", + common.WithParamsValue("service_discovery", "mock"), + common.WithParamsValue("subscribed-services", "a, b , c,d,e ,")) + url, _ := common.NewURL("dubbo://192.168.0.102:20880/" + serviceInterface + + "?&application=" + group + + "&interface=" + serviceInterface + + "&group=" + group + + "&version=" + version + + "&service_discovery=mock" + + "&methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs" + + "&side=provider") + registry, err := newServiceDiscoveryRegistry(®istryURL) + assert.Nil(t, err) + assert.NotNil(t, registry) + registry.Register(url) +} + +type mockEventDispatcher struct { +} + +func (m *mockEventDispatcher) AddEventListener(listener observer.EventListener) { + +} + +func (m *mockEventDispatcher) AddEventListeners(listenersSlice []observer.EventListener) { + +} + +func (m *mockEventDispatcher) RemoveEventListener(listener observer.EventListener) { + panic("implement me") +} + +func (m *mockEventDispatcher) RemoveEventListeners(listenersSlice []observer.EventListener) { + panic("implement me") +} + +func (m *mockEventDispatcher) GetAllEventListeners() []observer.EventListener { + return []observer.EventListener{} +} + +func (m *mockEventDispatcher) RemoveAllEventListeners() { + panic("implement me") +} + +func (m *mockEventDispatcher) Dispatch(event observer.Event) { +} + +type mockServiceNameMapping struct { +} + +func (m *mockServiceNameMapping) Map(serviceInterface string, group string, version string, protocol string) error { + return nil +} + +func (m *mockServiceNameMapping) Get(serviceInterface string, group string, version string, protocol string) (*gxset.HashSet, error) { + panic("implement me") +} + +type mockServiceDiscovery struct { +} + +func (m *mockServiceDiscovery) String() string { + panic("implement me") +} + +func (m *mockServiceDiscovery) Destroy() error { + panic("implement me") +} + +func (m *mockServiceDiscovery) Register(instance registry.ServiceInstance) error { + return nil +} + +func (m *mockServiceDiscovery) Update(instance registry.ServiceInstance) error { + panic("implement me") +} + +func (m *mockServiceDiscovery) Unregister(instance registry.ServiceInstance) error { + panic("implement me") +} + +func (m *mockServiceDiscovery) GetDefaultPageSize() int { + panic("implement me") +} + +func (m *mockServiceDiscovery) GetServices() *gxset.HashSet { + panic("implement me") +} + +func (m *mockServiceDiscovery) GetInstances(serviceName string) []registry.ServiceInstance { + panic("implement me") +} + +func (m *mockServiceDiscovery) GetInstancesByPage(serviceName string, offset int, pageSize int) gxpage.Pager { + panic("implement me") +} + +func (m *mockServiceDiscovery) GetHealthyInstancesByPage(serviceName string, offset int, pageSize int, healthy bool) gxpage.Pager { + panic("implement me") +} + +func (m *mockServiceDiscovery) GetRequestInstances(serviceNames []string, offset int, requestedSize int) map[string]gxpage.Pager { + panic("implement me") +} + +func (m *mockServiceDiscovery) AddListener(listener *registry.ServiceInstancesChangedListener) error { + panic("implement me") +} + +func (m *mockServiceDiscovery) DispatchEventByServiceName(serviceName string) error { + panic("implement me") +} + +func (m *mockServiceDiscovery) DispatchEventForInstances(serviceName string, instances []registry.ServiceInstance) error { + panic("implement me") +} + +func (m *mockServiceDiscovery) DispatchEvent(event *registry.ServiceInstancesChangedEvent) error { + panic("implement me") +} + +type mockMetadataService struct { +} + +func (m *mockMetadataService) Reference() string { + panic("implement me") +} + +func (m *mockMetadataService) ServiceName() (string, error) { + panic("implement me") +} + +func (m *mockMetadataService) ExportURL(url common.URL) (bool, error) { + return true, nil +} + +func (m *mockMetadataService) UnexportURL(url common.URL) error { + panic("implement me") +} + +func (m *mockMetadataService) SubscribeURL(url common.URL) (bool, error) { + panic("implement me") +} + +func (m *mockMetadataService) UnsubscribeURL(url common.URL) error { + panic("implement me") +} + +func (m *mockMetadataService) PublishServiceDefinition(url common.URL) error { + return nil +} + +func (m *mockMetadataService) GetExportedURLs(serviceInterface string, group string, version string, protocol string) ([]interface{}, error) { + panic("implement me") +} + +func (m *mockMetadataService) MethodMapper() map[string]string { + panic("implement me") +} + +func (m *mockMetadataService) GetSubscribedURLs() ([]common.URL, error) { + panic("implement me") +} + +func (m *mockMetadataService) GetServiceDefinition(interfaceName string, group string, version string) (string, error) { + panic("implement me") +} + +func (m *mockMetadataService) GetServiceDefinitionByServiceKey(serviceKey string) (string, error) { + panic("implement me") +} + +func (m *mockMetadataService) RefreshMetadata(exportedRevision string, subscribedRevision string) (bool, error) { + panic("implement me") +} + +func (m *mockMetadataService) Version() (string, error) { + panic("implement me") +} diff --git a/registry/servicediscovery/synthesizer/rest/rest_subscribed_urls_synthesizer.go b/registry/servicediscovery/synthesizer/rest/rest_subscribed_urls_synthesizer.go new file mode 100644 index 0000000000000000000000000000000000000000..086a26de58f8472e35e07a8a174fdee86afa82f2 --- /dev/null +++ b/registry/servicediscovery/synthesizer/rest/rest_subscribed_urls_synthesizer.go @@ -0,0 +1,65 @@ +/* + * 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 rest + +import ( + "net/url" + "strings" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/registry" + "github.com/apache/dubbo-go/registry/servicediscovery/synthesizer" +) + +func init() { + synthesizer.AddSynthesizer(NewRestSubscribedURLsSynthesizer()) +} + +//SubscribedURLsSynthesizer implementation for rest protocol +type RestSubscribedURLsSynthesizer struct { +} + +func (r RestSubscribedURLsSynthesizer) Support(subscribedURL *common.URL) bool { + if "rest" == subscribedURL.Protocol { + return true + } + return false +} + +func (r RestSubscribedURLsSynthesizer) Synthesize(subscribedURL *common.URL, serviceInstances []registry.ServiceInstance) []common.URL { + urls := make([]common.URL, len(serviceInstances), len(serviceInstances)) + for i, s := range serviceInstances { + splitHost := strings.Split(s.GetHost(), ":") + u := common.NewURLWithOptions(common.WithProtocol(subscribedURL.Protocol), common.WithIp(splitHost[0]), + common.WithPort(splitHost[1]), common.WithPath(subscribedURL.GetParam(constant.INTERFACE_KEY, subscribedURL.Path)), + common.WithParams(url.Values{}), + common.WithParamsValue(constant.SIDE_KEY, constant.PROVIDER_PROTOCOL), + common.WithParamsValue(constant.APPLICATION_KEY, s.GetServiceName()), + common.WithParamsValue(constant.REGISTRY_KEY, "true"), + ) + urls[i] = *u + } + return urls +} + +func NewRestSubscribedURLsSynthesizer() RestSubscribedURLsSynthesizer { + return RestSubscribedURLsSynthesizer{} +} diff --git a/registry/servicediscovery/synthesizer/rest/rest_subscribed_urls_synthesizer_test.go b/registry/servicediscovery/synthesizer/rest/rest_subscribed_urls_synthesizer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b52cc2323d6f9ae1bca8cfd1a4c5217af5e25f12 --- /dev/null +++ b/registry/servicediscovery/synthesizer/rest/rest_subscribed_urls_synthesizer_test.go @@ -0,0 +1,75 @@ +/* + * 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 rest + +import ( + "net/url" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/registry" +) + +func TestRestSubscribedURLsSynthesizer_Synthesize(t *testing.T) { + syn := RestSubscribedURLsSynthesizer{} + subUrl, _ := common.NewURL("rest://127.0.0.1:20000/org.apache.dubbo-go.mockService") + instances := []registry.ServiceInstance{ + ®istry.DefaultServiceInstance{ + Id: "test1", + ServiceName: "test1", + Host: "127.0.0.1:80", + Port: 80, + Enable: false, + Healthy: false, + Metadata: nil, + }, + ®istry.DefaultServiceInstance{ + Id: "test2", + ServiceName: "test2", + Host: "127.0.0.2:8081", + Port: 8081, + Enable: false, + Healthy: false, + Metadata: nil, + }, + } + + var expectUrls []common.URL + u1 := common.NewURLWithOptions(common.WithProtocol("rest"), common.WithIp("127.0.0.1"), + common.WithPort("80"), common.WithPath("org.apache.dubbo-go.mockService"), + common.WithParams(url.Values{}), + common.WithParamsValue(constant.SIDE_KEY, constant.PROVIDER_PROTOCOL), + common.WithParamsValue(constant.APPLICATION_KEY, "test1"), + common.WithParamsValue(constant.REGISTRY_KEY, "true")) + u2 := common.NewURLWithOptions(common.WithProtocol("rest"), common.WithIp("127.0.0.2"), + common.WithPort("8081"), common.WithPath("org.apache.dubbo-go.mockService"), + common.WithParams(url.Values{}), + common.WithParamsValue(constant.SIDE_KEY, constant.PROVIDER_PROTOCOL), + common.WithParamsValue(constant.APPLICATION_KEY, "test2"), + common.WithParamsValue(constant.REGISTRY_KEY, "true")) + expectUrls = append(expectUrls, *u1, *u2) + result := syn.Synthesize(&subUrl, instances) + assert.Equal(t, expectUrls, result) +} diff --git a/registry/servicediscovery/synthesizer/subscribed_urls_synthesizer.go b/registry/servicediscovery/synthesizer/subscribed_urls_synthesizer.go new file mode 100644 index 0000000000000000000000000000000000000000..949a5822c237de413b59d35efe94f807975795cf --- /dev/null +++ b/registry/servicediscovery/synthesizer/subscribed_urls_synthesizer.go @@ -0,0 +1,30 @@ +/* + * 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 synthesizer + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/registry" +) + +type SubscribedURLsSynthesizer interface { + // Supports the synthesis of the subscribed url or not + Support(subscribedURL *common.URL) bool + // synthesize the subscribed url + Synthesize(subscribedURL *common.URL, serviceInstances []registry.ServiceInstance) []common.URL +} diff --git a/registry/servicediscovery/synthesizer/subscribed_urls_synthesizer_factory.go b/registry/servicediscovery/synthesizer/subscribed_urls_synthesizer_factory.go new file mode 100644 index 0000000000000000000000000000000000000000..c9b1449bef1a8fba0afb8cda163d740e34ac1157 --- /dev/null +++ b/registry/servicediscovery/synthesizer/subscribed_urls_synthesizer_factory.go @@ -0,0 +1,41 @@ +/* + * 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 synthesizer + +import ( + "sync" +) + +var ( + synthesizers []SubscribedURLsSynthesizer + synthesizerMutex sync.RWMutex +) + +// nolint +func AddSynthesizer(synthesizer SubscribedURLsSynthesizer) { + synthesizerMutex.Lock() + defer synthesizerMutex.Unlock() + synthesizers = append(synthesizers, synthesizer) +} + +// nolint +func GetAllSynthesizer() []SubscribedURLsSynthesizer { + synthesizerMutex.RLock() + defer synthesizerMutex.RUnlock() + return synthesizers +} diff --git a/registry/zookeeper/listener.go b/registry/zookeeper/listener.go index 816e39bef5e8544de746cc950c599bf48eb14148..ec82fa0309118fba4b5c21772d4dfd356f3b0c5c 100644 --- a/registry/zookeeper/listener.go +++ b/registry/zookeeper/listener.go @@ -37,7 +37,7 @@ import ( // RegistryDataListener contains all URL information subscribed by zookeeper registry type RegistryDataListener struct { - subscribed map[*common.URL]config_center.ConfigurationListener + subscribed map[string]config_center.ConfigurationListener mutex sync.Mutex closed bool } @@ -45,7 +45,7 @@ type RegistryDataListener struct { // NewRegistryDataListener constructs a new RegistryDataListener func NewRegistryDataListener() *RegistryDataListener { return &RegistryDataListener{ - subscribed: make(map[*common.URL]config_center.ConfigurationListener)} + subscribed: make(map[string]config_center.ConfigurationListener)} } // SubscribeURL is used to set a watch listener for url @@ -53,7 +53,17 @@ func (l *RegistryDataListener) SubscribeURL(url *common.URL, listener config_cen if l.closed { return } - l.subscribed[url] = listener + l.subscribed[url.ServiceKey()] = listener +} + +// UnSubscribeURL is used to set a watch listener for url +func (l *RegistryDataListener) UnSubscribeURL(url *common.URL) config_center.ConfigurationListener { + if l.closed { + return nil + } + listener := l.subscribed[url.ServiceKey()] + delete(l.subscribed, url.ServiceKey()) + return listener } // DataChange accepts all events sent from the zookeeper server and trigger the corresponding listener for processing @@ -75,8 +85,8 @@ func (l *RegistryDataListener) DataChange(eventType remoting.Event) bool { if l.closed { return false } - for url, listener := range l.subscribed { - if serviceURL.URLEqual(*url) { + for serviceKey, listener := range l.subscribed { + if serviceURL.ServiceKey() == serviceKey { listener.Process( &config_center.ConfigChangeEvent{ Key: eventType.Path, @@ -101,17 +111,25 @@ func (l *RegistryDataListener) Close() { // RegistryConfigurationListener represent the processor of zookeeper watcher type RegistryConfigurationListener struct { - client *zk.ZookeeperClient - registry *zkRegistry - events chan *config_center.ConfigChangeEvent - isClosed bool - close chan struct{} - closeOnce sync.Once + client *zk.ZookeeperClient + registry *zkRegistry + events chan *config_center.ConfigChangeEvent + isClosed bool + close chan struct{} + closeOnce sync.Once + subscribeURL *common.URL } // NewRegistryConfigurationListener for listening the event of zk. -func NewRegistryConfigurationListener(client *zk.ZookeeperClient, reg *zkRegistry) *RegistryConfigurationListener { - return &RegistryConfigurationListener{client: client, registry: reg, events: make(chan *config_center.ConfigChangeEvent, 32), isClosed: false, close: make(chan struct{}, 1)} +func NewRegistryConfigurationListener(client *zk.ZookeeperClient, reg *zkRegistry, conf *common.URL) *RegistryConfigurationListener { + reg.WaitGroup().Add(1) + return &RegistryConfigurationListener{ + client: client, + registry: reg, + events: make(chan *config_center.ConfigChangeEvent, 32), + isClosed: false, + close: make(chan struct{}, 1), + subscribeURL: conf} } // Process submit the ConfigChangeEvent to the event chan to notify all observer diff --git a/registry/zookeeper/registry.go b/registry/zookeeper/registry.go index 04316bd9d88bbf84d2d0afdddedb78d37707856c..8f2ac1023b8ad34938b9996b480e3bbc4adbaaea 100644 --- a/registry/zookeeper/registry.go +++ b/registry/zookeeper/registry.go @@ -20,6 +20,7 @@ package zookeeper import ( "fmt" "net/url" + "path" "sync" "time" ) @@ -129,13 +130,17 @@ func (r *zkRegistry) InitListeners() { recoverd := r.dataListener.subscribed if recoverd != nil && len(recoverd) > 0 { // recover all subscribed url - for conf, oldListener := range recoverd { - if regConfigListener, ok := oldListener.(*RegistryConfigurationListener); ok { + for _, oldListener := range recoverd { + var ( + regConfigListener *RegistryConfigurationListener + ok bool + ) + + if regConfigListener, ok = oldListener.(*RegistryConfigurationListener); ok { regConfigListener.Close() } - newDataListener.SubscribeURL(conf, NewRegistryConfigurationListener(r.client, r)) - r.WaitGroup().Add(1) - go r.listener.ListenServiceEvent(conf, fmt.Sprintf("/dubbo/%s/"+constant.DEFAULT_CATEGORY, url.QueryEscape(conf.Service())), newDataListener) + newDataListener.SubscribeURL(regConfigListener.subscribeURL, NewRegistryConfigurationListener(r.client, r, regConfigListener.subscribeURL)) + go r.listener.ListenServiceEvent(regConfigListener.subscribeURL, fmt.Sprintf("/dubbo/%s/"+constant.DEFAULT_CATEGORY, url.QueryEscape(regConfigListener.subscribeURL.Service())), newDataListener) } } @@ -153,11 +158,24 @@ func (r *zkRegistry) DoRegister(root string, node string) error { return r.registerTempZookeeperNode(root, node) } +func (r *zkRegistry) DoUnregister(root string, node string) error { + r.cltLock.Lock() + defer r.cltLock.Unlock() + if !r.ZkClient().ZkConnValid() { + return perrors.Errorf("zk client is not valid.") + } + return r.ZkClient().Delete(path.Join(root, node)) +} + // DoSubscribe actually subscribes the provider URL func (r *zkRegistry) DoSubscribe(conf *common.URL) (registry.Listener, error) { return r.getListener(conf) } +func (r *zkRegistry) DoUnsubscribe(conf *common.URL) (registry.Listener, error) { + return r.getCloseListener(conf) +} + // CloseAndNilClient closes listeners and clear client func (r *zkRegistry) CloseAndNilClient() { r.client.Close() @@ -227,9 +245,9 @@ func (r *zkRegistry) getListener(conf *common.URL) (*RegistryConfigurationListen dataListener := r.dataListener dataListener.mutex.Lock() defer dataListener.mutex.Unlock() - if r.dataListener.subscribed[conf] != nil { + if r.dataListener.subscribed[conf.ServiceKey()] != nil { - zkListener, _ := r.dataListener.subscribed[conf].(*RegistryConfigurationListener) + zkListener, _ := r.dataListener.subscribed[conf.ServiceKey()].(*RegistryConfigurationListener) if zkListener != nil { r.listenerLock.Lock() defer r.listenerLock.Unlock() @@ -241,7 +259,7 @@ func (r *zkRegistry) getListener(conf *common.URL) (*RegistryConfigurationListen } } - zkListener = NewRegistryConfigurationListener(r.client, r) + zkListener = NewRegistryConfigurationListener(r.client, r, conf) if r.listener == nil { r.cltLock.Lock() client := r.client @@ -260,8 +278,42 @@ func (r *zkRegistry) getListener(conf *common.URL) (*RegistryConfigurationListen //Interested register to dataconfig. r.dataListener.SubscribeURL(conf, zkListener) - r.WaitGroup().Add(1) + go r.listener.ListenServiceEvent(conf, fmt.Sprintf("/dubbo/%s/"+constant.DEFAULT_CATEGORY, url.QueryEscape(conf.Service())), r.dataListener) return zkListener, nil } + +func (r *zkRegistry) getCloseListener(conf *common.URL) (*RegistryConfigurationListener, error) { + + var zkListener *RegistryConfigurationListener + r.dataListener.mutex.Lock() + configurationListener := r.dataListener.subscribed[conf.ServiceKey()] + if configurationListener != nil { + + zkListener, _ := configurationListener.(*RegistryConfigurationListener) + if zkListener != nil { + if zkListener.isClosed { + return nil, perrors.New("configListener already been closed") + } + } + } + + zkListener = r.dataListener.UnSubscribeURL(conf).(*RegistryConfigurationListener) + r.dataListener.mutex.Unlock() + + if r.listener == nil { + return nil, perrors.New("listener is null can not close.") + } + + //Interested register to dataconfig. + r.listenerLock.Lock() + listener := r.listener + r.listener = nil + r.listenerLock.Unlock() + + r.dataListener.Close() + listener.Close() + + return zkListener, nil +} diff --git a/registry/zookeeper/registry_test.go b/registry/zookeeper/registry_test.go index 688deccfbec67771c4071f6307802a16e4e0fc8b..d915fc2ce10359f0dd1970daf019746ce066f511 100644 --- a/registry/zookeeper/registry_test.go +++ b/registry/zookeeper/registry_test.go @@ -45,6 +45,31 @@ func Test_Register(t *testing.T) { assert.NoError(t, err) } +func Test_UnRegister(t *testing.T) { + // register + regurl, _ := common.NewURL("registry://127.0.0.1:1111", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER))) + url, _ := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider", common.WithParamsValue(constant.CLUSTER_KEY, "mock"), common.WithParamsValue("serviceid", "soa.mock"), common.WithMethods([]string{"GetUser", "AddUser"})) + + ts, reg, _ := newMockZkRegistry(®url) + defer ts.Stop() + err := reg.Register(url) + children, _ := reg.client.GetChildren("/dubbo/com.ikurento.user.UserProvider/providers") + assert.Regexp(t, ".*dubbo%3A%2F%2F127.0.0.1%3A20000%2Fcom.ikurento.user.UserProvider%3Fanyhost%3Dtrue%26cluster%3Dmock%26.*.serviceid%3Dsoa.mock", children) + assert.NoError(t, err) + + err = reg.UnRegister(url) + children, err = reg.client.GetChildren("/dubbo/com.ikurento.user.UserProvider/providers") + assert.Equal(t, 0, len(children)) + assert.Error(t, err) + assert.True(t, reg.IsAvailable()) + + err = reg.Register(url) + children, _ = reg.client.GetChildren("/dubbo/com.ikurento.user.UserProvider/providers") + assert.Regexp(t, ".*dubbo%3A%2F%2F127.0.0.1%3A20000%2Fcom.ikurento.user.UserProvider%3Fanyhost%3Dtrue%26cluster%3Dmock%26.*.serviceid%3Dsoa.mock", children) + assert.NoError(t, err) + +} + func Test_Subscribe(t *testing.T) { regurl, _ := common.NewURL("registry://127.0.0.1:1111", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER))) url, _ := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider", common.WithParamsValue(constant.CLUSTER_KEY, "mock"), common.WithMethods([]string{"GetUser", "AddUser"})) @@ -74,6 +99,39 @@ func Test_Subscribe(t *testing.T) { defer ts.Stop() } +func Test_UnSubscribe(t *testing.T) { + regurl, _ := common.NewURL("registry://127.0.0.1:1111", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER))) + url, _ := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider", common.WithParamsValue(constant.CLUSTER_KEY, "mock"), common.WithMethods([]string{"GetUser", "AddUser"})) + ts, reg, _ := newMockZkRegistry(®url) + + //provider register + err := reg.Register(url) + assert.NoError(t, err) + + if err != nil { + return + } + + //consumer register + regurl.SetParam(constant.ROLE_KEY, strconv.Itoa(common.CONSUMER)) + _, reg2, _ := newMockZkRegistry(®url, zookeeper.WithTestCluster(ts)) + + reg2.Register(url) + listener, _ := reg2.DoSubscribe(&url) + + serviceEvent, _ := listener.Next() + assert.NoError(t, err) + if err != nil { + return + } + assert.Regexp(t, ".*ServiceEvent{Action{add}.*", serviceEvent.String()) + + reg2.UnSubscribe(&url, nil) + assert.Nil(t, reg2.listener) + + defer ts.Stop() +} + func Test_ConsumerDestory(t *testing.T) { regurl, _ := common.NewURL("registry://127.0.0.1:1111", common.WithParamsValue(constant.ROLE_KEY, strconv.Itoa(common.CONSUMER))) url, _ := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider", common.WithParamsValue(constant.CLUSTER_KEY, "mock"), common.WithMethods([]string{"GetUser", "AddUser"})) diff --git a/registry/zookeeper/service_discovery.go b/registry/zookeeper/service_discovery.go new file mode 100644 index 0000000000000000000000000000000000000000..5ad83ef90947afc0a5ca75af5009e8b55b4f6627 --- /dev/null +++ b/registry/zookeeper/service_discovery.go @@ -0,0 +1,351 @@ +/* + * 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 zookeeper + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "sync" +) + +import ( + "github.com/dubbogo/gost/container/set" + "github.com/dubbogo/gost/page" + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/registry" + "github.com/apache/dubbo-go/remoting" + "github.com/apache/dubbo-go/remoting/zookeeper" + "github.com/apache/dubbo-go/remoting/zookeeper/curator_discovery" +) + +const ( + // RegistryZkClient zk client name + ServiceDiscoveryZkClient = "zk service discovery" +) + +var ( + // 16 would be enough. We won't use concurrentMap because in most cases, there are not race condition + instanceMap = make(map[string]registry.ServiceDiscovery, 16) + initLock sync.Mutex +) + +// init will put the service discovery into extension +func init() { + extension.SetServiceDiscovery(constant.ZOOKEEPER_KEY, newZookeeperServiceDiscovery) +} + +type zookeeperServiceDiscovery struct { + client *zookeeper.ZookeeperClient + csd *curator_discovery.ServiceDiscovery + listener *zookeeper.ZkEventListener + url *common.URL + wg sync.WaitGroup + cltLock sync.Mutex + listenLock sync.Mutex + done chan struct{} + rootPath string + listenNames []string +} + +// newZookeeperServiceDiscovery the constructor of newZookeeperServiceDiscovery +func newZookeeperServiceDiscovery(name string) (registry.ServiceDiscovery, error) { + instance, ok := instanceMap[name] + if ok { + return instance, nil + } + + initLock.Lock() + defer initLock.Unlock() + + // double check + instance, ok = instanceMap[name] + if ok { + return instance, nil + } + + sdc, ok := config.GetBaseConfig().GetServiceDiscoveries(name) + if !ok || len(sdc.RemoteRef) == 0 { + return nil, perrors.New("could not init the instance because the config is invalid") + } + remoteConfig, ok := config.GetBaseConfig().GetRemoteConfig(sdc.RemoteRef) + if !ok { + return nil, perrors.New("could not find the remote config for name: " + sdc.RemoteRef) + } + rootPath := remoteConfig.GetParam("rootPath", "/services") + url := common.NewURLWithOptions( + common.WithParams(make(url.Values)), + common.WithPassword(remoteConfig.Password), + common.WithUsername(remoteConfig.Username), + common.WithParamsValue(constant.REGISTRY_TIMEOUT_KEY, remoteConfig.TimeoutStr)) + url.Location = remoteConfig.Address + zksd := &zookeeperServiceDiscovery{ + url: url, + rootPath: rootPath, + } + err := zookeeper.ValidateZookeeperClient(zksd, zookeeper.WithZkName(ServiceDiscoveryZkClient)) + if err != nil { + return nil, err + } + go zookeeper.HandleClientRestart(zksd) + zksd.csd = curator_discovery.NewServiceDiscovery(zksd.client, rootPath) + return zksd, nil +} + +// nolint +func (zksd *zookeeperServiceDiscovery) ZkClient() *zookeeper.ZookeeperClient { + return zksd.client +} + +// nolint +func (zksd *zookeeperServiceDiscovery) SetZkClient(client *zookeeper.ZookeeperClient) { + zksd.client = client +} + +// nolint +func (zksd *zookeeperServiceDiscovery) ZkClientLock() *sync.Mutex { + return &zksd.cltLock +} + +// nolint +func (zksd *zookeeperServiceDiscovery) WaitGroup() *sync.WaitGroup { + return &zksd.wg +} + +// nolint +func (zksd *zookeeperServiceDiscovery) Done() chan struct{} { + return zksd.done +} + +// RestartCallBack when zookeeper connection reconnect this function will be invoked. +// try to re-register service, and listen services +func (zksd *zookeeperServiceDiscovery) RestartCallBack() bool { + zksd.csd.ReRegisterServices() + zksd.listenLock.Lock() + defer zksd.listenLock.Unlock() + for _, name := range zksd.listenNames { + zksd.csd.ListenServiceEvent(name, zksd) + } + return true +} + +// nolint +func (zksd *zookeeperServiceDiscovery) GetUrl() common.URL { + return *zksd.url +} + +// nolint +func (zksd *zookeeperServiceDiscovery) String() string { + return fmt.Sprintf("zookeeper-service-discovery[%s]", zksd.url) +} + +// Close client be closed +func (zksd *zookeeperServiceDiscovery) Destroy() error { + zksd.client.Close() + return nil +} + +// Register will register service in zookeeper, instance convert to curator's service instance +// which define in curator-x-discovery. +func (zksd *zookeeperServiceDiscovery) Register(instance registry.ServiceInstance) error { + cris := zksd.toCuratorInstance(instance) + return zksd.csd.RegisterService(cris) +} + +// Register will update service in zookeeper, instance convert to curator's service instance +// which define in curator-x-discovery, please refer to https://github.com/apache/curator. +func (zksd *zookeeperServiceDiscovery) Update(instance registry.ServiceInstance) error { + cris := zksd.toCuratorInstance(instance) + return zksd.csd.UpdateService(cris) +} + +// Unregister will unregister the instance in zookeeper +func (zksd *zookeeperServiceDiscovery) Unregister(instance registry.ServiceInstance) error { + cris := zksd.toCuratorInstance(instance) + return zksd.csd.UnregisterService(cris) +} + +// GetDefaultPageSize will return the constant registry.DefaultPageSize +func (zksd *zookeeperServiceDiscovery) GetDefaultPageSize() int { + return registry.DefaultPageSize +} + +// GetServices will return the all services in zookeeper +func (zksd *zookeeperServiceDiscovery) GetServices() *gxset.HashSet { + services, err := zksd.csd.QueryForNames() + res := gxset.NewSet() + if err != nil { + logger.Errorf("[zkServiceDiscovery] Could not query the services: %v", err) + return res + } + for _, service := range services { + res.Add(service) + } + return res +} + +// GetInstances will return the instances in a service +func (zksd *zookeeperServiceDiscovery) GetInstances(serviceName string) []registry.ServiceInstance { + criss, err := zksd.csd.QueryForInstances(serviceName) + if err != nil { + logger.Errorf("[zkServiceDiscovery] Could not query the instances for service{%s}, error = err{%v} ", + serviceName, err) + return make([]registry.ServiceInstance, 0, 0) + } + iss := make([]registry.ServiceInstance, 0, len(criss)) + for _, cris := range criss { + iss = append(iss, zksd.toZookeeperInstance(cris)) + } + return iss +} + +// GetInstancesByPage will return the instances +func (zksd *zookeeperServiceDiscovery) GetInstancesByPage(serviceName string, offset int, pageSize int) gxpage.Pager { + all := zksd.GetInstances(serviceName) + res := make([]interface{}, 0, pageSize) + // could not use res = all[a:b] here because the res should be []interface{}, not []ServiceInstance + for i := offset; i < len(all) && i < offset+pageSize; i++ { + res = append(res, all[i]) + } + return gxpage.New(offset, pageSize, res, len(all)) +} + +// GetHealthyInstancesByPage will return the instance +// In zookeeper, all service instance's is healthy. +// However, the healthy parameter in this method maybe false. So we can not use that API. +// Thus, we must query all instances and then do filter +func (zksd *zookeeperServiceDiscovery) GetHealthyInstancesByPage(serviceName string, offset int, pageSize int, healthy bool) gxpage.Pager { + all := zksd.GetInstances(serviceName) + res := make([]interface{}, 0, pageSize) + // could not use res = all[a:b] here because the res should be []interface{}, not []ServiceInstance + var ( + i = offset + count = 0 + ) + for i < len(all) && count < pageSize { + ins := all[i] + if ins.IsHealthy() == healthy { + res = append(res, all[i]) + count++ + } + i++ + } + return gxpage.New(offset, pageSize, res, len(all)) +} + +// GetRequestInstances will return the instances +func (zksd *zookeeperServiceDiscovery) GetRequestInstances(serviceNames []string, offset int, requestedSize int) map[string]gxpage.Pager { + res := make(map[string]gxpage.Pager, len(serviceNames)) + for _, name := range serviceNames { + res[name] = zksd.GetInstancesByPage(name, offset, requestedSize) + } + return res +} + +// AddListener ListenServiceEvent will add a data listener in service +func (zksd *zookeeperServiceDiscovery) AddListener(listener *registry.ServiceInstancesChangedListener) error { + zksd.listenLock.Lock() + defer zksd.listenLock.Unlock() + zksd.listenNames = append(zksd.listenNames, listener.ServiceName) + zksd.csd.ListenServiceEvent(listener.ServiceName, zksd) + return nil +} + +func (zksd *zookeeperServiceDiscovery) DispatchEventByServiceName(serviceName string) error { + return zksd.DispatchEventForInstances(serviceName, zksd.GetInstances(serviceName)) +} + +// DispatchEventForInstances dispatch ServiceInstancesChangedEvent +func (zksd *zookeeperServiceDiscovery) DispatchEventForInstances(serviceName string, instances []registry.ServiceInstance) error { + return zksd.DispatchEvent(registry.NewServiceInstancesChangedEvent(serviceName, instances)) +} + +// nolint +func (zksd *zookeeperServiceDiscovery) DispatchEvent(event *registry.ServiceInstancesChangedEvent) error { + extension.GetGlobalDispatcher().Dispatch(event) + return nil +} + +// DataChange implement DataListener's DataChange function +// to resolve event to do DispatchEventByServiceName +func (zksd *zookeeperServiceDiscovery) DataChange(eventType remoting.Event) bool { + path := strings.TrimPrefix(eventType.Path, zksd.rootPath) + path = strings.TrimPrefix(path, constant.PATH_SEPARATOR) + // get service name in zk path + serviceName := strings.Split(path, constant.PATH_SEPARATOR)[0] + err := zksd.DispatchEventByServiceName(serviceName) + if err != nil { + logger.Errorf("[zkServiceDiscovery] DispatchEventByServiceName{%s} error = err{%v}", serviceName, err) + return false + } + return true +} + +// toCuratorInstance convert to curator's service instance +func (zksd *zookeeperServiceDiscovery) toCuratorInstance(instance registry.ServiceInstance) *curator_discovery.ServiceInstance { + id := instance.GetHost() + ":" + strconv.Itoa(instance.GetPort()) + pl := make(map[string]interface{}, 8) + pl["id"] = id + pl["name"] = instance.GetServiceName() + pl["metadata"] = instance.GetMetadata() + cuis := &curator_discovery.ServiceInstance{ + Name: instance.GetServiceName(), + Id: id, + Address: instance.GetHost(), + Port: instance.GetPort(), + Payload: pl, + RegistrationTimeUTC: 0, + } + return cuis +} + +// toZookeeperInstance convert to registry's service instance +func (zksd *zookeeperServiceDiscovery) toZookeeperInstance(cris *curator_discovery.ServiceInstance) registry.ServiceInstance { + pl, ok := cris.Payload.(map[string]interface{}) + if !ok { + logger.Errorf("[zkServiceDiscovery] toZookeeperInstance{%s} payload is not map[string]interface{}", cris.Id) + return nil + } + mdi, ok := pl["metadata"].(map[string]interface{}) + if !ok { + logger.Errorf("[zkServiceDiscovery] toZookeeperInstance{%s} metadata is not map[string]interface{}", cris.Id) + return nil + } + md := make(map[string]string, len(mdi)) + for k, v := range mdi { + md[k] = fmt.Sprint(v) + } + return ®istry.DefaultServiceInstance{ + Id: cris.Id, + ServiceName: cris.Name, + Host: cris.Address, + Port: cris.Port, + Enable: true, + Healthy: true, + Metadata: md, + } +} diff --git a/registry/zookeeper/service_discovery_test.go b/registry/zookeeper/service_discovery_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ea3c7ddd48adc0adc4162d8306d28283575f694a --- /dev/null +++ b/registry/zookeeper/service_discovery_test.go @@ -0,0 +1,197 @@ +/* + * 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 zookeeper + +import ( + "strconv" + "sync" + "testing" +) + +import ( + "github.com/dubbogo/go-zookeeper/zk" + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/registry" +) + +var testName = "test" + +func prepareData(t *testing.T) *zk.TestCluster { + ts, err := zk.StartTestCluster(1, nil, nil) + assert.NoError(t, err) + assert.NotNil(t, ts.Servers[0]) + address := "127.0.0.1:" + strconv.Itoa(ts.Servers[0].Port) + + config.GetBaseConfig().ServiceDiscoveries[testName] = &config.ServiceDiscoveryConfig{ + Protocol: "zookeeper", + RemoteRef: "test", + } + + config.GetBaseConfig().Remotes[testName] = &config.RemoteConfig{ + Address: address, + TimeoutStr: "10s", + } + return ts +} + +func TestNewZookeeperServiceDiscovery(t *testing.T) { + name := "zookeeper1" + _, err := newZookeeperServiceDiscovery(name) + + // the ServiceDiscoveryConfig not found + assert.NotNil(t, err) + + sdc := &config.ServiceDiscoveryConfig{ + Protocol: "zookeeper", + RemoteRef: "mock", + } + config.GetBaseConfig().ServiceDiscoveries[name] = sdc + _, err = newZookeeperServiceDiscovery(name) + + // RemoteConfig not found + assert.NotNil(t, err) +} + +func TestCURDZookeeperServiceDiscovery(t *testing.T) { + ts := prepareData(t) + defer ts.Stop() + sd, err := newZookeeperServiceDiscovery(testName) + assert.Nil(t, err) + defer sd.Destroy() + md := make(map[string]string) + md["t1"] = "test1" + err = sd.Register(®istry.DefaultServiceInstance{ + Id: "testId", + ServiceName: testName, + Host: "127.0.0.1", + Port: 2233, + Enable: true, + Healthy: true, + Metadata: md, + }) + assert.Nil(t, err) + + testsPager := sd.GetHealthyInstancesByPage(testName, 0, 1, true) + assert.Equal(t, 1, testsPager.GetDataSize()) + assert.Equal(t, 1, testsPager.GetTotalPages()) + test := testsPager.GetData()[0].(registry.ServiceInstance) + assert.Equal(t, "127.0.0.1:2233", test.GetId()) + assert.Equal(t, "test1", test.GetMetadata()["t1"]) + + md["t1"] = "test12" + err = sd.Update(®istry.DefaultServiceInstance{ + Id: "testId", + ServiceName: testName, + Host: "127.0.0.1", + Port: 2233, + Enable: true, + Healthy: true, + Metadata: md, + }) + assert.Nil(t, err) + + testsPager = sd.GetInstancesByPage(testName, 0, 1) + assert.Equal(t, 1, testsPager.GetDataSize()) + test = testsPager.GetData()[0].(registry.ServiceInstance) + assert.Equal(t, "test12", test.GetMetadata()["t1"]) + + testsMap := sd.GetRequestInstances([]string{testName}, 0, 1) + assert.Equal(t, 1, len(testsMap)) + assert.Equal(t, 1, testsMap[testName].GetDataSize()) + test = testsMap[testName].GetData()[0].(registry.ServiceInstance) + assert.Equal(t, "test12", test.GetMetadata()["t1"]) + + names := sd.GetServices() + assert.Equal(t, 1, names.Size()) + assert.Equal(t, testName, names.Values()[0]) + + err = sd.Unregister(®istry.DefaultServiceInstance{ + Id: "testId", + ServiceName: testName, + Host: "127.0.0.1", + Port: 2233, + Enable: true, + Healthy: true, + Metadata: nil, + }) + assert.Nil(t, err) +} + +func TestAddListenerZookeeperServiceDiscovery(t *testing.T) { + ts := prepareData(t) + defer ts.Stop() + sd, err := newZookeeperServiceDiscovery(testName) + assert.Nil(t, err) + defer sd.Destroy() + + err = sd.Register(®istry.DefaultServiceInstance{ + Id: "testId", + ServiceName: testName, + Host: "127.0.0.1", + Port: 2233, + Enable: true, + Healthy: true, + Metadata: nil, + }) + assert.Nil(t, err) + + assert.Nil(t, err) + wg := &sync.WaitGroup{} + wg.Add(1) + tn := &testNotify{ + wg: wg, + t: t, + } + sicl := ®istry.ServiceInstancesChangedListener{ + ServiceName: testName, + ChangedNotify: tn, + } + extension.SetAndInitGlobalDispatcher("direct") + extension.GetGlobalDispatcher().AddEventListener(sicl) + err = sd.AddListener(sicl) + assert.Nil(t, err) + + err = sd.Update(®istry.DefaultServiceInstance{ + Id: "testId", + ServiceName: testName, + Host: "127.0.0.1", + Port: 2233, + Enable: true, + Healthy: true, + Metadata: nil, + }) + tn.wg.Wait() +} + +type testNotify struct { + wg *sync.WaitGroup + t *testing.T +} + +func (tn *testNotify) Notify(e observer.Event) { + ice := e.(*registry.ServiceInstancesChangedEvent) + assert.Equal(tn.t, 1, len(ice.Instances)) + assert.Equal(tn.t, "127.0.0.1:2233", ice.Instances[0].GetId()) + tn.wg.Done() +} diff --git a/remoting/consul/test_agent.go b/remoting/consul/test_agent.go new file mode 100644 index 0000000000000000000000000000000000000000..1744da7bd9992ae3cd376b22e9ea3a135dce2b16 --- /dev/null +++ b/remoting/consul/test_agent.go @@ -0,0 +1,64 @@ +/* + * 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 consul + +import ( + "io/ioutil" + "os" + "strconv" + "testing" +) + +import ( + "github.com/hashicorp/consul/agent" +) + +// Consul agent, used for test, simulates +// an embedded consul server. +type ConsulAgent struct { + dataDir string + testAgent *agent.TestAgent +} + +func NewConsulAgent(t *testing.T, port int) *ConsulAgent { + dataDir, _ := ioutil.TempDir("./", "agent") + hcl := ` + ports { + http = ` + strconv.Itoa(port) + ` + } + data_dir = "` + dataDir + `" + ` + testAgent := &agent.TestAgent{Name: t.Name(), DataDir: dataDir, HCL: hcl} + testAgent.Start(t) + + consulAgent := &ConsulAgent{ + dataDir: dataDir, + testAgent: testAgent, + } + return consulAgent +} + +func (consulAgent *ConsulAgent) Close() error { + var err error + + err = consulAgent.testAgent.Shutdown() + if err != nil { + return err + } + return os.RemoveAll(consulAgent.dataDir) +} diff --git a/remoting/consul/test_agent_test.go b/remoting/consul/test_agent_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8cf0ac6cd80e517ab7bc1b52cc7774a708082d5e --- /dev/null +++ b/remoting/consul/test_agent_test.go @@ -0,0 +1,32 @@ +/* + * 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 consul + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +func TestNewConsulAgent(t *testing.T) { + consulAgent := NewConsulAgent(t, 8500) + err := consulAgent.Close() + assert.NoError(t, err) +} diff --git a/remoting/etcdv3/client.go b/remoting/etcdv3/client.go index b337c79584cc5058e89bd582b007e72fb10da7ee..7632a7cd042d36db5e02280a14224b83e8736e6c 100644 --- a/remoting/etcdv3/client.go +++ b/remoting/etcdv3/client.go @@ -19,7 +19,6 @@ package etcdv3 import ( "context" - "path" "sync" "time" ) @@ -42,6 +41,8 @@ const ( MaxFailTimes = 15 // RegistryETCDV3Client client name RegistryETCDV3Client = "etcd registry" + // metadataETCDV3Client client name + MetadataETCDV3Client = "etcd metadata" ) var ( @@ -106,7 +107,7 @@ func ValidateClient(container clientFacade, opts ...Option) error { // new Client if container.Client() == nil { - newClient, err := newClient(options.name, options.endpoints, options.timeout, options.heartbeat) + newClient, err := NewClient(options.name, options.endpoints, options.timeout, options.heartbeat) if err != nil { logger.Warnf("new etcd client (name{%s}, etcd addresses{%v}, timeout{%d}) = error{%v}", options.name, options.endpoints, options.timeout, err) @@ -118,7 +119,7 @@ func ValidateClient(container clientFacade, opts ...Option) error { // Client lose connection with etcd server if container.Client().rawClient == nil { - newClient, err := newClient(options.name, options.endpoints, options.timeout, options.heartbeat) + newClient, err := NewClient(options.name, options.endpoints, options.timeout, options.heartbeat) if err != nil { logger.Warnf("new etcd client (name{%s}, etcd addresses{%v}, timeout{%d}) = error{%v}", options.name, options.endpoints, options.timeout, err) @@ -130,6 +131,26 @@ func ValidateClient(container clientFacade, opts ...Option) error { return nil } +// nolint +func NewServiceDiscoveryClient(opts ...Option) *Client { + options := &Options{ + heartbeat: 1, // default heartbeat + } + + for _, opt := range opts { + opt(options) + } + + newClient, err := NewClient(options.name, options.endpoints, options.timeout, options.heartbeat) + if err != nil { + logger.Errorf("new etcd client (name{%s}, etcd addresses{%v}, timeout{%d}) = error{%v}", + options.name, options.endpoints, options.timeout, err) + return nil + } + + return newClient +} + // Client represents etcd client Configuration type Client struct { lock sync.RWMutex @@ -148,7 +169,8 @@ type Client struct { Wait sync.WaitGroup } -func newClient(name string, endpoints []string, timeout time.Duration, heartbeat int) (*Client, error) { +// nolint +func NewClient(name string, endpoints []string, timeout time.Duration, heartbeat int) (*Client, error) { ctx, cancel := context.WithCancel(context.Background()) rawClient, err := clientv3.New(clientv3.Config{ @@ -285,6 +307,28 @@ func (c *Client) put(k string, v string, opts ...clientv3.OpOption) error { return nil } +// if k not exist will put k/v in etcd +// if k is already exist in etcd, replace it +func (c *Client) update(k string, v string, opts ...clientv3.OpOption) error { + + c.lock.RLock() + defer c.lock.RUnlock() + + if c.rawClient == nil { + return ErrNilETCDV3Client + } + + _, err := c.rawClient.Txn(c.ctx). + If(clientv3.Compare(clientv3.Version(k), "!=", -1)). + Then(clientv3.OpPut(k, v, opts...)). + Commit() + if err != nil { + return err + + } + return nil +} + func (c *Client) delete(k string) error { c.lock.RLock() @@ -455,6 +499,15 @@ func (c *Client) Create(k string, v string) error { return nil } +// Update key value ... +func (c *Client) Update(k, v string) error { + err := c.update(k, v) + if err != nil { + return perrors.WithMessagef(err, "Update k/v (key: %s value %s)", k, v) + } + return nil +} + // nolint func (c *Client) Delete(k string) error { @@ -467,16 +520,14 @@ func (c *Client) Delete(k string) error { } // RegisterTemp registers a temporary node -func (c *Client) RegisterTemp(basePath string, node string) (string, error) { +func (c *Client) RegisterTemp(k, v string) error { - completeKey := path.Join(basePath, node) - - err := c.keepAliveKV(completeKey, "") + err := c.keepAliveKV(k, v) if err != nil { - return "", perrors.WithMessagef(err, "keepalive kv (key %s)", completeKey) + return perrors.WithMessagef(err, "keepalive kv (key %s)", k) } - return completeKey, nil + return nil } // GetChildrenKVList gets children kv list by @k diff --git a/remoting/etcdv3/client_test.go b/remoting/etcdv3/client_test.go index 9a5ef60993598cf93c469989d68c991657bd5fb8..3de266f42ffbc69a1e2ba4662a9a9fff1d831cd4 100644 --- a/remoting/etcdv3/client_test.go +++ b/remoting/etcdv3/client_test.go @@ -120,7 +120,7 @@ func (suite *ClientTestSuite) TearDownSuite() { } func (suite *ClientTestSuite) setUpClient() *Client { - c, err := newClient(suite.etcdConfig.name, + c, err := NewClient(suite.etcdConfig.name, suite.etcdConfig.endpoints, suite.etcdConfig.timeout, suite.etcdConfig.heartbeat) @@ -384,7 +384,7 @@ func (suite *ClientTestSuite) TestClientRegisterTemp() { assert.Contains(t, events, eDelete) }() - _, err := c.RegisterTemp("scott", "wang") + err := c.RegisterTemp("scott/wang", "test") if err != nil { t.Fatal(err) } diff --git a/remoting/etcdv3/listener.go b/remoting/etcdv3/listener.go index 00b5b19b36d3baa8871efdd3d53e80f05d7aeac1..4f80a89dfb713036a5d4d812bc7a2d5551f42284 100644 --- a/remoting/etcdv3/listener.go +++ b/remoting/etcdv3/listener.go @@ -49,7 +49,7 @@ func NewEventListener(client *Client) *EventListener { } } -// ListenServiceNodeEvent Listen on a spec key +// listenServiceNodeEvent Listen on a spec key // this method will return true when spec key deleted, // this method will return false when deep layer connection lose func (l *EventListener) ListenServiceNodeEvent(key string, listener ...remoting.DataListener) bool { @@ -178,9 +178,9 @@ func timeSecondDuration(sec int) time.Duration { } // ListenServiceEvent is invoked by etcdv3 ConsumerRegistry::Registe/ etcdv3 ConsumerRegistry::get/etcdv3 ConsumerRegistry::getListener -// registry.go:Listen -> listenServiceEvent -> listenDirEvent -> ListenServiceNodeEvent +// registry.go:Listen -> listenServiceEvent -> listenDirEvent -> listenServiceNodeEvent // | -// --------> ListenServiceNodeEvent +// --------> listenServiceNodeEvent func (l *EventListener) ListenServiceEvent(key string, listener remoting.DataListener) { l.keyMapLock.Lock() diff --git a/registry/nacos/base_registry.go b/remoting/nacos/builder.go similarity index 64% rename from registry/nacos/base_registry.go rename to remoting/nacos/builder.go index 51c1eb8ad350231d1d2901f8acef3dbde204a741..8a247e267daa02d748c9a5e47ced698f617dfe9a 100644 --- a/registry/nacos/base_registry.go +++ b/remoting/nacos/builder.go @@ -26,6 +26,7 @@ import ( import ( "github.com/nacos-group/nacos-sdk-go/clients" + "github.com/nacos-group/nacos-sdk-go/clients/config_client" "github.com/nacos-group/nacos-sdk-go/clients/naming_client" nacosConstant "github.com/nacos-group/nacos-sdk-go/common/constant" perrors "github.com/pkg/errors" @@ -34,30 +35,16 @@ import ( import ( "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/config" ) -// baseRegistry is the parent of both interface-level registry -// and service discovery(related to application-level registry) -type nacosBaseRegistry struct { - *common.URL - namingClient naming_client.INamingClient -} - -// newBaseRegistry will create new instance -func newBaseRegistry(url *common.URL) (nacosBaseRegistry, error) { +// NewNacosConfigClient read the config from url and build an instance +func NewNacosConfigClient(url *common.URL) (config_client.IConfigClient, error) { nacosConfig, err := getNacosConfig(url) if err != nil { - return nacosBaseRegistry{}, err - } - client, err := clients.CreateNamingClient(nacosConfig) - if err != nil { - return nacosBaseRegistry{}, err - } - registry := nacosBaseRegistry{ - URL: url, - namingClient: client, + return nil, err } - return registry, nil + return clients.CreateConfigClient(nacosConfig) } // getNacosConfig will return the nacos config @@ -104,3 +91,40 @@ func getNacosConfig(url *common.URL) (map[string]interface{}, error) { return configMap, nil } + +// NewNacosClient creates an instance with the config +func NewNacosClient(rc *config.RemoteConfig) (naming_client.INamingClient, error) { + if len(rc.Address) == 0 { + return nil, perrors.New("nacos address is empty!") + } + configMap := make(map[string]interface{}, 2) + + addresses := strings.Split(rc.Address, ",") + serverConfigs := make([]nacosConstant.ServerConfig, 0, len(addresses)) + for _, addr := range addresses { + ip, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, perrors.WithMessagef(err, "split [%s] ", addr) + } + port, _ := strconv.Atoi(portStr) + serverConfigs = append(serverConfigs, nacosConstant.ServerConfig{ + IpAddr: ip, + Port: uint64(port), + }) + } + configMap["serverConfigs"] = serverConfigs + + var clientConfig nacosConstant.ClientConfig + timeout := rc.Timeout() + clientConfig.TimeoutMs = uint64(timeout.Nanoseconds() / constant.MsToNanoRate) + clientConfig.ListenInterval = 2 * clientConfig.TimeoutMs + clientConfig.CacheDir = rc.GetParam(constant.NACOS_CACHE_DIR_KEY, "") + clientConfig.LogDir = rc.GetParam(constant.NACOS_LOG_DIR_KEY, "") + clientConfig.Endpoint = rc.Address + clientConfig.Username = rc.Username + clientConfig.Password = rc.Password + clientConfig.NotLoadCacheAtStart = true + configMap["clientConfig"] = clientConfig + + return clients.CreateNamingClient(configMap) +} diff --git a/metadata/namemapping/memory/service_name_mapping.go b/remoting/nacos/builder_test.go similarity index 64% rename from metadata/namemapping/memory/service_name_mapping.go rename to remoting/nacos/builder_test.go index 8a891491bdb97808b77422092a1043c1c0ffafbf..61d13ef26f9f1d17173bbeb11468f9babdade2f5 100644 --- a/metadata/namemapping/memory/service_name_mapping.go +++ b/remoting/nacos/builder_test.go @@ -15,22 +15,35 @@ * limitations under the License. */ -package memory +package nacos import ( - gxset "github.com/dubbogo/gost/container/set" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" ) import ( "github.com/apache/dubbo-go/config" ) -type InMemoryServiceNameMapping struct{} +func TestNewNacosClient(t *testing.T) { + rc := &config.RemoteConfig{} + client, err := NewNacosClient(rc) -func (i InMemoryServiceNameMapping) Map(serviceInterface string, group string, version string, protocol string) error { - return nil -} + // address is nil + assert.NotNil(t, err) + + rc.Address = "console.nacos.io:80:123" + client, err = NewNacosClient(rc) + // invalid address + assert.NotNil(t, err) -func (i InMemoryServiceNameMapping) Get(serviceInterface string, group string, version string, protocol string) (*gxset.HashSet, error) { - return gxset.NewSet(config.GetApplicationConfig().Name), nil + rc.Address = "console.nacos.io:80" + rc.TimeoutStr = "10s" + client, err = NewNacosClient(rc) + assert.NotNil(t, client) + assert.Nil(t, err) } diff --git a/remoting/zookeeper/client.go b/remoting/zookeeper/client.go index 226ec24085ddd0cfaea0c0d6a0ba6c91a9840ad0..4ca34a6aeccf7b588a96edb44b4d5913a3e0fd8e 100644 --- a/remoting/zookeeper/client.go +++ b/remoting/zookeeper/client.go @@ -42,21 +42,23 @@ const ( ) var ( - errNilZkClientConn = perrors.New("zookeeperclient{conn} is nil") + errNilZkClientConn = perrors.New("zookeeper client{conn} is nil") errNilChildren = perrors.Errorf("has none children") errNilNode = perrors.Errorf("node does not exist") ) // ZookeeperClient represents zookeeper client Configuration type ZookeeperClient struct { - name string - ZkAddrs []string - sync.RWMutex // for conn - Conn *zk.Conn - Timeout time.Duration - exit chan struct{} - Wait sync.WaitGroup - eventRegistry map[string][]*chan struct{} + name string + ZkAddrs []string + sync.RWMutex // for conn + Conn *zk.Conn + Timeout time.Duration + exit chan struct{} + Wait sync.WaitGroup + + eventRegistry map[string][]*chan struct{} + eventRegistryLock sync.RWMutex } // nolint @@ -108,14 +110,15 @@ func WithZkName(name string) Option { } // ValidateZookeeperClient validates client and sets options -func ValidateZookeeperClient(container zkClientFacade, opts ...Option) error { - var err error +func ValidateZookeeperClient(container ZkClientFacade, opts ...Option) error { + var ( + err error + ) options := &Options{} for _, opt := range opts { opt(options) } connected := false - err = nil lock := container.ZkClientLock() url := container.GetUrl() @@ -124,16 +127,15 @@ func ValidateZookeeperClient(container zkClientFacade, opts ...Option) error { defer lock.Unlock() if container.ZkClient() == nil { - //in dubbo ,every registry only connect one node ,so this is []string{r.Address} - var timeout time.Duration - timeout, err = time.ParseDuration(url.GetParam(constant.REGISTRY_TIMEOUT_KEY, constant.DEFAULT_REG_TIMEOUT)) + // in dubbo, every registry only connect one node, so this is []string{r.Address} + timeout, err := time.ParseDuration(url.GetParam(constant.REGISTRY_TIMEOUT_KEY, constant.DEFAULT_REG_TIMEOUT)) if err != nil { logger.Errorf("timeout config %v is invalid ,err is %v", url.GetParam(constant.REGISTRY_TIMEOUT_KEY, constant.DEFAULT_REG_TIMEOUT), err.Error()) return perrors.WithMessagef(err, "newZookeeperClient(address:%+v)", url.Location) } zkAddresses := strings.Split(url.Location, ",") - newClient, err := newZookeeperClient(options.zkName, zkAddresses, timeout) + newClient, err := NewZookeeperClient(options.zkName, zkAddresses, timeout) if err != nil { logger.Warnf("newZookeeperClient(name{%s}, zk address{%v}, timeout{%d}) = error{%v}", options.zkName, url.Location, timeout.String(), err) @@ -154,14 +156,15 @@ func ValidateZookeeperClient(container zkClientFacade, opts ...Option) error { } if connected { - logger.Info("Connect to zookeeper successfully, name{%s}, zk address{%v}", options.zkName, url.Location) - container.WaitGroup().Add(1) //zk client start successful, then registry wg +1 + logger.Infof("Connect to zookeeper successfully, name{%s}, zk address{%v}", options.zkName, url.Location) + container.WaitGroup().Add(1) // zk client start successful, then registry wg +1 } return perrors.WithMessagef(err, "newZookeeperClient(address:%+v)", url.PrimitiveURL) } -func newZookeeperClient(name string, zkAddrs []string, timeout time.Duration) (*ZookeeperClient, error) { +// nolint +func NewZookeeperClient(name string, zkAddrs []string, timeout time.Duration) (*ZookeeperClient, error) { var ( err error event <-chan zk.Event @@ -211,14 +214,14 @@ func NewMockZookeeperClient(name string, timeout time.Duration, opts ...Option) eventRegistry: make(map[string][]*chan struct{}), } - opions := &Options{} + options := &Options{} for _, opt := range opts { - opt(opions) + opt(options) } // connect to zookeeper - if opions.ts != nil { - ts = opions.ts + if options.ts != nil { + ts = options.ts } else { ts, err = zk.StartTestCluster(1, nil, nil) if err != nil { @@ -246,13 +249,12 @@ func (z *ZookeeperClient) HandleZkEvent(session <-chan zk.Event) { logger.Infof("zk{path:%v, name:%s} connection goroutine game over.", z.ZkAddrs, z.name) }() -LOOP: for { select { case <-z.exit: - break LOOP + return case event = <-session: - logger.Warnf("client{%s} get a zookeeper event{type:%s, server:%s, path:%s, state:%d-%s, err:%v}", + logger.Infof("client{%s} get a zookeeper event{type:%s, server:%s, path:%s, state:%d-%s, err:%v}", z.name, event.Type, event.Server, event.Path, event.State, StateToString(event.State), event.Err) switch (int)(event.State) { case (int)(zk.StateDisconnected): @@ -265,11 +267,10 @@ LOOP: if conn != nil { conn.Close() } - - break LOOP + return case (int)(zk.EventNodeDataChanged), (int)(zk.EventNodeChildrenChanged): logger.Infof("zkClient{%s} get zk node changed event{path:%s}", z.name, event.Path) - z.Lock() + z.eventRegistryLock.RLock() for p, a := range z.eventRegistry { if strings.HasPrefix(p, event.Path) { logger.Infof("send event{state:zk.EventNodeDataChange, Path:%s} notify event to path{%s} related listener", @@ -279,16 +280,18 @@ LOOP: } } } - z.Unlock() + z.eventRegistryLock.RUnlock() case (int)(zk.StateConnecting), (int)(zk.StateConnected), (int)(zk.StateHasSession): if state == (int)(zk.StateHasSession) { continue } + z.eventRegistryLock.RLock() if a, ok := z.eventRegistry[event.Path]; ok && 0 < len(a) { for _, e := range a { *e <- struct{}{} } } + z.eventRegistryLock.RUnlock() } state = (int)(event.State) } @@ -301,13 +304,12 @@ func (z *ZookeeperClient) RegisterEvent(zkPath string, event *chan struct{}) { return } - z.Lock() + z.eventRegistryLock.Lock() + defer z.eventRegistryLock.Unlock() a := z.eventRegistry[zkPath] a = append(a, event) - z.eventRegistry[zkPath] = a logger.Debugf("zkClient{%s} register event{path:%s, ptr:%p}", z.name, zkPath, event) - z.Unlock() } // UnregisterEvent unregisters zookeeper events @@ -315,16 +317,16 @@ func (z *ZookeeperClient) UnregisterEvent(zkPath string, event *chan struct{}) { if zkPath == "" { return } - z.Lock() - defer z.Unlock() + + z.eventRegistryLock.Lock() + defer z.eventRegistryLock.Unlock() infoList, ok := z.eventRegistry[zkPath] if !ok { return } for i, e := range infoList { if e == event { - arr := infoList - infoList = append(arr[:i], arr[i+1:]...) + infoList = append(infoList[:i], infoList[i+1:]...) logger.Infof("zkClient{%s} unregister event{path:%s, event:%p}", z.name, zkPath, event) } } @@ -362,11 +364,11 @@ func (z *ZookeeperClient) ZkConnValid() bool { } valid := true - z.Lock() + z.RLock() if z.Conn == nil { valid = false } - z.Unlock() + z.RUnlock() return valid } @@ -384,11 +386,11 @@ func (z *ZookeeperClient) Close() { z.Conn = nil z.Unlock() if conn != nil { - logger.Warnf("zkClient Conn{name:%s, zk addr:%s} exit now.", z.name, conn.SessionID()) + logger.Infof("zkClient Conn{name:%s, zk addr:%d} exit now.", z.name, conn.SessionID()) conn.Close() } - logger.Warnf("zkClient{name:%s, zk addr:%s} exit now.", z.name, z.ZkAddrs) + logger.Infof("zkClient{name:%s, zk addr:%s} exit now.", z.name, z.ZkAddrs) } // Create will create the node recursively, which means that if the parent node is absent, @@ -407,21 +409,21 @@ func (z *ZookeeperClient) CreateWithValue(basePath string, value []byte) error { ) logger.Debugf("zookeeperClient.Create(basePath{%s})", basePath) + conn := z.getConn() + err = errNilZkClientConn + if conn == nil { + return perrors.WithMessagef(err, "zk.Create(path:%s)", basePath) + } + for _, str := range strings.Split(basePath, "/")[1:] { tmpPath = path.Join(tmpPath, "/", str) - err = errNilZkClientConn - z.Lock() - conn := z.Conn - z.Unlock() - if conn != nil { - _, err = conn.Create(tmpPath, value, 0, zk.WorldACL(zk.PermAll)) - } + _, err = conn.Create(tmpPath, value, 0, zk.WorldACL(zk.PermAll)) if err != nil { if err == zk.ErrNodeExists { - logger.Debugf("zk.create(\"%s\") exists\n", tmpPath) + logger.Debugf("zk.create(\"%s\") exists", tmpPath) } else { - logger.Errorf("zk.create(\"%s\") error(%v)\n", tmpPath, perrors.WithStack(err)) + logger.Errorf("zk.create(\"%s\") error(%v)", tmpPath, perrors.WithStack(err)) return perrors.WithMessagef(err, "zk.Create(path:%s)", basePath) } } @@ -430,16 +432,52 @@ func (z *ZookeeperClient) CreateWithValue(basePath string, value []byte) error { return nil } -// nolint -func (z *ZookeeperClient) Delete(basePath string) error { +// CreateTempWithValue will create the node recursively, which means that if the parent node is absent, +// it will create parent node first,and set value in last child path +// If the path exist, it will update data +func (z *ZookeeperClient) CreateTempWithValue(basePath string, value []byte) error { var ( - err error + err error + tmpPath string ) + logger.Debugf("zookeeperClient.Create(basePath{%s})", basePath) + conn := z.getConn() err = errNilZkClientConn - z.Lock() - conn := z.Conn - z.Unlock() + if conn == nil { + return perrors.WithMessagef(err, "zk.Create(path:%s)", basePath) + } + + pathSlice := strings.Split(basePath, "/")[1:] + length := len(pathSlice) + for i, str := range pathSlice { + tmpPath = path.Join(tmpPath, "/", str) + // last child need be ephemeral + if i == length-1 { + _, err = conn.Create(tmpPath, value, zk.FlagEphemeral, zk.WorldACL(zk.PermAll)) + if err == zk.ErrNodeExists { + return err + } + } else { + _, err = conn.Create(tmpPath, []byte{}, 0, zk.WorldACL(zk.PermAll)) + } + if err != nil { + if err == zk.ErrNodeExists { + logger.Debugf("zk.create(\"%s\") exists", tmpPath) + } else { + logger.Errorf("zk.create(\"%s\") error(%v)", tmpPath, perrors.WithStack(err)) + return perrors.WithMessagef(err, "zk.Create(path:%s)", basePath) + } + } + } + + return nil +} + +// nolint +func (z *ZookeeperClient) Delete(basePath string) error { + err := errNilZkClientConn + conn := z.getConn() if conn != nil { err = conn.Delete(basePath, -1) } @@ -451,26 +489,22 @@ func (z *ZookeeperClient) Delete(basePath string) error { func (z *ZookeeperClient) RegisterTemp(basePath string, node string) (string, error) { var ( err error - data []byte zkPath string tmpPath string ) err = errNilZkClientConn - data = []byte("") zkPath = path.Join(basePath) + "/" + node - z.Lock() - conn := z.Conn - z.Unlock() + conn := z.getConn() if conn != nil { - tmpPath, err = conn.Create(zkPath, data, zk.FlagEphemeral, zk.WorldACL(zk.PermAll)) + tmpPath, err = conn.Create(zkPath, []byte(""), zk.FlagEphemeral, zk.WorldACL(zk.PermAll)) } if err != nil { - logger.Warnf("conn.Create(\"%s\", zk.FlagEphemeral) = error(%v)\n", zkPath, perrors.WithStack(err)) + logger.Warnf("conn.Create(\"%s\", zk.FlagEphemeral) = error(%v)", zkPath, perrors.WithStack(err)) return zkPath, perrors.WithStack(err) } - logger.Debugf("zkClient{%s} create a temp zookeeper node:%s\n", z.name, tmpPath) + logger.Debugf("zkClient{%s} create a temp zookeeper node:%s", z.name, tmpPath) return tmpPath, nil } @@ -483,9 +517,7 @@ func (z *ZookeeperClient) RegisterTempSeq(basePath string, data []byte) (string, ) err = errNilZkClientConn - z.Lock() - conn := z.Conn - z.Unlock() + conn := z.getConn() if conn != nil { tmpPath, err = conn.Create( path.Join(basePath)+"/", @@ -497,11 +529,11 @@ func (z *ZookeeperClient) RegisterTempSeq(basePath string, data []byte) (string, logger.Debugf("zookeeperClient.RegisterTempSeq(basePath{%s}) = tempPath{%s}", basePath, tmpPath) if err != nil && err != zk.ErrNodeExists { - logger.Errorf("zkClient{%s} conn.Create(\"%s\", \"%s\", zk.FlagEphemeral|zk.FlagSequence) error(%v)\n", + logger.Errorf("zkClient{%s} conn.Create(\"%s\", \"%s\", zk.FlagEphemeral|zk.FlagSequence) error(%v)", z.name, basePath, string(data), err) return "", perrors.WithStack(err) } - logger.Debugf("zkClient{%s} create a temp zookeeper node:%s\n", z.name, tmpPath) + logger.Debugf("zkClient{%s} create a temp zookeeper node:%s", z.name, tmpPath) return tmpPath, nil } @@ -516,9 +548,7 @@ func (z *ZookeeperClient) GetChildrenW(path string) ([]string, <-chan zk.Event, ) err = errNilZkClientConn - z.Lock() - conn := z.Conn - z.Unlock() + conn := z.getConn() if conn != nil { children, stat, watcher, err = conn.ChildrenW(path) } @@ -552,9 +582,7 @@ func (z *ZookeeperClient) GetChildren(path string) ([]string, error) { ) err = errNilZkClientConn - z.Lock() - conn := z.Conn - z.Unlock() + conn := z.getConn() if conn != nil { children, stat, err = conn.Children(path) } @@ -585,9 +613,7 @@ func (z *ZookeeperClient) ExistW(zkPath string) (<-chan zk.Event, error) { ) err = errNilZkClientConn - z.Lock() - conn := z.Conn - z.Unlock() + conn := z.getConn() if conn != nil { exist, _, watcher, err = conn.ExistsW(zkPath) } @@ -608,3 +634,15 @@ func (z *ZookeeperClient) ExistW(zkPath string) (<-chan zk.Event, error) { func (z *ZookeeperClient) GetContent(zkPath string) ([]byte, *zk.Stat, error) { return z.Conn.Get(zkPath) } + +// nolint +func (z *ZookeeperClient) SetContent(zkPath string, content []byte, version int32) (*zk.Stat, error) { + return z.Conn.Set(zkPath, content, version) +} + +// getConn gets zookeeper connection safely +func (z *ZookeeperClient) getConn() *zk.Conn { + z.RLock() + defer z.RUnlock() + return z.Conn +} diff --git a/remoting/zookeeper/client_test.go b/remoting/zookeeper/client_test.go index 0f6899568ad4744dc58022c41e22db6f901ad5de..34741700ca2a9d86ee5321b0b19ed64b2b1a25a8 100644 --- a/remoting/zookeeper/client_test.go +++ b/remoting/zookeeper/client_test.go @@ -18,7 +18,6 @@ package zookeeper import ( - "fmt" "testing" "time" ) @@ -28,6 +27,10 @@ import ( "github.com/stretchr/testify/assert" ) +import ( + "github.com/apache/dubbo-go/common/logger" +) + func verifyEventStateOrder(t *testing.T, c <-chan zk.Event, expectedStates []zk.State, source string) { for _, state := range expectedStates { for { @@ -35,7 +38,7 @@ func verifyEventStateOrder(t *testing.T, c <-chan zk.Event, expectedStates []zk. if !ok { t.Fatalf("unexpected channel close for %s", source) } - fmt.Println(event) + logger.Debug(event) if event.Type != zk.EventSession { continue } @@ -87,9 +90,10 @@ func Test_newMockZookeeperClient(t *testing.T) { } func TestCreate(t *testing.T) { - ts, z, event, _ := NewMockZookeeperClient("test", 15*time.Second) + ts, z, event, err := NewMockZookeeperClient("test", 15*time.Second) + assert.NoError(t, err) defer ts.Stop() - err := z.Create("test1/test2/test3/test4") + err = z.Create("test1/test2/test3/test4") assert.NoError(t, err) states := []zk.State{zk.StateConnecting, zk.StateConnected, zk.StateHasSession} @@ -97,21 +101,24 @@ func TestCreate(t *testing.T) { } func TestCreateDelete(t *testing.T) { - ts, z, event, _ := NewMockZookeeperClient("test", 15*time.Second) + ts, z, event, err := NewMockZookeeperClient("test", 15*time.Second) + assert.NoError(t, err) defer ts.Stop() states := []zk.State{zk.StateConnecting, zk.StateConnected, zk.StateHasSession} verifyEventStateOrder(t, event, states, "event channel") - err := z.Create("/test1/test2/test3/test4") + err = z.Create("/test1/test2/test3/test4") + assert.NoError(t, err) + err = z.Delete("/test1/test2/test3/test4") assert.NoError(t, err) - err2 := z.Delete("/test1/test2/test3/test4") - assert.NoError(t, err2) + // verifyEventOrder(t, event, []zk.EventType{zk.EventNodeCreated}, "event channel") } func TestRegisterTemp(t *testing.T) { - ts, z, event, _ := NewMockZookeeperClient("test", 15*time.Second) + ts, z, event, err := NewMockZookeeperClient("test", 15*time.Second) + assert.NoError(t, err) defer ts.Stop() - err := z.Create("/test1/test2/test3") + err = z.Create("/test1/test2/test3") assert.NoError(t, err) tmpath, err := z.RegisterTemp("/test1/test2/test3", "test4") @@ -122,9 +129,10 @@ func TestRegisterTemp(t *testing.T) { } func TestRegisterTempSeq(t *testing.T) { - ts, z, event, _ := NewMockZookeeperClient("test", 15*time.Second) + ts, z, event, err := NewMockZookeeperClient("test", 15*time.Second) + assert.NoError(t, err) defer ts.Stop() - err := z.Create("/test1/test2/test3") + err = z.Create("/test1/test2/test3") assert.NoError(t, err) tmpath, err := z.RegisterTempSeq("/test1/test2/test3", []byte("test")) assert.NoError(t, err) diff --git a/remoting/zookeeper/curator_discovery/service_discovery.go b/remoting/zookeeper/curator_discovery/service_discovery.go new file mode 100644 index 0000000000000000000000000000000000000000..9566b5494389325520b4eb6a8eb170e0b305bb47 --- /dev/null +++ b/remoting/zookeeper/curator_discovery/service_discovery.go @@ -0,0 +1,279 @@ +/* + * 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 curator_discovery + +import ( + "encoding/json" + "path" + "strings" + "sync" +) + +import ( + "github.com/dubbogo/go-zookeeper/zk" + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/remoting" + "github.com/apache/dubbo-go/remoting/zookeeper" +) + +// Entry contain a service instance +type Entry struct { + sync.Mutex + instance *ServiceInstance +} + +// ServiceInstance which define in curator-x-discovery, please refer to +// https://github.com/apache/curator/blob/master/curator-x-discovery/src/main/java/org/apache/curator/x/discovery/ServiceDiscovery.java +// It's not exactly the same as curator-x-discovery's service discovery +type ServiceDiscovery struct { + client *zookeeper.ZookeeperClient + mutex *sync.Mutex + basePath string + services *sync.Map + listener *zookeeper.ZkEventListener +} + +// NewServiceDiscovery the constructor of service discovery +func NewServiceDiscovery(client *zookeeper.ZookeeperClient, basePath string) *ServiceDiscovery { + return &ServiceDiscovery{ + client: client, + mutex: &sync.Mutex{}, + basePath: basePath, + services: &sync.Map{}, + listener: zookeeper.NewZkEventListener(client), + } +} + +// registerService register service to zookeeper +func (sd *ServiceDiscovery) registerService(instance *ServiceInstance) error { + path := sd.pathForInstance(instance.Name, instance.Id) + data, err := json.Marshal(instance) + if err != nil { + return err + } + err = sd.client.CreateTempWithValue(path, data) + if err == zk.ErrNodeExists { + _, state, _ := sd.client.GetContent(path) + if state != nil { + _, err = sd.client.SetContent(path, data, state.Version+1) + if err != nil { + logger.Debugf("Try to update the node data failed. In most cases, it's not a problem. ") + } + } + return nil + } + if err != nil { + return err + } + return nil +} + +// RegisterService register service to zookeeper, and ensure cache is consistent with zookeeper +func (sd *ServiceDiscovery) RegisterService(instance *ServiceInstance) error { + value, loaded := sd.services.LoadOrStore(instance.Id, &Entry{}) + entry, ok := value.(*Entry) + if !ok { + return perrors.New("[ServiceDiscovery] services value not entry") + } + entry.Lock() + defer entry.Unlock() + entry.instance = instance + err := sd.registerService(instance) + if err != nil { + return err + } + if !loaded { + sd.ListenServiceInstanceEvent(instance.Name, instance.Id, sd) + } + return nil +} + +// UpdateService update service in zookeeper, and ensure cache is consistent with zookeeper +func (sd *ServiceDiscovery) UpdateService(instance *ServiceInstance) error { + value, ok := sd.services.Load(instance.Id) + if !ok { + return perrors.Errorf("[ServiceDiscovery] Service{%s} not registered", instance.Id) + } + entry, ok := value.(*Entry) + if !ok { + return perrors.New("[ServiceDiscovery] services value not entry") + } + data, err := json.Marshal(instance) + + if err != nil { + return err + } + + entry.Lock() + defer entry.Unlock() + entry.instance = instance + path := sd.pathForInstance(instance.Name, instance.Id) + + _, err = sd.client.SetContent(path, data, -1) + if err != nil { + return err + } + return nil +} + +// updateInternalService update service in cache +func (sd *ServiceDiscovery) updateInternalService(name, id string) { + value, ok := sd.services.Load(id) + if !ok { + return + } + entry, ok := value.(*Entry) + if !ok { + return + } + entry.Lock() + defer entry.Unlock() + instance, err := sd.QueryForInstance(name, id) + if err != nil { + logger.Infof("[zkServiceDiscovery] UpdateInternalService{%s} error = err{%v}", id, err) + return + } + entry.instance = instance + return +} + +// UnregisterService un-register service in zookeeper and delete service in cache +func (sd *ServiceDiscovery) UnregisterService(instance *ServiceInstance) error { + _, ok := sd.services.Load(instance.Id) + if !ok { + return nil + } + sd.services.Delete(instance.Id) + return sd.unregisterService(instance) +} + +// unregisterService un-register service in zookeeper +func (sd *ServiceDiscovery) unregisterService(instance *ServiceInstance) error { + path := sd.pathForInstance(instance.Name, instance.Id) + return sd.client.Delete(path) +} + +// ReRegisterServices re-register all cache services to zookeeper +func (sd *ServiceDiscovery) ReRegisterServices() { + sd.services.Range(func(key, value interface{}) bool { + entry, ok := value.(*Entry) + if !ok { + return true + } + entry.Lock() + defer entry.Unlock() + instance := entry.instance + err := sd.registerService(instance) + if err != nil { + logger.Errorf("[zkServiceDiscovery] registerService{%s} error = err{%v}", instance.Id, perrors.WithStack(err)) + return true + } + sd.ListenServiceInstanceEvent(instance.Name, instance.Id, sd) + return true + }) +} + +// QueryForInstances query instances in zookeeper by name +func (sd *ServiceDiscovery) QueryForInstances(name string) ([]*ServiceInstance, error) { + ids, err := sd.client.GetChildren(sd.pathForName(name)) + if err != nil { + return nil, err + } + var ( + instance *ServiceInstance + instances []*ServiceInstance + ) + for _, id := range ids { + instance, err = sd.QueryForInstance(name, id) + if err != nil { + return nil, err + } + instances = append(instances, instance) + } + return instances, nil +} + +// QueryForInstance query instances in zookeeper by name and id +func (sd *ServiceDiscovery) QueryForInstance(name string, id string) (*ServiceInstance, error) { + path := sd.pathForInstance(name, id) + data, _, err := sd.client.GetContent(path) + if err != nil { + return nil, err + } + instance := &ServiceInstance{} + err = json.Unmarshal(data, instance) + if err != nil { + return nil, err + } + return instance, nil +} + +// QueryForInstance query all service name in zookeeper +func (sd *ServiceDiscovery) QueryForNames() ([]string, error) { + return sd.client.GetChildren(sd.basePath) +} + +// ListenServiceEvent add a listener in a service +func (sd *ServiceDiscovery) ListenServiceEvent(name string, listener remoting.DataListener) { + sd.listener.ListenServiceEvent(nil, sd.pathForName(name), listener) +} + +// ListenServiceEvent add a listener in a instance +func (sd *ServiceDiscovery) ListenServiceInstanceEvent(name, id string, listener remoting.DataListener) { + sd.listener.ListenServiceNodeEvent(sd.pathForInstance(name, id), listener) +} + +// DataChange implement DataListener's DataChange function +func (sd *ServiceDiscovery) DataChange(eventType remoting.Event) bool { + path := eventType.Path + name, id, err := sd.getNameAndId(path) + if err != nil { + logger.Errorf("[ServiceDiscovery] data change error = {%v}", err) + return true + } + sd.updateInternalService(name, id) + return true +} + +// getNameAndId get service name and instance id by path +func (sd *ServiceDiscovery) getNameAndId(path string) (string, string, error) { + path = strings.TrimPrefix(path, sd.basePath) + path = strings.TrimPrefix(path, constant.PATH_SEPARATOR) + pathSlice := strings.Split(path, constant.PATH_SEPARATOR) + if len(pathSlice) < 2 { + return "", "", perrors.Errorf("[ServiceDiscovery] path{%s} dont contain name and id", path) + } + name := pathSlice[0] + id := pathSlice[1] + return name, id, nil +} + +// nolint +func (sd *ServiceDiscovery) pathForInstance(name, id string) string { + return path.Join(sd.basePath, name, id) +} + +// nolint +func (sd *ServiceDiscovery) pathForName(name string) string { + return path.Join(sd.basePath, name) +} diff --git a/remoting/zookeeper/curator_discovery/service_instance.go b/remoting/zookeeper/curator_discovery/service_instance.go new file mode 100644 index 0000000000000000000000000000000000000000..f8d2bc723e0e0dd90ffdaa6ccd7c9908d65ac9a0 --- /dev/null +++ b/remoting/zookeeper/curator_discovery/service_instance.go @@ -0,0 +1,29 @@ +/* + * 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 curator_discovery + +// ServiceInstance which define in curator-x-discovery, please refer to +// https://github.com/apache/curator/blob/master/curator-x-discovery/src/main/java/org/apache/curator/x/discovery/ServiceInstance.java +type ServiceInstance struct { + Name string + Id string + Address string + Port int + Payload interface{} + RegistrationTimeUTC int64 +} diff --git a/remoting/zookeeper/facade.go b/remoting/zookeeper/facade.go index f9b9332504f445724a54b94356771e4ad49b62f0..d5d9e6e74858e3ec520aedee5b8ba059baf928d8 100644 --- a/remoting/zookeeper/facade.go +++ b/remoting/zookeeper/facade.go @@ -30,18 +30,18 @@ import ( "github.com/apache/dubbo-go/common/logger" ) -type zkClientFacade interface { +type ZkClientFacade interface { ZkClient() *ZookeeperClient SetZkClient(*ZookeeperClient) ZkClientLock() *sync.Mutex - WaitGroup() *sync.WaitGroup //for wait group control, zk client listener & zk client container - Done() chan struct{} //for zk client control + WaitGroup() *sync.WaitGroup // for wait group control, zk client listener & zk client container + Done() chan struct{} // for zk client control RestartCallBack() bool - common.Node + GetUrl() common.URL } // HandleClientRestart keeps the connection between client and server -func HandleClientRestart(r zkClientFacade) { +func HandleClientRestart(r ZkClientFacade) { var ( err error diff --git a/remoting/zookeeper/facade_test.go b/remoting/zookeeper/facade_test.go index 01d46da6cc1abae90210a323d32ac84bad80249b..1cd8f064bb15a2ac48b0d62154309b27c55ab946 100644 --- a/remoting/zookeeper/facade_test.go +++ b/remoting/zookeeper/facade_test.go @@ -38,7 +38,7 @@ type mockFacade struct { done chan struct{} } -func newMockFacade(client *ZookeeperClient, url *common.URL) zkClientFacade { +func newMockFacade(client *ZookeeperClient, url *common.URL) ZkClientFacade { mock := &mockFacade{ client: client, URL: url, diff --git a/remoting/zookeeper/listener.go b/remoting/zookeeper/listener.go index 948e0d73a579c82601eb759ea89953d1748bf55b..9a4874db24696d90e4fcc7d9d987f5888f1be599 100644 --- a/remoting/zookeeper/listener.go +++ b/remoting/zookeeper/listener.go @@ -58,8 +58,20 @@ func (l *ZkEventListener) SetClient(client *ZookeeperClient) { l.client = client } +// ListenServiceNodeEvent listen a path node event +func (l *ZkEventListener) ListenServiceNodeEvent(zkPath string, listener remoting.DataListener) { + // listen l service node + l.wg.Add(1) + go func(zkPath string, listener remoting.DataListener) { + if l.listenServiceNodeEvent(zkPath, listener) { + listener.DataChange(remoting.Event{Path: zkPath, Action: remoting.EventTypeDel}) + } + logger.Warnf("listenSelf(zk path{%s}) goroutine exit now", zkPath) + }(zkPath, listener) +} + // nolint -func (l *ZkEventListener) ListenServiceNodeEvent(zkPath string, listener ...remoting.DataListener) bool { +func (l *ZkEventListener) listenServiceNodeEvent(zkPath string, listener ...remoting.DataListener) bool { defer l.wg.Done() var zkEvent zk.Event for { @@ -154,7 +166,7 @@ func (l *ZkEventListener) handleZkNodeEvent(zkPath string, children []string, li l.wg.Add(1) go func(node string, zkPath string, listener remoting.DataListener) { logger.Infof("delete zkNode{%s}", node) - if l.ListenServiceNodeEvent(node, listener) { + if l.listenServiceNodeEvent(node, listener) { logger.Infof("delete content{%s}", node) listener.DataChange(remoting.Event{Path: zkPath, Action: remoting.EventTypeDel}) } @@ -240,10 +252,10 @@ func (l *ZkEventListener) listenDirEvent(conf *common.URL, zkPath string, listen } } - //listen l service node + // listen l service node dubboPath := path.Join(zkPath, c) - //Save the path to avoid listen repeatedly + // Save the path to avoid listen repeatedly l.pathMapLock.Lock() _, ok := l.pathMap[dubboPath] l.pathMapLock.Unlock() @@ -255,7 +267,7 @@ func (l *ZkEventListener) listenDirEvent(conf *common.URL, zkPath string, listen l.pathMapLock.Lock() l.pathMap[dubboPath] = struct{}{} l.pathMapLock.Unlock() - //When Zk disconnected, the Conn will be set to nil, so here need check the value of Conn + // When Zk disconnected, the Conn will be set to nil, so here need check the value of Conn l.client.RLock() if l.client.Conn == nil { l.client.RUnlock() @@ -273,14 +285,14 @@ func (l *ZkEventListener) listenDirEvent(conf *common.URL, zkPath string, listen logger.Infof("listen dubbo service key{%s}", dubboPath) l.wg.Add(1) go func(zkPath string, listener remoting.DataListener) { - if l.ListenServiceNodeEvent(zkPath) { + if l.listenServiceNodeEvent(zkPath) { listener.DataChange(remoting.Event{Path: zkPath, Action: remoting.EventTypeDel}) } logger.Warnf("listenSelf(zk path{%s}) goroutine exit now", zkPath) }(dubboPath, listener) - //listen sub path recursive - //if zkPath is end of "providers/ & consumers/" we do not listen children dir + // listen sub path recursive + // if zkPath is end of "providers/ & consumers/" we do not listen children dir if strings.LastIndex(zkPath, constant.PROVIDER_CATEGORY) == -1 && strings.LastIndex(zkPath, constant.CONSUMER_CATEGORY) == -1 { l.wg.Add(1) @@ -310,9 +322,9 @@ func timeSecondDuration(sec int) time.Duration { } // ListenServiceEvent is invoked by ZkConsumerRegistry::Register/ZkConsumerRegistry::get/ZkConsumerRegistry::getListener -// registry.go:Listen -> listenServiceEvent -> listenDirEvent -> ListenServiceNodeEvent +// registry.go:Listen -> listenServiceEvent -> listenDirEvent -> listenServiceNodeEvent // | -// --------> ListenServiceNodeEvent +// --------> listenServiceNodeEvent func (l *ZkEventListener) ListenServiceEvent(conf *common.URL, zkPath string, listener remoting.DataListener) { logger.Infof("listen dubbo path{%s}", zkPath) l.wg.Add(1) @@ -326,7 +338,8 @@ func (l *ZkEventListener) valid() bool { return l.client.ZkConnValid() } -// nolint +// Close will let client listen exit func (l *ZkEventListener) Close() { + close(l.client.exit) l.wg.Wait() }