diff --git a/.travis.yml b/.travis.yml index ba30c9d010..1b46f5d872 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,9 @@ install: true script: - go fmt ./... && [[ -z `git status -s` ]] + - sh before_validate_license.sh + - chmod u+x /tmp/tools/license/license-header-checker + - /tmp/tools/license/license-header-checker -v -a -r -i vendor /tmp/tools/license/license.txt . go && [[ -z `git status -s` ]] - chmod u+x before_ut.sh && ./before_ut.sh - go mod vendor && go test ./... -coverprofile=coverage.txt -covermode=atomic @@ -20,4 +23,4 @@ after_success: - bash <(curl -s https://codecov.io/bash) notifications: - webhooks: https://oapi.dingtalk.com/robot/send?access_token=f5d6237f2c79db584e75604f7f88db1ce1673c8c0e98451217b28fde791e1d4f \ No newline at end of file + webhooks: https://oapi.dingtalk.com/robot/send?access_token=f5d6237f2c79db584e75604f7f88db1ce1673c8c0e98451217b28fde791e1d4f diff --git a/README.md b/README.md index b3e7173c43..9bade617c8 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Apache License, Version 2.0 Both extension module and layered project architecture is according to Apache Dubbo (including protocol layer, registry layer, cluster layer, config layer and so on), the advantage of this arch is as following: you can implement these layered interfaces in your own way, override the default implementation of dubbo-go by calling 'extension.SetXXX' of extension, complete your special needs without modifying the source code. At the same time, you are welcome to contribute implementation of useful extension to the community. -![frame design](https://raw.githubusercontent.com/wiki/dubbo/dubbo-go/dubbo-go%E4%BB%A3%E7%A0%81%E5%88%86%E5%B1%82%E8%AE%BE%E8%AE%A1.png) +![dubbo go extend](./doc/pic/arch/dubbo-go-ext.png) If you wanna know more about dubbo-go, please visit this reference [Project Architeture design](https://github.com/apache/dubbo-go/wiki/dubbo-go-V1.0-design) diff --git a/README_CN.md b/README_CN.md index 5c40249ed2..180759f366 100644 --- a/README_CN.md +++ b/README_CN.md @@ -29,7 +29,7 @@ Apache License, Version 2.0 基于dubbo的extension模块和分层的代码设计(包括 protocol layer, registry layer, cluster layer, config 等等)。我们的目标是:你可以对这些分层接口进行新的实现,并通过调用 extension 模块的“ extension.SetXXX ”方法来覆盖 dubbo-go [同 go-for-apache-dubbo ]的默认实现,以完成自己的特殊需求而无需修改源代码。同时,欢迎你为社区贡献有用的拓展实现。 -![框架设计](https://raw.githubusercontent.com/wiki/dubbo/dubbo-go/dubbo-go%E4%BB%A3%E7%A0%81%E5%88%86%E5%B1%82%E8%AE%BE%E8%AE%A1.png) +![dubbo go extend](./doc/pic/arch/dubbo-go-ext.png) 关于详细设计请阅读 [code layered design](https://github.com/apache/dubbo-go/wiki/dubbo-go-V1.0-design) @@ -108,6 +108,7 @@ Apache License, Version 2.0 * [For dubbo](https://github.com/apache/dubbo-go/pull/344) * [For grpc](https://github.com/apache/dubbo-go/pull/397) + - 其他功能支持: * 启动时检查 * 服务直连 diff --git a/before_validate_license.sh b/before_validate_license.sh new file mode 100644 index 0000000000..8fa6e381c7 --- /dev/null +++ b/before_validate_license.sh @@ -0,0 +1,26 @@ +# +# 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. + +remoteLicenseCheckerPath="https://github.com/dubbogo/resources/raw/master/tools/license" +remoteLicenseCheckerName="license-header-checker" +remoteLicenseCheckerURL="${remoteLicenseCheckerPath}/${remoteLicenseCheckerName}" +remoteLicenseName="license.txt" +remoteLicenseURL="${remoteLicenseCheckerPath}/${remoteLicenseName}" + +licensePath="/tmp/tools/license" +mkdir -p ${licensePath} +wget -P "${licensePath}" ${remoteLicenseCheckerURL} +wget -P "${licensePath}" ${remoteLicenseURL} diff --git a/cluster/router/condition/app_router_test.go b/cluster/router/condition/app_router_test.go index bd817af36c..e99307625b 100644 --- a/cluster/router/condition/app_router_test.go +++ b/cluster/router/condition/app_router_test.go @@ -113,7 +113,7 @@ conditions: assert.Nil(t, err) assert.NotNil(t, appRouter) - rule, err := Parse(testYML) + rule, err := getRule(testYML) assert.Nil(t, err) appRouter.generateConditions(rule) diff --git a/cluster/router/condition/file.go b/cluster/router/condition/file.go index efeec53efc..b2c8766900 100644 --- a/cluster/router/condition/file.go +++ b/cluster/router/condition/file.go @@ -44,7 +44,7 @@ type FileConditionRouter struct { // NewFileConditionRouter Create file condition router instance with content ( from config file) func NewFileConditionRouter(content []byte) (*FileConditionRouter, error) { fileRouter := &FileConditionRouter{} - rule, err := Parse(string(content)) + rule, err := getRule(string(content)) if err != nil { return nil, perrors.Errorf("yaml.Unmarshal() failed , error:%v", perrors.WithStack(err)) } diff --git a/cluster/router/condition/listenable_router.go b/cluster/router/condition/listenable_router.go index ba2fbb0eb2..4ccc19e955 100644 --- a/cluster/router/condition/listenable_router.go +++ b/cluster/router/condition/listenable_router.go @@ -102,7 +102,7 @@ func (l *listenableRouter) Process(event *config_center.ConfigChangeEvent) { return } - routerRule, err := Parse(content) + routerRule, err := getRule(content) if err != nil { logger.Errorf("Parse condition router rule fail,error:[%s] ", err) return diff --git a/cluster/router/condition/router.go b/cluster/router/condition/router.go index c5d46444bd..0267a3c7a4 100644 --- a/cluster/router/condition/router.go +++ b/cluster/router/condition/router.go @@ -27,7 +27,6 @@ import ( ) import ( - matcher "github.com/apache/dubbo-go/cluster/router/match" "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/logger" @@ -301,7 +300,7 @@ func (pair MatchPair) isMatch(value string, param *common.URL) bool { if !pair.Matches.Empty() && pair.Mismatches.Empty() { for match := range pair.Matches.Items { - if matcher.IsMatchGlobalPattern(match.(string), value, param) { + if isMatchGlobalPattern(match.(string), value, param) { return true } } @@ -310,7 +309,7 @@ func (pair MatchPair) isMatch(value string, param *common.URL) bool { if !pair.Mismatches.Empty() && pair.Matches.Empty() { for mismatch := range pair.Mismatches.Items { - if matcher.IsMatchGlobalPattern(mismatch.(string), value, param) { + if isMatchGlobalPattern(mismatch.(string), value, param) { return false } } @@ -319,12 +318,12 @@ func (pair MatchPair) isMatch(value string, param *common.URL) bool { if !pair.Mismatches.Empty() && !pair.Matches.Empty() { //when both mismatches and matches contain the same value, then using mismatches first for mismatch := range pair.Mismatches.Items { - if matcher.IsMatchGlobalPattern(mismatch.(string), value, param) { + if isMatchGlobalPattern(mismatch.(string), value, param) { return false } } for match := range pair.Matches.Items { - if matcher.IsMatchGlobalPattern(match.(string), value, param) { + if isMatchGlobalPattern(match.(string), value, param) { return true } } diff --git a/cluster/router/condition/router_rule.go b/cluster/router/condition/router_rule.go index 1374cf9de2..ce397d6cc0 100644 --- a/cluster/router/condition/router_rule.go +++ b/cluster/router/condition/router_rule.go @@ -18,11 +18,17 @@ package condition import ( - "gopkg.in/yaml.v2" + "strings" +) + +import ( + gxstrings "github.com/dubbogo/gost/strings" ) import ( "github.com/apache/dubbo-go/cluster/router" + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/yaml" ) // RouterRule RouterRule config read from config file or config center @@ -44,9 +50,9 @@ type RouterRule struct { * => * 1.1.1.1 */ -func Parse(rawRule string) (*RouterRule, error) { +func getRule(rawRule string) (*RouterRule, error) { r := &RouterRule{} - err := yaml.Unmarshal([]byte(rawRule), r) + err := yaml.UnmarshalYML([]byte(rawRule), r) if err != nil { return r, err } @@ -57,3 +63,11 @@ func Parse(rawRule string) (*RouterRule, error) { return r, nil } + +// isMatchGlobalPattern Match value to param content by pattern +func isMatchGlobalPattern(pattern string, value string, param *common.URL) bool { + if param != nil && strings.HasPrefix(pattern, "$") { + pattern = param.GetRawParam(pattern[1:]) + } + return gxstrings.IsMatchPattern(pattern, value) +} diff --git a/cluster/router/condition/router_rule_test.go b/cluster/router/condition/router_rule_test.go index 5acc728391..675acaec91 100644 --- a/cluster/router/condition/router_rule_test.go +++ b/cluster/router/condition/router_rule_test.go @@ -20,11 +20,16 @@ package condition import ( "testing" ) + import ( "github.com/stretchr/testify/assert" ) -func TestParse(t *testing.T) { +import ( + "github.com/apache/dubbo-go/common" +) + +func TestGetRule(t *testing.T) { testyml := ` scope: application runtime: true @@ -36,7 +41,7 @@ conditions: ip=127.0.0.1 => 1.1.1.1` - rule, e := Parse(testyml) + rule, e := getRule(testyml) assert.Nil(t, e) assert.NotNil(t, rule) @@ -50,3 +55,8 @@ conditions: assert.Equal(t, false, rule.Dynamic) assert.Equal(t, "", rule.Key) } + +func TestIsMatchGlobPattern(t *testing.T) { + url, _ := common.NewURL("dubbo://localhost:8080/Foo?key=v*e") + assert.Equal(t, true, isMatchGlobalPattern("$key", "value", &url)) +} diff --git a/cluster/router/match/match_utils.go b/cluster/router/match/match_utils.go deleted file mode 100644 index 28fe7151c5..0000000000 --- a/cluster/router/match/match_utils.go +++ /dev/null @@ -1,63 +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 match - -import ( - "strings" -) - -import ( - "github.com/apache/dubbo-go/common" -) - -// IsMatchGlobalPattern Match value to param content by pattern -func IsMatchGlobalPattern(pattern string, value string, param *common.URL) bool { - if param != nil && strings.HasPrefix(pattern, "$") { - pattern = param.GetRawParam(pattern[1:]) - } - return isMatchInternalPattern(pattern, value) -} - -func isMatchInternalPattern(pattern string, value string) bool { - if "*" == pattern { - return true - } - if len(pattern) == 0 && len(value) == 0 { - return true - } - if len(pattern) == 0 || len(value) == 0 { - return false - } - i := strings.LastIndex(pattern, "*") - switch i { - case -1: - // doesn't find "*" - return value == pattern - case len(pattern) - 1: - // "*" is at the end - return strings.HasPrefix(value, pattern[0:i]) - case 0: - // "*" is at the beginning - return strings.HasSuffix(value, pattern[i+1:]) - default: - // "*" is in the middle - prefix := pattern[0:1] - suffix := pattern[i+1:] - return strings.HasPrefix(value, prefix) && strings.HasSuffix(value, suffix) - } -} diff --git a/cluster/router/router.go b/cluster/router/router.go index a28002a09e..9ee1154437 100644 --- a/cluster/router/router.go +++ b/cluster/router/router.go @@ -31,7 +31,7 @@ type RouterFactory interface { } // RouterFactory Router create factory use for parse config file -type FIleRouterFactory interface { +type FileRouterFactory interface { // NewFileRouters Create file router with config file NewFileRouter([]byte) (Router, error) } diff --git a/cluster/router/tag/factory.go b/cluster/router/tag/factory.go new file mode 100644 index 0000000000..d74924c898 --- /dev/null +++ b/cluster/router/tag/factory.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 tag + +import ( + "github.com/apache/dubbo-go/cluster/router" + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" +) + +func init() { + extension.SetRouterFactory(constant.TagRouterName, NewTagRouterFactory) +} + +type tagRouterFactory struct{} + +// NewTagRouterFactory create a tagRouterFactory +func NewTagRouterFactory() router.RouterFactory { + return &tagRouterFactory{} +} + +// NewRouter create a tagRouter by tagRouterFactory with a url +// The url contains router configuration information +func (c *tagRouterFactory) NewRouter(url *common.URL) (router.Router, error) { + return NewTagRouter(url) +} + +// NewFileRouter create a tagRouter by profile content +func (c *tagRouterFactory) NewFileRouter(content []byte) (router.Router, error) { + return NewFileTagRouter(content) +} diff --git a/cluster/router/match/match_utils_test.go b/cluster/router/tag/factory_test.go similarity index 55% rename from cluster/router/match/match_utils_test.go rename to cluster/router/tag/factory_test.go index f16480f1d3..58bff5b181 100644 --- a/cluster/router/match/match_utils_test.go +++ b/cluster/router/tag/factory_test.go @@ -15,7 +15,7 @@ * limitations under the License. */ -package match +package tag import ( "testing" @@ -29,18 +29,11 @@ import ( "github.com/apache/dubbo-go/common" ) -func TestIsMatchInternalPattern(t *testing.T) { - assert.Equal(t, true, isMatchInternalPattern("*", "value")) - assert.Equal(t, true, isMatchInternalPattern("", "")) - assert.Equal(t, false, isMatchInternalPattern("", "value")) - assert.Equal(t, true, isMatchInternalPattern("value", "value")) - assert.Equal(t, true, isMatchInternalPattern("v*", "value")) - assert.Equal(t, true, isMatchInternalPattern("*ue", "value")) - assert.Equal(t, true, isMatchInternalPattern("*e", "value")) - assert.Equal(t, true, isMatchInternalPattern("v*e", "value")) -} - -func TestIsMatchGlobPattern(t *testing.T) { - url, _ := common.NewURL("dubbo://localhost:8080/Foo?key=v*e") - assert.Equal(t, true, IsMatchGlobalPattern("$key", "value", &url)) +func TestTagRouterFactory_NewRouter(t *testing.T) { + u1, err := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider?interface=com.ikurento.user.UserProvider&group=&version=2.6.0&enabled=true") + assert.Nil(t, err) + factory := NewTagRouterFactory() + tagRouter, e := factory.NewRouter(&u1) + assert.Nil(t, e) + assert.NotNil(t, tagRouter) } diff --git a/cluster/router/tag/file.go b/cluster/router/tag/file.go new file mode 100644 index 0000000000..8144c83203 --- /dev/null +++ b/cluster/router/tag/file.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 tag + +import ( + "net/url" + "strconv" + "sync" +) + +import ( + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/protocol" +) + +// FileTagRouter Use for parse config file of Tag router +type FileTagRouter struct { + parseOnce sync.Once + router *tagRouter + routerRule *RouterRule + url *common.URL + force bool +} + +// NewFileTagRouter Create file tag router instance with content ( from config file) +func NewFileTagRouter(content []byte) (*FileTagRouter, error) { + fileRouter := &FileTagRouter{} + rule, err := getRule(string(content)) + if err != nil { + return nil, perrors.Errorf("yaml.Unmarshal() failed , error:%v", perrors.WithStack(err)) + } + fileRouter.routerRule = rule + url := fileRouter.URL() + fileRouter.router, err = NewTagRouter(&url) + return fileRouter, err +} + +// URL Return URL in file tag router n +func (f *FileTagRouter) URL() common.URL { + f.parseOnce.Do(func() { + routerRule := f.routerRule + f.url = common.NewURLWithOptions( + common.WithProtocol(constant.TAG_ROUTE_PROTOCOL), + common.WithParams(url.Values{}), + common.WithParamsValue(constant.ForceUseTag, strconv.FormatBool(routerRule.Force)), + common.WithParamsValue(constant.RouterPriority, strconv.Itoa(routerRule.Priority)), + common.WithParamsValue(constant.ROUTER_KEY, constant.TAG_ROUTE_PROTOCOL)) + }) + return *f.url +} + +// Priority Return Priority in listenable router +func (f *FileTagRouter) Priority() int64 { + return f.router.priority +} + +func (f *FileTagRouter) Route(invokers []protocol.Invoker, url *common.URL, invocation protocol.Invocation) []protocol.Invoker { + if len(invokers) == 0 { + return invokers + } + return f.Route(invokers, url, invocation) +} diff --git a/cluster/router/tag/file_test.go b/cluster/router/tag/file_test.go new file mode 100644 index 0000000000..94fcf9e0e0 --- /dev/null +++ b/cluster/router/tag/file_test.go @@ -0,0 +1,62 @@ +/* + * 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 tag + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common/constant" +) + +func TestNewFileTagRouter(t *testing.T) { + router, e := NewFileTagRouter([]byte(`priority: 100 +force: true`)) + assert.Nil(t, e) + assert.NotNil(t, router) + assert.Equal(t, 100, router.routerRule.Priority) + assert.Equal(t, true, router.routerRule.Force) +} + +func TestFileTagRouter_URL(t *testing.T) { + router, e := NewFileTagRouter([]byte(`priority: 100 +force: true`)) + assert.Nil(t, e) + assert.NotNil(t, router) + url := router.URL() + assert.NotNil(t, url) + force := url.GetParam(constant.ForceUseTag, "false") + priority := url.GetParam(constant.RouterPriority, "0") + assert.Equal(t, "true", force) + assert.Equal(t, "100", priority) + +} + +func TestFileTagRouter_Priority(t *testing.T) { + router, e := NewFileTagRouter([]byte(`priority: 100 +force: true`)) + assert.Nil(t, e) + assert.NotNil(t, router) + priority := router.Priority() + assert.Equal(t, int64(100), priority) +} diff --git a/cluster/router/tag/router_rule.go b/cluster/router/tag/router_rule.go new file mode 100644 index 0000000000..926446dcb2 --- /dev/null +++ b/cluster/router/tag/router_rule.go @@ -0,0 +1,38 @@ +/* + * 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 tag + +import ( + "github.com/apache/dubbo-go/cluster/router" + "github.com/apache/dubbo-go/common/yaml" +) + +// RouterRule RouterRule config read from config file or config center +type RouterRule struct { + router.BaseRouterRule `yaml:",inline""` +} + +func getRule(rawRule string) (*RouterRule, error) { + r := &RouterRule{} + err := yaml.UnmarshalYML([]byte(rawRule), r) + if err != nil { + return r, err + } + r.RawRule = rawRule + return r, nil +} diff --git a/cluster/router/tag/router_rule_test.go b/cluster/router/tag/router_rule_test.go new file mode 100644 index 0000000000..2df65193f9 --- /dev/null +++ b/cluster/router/tag/router_rule_test.go @@ -0,0 +1,40 @@ +/* + * 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 tag + +import ( + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +func TestGetRule(t *testing.T) { + yml := ` +scope: application +runtime: true +force: true +` + rule, e := getRule(yml) + assert.Nil(t, e) + assert.NotNil(t, rule) + assert.Equal(t, true, rule.Force) + assert.Equal(t, true, rule.Runtime) + assert.Equal(t, "application", rule.Scope) +} diff --git a/cluster/router/tag/tag_router.go b/cluster/router/tag/tag_router.go new file mode 100644 index 0000000000..87da418943 --- /dev/null +++ b/cluster/router/tag/tag_router.go @@ -0,0 +1,94 @@ +/* + * 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 tag + +import ( + "strconv" +) + +import ( + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/protocol" +) + +type tagRouter struct { + url *common.URL + enabled bool + priority int64 +} + +func NewTagRouter(url *common.URL) (*tagRouter, error) { + if url == nil { + return nil, perrors.Errorf("Illegal route URL!") + } + return &tagRouter{ + url: url, + enabled: url.GetParamBool(constant.RouterEnabled, true), + priority: url.GetParamInt(constant.RouterPriority, 0), + }, nil +} + +func (c *tagRouter) isEnabled() bool { + return c.enabled +} + +func (c *tagRouter) Route(invokers []protocol.Invoker, url *common.URL, invocation protocol.Invocation) []protocol.Invoker { + if !c.isEnabled() { + return invokers + } + if len(invokers) == 0 { + return invokers + } + return filterUsingStaticTag(invokers, url, invocation) +} + +func (c *tagRouter) URL() common.URL { + return *c.url +} + +func (c *tagRouter) Priority() int64 { + return c.priority +} + +func filterUsingStaticTag(invokers []protocol.Invoker, url *common.URL, invocation protocol.Invocation) []protocol.Invoker { + if tag, ok := invocation.Attachments()[constant.Tagkey]; ok { + result := make([]protocol.Invoker, 0, 8) + for _, v := range invokers { + if v.GetUrl().GetParam(constant.Tagkey, "") == tag { + result = append(result, v) + } + } + if len(result) == 0 && !isForceUseTag(url, invocation) { + return invokers + } + return result + } + return invokers +} + +func isForceUseTag(url *common.URL, invocation protocol.Invocation) bool { + if b, e := strconv.ParseBool(invocation.AttachmentsByKey(constant.ForceUseTag, url.GetParam(constant.ForceUseTag, "false"))); e == nil { + return b + } + return false +} diff --git a/cluster/router/tag/tag_router_test.go b/cluster/router/tag/tag_router_test.go new file mode 100644 index 0000000000..280b56c8cc --- /dev/null +++ b/cluster/router/tag/tag_router_test.go @@ -0,0 +1,147 @@ +/* + * 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 tag + +import ( + "context" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/protocol" + "github.com/apache/dubbo-go/protocol/invocation" +) + +// MockInvoker is only mock the Invoker to support test tagRouter +type MockInvoker struct { + url common.URL + available bool + destroyed bool + successCount int +} + +func NewMockInvoker(url common.URL) *MockInvoker { + return &MockInvoker{ + url: url, + available: true, + destroyed: false, + successCount: 0, + } +} + +func (bi *MockInvoker) GetUrl() common.URL { + return bi.url +} + +func (bi *MockInvoker) IsAvailable() bool { + return bi.available +} + +func (bi *MockInvoker) IsDestroyed() bool { + return bi.destroyed +} + +func (bi *MockInvoker) Invoke(_ context.Context, _ protocol.Invocation) protocol.Result { + bi.successCount++ + + result := &protocol.RPCResult{Err: nil} + return result +} + +func (bi *MockInvoker) Destroy() { + bi.destroyed = true + bi.available = false +} + +func TestTagRouter_Priority(t *testing.T) { + u1, err := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserConsumer?interface=com.ikurento.user.UserConsumer&group=&version=2.6.0&enabled=true&dubbo.force.tag=true") + assert.Nil(t, err) + tagRouter, e := NewTagRouter(&u1) + assert.Nil(t, e) + p := tagRouter.Priority() + assert.Equal(t, int64(0), p) +} + +func TestTagRouter_Route_force(t *testing.T) { + u1, e1 := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserConsumer?interface=com.ikurento.user.UserConsumer&group=&version=2.6.0&enabled=true&dubbo.force.tag=true") + assert.Nil(t, e1) + tagRouter, e := NewTagRouter(&u1) + assert.Nil(t, e) + + u2, e2 := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider?interface=com.ikurento.user.UserProvider&group=&version=2.6.0&enabled=true&dubbo.tag=hangzhou") + u3, e3 := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider?interface=com.ikurento.user.UserProvider&group=&version=2.6.0&enabled=true&dubbo.tag=shanghai") + u4, e4 := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider?interface=com.ikurento.user.UserProvider&group=&version=2.6.0&enabled=true&dubbo.tag=beijing") + assert.Nil(t, e2) + assert.Nil(t, e3) + assert.Nil(t, e4) + inv2 := NewMockInvoker(u2) + inv3 := NewMockInvoker(u3) + inv4 := NewMockInvoker(u4) + var invokers []protocol.Invoker + invokers = append(invokers, inv2, inv3, inv4) + inv := &invocation.RPCInvocation{} + inv.SetAttachments("dubbo.tag", "hangzhou") + invRst1 := tagRouter.Route(invokers, &u1, inv) + assert.Equal(t, 1, len(invRst1)) + assert.Equal(t, "hangzhou", invRst1[0].GetUrl().GetParam("dubbo.tag", "")) + + inv.SetAttachments("dubbo.tag", "guangzhou") + invRst2 := tagRouter.Route(invokers, &u1, inv) + assert.Equal(t, 0, len(invRst2)) + inv.SetAttachments("dubbo.force.tag", "false") + inv.SetAttachments("dubbo.tag", "guangzhou") + invRst3 := tagRouter.Route(invokers, &u1, inv) + assert.Equal(t, 3, len(invRst3)) +} + +func TestTagRouter_Route_noForce(t *testing.T) { + u1, e1 := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserConsumer?interface=com.ikurento.user.UserConsumer&group=&version=2.6.0&enabled=true") + assert.Nil(t, e1) + tagRouter, e := NewTagRouter(&u1) + assert.Nil(t, e) + + u2, e2 := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider?interface=com.ikurento.user.UserProvider&group=&version=2.6.0&enabled=true&dubbo.tag=hangzhou") + u3, e3 := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider?interface=com.ikurento.user.UserProvider&group=&version=2.6.0&enabled=true&dubbo.tag=shanghai") + u4, e4 := common.NewURL("dubbo://127.0.0.1:20000/com.ikurento.user.UserProvider?interface=com.ikurento.user.UserProvider&group=&version=2.6.0&enabled=true&dubbo.tag=beijing") + assert.Nil(t, e2) + assert.Nil(t, e3) + assert.Nil(t, e4) + inv2 := NewMockInvoker(u2) + inv3 := NewMockInvoker(u3) + inv4 := NewMockInvoker(u4) + var invokers []protocol.Invoker + invokers = append(invokers, inv2, inv3, inv4) + inv := &invocation.RPCInvocation{} + inv.SetAttachments("dubbo.tag", "hangzhou") + invRst := tagRouter.Route(invokers, &u1, inv) + assert.Equal(t, 1, len(invRst)) + assert.Equal(t, "hangzhou", invRst[0].GetUrl().GetParam("dubbo.tag", "")) + + inv.SetAttachments("dubbo.tag", "guangzhou") + inv.SetAttachments("dubbo.force.tag", "true") + invRst1 := tagRouter.Route(invokers, &u1, inv) + assert.Equal(t, 0, len(invRst1)) + inv.SetAttachments("dubbo.force.tag", "false") + invRst2 := tagRouter.Route(invokers, &u1, inv) + assert.Equal(t, 3, len(invRst2)) +} diff --git a/common/constant/default.go b/common/constant/default.go index 3c889158e4..6b9d914c87 100644 --- a/common/constant/default.go +++ b/common/constant/default.go @@ -74,3 +74,7 @@ const ( const ( COMMA_SPLIT_PATTERN = "\\s*[,]+\\s*" ) + +const ( + SIMPLE_METADATA_SERVICE_NAME = "MetadataService" +) diff --git a/common/constant/key.go b/common/constant/key.go index 07335bed59..6acb2299c4 100644 --- a/common/constant/key.go +++ b/common/constant/key.go @@ -26,6 +26,7 @@ const ( VERSION_KEY = "version" INTERFACE_KEY = "interface" PATH_KEY = "path" + PROTOCOL_KEY = "protocol" SERVICE_KEY = "service" METHODS_KEY = "methods" TIMEOUT_KEY = "timeout" @@ -41,6 +42,9 @@ const ( LOCAL_ADDR = "local-addr" REMOTE_ADDR = "remote-addr" PATH_SEPARATOR = "/" + DUBBO_KEY = "dubbo" + RELEASE_KEY = "release" + ANYHOST_KEY = "anyhost" ) const ( @@ -75,6 +79,10 @@ const ( EXECUTE_REJECTED_EXECUTION_HANDLER_KEY = "execute.limit.rejected.handler" PROVIDER_SHUTDOWN_FILTER = "pshutdown" CONSUMER_SHUTDOWN_FILTER = "cshutdown" + SYNC_REPORT_KEY = "sync.report" + RETRY_PERIOD_KEY = "retry.period" + RETRY_TIMES_KEY = "retry.times" + CYCLE_REPORT_KEY = "cycle.report" ) const ( @@ -105,6 +113,7 @@ const ( ROUTERS_CATEGORY = "routers" ROUTE_PROTOCOL = "route" CONDITION_ROUTE_PROTOCOL = "condition" + TAG_ROUTE_PROTOCOL = "tag" PROVIDERS_CATEGORY = "providers" ROUTER_KEY = "router" ) @@ -128,6 +137,7 @@ const ( ProviderConfigPrefix = "dubbo.provider." ConsumerConfigPrefix = "dubbo.consumer." ShutdownConfigPrefix = "dubbo.shutdown." + MetadataReportPrefix = "dubbo.metadata-report." RouterConfigPrefix = "dubbo.router." ) @@ -161,7 +171,8 @@ const ( ListenableRouterName = "listenable" // HealthCheckRouterName Specify the name of HealthCheckRouter HealthCheckRouterName = "health_check" - + // TagRouterName Specify the name of TagRouter + TagRouterName = "tag" // ConditionRouterRuleSuffix Specify condition router suffix ConditionRouterRuleSuffix = ".condition-router" @@ -171,6 +182,10 @@ const ( RouterEnabled = "enabled" // Priority Priority key in router module RouterPriority = "priority" + + // ForceUseTag is the tag in attachment + ForceUseTag = "dubbo.force.tag" + Tagkey = "dubbo.tag" ) const ( @@ -206,6 +221,19 @@ const ( SECRET_ACCESS_KEY_KEY = "secretAccessKey" ) +// metadata report + +const ( + METACONFIG_REMOTE = "remote" + METACONFIG_LOCAL = "local" + KEY_SEPARATOR = ":" + DEFAULT_PATH_TAG = "metadata" + KEY_REVISON_PREFIX = "revision" + + // metadata service + METADATA_SERVICE_NAME = "org.apache.dubbo.metadata.MetadataService" +) + // HealthCheck Router const ( // The key of HealthCheck SPI @@ -227,3 +255,9 @@ const ( // The default time window of circuit-tripped in millisecond if not specfied MAX_CIRCUIT_TRIPPED_TIMEOUT_IN_MS = 30000 ) + +// service discovery + +const ( + NACOS_GROUP = "nacos.group" +) diff --git a/common/extension/event_dispatcher.go b/common/extension/event_dispatcher.go new file mode 100644 index 0000000000..2d33528259 --- /dev/null +++ b/common/extension/event_dispatcher.go @@ -0,0 +1,62 @@ +/* + * 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/common/logger" + "github.com/apache/dubbo-go/common/observer" +) + +var ( + globalEventDispatcher observer.EventDispatcher + initEventListeners []observer.EventListener +) + +var ( + dispatchers = make(map[string]func() observer.EventDispatcher, 8) +) + +// SetEventDispatcher by name +func SetEventDispatcher(name string, v func() observer.EventDispatcher) { + dispatchers[name] = v +} + +// SetAndInitGlobalDispatcher +func SetAndInitGlobalDispatcher(name string) { + if len(name) == 0 { + name = "direct" + } + if globalEventDispatcher != nil { + logger.Warnf("EventDispatcher already init. It will be replaced") + } + if dp, ok := dispatchers[name]; !ok || dp == nil { + panic("EventDispatcher for " + name + " is not existing, make sure you have import the package.") + } + globalEventDispatcher = dispatchers[name]() + globalEventDispatcher.AddEventListeners(initEventListeners) +} + +// GetGlobalDispatcher +func GetGlobalDispatcher() observer.EventDispatcher { + return globalEventDispatcher +} + +// AddEventListener it will be added in global event dispatcher +func AddEventListener(listener observer.EventListener) { + initEventListeners = append(initEventListeners, listener) +} diff --git a/common/extension/metadata_report_factory.go b/common/extension/metadata_report_factory.go new file mode 100644 index 0000000000..89dab04099 --- /dev/null +++ b/common/extension/metadata_report_factory.go @@ -0,0 +1,39 @@ +/* + * 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/report/factory" +) + +var ( + metaDataReportFactories = make(map[string]func() factory.MetadataReportFactory, 8) +) + +// SetMetadataReportFactory ... +func SetMetadataReportFactory(name string, v func() factory.MetadataReportFactory) { + metaDataReportFactories[name] = v +} + +// GetMetadataReportFactory ... +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.") + } + return metaDataReportFactories[name]() +} diff --git a/common/extension/registry_directory.go b/common/extension/registry_directory.go new file mode 100644 index 0000000000..6b92189c4e --- /dev/null +++ b/common/extension/registry_directory.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 extension + +import ( + "github.com/apache/dubbo-go/cluster" + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/registry" +) + +type registryDirectory func(url *common.URL, registry registry.Registry) (cluster.Directory, error) + +var defaultRegistry registryDirectory + +// SetDefaultRegistryDirectory ... +func SetDefaultRegistryDirectory(v registryDirectory) { + defaultRegistry = v +} + +// GetDefaultRegistryDirectory ... +func GetDefaultRegistryDirectory(config *common.URL, registry registry.Registry) (cluster.Directory, error) { + if defaultRegistry == nil { + panic("registry directory is not existing, make sure you have import the package.") + } + return defaultRegistry(config, registry) +} diff --git a/common/extension/router_factory.go b/common/extension/router_factory.go index 70d71dfa85..1339228618 100644 --- a/common/extension/router_factory.go +++ b/common/extension/router_factory.go @@ -28,7 +28,7 @@ import ( var ( routers = make(map[string]func() router.RouterFactory) fileRouterFactoryOnce sync.Once - fileRouterFactories = make(map[string]router.FIleRouterFactory) + fileRouterFactories = make(map[string]router.FileRouterFactory) ) // SetRouterFactory Set create router factory function by name @@ -50,7 +50,7 @@ func GetRouterFactories() map[string]func() router.RouterFactory { } // GetFileRouterFactories Get all create file router factory instance -func GetFileRouterFactories() map[string]router.FIleRouterFactory { +func GetFileRouterFactories() map[string]router.FileRouterFactory { l := len(routers) if l == 0 { return nil @@ -58,7 +58,7 @@ func GetFileRouterFactories() map[string]router.FIleRouterFactory { fileRouterFactoryOnce.Do(func() { for k := range routers { factory := GetRouterFactory(k) - if fr, ok := factory.(router.FIleRouterFactory); ok { + if fr, ok := factory.(router.FileRouterFactory); ok { fileRouterFactories[k] = fr } } diff --git a/common/extension/service_discovery.go b/common/extension/service_discovery.go new file mode 100644 index 0000000000..25b80cf335 --- /dev/null +++ b/common/extension/service_discovery.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 extension + +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) +) + +// SetServiceDiscovery will store the creator and name +func SetServiceDiscovery(name string, creator func(url *common.URL) (registry.ServiceDiscovery, error)) { + discoveryCreatorMap[name] = creator +} + +// GetServiceDiscovery will return the registry.ServiceDiscovery +// 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] + if !ok { + return nil, perrors.New("Could not find the service discovery with name: " + name) + } + return creator(url) +} diff --git a/common/observer/dispatcher/direct_event_dispatcher.go b/common/observer/dispatcher/direct_event_dispatcher.go new file mode 100644 index 0000000000..2b7567b47e --- /dev/null +++ b/common/observer/dispatcher/direct_event_dispatcher.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 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.BaseListenable +} + +// NewDirectEventDispatcher ac constructor of DirectEventDispatcher +func NewDirectEventDispatcher() observer.EventDispatcher { + return &DirectEventDispatcher{} +} + +// Dispatch event directly +func (ded *DirectEventDispatcher) Dispatch(event observer.Event) { + if event == nil { + logger.Warnf("[DirectEventDispatcher] dispatch event nil") + return + } + eventType := reflect.TypeOf(event).Elem() + value, loaded := ded.ListenersCache.Load(eventType) + if !loaded { + return + } + listenersSlice := value.([]observer.EventListener) + 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 0000000000..355c930a9e --- /dev/null +++ b/common/observer/dispatcher/direct_event_dispatcher_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 dispatcher + +import ( + "fmt" + "reflect" + "testing" +) + +import ( + "github.com/apache/dubbo-go/common/observer" +) + +func TestDirectEventDispatcher_Dispatch(t *testing.T) { + ded := NewDirectEventDispatcher() + ded.AddEventListener(&TestEventListener{}) + ded.AddEventListener(&TestEventListener1{}) + ded.Dispatch(&TestEvent{}) + ded.Dispatch(nil) +} + +type TestEvent struct { + observer.BaseEvent +} + +type TestEventListener struct { + observer.BaseListenable + 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 0000000000..45cdaa71a2 --- /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 0000000000..d78179043e --- /dev/null +++ b/common/observer/event.go @@ -0,0 +1,66 @@ +/* + * 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) +} + +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 0000000000..17745e68c0 --- /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 0000000000..8db60d8475 --- /dev/null +++ b/common/observer/event_listener.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 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 +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 +} + +// TODO (implement ConditionalEventListener) +type ServiceInstancesChangedListener struct { + ServiceName string +} diff --git a/common/observer/listenable.go b/common/observer/listenable.go new file mode 100644 index 0000000000..7b64aa8f2d --- /dev/null +++ b/common/observer/listenable.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 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() +} + +// BaseListenable base listenable +type BaseListenable struct { + Listenable + ListenersCache sync.Map + Mutex sync.Mutex +} + +// NewBaseListenable a constructor of base listenable +func NewBaseListenable() Listenable { + return &BaseListenable{} +} + +// AddEventListener add event listener +func (bl *BaseListenable) AddEventListener(listener EventListener) { + eventType := listener.GetEventType() + if eventType.Kind() == reflect.Ptr { + eventType = eventType.Elem() + } + bl.Mutex.Lock() + defer bl.Mutex.Unlock() + value, loaded := bl.ListenersCache.LoadOrStore(eventType, make([]EventListener, 0, 8)) + listenersSlice := value.([]EventListener) + // 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.Store(eventType, listenersSlice) +} + +// AddEventListeners add the slice of event listener +func (bl *BaseListenable) AddEventListeners(listenersSlice []EventListener) { + for _, listener := range listenersSlice { + bl.AddEventListener(listener) + } +} + +// RemoveEventListener remove the event listener +func (bl *BaseListenable) RemoveEventListener(listener EventListener) { + eventType := listener.GetEventType() + if eventType.Kind() == reflect.Ptr { + eventType = eventType.Elem() + } + bl.Mutex.Lock() + defer bl.Mutex.Unlock() + value, loaded := bl.ListenersCache.Load(eventType) + if !loaded { + return + } + listenersSlice := value.([]EventListener) + for i, l := range listenersSlice { + if l == listener { + listenersSlice = append(listenersSlice[:i], listenersSlice[i+1:]...) + } + } + bl.ListenersCache.Store(eventType, listenersSlice) +} + +// RemoveEventListeners remove the slice of event listener +func (bl *BaseListenable) RemoveEventListeners(listenersSlice []EventListener) { + for _, listener := range listenersSlice { + bl.RemoveEventListener(listener) + } +} + +// RemoveAllEventListeners remove all +func (bl *BaseListenable) RemoveAllEventListeners() { + bl.Mutex.Lock() + defer bl.Mutex.Unlock() + bl.ListenersCache = sync.Map{} +} + +// GetAllEventListeners get all +func (bl *BaseListenable) GetAllEventListeners() []EventListener { + allListenersSlice := make([]EventListener, 0, 16) + bl.ListenersCache.Range(func(_, value interface{}) bool { + listenersSlice := value.([]EventListener) + allListenersSlice = append(allListenersSlice, listenersSlice...) + return true + }) + sort.Slice(allListenersSlice, func(i, j int) bool { + return allListenersSlice[i].GetPriority() < allListenersSlice[j].GetPriority() + }) + return allListenersSlice +} + +// containListener true if contain listener +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 0000000000..df46bfc2ba --- /dev/null +++ b/common/observer/listenable_test.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 observer + +import ( + "reflect" + "testing" +) + +import ( + "github.com/stretchr/testify/assert" +) + +func TestListenable(t *testing.T) { + el := &TestEventListener{} + b := &BaseListenable{} + 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/proxy/proxy.go b/common/proxy/proxy.go index 68ba3ff788..f98a44873a 100644 --- a/common/proxy/proxy.go +++ b/common/proxy/proxy.go @@ -44,7 +44,7 @@ var ( typError = reflect.Zero(reflect.TypeOf((*error)(nil)).Elem()).Type() ) -// NewProxy ... +// NewProxy create service proxy. func NewProxy(invoke protocol.Invoker, callBack interface{}, attachments map[string]string) *Proxy { return &Proxy{ invoke: invoke, @@ -59,7 +59,6 @@ func NewProxy(invoke protocol.Invoker, callBack interface{}, attachments map[str // type XxxProvider struct { // Yyy func(ctx context.Context, args []interface{}, rsp *Zzz) error // } - func (p *Proxy) Implement(v common.RPCService) { // check parameters, incoming interface must be a elem's pointer. @@ -202,12 +201,12 @@ func (p *Proxy) Implement(v common.RPCService) { } -// Get ... +// Get get rpc service instance. func (p *Proxy) Get() common.RPCService { return p.rpc } -// GetCallback ... +// GetCallback get callback. func (p *Proxy) GetCallback() interface{} { return p.callBack } diff --git a/common/proxy/proxy_factory.go b/common/proxy/proxy_factory.go index 7b249a3e97..34fa3fd07e 100644 --- a/common/proxy/proxy_factory.go +++ b/common/proxy/proxy_factory.go @@ -22,7 +22,7 @@ import ( "github.com/apache/dubbo-go/protocol" ) -// ProxyFactory ... +// ProxyFactory interface. type ProxyFactory interface { GetProxy(invoker protocol.Invoker, url *common.URL) *Proxy GetAsyncProxy(invoker protocol.Invoker, callBack interface{}, url *common.URL) *Proxy diff --git a/common/rpc_service.go b/common/rpc_service.go index b235c32abc..90ebdaa6dc 100644 --- a/common/rpc_service.go +++ b/common/rpc_service.go @@ -59,7 +59,6 @@ type AsyncCallback func(response CallbackResponse) // return map[string][string]{} // } const ( - // METHOD_MAPPER ... METHOD_MAPPER = "MethodMapper" ) @@ -68,10 +67,11 @@ var ( // because Typeof takes an empty interface value. This is annoying. typeOfError = reflect.TypeOf((*error)(nil)).Elem() - // ServiceMap ... + // ServiceMap store description of service. // todo: lowerecas? ServiceMap = &serviceMap{ - serviceMap: make(map[string]map[string]*Service), + serviceMap: make(map[string]map[string]*Service), + interfaceMap: make(map[string][]*Service), } ) @@ -79,7 +79,7 @@ var ( // info of method ////////////////////////// -// MethodType ... +// MethodType is description of service method. type MethodType struct { method reflect.Method ctxType reflect.Type // request context @@ -87,27 +87,27 @@ type MethodType struct { replyType reflect.Type // return value, otherwise it is nil } -// Method ... +// Method get @m.method. func (m *MethodType) Method() reflect.Method { return m.method } -// CtxType ... +// CtxType get @m.ctxType. func (m *MethodType) CtxType() reflect.Type { return m.ctxType } -// ArgsType ... +// ArgsType get @m.argsType. func (m *MethodType) ArgsType() []reflect.Type { return m.argsType } -// ReplyType ... +// ReplyType get @m.replyType. func (m *MethodType) ReplyType() reflect.Type { return m.replyType } -// SuiteContext ... +// SuiteContext tranfer @ctx to reflect.Value type or get it from @m.ctxType. func (m *MethodType) SuiteContext(ctx context.Context) reflect.Value { if contextv := reflect.ValueOf(ctx); contextv.IsValid() { return contextv @@ -119,7 +119,7 @@ func (m *MethodType) SuiteContext(ctx context.Context) reflect.Value { // info of service interface ////////////////////////// -// Service ... +// Service is description of service type Service struct { name string rcvr reflect.Value @@ -127,17 +127,22 @@ type Service struct { methods map[string]*MethodType } -// Method ... +// Method get @s.methods. func (s *Service) Method() map[string]*MethodType { return s.methods } -// RcvrType ... +// Name will return service name +func (s *Service) Name() string { + return s.name +} + +// RcvrType get @s.rcvrType. func (s *Service) RcvrType() reflect.Type { return s.rcvrType } -// Rcvr ... +// Rcvr get @s.rcvr. func (s *Service) Rcvr() reflect.Value { return s.rcvr } @@ -147,10 +152,12 @@ func (s *Service) Rcvr() reflect.Value { ////////////////////////// type serviceMap struct { - mutex sync.RWMutex // protects the serviceMap - serviceMap map[string]map[string]*Service // protocol -> service name -> service + mutex sync.RWMutex // protects the serviceMap + serviceMap map[string]map[string]*Service // protocol -> service name -> service + interfaceMap map[string][]*Service // interface -> service } +// GetService get a service defination by protocol and name func (sm *serviceMap) GetService(protocol, name string) *Service { sm.mutex.RLock() defer sm.mutex.RUnlock() @@ -163,10 +170,24 @@ func (sm *serviceMap) GetService(protocol, name string) *Service { return nil } -func (sm *serviceMap) Register(protocol string, rcvr RPCService) (string, error) { +// GetInterface get an interface defination by interface name +func (sm *serviceMap) GetInterface(interfaceName string) []*Service { + sm.mutex.RLock() + defer sm.mutex.RUnlock() + if s, ok := sm.interfaceMap[interfaceName]; ok { + return s + } + return nil +} + +// Register register a service by @interfaceName and @protocol +func (sm *serviceMap) Register(interfaceName, protocol string, rcvr RPCService) (string, error) { if sm.serviceMap[protocol] == nil { sm.serviceMap[protocol] = make(map[string]*Service) } + if sm.interfaceMap[interfaceName] == nil { + sm.interfaceMap[interfaceName] = make([]*Service, 0, 16) + } s := new(Service) s.rcvrType = reflect.TypeOf(rcvr) @@ -201,32 +222,65 @@ func (sm *serviceMap) Register(protocol string, rcvr RPCService) (string, error) } sm.mutex.Lock() sm.serviceMap[protocol][s.name] = s + sm.interfaceMap[interfaceName] = append(sm.interfaceMap[interfaceName], s) sm.mutex.Unlock() return strings.TrimSuffix(methods, ","), nil } -func (sm *serviceMap) UnRegister(protocol, serviceId string) error { +// UnRegister cancel a service by @interfaceName, @protocol and @serviceId +func (sm *serviceMap) UnRegister(interfaceName, protocol, serviceId string) error { if protocol == "" || serviceId == "" { return perrors.New("protocol or serviceName is nil") } - sm.mutex.RLock() - svcs, ok := sm.serviceMap[protocol] - if !ok { - sm.mutex.RUnlock() - return perrors.New("no services for " + protocol) + + var ( + err error + index = -1 + svcs map[string]*Service + svrs []*Service + ok bool + ) + + f := func() error { + sm.mutex.RLock() + defer sm.mutex.RUnlock() + svcs, ok = sm.serviceMap[protocol] + if !ok { + return perrors.New("no services for " + protocol) + } + s, ok := svcs[serviceId] + if !ok { + return perrors.New("no service for " + serviceId) + } + svrs, ok = sm.interfaceMap[interfaceName] + if !ok { + return perrors.New("no service for " + interfaceName) + } + for i, svr := range svrs { + if svr == s { + index = i + } + } + return nil } - _, ok = svcs[serviceId] - if !ok { - sm.mutex.RUnlock() - return perrors.New("no service for " + serviceId) + + if err = f(); err != nil { + return err } - sm.mutex.RUnlock() sm.mutex.Lock() defer sm.mutex.Unlock() + sm.interfaceMap[interfaceName] = make([]*Service, 0, len(svrs)) + for i, _ := range svrs { + if i != index { + sm.interfaceMap[interfaceName] = append(sm.interfaceMap[interfaceName], svrs[i]) + } + } delete(svcs, serviceId) - delete(sm.serviceMap, protocol) + if len(sm.serviceMap) == 0 { + delete(sm.serviceMap, protocol) + } return nil } diff --git a/common/rpc_service_test.go b/common/rpc_service_test.go index 8c9b9d15cd..2311205d0e 100644 --- a/common/rpc_service_test.go +++ b/common/rpc_service_test.go @@ -77,46 +77,48 @@ func TestServiceMap_Register(t *testing.T) { // lowercase s0 := &testService{} // methods, err := ServiceMap.Register("testporotocol", s0) - _, err := ServiceMap.Register("testporotocol", s0) + _, err := ServiceMap.Register("testService", "testporotocol", s0) assert.EqualError(t, err, "type testService is not exported") // succ s := &TestService{} - methods, err := ServiceMap.Register("testporotocol", s) + methods, err := ServiceMap.Register("testService", "testporotocol", s) assert.NoError(t, err) assert.Equal(t, "MethodOne,MethodThree,methodTwo", methods) // repeat - _, err = ServiceMap.Register("testporotocol", s) + _, err = ServiceMap.Register("testService", "testporotocol", s) assert.EqualError(t, err, "service already defined: com.test.Path") // no method s1 := &TestService1{} - _, err = ServiceMap.Register("testporotocol", s1) + _, err = ServiceMap.Register("testService", "testporotocol", s1) assert.EqualError(t, err, "type com.test.Path1 has no exported methods of suitable type") ServiceMap = &serviceMap{ - serviceMap: make(map[string]map[string]*Service), + serviceMap: make(map[string]map[string]*Service), + interfaceMap: make(map[string][]*Service), } } func TestServiceMap_UnRegister(t *testing.T) { s := &TestService{} - _, err := ServiceMap.Register("testprotocol", s) + _, err := ServiceMap.Register("TestService", "testprotocol", s) assert.NoError(t, err) assert.NotNil(t, ServiceMap.GetService("testprotocol", "com.test.Path")) + assert.Equal(t, 1, len(ServiceMap.GetInterface("TestService"))) - err = ServiceMap.UnRegister("", "com.test.Path") + err = ServiceMap.UnRegister("", "", "com.test.Path") assert.EqualError(t, err, "protocol or serviceName is nil") - err = ServiceMap.UnRegister("protocol", "com.test.Path") + err = ServiceMap.UnRegister("", "protocol", "com.test.Path") assert.EqualError(t, err, "no services for protocol") - err = ServiceMap.UnRegister("testprotocol", "com.test.Path1") + err = ServiceMap.UnRegister("", "testprotocol", "com.test.Path1") assert.EqualError(t, err, "no service for com.test.Path1") // succ - err = ServiceMap.UnRegister("testprotocol", "com.test.Path") + err = ServiceMap.UnRegister("TestService", "testprotocol", "com.test.Path") assert.NoError(t, err) } diff --git a/common/url.go b/common/url.go index ebb648db27..a70ac7dc9d 100644 --- a/common/url.go +++ b/common/url.go @@ -195,7 +195,6 @@ func NewURLWithOptions(opts ...option) *URL { // NewURL will create a new url // the urlString should not be empty func NewURL(urlString string, opts ...option) (URL, error) { - var ( err error rawUrlString string @@ -249,7 +248,7 @@ func NewURL(urlString string, opts ...option) (URL, error) { return s, nil } -// URLEqual ... +// URLEqual judge @url and @c is equal or not. func (c URL) URLEqual(url URL) bool { c.Ip = "" c.Port = "" @@ -265,17 +264,19 @@ func (c URL) URLEqual(url URL) bool { } else if urlGroup == constant.ANY_VALUE { urlKey = strings.Replace(urlKey, "group=*", "group="+cGroup, 1) } + + // 1. protocol, username, password, ip, port, service name, group, version should be equal if cKey != urlKey { return false } + + // 2. if url contains enabled key, should be true, or * if url.GetParam(constant.ENABLED_KEY, "true") != "true" && url.GetParam(constant.ENABLED_KEY, "") != constant.ANY_VALUE { return false } + //TODO :may need add interface key any value condition - if !isMatchCategory(url.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY), c.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY)) { - return false - } - return true + return isMatchCategory(url.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY), c.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY)) } func isMatchCategory(category1 string, category2 string) bool { @@ -313,10 +314,9 @@ func (c URL) Key() string { "%s://%s:%s@%s:%s/?interface=%s&group=%s&version=%s", c.Protocol, c.Username, c.Password, c.Ip, c.Port, c.Service(), c.GetParam(constant.GROUP_KEY, ""), c.GetParam(constant.VERSION_KEY, "")) return buildString - //return c.ServiceKey() } -// ServiceKey ... +// ServiceKey get a unique key of a service. func (c URL) ServiceKey() string { intf := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")) if intf == "" { @@ -409,12 +409,12 @@ func (c *URL) RangeParams(f func(key, value string) bool) { // GetParam ... func (c URL) GetParam(s string, d string) string { - var r string c.paramsLock.RLock() - if r = c.params.Get(s); len(r) == 0 { + defer c.paramsLock.RUnlock() + r := c.params.Get(s) + if len(r) == 0 { r = d } - c.paramsLock.RUnlock() return r } @@ -454,10 +454,8 @@ func (c URL) GetRawParam(key string) string { // GetParamBool ... func (c URL) GetParamBool(s string, d bool) bool { - - var r bool - var err error - if r, err = strconv.ParseBool(c.GetParam(s, "")); err != nil { + r, err := strconv.ParseBool(c.GetParam(s, "")) + if err != nil { return d } return r @@ -465,10 +463,8 @@ func (c URL) GetParamBool(s string, d bool) bool { // GetParamInt ... func (c URL) GetParamInt(s string, d int64) int64 { - var r int - var err error - - if r, err = strconv.Atoi(c.GetParam(s, "")); r == 0 || err != nil { + r, err := strconv.Atoi(c.GetParam(s, "")) + if r == 0 || err != nil { return d } return int64(r) @@ -476,11 +472,8 @@ func (c URL) GetParamInt(s string, d int64) int64 { // GetMethodParamInt ... func (c URL) GetMethodParamInt(method string, key string, d int64) int64 { - var r int - var err error - c.paramsLock.RLock() - defer c.paramsLock.RUnlock() - if r, err = strconv.Atoi(c.GetParam("methods."+method+"."+key, "")); r == 0 || err != nil { + r, err := strconv.Atoi(c.GetParam("methods."+method+"."+key, "")) + if r == 0 || err != nil { return d } return int64(r) @@ -492,14 +485,13 @@ func (c URL) GetMethodParamInt64(method string, key string, d int64) int64 { if r == math.MinInt64 { return c.GetParamInt(key, d) } - return r } // GetMethodParam ... func (c URL) GetMethodParam(method string, key string, d string) string { - var r string - if r = c.GetParam("methods."+method+"."+key, ""); r == "" { + r := c.GetParam("methods."+method+"."+key, "") + if r == "" { r = d } return r @@ -530,7 +522,6 @@ func (c *URL) SetParams(m url.Values) { // ToMap transfer URL to Map func (c URL) ToMap() map[string]string { - paramsMap := make(map[string]string) c.RangeParams(func(key, value string) bool { @@ -615,8 +606,30 @@ func (c *URL) Clone() *URL { return newUrl } +// Copy url based on the reserved parameters' keys. +func (c *URL) CloneWithParams(reserveParams []string) *URL { + params := url.Values{} + for _, reserveParam := range reserveParams { + v := c.GetParam(reserveParam, "") + if len(v) != 0 { + params.Set(reserveParam, v) + } + } + + return NewURLWithOptions( + WithProtocol(c.Protocol), + WithUsername(c.Username), + WithPassword(c.Password), + WithIp(c.Ip), + WithPort(c.Port), + WithPath(c.Path), + WithMethods(c.Methods), + WithParams(params), + ) +} + func mergeNormalParam(mergedUrl *URL, referenceUrl *URL, paramKeys []string) []func(method string) { - var methodConfigMergeFcn = []func(method string){} + methodConfigMergeFcn := make([]func(method string), 0, len(paramKeys)) for _, paramKey := range paramKeys { if v := referenceUrl.GetParam(paramKey, ""); len(v) > 0 { mergedUrl.SetParam(paramKey, v) diff --git a/common/yaml/yaml.go b/common/yaml/yaml.go index 7c31d71c35..93ebb16614 100644 --- a/common/yaml/yaml.go +++ b/common/yaml/yaml.go @@ -48,3 +48,7 @@ func UnmarshalYMLConfig(confProFile string, out interface{}) ([]byte, error) { } return confFileStream, yaml.Unmarshal(confFileStream, out) } + +func UnmarshalYML(data []byte, out interface{}) error { + return yaml.Unmarshal(data, out) +} diff --git a/common/yaml/yaml_test.go b/common/yaml/yaml_test.go index 45eee59048..c8b8258a68 100644 --- a/common/yaml/yaml_test.go +++ b/common/yaml/yaml_test.go @@ -46,6 +46,18 @@ func TestUnmarshalYMLConfig_Error(t *testing.T) { assert.Error(t, err) } +func TestUnmarshalYML(t *testing.T) { + c := &Config{} + b, err := LoadYMLConfig("./testdata/config.yml") + assert.NoError(t, err) + err = UnmarshalYML(b, c) + assert.NoError(t, err) + assert.Equal(t, "strTest", c.StrTest) + assert.Equal(t, 11, c.IntTest) + assert.Equal(t, false, c.BooleanTest) + assert.Equal(t, "childStrTest", c.ChildConfig.StrTest) +} + type Config struct { StrTest string `yaml:"strTest" default:"default" json:"strTest,omitempty" property:"strTest"` IntTest int `default:"109" yaml:"intTest" json:"intTest,omitempty" property:"intTest"` diff --git a/config/application_config.go b/config/application_config.go index 23ab7d34ac..33b47c81dd 100644 --- a/config/application_config.go +++ b/config/application_config.go @@ -33,6 +33,7 @@ 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 } // Prefix ... diff --git a/config/base_config.go b/config/base_config.go index 93c0ce6a66..f58138d2e5 100644 --- a/config/base_config.go +++ b/config/base_config.go @@ -42,14 +42,13 @@ 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"` - - fileStream *bytes.Buffer + ConfigCenterConfig *ConfigCenterConfig `yaml:"config_center" json:"config_center,omitempty"` + 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 } // startConfigCenter will start the config center. diff --git a/config/base_config_test.go b/config/base_config_test.go index d16b242092..60eccfb183 100644 --- a/config/base_config_test.go +++ b/config/base_config_test.go @@ -53,15 +53,6 @@ func Test_refresh(t *testing.T) { Owner: "dubbo", Environment: "test"}, Registries: map[string]*RegistryConfig{ - //"shanghai_reg1": { - // id: "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", @@ -156,15 +147,6 @@ func Test_appExternal_refresh(t *testing.T) { Owner: "dubbo", Environment: "test"}, Registries: map[string]*RegistryConfig{ - //"shanghai_reg1": { - // id: "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", @@ -251,15 +233,6 @@ func Test_appExternalWithoutId_refresh(t *testing.T) { Owner: "dubbo", Environment: "test"}, Registries: map[string]*RegistryConfig{ - //"shanghai_reg1": { - // id: "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", @@ -408,15 +381,6 @@ func Test_refreshProvider(t *testing.T) { Owner: "dubbo", Environment: "test"}, Registries: map[string]*RegistryConfig{ - //"shanghai_reg1": { - // id: "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", diff --git a/config/config_loader.go b/config/config_loader.go index c0687d8fc1..eb6b8a2fc6 100644 --- a/config/config_loader.go +++ b/config/config_loader.go @@ -33,6 +33,7 @@ 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 ( @@ -81,128 +82,145 @@ func checkApplicationName(config *ApplicationConfig) { } } -// Load Dubbo Init -func Load() { - - // init router - if confRouterFile != "" { - if errPro := RouterInit(confRouterFile); errPro != nil { - log.Printf("[routerConfig init] %#v", errPro) - } - } - - // reference config +func loadConsumerConfig() { if consumerConfig == nil { logger.Warnf("consumerConfig is nil!") - } else { - // init other consumer config - conConfigType := consumerConfig.ConfigType - for key, value := range extension.GetDefaultConfigReader() { - if conConfigType == nil { - if v, ok := conConfigType[key]; ok { - value = v - } - } - if err := extension.GetConfigReaders(value).ReadConsumerConfig(consumerConfig.fileStream); err != nil { - logger.Errorf("ReadConsumerConfig error: %#v for %s", perrors.WithStack(err), value) + return + } + // init other consumer config + conConfigType := consumerConfig.ConfigType + for key, value := range extension.GetDefaultConfigReader() { + if conConfigType == nil { + if v, ok := conConfigType[key]; ok { + value = v } } + if err := extension.GetConfigReaders(value).ReadConsumerConfig(consumerConfig.fileStream); err != nil { + logger.Errorf("ReadConsumerConfig error: %#v for %s", perrors.WithStack(err), value) + } + } - metricConfig = consumerConfig.MetricConfig - applicationConfig = consumerConfig.ApplicationConfig + metricConfig = consumerConfig.MetricConfig + applicationConfig = consumerConfig.ApplicationConfig + extension.SetAndInitGlobalDispatcher(consumerConfig.eventDispatcherType) - checkApplicationName(consumerConfig.ApplicationConfig) - if err := configCenterRefreshConsumer(); err != nil { - logger.Errorf("[consumer config center refresh] %#v", err) + checkApplicationName(consumerConfig.ApplicationConfig) + if err := configCenterRefreshConsumer(); err != nil { + logger.Errorf("[consumer config center refresh] %#v", err) + } + checkRegistries(consumerConfig.Registries, consumerConfig.Registry) + for key, ref := range consumerConfig.References { + if ref.Generic { + genericService := NewGenericService(key) + SetConsumerService(genericService) } - checkRegistries(consumerConfig.Registries, consumerConfig.Registry) - for key, ref := range consumerConfig.References { - if ref.Generic { - genericService := NewGenericService(key) - SetConsumerService(genericService) - } - rpcService := GetConsumerService(key) - if rpcService == nil { - logger.Warnf("%s does not exist!", key) - continue - } - ref.id = key - ref.Refer(rpcService) - ref.Implement(rpcService) + rpcService := GetConsumerService(key) + if rpcService == nil { + logger.Warnf("%s does not exist!", key) + continue } + ref.id = key + ref.Refer(rpcService) + ref.Implement(rpcService) + } - //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 - - if refconfig.invoker != nil && - !refconfig.invoker.IsAvailable() { - checkok = false - count++ - if count > maxWait { - errMsg := fmt.Sprintf("Failed to check the status of the service %v . No provider available for the service to the consumer use dubbo version %v", refconfig.InterfaceName, constant.Version) - logger.Error(errMsg) - panic(errMsg) - } - time.Sleep(time.Second * 1) - break - } - if refconfig.invoker == nil { - logger.Warnf("The interface %s invoker not exist , may you should check your interface config.", refconfig.InterfaceName) + //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 + + if refconfig.invoker != nil && + !refconfig.invoker.IsAvailable() { + checkok = false + count++ + if count > maxWait { + errMsg := fmt.Sprintf("Failed to check the status of the service %v . No provider available for the service to the consumer use dubbo version %v", refconfig.InterfaceName, constant.Version) + logger.Error(errMsg) + panic(errMsg) } + time.Sleep(time.Second * 1) + break + } + if refconfig.invoker == nil { + logger.Warnf("The interface %s invoker not exist , may you should check your interface config.", refconfig.InterfaceName) } } - if checkok { - break - } - checkok = true } + if checkok { + break + } + checkok = true } +} - // service config +func loadProviderConfig() { if providerConfig == nil { logger.Warnf("providerConfig is nil!") - } else { - // init other provider config - proConfigType := providerConfig.ConfigType - for key, value := range extension.GetDefaultConfigReader() { - if proConfigType != nil { - if v, ok := proConfigType[key]; ok { - value = v - } - } - if err := extension.GetConfigReaders(value).ReadProviderConfig(providerConfig.fileStream); err != nil { - logger.Errorf("ReadProviderConfig error: %#v for %s", perrors.WithStack(err), value) + return + } + + // init other provider config + proConfigType := providerConfig.ConfigType + for key, value := range extension.GetDefaultConfigReader() { + if proConfigType != nil { + if v, ok := proConfigType[key]; ok { + value = v } } + if err := extension.GetConfigReaders(value).ReadProviderConfig(providerConfig.fileStream); err != nil { + logger.Errorf("ReadProviderConfig error: %#v for %s", perrors.WithStack(err), value) + } + } - // so, you should know that the consumer's config will be override - metricConfig = providerConfig.MetricConfig - applicationConfig = providerConfig.ApplicationConfig + // so, you should know that the consumer's config will be override + metricConfig = providerConfig.MetricConfig + applicationConfig = providerConfig.ApplicationConfig + extension.SetAndInitGlobalDispatcher(providerConfig.eventDispatcherType) - checkApplicationName(providerConfig.ApplicationConfig) - if err := configCenterRefreshProvider(); err != nil { - logger.Errorf("[provider config center refresh] %#v", err) + 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 { + logger.Warnf("%s does not exist!", key) + continue } - checkRegistries(providerConfig.Registries, providerConfig.Registry) - for key, svs := range providerConfig.Services { - rpcService := GetProviderService(key) - if rpcService == nil { - logger.Warnf("%s does not exist!", key) - continue - } - svs.id = key - svs.Implement(rpcService) - if err := svs.Export(); err != nil { - panic(fmt.Sprintf("service %s export failed! ", key)) - } + 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)) } } +} + +func initRouter() { + if confRouterFile != "" { + if err := RouterInit(confRouterFile); err != nil { + log.Printf("[routerConfig init] %#v", err) + } + } +} + +// Load Dubbo Init +func Load() { + + // init router + initRouter() + + // reference config + loadConsumerConfig() + + // service config + loadProviderConfig() + // init the shutdown callback GracefulShutdownInit() } diff --git a/config/config_loader_test.go b/config/config_loader_test.go index 498f826780..0192b4c8a0 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 ( @@ -82,14 +83,15 @@ func TestLoad(t *testing.T) { conServices = map[string]common.RPCService{} proServices = map[string]common.RPCService{} - common.ServiceMap.UnRegister("mock", "MockService") + err := common.ServiceMap.UnRegister("com.MockService", "mock", "MockService") + assert.Nil(t, err) consumerConfig = nil providerConfig = nil } func TestLoadWithSingleReg(t *testing.T) { doInitConsumerWithSingleRegistry() - doInitProviderWithSingleRegistry() + mockInitProviderWithSingleRegistry() ms := &MockService{} SetConsumerService(ms) @@ -110,7 +112,7 @@ func TestLoadWithSingleReg(t *testing.T) { conServices = map[string]common.RPCService{} proServices = map[string]common.RPCService{} - common.ServiceMap.UnRegister("mock", "MockService") + common.ServiceMap.UnRegister("com.MockService", "mock", "MockService") consumerConfig = nil providerConfig = nil } @@ -139,7 +141,7 @@ func TestWithNoRegLoad(t *testing.T) { conServices = map[string]common.RPCService{} proServices = map[string]common.RPCService{} - common.ServiceMap.UnRegister("mock", "MockService") + common.ServiceMap.UnRegister("com.MockService", "mock", "MockService") consumerConfig = nil providerConfig = nil } @@ -232,3 +234,55 @@ func TestConfigLoaderWithConfigCenterSingleRegistry(t *testing.T) { assert.Equal(t, "mock://127.0.0.1:2182", consumerConfig.Registries[constant.DEFAULT_KEY].Address) } + +// mockInitProviderWithSingleRegistry will init a mocked providerConfig +func mockInitProviderWithSingleRegistry() { + providerConfig = &ProviderConfig{ + 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 1fa68415bf..debcd79fa2 100644 --- a/config/consumer_config.go +++ b/config/consumer_config.go @@ -44,6 +44,7 @@ type ConsumerConfig struct { 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 @@ -117,6 +118,7 @@ func ConsumerInit(confConFile string) error { return perrors.WithMessagef(err, "time.ParseDuration(Connect_Timeout{%#v})", consumerConfig.Connect_Timeout) } } + logger.Debugf("consumer config{%#v}\n", consumerConfig) return nil diff --git a/config/instance/metedata_report.go b/config/instance/metedata_report.go new file mode 100644 index 0000000000..8e833dd70b --- /dev/null +++ b/config/instance/metedata_report.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 instance + +import ( + "sync" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/metadata/report" +) + +var ( + instance report.MetadataReport + reportUrl common.URL + once sync.Once +) + +// 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() { + 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/metadata_report_config.go b/config/metadata_report_config.go new file mode 100644 index 0000000000..41fb6b4769 --- /dev/null +++ b/config/metadata_report_config.go @@ -0,0 +1,110 @@ +/* + * 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 ( + "net/url" +) + +import ( + "github.com/creasty/defaults" + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/config/instance" +) + +// MethodConfig ... +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"` +} + +// Prefix ... +func (c *MetadataReportConfig) Prefix() string { + return constant.MetadataReportPrefix +} + +// UnmarshalYAML ... +func (c *MetadataReportConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + if err := defaults.Set(c); err != nil { + return perrors.WithStack(err) + } + type plain MetadataReportConfig + if err := unmarshal((*plain)(c)); err != nil { + return perrors.WithStack(err) + } + return nil +} + +// ToUrl ... +func (c *MetadataReportConfig) ToUrl() (*common.URL, error) { + urlMap := make(url.Values) + + if c.Params != nil { + for k, v := range c.Params { + urlMap.Set(k, v) + } + } + + url, err := common.NewURL(c.Address, + common.WithParams(urlMap), + common.WithUsername(c.Username), + common.WithPassword(c.Password), + common.WithLocation(c.Address), + common.WithProtocol(c.Protocol), + ) + if err != nil || len(url.Protocol) == 0 { + return nil, perrors.New("Invalid MetadataReportConfig.") + } + url.SetParam("metadata", url.Protocol) + return &url, nil +} + +func (c *MetadataReportConfig) IsValid() bool { + return len(c.Protocol) != 0 +} + +// StartMetadataReport: The entry of metadata report start +func startMetadataReport(metadataType string, metadataReportConfig *MetadataReportConfig) error { + 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 url, err := metadataReportConfig.ToUrl(); err == nil { + instance.GetMetadataReportInstance(url) + } else { + return perrors.New("MetadataConfig is invalid!") + } + + return nil +} diff --git a/config/metadata_report_config_test.go b/config/metadata_report_config_test.go new file mode 100644 index 0000000000..635feecc2d --- /dev/null +++ b/config/metadata_report_config_test.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 config + +import "testing" + +import ( + "github.com/stretchr/testify/assert" +) + +func TestMetadataReportConfig_ToUrl(t *testing.T) { + metadataReportConfig := MetadataReportConfig{ + Protocol: "mock", + Address: "127.0.0.1:2181", + Username: "test", + Password: "test", + TimeoutStr: "3s", + Params: map[string]string{ + "k": "v", + }, + } + url, error := metadataReportConfig.ToUrl() + assert.NoError(t, error) + assert.Equal(t, "mock", url.Protocol) + assert.Equal(t, "127.0.0.1:2181", url.Location) + assert.Equal(t, "127.0.0.1", url.Ip) + assert.Equal(t, "2181", url.Port) + assert.Equal(t, "test", url.Username) + assert.Equal(t, "test", url.Password) + assert.Equal(t, "v", url.GetParam("k", "")) + assert.Equal(t, "mock", url.GetParam("metadata", "")) +} diff --git a/config/provider_config.go b/config/provider_config.go index 14b77cafb3..7956991745 100644 --- a/config/provider_config.go +++ b/config/provider_config.go @@ -41,6 +41,8 @@ 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"` ApplicationConfig *ApplicationConfig `yaml:"application" json:"application,omitempty" property:"application"` Registry *RegistryConfig `yaml:"registry" json:"registry,omitempty" property:"registry"` @@ -95,7 +97,10 @@ func ProviderInit(confProFile string) error { 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 diff --git a/config/reference_config.go b/config/reference_config.go index 7ce0013194..3710cbc4bc 100644 --- a/config/reference_config.go +++ b/config/reference_config.go @@ -63,6 +63,7 @@ type ReferenceConfig struct { Generic bool `yaml:"generic" json:"generic,omitempty" property:"generic"` Sticky bool `yaml:"sticky" json:"sticky,omitempty" property:"sticky"` RequestTimeout string `yaml:"timeout" json:"timeout,omitempty" property:"timeout"` + ForceTag bool `yaml:"force.tag" json:"force.tag,omitempty" property:"force.tag"` } // Prefix ... @@ -99,7 +100,9 @@ func (c *ReferenceConfig) Refer(_ interface{}) { common.WithParams(c.getUrlMap()), common.WithParamsValue(constant.BEAN_NAME_KEY, c.id), ) - + if c.ForceTag { + cfgURL.AddParam(constant.ForceUseTag, "true") + } if c.Url != "" { // 1. user specified URL, could be peer-to-peer address, or register center's address. urlStrings := gxstrings.RegSplit(c.Url, "\\s*[;]+\\s*") @@ -185,6 +188,10 @@ 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.RELEASE_KEY, "dubbo-golang-"+constant.Version) + urlMap.Set(constant.SIDE_KEY, (common.RoleType(common.CONSUMER)).Role()) + if len(c.RequestTimeout) != 0 { urlMap.Set(constant.TIMEOUT_KEY, c.RequestTimeout) } diff --git a/config/registry_config.go b/config/registry_config.go index 4e4b6e97d7..e877a2c19d 100644 --- a/config/registry_config.go +++ b/config/registry_config.go @@ -36,14 +36,15 @@ import ( // RegistryConfig ... type RegistryConfig struct { Protocol string `required:"true" yaml:"protocol" json:"protocol,omitempty" property:"protocol"` - //I changed "type" to "protocol" ,the same as "protocol" field in java class RegistryConfig + // I changed "type" to "protocol" ,the same as "protocol" field in java class RegistryConfig TimeoutStr string `yaml:"timeout" default:"5s" json:"timeout,omitempty" property:"timeout"` // unit: second Group string `yaml:"group" json:"group,omitempty" property:"group"` - //for registry - 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"` + // for registry + 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"` + Simplified bool `yaml:"simplified" json:"simplified,omitempty" property:"simplified"` + Params map[string]string `yaml:"params" json:"params,omitempty" property:"params"` } // UnmarshalYAML ... @@ -70,9 +71,11 @@ func loadRegistries(targetRegistries string, registries map[string]*RegistryConf for k, registryConf := range registries { target := false - // if user not config targetRegistries,default load all - // Notice:in func "func Split(s, sep string) []string" comment : if s does not contain sep and sep is not empty, SplitAfter returns a slice of length 1 whose only element is s. - // So we have to add the condition when targetRegistries string is not set (it will be "" when not set) + // if user not config targetRegistries, default load all + // Notice: in func "func Split(s, sep string) []string" comment: + // if s does not contain sep and sep is not empty, SplitAfter returns + // a slice of length 1 whose only element is s. So we have to add the + // condition when targetRegistries string is not set (it will be "" when not set) if len(trSlice) == 0 || (len(trSlice) == 1 && trSlice[0] == "") { target = true } else { @@ -86,29 +89,24 @@ func loadRegistries(targetRegistries string, registries map[string]*RegistryConf } if target { - var ( - url common.URL - err error - ) - addresses := strings.Split(registryConf.Address, ",") address := addresses[0] - address = traslateRegistryConf(address, registryConf) - url, err = common.NewURL(constant.REGISTRY_PROTOCOL+"://"+address, + address = translateRegistryConf(address, registryConf) + url, err := common.NewURL(constant.REGISTRY_PROTOCOL+"://"+address, common.WithParams(registryConf.getUrlMap(roleType)), + common.WithParamsValue("simplified", strconv.FormatBool(registryConf.Simplified)), common.WithUsername(registryConf.Username), common.WithPassword(registryConf.Password), common.WithLocation(registryConf.Address), ) if err != nil { - logger.Errorf("The registry id:%s url is invalid , error: %#v", k, err) + logger.Errorf("The registry id: %s url is invalid, error: %#v", k, err) panic(err) } else { urls = append(urls, &url) } } - } return urls @@ -123,15 +121,14 @@ func (c *RegistryConfig) getUrlMap(roleType common.RoleType) url.Values { for k, v := range c.Params { urlMap.Set(k, v) } - return urlMap } -func traslateRegistryConf(address string, registryConf *RegistryConfig) string { +func translateRegistryConf(address string, registryConf *RegistryConfig) string { if strings.Contains(address, "://") { translatedUrl, err := url.Parse(address) if err != nil { - logger.Errorf("The registry url is invalid , error: %#v", err) + logger.Errorf("The registry url is invalid, error: %#v", err) panic(err) } address = translatedUrl.Host diff --git a/config/condition_router_config.go b/config/router_config.go similarity index 89% rename from config/condition_router_config.go rename to config/router_config.go index 87e835108e..0670ee9c20 100644 --- a/config/condition_router_config.go +++ b/config/router_config.go @@ -28,13 +28,14 @@ import ( "github.com/apache/dubbo-go/common/yaml" ) -//RouterInit Load config file to init router config +// RouterInit Load config file to init router config func RouterInit(confRouterFile string) error { fileRouterFactories := extension.GetFileRouterFactories() bytes, err := yaml.LoadYMLConfig(confRouterFile) if err != nil { return perrors.Errorf("ioutil.ReadFile(file:%s) = error:%v", confRouterFile, perrors.WithStack(err)) } + logger.Warnf("get fileRouterFactories len{%+v})", len(fileRouterFactories)) for k, factory := range fileRouterFactories { r, e := factory.NewFileRouter(bytes) if e == nil { @@ -42,7 +43,7 @@ func RouterInit(confRouterFile string) error { directory.AddRouterURLSet(&url) return nil } - logger.Warnf("router config type %s create fail \n", k) + 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) } diff --git a/config/condition_router_config_test.go b/config/router_config_test.go similarity index 100% rename from config/condition_router_config_test.go rename to config/router_config_test.go diff --git a/config/service_config.go b/config/service_config.go index 7d97fa4d1e..d233e2b8a5 100644 --- a/config/service_config.go +++ b/config/service_config.go @@ -18,6 +18,7 @@ package config import ( + "container/list" "context" "fmt" "net/url" @@ -29,6 +30,7 @@ import ( import ( "github.com/creasty/defaults" + gxnet "github.com/dubbogo/gost/net" perrors "github.com/pkg/errors" "go.uber.org/atomic" ) @@ -69,12 +71,17 @@ type ServiceConfig struct { ExecuteLimitRejectedHandler string `yaml:"execute.limit.rejected.handler" json:"execute.limit.rejected.handler,omitempty" property:"execute.limit.rejected.handler"` Auth string `yaml:"auth" json:"auth,omitempty" property:"auth"` 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 } // Prefix ... @@ -91,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 } @@ -104,6 +113,34 @@ 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() + for _, proto := range protocolConfigs { + if len(proto.Port) > 0 { + continue + } + + tcp, err := gxnet.ListenOnTCPRandomPort(proto.Ip) + if err != nil { + panic(perrors.New(fmt.Sprintf("Get tcp port error,err is {%v}", err))) + } + defer tcp.Close() + ports.PushBack(strings.Split(tcp.Addr().String(), ":")[1]) + } + return ports +} + // Export ... func (c *ServiceConfig) Export() error { // TODO: config center start here @@ -121,29 +158,44 @@ 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 } + + ports := getRandomPort(protocolConfigs) + nextPort := ports.Front() for _, proto := range protocolConfigs { // registry the service reflect - methods, err := common.ServiceMap.Register(proto.Name, c.rpcService) + methods, err := common.ServiceMap.Register(c.InterfaceName, proto.Name, c.rpcService) if err != nil { err := perrors.Errorf("The service %v export the protocol %v error! Error message is %v .", c.InterfaceName, proto.Name, err.Error()) logger.Errorf(err.Error()) return err } + + port := proto.Port + + if len(proto.Port) == 0 { + port = nextPort.Value.(string) + nextPort = nextPort.Next() + } ivkURL := common.NewURLWithOptions( common.WithPath(c.id), common.WithProtocol(proto.Name), common.WithIp(proto.Ip), - common.WithPort(proto.Port), + common.WithPort(port), common.WithParams(urlMap), common.WithParamsValue(constant.BEAN_NAME_KEY, c.id), common.WithMethods(strings.Split(methods, ",")), common.WithToken(c.Token), ) + if len(c.Tag) > 0 { + ivkURL.AddParam(constant.Tagkey, c.Tag) + } + + var exporter protocol.Exporter if len(regUrls) > 0 { for _, regUrl := range regUrls { @@ -157,22 +209,46 @@ 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 } +// 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 ... func (c *ServiceConfig) Implement(s common.RPCService) { c.rpcService = s @@ -193,6 +269,9 @@ func (c *ServiceConfig) getUrlMap() url.Values { urlMap.Set(constant.GROUP_KEY, c.Group) urlMap.Set(constant.VERSION_KEY, c.Version) urlMap.Set(constant.ROLE_KEY, strconv.Itoa(common.PROVIDER)) + urlMap.Set(constant.RELEASE_KEY, "dubbo-golang-"+constant.Version) + urlMap.Set(constant.SIDE_KEY, (common.RoleType(common.PROVIDER)).Role()) + // application info urlMap.Set(constant.APPLICATION_KEY, providerConfig.ApplicationConfig.Name) urlMap.Set(constant.ORGANIZATION_KEY, providerConfig.ApplicationConfig.Organization) @@ -239,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 6f32308903..387e3ced7a 100644 --- a/config/service_config_test.go +++ b/config/service_config_test.go @@ -21,6 +21,12 @@ import ( "testing" ) +import ( + gxnet "github.com/dubbogo/gost/net" + "github.com/stretchr/testify/assert" + "go.uber.org/atomic" +) + import ( "github.com/apache/dubbo-go/common/extension" ) @@ -92,6 +98,7 @@ func doInitProvider() { Weight: 200, }, }, + exported: new(atomic.Bool), }, "MockServiceNoRightProtocol": { InterfaceName: "com.MockService", @@ -116,56 +123,7 @@ func doInitProvider() { Weight: 200, }, }, - }, - }, - 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, - }, - }, + exported: new(atomic.Bool), }, }, Protocols: map[string]*ProtocolConfig{ @@ -189,3 +147,35 @@ func Test_Export(t *testing.T) { } providerConfig = nil } + +func Test_getRandomPort(t *testing.T) { + protocolConfigs := make([]*ProtocolConfig, 0, 3) + + ip, err := gxnet.GetLocalIP() + protocolConfigs = append(protocolConfigs, &ProtocolConfig{ + Ip: ip, + }) + protocolConfigs = append(protocolConfigs, &ProtocolConfig{ + Ip: ip, + }) + protocolConfigs = append(protocolConfigs, &ProtocolConfig{ + Ip: ip, + }) + assert.NoError(t, err) + ports := getRandomPort(protocolConfigs) + + assert.Equal(t, ports.Len(), len(protocolConfigs)) + + front := ports.Front() + for { + if front == nil { + break + } + t.Logf("port:%v", front.Value) + front = front.Next() + } + + protocolConfigs = make([]*ProtocolConfig, 0, 3) + ports = getRandomPort(protocolConfigs) + assert.Equal(t, ports.Len(), len(protocolConfigs)) +} diff --git a/config_center/apollo/impl.go b/config_center/apollo/impl.go index 3b5d1f4ebe..b049d334bc 100644 --- a/config_center/apollo/impl.go +++ b/config_center/apollo/impl.go @@ -25,7 +25,8 @@ import ( ) import ( - "github.com/pkg/errors" + gxset "github.com/dubbogo/gost/container/set" + perrors "github.com/pkg/errors" "github.com/zouyx/agollo" ) @@ -119,7 +120,7 @@ func getNamespaceName(namespace string, configFileFormat agollo.ConfigFileFormat func (c *apolloConfiguration) GetInternalProperty(key string, opts ...cc.Option) (string, error) { config := agollo.GetConfig(c.appConf.NamespaceName) if config == nil { - return "", errors.New(fmt.Sprintf("nothing in namespace:%s ", key)) + return "", perrors.New(fmt.Sprintf("nothing in namespace:%s ", key)) } return config.GetStringValue(key, ""), nil } @@ -128,6 +129,16 @@ func (c *apolloConfiguration) GetRule(key string, opts ...cc.Option) (string, er return c.GetInternalProperty(key, opts...) } +// PublishConfig will publish the config with the (key, group, value) pair +func (c *apolloConfiguration) PublishConfig(string, string, string) error { + return perrors.New("unsupport operation") +} + +// GetConfigKeysByGroup will return all keys with the group +func (c *apolloConfiguration) GetConfigKeysByGroup(group string) (*gxset.HashSet, error) { + return nil, perrors.New("unsupport operation") +} + func (c *apolloConfiguration) GetProperties(key string, opts ...cc.Option) (string, error) { /** * when group is not null, we are getting startup configs(config file) from Config Center, for example: @@ -135,7 +146,7 @@ func (c *apolloConfiguration) GetProperties(key string, opts ...cc.Option) (stri */ config := agollo.GetConfig(key) if config == nil { - return "", errors.New(fmt.Sprintf("nothing in namespace:%s ", key)) + return "", perrors.New(fmt.Sprintf("nothing in namespace:%s ", key)) } return config.GetContent(agollo.Properties), nil } diff --git a/config_center/dynamic_configuration.go b/config_center/dynamic_configuration.go index d6c3b06b32..9013d7140e 100644 --- a/config_center/dynamic_configuration.go +++ b/config_center/dynamic_configuration.go @@ -21,14 +21,18 @@ import ( "time" ) +import ( + gxset "github.com/dubbogo/gost/container/set" +) + import ( "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/config_center/parser" ) -////////////////////////////////////////// +// //////////////////////////////////////// // DynamicConfiguration -////////////////////////////////////////// +// //////////////////////////////////////// const ( // DEFAULT_GROUP: default group DEFAULT_GROUP = "dubbo" @@ -42,14 +46,20 @@ type DynamicConfiguration interface { SetParser(parser.ConfigurationParser) AddListener(string, ConfigurationListener, ...Option) RemoveListener(string, ConfigurationListener, ...Option) - //GetProperties get properties file + // GetProperties get properties file GetProperties(string, ...Option) (string, error) - //GetRule get Router rule properties file + // GetRule get Router rule properties file GetRule(string, ...Option) (string, error) - //GetInternalProperty get value by key in Default properties file(dubbo.properties) + // GetInternalProperty get value by key in Default properties file(dubbo.properties) GetInternalProperty(string, ...Option) (string, error) + + // PublishConfig will publish the config with the (key, group, value) pair + PublishConfig(string, string, string) error + + // GetConfigKeysByGroup will return all keys with the group + GetConfigKeysByGroup(group string) (*gxset.HashSet, error) } // Options ... @@ -75,7 +85,7 @@ func WithTimeout(time time.Duration) Option { } } -//GetRuleKey The format is '{interfaceName}:[version]:[group]' +// GetRuleKey The format is '{interfaceName}:[version]:[group]' func GetRuleKey(url common.URL) string { return url.ColonSeparatedKey() } diff --git a/config_center/mock_dynamic_config.go b/config_center/mock_dynamic_config.go index 4d972b629a..59c788b65b 100644 --- a/config_center/mock_dynamic_config.go +++ b/config_center/mock_dynamic_config.go @@ -22,6 +22,7 @@ import ( ) import ( + gxset "github.com/dubbogo/gost/container/set" "gopkg.in/yaml.v2" ) @@ -81,6 +82,16 @@ func (f *MockDynamicConfigurationFactory) GetDynamicConfiguration(_ *common.URL) } +// PublishConfig will publish the config with the (key, group, value) pair +func (c *MockDynamicConfiguration) PublishConfig(string, string, string) error { + return nil +} + +// GetConfigKeysByGroup will return all keys with the group +func (c *MockDynamicConfiguration) GetConfigKeysByGroup(group string) (*gxset.HashSet, error) { + return gxset.NewSet(c.content), nil +} + // MockDynamicConfiguration ... type MockDynamicConfiguration struct { parser parser.ConfigurationParser diff --git a/config_center/nacos/impl.go b/config_center/nacos/impl.go index 60ab89b003..0b8aceb23c 100644 --- a/config_center/nacos/impl.go +++ b/config_center/nacos/impl.go @@ -18,10 +18,12 @@ package nacos import ( + "strings" "sync" ) import ( + gxset "github.com/dubbogo/gost/container/set" "github.com/nacos-group/nacos-sdk-go/vo" perrors "github.com/pkg/errors" ) @@ -34,7 +36,10 @@ import ( "github.com/apache/dubbo-go/config_center/parser" ) -const nacosClientName = "nacos config_center" +const ( + nacosClientName = "nacos config_center" + maxKeysNum = 9999 +) type nacosDynamicConfiguration struct { url *common.URL @@ -74,7 +79,7 @@ func (n *nacosDynamicConfiguration) RemoveListener(key string, listener config_c n.removeListener(key, listener) } -//nacos distinguishes configuration files based on group and dataId. defalut group = "dubbo" and dataId = key +// GetProperties nacos distinguishes configuration files based on group and dataId. defalut group = "dubbo" and dataId = key func (n *nacosDynamicConfiguration) GetProperties(key string, opts ...config_center.Option) (string, error) { return n.GetRule(key, opts...) } @@ -84,6 +89,47 @@ func (n *nacosDynamicConfiguration) GetInternalProperty(key string, opts ...conf return n.GetProperties(key, opts...) } +// PublishConfig will publish the config with the (key, group, value) pair +func (n *nacosDynamicConfiguration) PublishConfig(key string, group string, value string) error { + + group = n.resolvedGroup(group) + + ok, err := (*n.client.Client()).PublishConfig(vo.ConfigParam{ + DataId: key, + Group: group, + Content: value, + }) + + if err != nil { + return perrors.WithStack(err) + } + if !ok { + return perrors.New("publish config to Nocos failed") + } + return nil +} + +// GetConfigKeysByGroup will return all keys with the group +func (n *nacosDynamicConfiguration) GetConfigKeysByGroup(group string) (*gxset.HashSet, error) { + 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.Content) + } + return result, nil +} + // GetRule Get router rule func (n *nacosDynamicConfiguration) GetRule(key string, opts ...config_center.Option) (string, error) { tmpOpts := &config_center.Options{} @@ -92,12 +138,12 @@ func (n *nacosDynamicConfiguration) GetRule(key string, opts ...config_center.Op } content, err := (*n.client.Client()).GetConfig(vo.ConfigParam{ DataId: key, - Group: tmpOpts.Group, + Group: n.resolvedGroup(tmpOpts.Group), }) if err != nil { return "", perrors.WithStack(err) } else { - return string(content), nil + return content, nil } } @@ -145,6 +191,15 @@ func (n *nacosDynamicConfiguration) Destroy() { n.closeConfigs() } +// resolvedGroup will regular the group. Now, it will replace the '/' with '-'. +// '/' is a special character for nacos +func (n *nacosDynamicConfiguration) resolvedGroup(group string) string { + if len(group) <= 0 { + return group + } + return strings.ReplaceAll(group, "/", "-") +} + // IsAvailable Get available status func (n *nacosDynamicConfiguration) IsAvailable() bool { select { @@ -155,12 +210,12 @@ func (n *nacosDynamicConfiguration) IsAvailable() bool { } } -func (r *nacosDynamicConfiguration) closeConfigs() { - r.cltLock.Lock() - client := r.client - r.client = nil - r.cltLock.Unlock() +func (n *nacosDynamicConfiguration) closeConfigs() { + n.cltLock.Lock() + client := n.client + n.client = nil + n.cltLock.Unlock() // Close the old client first to close the tmp node client.Close() - logger.Infof("begin to close provider nacos client") + logger.Infof("begin to close provider n client") } diff --git a/config_center/nacos/impl_test.go b/config_center/nacos/impl_test.go index b4e6f1d025..b74c155d92 100644 --- a/config_center/nacos/impl_test.go +++ b/config_center/nacos/impl_test.go @@ -60,12 +60,7 @@ func runMockConfigServer(configHandler func(http.ResponseWriter, *http.Request), func mockCommonNacosServer() *httptest.Server { return runMockConfigServer(func(writer http.ResponseWriter, request *http.Request) { - data := ` - dubbo.service.com.ikurento.user.UserProvider.cluster=failback - dubbo.service.com.ikurento.user.UserProvider.protocol=myDubbo1 - dubbo.protocols.myDubbo.port=20000 - dubbo.protocols.myDubbo.name=dubbo -` + data := "true" fmt.Fprintf(writer, "%s", data) }, func(writer http.ResponseWriter, request *http.Request) { data := `dubbo.properties%02dubbo%02dubbo.service.com.ikurento.user.UserProvider.cluster=failback` @@ -93,6 +88,44 @@ func Test_GetConfig(t *testing.T) { assert.NoError(t, err) } +func TestNacosDynamicConfiguration_GetConfigKeysByGroup(t *testing.T) { + data := ` +{ + "PageItems": [ + { + "Content": "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 TestNacosDynamicConfiguration_PublishConfig(t *testing.T) { + nacos, err := initNacosData(t) + assert.Nil(t, err) + key := "myKey" + group := "/custom/a/b" + value := "MyValue" + err = nacos.PublishConfig(key, group, value) + assert.Nil(t, err) +} + func Test_AddListener(t *testing.T) { nacos, err := initNacosData(t) assert.NoError(t, err) @@ -104,7 +137,7 @@ func Test_AddListener(t *testing.T) { } func Test_RemoveListener(t *testing.T) { - //TODO not supported in current go_nacos_sdk version + // TODO not supported in current go_nacos_sdk version } type mockDataListener struct { diff --git a/config_center/nacos/listener.go b/config_center/nacos/listener.go index 25c586586c..de74cff8f6 100644 --- a/config_center/nacos/listener.go +++ b/config_center/nacos/listener.go @@ -35,11 +35,11 @@ func callback(listener config_center.ConfigurationListener, namespace, group, da listener.Process(&config_center.ConfigChangeEvent{Key: dataId, Value: data, ConfigType: remoting.EventTypeUpdate}) } -func (l *nacosDynamicConfiguration) addListener(key string, listener config_center.ConfigurationListener) { - _, loaded := l.keyListeners.Load(key) +func (n *nacosDynamicConfiguration) addListener(key string, listener config_center.ConfigurationListener) { + _, loaded := n.keyListeners.Load(key) if !loaded { _, cancel := context.WithCancel(context.Background()) - err := (*l.client.Client()).ListenConfig(vo.ConfigParam{ + err := (*n.client.Client()).ListenConfig(vo.ConfigParam{ DataId: key, Group: "dubbo", OnChange: func(namespace, group, dataId, data string) { @@ -49,14 +49,14 @@ func (l *nacosDynamicConfiguration) addListener(key string, listener config_cent logger.Errorf("nacos : listen config fail, error:%v ", err) newListener := make(map[config_center.ConfigurationListener]context.CancelFunc) newListener[listener] = cancel - l.keyListeners.Store(key, newListener) + n.keyListeners.Store(key, newListener) } else { // TODO check goroutine alive, but this version of go_nacos_sdk is not support. logger.Infof("profile:%s. this profile is already listening", key) } } -func (l *nacosDynamicConfiguration) removeListener(key string, listener config_center.ConfigurationListener) { +func (n *nacosDynamicConfiguration) removeListener(key string, listener config_center.ConfigurationListener) { // TODO: not supported in current go_nacos_sdk version logger.Warn("not supported in current go_nacos_sdk version") } diff --git a/config_center/zookeeper/impl.go b/config_center/zookeeper/impl.go index 404243d475..0a1ce35306 100644 --- a/config_center/zookeeper/impl.go +++ b/config_center/zookeeper/impl.go @@ -25,6 +25,7 @@ import ( import ( "github.com/dubbogo/go-zookeeper/zk" + gxset "github.com/dubbogo/gost/container/set" perrors "github.com/pkg/errors" ) @@ -39,8 +40,9 @@ import ( const ( // ZkClient - //zookeeper client name - ZkClient = "zk config_center" + // zookeeper client name + ZkClient = "zk config_center" + pathSeparator = "/" ) type zookeeperDynamicConfiguration struct { @@ -74,7 +76,7 @@ func newZookeeperDynamicConfiguration(url *common.URL) (*zookeeperDynamicConfigu c.cacheListener = NewCacheListener(c.rootPath) err = c.client.Create(c.rootPath) - c.listener.ListenServiceEvent(c.rootPath, c.cacheListener) + c.listener.ListenServiceEvent(url, c.rootPath, c.cacheListener) return c, err } @@ -100,7 +102,7 @@ func newMockZookeeperDynamicConfiguration(url *common.URL, opts ...zookeeper.Opt c.cacheListener = NewCacheListener(c.rootPath) err = c.client.Create(c.rootPath) - go c.listener.ListenServiceEvent(c.rootPath, c.cacheListener) + go c.listener.ListenServiceEvent(url, c.rootPath, c.cacheListener) return tc, c, err } @@ -143,11 +145,39 @@ func (c *zookeeperDynamicConfiguration) GetProperties(key string, opts ...config return string(content), nil } -//For zookeeper, getConfig and getConfigs have the same meaning. +// GetInternalProperty For zookeeper, getConfig and getConfigs have the same meaning. func (c *zookeeperDynamicConfiguration) GetInternalProperty(key string, opts ...config_center.Option) (string, error) { return c.GetProperties(key, opts...) } +// PublishConfig will put the value into Zk with specific path +func (c *zookeeperDynamicConfiguration) PublishConfig(key string, group string, value string) error { + path := c.getPath(key, group) + err := c.client.CreateWithValue(path, []byte(value)) + if err != nil { + return perrors.WithStack(err) + } + return nil +} + +// GetConfigKeysByGroup will return all keys with the group +func (c *zookeeperDynamicConfiguration) GetConfigKeysByGroup(group string) (*gxset.HashSet, error) { + path := c.getPath("", group) + result, err := c.client.GetChildren(path) + if err != nil { + return nil, perrors.WithStack(err) + } + + if len(result) == 0 { + return nil, perrors.New("could not find keys with group: " + group) + } + set := gxset.NewSet() + for _, e := range result { + set.Add(e) + } + return set, nil +} + func (c *zookeeperDynamicConfiguration) GetRule(key string, opts ...config_center.Option) (string, error) { return c.GetProperties(key, opts...) } @@ -214,3 +244,17 @@ func (c *zookeeperDynamicConfiguration) closeConfigs() { func (c *zookeeperDynamicConfiguration) RestartCallBack() bool { return true } + +func (c *zookeeperDynamicConfiguration) getPath(key string, group string) string { + if len(key) == 0 { + return c.buildPath(group) + } + return c.buildPath(group) + pathSeparator + key +} + +func (c *zookeeperDynamicConfiguration) buildPath(group string) string { + if len(group) == 0 { + group = config_center.DEFAULT_GROUP + } + return c.rootPath + pathSeparator + group +} diff --git a/config_center/zookeeper/impl_test.go b/config_center/zookeeper/impl_test.go index 22e15193cb..30389122a3 100644 --- a/config_center/zookeeper/impl_test.go +++ b/config_center/zookeeper/impl_test.go @@ -24,6 +24,7 @@ import ( import ( "github.com/dubbogo/go-zookeeper/zk" + gxset "github.com/dubbogo/gost/container/set" "github.com/stretchr/testify/assert" ) @@ -156,6 +157,26 @@ func Test_RemoveListener(t *testing.T) { assert.Equal(t, "", listener.event) } +func TestZookeeperDynamicConfiguration_PublishConfig(t *testing.T) { + value := "Test Data" + customGroup := "Custom Group" + key := "myKey" + ts, zk := initZkData(config_center.DEFAULT_GROUP, t) + defer ts.Stop() + err := zk.PublishConfig(key, customGroup, value) + assert.Nil(t, err) + result, err := zk.GetInternalProperty("myKey", config_center.WithGroup(customGroup)) + assert.Nil(t, err) + assert.Equal(t, value, result) + + var keys *gxset.HashSet + keys, err = zk.GetConfigKeysByGroup(customGroup) + assert.Nil(t, err) + assert.Equal(t, 1, keys.Size()) + assert.True(t, keys.Contains(key)) + +} + type mockDataListener struct { wg sync.WaitGroup event string diff --git a/doc/pic/arch/dubbo-go-arch.png b/doc/pic/arch/dubbo-go-arch.png index 87726d8848..e5f1927152 100644 Binary files a/doc/pic/arch/dubbo-go-arch.png and b/doc/pic/arch/dubbo-go-arch.png differ diff --git a/doc/pic/arch/dubbo-go-ext.png b/doc/pic/arch/dubbo-go-ext.png new file mode 100644 index 0000000000..a5285c9557 Binary files /dev/null and b/doc/pic/arch/dubbo-go-ext.png differ diff --git a/filter/filter_impl/access_log_filter.go b/filter/filter_impl/access_log_filter.go index fbfe756517..49cdc2287c 100644 --- a/filter/filter_impl/access_log_filter.go +++ b/filter/filter_impl/access_log_filter.go @@ -71,14 +71,18 @@ func init() { * * the value of "accesslog" can be "true" or "default" too. * If the value is one of them, the access log will be record in log file which defined in log.yml + * AccessLogFilter is designed to be singleton */ type AccessLogFilter struct { logChan chan AccessLogData } -// Invoke ... +// Invoke will check whether user wants to use this filter. +// If we find the value of key constant.ACCESS_LOG_KEY, we will log the invocation info func (ef *AccessLogFilter) Invoke(ctx context.Context, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result { accessLog := invoker.GetUrl().GetParam(constant.ACCESS_LOG_KEY, "") + + // the user do not if len(accessLog) > 0 { accessLogData := AccessLogData{data: ef.buildAccessLogData(invoker, invocation), accessLog: accessLog} ef.logIntoChannel(accessLogData) @@ -86,7 +90,7 @@ func (ef *AccessLogFilter) Invoke(ctx context.Context, invoker protocol.Invoker, return invoker.Invoke(ctx, invocation) } -// it won't block the invocation +// logIntoChannel won't block the invocation func (ef *AccessLogFilter) logIntoChannel(accessLogData AccessLogData) { select { case ef.logChan <- accessLogData: @@ -97,6 +101,7 @@ func (ef *AccessLogFilter) logIntoChannel(accessLogData AccessLogData) { } } +// buildAccessLogData builds the access log data func (ef *AccessLogFilter) buildAccessLogData(_ protocol.Invoker, invocation protocol.Invocation) map[string]string { dataMap := make(map[string]string, 16) attachments := invocation.Attachments() @@ -130,11 +135,12 @@ func (ef *AccessLogFilter) buildAccessLogData(_ protocol.Invoker, invocation pro return dataMap } -// OnResponse ... +// OnResponse do nothing func (ef *AccessLogFilter) OnResponse(_ context.Context, result protocol.Result, _ protocol.Invoker, _ protocol.Invocation) protocol.Result { return result } +// writeLogToFile actually write the logs into file func (ef *AccessLogFilter) writeLogToFile(data AccessLogData) { accessLog := data.accessLog if isDefault(accessLog) { @@ -156,6 +162,12 @@ func (ef *AccessLogFilter) writeLogToFile(data AccessLogData) { } } +// openLogFile will open the log file with append mode. +// If the file is not found, it will create the file. +// Actually, the accessLog is the filename +// You may find out that, once we want to write access log into log file, +// we open the file again and again. +// It needs to be optimized. func (ef *AccessLogFilter) openLogFile(accessLog string) (*os.File, error) { logFile, err := os.OpenFile(accessLog, os.O_CREATE|os.O_APPEND|os.O_RDWR, LogFileMode) if err != nil { @@ -169,6 +181,12 @@ func (ef *AccessLogFilter) openLogFile(accessLog string) (*os.File, error) { return nil, err } last := fileInfo.ModTime().Format(FileDateFormat) + + // this is confused. + // for example, if the last = '2020-03-04' + // and today is '2020-03-05' + // we will create one new file to log access data + // By this way, we can split the access log based on days. if now != last { err = os.Rename(fileInfo.Name(), fileInfo.Name()+"."+now) if err != nil { @@ -180,11 +198,12 @@ func (ef *AccessLogFilter) openLogFile(accessLog string) (*os.File, error) { return logFile, err } +// isDefault check whether accessLog == true or accessLog == default func isDefault(accessLog string) bool { return strings.EqualFold("true", accessLog) || strings.EqualFold("default", accessLog) } -// GetAccessLogFilter ... +// GetAccessLogFilter return the instance of AccessLogFilter func GetAccessLogFilter() filter.Filter { accessLogFilter := &AccessLogFilter{logChan: make(chan AccessLogData, LogMaxBuffer)} go func() { @@ -195,12 +214,13 @@ func GetAccessLogFilter() filter.Filter { return accessLogFilter } -// AccessLogData ... +// AccessLogData defines the data that will be log into file type AccessLogData struct { accessLog string data map[string]string } +// toLogMessage convert the AccessLogData to String func (ef *AccessLogData) toLogMessage() string { builder := strings.Builder{} builder.WriteString("[") diff --git a/filter/filter_impl/generic_service_filter_test.go b/filter/filter_impl/generic_service_filter_test.go index 37c6af7450..2a911659f0 100644 --- a/filter/filter_impl/generic_service_filter_test.go +++ b/filter/filter_impl/generic_service_filter_test.go @@ -96,7 +96,7 @@ func TestGenericServiceFilter_Invoke(t *testing.T) { hessian.Object("222")}, } s := &TestService{} - _, _ = common.ServiceMap.Register("testprotocol", s) + _, _ = common.ServiceMap.Register("TestService", "testprotocol", s) rpcInvocation := invocation.NewRPCInvocation(methodName, aurguments, nil) filter := GetGenericServiceFilter() url, _ := common.NewURL("testprotocol://127.0.0.1:20000/com.test.Path") diff --git a/filter/filter_impl/tps/tps_limit_strategy_mock.go b/filter/filter_impl/tps/tps_limit_strategy_mock.go index 18809fccc9..c228c7349c 100644 --- a/filter/filter_impl/tps/tps_limit_strategy_mock.go +++ b/filter/filter_impl/tps/tps_limit_strategy_mock.go @@ -15,22 +15,6 @@ * limitations under the License. */ -// 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. -// - // Code generated by MockGen. DO NOT EDIT. // Source: tps_limit_strategy.go diff --git a/filter/filter_impl/tps/tps_limiter_method_service.go b/filter/filter_impl/tps/tps_limiter_method_service.go index 7fe8de9237..2d44c688eb 100644 --- a/filter/filter_impl/tps/tps_limiter_method_service.go +++ b/filter/filter_impl/tps/tps_limiter_method_service.go @@ -115,7 +115,12 @@ type MethodServiceTpsLimiterImpl struct { tpsState *concurrent.Map } -// IsAllowable ... +// IsAllowable based on method-level and service-level. +// The method-level has high priority which means that if there is any rate limit configuration for the method, +// the service-level rate limit strategy will be ignored. +// The key point is how to keep thread-safe +// This implementation use concurrent map + loadOrStore to make implementation thread-safe +// You can image that even multiple threads create limiter, but only one could store the limiter into tpsState func (limiter MethodServiceTpsLimiterImpl) IsAllowable(url common.URL, invocation protocol.Invocation) bool { methodConfigPrefix := "methods." + invocation.MethodName() + "." @@ -123,23 +128,30 @@ func (limiter MethodServiceTpsLimiterImpl) IsAllowable(url common.URL, invocatio methodLimitRateConfig := url.GetParam(methodConfigPrefix+constant.TPS_LIMIT_RATE_KEY, "") methodIntervalConfig := url.GetParam(methodConfigPrefix+constant.TPS_LIMIT_INTERVAL_KEY, "") + // service-level tps limit limitTarget := url.ServiceKey() // method-level tps limit if len(methodIntervalConfig) > 0 || len(methodLimitRateConfig) > 0 { + // it means that if the method-level rate limit exist, we will use method-level rate limit strategy limitTarget = limitTarget + "#" + invocation.MethodName() } + // looking up the limiter from 'cache' limitState, found := limiter.tpsState.Load(limitTarget) if found { + // the limiter has been cached, we return its result return limitState.(filter.TpsLimitStrategy).IsAllowable() } + // we could not find the limiter, and try to create one. + limitRate := getLimitConfig(methodLimitRateConfig, url, invocation, constant.TPS_LIMIT_RATE_KEY, constant.DEFAULT_TPS_LIMIT_RATE) if limitRate < 0 { + // the limitTarget is not necessary to be limited. return true } @@ -150,13 +162,20 @@ func (limiter MethodServiceTpsLimiterImpl) IsAllowable(url common.URL, invocatio panic(fmt.Sprintf("The interval must be positive, please check your configuration! url: %s", url.String())) } + // find the strategy config and then create one limitStrategyConfig := url.GetParam(methodConfigPrefix+constant.TPS_LIMIT_STRATEGY_KEY, url.GetParam(constant.TPS_LIMIT_STRATEGY_KEY, constant.DEFAULT_KEY)) limitStateCreator := extension.GetTpsLimitStrategyCreator(limitStrategyConfig) + + // we using loadOrStore to ensure thread-safe limitState, _ = limiter.tpsState.LoadOrStore(limitTarget, limitStateCreator.Create(int(limitRate), int(limitInterval))) + return limitState.(filter.TpsLimitStrategy).IsAllowable() } +// getLimitConfig will try to fetch the configuration from url. +// If we can convert the methodLevelConfig to int64, return; +// Or, we will try to look up server-level configuration and then convert it to int64 func getLimitConfig(methodLevelConfig string, url common.URL, invocation protocol.Invocation, @@ -172,6 +191,8 @@ func getLimitConfig(methodLevelConfig string, return result } + // actually there is no method-level configuration, so we use the service-level configuration + result, err := strconv.ParseInt(url.GetParam(configKey, defaultVal), 0, 0) if err != nil { @@ -183,7 +204,7 @@ func getLimitConfig(methodLevelConfig string, var methodServiceTpsLimiterInstance *MethodServiceTpsLimiterImpl var methodServiceTpsLimiterOnce sync.Once -// GetMethodServiceTpsLimiter ... +// GetMethodServiceTpsLimiter will return an MethodServiceTpsLimiterImpl instance. func GetMethodServiceTpsLimiter() filter.TpsLimiter { methodServiceTpsLimiterOnce.Do(func() { methodServiceTpsLimiterInstance = &MethodServiceTpsLimiterImpl{ diff --git a/filter/filter_impl/tps/tps_limiter_mock.go b/filter/filter_impl/tps/tps_limiter_mock.go index 131a2e5121..b49084f28e 100644 --- a/filter/filter_impl/tps/tps_limiter_mock.go +++ b/filter/filter_impl/tps/tps_limiter_mock.go @@ -15,22 +15,6 @@ * limitations under the License. */ -// 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. -// - // Code generated by MockGen. DO NOT EDIT. // Source: tps_limiter.go diff --git a/filter/handler/rejected_execution_handler_mock.go b/filter/handler/rejected_execution_handler_mock.go index 469c06b6b9..bff54769cb 100644 --- a/filter/handler/rejected_execution_handler_mock.go +++ b/filter/handler/rejected_execution_handler_mock.go @@ -15,22 +15,6 @@ * limitations under the License. */ -// 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. -// - // Code generated by MockGen. DO NOT EDIT. // Source: rejected_execution_handler.go diff --git a/filter/handler/rejected_execution_handler_only_log.go b/filter/handler/rejected_execution_handler_only_log.go index 0f9003c7df..fe9cf4869f 100644 --- a/filter/handler/rejected_execution_handler_only_log.go +++ b/filter/handler/rejected_execution_handler_only_log.go @@ -36,6 +36,7 @@ const ( ) func init() { + // this implementation is the the default implementation of RejectedExecutionHandler extension.SetRejectedExecutionHandler(HandlerName, GetOnlyLogRejectedExecutionHandler) extension.SetRejectedExecutionHandler(constant.DEFAULT_KEY, GetOnlyLogRejectedExecutionHandler) } @@ -56,11 +57,12 @@ var onlyLogHandlerOnce sync.Once * tps.limit.rejected.handler: "default" or "log" * methods: * - name: "GetUser" + * OnlyLogRejectedExecutionHandler is designed to be singleton */ type OnlyLogRejectedExecutionHandler struct { } -// RejectedExecution ... +// RejectedExecution will do nothing, it only log the invocation. func (handler *OnlyLogRejectedExecutionHandler) RejectedExecution(url common.URL, _ protocol.Invocation) protocol.Result { @@ -68,7 +70,7 @@ func (handler *OnlyLogRejectedExecutionHandler) RejectedExecution(url common.URL return &protocol.RPCResult{} } -// GetOnlyLogRejectedExecutionHandler ... +// GetOnlyLogRejectedExecutionHandler will return the instance of OnlyLogRejectedExecutionHandler func GetOnlyLogRejectedExecutionHandler() filter.RejectedExecutionHandler { onlyLogHandlerOnce.Do(func() { onlyLogHandlerInstance = &OnlyLogRejectedExecutionHandler{} diff --git a/filter/rejected_execution_handler.go b/filter/rejected_execution_handler.go index caeea1db66..d02481b98d 100644 --- a/filter/rejected_execution_handler.go +++ b/filter/rejected_execution_handler.go @@ -31,5 +31,7 @@ import ( * In such situation, implement this interface and register it by invoking extension.SetRejectedExecutionHandler. */ type RejectedExecutionHandler interface { + + // RejectedExecution will be called if the invocation was rejected by some component. RejectedExecution(url common.URL, invocation protocol.Invocation) protocol.Result } diff --git a/filter/tps_limit_strategy.go b/filter/tps_limit_strategy.go index 5edf32ce19..e194f1da06 100644 --- a/filter/tps_limit_strategy.go +++ b/filter/tps_limit_strategy.go @@ -33,10 +33,16 @@ package filter * tps.limit.strategy: "name of implementation" # method-level */ type TpsLimitStrategy interface { + // IsAllowable will return true if this invocation is not over limitation IsAllowable() bool } -// TpsLimitStrategyCreator ... +// TpsLimitStrategyCreator, the creator abstraction for TpsLimitStrategy type TpsLimitStrategyCreator interface { - Create(rate int, interval int) TpsLimitStrategy + // Create will create an instance of TpsLimitStrategy + // It will be a little hard to understand this method. + // The unit of interval is ms + // for example, if the limit = 100, interval = 1000 + // which means that the tps limitation is 100 times per 1000ms (100/1000ms) + Create(limit int, interval int) TpsLimitStrategy } diff --git a/filter/tps_limiter.go b/filter/tps_limiter.go index dbc9f76838..531eb09823 100644 --- a/filter/tps_limiter.go +++ b/filter/tps_limiter.go @@ -34,5 +34,6 @@ import ( * tps.limiter: "the name of limiter", */ type TpsLimiter interface { + // IsAllowable will check whether this invocation should be enabled for further process IsAllowable(common.URL, protocol.Invocation) bool } diff --git a/go.mod b/go.mod index 83091cf8b9..ba3cf3e219 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,18 @@ 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/alibaba-cloud-sdk-go v0.0.0-20190802083043-4cd0c391755e // indirect - github.com/apache/dubbo-go-hessian2 v1.4.0 - github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 // indirect + github.com/apache/dubbo-go-hessian2 v1.5.0 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 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f // indirect github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect github.com/creasty/defaults v1.3.0 - github.com/dubbogo/getty v1.3.3 + github.com/dubbogo/getty v1.3.5 github.com/dubbogo/go-zookeeper v1.0.0 - github.com/dubbogo/gost v1.5.2 + github.com/dubbogo/gost v1.9.0 github.com/emicklei/go-restful/v3 v3.0.0 - github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect - github.com/go-errors/errors v1.0.1 // indirect + 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 @@ -29,32 +26,28 @@ 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/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect github.com/jinzhu/copier v0.0.0-20190625015134-976e0346caa8 - github.com/jonboulle/clockwork v0.1.0 // indirect - github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 // indirect - github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f // indirect - github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect + 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.0.0-20190723125407-0242d42e3dbb + github.com/nacos-group/nacos-sdk-go v0.3.1 github.com/opentracing/opentracing-go v1.1.0 - github.com/pkg/errors v0.8.1 + github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.1.0 github.com/satori/go.uuid v1.2.0 github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 // indirect github.com/soheilhy/cmux v0.1.4 // indirect github.com/stretchr/testify v1.5.1 - github.com/tebeka/strftime v0.1.3 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect - github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect github.com/zouyx/agollo v0.0.0-20191114083447-dde9fc9f35b8 go.etcd.io/bbolt v1.3.3 // indirect go.etcd.io/etcd v3.3.13+incompatible go.uber.org/atomic v1.4.0 go.uber.org/zap v1.10.0 + golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect google.golang.org/grpc v1.22.1 gopkg.in/yaml.v2 v2.2.2 k8s.io/api v0.0.0-20190325185214-7544f9db76f6 diff --git a/go.sum b/go.sum index 813496b6ee..eb84bde1fb 100644 --- a/go.sum +++ b/go.sum @@ -35,11 +35,10 @@ 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 h1:MSuLXx/mveDbpDNhVrcWTMeV4lbYWKcyO4rH+jAxmX0= -github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190802083043-4cd0c391755e/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ= -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.4.0 h1:Cb9FQVTy3G93dnDr7P93U8DeKFYpDTJjQp44JG5TafA= -github.com/apache/dubbo-go-hessian2 v1.4.0/go.mod h1:VwEnsOMidkM1usya2uPfGpSLO9XUF//WQcWn3y+jFz8= +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/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/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -51,7 +50,6 @@ 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= @@ -106,13 +104,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.3 h1:8m4zZBqFHO+NmhH7rMPlFuuYRVjcPD7cUhumevqMZZs= -github.com/dubbogo/getty v1.3.3/go.mod h1:U92BDyJ6sW9Jpohr2Vlz8w2uUbIbNZ3d+6rJvFTSPp0= +github.com/dubbogo/getty v1.3.5 h1:xJxdDj9jm7wlrRSsVZSk2TDNxJbbac5GpxV0QpjO+Tw= +github.com/dubbogo/getty v1.3.5/go.mod h1:T55vN8Q6tZjf2AQZiGmkujneD3LfqYbv2b3QjacwYOY= 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/gost v1.5.1/go.mod h1:pPTjVyoJan3aPxBPNUX0ADkXjPibLo+/Ib0/fADXSG8= -github.com/dubbogo/gost v1.5.2 h1:ri/03971hdpnn3QeCU+4UZgnRNGDXLDGDucR/iozZm8= -github.com/dubbogo/gost v1.5.2/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= github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74/go.mod h1:UqXY1lYT/ERa4OEAywUqdok1T4RCRdArkhic1Opuavo= github.com/elazarl/go-bindata-assetfs v0.0.0-20160803192304-e1a2a7ec64b0 h1:ZoRgc53qJCfSLimXqJDrmBhnt5GChDsExMCK7t48o0Y= @@ -136,6 +134,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= @@ -151,6 +151,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= @@ -322,6 +323,12 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/errors v0.0.0-20190930114154-d42613fe1ab9 h1:hJix6idebFclqlfZCHE7EUX7uqLCyb70nHNHH1XKGBg= +github.com/juju/errors v0.0.0-20190930114154-d42613fe1ab9/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b h1:Rrp0ByJXEjhREMPGTt3aWYjoIsUGCbt21ekbeJcTWv0= +github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/keybase/go-crypto v0.0.0-20180614160407-5114a9a81e1b h1:VE6r2OwP5gj+Z9aCkSKl3MlmnZbfMAjhvR5T7abKHEo= github.com/keybase/go-crypto v0.0.0-20180614160407-5114a9a81e1b/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= @@ -381,8 +388,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nacos-group/nacos-sdk-go v0.0.0-20190723125407-0242d42e3dbb h1:lbmvw8r9W55w+aQgWn35W1nuleRIECMoqUrmwAOAvoI= -github.com/nacos-group/nacos-sdk-go v0.0.0-20190723125407-0242d42e3dbb/go.mod h1:CEkSvEpoveoYjA81m4HNeYQ0sge0LFGKSEqO3JKHllo= +github.com/nacos-group/nacos-sdk-go v0.3.1 h1:MI7bNDAN5m9UFcRRUTSPfJi4dCQo+TYG85qVB1rCHeg= +github.com/nacos-group/nacos-sdk-go v0.3.1/go.mod h1:ESKb6yF0gxSc8GuS+0jaMBe+n8rJ5/k4ya6LyFG2xi8= 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= @@ -390,10 +397,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= @@ -416,6 +427,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -525,6 +538,8 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/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= @@ -533,6 +548,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= diff --git a/metadata/definition/definition.go b/metadata/definition/definition.go new file mode 100644 index 0000000000..11e137a14b --- /dev/null +++ b/metadata/definition/definition.go @@ -0,0 +1,127 @@ +/* + * 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 ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" +) + +// ServiceDefinition 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 + Methods []MethodDefinition + Types []TypeDefinition +} + +func (def ServiceDefinition) ToBytes() ([]byte, error) { + return json.Marshal(def) +} + +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 + ReturnType string + Parameters []TypeDefinition +} + +// TypeDefinition is the describer of type definition +type TypeDefinition struct { + Id string + Type string + Items []TypeDefinition + Enums []string + 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 + for _, t := range m.ArgsType() { + paramTypes = append(paramTypes, t.Kind().String()) + } + methodD := MethodDefinition{ + Name: k, + ParameterTypes: paramTypes, + ReturnType: m.ReplyType().Kind().String(), + } + 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 0000000000..958f9324d0 --- /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 0000000000..ca9e125a74 --- /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 new file mode 100644 index 0000000000..64290c668f --- /dev/null +++ b/metadata/identifier/base_metadata_identifier.go @@ -0,0 +1,94 @@ +/* + * 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 ( + "encoding/base64" +) + +import ( + "github.com/apache/dubbo-go/common/constant" +) + +// BaseMetadataIdentifier defined for describe the Metadata base identify +type IMetadataIdentifier interface { + GetFilePathKey() string + GetIdentifierKey() string +} + +// BaseMetadataIdentifier is the base implement of BaseMetadataIdentifier interface +type BaseMetadataIdentifier struct { + ServiceInterface string + Version string + Group string + Side 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 { + joinedStr += joinChar + joinedStr += param + } + return joinedStr +} + +// getIdentifierKey will return string format as service:Version:Group:Side:param1:param2... +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 will return string format as metadata/path/Version/Group/Side/param1/param2... +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) + + joinParams(constant.PATH_SEPARATOR, params) + +} + +// serviceToPath... +func serviceToPath(serviceInterface string) string { + if serviceInterface == constant.ANY_VALUE { + return "" + } else { + decoded, err := base64.URLEncoding.DecodeString(serviceInterface) + if err != nil { + return "" + } + return string(decoded) + } + +} + +//withPathSeparator... +func withPathSeparator(path string) string { + if len(path) != 0 { + path = constant.PATH_SEPARATOR + path + } + return path +} diff --git a/metadata/identifier/base_metadata_identifier_test.go b/metadata/identifier/base_metadata_identifier_test.go new file mode 100644 index 0000000000..5b60992ab6 --- /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 new file mode 100644 index 0000000000..18b330ae08 --- /dev/null +++ b/metadata/identifier/metadata_identifier.go @@ -0,0 +1,34 @@ +/* + * 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 + +// MetadataIdentifier is inherit baseMetaIdentifier with Application name +type MetadataIdentifier struct { + Application string + BaseMetadataIdentifier +} + +// GetIdentifierKey will return string format as service:Version:Group:Side:Application +func (mdi *MetadataIdentifier) GetIdentifierKey() string { + return mdi.BaseMetadataIdentifier.getIdentifierKey(mdi.Application) +} + +// GetFilePathKey will return string format as metadata/path/Version/Group/Side/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 0000000000..cba3c0dd76 --- /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 new file mode 100644 index 0000000000..7cdb55e53d --- /dev/null +++ b/metadata/identifier/service_metadata_identifier.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 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 + BaseMetadataIdentifier +} + +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 will return string format as service:Version:Group:Side:Protocol:"revision"+Revision +func (mdi *ServiceMetadataIdentifier) GetIdentifierKey() string { + return mdi.BaseMetadataIdentifier.getIdentifierKey(mdi.Protocol, constant.KEY_REVISON_PREFIX+mdi.Revision) +} + +// GetFilePathKey will return string format as metadata/path/Version/Group/Side/Protocol/"revision"+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 0000000000..d7ef44a4bb --- /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 new file mode 100644 index 0000000000..fa35ab79d6 --- /dev/null +++ b/metadata/identifier/subscribe_metadata_identifier.go @@ -0,0 +1,34 @@ +/* + * 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 + +// SubscriberMetadataIdentifier is inherit baseMetaIdentifier with service params: Revision +type SubscriberMetadataIdentifier struct { + Revision string + MetadataIdentifier +} + +// GetIdentifierKey will return string format as service:Version:Group:Side:Revision +func (mdi *SubscriberMetadataIdentifier) GetIdentifierKey() string { + return mdi.BaseMetadataIdentifier.getIdentifierKey(mdi.Revision) +} + +// GetFilePathKey will return string format as metadata/path/Version/Group/Side/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 0000000000..215aa3c569 --- /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/mapping/dynamic/service_name_mapping.go b/metadata/mapping/dynamic/service_name_mapping.go new file mode 100644 index 0000000000..4cfac8f828 --- /dev/null +++ b/metadata/mapping/dynamic/service_name_mapping.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 dynamic + +import ( + "strconv" + "time" +) + +import ( + "github.com/dubbogo/gost/container/set" + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/config_center" + "github.com/apache/dubbo-go/metadata/mapping" +) + +const ( + defaultGroup = config_center.DEFAULT_GROUP + slash = "/" +) + +// DynamicConfigurationServiceNameMapping is the implementation based on config center +type DynamicConfigurationServiceNameMapping struct { + dc config_center.DynamicConfiguration +} + +// Map will map the service to this application-level service +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") + } + + appName := config.GetApplicationConfig().Name + value := time.Now().UnixNano() + + err := d.dc.PublishConfig(appName, + d.buildGroup(serviceInterface), + strconv.FormatInt(value, 10)) + if err != nil { + return perrors.WithStack(err) + } + return nil +} + +// Get will return the application-level services. If not found, the empty set will be returned. +// if the dynamic configuration got error, the error will return +func (d *DynamicConfigurationServiceNameMapping) Get(serviceInterface string, group string, version string, protocol string) (*gxset.HashSet, error) { + return d.dc.GetConfigKeysByGroup(d.buildGroup(serviceInterface)) +} + +// buildGroup will return group, now it looks like defaultGroup/serviceInterface +func (d *DynamicConfigurationServiceNameMapping) buildGroup(serviceInterface string) string { + // the issue : https://github.com/apache/dubbo/issues/4671 + // so other params are ignored and remove, including group string, version string, protocol string + return defaultGroup + slash + serviceInterface +} + +// NewServiceNameMapping will create an instance of DynamicConfigurationServiceNameMapping +func NewServiceNameMapping(dc config_center.DynamicConfiguration) mapping.ServiceNameMapping { + return &DynamicConfigurationServiceNameMapping{dc: dc} +} diff --git a/metadata/mapping/dynamic/service_name_mapping_test.go b/metadata/mapping/dynamic/service_name_mapping_test.go new file mode 100644 index 0000000000..e3d620cd73 --- /dev/null +++ b/metadata/mapping/dynamic/service_name_mapping_test.go @@ -0,0 +1,61 @@ +/* + * 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 dynamic + +import ( + "testing" +) + +import ( + gxset "github.com/dubbogo/gost/container/set" + "github.com/stretchr/testify/assert" +) + +import ( + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/config_center" +) + +func TestDynamicConfigurationServiceNameMapping(t *testing.T) { + + // mock data + appName := "myApp" + dc, err := (&config_center.MockDynamicConfigurationFactory{ + Content: appName, + }).GetDynamicConfiguration(nil) + config.GetApplicationConfig().Name = appName + + mapping := NewServiceNameMapping(dc) + intf := constant.METADATA_SERVICE_NAME + group := "myGroup" + version := "myVersion" + protocol := "myProtocol" + + err = mapping.Map(intf, group, version, protocol) + assert.NotNil(t, err) + intf = "MyService" + err = mapping.Map(intf, group, version, protocol) + assert.Nil(t, err) + + var result *gxset.HashSet + result, err = mapping.Get(intf, group, version, protocol) + assert.Nil(t, err) + assert.Equal(t, 1, result.Size()) + assert.True(t, result.Contains(appName)) +} diff --git a/metadata/mapping/memory/service_name_mapping.go b/metadata/mapping/memory/service_name_mapping.go new file mode 100644 index 0000000000..8a891491bd --- /dev/null +++ b/metadata/mapping/memory/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 memory + +import ( + gxset "github.com/dubbogo/gost/container/set" +) + +import ( + "github.com/apache/dubbo-go/config" +) + +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 +} diff --git a/metadata/mapping/service_name_mapping.go b/metadata/mapping/service_name_mapping.go new file mode 100644 index 0000000000..6caed9f0b4 --- /dev/null +++ b/metadata/mapping/service_name_mapping.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 mapping + +import ( + gxset "github.com/dubbogo/gost/container/set" +) + +// ServiceNameMapping try to build the mapping between application-level service and interface-level service. +type ServiceNameMapping interface { + + // Map will map the service to this application-level service + Map(serviceInterface string, group string, version string, protocol string) error + + // Get will return the application-level services + Get(serviceInterface string, group string, version string, protocol string) (*gxset.HashSet, error) +} diff --git a/metadata/report/delegate/delegate_report.go b/metadata/report/delegate/delegate_report.go new file mode 100644 index 0000000000..4e3995d2ea --- /dev/null +++ b/metadata/report/delegate/delegate_report.go @@ -0,0 +1,275 @@ +/* + * 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" + "sync" + "time" +) + +import ( + "github.com/go-co-op/gocron" + "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.Info("start to publish all metadata.") + 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 (bmr *MetadataReport) retry() bool { + bmr.failedReportsLock.RLock() + defer bmr.failedReportsLock.RUnlock() + return bmr.doHandlerMetadataCollection(bmr.failedReports) +} + +// StoreProviderMetadata will delegate to call remote metadata's sdk to store provider service definition +func (bmr *MetadataReport) StoreProviderMetadata(identifier *identifier.MetadataIdentifier, definer definition.ServiceDefiner) { + if bmr.syncReport { + bmr.storeMetadataTask(common.PROVIDER, identifier, definer) + } + go bmr.storeMetadataTask(common.PROVIDER, identifier, definer) +} + +// storeMetadataTask will delegate to call remote metadata's sdk to store +func (bmr *MetadataReport) storeMetadataTask(role int, identifier *identifier.MetadataIdentifier, definer interface{}) { + logger.Infof("store provider metadata. Identifier :%v ; definition: %v .", identifier, definer) + bmr.allMetadataReportsLock.Lock() + bmr.allMetadataReports[identifier] = definer + bmr.allMetadataReportsLock.Unlock() + + bmr.failedReportsLock.Lock() + delete(bmr.failedReports, identifier) + bmr.failedReportsLock.Unlock() + // data is store the json marshaled definition + var ( + data []byte + err error + ) + + defer func() { + if r := recover(); r != nil { + bmr.failedReportsLock.Lock() + bmr.failedReports[identifier] = definer + bmr.failedReportsLock.Unlock() + bmr.metadataReportRetry.startRetryTask() + logger.Errorf("Failed to put provider metadata %v in %v, cause: %v", identifier, string(data), r) + } + }() + + 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 (bmr *MetadataReport) StoreConsumerMetadata(identifier *identifier.MetadataIdentifier, definer map[string]string) { + if bmr.syncReport { + bmr.storeMetadataTask(common.CONSUMER, identifier, definer) + } + go bmr.storeMetadataTask(common.CONSUMER, identifier, definer) +} + +// SaveServiceMetadata will delegate to call remote metadata's sdk to save service metadata +func (bmr *MetadataReport) SaveServiceMetadata(identifier *identifier.ServiceMetadataIdentifier, url common.URL) error { + report := instance.GetMetadataReportInstance() + if bmr.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 (bmr *MetadataReport) RemoveServiceMetadata(identifier *identifier.ServiceMetadataIdentifier) error { + report := instance.GetMetadataReportInstance() + if bmr.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 (bmr *MetadataReport) GetExportedURLs(identifier *identifier.ServiceMetadataIdentifier) []string { + report := instance.GetMetadataReportInstance() + return report.GetExportedURLs(identifier) +} + +// SaveSubscribedData will delegate to call remote metadata's sdk to save subscribed data +func (bmr *MetadataReport) SaveSubscribedData(identifier *identifier.SubscriberMetadataIdentifier, urls []common.URL) error { + report := instance.GetMetadataReportInstance() + if bmr.syncReport { + return report.SaveSubscribedData(identifier, urls) + } + go report.SaveSubscribedData(identifier, urls) + return nil +} + +// GetSubscribedURLs will delegate to call remote metadata's sdk to get subscribed urls +func (MetadataReport) GetSubscribedURLs(identifier *identifier.SubscriberMetadataIdentifier) []string { + 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 { + report := instance.GetMetadataReportInstance() + return report.GetServiceDefinition(identifier) +} + +// doHandlerMetadataCollection will store metadata to metadata support with given metadataMap +func (bmr *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 { + bmr.StoreProviderMetadata(e, metadataMap[e].(*definition.FullServiceDefinition)) + } else if common.RoleType(common.CONSUMER).Role() == e.Side { + bmr.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 0000000000..0e8da60700 --- /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" + retryPeroidKey := "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, retryPeroidKey, 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/factory/report_factory.go b/metadata/report/factory/report_factory.go new file mode 100644 index 0000000000..8769ebdd2f --- /dev/null +++ b/metadata/report/factory/report_factory.go @@ -0,0 +1,35 @@ +/* + * 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 factory + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/metadata/report" +) + +var ( + MetadataReportInstance report.MetadataReport +) + +// MetadataReportFactory interface will create metadata report +type MetadataReportFactory interface { + 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 0000000000..4333d52ac1 --- /dev/null +++ b/metadata/report/nacos/report.go @@ -0,0 +1,171 @@ +/* + * 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" + "net/url" + + "github.com/nacos-group/nacos-sdk-go/clients/config_client" + "github.com/nacos-group/nacos-sdk-go/vo" + perrors "github.com/pkg/errors" + + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/metadata/identifier" +) + +// nacosMetadataReport is the implementation of MetadataReport based Nacos +type nacosMetadataReport struct { + client config_client.IConfigClient +} + +// StoreProviderMetadata will store 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 will store 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 will store 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 will remove the service metadata +func (n *nacosMetadataReport) RemoveServiceMetadata(metadataIdentifier *identifier.ServiceMetadataIdentifier) error { + return n.deleteMetadata(vo.ConfigParam{ + DataId: metadataIdentifier.GetIdentifierKey(), + Group: metadataIdentifier.Group, + }) +} + +// GetExportedURLs will look up the exported urls. +// if not found, an empty list will be returned. +func (n *nacosMetadataReport) GetExportedURLs(metadataIdentifier *identifier.ServiceMetadataIdentifier) []string { + return n.getConfigAsArray(vo.ConfigParam{ + DataId: metadataIdentifier.GetIdentifierKey(), + Group: metadataIdentifier.Group, + }) +} + +// SaveSubscribedData will convert the urlList to json array and then store it +func (n *nacosMetadataReport) SaveSubscribedData(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier, urlList []common.URL) error { + if len(urlList) == 0 { + logger.Warnf("The url list is empty") + return nil + } + urlStrList := make([]string, 0, len(urlList)) + + for _, e := range urlList { + urlStrList = append(urlStrList, e.String()) + } + + bytes, err := json.Marshal(urlStrList) + + if err != nil { + return perrors.WithMessage(err, "Could not convert the array to json") + } + return n.storeMetadata(vo.ConfigParam{ + DataId: subscriberMetadataIdentifier.GetIdentifierKey(), + Group: subscriberMetadataIdentifier.Group, + Content: string(bytes), + }) +} + +// GetSubscribedURLs will lookup the url +// if not found, an empty list will be returned +func (n *nacosMetadataReport) GetSubscribedURLs(subscriberMetadataIdentifier *identifier.SubscriberMetadataIdentifier) []string { + return n.getConfigAsArray(vo.ConfigParam{ + DataId: subscriberMetadataIdentifier.GetIdentifierKey(), + Group: subscriberMetadataIdentifier.Group, + }) +} + +// GetServiceDefinition will lookup the service definition +func (n *nacosMetadataReport) GetServiceDefinition(metadataIdentifier *identifier.MetadataIdentifier) string { + 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 { + cfg := n.getConfig(param) + res := make([]string, 0, 1) + if len(cfg) == 0 { + return res + } + decodeCfg, err := url.QueryUnescape(cfg) + if err != nil { + logger.Errorf("The config is invalid: %s", cfg) + } + res = append(res, decodeCfg) + return res +} + +// getConfig will read the config +func (n *nacosMetadataReport) getConfig(param vo.ConfigParam) string { + cfg, err := n.client.GetConfig(param) + if err != nil { + logger.Errorf("Finding the configuration failed: %v", param) + } + return cfg +} diff --git a/metadata/report/report.go b/metadata/report/report.go new file mode 100644 index 0000000000..61cdda1f96 --- /dev/null +++ b/metadata/report/report.go @@ -0,0 +1,35 @@ +/* + * 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(*identifier.MetadataIdentifier, string) error + StoreConsumerMetadata(*identifier.MetadataIdentifier, string) error + SaveServiceMetadata(*identifier.ServiceMetadataIdentifier, common.URL) error + RemoveServiceMetadata(*identifier.ServiceMetadataIdentifier) error + GetExportedURLs(*identifier.ServiceMetadataIdentifier) []string + SaveSubscribedData(*identifier.SubscriberMetadataIdentifier, []common.URL) error + GetSubscribedURLs(*identifier.SubscriberMetadataIdentifier) []string + GetServiceDefinition(*identifier.MetadataIdentifier) string +} diff --git a/metadata/service/exporter/configurable/exporter.go b/metadata/service/exporter/configurable/exporter.go new file mode 100644 index 0000000000..ec3f8ec2d0 --- /dev/null +++ b/metadata/service/exporter/configurable/exporter.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 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 + serviceConfig.Group = config.GetApplicationConfig().Name + 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 0000000000..220ef71dac --- /dev/null +++ b/metadata/service/exporter/configurable/exporter_test.go @@ -0,0 +1,117 @@ +/* + * 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{ + 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/service/exporter/exporter.go b/metadata/service/exporter/exporter.go new file mode 100644 index 0000000000..cfdef3a0e7 --- /dev/null +++ b/metadata/service/exporter/exporter.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 exporter + +import ( + "github.com/apache/dubbo-go/common" +) + +// 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/service.go b/metadata/service/inmemory/service.go new file mode 100644 index 0000000000..4b6f4330a1 --- /dev/null +++ b/metadata/service/inmemory/service.go @@ -0,0 +1,234 @@ +/* + * 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 ( + "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/logger" + "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" + +// 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 +} + +// NewMetadataService: initiate a metadata service +func NewMetadataService() *MetadataService { + return &MetadataService{ + exportedServiceURLs: &sync.Map{}, + subscribedServiceURLs: &sync.Map{}, + serviceDefinitions: &sync.Map{}, + lock: &sync.RWMutex{}, + } +} + +// 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) *skip.SkipList { + skipList := skip.New(uint64(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.SIMPLE_METADATA_SERVICE_NAME { + skipList.Insert(Comparator(url)) + } + } + return true + }) + return skipList +} + +// getSpecifiedService can return specified service url by serviceKey +func (mts *MetadataService) getSpecifiedService(services *sync.Map, serviceKey string, protocol string) *skip.SkipList { + skipList := skip.New(uint64(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 || url.Protocol == protocol || url.GetParam(constant.PROTOCOL_KEY, "") == protocol { + skipList.Insert(Comparator(url)) + } + } + } + return skipList +} + +// 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 { + //judge is consumer or provider + //side := url.GetParam(constant.SIDE_KEY, "") + //var service common.RPCService + service := common.ServiceMap.GetService(url.Protocol, url.GetParam(constant.BEAN_NAME_KEY, url.Service())) + //if side == common.RoleType(common.CONSUMER).Role() { + // //TODO:generate the service definition and store it + // + //} else if side == common.RoleType(common.PROVIDER).Role() { + // //TODO:generate the service definition and store it + //} + sd := definition.BuildServiceDefinition(*service, url) + data, err := sd.ToBytes() + if err != nil { + logger.Errorf("publishProvider getServiceDescriptor error. providerUrl:%v , error:%v ", url, err) + } + 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) (*skip.SkipList, error) { + if serviceInterface == constant.ANY_VALUE { + return mts.getAllService(mts.exportedServiceURLs), nil + } else { + serviceKey := definition.ServiceDescriperBuild(serviceInterface, group, version) + return mts.getSpecifiedService(mts.exportedServiceURLs, serviceKey, protocol), nil + } +} + +// GetSubscribedURLs get all subscribedUrl +func (mts *MetadataService) GetSubscribedURLs() (*skip.SkipList, 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 { + return true +} + +// Version will return the version of metadata service +func (mts *MetadataService) Version() string { + return version +} diff --git a/metadata/service/inmemory/service_test.go b/metadata/service/inmemory/service_test.go new file mode 100644 index 0000000000..fc0410ecca --- /dev/null +++ b/metadata/service/inmemory/service_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 ( + "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, uint64(3), list.Len()) + iter := list.IterAtPosition(0) + for iter.Next() { + comparator := iter.Value() + fmt.Println(comparator) + } + mts.SubscribeURL(u) + + mts.SubscribeURL(u) + list2, _ := mts.GetSubscribedURLs() + assert.Equal(t, uint64(1), list2.Len()) + + mts.UnexportURL(u) + list3, _ := mts.GetExportedURLs(serviceName, group, version, protocol) + assert.Equal(t, uint64(2), list3.Len()) + + mts.UnsubscribeURL(u) + list4, _ := mts.GetSubscribedURLs() + assert.Equal(t, uint64(0), list4.Len()) + + 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/service.go b/metadata/service/remote/service.go new file mode 100644 index 0000000000..f4587638ef --- /dev/null +++ b/metadata/service/remote/service.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 remote + +import ( + "github.com/Workiva/go-datastructures/slice/skip" + "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" + "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" + +// MetadataService is a implement of metadata service which will delegate the remote metadata report +type MetadataService struct { + service.BaseMetadataService + inMemoryMetadataService *inmemory.MetadataService + exportedRevision atomic.String + subscribedRevision atomic.String + delegateReport *delegate.MetadataReport +} + +// NewMetadataService will create a new remote MetadataService instance +func NewMetadataService() (*MetadataService, error) { + mr, err := delegate.NewMetadataReport() + if err != nil { + return nil, err + } + return &MetadataService{ + inMemoryMetadataService: inmemory.NewMetadataService(), + delegateReport: mr, + }, nil +} + +// 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 true, nil +} + +// UnexportURL +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 (MetadataService) SubscribeURL(url common.URL) (bool, error) { + return true, nil +} + +// UnsubscribeURL will be implemented by in memory service +func (MetadataService) UnsubscribeURL(url common.URL) error { + return nil +} + +// 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 { + service := common.ServiceMap.GetService(url.Protocol, url.GetParam(constant.BEAN_NAME_KEY, url.Service())) + sd := definition.BuildServiceDefinition(*service, url) + id := &identifier.MetadataIdentifier{ + BaseMetadataIdentifier: identifier.BaseMetadataIdentifier{ + ServiceInterface: interfaceName, + Version: url.GetParam(constant.VERSION_KEY, ""), + Group: url.GetParam(constant.GROUP_KEY, ""), + }, + } + mts.delegateReport.StoreProviderMetadata(id, sd) + } + logger.Errorf("publishProvider interfaceName is empty . providerUrl:%v ", url) + return nil +} + +// GetExportedURLs will be implemented by in memory service +func (MetadataService) GetExportedURLs(serviceInterface string, group string, version string, protocol string) (*skip.SkipList, error) { + return nil, nil +} + +// GetSubscribedURLs will be implemented by in memory service +func (MetadataService) GetSubscribedURLs() (*skip.SkipList, error) { + return nil, nil +} + +// GetServiceDefinition will be implemented by in memory service +func (MetadataService) GetServiceDefinition(interfaceName string, group string, version string) (string, error) { + return "", nil +} + +// GetServiceDefinitionByServiceKey will be implemented by in memory service +func (MetadataService) GetServiceDefinitionByServiceKey(serviceKey string) (string, error) { + return "", nil +} + +// 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 { + result := true + 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) + result = false + } + iterator := urls.Iter(inmemory.Comparator{}) + logger.Infof("urls length = %v", urls.Len()) + for { + if !iterator.Next() { + break + } + url := iterator.Value().(inmemory.Comparator) + id := identifier.NewServiceMetadataIdentifier(common.URL(url)) + id.Revision = mts.exportedRevision.Load() + if err := mts.delegateReport.SaveServiceMetadata(id, common.URL(url)); err != nil { + logger.Errorf("Error occur when execute remote.MetadataService.RefreshMetadata, error message is %v", err) + result = false + } + } + } + + 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) + result = false + } + if urls != nil && urls.Len() > 0 { + id := &identifier.SubscriberMetadataIdentifier{ + MetadataIdentifier: identifier.MetadataIdentifier{ + Application: config.GetApplicationConfig().Name, + }, + Revision: subscribedRevision, + } + if err := mts.delegateReport.SaveSubscribedData(id, convertUrls(urls)); err != nil { + logger.Errorf("Error occur when execute remote.MetadataService.RefreshMetadata, error message is %v", err) + result = false + } + } + } + return result +} + +// Version will return the remote service version +func (MetadataService) Version() string { + return version +} + +// convertUrls will convert the skip list to slice +func convertUrls(list *skip.SkipList) []common.URL { + urls := make([]common.URL, list.Len()) + iterator := list.Iter(inmemory.Comparator{}) + for { + if iterator.Value() == nil { + break + } + url := iterator.Value().(inmemory.Comparator) + urls = append(urls, common.URL(url)) + if !iterator.Next() { + break + } + } + return urls +} diff --git a/metadata/service/remote/service_test.go b/metadata/service/remote/service_test.go new file mode 100644 index 0000000000..308c631e41 --- /dev/null +++ b/metadata/service/remote/service_test.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 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) +var subscribedMetadata = make(map[*identifier.SubscriberMetadataIdentifier][]common.URL, 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 { + return nil +} + +func (mr *metadataReport) SaveSubscribedData(id *identifier.SubscriberMetadataIdentifier, urls []common.URL) error { + logger.Infof("SaveSubscribedData, , url is %v", urls) + subscribedMetadata[id] = urls + return nil +} + +func (metadataReport) GetSubscribedURLs(*identifier.SubscriberMetadataIdentifier) []string { + return nil +} + +func (metadataReport) GetServiceDefinition(*identifier.MetadataIdentifier) string { + return "" +} + +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.setInMemoryMetadataService(mockInmemoryProc(t)) + mts.RefreshMetadata("0.0.1", "0.0.1") + assert.Equal(t, 1, len(serviceMetadata)) + assert.Equal(t, 1, len(subscribedMetadata)) +} + +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" + + 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) + + mts.SubscribeURL(u) + + 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) + return mts +} diff --git a/metadata/service/service.go b/metadata/service/service.go new file mode 100644 index 0000000000..13464087ed --- /dev/null +++ b/metadata/service/service.go @@ -0,0 +1,71 @@ +/* + * 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 ( + "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/config" +) + +// Metadataservice is used to define meta data related behaviors +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 + GetExportedURLs(serviceInterface string, group string, version string, protocol string) (*skip.SkipList, error) + // GetExportedURLs will get the target subscribed url in metadata + GetSubscribedURLs() (*skip.SkipList, 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 + // Version will return the metadata service version + Version() string +} + +// BaseMetadataService is used for the common logic for struct who will implement interface MetadataService +type BaseMetadataService struct { +} + +// ServiceName can get the service's name in meta service , which is application name +func (mts *BaseMetadataService) ServiceName() (string, error) { + return config.GetApplicationConfig().Name, nil +} + +// Version will return the version of metadata service +func (mts *BaseMetadataService) Reference() string { + return constant.SIMPLE_METADATA_SERVICE_NAME +} diff --git a/protocol/dubbo/client.go b/protocol/dubbo/client.go index 5ec7db51af..e6ffa64d80 100644 --- a/protocol/dubbo/client.go +++ b/protocol/dubbo/client.go @@ -88,7 +88,7 @@ func init() { rand.Seed(time.Now().UnixNano()) } -// SetClientConf ... +// SetClientConf set dubbo client config. func SetClientConf(c ClientConfig) { clientConf = &c err := clientConf.CheckValidity() @@ -99,7 +99,7 @@ func SetClientConf(c ClientConfig) { setClientGrpool() } -// GetClientConf ... +// GetClientConf get dubbo client config. func GetClientConf() ClientConfig { return *clientConf } @@ -129,7 +129,7 @@ type AsyncCallbackResponse struct { Reply interface{} } -// Client ... +// Client is dubbo protocol client. type Client struct { opts Options conf ClientConfig @@ -139,7 +139,7 @@ type Client struct { pendingResponses *sync.Map } -// NewClient ... +// NewClient create a new Client. func NewClient(opt Options) *Client { switch { @@ -167,7 +167,7 @@ func NewClient(opt Options) *Client { return c } -// Request ... +// Request is dubbo protocol request. type Request struct { addr string svcUrl common.URL @@ -176,7 +176,7 @@ type Request struct { atta map[string]string } -// NewRequest ... +// NewRequest create a new Request. func NewRequest(addr string, svcUrl common.URL, method string, args interface{}, atta map[string]string) *Request { return &Request{ addr: addr, @@ -187,13 +187,13 @@ func NewRequest(addr string, svcUrl common.URL, method string, args interface{}, } } -// Response ... +// Response is dubbo protocol response. type Response struct { reply interface{} atta map[string]string } -// NewResponse ... +// NewResponse create a new Response. func NewResponse(reply interface{}, atta map[string]string) *Response { return &Response{ reply: reply, @@ -201,15 +201,14 @@ func NewResponse(reply interface{}, atta map[string]string) *Response { } } -// CallOneway call one way +// CallOneway call by one way func (c *Client) CallOneway(request *Request) error { return perrors.WithStack(c.call(CT_OneWay, request, NewResponse(nil, nil), nil)) } -// Call if @response is nil, the transport layer will get the response without notify the invoker. +// Call call remoting by two way or one way, if @response.reply is nil, the way of call is one way. func (c *Client) Call(request *Request, response *Response) error { - ct := CT_TwoWay if response.reply == nil { ct = CT_OneWay @@ -218,14 +217,12 @@ func (c *Client) Call(request *Request, response *Response) error { return perrors.WithStack(c.call(ct, request, response, nil)) } -// AsyncCall ... +// AsyncCall call remoting by async with callback. func (c *Client) AsyncCall(request *Request, callback common.AsyncCallback, response *Response) error { - return perrors.WithStack(c.call(CT_TwoWay, request, response, callback)) } func (c *Client) call(ct CallType, request *Request, response *Response, callback common.AsyncCallback) error { - p := &DubboPackage{} p.Service.Path = strings.TrimPrefix(request.svcUrl.Path, "/") p.Service.Interface = request.svcUrl.GetParam(constant.INTERFACE_KEY, "") @@ -293,7 +290,7 @@ func (c *Client) call(ct CallType, request *Request, response *Response, callbac return perrors.WithStack(err) } -// Close ... +// Close close the client pool. func (c *Client) Close() { if c.pool != nil { c.pool.close() diff --git a/protocol/dubbo/client_test.go b/protocol/dubbo/client_test.go index 1e0a73fac1..744ffa80d6 100644 --- a/protocol/dubbo/client_test.go +++ b/protocol/dubbo/client_test.go @@ -162,7 +162,7 @@ func InitTest(t *testing.T) (protocol.Protocol, common.URL) { hessian.RegisterPOJO(&User{}) - methods, err := common.ServiceMap.Register("dubbo", &UserProvider{}) + methods, err := common.ServiceMap.Register("com.ikurento.user.UserProvider", "dubbo", &UserProvider{}) assert.NoError(t, err) assert.Equal(t, "GetBigPkg,GetUser,GetUser0,GetUser1,GetUser2,GetUser3,GetUser4,GetUser5,GetUser6", methods) diff --git a/protocol/dubbo/codec.go b/protocol/dubbo/codec.go index 76416b2baf..620a57d4a7 100644 --- a/protocol/dubbo/codec.go +++ b/protocol/dubbo/codec.go @@ -69,7 +69,7 @@ func (p DubboPackage) String() string { return fmt.Sprintf("DubboPackage: Header-%v, Path-%v, Body-%v", p.Header, p.Service, p.Body) } -// Marshal ... +// Marshal encode hessian package. func (p *DubboPackage) Marshal() (*bytes.Buffer, error) { codec := hessian.NewHessianCodec(nil) @@ -81,7 +81,7 @@ func (p *DubboPackage) Marshal() (*bytes.Buffer, error) { return bytes.NewBuffer(pkg), nil } -// Unmarshal ... +// Unmarshal dncode hessian package. func (p *DubboPackage) Unmarshal(buf *bytes.Buffer, opts ...interface{}) error { // fix issue https://github.com/apache/dubbo-go/issues/380 bufLen := buf.Len() @@ -125,7 +125,7 @@ func (p *DubboPackage) Unmarshal(buf *bytes.Buffer, opts ...interface{}) error { // PendingResponse //////////////////////////////////////////// -// PendingResponse ... +// PendingResponse is a pending response. type PendingResponse struct { seq uint64 err error @@ -136,7 +136,7 @@ type PendingResponse struct { done chan struct{} } -// NewPendingResponse ... +// NewPendingResponse create a PendingResponses. func NewPendingResponse() *PendingResponse { return &PendingResponse{ start: time.Now(), @@ -145,7 +145,7 @@ func NewPendingResponse() *PendingResponse { } } -// GetCallResponse ... +// GetCallResponse get AsyncCallbackResponse. func (r PendingResponse) GetCallResponse() common.CallbackResponse { return AsyncCallbackResponse{ Cause: r.err, diff --git a/protocol/dubbo/config.go b/protocol/dubbo/config.go index dbc6989c54..6a1daf857a 100644 --- a/protocol/dubbo/config.go +++ b/protocol/dubbo/config.go @@ -147,7 +147,7 @@ func GetDefaultServerConfig() ServerConfig { } } -// CheckValidity ... +// CheckValidity confirm getty sessian params. func (c *GettySessionParam) CheckValidity() error { var err error @@ -170,7 +170,7 @@ func (c *GettySessionParam) CheckValidity() error { return nil } -// CheckValidity ... +// CheckValidity confirm client params. func (c *ClientConfig) CheckValidity() error { var err error @@ -192,7 +192,7 @@ func (c *ClientConfig) CheckValidity() error { return perrors.WithStack(c.GettySessionParam.CheckValidity()) } -// CheckValidity ... +// CheckValidity confirm server params. func (c *ServerConfig) CheckValidity() error { var err error diff --git a/protocol/dubbo/dubbo_exporter.go b/protocol/dubbo/dubbo_exporter.go index f4cd0cc123..dd80937c5b 100644 --- a/protocol/dubbo/dubbo_exporter.go +++ b/protocol/dubbo/dubbo_exporter.go @@ -28,23 +28,24 @@ import ( "github.com/apache/dubbo-go/protocol" ) -// DubboExporter ... +// DubboExporter is dubbo service exporter. type DubboExporter struct { protocol.BaseExporter } -// NewDubboExporter ... +// NewDubboExporter get a DubboExporter. func NewDubboExporter(key string, invoker protocol.Invoker, exporterMap *sync.Map) *DubboExporter { return &DubboExporter{ BaseExporter: *protocol.NewBaseExporter(key, invoker, exporterMap), } } -// Unexport ... +// Unexport unexport dubbo service exporter. func (de *DubboExporter) Unexport() { serviceId := de.GetInvoker().GetUrl().GetParam(constant.BEAN_NAME_KEY, "") + interfaceName := de.GetInvoker().GetUrl().GetParam(constant.INTERFACE_KEY, "") de.BaseExporter.Unexport() - err := common.ServiceMap.UnRegister(DUBBO, serviceId) + err := common.ServiceMap.UnRegister(interfaceName, DUBBO, serviceId) if err != nil { logger.Errorf("[DubboExporter.Unexport] error: %v", err) } diff --git a/protocol/dubbo/dubbo_invoker.go b/protocol/dubbo/dubbo_invoker.go index 09c3725710..59202d5f49 100644 --- a/protocol/dubbo/dubbo_invoker.go +++ b/protocol/dubbo/dubbo_invoker.go @@ -39,8 +39,9 @@ import ( ) var ( - // ErrNoReply ... - ErrNoReply = perrors.New("request need @response") + // ErrNoReply + ErrNoReply = perrors.New("request need @response") + // ErrDestroyedInvoker ErrDestroyedInvoker = perrors.New("request Destroyed invoker") ) @@ -48,7 +49,7 @@ var ( attachmentKey = []string{constant.INTERFACE_KEY, constant.GROUP_KEY, constant.TOKEN_KEY, constant.TIMEOUT_KEY} ) -// DubboInvoker ... +// DubboInvoker is dubbo client invoker. type DubboInvoker struct { protocol.BaseInvoker client *Client @@ -57,7 +58,7 @@ type DubboInvoker struct { reqNum int64 } -// NewDubboInvoker ... +// NewDubboInvoker create dubbo client invoker. func NewDubboInvoker(url common.URL, client *Client) *DubboInvoker { return &DubboInvoker{ BaseInvoker: *protocol.NewBaseInvoker(url), @@ -66,7 +67,7 @@ func NewDubboInvoker(url common.URL, client *Client) *DubboInvoker { } } -// Invoke ... +// Invoke call remoting. func (di *DubboInvoker) Invoke(ctx context.Context, invocation protocol.Invocation) protocol.Result { var ( err error @@ -122,7 +123,7 @@ func (di *DubboInvoker) Invoke(ctx context.Context, invocation protocol.Invocati return &result } -// Destroy ... +// Destroy destroy dubbo client invoker. func (di *DubboInvoker) Destroy() { di.quitOnce.Do(func() { for { diff --git a/protocol/dubbo/dubbo_protocol.go b/protocol/dubbo/dubbo_protocol.go index 355dbc8024..b7d0a8a268 100644 --- a/protocol/dubbo/dubbo_protocol.go +++ b/protocol/dubbo/dubbo_protocol.go @@ -45,14 +45,14 @@ var ( dubboProtocol *DubboProtocol ) -// DubboProtocol ... +// DubboProtocol is a dubbo protocol implement. type DubboProtocol struct { protocol.BaseProtocol serverMap map[string]*Server serverLock sync.Mutex } -// NewDubboProtocol ... +// NewDubboProtocol create a dubbo protocol. func NewDubboProtocol() *DubboProtocol { return &DubboProtocol{ BaseProtocol: protocol.NewBaseProtocol(), @@ -60,7 +60,7 @@ func NewDubboProtocol() *DubboProtocol { } } -// Export ... +// Export export dubbo service. func (dp *DubboProtocol) Export(invoker protocol.Invoker) protocol.Exporter { url := invoker.GetUrl() serviceKey := url.ServiceKey() @@ -73,7 +73,7 @@ func (dp *DubboProtocol) Export(invoker protocol.Invoker) protocol.Exporter { return exporter } -// Refer ... +// Refer create dubbo service reference. func (dp *DubboProtocol) Refer(url common.URL) protocol.Invoker { //default requestTimeout var requestTimeout = config.GetConsumerConfig().RequestTimeout @@ -92,7 +92,7 @@ func (dp *DubboProtocol) Refer(url common.URL) protocol.Invoker { return invoker } -// Destroy ... +// Destroy destroy dubbo service. func (dp *DubboProtocol) Destroy() { logger.Infof("DubboProtocol destroy.") @@ -124,7 +124,7 @@ func (dp *DubboProtocol) openServer(url common.URL) { } } -// GetProtocol ... +// GetProtocol get a single dubbo protocol. func GetProtocol() protocol.Protocol { if dubboProtocol == nil { dubboProtocol = NewDubboProtocol() diff --git a/protocol/dubbo/listener.go b/protocol/dubbo/listener.go index 0251b78a2b..1f4cc0068e 100644 --- a/protocol/dubbo/listener.go +++ b/protocol/dubbo/listener.go @@ -86,7 +86,7 @@ func (h *RpcClientHandler) OnOpen(session getty.Session) error { // OnError ... func (h *RpcClientHandler) OnError(session getty.Session, err error) { - logger.Infof("session{%s} got error{%v}, will be closed.", session.Stat(), err) + logger.Warnf("session{%s} got error{%v}, will be closed.", session.Stat(), err) h.conn.removeSession(session) } @@ -201,7 +201,7 @@ func (h *RpcServerHandler) OnOpen(session getty.Session) error { // OnError ... func (h *RpcServerHandler) OnError(session getty.Session, err error) { - logger.Infof("session{%s} got error{%v}, will be closed.", session.Stat(), err) + logger.Warnf("session{%s} got error{%v}, will be closed.", session.Stat(), err) h.rwlock.Lock() delete(h.sessionMap, session) h.rwlock.Unlock() diff --git a/protocol/dubbo/readwriter.go b/protocol/dubbo/readwriter.go index b5c4f50919..9cc7ea25cd 100644 --- a/protocol/dubbo/readwriter.go +++ b/protocol/dubbo/readwriter.go @@ -38,16 +38,17 @@ import ( // RpcClientPackageHandler //////////////////////////////////////////// -// RpcClientPackageHandler ... +// RpcClientPackageHandler handle package for client in getty. type RpcClientPackageHandler struct { client *Client } -// NewRpcClientPackageHandler ... +// NewRpcClientPackageHandler create a RpcClientPackageHandler. func NewRpcClientPackageHandler(client *Client) *RpcClientPackageHandler { return &RpcClientPackageHandler{client: client} } +// Read decode @data to DubboPackage. func (p *RpcClientPackageHandler) Read(ss getty.Session, data []byte) (interface{}, int, error) { pkg := &DubboPackage{} @@ -72,6 +73,7 @@ func (p *RpcClientPackageHandler) Read(ss getty.Session, data []byte) (interface return pkg, hessian.HEADER_LENGTH + pkg.Header.BodyLen, nil } +// Write encode @pkg. func (p *RpcClientPackageHandler) Write(ss getty.Session, pkg interface{}) ([]byte, error) { req, ok := pkg.(*DubboPackage) if !ok { @@ -96,9 +98,10 @@ var ( rpcServerPkgHandler = &RpcServerPackageHandler{} ) -// RpcServerPackageHandler ... +// RpcServerPackageHandler handle package for server in getty. type RpcServerPackageHandler struct{} +// Read decode @data to DubboPackage. func (p *RpcServerPackageHandler) Read(ss getty.Session, data []byte) (interface{}, int, error) { pkg := &DubboPackage{ Body: make([]interface{}, 7), @@ -169,6 +172,7 @@ func (p *RpcServerPackageHandler) Read(ss getty.Session, data []byte) (interface return pkg, hessian.HEADER_LENGTH + pkg.Header.BodyLen, nil } +// Write encode @pkg. func (p *RpcServerPackageHandler) Write(ss getty.Session, pkg interface{}) ([]byte, error) { res, ok := pkg.(*DubboPackage) if !ok { diff --git a/protocol/dubbo/server.go b/protocol/dubbo/server.go index bd2b37b7a9..8de353a0b3 100644 --- a/protocol/dubbo/server.go +++ b/protocol/dubbo/server.go @@ -71,10 +71,10 @@ func init() { if err := srvConf.CheckValidity(); err != nil { panic(err) } - SetServerGrpool() + setServerGrpool() } -// SetServerConfig ... +// SetServerConfig set dubbo server config. func SetServerConfig(s ServerConfig) { srvConf = &s err := srvConf.CheckValidity() @@ -82,30 +82,29 @@ func SetServerConfig(s ServerConfig) { logger.Warnf("[ServerConfig CheckValidity] error: %v", err) return } - SetServerGrpool() + setServerGrpool() } -// GetServerConfig ... +// GetServerConfig get dubbo server config. func GetServerConfig() ServerConfig { return *srvConf } -// SetServerGrpool ... -func SetServerGrpool() { +func setServerGrpool() { if srvConf.GrPoolSize > 1 { srvGrpool = gxsync.NewTaskPool(gxsync.WithTaskPoolTaskPoolSize(srvConf.GrPoolSize), gxsync.WithTaskPoolTaskQueueLength(srvConf.QueueLen), gxsync.WithTaskPoolTaskQueueNumber(srvConf.QueueNumber)) } } -// Server ... +// Server is dubbo protocol server. type Server struct { conf ServerConfig tcpServer getty.Server rpcHandler *RpcServerHandler } -// NewServer ... +// NewServer create a new Server. func NewServer() *Server { s := &Server{ @@ -156,7 +155,7 @@ func (s *Server) newSession(session getty.Session) error { return nil } -// Start ... +// Start start dubbo server. func (s *Server) Start(url common.URL) { var ( addr string @@ -173,7 +172,7 @@ func (s *Server) Start(url common.URL) { } -// Stop ... +// Stop stop dubbo server. func (s *Server) Stop() { s.tcpServer.Close() } diff --git a/protocol/grpc/grpc_exporter.go b/protocol/grpc/grpc_exporter.go index 1acd2fec39..0296ad8b5b 100644 --- a/protocol/grpc/grpc_exporter.go +++ b/protocol/grpc/grpc_exporter.go @@ -43,8 +43,9 @@ func NewGrpcExporter(key string, invoker protocol.Invoker, exporterMap *sync.Map // Unexport ... func (gg *GrpcExporter) Unexport() { serviceId := gg.GetInvoker().GetUrl().GetParam(constant.BEAN_NAME_KEY, "") + interfaceName := gg.GetInvoker().GetUrl().GetParam(constant.INTERFACE_KEY, "") gg.BaseExporter.Unexport() - err := common.ServiceMap.UnRegister(GRPC, serviceId) + err := common.ServiceMap.UnRegister(interfaceName, GRPC, serviceId) if err != nil { logger.Errorf("[GrpcExporter.Unexport] error: %v", err) } diff --git a/protocol/jsonrpc/http_test.go b/protocol/jsonrpc/http_test.go index 0cb88b36a8..f8480bf32e 100644 --- a/protocol/jsonrpc/http_test.go +++ b/protocol/jsonrpc/http_test.go @@ -50,7 +50,7 @@ type ( func TestHTTPClient_Call(t *testing.T) { - methods, err := common.ServiceMap.Register("jsonrpc", &UserProvider{}) + methods, err := common.ServiceMap.Register("com.ikurento.user.UserProvider", "jsonrpc", &UserProvider{}) assert.NoError(t, err) assert.Equal(t, "GetUser,GetUser0,GetUser1,GetUser2,GetUser3,GetUser4", methods) diff --git a/protocol/jsonrpc/jsonrpc_exporter.go b/protocol/jsonrpc/jsonrpc_exporter.go index 7f8fd49185..c61cf9adae 100644 --- a/protocol/jsonrpc/jsonrpc_exporter.go +++ b/protocol/jsonrpc/jsonrpc_exporter.go @@ -43,8 +43,9 @@ func NewJsonrpcExporter(key string, invoker protocol.Invoker, exporterMap *sync. // Unexport ... func (je *JsonrpcExporter) Unexport() { serviceId := je.GetInvoker().GetUrl().GetParam(constant.BEAN_NAME_KEY, "") + interfaceName := je.GetInvoker().GetUrl().GetParam(constant.INTERFACE_KEY, "") je.BaseExporter.Unexport() - err := common.ServiceMap.UnRegister(JSONRPC, serviceId) + err := common.ServiceMap.UnRegister(interfaceName, JSONRPC, serviceId) if err != nil { logger.Errorf("[JsonrpcExporter.Unexport] error: %v", err) } diff --git a/protocol/jsonrpc/jsonrpc_invoker_test.go b/protocol/jsonrpc/jsonrpc_invoker_test.go index 9e08eed2b4..0f14ba11e2 100644 --- a/protocol/jsonrpc/jsonrpc_invoker_test.go +++ b/protocol/jsonrpc/jsonrpc_invoker_test.go @@ -36,7 +36,7 @@ import ( func TestJsonrpcInvoker_Invoke(t *testing.T) { - methods, err := common.ServiceMap.Register("jsonrpc", &UserProvider{}) + methods, err := common.ServiceMap.Register("UserProvider", "jsonrpc", &UserProvider{}) assert.NoError(t, err) assert.Equal(t, "GetUser,GetUser0,GetUser1,GetUser2,GetUser3,GetUser4", methods) diff --git a/protocol/mock/mock_invoker.go b/protocol/mock/mock_invoker.go index f22b803e75..0c88b47e36 100644 --- a/protocol/mock/mock_invoker.go +++ b/protocol/mock/mock_invoker.go @@ -15,22 +15,6 @@ * limitations under the License. */ -// 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. -// - // Code generated by MockGen. DO NOT EDIT. // Source: invoker.go diff --git a/protocol/rest/client/client_impl/resty_client.go b/protocol/rest/client/client_impl/resty_client.go index aa6c23137d..b60f50a5a7 100644 --- a/protocol/rest/client/client_impl/resty_client.go +++ b/protocol/rest/client/client_impl/resty_client.go @@ -40,10 +40,12 @@ func init() { extension.SetRestClient(constant.DEFAULT_REST_CLIENT, NewRestyClient) } +// RestyClient a rest client implement by Resty type RestyClient struct { client *resty.Client } +// NewRestyClient a constructor of RestyClient func NewRestyClient(restOption *client.RestOptions) client.RestClient { client := resty.New() client.SetTransport( @@ -65,21 +67,21 @@ func NewRestyClient(restOption *client.RestOptions) client.RestClient { } } -func (rc *RestyClient) Do(restRequest *client.RestRequest, res interface{}) error { - r, err := rc.client.R(). - SetHeader("Content-Type", restRequest.Consumes). - SetHeader("Accept", restRequest.Produces). +// Do send request by RestyClient +func (rc *RestyClient) Do(restRequest *client.RestClientRequest, res interface{}) error { + req := rc.client.R() + req.Header = restRequest.Header + resp, err := req. SetPathParams(restRequest.PathParams). SetQueryParams(restRequest.QueryParams). - SetHeaders(restRequest.Headers). SetBody(restRequest.Body). SetResult(res). Execute(restRequest.Method, "http://"+path.Join(restRequest.Location, restRequest.Path)) if err != nil { return perrors.WithStack(err) } - if r.IsError() { - return perrors.New(r.String()) + if resp.IsError() { + return perrors.New(resp.String()) } return nil } diff --git a/protocol/rest/client/rest_client.go b/protocol/rest/client/rest_client.go index 7d020abc81..d63c5e0bd0 100644 --- a/protocol/rest/client/rest_client.go +++ b/protocol/rest/client/rest_client.go @@ -18,26 +18,28 @@ package client import ( + "net/http" "time" ) +// RestOptions type RestOptions struct { RequestTimeout time.Duration ConnectTimeout time.Duration } -type RestRequest struct { +// RestClientRequest +type RestClientRequest struct { + Header http.Header Location string Path string - Produces string - Consumes string Method string PathParams map[string]string QueryParams map[string]string Body interface{} - Headers map[string]string } +// RestClient user can implement this client interface to send request type RestClient interface { - Do(request *RestRequest, res interface{}) error + Do(request *RestClientRequest, res interface{}) error } diff --git a/protocol/rest/rest_exporter.go b/protocol/rest/rest_exporter.go index 470d525ad8..1ee208615e 100644 --- a/protocol/rest/rest_exporter.go +++ b/protocol/rest/rest_exporter.go @@ -40,8 +40,9 @@ func NewRestExporter(key string, invoker protocol.Invoker, exporterMap *sync.Map func (re *RestExporter) Unexport() { serviceId := re.GetInvoker().GetUrl().GetParam(constant.BEAN_NAME_KEY, "") + interfaceName := re.GetInvoker().GetUrl().GetParam(constant.INTERFACE_KEY, "") re.BaseExporter.Unexport() - err := common.ServiceMap.UnRegister(REST, serviceId) + err := common.ServiceMap.UnRegister(interfaceName, REST, serviceId) if err != nil { logger.Errorf("[RestExporter.Unexport] error: %v", err) } diff --git a/protocol/rest/rest_invoker.go b/protocol/rest/rest_invoker.go index 0c82035ac5..121d1217ef 100644 --- a/protocol/rest/rest_invoker.go +++ b/protocol/rest/rest_invoker.go @@ -20,6 +20,7 @@ package rest import ( "context" "fmt" + "net/http" ) import ( @@ -56,7 +57,7 @@ func (ri *RestInvoker) Invoke(ctx context.Context, invocation protocol.Invocatio body interface{} pathParams map[string]string queryParams map[string]string - headers map[string]string + header http.Header err error ) if methodConfig == nil { @@ -71,24 +72,21 @@ func (ri *RestInvoker) Invoke(ctx context.Context, invocation protocol.Invocatio result.Err = err return &result } - if headers, err = restStringMapTransform(methodConfig.HeadersMap, inv.Arguments()); err != nil { + if header, err = getRestHttpHeader(methodConfig, inv.Arguments()); err != nil { result.Err = err return &result } if len(inv.Arguments()) > methodConfig.Body && methodConfig.Body >= 0 { body = inv.Arguments()[methodConfig.Body] } - - req := &client.RestRequest{ + req := &client.RestClientRequest{ Location: ri.GetUrl().Location, - Produces: methodConfig.Produces, - Consumes: methodConfig.Consumes, Method: methodConfig.MethodType, Path: methodConfig.Path, PathParams: pathParams, QueryParams: queryParams, Body: body, - Headers: headers, + Header: header, } result.Err = ri.client.Do(req, inv.Reply()) if result.Err == nil { @@ -107,3 +105,17 @@ func restStringMapTransform(paramsMap map[int]string, args []interface{}) (map[s } return resMap, nil } + +func getRestHttpHeader(methodConfig *config.RestMethodConfig, args []interface{}) (http.Header, error) { + header := http.Header{} + headersMap := methodConfig.HeadersMap + header.Set("Content-Type", methodConfig.Consumes) + header.Set("Accept", methodConfig.Produces) + for k, v := range headersMap { + if k >= len(args) || k < 0 { + return nil, perrors.Errorf("[Rest Invoke] Index %v is out of bundle", k) + } + header.Set(v, fmt.Sprint(args[k])) + } + return header, nil +} diff --git a/protocol/rest/rest_invoker_test.go b/protocol/rest/rest_invoker_test.go index e44c5d9a21..2ea260c58d 100644 --- a/protocol/rest/rest_invoker_test.go +++ b/protocol/rest/rest_invoker_test.go @@ -61,7 +61,7 @@ func TestRestInvoker_Invoke(t *testing.T) { "module=dubbogo+user-info+server&org=ikurento.com&owner=ZX&pid=1447&revision=0.0.1&" + "side=provider&timeout=3000×tamp=1556509797245") assert.NoError(t, err) - _, err = common.ServiceMap.Register(url.Protocol, &UserProvider{}) + _, err = common.ServiceMap.Register("UserProvider", url.Protocol, &UserProvider{}) assert.NoError(t, err) con := config.ProviderConfig{} config.SetProviderConfig(con) @@ -206,6 +206,6 @@ func TestRestInvoker_Invoke(t *testing.T) { assert.Error(t, res.Error(), "test error") assert.Equal(t, filterNum, 12) - err = common.ServiceMap.UnRegister(url.Protocol, "com.ikurento.user.UserProvider") + err = common.ServiceMap.UnRegister("UserProvider", url.Protocol, "com.ikurento.user.UserProvider") assert.NoError(t, err) } diff --git a/protocol/rest/rest_protocol.go b/protocol/rest/rest_protocol.go index 47ecb6093b..e15eeb39d7 100644 --- a/protocol/rest/rest_protocol.go +++ b/protocol/rest/rest_protocol.go @@ -75,7 +75,9 @@ func (rp *RestProtocol) Export(invoker protocol.Invoker) protocol.Exporter { } rp.SetExporterMap(serviceKey, exporter) restServer := rp.getServer(url, restServiceConfig.Server) - restServer.Deploy(invoker, restServiceConfig.RestMethodConfigsMap) + for _, methodConfig := range restServiceConfig.RestMethodConfigsMap { + restServer.Deploy(methodConfig, server.GetRouteFunc(invoker, methodConfig)) + } return exporter } diff --git a/protocol/rest/rest_protocol_test.go b/protocol/rest/rest_protocol_test.go index 8af73a1839..9117148777 100644 --- a/protocol/rest/rest_protocol_test.go +++ b/protocol/rest/rest_protocol_test.go @@ -80,7 +80,7 @@ func TestRestProtocol_Export(t *testing.T) { "module=dubbogo+user-info+server&org=ikurento.com&owner=ZX&pid=1447&revision=0.0.1&" + "side=provider&timeout=3000×tamp=1556509797245") assert.NoError(t, err) - _, err = common.ServiceMap.Register(url.Protocol, &UserProvider{}) + _, err = common.ServiceMap.Register("UserProvider", url.Protocol, &UserProvider{}) assert.NoError(t, err) con := config.ProviderConfig{} config.SetProviderConfig(con) @@ -128,7 +128,7 @@ func TestRestProtocol_Export(t *testing.T) { proto.Destroy() _, ok = proto.(*RestProtocol).serverMap[url.Location] assert.False(t, ok) - err = common.ServiceMap.UnRegister(url.Protocol, "com.ikurento.user.UserProvider") + err = common.ServiceMap.UnRegister("UserProvider", url.Protocol, "com.ikurento.user.UserProvider") assert.NoError(t, err) } diff --git a/protocol/rest/server/rest_server.go b/protocol/rest/server/rest_server.go index c10c98a7b6..fbd6fb7ad9 100644 --- a/protocol/rest/server/rest_server.go +++ b/protocol/rest/server/rest_server.go @@ -17,15 +17,306 @@ package server +import ( + "context" + "errors" + "net/http" + "reflect" + "strconv" + "strings" +) + +import ( + perrors "github.com/pkg/errors" +) + import ( "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/logger" "github.com/apache/dubbo-go/protocol" - "github.com/apache/dubbo-go/protocol/rest/config" + "github.com/apache/dubbo-go/protocol/invocation" + rest_config "github.com/apache/dubbo-go/protocol/rest/config" ) +const parseParameterErrorStr = "An error occurred while parsing parameters on the server" + +// RestServer user can implement this server interface type RestServer interface { + // Start rest server Start(url common.URL) - Deploy(invoker protocol.Invoker, restMethodConfig map[string]*config.RestMethodConfig) - UnDeploy(restMethodConfig map[string]*config.RestMethodConfig) + // Deploy a http api + Deploy(restMethodConfig *rest_config.RestMethodConfig, routeFunc func(request RestServerRequest, response RestServerResponse)) + // UnDeploy a http api + UnDeploy(restMethodConfig *rest_config.RestMethodConfig) + // Destroy rest server Destroy() } + +// RestServerRequest interface +type RestServerRequest interface { + // RawRequest get the Ptr of http.Request + RawRequest() *http.Request + // PathParameter get the path parameter by name + PathParameter(name string) string + // PathParameters get the map of the path parameters + PathParameters() map[string]string + // QueryParameter get the query parameter by name + QueryParameter(name string) string + // QueryParameters get the map of query parameters + QueryParameters(name string) []string + // BodyParameter get the body parameter of name + BodyParameter(name string) (string, error) + // HeaderParameter get the header parameter of name + HeaderParameter(name string) string + // ReadEntity checks the Accept header and reads the content into the entityPointer. + ReadEntity(entityPointer interface{}) error +} + +// RestServerResponse interface +type RestServerResponse interface { + http.ResponseWriter + // WriteError writes the http status and the error string on the response. err can be nil. + // Return an error if writing was not successful. + WriteError(httpStatus int, err error) (writeErr error) + // WriteEntity marshals the value using the representation denoted by the Accept Header. + WriteEntity(value interface{}) error +} + +// GetRouteFunc +// A route function will be invoked by http server +func GetRouteFunc(invoker protocol.Invoker, methodConfig *rest_config.RestMethodConfig) func(req RestServerRequest, resp RestServerResponse) { + return func(req RestServerRequest, resp RestServerResponse) { + var ( + err error + args []interface{} + ) + svc := common.ServiceMap.GetService(invoker.GetUrl().Protocol, strings.TrimPrefix(invoker.GetUrl().Path, "/")) + // get method + method := svc.Method()[methodConfig.MethodName] + argsTypes := method.ArgsType() + replyType := method.ReplyType() + // two ways to prepare arguments + // if method like this 'func1(req []interface{}, rsp *User) error' + // we don't have arguments type + if (len(argsTypes) == 1 || len(argsTypes) == 2 && replyType == nil) && + argsTypes[0].String() == "[]interface {}" { + args, err = getArgsInterfaceFromRequest(req, methodConfig) + } else { + args, err = getArgsFromRequest(req, argsTypes, methodConfig) + } + if err != nil { + logger.Errorf("[Go Restful] parsing http parameters error:%v", err) + err = resp.WriteError(http.StatusInternalServerError, errors.New(parseParameterErrorStr)) + if err != nil { + logger.Errorf("[Go Restful] WriteErrorString error:%v", err) + } + } + result := invoker.Invoke(context.Background(), invocation.NewRPCInvocation(methodConfig.MethodName, args, make(map[string]string))) + if result.Error() != nil { + err = resp.WriteError(http.StatusInternalServerError, result.Error()) + if err != nil { + logger.Errorf("[Go Restful] WriteError error:%v", err) + } + return + } + err = resp.WriteEntity(result.Result()) + if err != nil { + logger.Errorf("[Go Restful] WriteEntity error:%v", err) + } + } +} + +// getArgsInterfaceFromRequest when service function like GetUser(req []interface{}, rsp *User) error +// use this method to get arguments +func getArgsInterfaceFromRequest(req RestServerRequest, methodConfig *rest_config.RestMethodConfig) ([]interface{}, error) { + argsMap := make(map[int]interface{}, 8) + maxKey := 0 + for k, v := range methodConfig.PathParamsMap { + if maxKey < k { + maxKey = k + } + argsMap[k] = req.PathParameter(v) + } + for k, v := range methodConfig.QueryParamsMap { + if maxKey < k { + maxKey = k + } + params := req.QueryParameters(v) + if len(params) == 1 { + argsMap[k] = params[0] + } else { + argsMap[k] = params + } + } + for k, v := range methodConfig.HeadersMap { + if maxKey < k { + maxKey = k + } + argsMap[k] = req.HeaderParameter(v) + } + if methodConfig.Body >= 0 { + if maxKey < methodConfig.Body { + maxKey = methodConfig.Body + } + m := make(map[string]interface{}) + // TODO read as a slice + if err := req.ReadEntity(&m); err != nil { + return nil, perrors.Errorf("[Go restful] Read body entity as map[string]interface{} error:%v", err) + } + argsMap[methodConfig.Body] = m + } + args := make([]interface{}, maxKey+1) + for k, v := range argsMap { + if k >= 0 { + args[k] = v + } + } + return args, nil +} + +// getArgsFromRequest get arguments from server.RestServerRequest +func getArgsFromRequest(req RestServerRequest, argsTypes []reflect.Type, methodConfig *rest_config.RestMethodConfig) ([]interface{}, error) { + argsLength := len(argsTypes) + args := make([]interface{}, argsLength) + for i, t := range argsTypes { + args[i] = reflect.Zero(t).Interface() + } + if err := assembleArgsFromPathParams(methodConfig, argsLength, argsTypes, req, args); err != nil { + return nil, err + } + if err := assembleArgsFromQueryParams(methodConfig, argsLength, argsTypes, req, args); err != nil { + return nil, err + } + if err := assembleArgsFromBody(methodConfig, argsTypes, req, args); err != nil { + return nil, err + } + if err := assembleArgsFromHeaders(methodConfig, req, argsLength, argsTypes, args); err != nil { + return nil, err + } + return args, nil +} + +// assembleArgsFromHeaders assemble arguments from headers +func assembleArgsFromHeaders(methodConfig *rest_config.RestMethodConfig, req RestServerRequest, argsLength int, argsTypes []reflect.Type, args []interface{}) error { + for k, v := range methodConfig.HeadersMap { + param := req.HeaderParameter(v) + if k < 0 || k >= argsLength { + return perrors.Errorf("[Go restful] Header param parse error, the index %v args of method:%v doesn't exist", k, methodConfig.MethodName) + } + t := argsTypes[k] + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() == reflect.String { + args[k] = param + } else { + return perrors.Errorf("[Go restful] Header param parse error, the index %v args's type isn't string", k) + } + } + return nil +} + +// assembleArgsFromBody assemble arguments from body +func assembleArgsFromBody(methodConfig *rest_config.RestMethodConfig, argsTypes []reflect.Type, req RestServerRequest, args []interface{}) error { + if methodConfig.Body >= 0 && methodConfig.Body < len(argsTypes) { + t := argsTypes[methodConfig.Body] + kind := t.Kind() + if kind == reflect.Ptr { + t = t.Elem() + } + var ni interface{} + if t.String() == "[]interface {}" { + ni = make([]map[string]interface{}, 0) + } else if t.String() == "interface {}" { + ni = make(map[string]interface{}) + } else { + n := reflect.New(t) + if n.CanInterface() { + ni = n.Interface() + } + } + if err := req.ReadEntity(&ni); err != nil { + return perrors.Errorf("[Go restful] Read body entity error, error is %v", perrors.WithStack(err)) + } + args[methodConfig.Body] = ni + } + return nil +} + +// assembleArgsFromQueryParams assemble arguments from query params +func assembleArgsFromQueryParams(methodConfig *rest_config.RestMethodConfig, argsLength int, argsTypes []reflect.Type, req RestServerRequest, args []interface{}) error { + var ( + err error + param interface{} + i64 int64 + ) + for k, v := range methodConfig.QueryParamsMap { + if k < 0 || k >= argsLength { + return perrors.Errorf("[Go restful] Query param parse error, the index %v args of method:%v doesn't exist", k, methodConfig.MethodName) + } + t := argsTypes[k] + kind := t.Kind() + if kind == reflect.Ptr { + t = t.Elem() + } + if kind == reflect.Slice { + param = req.QueryParameters(v) + } else if kind == reflect.String { + param = req.QueryParameter(v) + } else if kind == reflect.Int { + param, err = strconv.Atoi(req.QueryParameter(v)) + } else if kind == reflect.Int32 { + i64, err = strconv.ParseInt(req.QueryParameter(v), 10, 32) + if err == nil { + param = int32(i64) + } + } else if kind == reflect.Int64 { + param, err = strconv.ParseInt(req.QueryParameter(v), 10, 64) + } else { + return perrors.Errorf("[Go restful] Query param parse error, the index %v args's type isn't int or string or slice", k) + } + if err != nil { + return perrors.Errorf("[Go restful] Query param parse error, error:%v", perrors.WithStack(err)) + } + args[k] = param + } + return nil +} + +// assembleArgsFromPathParams assemble arguments from path params +func assembleArgsFromPathParams(methodConfig *rest_config.RestMethodConfig, argsLength int, argsTypes []reflect.Type, req RestServerRequest, args []interface{}) error { + var ( + err error + param interface{} + i64 int64 + ) + for k, v := range methodConfig.PathParamsMap { + if k < 0 || k >= argsLength { + return perrors.Errorf("[Go restful] Path param parse error, the index %v args of method:%v doesn't exist", k, methodConfig.MethodName) + } + t := argsTypes[k] + kind := t.Kind() + if kind == reflect.Ptr { + t = t.Elem() + } + if kind == reflect.Int { + param, err = strconv.Atoi(req.PathParameter(v)) + } else if kind == reflect.Int32 { + i64, err = strconv.ParseInt(req.PathParameter(v), 10, 32) + if err == nil { + param = int32(i64) + } + } else if kind == reflect.Int64 { + param, err = strconv.ParseInt(req.PathParameter(v), 10, 64) + } else if kind == reflect.String { + param = req.PathParameter(v) + } else { + return perrors.Errorf("[Go restful] Path param parse error, the index %v args's type isn't int or string", k) + } + if err != nil { + return perrors.Errorf("[Go restful] Path param parse error, error is %v", perrors.WithStack(err)) + } + args[k] = param + } + return nil +} diff --git a/protocol/rest/server/server_impl/go_restful_server.go b/protocol/rest/server/server_impl/go_restful_server.go index 69f36a5c80..c7d971fcaa 100644 --- a/protocol/rest/server/server_impl/go_restful_server.go +++ b/protocol/rest/server/server_impl/go_restful_server.go @@ -22,8 +22,6 @@ import ( "fmt" "net" "net/http" - "reflect" - "strconv" "strings" "time" ) @@ -38,27 +36,29 @@ 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/protocol" - "github.com/apache/dubbo-go/protocol/invocation" "github.com/apache/dubbo-go/protocol/rest/config" "github.com/apache/dubbo-go/protocol/rest/server" ) func init() { - extension.SetRestServer(constant.DEFAULT_REST_SERVER, GetNewGoRestfulServer) + extension.SetRestServer(constant.DEFAULT_REST_SERVER, NewGoRestfulServer) } var filterSlice []restful.FilterFunction +// GoRestfulServer a rest server implement by go-restful type GoRestfulServer struct { srv *http.Server container *restful.Container } -func NewGoRestfulServer() *GoRestfulServer { +// NewGoRestfulServer a constructor of GoRestfulServer +func NewGoRestfulServer() server.RestServer { return &GoRestfulServer{} } +// Start go-restful server +// It will add all go-restful filters func (grs *GoRestfulServer) Start(url common.URL) { grs.container = restful.NewContainer() for _, filter := range filterSlice { @@ -80,61 +80,32 @@ func (grs *GoRestfulServer) Start(url common.URL) { }() } -func (grs *GoRestfulServer) Deploy(invoker protocol.Invoker, restMethodConfig map[string]*config.RestMethodConfig) { - svc := common.ServiceMap.GetService(invoker.GetUrl().Protocol, strings.TrimPrefix(invoker.GetUrl().Path, "/")) - for methodName, config := range restMethodConfig { - // get method - method := svc.Method()[methodName] - argsTypes := method.ArgsType() - replyType := method.ReplyType() - ws := new(restful.WebService) - ws.Path(config.Path). - Produces(strings.Split(config.Produces, ",")...). - Consumes(strings.Split(config.Consumes, ",")...). - Route(ws.Method(config.MethodType).To(getFunc(methodName, invoker, argsTypes, replyType, config))) - grs.container.Add(ws) +// Publish a http api in go-restful server +// The routeFunc should be invoked when the server receive a request +func (grs *GoRestfulServer) Deploy(restMethodConfig *config.RestMethodConfig, routeFunc func(request server.RestServerRequest, response server.RestServerResponse)) { + ws := &restful.WebService{} + rf := func(req *restful.Request, resp *restful.Response) { + routeFunc(NewGoRestfulRequestAdapter(req), resp) } + ws.Path(restMethodConfig.Path). + Produces(strings.Split(restMethodConfig.Produces, ",")...). + Consumes(strings.Split(restMethodConfig.Consumes, ",")...). + Route(ws.Method(restMethodConfig.MethodType).To(rf)) + grs.container.Add(ws) } -func getFunc(methodName string, invoker protocol.Invoker, argsTypes []reflect.Type, - replyType reflect.Type, config *config.RestMethodConfig) func(req *restful.Request, resp *restful.Response) { - return func(req *restful.Request, resp *restful.Response) { - var ( - err error - args []interface{} - ) - if (len(argsTypes) == 1 || len(argsTypes) == 2 && replyType == nil) && - argsTypes[0].String() == "[]interface {}" { - args = getArgsInterfaceFromRequest(req, config) - } else { - args = getArgsFromRequest(req, argsTypes, config) - } - result := invoker.Invoke(context.Background(), invocation.NewRPCInvocation(methodName, args, make(map[string]string))) - if result.Error() != nil { - err = resp.WriteError(http.StatusInternalServerError, result.Error()) - if err != nil { - logger.Errorf("[Go Restful] WriteError error:%v", err) - } - return - } - err = resp.WriteEntity(result.Result()) - if err != nil { - logger.Error("[Go Restful] WriteEntity error:%v", err) - } - } -} -func (grs *GoRestfulServer) UnDeploy(restMethodConfig map[string]*config.RestMethodConfig) { - for _, config := range restMethodConfig { - ws := new(restful.WebService) - ws.Path(config.Path) - err := grs.container.Remove(ws) - if err != nil { - logger.Warnf("[Go restful] Remove web service error:%v", err) - } +// Delete a http api in go-restful server +func (grs *GoRestfulServer) UnDeploy(restMethodConfig *config.RestMethodConfig) { + ws := new(restful.WebService) + ws.Path(restMethodConfig.Path) + err := grs.container.Remove(ws) + if err != nil { + logger.Warnf("[Go restful] Remove web service error:%v", err) } } +// Destroy the go-restful server func (grs *GoRestfulServer) Destroy() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -144,179 +115,59 @@ func (grs *GoRestfulServer) Destroy() { logger.Infof("[Go Restful] Server exiting") } -func getArgsInterfaceFromRequest(req *restful.Request, config *config.RestMethodConfig) []interface{} { - argsMap := make(map[int]interface{}, 8) - maxKey := 0 - for k, v := range config.PathParamsMap { - if maxKey < k { - maxKey = k - } - argsMap[k] = req.PathParameter(v) - } - for k, v := range config.QueryParamsMap { - if maxKey < k { - maxKey = k - } - params := req.QueryParameters(v) - if len(params) == 1 { - argsMap[k] = params[0] - } else { - argsMap[k] = params - } - } - for k, v := range config.HeadersMap { - if maxKey < k { - maxKey = k - } - argsMap[k] = req.HeaderParameter(v) - } - if config.Body >= 0 { - if maxKey < config.Body { - maxKey = config.Body - } - m := make(map[string]interface{}) - // TODO read as a slice - if err := req.ReadEntity(&m); err != nil { - logger.Warnf("[Go restful] Read body entity as map[string]interface{} error:%v", perrors.WithStack(err)) - } else { - argsMap[config.Body] = m - } - } - args := make([]interface{}, maxKey+1) - for k, v := range argsMap { - if k >= 0 { - args[k] = v - } - } - return args +// AddGoRestfulServerFilter let user add the http server of go-restful +// addFilter should before config.Load() +func AddGoRestfulServerFilter(filterFuc restful.FilterFunction) { + filterSlice = append(filterSlice, filterFuc) } -func getArgsFromRequest(req *restful.Request, argsTypes []reflect.Type, config *config.RestMethodConfig) []interface{} { - argsLength := len(argsTypes) - args := make([]interface{}, argsLength) - for i, t := range argsTypes { - args[i] = reflect.Zero(t).Interface() - } - var ( - err error - param interface{} - i64 int64 - ) - for k, v := range config.PathParamsMap { - if k < 0 || k >= argsLength { - logger.Errorf("[Go restful] Path param parse error, the args:%v doesn't exist", k) - continue - } - t := argsTypes[k] - kind := t.Kind() - if kind == reflect.Ptr { - t = t.Elem() - } - if kind == reflect.Int { - param, err = strconv.Atoi(req.PathParameter(v)) - } else if kind == reflect.Int32 { - i64, err = strconv.ParseInt(req.PathParameter(v), 10, 32) - if err == nil { - param = int32(i64) - } - } else if kind == reflect.Int64 { - param, err = strconv.ParseInt(req.PathParameter(v), 10, 64) - } else if kind == reflect.String { - param = req.PathParameter(v) - } else { - logger.Warnf("[Go restful] Path param parse error, the args:%v of type isn't int or string", k) - continue - } - if err != nil { - logger.Errorf("[Go restful] Path param parse error, error is %v", err) - continue - } - args[k] = param - } - for k, v := range config.QueryParamsMap { - if k < 0 || k >= argsLength { - logger.Errorf("[Go restful] Query param parse error, the args:%v doesn't exist", k) - continue - } - t := argsTypes[k] - kind := t.Kind() - if kind == reflect.Ptr { - t = t.Elem() - } - if kind == reflect.Slice { - param = req.QueryParameters(v) - } else if kind == reflect.String { - param = req.QueryParameter(v) - } else if kind == reflect.Int { - param, err = strconv.Atoi(req.QueryParameter(v)) - } else if kind == reflect.Int32 { - i64, err = strconv.ParseInt(req.QueryParameter(v), 10, 32) - if err == nil { - param = int32(i64) - } - } else if kind == reflect.Int64 { - param, err = strconv.ParseInt(req.QueryParameter(v), 10, 64) - } else { - logger.Errorf("[Go restful] Query param parse error, the args:%v of type isn't int or string or slice", k) - continue - } - if err != nil { - logger.Errorf("[Go restful] Query param parse error, error is %v", err) - continue - } - args[k] = param - } +// GoRestfulRequestAdapter a adapter struct about RestServerRequest +type GoRestfulRequestAdapter struct { + server.RestServerRequest + request *restful.Request +} - if config.Body >= 0 && config.Body < len(argsTypes) { - t := argsTypes[config.Body] - kind := t.Kind() - if kind == reflect.Ptr { - t = t.Elem() - } - var ni interface{} - if t.String() == "[]interface {}" { - ni = make([]map[string]interface{}, 0) - } else if t.String() == "interface {}" { - ni = make(map[string]interface{}) - } else { - n := reflect.New(t) - if n.CanInterface() { - ni = n.Interface() - } - } - if err := req.ReadEntity(&ni); err != nil { - logger.Errorf("[Go restful] Read body entity error:%v", err) - } else { - args[config.Body] = ni - } - } +// NewGoRestfulRequestAdapter a constructor of GoRestfulRequestAdapter +func NewGoRestfulRequestAdapter(request *restful.Request) *GoRestfulRequestAdapter { + return &GoRestfulRequestAdapter{request: request} +} - for k, v := range config.HeadersMap { - param := req.HeaderParameter(v) - if k < 0 || k >= argsLength { - logger.Errorf("[Go restful] Header param parse error, the args:%v doesn't exist", k) - continue - } - t := argsTypes[k] - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - if t.Kind() == reflect.String { - args[k] = param - } else { - logger.Errorf("[Go restful] Header param parse error, the args:%v of type isn't string", k) - } - } +// RawRequest a adapter function of server.RestServerRequest's RawRequest +func (grra *GoRestfulRequestAdapter) RawRequest() *http.Request { + return grra.request.Request +} - return args +// PathParameter a adapter function of server.RestServerRequest's PathParameter +func (grra *GoRestfulRequestAdapter) PathParameter(name string) string { + return grra.request.PathParameter(name) } -func GetNewGoRestfulServer() server.RestServer { - return NewGoRestfulServer() +// PathParameters a adapter function of server.RestServerRequest's QueryParameter +func (grra *GoRestfulRequestAdapter) PathParameters() map[string]string { + return grra.request.PathParameters() } -// Let user addFilter -// addFilter should before config.Load() -func AddGoRestfulServerFilter(filterFuc restful.FilterFunction) { - filterSlice = append(filterSlice, filterFuc) +// QueryParameter a adapter function of server.RestServerRequest's QueryParameters +func (grra *GoRestfulRequestAdapter) QueryParameter(name string) string { + return grra.request.QueryParameter(name) +} + +// QueryParameters a adapter function of server.RestServerRequest's QueryParameters +func (grra *GoRestfulRequestAdapter) QueryParameters(name string) []string { + return grra.request.QueryParameters(name) +} + +// BodyParameter a adapter function of server.RestServerRequest's BodyParameter +func (grra *GoRestfulRequestAdapter) BodyParameter(name string) (string, error) { + return grra.request.BodyParameter(name) +} + +// HeaderParameter a adapter function of server.RestServerRequest's HeaderParameter +func (grra *GoRestfulRequestAdapter) HeaderParameter(name string) string { + return grra.request.HeaderParameter(name) +} + +// ReadEntity a adapter func of server.RestServerRequest's ReadEntity +func (grra *GoRestfulRequestAdapter) ReadEntity(entityPointer interface{}) error { + return grra.request.ReadEntity(entityPointer) } diff --git a/registry/base_registry.go b/registry/base_registry.go index 3b64e93e2f..3e1bddf233 100644 --- a/registry/base_registry.go +++ b/registry/base_registry.go @@ -121,6 +121,7 @@ func (r *BaseRegistry) Destroy() { close(r.done) // wait waitgroup done (wait listeners outside close over) r.wg.Wait() + //close registry client r.closeRegisters() } @@ -178,7 +179,10 @@ func (r *BaseRegistry) RestartCallBack() bool { } logger.Infof("success to re-register service :%v", confIf.Key()) } - r.facadeBasedRegistry.InitListeners() + + if flag { + r.facadeBasedRegistry.InitListeners() + } return flag } @@ -245,19 +249,13 @@ func (r *BaseRegistry) providerRegistry(c common.URL, params url.Values) (string logger.Errorf("facadeBasedRegistry.CreatePath(path{%s}) = error{%#v}", dubboPath, perrors.WithStack(err)) return "", "", perrors.WithMessagef(err, "facadeBasedRegistry.CreatePath(path:%s)", dubboPath) } - params.Add("anyhost", "true") + params.Add(constant.ANYHOST_KEY, "true") // Dubbo java consumer to start looking for the provider url,because the category does not match, // the provider will not find, causing the consumer can not start, so we use consumers. - // DubboRole = [...]string{"consumer", "", "", "provider"} - // params.Add("category", (RoleType(PROVIDER)).Role()) - params.Add("category", (common.RoleType(common.PROVIDER)).String()) - params.Add("dubbo", "dubbo-provider-golang-"+constant.Version) - - params.Add("side", (common.RoleType(common.PROVIDER)).Role()) if len(c.Methods) == 0 { - params.Add("methods", strings.Join(c.Methods, ",")) + params.Add(constant.METHODS_KEY, strings.Join(c.Methods, ",")) } logger.Debugf("provider url params:%#v", params) var host string @@ -308,9 +306,6 @@ func (r *BaseRegistry) consumerRegistry(c common.URL, params url.Values) (string } params.Add("protocol", c.Protocol) - params.Add("category", (common.RoleType(common.CONSUMER)).String()) - params.Add("dubbo", "dubbogo-consumer-"+constant.Version) - rawURL = fmt.Sprintf("consumer://%s%s?%s", localIP, c.Path, params.Encode()) dubboPath = fmt.Sprintf("/dubbo/%s/%s", r.service(c), (common.RoleType(common.CONSUMER)).String()) diff --git a/registry/common/event_publishing_service_deiscovery_test.go b/registry/common/event_publishing_service_deiscovery_test.go new file mode 100644 index 0000000000..1e08335e04 --- /dev/null +++ b/registry/common/event_publishing_service_deiscovery_test.go @@ -0,0 +1,161 @@ +/* + * 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 common + +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/registry" +) + +func TestEventPublishingServiceDiscovery_DispatchEvent(t *testing.T) { + dc := NewEventPublishingServiceDiscovery(&ServiceDiscoveryA{}) + tsd := &TestServiceDiscoveryDestroyingEventListener{} + tsd.SetT(t) + tsi := &TestServiceInstancePreRegisteredEventListener{} + tsi.SetT(t) + extension.AddEventListener(tsd) + extension.AddEventListener(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.BaseListenable +} + +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.BaseListenable +} + +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 +} diff --git a/registry/common/event_publishing_service_discovery.go b/registry/common/event_publishing_service_discovery.go new file mode 100644 index 0000000000..f61dd84690 --- /dev/null +++ b/registry/common/event_publishing_service_discovery.go @@ -0,0 +1,141 @@ +/* + * 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 common + +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/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 +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 delegate function +func (epsd *EventPublishingServiceDiscovery) Update(instance registry.ServiceInstance) error { + f := func() error { + return epsd.serviceDiscovery.Update(instance) + } + return epsd.executeWithEvents(nil, f, nil) +} + +// Unregister delegate function +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)) +} + +func (epsd *EventPublishingServiceDiscovery) GetDefaultPageSize() int { + return epsd.serviceDiscovery.GetDefaultPageSize() +} + +func (epsd *EventPublishingServiceDiscovery) GetServices() *gxset.HashSet { + return epsd.serviceDiscovery.GetServices() +} + +func (epsd *EventPublishingServiceDiscovery) GetInstances(serviceName string) []registry.ServiceInstance { + return epsd.serviceDiscovery.GetInstances(serviceName) +} + +func (epsd *EventPublishingServiceDiscovery) GetInstancesByPage(serviceName string, offset int, pageSize int) gxpage.Pager { + return epsd.serviceDiscovery.GetInstancesByPage(serviceName, offset, pageSize) +} + +func (epsd *EventPublishingServiceDiscovery) GetHealthyInstancesByPage(serviceName string, offset int, pageSize int, healthy bool) gxpage.Pager { + return epsd.serviceDiscovery.GetHealthyInstancesByPage(serviceName, offset, pageSize, healthy) +} + +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) +} + +func (epsd *EventPublishingServiceDiscovery) DispatchEventByServiceName(serviceName string) error { + return epsd.DispatchEventByServiceName(serviceName) +} + +func (epsd *EventPublishingServiceDiscovery) DispatchEventForInstances(serviceName string, instances []registry.ServiceInstance) error { + return epsd.serviceDiscovery.DispatchEventForInstances(serviceName, instances) +} + +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 +} diff --git a/registry/common/service_discovery_event.go b/registry/common/service_discovery_event.go new file mode 100644 index 0000000000..a60ca56a39 --- /dev/null +++ b/registry/common/service_discovery_event.go @@ -0,0 +1,99 @@ +/* + * 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 common + +import ( + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/registry" +) + +type ServiceDiscoveryEvent struct { + observer.BaseEvent + original registry.ServiceDiscovery +} + +func NewServiceDiscoveryEvent(discovery registry.ServiceDiscovery, original registry.ServiceDiscovery) *ServiceDiscoveryEvent { + return &ServiceDiscoveryEvent{ + BaseEvent: *observer.NewBaseEvent(discovery), + original: original, + } +} + +func (sde *ServiceDiscoveryEvent) GetServiceDiscovery() registry.ServiceDiscovery { + return sde.GetSource().(registry.ServiceDiscovery) +} + +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/common/service_instance_event.go b/registry/common/service_instance_event.go new file mode 100644 index 0000000000..f70e7ee0ff --- /dev/null +++ b/registry/common/service_instance_event.go @@ -0,0 +1,84 @@ +/* + * 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 common + +import ( + "github.com/apache/dubbo-go/common/observer" + "github.com/apache/dubbo-go/registry" +) + +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, + } +} + +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/directory/directory.go b/registry/directory/directory.go index a6d2cdf49b..552aa57061 100644 --- a/registry/directory/directory.go +++ b/registry/directory/directory.go @@ -19,7 +19,6 @@ package directory import ( "sync" - "time" ) import ( @@ -28,6 +27,7 @@ import ( ) import ( + "github.com/apache/dubbo-go/cluster" "github.com/apache/dubbo-go/cluster/directory" "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/common/constant" @@ -42,15 +42,11 @@ import ( "github.com/apache/dubbo-go/remoting" ) -// Options ... -type Options struct { - serviceTTL time.Duration +func init() { + extension.SetDefaultRegistryDirectory(NewRegistryDirectory) } -// Option ... -type Option func(*Options) - -type registryDirectory struct { +type RegistryDirectory struct { directory.BaseDirectory cacheInvokers []protocol.Invoker listenerLock sync.Mutex @@ -61,48 +57,41 @@ type registryDirectory struct { configurators []config_center.Configurator consumerConfigurationListener *consumerConfigurationListener referenceConfigurationListener *referenceConfigurationListener - Options - serviceKey string - forbidden atomic.Bool + serviceKey string + forbidden atomic.Bool } // NewRegistryDirectory ... -func NewRegistryDirectory(url *common.URL, registry registry.Registry, opts ...Option) (*registryDirectory, error) { - options := Options{ - //default 300s - serviceTTL: time.Duration(300e9), - } - for _, opt := range opts { - opt(&options) - } +func NewRegistryDirectory(url *common.URL, registry registry.Registry) (cluster.Directory, error) { if url.SubURL == nil { return nil, perrors.Errorf("url is invalid, suburl can not be nil") } - dir := ®istryDirectory{ + dir := &RegistryDirectory{ BaseDirectory: directory.NewBaseDirectory(url), cacheInvokers: []protocol.Invoker{}, cacheInvokersMap: &sync.Map{}, serviceType: url.SubURL.Service(), registry: registry, - Options: options, } dir.consumerConfigurationListener = newConsumerConfigurationListener(dir) + + go dir.subscribe(url.SubURL) return dir, nil } //subscribe from registry -func (dir *registryDirectory) Subscribe(url *common.URL) { +func (dir *RegistryDirectory) subscribe(url *common.URL) { dir.consumerConfigurationListener.addNotifyListener(dir) dir.referenceConfigurationListener = newReferenceConfigurationListener(dir, url) dir.registry.Subscribe(url, dir) } -func (dir *registryDirectory) Notify(event *registry.ServiceEvent) { +func (dir *RegistryDirectory) Notify(event *registry.ServiceEvent) { go dir.update(event) } -//subscribe service from registry, and update the cacheServices -func (dir *registryDirectory) update(res *registry.ServiceEvent) { +// update the cacheServices and subscribe service from registry +func (dir *RegistryDirectory) update(res *registry.ServiceEvent) { if res == nil { return } @@ -111,7 +100,7 @@ func (dir *registryDirectory) update(res *registry.ServiceEvent) { dir.refreshInvokers(res) } -func (dir *registryDirectory) refreshInvokers(res *registry.ServiceEvent) { +func (dir *RegistryDirectory) refreshInvokers(res *registry.ServiceEvent) { var ( url *common.URL oldInvoker protocol.Invoker = nil @@ -127,12 +116,13 @@ func (dir *registryDirectory) refreshInvokers(res *registry.ServiceEvent) { } else if url.Protocol == constant.ROUTER_PROTOCOL || //2.for router url.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY) == constant.ROUTER_CATEGORY { url = nil + } switch res.Action { case remoting.EventTypeAdd, remoting.EventTypeUpdate: logger.Infof("selector add service url{%s}", res.Service) - var urls []*common.URL + var urls []*common.URL for _, v := range directory.GetRouterURLSet().Values() { urls = append(urls, v.(*common.URL)) } @@ -140,8 +130,6 @@ func (dir *registryDirectory) refreshInvokers(res *registry.ServiceEvent) { if len(urls) > 0 { dir.SetRouters(urls) } - - //dir.cacheService.EventTypeAdd(res.Path, dir.serviceTTL) oldInvoker = dir.cacheInvoker(url) case remoting.EventTypeDel: oldInvoker = dir.uncacheInvoker(url) @@ -163,7 +151,7 @@ func (dir *registryDirectory) refreshInvokers(res *registry.ServiceEvent) { } -func (dir *registryDirectory) toGroupInvokers() []protocol.Invoker { +func (dir *RegistryDirectory) toGroupInvokers() []protocol.Invoker { newInvokersList := []protocol.Invoker{} groupInvokersMap := make(map[string][]protocol.Invoker) groupInvokersList := []protocol.Invoker{} @@ -199,8 +187,8 @@ func (dir *registryDirectory) toGroupInvokers() []protocol.Invoker { return groupInvokersList } -// uncacheInvoker return abandoned Invoker,if no Invoker to be abandoned,return nil -func (dir *registryDirectory) uncacheInvoker(url *common.URL) protocol.Invoker { +// uncacheInvoker will return abandoned Invoker,if no Invoker to be abandoned,return nil +func (dir *RegistryDirectory) uncacheInvoker(url *common.URL) protocol.Invoker { logger.Debugf("service will be deleted in cache invokers: invokers key is %s!", url.Key()) if cacheInvoker, ok := dir.cacheInvokersMap.Load(url.Key()); ok { dir.cacheInvokersMap.Delete(url.Key()) @@ -209,8 +197,8 @@ func (dir *registryDirectory) uncacheInvoker(url *common.URL) protocol.Invoker { return nil } -// cacheInvoker return abandoned Invoker,if no Invoker to be abandoned,return nil -func (dir *registryDirectory) cacheInvoker(url *common.URL) protocol.Invoker { +// cacheInvoker will return abandoned Invoker,if no Invoker to be abandoned,return nil +func (dir *RegistryDirectory) cacheInvoker(url *common.URL) protocol.Invoker { dir.overrideUrl(dir.GetDirectoryUrl()) referenceUrl := dir.GetDirectoryUrl().SubURL @@ -245,8 +233,8 @@ func (dir *registryDirectory) cacheInvoker(url *common.URL) protocol.Invoker { return nil } -//select the protocol invokers from the directory -func (dir *registryDirectory) List(invocation protocol.Invocation) []protocol.Invoker { +// List selected protocol invokers from the directory +func (dir *RegistryDirectory) List(invocation protocol.Invocation) []protocol.Invoker { invokers := dir.cacheInvokers routerChain := dir.RouterChain() @@ -256,7 +244,7 @@ func (dir *registryDirectory) List(invocation protocol.Invocation) []protocol.In return routerChain.Route(invokers, dir.cacheOriginUrl, invocation) } -func (dir *registryDirectory) IsAvailable() bool { +func (dir *RegistryDirectory) IsAvailable() bool { if !dir.BaseDirectory.IsAvailable() { return dir.BaseDirectory.IsAvailable() } @@ -270,7 +258,7 @@ func (dir *registryDirectory) IsAvailable() bool { return false } -func (dir *registryDirectory) Destroy() { +func (dir *RegistryDirectory) Destroy() { //TODO:unregister & unsubscribe dir.BaseDirectory.Destroy(func() { invokers := dir.cacheInvokers @@ -281,7 +269,7 @@ func (dir *registryDirectory) Destroy() { }) } -func (dir *registryDirectory) overrideUrl(targetUrl *common.URL) { +func (dir *RegistryDirectory) overrideUrl(targetUrl *common.URL) { doOverrideUrl(dir.configurators, targetUrl) doOverrideUrl(dir.consumerConfigurationListener.Configurators(), targetUrl) doOverrideUrl(dir.referenceConfigurationListener.Configurators(), targetUrl) @@ -295,11 +283,11 @@ func doOverrideUrl(configurators []config_center.Configurator, targetUrl *common type referenceConfigurationListener struct { registry.BaseConfigurationListener - directory *registryDirectory + directory *RegistryDirectory url *common.URL } -func newReferenceConfigurationListener(dir *registryDirectory, url *common.URL) *referenceConfigurationListener { +func newReferenceConfigurationListener(dir *RegistryDirectory, url *common.URL) *referenceConfigurationListener { listener := &referenceConfigurationListener{directory: dir, url: url} listener.InitWith( url.EncodedServiceKey()+constant.CONFIGURATORS_SUFFIX, @@ -317,10 +305,10 @@ func (l *referenceConfigurationListener) Process(event *config_center.ConfigChan type consumerConfigurationListener struct { registry.BaseConfigurationListener listeners []registry.NotifyListener - directory *registryDirectory + directory *RegistryDirectory } -func newConsumerConfigurationListener(dir *registryDirectory) *consumerConfigurationListener { +func newConsumerConfigurationListener(dir *RegistryDirectory) *consumerConfigurationListener { listener := &consumerConfigurationListener{directory: dir} listener.InitWith( config.GetConsumerConfig().ApplicationConfig.Name+constant.CONFIGURATORS_SUFFIX, diff --git a/registry/directory/directory_test.go b/registry/directory/directory_test.go index 0dde44e73c..f1d5ce434a 100644 --- a/registry/directory/directory_test.go +++ b/registry/directory/directory_test.go @@ -79,10 +79,9 @@ func TestSubscribe_Group(t *testing.T) { suburl.SetParam(constant.CLUSTER_KEY, "mock") regurl.SubURL = &suburl mockRegistry, _ := registry.NewMockRegistry(&common.URL{}) - registryDirectory, _ := NewRegistryDirectory(®url, mockRegistry) - - go registryDirectory.Subscribe(common.NewURLWithOptions(common.WithPath("testservice"))) + dir, _ := NewRegistryDirectory(®url, mockRegistry) + go dir.(*RegistryDirectory).subscribe(common.NewURLWithOptions(common.WithPath("testservice"))) //for group1 urlmap := url.Values{} urlmap.Set(constant.GROUP_KEY, "group1") @@ -101,7 +100,7 @@ func TestSubscribe_Group(t *testing.T) { } time.Sleep(1e9) - assert.Len(t, registryDirectory.cacheInvokers, 2) + assert.Len(t, dir.(*RegistryDirectory).cacheInvokers, 2) } func Test_Destroy(t *testing.T) { @@ -173,7 +172,7 @@ Loop1: } -func normalRegistryDir(noMockEvent ...bool) (*registryDirectory, *registry.MockRegistry) { +func normalRegistryDir(noMockEvent ...bool) (*RegistryDirectory, *registry.MockRegistry) { extension.SetProtocol(protocolwrapper.FILTER, protocolwrapper.NewMockProtocolFilter) url, _ := common.NewURL("mock://127.0.0.1:1111") @@ -185,9 +184,9 @@ func normalRegistryDir(noMockEvent ...bool) (*registryDirectory, *registry.MockR ) url.SubURL = &suburl mockRegistry, _ := registry.NewMockRegistry(&common.URL{}) - registryDirectory, _ := NewRegistryDirectory(&url, mockRegistry) + dir, _ := NewRegistryDirectory(&url, mockRegistry) - go registryDirectory.Subscribe(&suburl) + go dir.(*RegistryDirectory).subscribe(&suburl) if len(noMockEvent) == 0 { for i := 0; i < 3; i++ { mockRegistry.(*registry.MockRegistry).MockEvent( @@ -201,5 +200,5 @@ func normalRegistryDir(noMockEvent ...bool) (*registryDirectory, *registry.MockR ) } } - return registryDirectory, mockRegistry.(*registry.MockRegistry) + return dir.(*RegistryDirectory), mockRegistry.(*registry.MockRegistry) } diff --git a/registry/etcdv3/registry.go b/registry/etcdv3/registry.go index e1c2576811..5d389c3637 100644 --- a/registry/etcdv3/registry.go +++ b/registry/etcdv3/registry.go @@ -164,9 +164,7 @@ func (r *etcdV3Registry) DoSubscribe(svc *common.URL) (registry.Listener, error) //register the svc to dataListener r.dataListener.AddInterestedURL(svc) - for _, v := range strings.Split(svc.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY), ",") { - go r.listener.ListenServiceEvent(fmt.Sprintf("/dubbo/%s/"+v, svc.Service()), r.dataListener) - } + go r.listener.ListenServiceEvent(fmt.Sprintf("/dubbo/%s/"+constant.DEFAULT_CATEGORY, svc.Service()), r.dataListener) return configListener, nil } diff --git a/registry/etcdv3/registry_test.go b/registry/etcdv3/registry_test.go index dc4e382979..164fe9ca61 100644 --- a/registry/etcdv3/registry_test.go +++ b/registry/etcdv3/registry_test.go @@ -63,7 +63,7 @@ func (suite *RegistryTestSuite) TestRegister() { if err != nil { t.Fatal(err) } - assert.Regexp(t, ".*dubbo%3A%2F%2F127.0.0.1%3A20000%2Fcom.ikurento.user.UserProvider%3Fanyhost%3Dtrue%26category%3Dproviders%26cluster%3Dmock%26dubbo%3Ddubbo-provider-golang-1.3.0%26.*provider", children) + assert.Regexp(t, ".*dubbo%3A%2F%2F127.0.0.1%3A20000%2Fcom.ikurento.user.UserProvider%3Fanyhost%3Dtrue%26cluster%3Dmock", children) assert.NoError(t, err) } diff --git a/registry/event.go b/registry/event.go index 37d863d216..6f647185cc 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" ) @@ -32,9 +33,9 @@ func init() { rand.Seed(time.Now().UnixNano()) } -////////////////////////////////////////// +// //////////////////////////////////////// // service event -////////////////////////////////////////// +// //////////////////////////////////////// // ServiceEvent ... type ServiceEvent struct { @@ -42,6 +43,31 @@ type ServiceEvent struct { Service common.URL } +// String return the description of event func (e ServiceEvent) String() string { return fmt.Sprintf("ServiceEvent{Action{%s}, Path{%s}}", e.Action, e.Service) } + +// ServiceInstancesChangedEvent represents service instances make some changing +type ServiceInstancesChangedEvent struct { + observer.BaseEvent + ServiceName string + Instances []ServiceInstance +} + +// String return the description of the event +func (s *ServiceInstancesChangedEvent) String() string { + return fmt.Sprintf("ServiceInstancesChangedEvent[source=%s]", s.ServiceName) +} + +// NewServiceInstancesChangedEvent will create the ServiceInstanceChangedEvent instance +func NewServiceInstancesChangedEvent(serviceName string, instances []ServiceInstance) *ServiceInstancesChangedEvent { + return &ServiceInstancesChangedEvent{ + BaseEvent: observer.BaseEvent{ + Source: serviceName, + Timestamp: time.Now(), + }, + ServiceName: serviceName, + Instances: instances, + } +} diff --git a/registry/event_listener.go b/registry/event_listener.go new file mode 100644 index 0000000000..0aaa081a44 --- /dev/null +++ b/registry/event_listener.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 registry + +import ( + "reflect" +) + +import ( + "github.com/apache/dubbo-go/common/observer" +) + +// TODO (implement ConditionalEventListener) +type ServiceInstancesChangedListener struct { + ServiceName string + observer.EventListener +} + +func (sicl *ServiceInstancesChangedListener) OnEvent(e observer.Event) error { + return nil +} + +func (sicl *ServiceInstancesChangedListener) GetPriority() int { + return -1 +} + +func (sicl *ServiceInstancesChangedListener) GetEventType() reflect.Type { + return reflect.TypeOf(&ServiceInstancesChangedEvent{}) +} diff --git a/registry/inmemory/service_discovery.go b/registry/inmemory/service_discovery.go new file mode 100644 index 0000000000..3dac35cd38 --- /dev/null +++ b/registry/inmemory/service_discovery.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 inmemory + +import ( + "github.com/dubbogo/gost/container/set" + "github.com/dubbogo/gost/page" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/registry" +) + +const ( + name = "in-memory" +) + +func init() { + + instance := &InMemoryServiceDiscovery{ + instances: make(map[string]registry.ServiceInstance, 4), + listeners: make([]*registry.ServiceInstancesChangedListener, 0, 2), + } + + extension.SetServiceDiscovery(name, func(url *common.URL) (discovery registry.ServiceDiscovery, err error) { + return instance, nil + }) +} + +// InMemoryServiceDiscovery is an implementation based on memory. +// Usually you will not use this implementation except for tests. +type InMemoryServiceDiscovery struct { + instances map[string]registry.ServiceInstance + listeners []*registry.ServiceInstancesChangedListener +} + +func (i *InMemoryServiceDiscovery) String() string { + return name +} + +// Destroy doesn't destroy the instance, it just clear the instances +func (i *InMemoryServiceDiscovery) Destroy() error { + // reset to empty + i.instances = make(map[string]registry.ServiceInstance, 4) + i.listeners = make([]*registry.ServiceInstancesChangedListener, 0, 2) + return nil +} + +// Register will store the instance using its id as key +func (i *InMemoryServiceDiscovery) Register(instance registry.ServiceInstance) error { + i.instances[instance.GetId()] = instance + return nil +} + +// Update will act like register +func (i *InMemoryServiceDiscovery) Update(instance registry.ServiceInstance) error { + return i.Register(instance) +} + +// Unregister will remove the instance +func (i *InMemoryServiceDiscovery) Unregister(instance registry.ServiceInstance) error { + delete(i.instances, instance.GetId()) + return nil +} + +// GetDefaultPageSize will return the default page size +func (i *InMemoryServiceDiscovery) GetDefaultPageSize() int { + return registry.DefaultPageSize +} + +// GetServices will return all service names +func (i *InMemoryServiceDiscovery) GetServices() *gxset.HashSet { + result := gxset.NewSet() + for _, value := range i.instances { + result.Add(value.GetServiceName()) + } + return result +} + +// GetInstances will find out all instances with serviceName +func (i *InMemoryServiceDiscovery) GetInstances(serviceName string) []registry.ServiceInstance { + result := make([]registry.ServiceInstance, 0, len(i.instances)) + for _, value := range i.instances { + if value.GetServiceName() == serviceName { + result = append(result, value) + } + } + return result +} + +// GetInstancesByPage will return the part of instances +func (i *InMemoryServiceDiscovery) GetInstancesByPage(serviceName string, offset int, pageSize int) gxpage.Pager { + instances := i.GetInstances(serviceName) + // we can not use []registry.ServiceInstance since New(...) received []interface{} as parameter + result := make([]interface{}, 0, pageSize) + for i := offset; i < len(instances) && i < offset+pageSize; i++ { + result = append(result, instances[i]) + } + return gxpage.New(offset, pageSize, result, len(instances)) +} + +// GetHealthyInstancesByPage will return the instances +func (i *InMemoryServiceDiscovery) GetHealthyInstancesByPage(serviceName string, offset int, pageSize int, healthy bool) gxpage.Pager { + instances := i.GetInstances(serviceName) + // we can not use []registry.ServiceInstance since New(...) received []interface{} as parameter + result := make([]interface{}, 0, pageSize) + count := 0 + for i := offset; i < len(instances) && count < pageSize; i++ { + if instances[i].IsHealthy() == healthy { + result = append(result, instances[i]) + count++ + } + } + return gxpage.New(offset, pageSize, result, len(instances)) +} + +// GetRequestInstances will iterate the serviceName and aggregate them +func (i *InMemoryServiceDiscovery) 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] = i.GetInstancesByPage(name, offset, requestedSize) + } + return res +} + +// AddListener will save the listener inside the memory +func (i *InMemoryServiceDiscovery) AddListener(listener *registry.ServiceInstancesChangedListener) error { + i.listeners = append(i.listeners, listener) + return nil +} + +// DispatchEventByServiceName will do nothing +func (i *InMemoryServiceDiscovery) DispatchEventByServiceName(serviceName string) error { + return nil +} + +// DispatchEventForInstances will do nothing +func (i *InMemoryServiceDiscovery) DispatchEventForInstances(serviceName string, instances []registry.ServiceInstance) error { + return nil +} + +// DispatchEvent will do nothing +func (i *InMemoryServiceDiscovery) DispatchEvent(event *registry.ServiceInstancesChangedEvent) error { + return nil +} diff --git a/registry/inmemory/service_discovery_test.go b/registry/inmemory/service_discovery_test.go new file mode 100644 index 0000000000..a934dbabff --- /dev/null +++ b/registry/inmemory/service_discovery_test.go @@ -0,0 +1,98 @@ +/* + * 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/extension" + "github.com/apache/dubbo-go/registry" +) + +func TestInMemoryServiceDiscovery(t *testing.T) { + discovery, _ := extension.GetServiceDiscovery(name, nil) + serviceName := "my-service" + err := discovery.Register(®istry.DefaultServiceInstance{ + ServiceName: serviceName, + Id: "1", + Healthy: true, + }) + assert.Nil(t, err) + + err = discovery.Register(®istry.DefaultServiceInstance{ + Id: "2", + ServiceName: "mock-service", + Healthy: false, + }) + + assert.Nil(t, err) + + services := discovery.GetServices() + assert.Equal(t, 2, services.Size()) + assert.Equal(t, registry.DefaultPageSize, discovery.GetDefaultPageSize()) + + reqInstances := discovery.GetRequestInstances([]string{serviceName, "mock-service"}, 0, 10) + assert.Equal(t, 2, len(reqInstances)) + + page := discovery.GetInstancesByPage(serviceName, 0, 10) + assert.Equal(t, 1, page.GetDataSize()) + + discovery.GetHealthyInstancesByPage(serviceName, 0, 10, true) + page = discovery.GetInstancesByPage(serviceName, 0, 10) + assert.Equal(t, 1, page.GetDataSize()) + + err = discovery.AddListener(®istry.ServiceInstancesChangedListener{}) + assert.Nil(t, err) + + err = discovery.DispatchEvent(®istry.ServiceInstancesChangedEvent{}) + assert.Nil(t, err) + + err = discovery.DispatchEventForInstances(serviceName, nil) + assert.Nil(t, err) + + err = discovery.DispatchEventByServiceName(serviceName) + assert.Nil(t, err) + + err = discovery.Unregister(®istry.DefaultServiceInstance{ + Id: "2", + }) + assert.Nil(t, err) + + services = discovery.GetServices() + assert.Equal(t, 1, services.Size()) + + err = discovery.Update(®istry.DefaultServiceInstance{ + Id: "3", + }) + assert.Nil(t, err) + + services = discovery.GetServices() + assert.Equal(t, 2, services.Size()) + + err = discovery.Destroy() + assert.Nil(t, err) + + services = discovery.GetServices() + assert.Equal(t, 0, services.Size()) +} diff --git a/registry/kubernetes/registry.go b/registry/kubernetes/registry.go index 7212a83d63..8a02d0e3e6 100644 --- a/registry/kubernetes/registry.go +++ b/registry/kubernetes/registry.go @@ -21,7 +21,6 @@ import ( "fmt" "os" "path" - "strings" "sync" "time" ) @@ -135,9 +134,7 @@ func (r *kubernetesRegistry) DoSubscribe(svc *common.URL) (registry.Listener, er //register the svc to dataListener r.dataListener.AddInterestedURL(svc) - for _, v := range strings.Split(svc.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY), ",") { - go r.listener.ListenServiceEvent(fmt.Sprintf("/dubbo/%s/"+v, svc.Service()), r.dataListener) - } + go r.listener.ListenServiceEvent(fmt.Sprintf("/dubbo/%s/"+constant.DEFAULT_CATEGORY, svc.Service()), r.dataListener) return configListener, nil } diff --git a/registry/nacos/base_registry.go b/registry/nacos/base_registry.go new file mode 100644 index 0000000000..63f4999675 --- /dev/null +++ b/registry/nacos/base_registry.go @@ -0,0 +1,102 @@ +/* + * 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" + "strconv" + "strings" + "time" +) + +import ( + "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" + perrors "github.com/pkg/errors" +) + +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" +) + +// 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) { + 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 registry, nil +} + +// getNacosConfig will return the nacos config +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/registry.go b/registry/nacos/registry.go index 965e91e894..a436b85064 100644 --- a/registry/nacos/registry.go +++ b/registry/nacos/registry.go @@ -19,7 +19,6 @@ package nacos import ( "bytes" - "net" "strconv" "strings" "time" @@ -27,9 +26,6 @@ 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" ) @@ -57,64 +53,18 @@ func init() { } type nacosRegistry struct { - *common.URL - namingClient naming_client.INamingClient -} - -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 + nacosBaseRegistry } +// newNacosRegistry will create an instance func newNacosRegistry(url *common.URL) (registry.Registry, error) { - nacosConfig, err := getNacosConfig(url) + base, err := newBaseRegistry(url) if err != nil { - return nil, err - } - client, err := clients.CreateNamingClient(nacosConfig) - if err != nil { - return nil, err - } - registry := nacosRegistry{ - URL: url, - namingClient: client, + return nil, perrors.WithStack(err) } - return ®istry, nil + return &nacosRegistry{ + base, + }, nil } func getCategory(url common.URL) string { diff --git a/registry/nacos/service_discovery.go b/registry/nacos/service_discovery.go new file mode 100644 index 0000000000..fbd84ac44d --- /dev/null +++ b/registry/nacos/service_discovery.go @@ -0,0 +1,285 @@ +/* + * 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 ( + "github.com/dubbogo/gost/container/set" + "github.com/dubbogo/gost/page" + "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/registry" +) + +const ( + defaultGroup = "DEFAULT_GROUP" + idKey = "id" +) + +// init will put the service discovery into extension +func init() { + extension.SetServiceDiscovery(constant.NACOS_KEY, newNacosServiceDiscovery) +} + +// nacosServiceDiscovery is the implementation of service discovery based on nacos. +// 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 +} + +// Destroy will close the service discovery. +// Actually, it only marks the naming client as null and then return +func (n *nacosServiceDiscovery) Destroy() error { + n.namingClient = nil + return nil +} + +// Register will register the service to nacos +func (n *nacosServiceDiscovery) Register(instance registry.ServiceInstance) error { + ins := n.toRegisterInstance(instance) + ok, err := n.namingClient.RegisterInstance(ins) + if err != nil || !ok { + return perrors.WithMessage(err, "Could not register the instance. "+instance.GetServiceName()) + } + return nil +} + +// Update will update the information +// However, because nacos client doesn't support the update API, +// so we should unregister the instance and then register it again. +// the error handling is hard to implement +func (n *nacosServiceDiscovery) Update(instance registry.ServiceInstance) error { + // TODO(wait for nacos support) + err := n.Unregister(instance) + if err != nil { + return perrors.WithStack(err) + } + return n.Register(instance) +} + +// Unregister will unregister the instance +func (n *nacosServiceDiscovery) Unregister(instance registry.ServiceInstance) error { + ok, err := n.namingClient.DeregisterInstance(n.toDeregisterInstance(instance)) + if err != nil || !ok { + return perrors.WithMessage(err, "Could not unregister the instance. "+instance.GetServiceName()) + } + return nil +} + +// GetDefaultPageSize will return the constant registry.DefaultPageSize +func (n *nacosServiceDiscovery) GetDefaultPageSize() int { + return registry.DefaultPageSize +} + +// GetServices will return the all services +func (n *nacosServiceDiscovery) GetServices() *gxset.HashSet { + services, err := n.namingClient.GetAllServicesInfo(vo.GetAllServiceInfoParam{ + GroupName: n.group, + }) + + res := gxset.NewSet() + if err != nil { + logger.Errorf("Could not query the services: %v", err) + return res + } + + for _, e := range services { + res.Add(e.Name) + } + return res +} + +// GetInstances will return the instances of serviceName and the group +func (n *nacosServiceDiscovery) GetInstances(serviceName string) []registry.ServiceInstance { + instances, err := n.namingClient.SelectAllInstances(vo.SelectAllInstancesParam{ + ServiceName: serviceName, + GroupName: n.group, + }) + if err != nil { + logger.Errorf("Could not query the instances for service: " + serviceName + ", group: " + n.group) + return make([]registry.ServiceInstance, 0, 0) + } + res := make([]registry.ServiceInstance, 0, len(instances)) + for _, ins := range instances { + metadata := ins.Metadata + id := metadata[idKey] + + delete(metadata, idKey) + + res = append(res, ®istry.DefaultServiceInstance{ + Id: id, + ServiceName: ins.ServiceName, + Host: ins.Ip, + Port: int(ins.Port), + Enable: ins.Enable, + Healthy: ins.Healthy, + Metadata: metadata, + }) + } + + return res +} + +// GetInstancesByPage will return the instances +// Due to nacos client does not support pagination, so we have to query all instances and then return part of them +func (n *nacosServiceDiscovery) GetInstancesByPage(serviceName string, offset int, pageSize int) gxpage.Pager { + all := n.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 +// The nacos client has an API SelectInstances, which has a parameter call HealthyOnly. +// 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 (n *nacosServiceDiscovery) GetHealthyInstancesByPage(serviceName string, offset int, pageSize int, healthy bool) gxpage.Pager { + all := n.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 +// The nacos client doesn't have batch API, so we should query those serviceNames one by one. +func (n *nacosServiceDiscovery) 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] = n.GetInstancesByPage(name, offset, requestedSize) + } + return res +} + +// AddListener will add a listener +func (n *nacosServiceDiscovery) AddListener(listener *registry.ServiceInstancesChangedListener) error { + return n.namingClient.Subscribe(&vo.SubscribeParam{ + ServiceName: listener.ServiceName, + SubscribeCallback: func(services []model.SubscribeService, err error) { + if err != nil { + logger.Errorf("Could not handle the subscribe notification because the err is not nil."+ + " service name: %s, err: %v", listener.ServiceName, err) + } + instances := make([]registry.ServiceInstance, 0, len(services)) + for _, service := range services { + // we won't use the nacos instance id here but use our instance id + metadata := service.Metadata + id := metadata[idKey] + + delete(metadata, idKey) + + instances = append(instances, ®istry.DefaultServiceInstance{ + Id: id, + ServiceName: service.ServiceName, + Host: service.Ip, + Port: int(service.Port), + Enable: service.Enable, + Healthy: true, + Metadata: metadata, + }) + } + + e := n.DispatchEventForInstances(listener.ServiceName, instances) + if e != nil { + logger.Errorf("Dispatching event got exception, service name: %s, err: %v", listener.ServiceName, err) + } + }, + }) +} + +// DispatchEventByServiceName will dispatch the event for the service with the service name +func (n *nacosServiceDiscovery) DispatchEventByServiceName(serviceName string) error { + return n.DispatchEventForInstances(serviceName, n.GetInstances(serviceName)) +} + +// DispatchEventForInstances will dispatch the event to those instances +func (n *nacosServiceDiscovery) DispatchEventForInstances(serviceName string, instances []registry.ServiceInstance) error { + return n.DispatchEvent(registry.NewServiceInstancesChangedEvent(serviceName, instances)) +} + +// DispatchEvent will dispatch the event +func (n *nacosServiceDiscovery) DispatchEvent(event *registry.ServiceInstancesChangedEvent) error { + extension.GetGlobalDispatcher().Dispatch(event) + return nil +} + +// toRegisterInstance convert the ServiceInstance to RegisterInstanceParam +// the Ephemeral will be true +func (n *nacosServiceDiscovery) toRegisterInstance(instance registry.ServiceInstance) vo.RegisterInstanceParam { + metadata := instance.GetMetadata() + if metadata == nil { + metadata = make(map[string]string, 1) + } + metadata[idKey] = instance.GetId() + return vo.RegisterInstanceParam{ + ServiceName: instance.GetServiceName(), + Ip: instance.GetHost(), + Port: uint64(instance.GetPort()), + Metadata: metadata, + Enable: instance.IsEnable(), + Healthy: instance.IsHealthy(), + GroupName: n.group, + Ephemeral: true, + } +} + +// toDeregisterInstance will convert the ServiceInstance to DeregisterInstanceParam +func (n *nacosServiceDiscovery) toDeregisterInstance(instance registry.ServiceInstance) vo.DeregisterInstanceParam { + return vo.DeregisterInstanceParam{ + ServiceName: instance.GetServiceName(), + Ip: instance.GetHost(), + Port: uint64(instance.GetPort()), + GroupName: n.group, + } +} + +// toDeregisterInstance will create new service discovery instance +func newNacosServiceDiscovery(url *common.URL) (registry.ServiceDiscovery, error) { + + base, err := newBaseRegistry(url) + if err != nil { + return nil, perrors.WithStack(err) + } + return &nacosServiceDiscovery{ + nacosBaseRegistry: base, + group: url.GetParam(constant.NACOS_GROUP, defaultGroup), + }, nil +} diff --git a/registry/nacos/service_discovery_test.go b/registry/nacos/service_discovery_test.go new file mode 100644 index 0000000000..0ac46cb9a2 --- /dev/null +++ b/registry/nacos/service_discovery_test.go @@ -0,0 +1,131 @@ +/* + * 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 ( + "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/common/observer" + "github.com/apache/dubbo-go/common/observer/dispatcher" + "github.com/apache/dubbo-go/registry" +) + +func TestNacosServiceDiscovery_Destroy(t *testing.T) { + serviceDiscovery, err := extension.GetServiceDiscovery(constant.NACOS_KEY, mockUrl()) + assert.Nil(t, err) + assert.NotNil(t, serviceDiscovery) + err = serviceDiscovery.Destroy() + assert.Nil(t, err) + assert.Nil(t, serviceDiscovery.(*nacosServiceDiscovery).namingClient) +} + +func TestNacosServiceDiscovery_CRUD(t *testing.T) { + + extension.SetEventDispatcher("mock", func() observer.EventDispatcher { + return &dispatcher.MockEventDispatcher{} + }) + + extension.SetAndInitGlobalDispatcher("mock") + + serviceName := "service-name" + id := "id" + host := "host" + port := 123 + instance := ®istry.DefaultServiceInstance{ + Id: id, + ServiceName: serviceName, + Host: host, + Port: port, + Enable: true, + Healthy: true, + Metadata: nil, + } + + // clean data + + serviceDiscovry, _ := extension.GetServiceDiscovery(constant.NACOS_KEY, mockUrl()) + + // clean data for local test + serviceDiscovry.Unregister(®istry.DefaultServiceInstance{ + Id: id, + ServiceName: serviceName, + Host: host, + Port: port, + }) + + err := serviceDiscovry.Register(instance) + assert.Nil(t, err) + + page := serviceDiscovry.GetHealthyInstancesByPage(serviceName, 0, 10, true) + assert.NotNil(t, page) + + assert.Equal(t, 0, page.GetOffset()) + assert.Equal(t, 10, page.GetPageSize()) + assert.Equal(t, 1, page.GetDataSize()) + + instance = page.GetData()[0].(*registry.DefaultServiceInstance) + assert.NotNil(t, instance) + assert.Equal(t, id, instance.GetId()) + assert.Equal(t, host, instance.GetHost()) + assert.Equal(t, port, instance.GetPort()) + assert.Equal(t, serviceName, instance.GetServiceName()) + assert.Equal(t, 0, len(instance.GetMetadata())) + + instance.Metadata["a"] = "b" + + err = serviceDiscovry.Update(instance) + assert.Nil(t, err) + + pageMap := serviceDiscovry.GetRequestInstances([]string{serviceName}, 0, 1) + assert.Equal(t, 1, len(pageMap)) + page = pageMap[serviceName] + assert.NotNil(t, page) + assert.Equal(t, 1, len(page.GetData())) + + instance = page.GetData()[0].(*registry.DefaultServiceInstance) + v, _ := instance.Metadata["a"] + assert.Equal(t, "b", v) + + // test dispatcher event + err = serviceDiscovry.DispatchEventByServiceName(serviceName) + assert.Nil(t, err) + + // test AddListener + err = serviceDiscovry.AddListener(®istry.ServiceInstancesChangedListener{}) + assert.Nil(t, err) +} + +func TestNacosServiceDiscovery_GetDefaultPageSize(t *testing.T) { + serviceDiscovry, _ := extension.GetServiceDiscovery(constant.NACOS_KEY, mockUrl()) + 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 +} diff --git a/registry/protocol/protocol.go b/registry/protocol/protocol.go index a7678ba4e2..52a7dcbfc7 100644 --- a/registry/protocol/protocol.go +++ b/registry/protocol/protocol.go @@ -24,7 +24,7 @@ import ( ) import ( - gxset "github.com/dubbogo/gost/container/set" + "github.com/dubbogo/gost/container/set" ) import ( @@ -39,12 +39,18 @@ import ( "github.com/apache/dubbo-go/protocol" "github.com/apache/dubbo-go/protocol/protocolwrapper" "github.com/apache/dubbo-go/registry" - directory2 "github.com/apache/dubbo-go/registry/directory" + _ "github.com/apache/dubbo-go/registry/directory" "github.com/apache/dubbo-go/remoting" ) var ( - regProtocol *registryProtocol + regProtocol *registryProtocol + once sync.Once + reserveParams = []string{ + "application", "codec", "exchanger", "serialization", "cluster", "connections", "deprecated", "group", + "loadbalance", "mock", "path", "timeout", "token", "version", "warmup", "weight", "timestamp", "dubbo", + "release", "interface", + } ) type registryProtocol struct { @@ -87,6 +93,13 @@ func getRegistry(regUrl *common.URL) registry.Registry { return reg } +func getUrlToRegistry(providerUrl *common.URL, registryUrl *common.URL) *common.URL { + if registryUrl.GetParamBool("simplified", false) { + return providerUrl.CloneWithParams(reserveParams) + } + return providerUrl +} + func (proto *registryProtocol) initConfigurationListeners() { proto.overrideListeners = &sync.Map{} proto.serviceConfigurationListeners = &sync.Map{} @@ -111,7 +124,7 @@ func (proto *registryProtocol) Refer(url common.URL) protocol.Invoker { } //new registry directory for store service url from registry - directory, err := directory2.NewRegistryDirectory(®istryUrl, reg) + directory, err := extension.GetDefaultRegistryDirectory(®istryUrl, reg) if err != nil { logger.Errorf("consumer service %v create registry directory error, error message is %s, and will return nil invoker!", serviceUrl.String(), err.Error()) @@ -123,7 +136,6 @@ func (proto *registryProtocol) Refer(url common.URL) protocol.Invoker { logger.Errorf("consumer service %v register registry %v error, error message is %s", serviceUrl.String(), registryUrl.String(), err.Error()) } - go directory.Subscribe(serviceUrl) //new cluster invoker cluster := extension.GetCluster(serviceUrl.GetParam(constant.CLUSTER_KEY, constant.DEFAULT_CLUSTER)) @@ -150,7 +162,6 @@ func (proto *registryProtocol) Export(invoker protocol.Invoker) protocol.Exporte serviceConfigurationListener.OverrideUrl(providerUrl) var reg registry.Registry - if regI, loaded := proto.registries.Load(registryUrl.Key()); !loaded { reg = getRegistry(registryUrl) proto.registries.Store(registryUrl.Key(), reg) @@ -158,7 +169,8 @@ func (proto *registryProtocol) Export(invoker protocol.Invoker) protocol.Exporte reg = regI.(registry.Registry) } - err := reg.Register(*providerUrl) + registeredProviderUrl := getUrlToRegistry(providerUrl, registryUrl) + err := reg.Register(*registeredProviderUrl) if err != nil { logger.Errorf("provider service %v register registry %v error, error message is %s", providerUrl.Key(), registryUrl.Key(), err.Error()) @@ -346,12 +358,12 @@ func setProviderUrl(regURL *common.URL, providerURL *common.URL) { regURL.SubURL = providerURL } -// GetProtocol ... +// GetProtocol return the singleton RegistryProtocol func GetProtocol() protocol.Protocol { - if regProtocol != nil { - return regProtocol - } - return newRegistryProtocol() + once.Do(func() { + regProtocol = newRegistryProtocol() + }) + return regProtocol } type wrappedInvoker struct { diff --git a/registry/service_discovery.go b/registry/service_discovery.go new file mode 100644 index 0000000000..a8228a4abe --- /dev/null +++ b/registry/service_discovery.go @@ -0,0 +1,86 @@ +/* + * 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 registry + +import ( + "fmt" +) + +import ( + gxset "github.com/dubbogo/gost/container/set" + gxpage "github.com/dubbogo/gost/page" +) + +const DefaultPageSize = 100 + +type ServiceDiscovery interface { + fmt.Stringer + + // ----------------- lifecycle ------------------- + + // Destroy will destroy the service discovery. + // If the discovery cannot be destroy, it will return an error. + Destroy() error + + // ----------------- registration ---------------- + + // Register will register an instance of ServiceInstance to registry + Register(instance ServiceInstance) error + + // Update will update the data of the instance in registry + Update(instance ServiceInstance) error + + // Unregister will unregister this instance from registry + Unregister(instance ServiceInstance) error + + // ----------------- discovery ------------------- + // GetDefaultPageSize will return the default page size + GetDefaultPageSize() int + + // GetServices will return the all service names. + GetServices() *gxset.HashSet + + // GetInstances will return all service instances with serviceName + GetInstances(serviceName string) []ServiceInstance + + // GetInstancesByPage will return a page containing instances of ServiceInstance with the serviceName + // the page will start at offset + GetInstancesByPage(serviceName string, offset int, pageSize int) gxpage.Pager + + // 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 + GetHealthyInstancesByPage(serviceName string, offset int, pageSize int, healthy bool) gxpage.Pager + + // Batch get all instances by the specified service names + GetRequestInstances(serviceNames []string, offset int, requestedSize int) map[string]gxpage.Pager + + // ----------------- event ---------------------- + // AddListener adds a new ServiceInstancesChangedListener + // see addServiceInstancesChangedListener in Java + AddListener(listener *ServiceInstancesChangedListener) error + + // DispatchEventByServiceName dispatches the ServiceInstancesChangedEvent to service instance whose name is serviceName + DispatchEventByServiceName(serviceName string) error + + // DispatchEventForInstances dispatches the ServiceInstancesChangedEvent to target instances + DispatchEventForInstances(serviceName string, instances []ServiceInstance) error + + // DispatchEvent dispatches the event + DispatchEvent(event *ServiceInstancesChangedEvent) error +} diff --git a/registry/service_instance.go b/registry/service_instance.go new file mode 100644 index 0000000000..2cc229ee3b --- /dev/null +++ b/registry/service_instance.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 registry + +type ServiceInstance interface { + + // GetId will return this instance's id. It should be unique. + GetId() string + + // GetServiceName will return the serviceName + GetServiceName() string + + // GetHost will return the hostname + GetHost() string + + // GetPort will return the port. + GetPort() int + + // IsEnable will return the enable status of this instance + IsEnable() bool + + // IsHealthy will return the value represent the instance whether healthy or not + IsHealthy() bool + + // GetMetadata will return the metadata + GetMetadata() map[string]string +} + +// DefaultServiceInstance the default implementation of ServiceInstance +// or change the ServiceInstance to be struct??? +type DefaultServiceInstance struct { + Id string + ServiceName string + Host string + Port int + Enable bool + Healthy bool + Metadata map[string]string +} + +// GetId will return this instance's id. It should be unique. +func (d *DefaultServiceInstance) GetId() string { + return d.Id +} + +// GetServiceName will return the serviceName +func (d *DefaultServiceInstance) GetServiceName() string { + return d.ServiceName +} + +// GetHost will return the hostname +func (d *DefaultServiceInstance) GetHost() string { + return d.Host +} + +// GetPort will return the port. +func (d *DefaultServiceInstance) GetPort() int { + return d.Port +} + +// IsEnable will return the enable status of this instance +func (d *DefaultServiceInstance) IsEnable() bool { + return d.Enable +} + +// IsHealthy will return the value represent the instance whether healthy or not +func (d *DefaultServiceInstance) IsHealthy() bool { + return d.Healthy +} + +// GetMetadata will return the metadata +func (d *DefaultServiceInstance) GetMetadata() map[string]string { + return d.Metadata +} diff --git a/registry/zookeeper/listener.go b/registry/zookeeper/listener.go index bef1760e04..c5b2f33c61 100644 --- a/registry/zookeeper/listener.go +++ b/registry/zookeeper/listener.go @@ -35,23 +35,28 @@ import ( zk "github.com/apache/dubbo-go/remoting/zookeeper" ) -// RegistryDataListener ... +// RegistryDataListener contains all URL information subscribed by zookeeper registry type RegistryDataListener struct { - interestedURL []*common.URL - listener config_center.ConfigurationListener + subscribed map[*common.URL]config_center.ConfigurationListener + mutex sync.Mutex + closed bool } -// NewRegistryDataListener ... -func NewRegistryDataListener(listener config_center.ConfigurationListener) *RegistryDataListener { - return &RegistryDataListener{listener: listener} +// NewRegistryDataListener constructs a new RegistryDataListener +func NewRegistryDataListener() *RegistryDataListener { + return &RegistryDataListener{ + subscribed: make(map[*common.URL]config_center.ConfigurationListener)} } -// AddInterestedURL ... -func (l *RegistryDataListener) AddInterestedURL(url *common.URL) { - l.interestedURL = append(l.interestedURL, url) +// SubscribeURL is used to set a watch listener for url +func (l *RegistryDataListener) SubscribeURL(url *common.URL, listener config_center.ConfigurationListener) { + if l.closed { + return + } + l.subscribed[url] = listener } -// DataChange ... +// DataChange accepts all events sent from the zookeeper server and trigger the corresponding listener for processing func (l *RegistryDataListener) DataChange(eventType remoting.Event) bool { // Intercept the last bit index := strings.Index(eventType.Path, "/providers/") @@ -65,10 +70,14 @@ func (l *RegistryDataListener) DataChange(eventType remoting.Event) bool { logger.Errorf("Listen NewURL(r{%s}) = error{%v} eventType.Path={%v}", url, err, eventType.Path) return false } - - for _, v := range l.interestedURL { - if serviceURL.URLEqual(*v) { - l.listener.Process( + l.mutex.Lock() + defer l.mutex.Unlock() + if l.closed { + return false + } + for url, listener := range l.subscribed { + if serviceURL.URLEqual(*url) { + listener.Process( &config_center.ConfigChangeEvent{ Key: eventType.Path, Value: serviceURL, @@ -81,38 +90,48 @@ func (l *RegistryDataListener) DataChange(eventType remoting.Event) bool { return false } -// RegistryConfigurationListener ... +// Close all RegistryConfigurationListener in subscribed +func (l *RegistryDataListener) Close() { + l.mutex.Lock() + defer l.mutex.Unlock() + for _, listener := range l.subscribed { + listener.(*RegistryConfigurationListener).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 } // NewRegistryConfigurationListener for listening the event of zk. func NewRegistryConfigurationListener(client *zk.ZookeeperClient, reg *zkRegistry) *RegistryConfigurationListener { reg.WaitGroup().Add(1) - return &RegistryConfigurationListener{client: client, registry: reg, events: make(chan *config_center.ConfigChangeEvent, 32), isClosed: false} + return &RegistryConfigurationListener{client: client, registry: reg, events: make(chan *config_center.ConfigChangeEvent, 32), isClosed: false, close: make(chan struct{}, 1)} } -// Process ... +// Process submit the ConfigChangeEvent to the event chan to notify all observer func (l *RegistryConfigurationListener) Process(configType *config_center.ConfigChangeEvent) { l.events <- configType } -// Next ... +// Next will observe the registry state and events chan func (l *RegistryConfigurationListener) Next() (*registry.ServiceEvent, error) { for { select { case <-l.client.Done(): - logger.Warnf("listener's zk client connection is broken, so zk event listener exit now.") - return nil, perrors.New("listener stopped") - + logger.Warnf("listener's zk client connection (address {%s}) is broken, so zk event listener exit now.", l.client.ZkAddrs) + return nil, perrors.New("zookeeper client stopped") + case <-l.close: + return nil, perrors.New("listener have been closed") case <-l.registry.Done(): - logger.Warnf("zk consumer register has quit, so zk event listener exit now.") - return nil, perrors.New("listener stopped") - + logger.Warnf("zk consumer register has quit, so zk event listener exit now. (registry url {%v}", l.registry.BaseRegistry.URL) + return nil, perrors.New("zookeeper registry, (registry url{%v}) stopped") case e := <-l.events: logger.Debugf("got zk event %s", e) if e.ConfigType == remoting.EventTypeDel && !l.valid() { @@ -127,15 +146,17 @@ func (l *RegistryConfigurationListener) Next() (*registry.ServiceEvent, error) { } } -// Close ... +// Close RegistryConfigurationListener only once func (l *RegistryConfigurationListener) Close() { // ensure that the listener will be closed at most once. l.closeOnce.Do(func() { l.isClosed = true + l.close <- struct{}{} l.registry.WaitGroup().Done() }) } +// valid return the true if the client conn isn't nil func (l *RegistryConfigurationListener) valid() bool { return l.client.ZkConnValid() } diff --git a/registry/zookeeper/listener_test.go b/registry/zookeeper/listener_test.go index 1a76b29a6f..a0e9147a9e 100644 --- a/registry/zookeeper/listener_test.go +++ b/registry/zookeeper/listener_test.go @@ -32,15 +32,15 @@ import ( ) func Test_DataChange(t *testing.T) { - listener := NewRegistryDataListener(&MockDataListener{}) + listener := NewRegistryDataListener() url, _ := common.NewURL("jsonrpc%3A%2F%2F127.0.0.1%3A20001%2Fcom.ikurento.user.UserProvider%3Fanyhost%3Dtrue%26app.version%3D0.0.1%26application%3DBDTService%26category%3Dproviders%26cluster%3Dfailover%26dubbo%3Ddubbo-provider-golang-1.3.0%26environment%3Ddev%26group%3D%26interface%3Dcom.ikurento.user.UserProvider%26ip%3D10.32.20.124%26loadbalance%3Drandom%26methods.GetUser.loadbalance%3Drandom%26methods.GetUser.retries%3D1%26methods.GetUser.weight%3D0%26module%3Ddubbogo%2Buser-info%2Bserver%26name%3DBDTService%26organization%3Dikurento.com%26owner%3DZX%26pid%3D74500%26retries%3D0%26service.filter%3Decho%26side%3Dprovider%26timestamp%3D1560155407%26version%3D%26warmup%3D100") - listener.AddInterestedURL(&url) + listener.SubscribeURL(&url, &MockConfigurationListener{}) int := listener.DataChange(remoting.Event{Path: "/dubbo/com.ikurento.user.UserProvider/providers/jsonrpc%3A%2F%2F127.0.0.1%3A20001%2Fcom.ikurento.user.UserProvider%3Fanyhost%3Dtrue%26app.version%3D0.0.1%26application%3DBDTService%26category%3Dproviders%26cluster%3Dfailover%26dubbo%3Ddubbo-provider-golang-1.3.0%26environment%3Ddev%26group%3D%26interface%3Dcom.ikurento.user.UserProvider%26ip%3D10.32.20.124%26loadbalance%3Drandom%26methods.GetUser.loadbalance%3Drandom%26methods.GetUser.retries%3D1%26methods.GetUser.weight%3D0%26module%3Ddubbogo%2Buser-info%2Bserver%26name%3DBDTService%26organization%3Dikurento.com%26owner%3DZX%26pid%3D74500%26retries%3D0%26service.filter%3Decho%26side%3Dprovider%26timestamp%3D1560155407%26version%3D%26warmup%3D100"}) assert.Equal(t, true, int) } -type MockDataListener struct { +type MockConfigurationListener struct { } -func (*MockDataListener) Process(configType *config_center.ConfigChangeEvent) { +func (*MockConfigurationListener) Process(configType *config_center.ConfigChangeEvent) { } diff --git a/registry/zookeeper/registry.go b/registry/zookeeper/registry.go index e13443d57d..88d5d6221b 100644 --- a/registry/zookeeper/registry.go +++ b/registry/zookeeper/registry.go @@ -20,7 +20,6 @@ package zookeeper import ( "fmt" "net/url" - "strings" "sync" "time" ) @@ -54,12 +53,11 @@ func init() { type zkRegistry struct { registry.BaseRegistry - client *zookeeper.ZookeeperClient - listenerLock sync.Mutex - listener *zookeeper.ZkEventListener - dataListener *RegistryDataListener - configListener *RegistryConfigurationListener - cltLock sync.Mutex + client *zookeeper.ZookeeperClient + listenerLock sync.Mutex + listener *zookeeper.ZkEventListener + dataListener *RegistryDataListener + cltLock sync.Mutex //for provider zkPath map[string]int // key = protocol://ip:port/interface } @@ -78,13 +76,12 @@ func newZkRegistry(url *common.URL) (registry.Registry, error) { if err != nil { return nil, err } - r.WaitGroup().Add(1) //zk client start successful, then wg +1 go zookeeper.HandleClientRestart(r) r.listener = zookeeper.NewZkEventListener(r.client) - r.configListener = NewRegistryConfigurationListener(r.client, r) - r.dataListener = NewRegistryDataListener(r.configListener) + + r.dataListener = NewRegistryDataListener() return r, nil } @@ -121,8 +118,27 @@ func newMockZkRegistry(url *common.URL, opts ...zookeeper.Option) (*zk.TestClust func (r *zkRegistry) InitListeners() { r.listener = zookeeper.NewZkEventListener(r.client) - r.configListener = NewRegistryConfigurationListener(r.client, r) - r.dataListener = NewRegistryDataListener(r.configListener) + newDataListener := NewRegistryDataListener() + // should recover if dataListener isn't nil before + if r.dataListener != nil { + // close all old listener + oldDataListener := r.dataListener + oldDataListener.mutex.Lock() + defer oldDataListener.mutex.Unlock() + 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 { + regConfigListener.Close() + } + newDataListener.SubscribeURL(conf, NewRegistryConfigurationListener(r.client, r)) + go r.listener.ListenServiceEvent(conf, fmt.Sprintf("/dubbo/%s/"+constant.DEFAULT_CATEGORY, url.QueryEscape(conf.Service())), newDataListener) + + } + } + } + r.dataListener = newDataListener } func (r *zkRegistry) CreatePath(path string) error { @@ -155,8 +171,8 @@ func (r *zkRegistry) ZkClientLock() *sync.Mutex { } func (r *zkRegistry) CloseListener() { - if r.configListener != nil { - r.configListener.Close() + if r.dataListener != nil { + r.dataListener.Close() } } @@ -173,32 +189,49 @@ func (r *zkRegistry) registerTempZookeeperNode(root string, node string) error { logger.Errorf("zk.Create(root{%s}) = err{%v}", root, perrors.WithStack(err)) return perrors.WithStack(err) } + + // try to register the node zkPath, err = r.client.RegisterTemp(root, node) if err != nil { - if err == zk.ErrNodeExists { - logger.Warnf("RegisterTempNode(root{%s}, node{%s}) = error{%v}", root, node, perrors.WithStack(err)) - } else { - logger.Errorf("RegisterTempNode(root{%s}, node{%s}) = error{%v}", root, node, perrors.WithStack(err)) + logger.Errorf("Register temp node(root{%s}, node{%s}) = error{%v}", root, node, perrors.WithStack(err)) + if perrors.Cause(err) == zk.ErrNodeExists { + // should delete the old node + logger.Info("Register temp node failed, try to delete the old and recreate (root{%s}, node{%s}) , ignore!", root, node) + if err = r.client.Delete(zkPath); err == nil { + _, err = r.client.RegisterTemp(root, node) + } + if err != nil { + logger.Errorf("Recreate the temp node failed, (root{%s}, node{%s}) = error{%v}", root, node, perrors.WithStack(err)) + } } return perrors.WithMessagef(err, "RegisterTempNode(root{%s}, node{%s})", root, node) } - logger.Debugf("create a zookeeper node:%s", zkPath) + logger.Debugf("Create a zookeeper node:%s", zkPath) return nil } func (r *zkRegistry) getListener(conf *common.URL) (*RegistryConfigurationListener, error) { - var ( - zkListener *RegistryConfigurationListener - ) - r.listenerLock.Lock() - if r.configListener.isClosed { - r.listenerLock.Unlock() - return nil, perrors.New("configListener already been closed") + var zkListener *RegistryConfigurationListener + dataListener := r.dataListener + dataListener.mutex.Lock() + defer dataListener.mutex.Unlock() + if r.dataListener.subscribed[conf] != nil { + + zkListener, _ := r.dataListener.subscribed[conf].(*RegistryConfigurationListener) + if zkListener != nil { + r.listenerLock.Lock() + defer r.listenerLock.Unlock() + if zkListener.isClosed { + return nil, perrors.New("configListener already been closed") + } else { + return zkListener, nil + } + } } - zkListener = r.configListener - r.listenerLock.Unlock() + + zkListener = NewRegistryConfigurationListener(r.client, r) if r.listener == nil { r.cltLock.Lock() client := r.client @@ -216,10 +249,9 @@ func (r *zkRegistry) getListener(conf *common.URL) (*RegistryConfigurationListen } //Interested register to dataconfig. - r.dataListener.AddInterestedURL(conf) - for _, v := range strings.Split(conf.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY), ",") { - go r.listener.ListenServiceEvent(fmt.Sprintf("/dubbo/%s/"+v, url.QueryEscape(conf.Service())), r.dataListener) - } + r.dataListener.SubscribeURL(conf, zkListener) + + go r.listener.ListenServiceEvent(conf, fmt.Sprintf("/dubbo/%s/"+constant.DEFAULT_CATEGORY, url.QueryEscape(conf.Service())), r.dataListener) return zkListener, nil } diff --git a/registry/zookeeper/registry_test.go b/registry/zookeeper/registry_test.go index 0d7623ca12..688deccfbe 100644 --- a/registry/zookeeper/registry_test.go +++ b/registry/zookeeper/registry_test.go @@ -41,7 +41,7 @@ func Test_Register(t *testing.T) { 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%26category%3Dproviders%26cluster%3Dmock%26dubbo%3Ddubbo-provider-golang-1.3.0%26.*.serviceid%3Dsoa.mock%26.*provider", children) + 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) } diff --git a/remoting/zookeeper/client.go b/remoting/zookeeper/client.go index 21486aab59..bd1da54776 100644 --- a/remoting/zookeeper/client.go +++ b/remoting/zookeeper/client.go @@ -118,7 +118,7 @@ func ValidateZookeeperClient(container zkClientFacade, opts ...Option) error { for _, opt := range opts { opt(opions) } - + connected := false err = nil lock := container.ZkClientLock() @@ -143,6 +143,7 @@ func ValidateZookeeperClient(container zkClientFacade, opts ...Option) error { return perrors.WithMessagef(err, "newZookeeperClient(address:%+v)", url.Location) } container.SetZkClient(newClient) + connected = true } if container.ZkClient().Conn == nil { @@ -150,10 +151,16 @@ func ValidateZookeeperClient(container zkClientFacade, opts ...Option) error { container.ZkClient().Conn, event, err = zk.Connect(container.ZkClient().ZkAddrs, container.ZkClient().Timeout) if err == nil { container.ZkClient().Wait.Add(1) + connected = true go container.ZkClient().HandleZkEvent(event) } } + if connected { + logger.Info("Connect to zookeeper successfully, name{%s}, zk address{%v}", opions.zkName, url.Location) + container.WaitGroup().Add(1) //zk client start successful, then registry wg +1 + } + return perrors.WithMessagef(err, "newZookeeperClient(address:%+v)", url.PrimitiveURL) } @@ -386,14 +393,23 @@ 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()) conn.Close() } logger.Warnf("zkClient{name:%s, zk addr:%s} exit now.", z.name, z.ZkAddrs) } -// Create ... +// Create will create the node recursively, which means that if the parent node is absent, +// it will create parent node first. +// And the value for the basePath is "" func (z *ZookeeperClient) Create(basePath string) error { + return z.CreateWithValue(basePath, []byte("")) +} + +// CreateWithValue will create the node recursively, which means that if the parent node is absent, +// it will create parent node first. +func (z *ZookeeperClient) CreateWithValue(basePath string, value []byte) error { var ( err error tmpPath string @@ -407,7 +423,7 @@ func (z *ZookeeperClient) Create(basePath string) error { conn := z.Conn z.Unlock() if conn != nil { - _, err = conn.Create(tmpPath, []byte(""), 0, zk.WorldACL(zk.PermAll)) + _, err = conn.Create(tmpPath, value, 0, zk.WorldACL(zk.PermAll)) } if err != nil { @@ -462,7 +478,7 @@ func (z *ZookeeperClient) RegisterTemp(basePath string, node string) (string, er //if err != nil && err != zk.ErrNodeExists { if err != nil { logger.Warnf("conn.Create(\"%s\", zk.FlagEphemeral) = error(%v)\n", zkPath, perrors.WithStack(err)) - return "", perrors.WithStack(err) + return zkPath, perrors.WithStack(err) } logger.Debugf("zkClient{%s} create a temp zookeeper node:%s\n", z.name, tmpPath) diff --git a/remoting/zookeeper/facade.go b/remoting/zookeeper/facade.go index 055db4f716..4e3945388f 100644 --- a/remoting/zookeeper/facade.go +++ b/remoting/zookeeper/facade.go @@ -48,11 +48,11 @@ func HandleClientRestart(r zkClientFacade) { failTimes int ) - defer r.WaitGroup().Done() LOOP: for { select { case <-r.Done(): + r.WaitGroup().Done() // dec the wg when registry is closed logger.Warnf("(ZkProviderRegistry)reconnectZkRegistry goroutine exit now...") break LOOP // re-register all services @@ -63,12 +63,14 @@ LOOP: zkAddress := r.ZkClient().ZkAddrs r.SetZkClient(nil) r.ZkClientLock().Unlock() + r.WaitGroup().Done() // dec the wg when zk client is closed // Connect zk until success. failTimes = 0 for { select { case <-r.Done(): + r.WaitGroup().Done() // dec the wg when registry is closed logger.Warnf("(ZkProviderRegistry)reconnectZkRegistry goroutine exit now...") break LOOP case <-getty.GetTimeWheel().After(timeSecondDuration(failTimes * ConnDelay)): // Prevent crazy reconnection zk. diff --git a/remoting/zookeeper/facade_test.go b/remoting/zookeeper/facade_test.go index a41f6cd323..01d46da6cc 100644 --- a/remoting/zookeeper/facade_test.go +++ b/remoting/zookeeper/facade_test.go @@ -38,6 +38,16 @@ type mockFacade struct { done chan struct{} } +func newMockFacade(client *ZookeeperClient, url *common.URL) zkClientFacade { + mock := &mockFacade{ + client: client, + URL: url, + } + + mock.wg.Add(1) + return mock +} + func (r *mockFacade) ZkClient() *ZookeeperClient { return r.client } @@ -80,7 +90,7 @@ func Test_Facade(t *testing.T) { assert.NoError(t, err) defer ts.Stop() url, _ := common.NewURL("mock://127.0.0.1") - mock := &mockFacade{client: z, URL: &url} + mock := newMockFacade(z, &url) go HandleClientRestart(mock) states := []zk.State{zk.StateConnecting, zk.StateConnected, zk.StateHasSession} verifyEventStateOrder(t, event, states, "event channel") diff --git a/remoting/zookeeper/listener.go b/remoting/zookeeper/listener.go index eaf259f441..8487766776 100644 --- a/remoting/zookeeper/listener.go +++ b/remoting/zookeeper/listener.go @@ -18,6 +18,7 @@ package zookeeper import ( + "github.com/apache/dubbo-go/common" "path" "strings" "sync" @@ -173,7 +174,7 @@ func (l *ZkEventListener) handleZkNodeEvent(zkPath string, children []string, li } } -func (l *ZkEventListener) listenDirEvent(zkPath string, listener remoting.DataListener) { +func (l *ZkEventListener) listenDirEvent(conf *common.URL, zkPath string, listener remoting.DataListener) { defer l.wg.Done() var ( @@ -224,7 +225,16 @@ func (l *ZkEventListener) listenDirEvent(zkPath string, listener remoting.DataLi } failTimes = 0 for _, c := range children { - // listen l service node + + // Only need to compare Path when subscribing to provider + if strings.LastIndex(zkPath, constant.PROVIDER_CATEGORY) != -1 { + provider, _ := common.NewURL(c) + if provider.Path != conf.Path { + continue + } + } + + //listen l service node dubboPath := path.Join(zkPath, c) //Save the path to avoid listen repeatedly @@ -232,7 +242,7 @@ func (l *ZkEventListener) listenDirEvent(zkPath string, listener remoting.DataLi _, ok := l.pathMap[dubboPath] l.pathMapLock.Unlock() if ok { - logger.Warnf("@zkPath %s has already been listened.", zkPath) + logger.Warnf("@zkPath %s has already been listened.", dubboPath) continue } @@ -263,7 +273,7 @@ func (l *ZkEventListener) listenDirEvent(zkPath string, listener remoting.DataLi strings.LastIndex(zkPath, constant.CONSUMER_CATEGORY) == -1 { l.wg.Add(1) go func(zkPath string, listener remoting.DataListener) { - l.listenDirEvent(zkPath, listener) + l.listenDirEvent(conf, zkPath, listener) logger.Warnf("listenDirEvent(zkPath{%s}) goroutine exit now", zkPath) }(dubboPath, listener) } @@ -291,11 +301,11 @@ func timeSecondDuration(sec int) time.Duration { // registry.go:Listen -> listenServiceEvent -> listenDirEvent -> ListenServiceNodeEvent // | // --------> ListenServiceNodeEvent -func (l *ZkEventListener) ListenServiceEvent(zkPath string, listener remoting.DataListener) { +func (l *ZkEventListener) ListenServiceEvent(conf *common.URL, zkPath string, listener remoting.DataListener) { logger.Infof("listen dubbo path{%s}", zkPath) l.wg.Add(1) go func(zkPath string, listener remoting.DataListener) { - l.listenDirEvent(zkPath, listener) + l.listenDirEvent(conf, zkPath, listener) logger.Warnf("listenDirEvent(zkPath{%s}) goroutine exit now", zkPath) }(zkPath, listener) } diff --git a/remoting/zookeeper/listener_test.go b/remoting/zookeeper/listener_test.go index 7301cd52c3..ba7d6ba81b 100644 --- a/remoting/zookeeper/listener_test.go +++ b/remoting/zookeeper/listener_test.go @@ -97,7 +97,7 @@ func TestListener(t *testing.T) { go client.HandleZkEvent(event) listener := NewZkEventListener(client) dataListener := &mockDataListener{client: client, changedData: changedData, wait: &wait} - listener.ListenServiceEvent("/dubbo", dataListener) + listener.ListenServiceEvent(nil, "/dubbo", dataListener) time.Sleep(1 * time.Second) _, err := client.Conn.Set("/dubbo/dubbo.properties", []byte(changedData), 1) assert.NoError(t, err)