* support ipv6 * add license * support ipv6 * support ipv6 --------- Co-authored-by: JayLiu <38887641+luky116@users.noreply.github.com> Co-authored-by: FengZhang <zfcode@qq.com>tags/v2.0.0-rc01
| @@ -20,11 +20,13 @@ package discovery | |||
| import ( | |||
| "context" | |||
| "fmt" | |||
| etcd3 "go.etcd.io/etcd/client/v3" | |||
| "seata.apache.org/seata-go/pkg/util/log" | |||
| "strconv" | |||
| "strings" | |||
| "sync" | |||
| etcd3 "go.etcd.io/etcd/client/v3" | |||
| "seata.apache.org/seata-go/pkg/util/log" | |||
| "seata.apache.org/seata-go/pkg/util/net" | |||
| ) | |||
| const ( | |||
| @@ -189,12 +191,7 @@ func getClusterName(key []byte) (string, error) { | |||
| func getServerInstance(value []byte) (*ServiceInstance, error) { | |||
| stringValue := string(value) | |||
| valueSplit := strings.Split(stringValue, addressSplitChar) | |||
| if len(valueSplit) != 2 { | |||
| return nil, fmt.Errorf("etcd value has an incorrect format. value: %s", stringValue) | |||
| } | |||
| ip := valueSplit[0] | |||
| port, err := strconv.Atoi(valueSplit[1]) | |||
| ip, port, err := net.SplitIPPortStr(stringValue) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("etcd port has an incorrect format. err: %w", err) | |||
| } | |||
| @@ -213,9 +210,7 @@ func getClusterAndAddress(key []byte) (string, string, int, error) { | |||
| return "", "", 0, fmt.Errorf("etcd key has an incorrect format. key: %s", stringKey) | |||
| } | |||
| cluster := keySplit[2] | |||
| address := strings.Split(keySplit[3], addressSplitChar) | |||
| ip := address[0] | |||
| port, err := strconv.Atoi(address[1]) | |||
| ip, port, err := net.SplitIPPortStr(keySplit[3]) | |||
| if err != nil { | |||
| return "", "", 0, fmt.Errorf("etcd port has an incorrect format. err: %w", err) | |||
| } | |||
| @@ -54,6 +54,24 @@ func TestEtcd3RegistryService_Lookup(t *testing.T) { | |||
| }, | |||
| }, | |||
| }, | |||
| { | |||
| name: "host is ipv6", | |||
| getResp: &clientv3.GetResponse{ | |||
| Kvs: []*mvccpb.KeyValue{ | |||
| { | |||
| Key: []byte("registry-seata-default-2000:0000:0000:0000:0001:2345:6789:abcd:8091"), | |||
| Value: []byte("2000:0000:0000:0000:0001:2345:6789:abcd:8091"), | |||
| }, | |||
| }, | |||
| }, | |||
| watchResp: nil, | |||
| want: []*ServiceInstance{ | |||
| { | |||
| Addr: "2000:0000:0000:0000:0001:2345:6789:abcd", | |||
| Port: 8091, | |||
| }, | |||
| }, | |||
| }, | |||
| { | |||
| name: "use watch update ServiceInstances", | |||
| getResp: nil, | |||
| @@ -19,15 +19,14 @@ package discovery | |||
| import ( | |||
| "fmt" | |||
| "strconv" | |||
| "strings" | |||
| "seata.apache.org/seata-go/pkg/util/log" | |||
| "seata.apache.org/seata-go/pkg/util/net" | |||
| ) | |||
| const ( | |||
| endPointSplitChar = ";" | |||
| ipPortSplitChar = ":" | |||
| ) | |||
| type FileRegistryService struct { | |||
| @@ -66,17 +65,13 @@ func (s *FileRegistryService) Lookup(key string) ([]*ServiceInstance, error) { | |||
| addrs := strings.Split(addrStr, endPointSplitChar) | |||
| instances := make([]*ServiceInstance, 0) | |||
| for _, addr := range addrs { | |||
| ipPort := strings.Split(addr, ipPortSplitChar) | |||
| if len(ipPort) != 2 { | |||
| return nil, fmt.Errorf("endpoint format should like ip:port. endpoint: %s", addr) | |||
| } | |||
| ip := ipPort[0] | |||
| port, err := strconv.Atoi(ipPort[1]) | |||
| host, port, err := net.SplitIPPortStr(addr) | |||
| if err != nil { | |||
| log.Errorf("endpoint err. endpoint: %s", addr) | |||
| return nil, err | |||
| } | |||
| instances = append(instances, &ServiceInstance{ | |||
| Addr: ip, | |||
| Addr: host, | |||
| Port: port, | |||
| }) | |||
| } | |||
| @@ -136,7 +136,7 @@ func TestFileRegistryService_Lookup(t *testing.T) { | |||
| }, | |||
| want: nil, | |||
| wantErr: true, | |||
| wantErrMsg: "endpoint format should like ip:port. endpoint: 127.0.0.18091", | |||
| wantErrMsg: "address 127.0.0.18091: missing port in address", | |||
| }, | |||
| { | |||
| name: "port is not number", | |||
| @@ -157,6 +157,75 @@ func TestFileRegistryService_Lookup(t *testing.T) { | |||
| wantErr: true, | |||
| wantErrMsg: "strconv.Atoi: parsing \"abc\": invalid syntax", | |||
| }, | |||
| { | |||
| name: "endpoint is ipv6", | |||
| args: args{ | |||
| key: "default_tx_group", | |||
| }, | |||
| fields: fields{ | |||
| serviceConfig: &ServiceConfig{ | |||
| VgroupMapping: map[string]string{ | |||
| "default_tx_group": "default", | |||
| }, | |||
| Grouplist: map[string]string{ | |||
| "default": "[2000:0000:0000:0000:0001:2345:6789:abcd]:8080", | |||
| }, | |||
| }, | |||
| }, | |||
| want: []*ServiceInstance{ | |||
| { | |||
| Addr: "2000:0000:0000:0000:0001:2345:6789:abcd", | |||
| Port: 8080, | |||
| }, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| { | |||
| name: "endpoint is ipv6", | |||
| args: args{ | |||
| key: "default_tx_group", | |||
| }, | |||
| fields: fields{ | |||
| serviceConfig: &ServiceConfig{ | |||
| VgroupMapping: map[string]string{ | |||
| "default_tx_group": "default", | |||
| }, | |||
| Grouplist: map[string]string{ | |||
| "default": "[2000:0000:0000:0000:0001:2345:6789:abcd%10]:8080", | |||
| }, | |||
| }, | |||
| }, | |||
| want: []*ServiceInstance{ | |||
| { | |||
| Addr: "2000:0000:0000:0000:0001:2345:6789:abcd", | |||
| Port: 8080, | |||
| }, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| { | |||
| name: "endpoint is ipv6", | |||
| args: args{ | |||
| key: "default_tx_group", | |||
| }, | |||
| fields: fields{ | |||
| serviceConfig: &ServiceConfig{ | |||
| VgroupMapping: map[string]string{ | |||
| "default_tx_group": "default", | |||
| }, | |||
| Grouplist: map[string]string{ | |||
| "default": "[::]:8080", | |||
| }, | |||
| }, | |||
| }, | |||
| want: []*ServiceInstance{ | |||
| { | |||
| Addr: "::", | |||
| Port: 8080, | |||
| }, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| } | |||
| for _, tt := range tests { | |||
| t.Run(tt.name, func(t *testing.T) { | |||
| @@ -22,6 +22,7 @@ import ( | |||
| "fmt" | |||
| "net" | |||
| "reflect" | |||
| "strconv" | |||
| "sync" | |||
| "sync/atomic" | |||
| "time" | |||
| @@ -76,7 +77,7 @@ func (g *SessionManager) init() { | |||
| } | |||
| for _, address := range addressList { | |||
| gettyClient := getty.NewTCPClient( | |||
| getty.WithServerAddress(fmt.Sprintf("%s:%d", address.Addr, address.Port)), | |||
| getty.WithServerAddress(net.JoinHostPort(address.Addr, strconv.Itoa(address.Port))), | |||
| // todo if read c.gettyConf.ConnectionNum, will cause the connect to fail | |||
| getty.WithConnectionNumber(1), | |||
| getty.WithReconnectInterval(g.gettyConf.ReconnectInterval), | |||
| @@ -18,34 +18,46 @@ | |||
| package loadbalance | |||
| import ( | |||
| "fmt" | |||
| "strings" | |||
| "sync" | |||
| getty "github.com/apache/dubbo-getty" | |||
| "seata.apache.org/seata-go/pkg/util/log" | |||
| "seata.apache.org/seata-go/pkg/util/net" | |||
| ) | |||
| func XidLoadBalance(sessions *sync.Map, xid string) getty.Session { | |||
| var session getty.Session | |||
| const delimiter = ":" | |||
| if len(xid) > 0 && strings.Contains(xid, delimiter) { | |||
| // ip:port:transactionId -> ip:port | |||
| index := strings.LastIndex(xid, delimiter) | |||
| serverAddress := xid[:index] | |||
| // ip:port:transactionId | |||
| tmpSplits := strings.Split(xid, ":") | |||
| if len(tmpSplits) == 3 { | |||
| ip := tmpSplits[0] | |||
| port := tmpSplits[1] | |||
| ipPort := ip + ":" + port | |||
| sessions.Range(func(key, value interface{}) bool { | |||
| tmpSession := key.(getty.Session) | |||
| if tmpSession.IsClosed() { | |||
| sessions.Delete(tmpSession) | |||
| // ip:port -> port | |||
| // ipv4/v6 | |||
| ip, port, err := net.SplitIPPortStr(serverAddress) | |||
| if err != nil { | |||
| log.Errorf("xid load balance err, xid:%s, %v , change use random load balance", xid, err) | |||
| } else { | |||
| sessions.Range(func(key, value interface{}) bool { | |||
| tmpSession := key.(getty.Session) | |||
| if tmpSession.IsClosed() { | |||
| sessions.Delete(tmpSession) | |||
| return true | |||
| } | |||
| ipPort := fmt.Sprintf("%s:%d", ip, port) | |||
| connectedIpPort := tmpSession.RemoteAddr() | |||
| if ipPort == connectedIpPort { | |||
| session = tmpSession | |||
| return false | |||
| } | |||
| return true | |||
| } | |||
| connectedIpPort := tmpSession.RemoteAddr() | |||
| if ipPort == connectedIpPort { | |||
| session = tmpSession | |||
| return false | |||
| } | |||
| return true | |||
| }) | |||
| }) | |||
| } | |||
| } | |||
| if session == nil { | |||
| @@ -84,6 +84,12 @@ func TestXidLoadBalance(t *testing.T) { | |||
| xid: "127.0.0.1:9000:111", | |||
| returnAddrs: []string{"127.0.0.1:8000", "127.0.0.1:8002"}, | |||
| }, | |||
| { | |||
| name: "ip is ipv6", | |||
| sessions: sessions, | |||
| xid: "2000:0000:0000:0000:0001:2345:6789:abcd:8002:111", | |||
| returnAddrs: []string{"127.0.0.1:8000", "127.0.0.1:8002"}, | |||
| }, | |||
| } | |||
| for _, test := range testCases { | |||
| session := XidLoadBalance(test.sessions, test.xid) | |||
| @@ -0,0 +1,59 @@ | |||
| /* | |||
| * 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 net | |||
| import ( | |||
| "fmt" | |||
| "regexp" | |||
| "strconv" | |||
| "strings" | |||
| ) | |||
| const ( | |||
| addressSplitChar = ":" | |||
| ) | |||
| func SplitIPPortStr(addr string) (string, int, error) { | |||
| if addr == "" { | |||
| return "", 0, fmt.Errorf("split ip err: param addr must not empty") | |||
| } | |||
| if addr[0] == '[' { | |||
| reg := regexp.MustCompile("[\\[\\]]") | |||
| addr = reg.ReplaceAllString(addr, "") | |||
| } | |||
| i := strings.LastIndex(addr, addressSplitChar) | |||
| if i < 0 { | |||
| return "", 0, fmt.Errorf("address %s: missing port in address", addr) | |||
| } | |||
| host := addr[:i] | |||
| port := addr[i+1:] | |||
| if strings.Contains(host, "%") { | |||
| reg := regexp.MustCompile("\\%[0-9]+") | |||
| host = reg.ReplaceAllString(host, "") | |||
| } | |||
| portInt, err := strconv.Atoi(port) | |||
| if err != nil { | |||
| return "", 0, err | |||
| } | |||
| return host, portInt, nil | |||
| } | |||
| @@ -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 net | |||
| import ( | |||
| "reflect" | |||
| "testing" | |||
| ) | |||
| func TestAddressValidator(t *testing.T) { | |||
| type IPAddr struct { | |||
| host string | |||
| port int | |||
| } | |||
| tests := []struct { | |||
| name string | |||
| address string | |||
| want *IPAddr | |||
| wantErr bool | |||
| wantErrMsg string | |||
| }{ | |||
| { | |||
| name: "normal single endpoint.", | |||
| address: "127.0.0.1:8091", | |||
| want: &IPAddr{ | |||
| "127.0.0.1", 8091, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| { | |||
| name: "addr is empty.", | |||
| address: "", | |||
| want: nil, | |||
| wantErr: true, | |||
| wantErrMsg: "split ip err: param addr must not empty", | |||
| }, | |||
| { | |||
| name: "format is not ip:port", | |||
| address: "127.0.0.18091", | |||
| want: nil, | |||
| wantErr: true, | |||
| wantErrMsg: "address 127.0.0.18091: missing port in address", | |||
| }, | |||
| { | |||
| name: "port is not number", | |||
| address: "127.0.0.1:abc", | |||
| want: nil, | |||
| wantErr: true, | |||
| wantErrMsg: "strconv.Atoi: parsing \"abc\": invalid syntax", | |||
| }, | |||
| { | |||
| name: "endpoint is ipv6", | |||
| address: "[2000:0000:0000:0000:0001:2345:6789:abcd]:8080", | |||
| want: &IPAddr{ | |||
| "2000:0000:0000:0000:0001:2345:6789:abcd", 8080, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| { | |||
| name: "endpoint is ipv6", | |||
| address: "[2000:0000:0000:0000:0001:2345:6789:abcd%10]:8080", | |||
| want: &IPAddr{ | |||
| "2000:0000:0000:0000:0001:2345:6789:abcd", 8080, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| { | |||
| name: "endpoint is ipv6", | |||
| address: "2000:0000:0000:0000:0001:2345:6789:abcd:8080", | |||
| want: &IPAddr{ | |||
| "2000:0000:0000:0000:0001:2345:6789:abcd", 8080, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| { | |||
| name: "endpoint is ipv6", | |||
| address: "[::]:8080", | |||
| want: &IPAddr{ | |||
| "::", 8080, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| { | |||
| name: "endpoint is ipv6", | |||
| address: "::FFFF:192.168.1.2:8080", | |||
| want: &IPAddr{ | |||
| "::FFFF:192.168.1.2", 8080, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| { | |||
| name: "endpoint is ipv6", | |||
| address: "[::FFFF:192.168.1.2]:8080", | |||
| want: &IPAddr{ | |||
| "::FFFF:192.168.1.2", 8080, | |||
| }, | |||
| wantErr: false, | |||
| }, | |||
| } | |||
| for _, tt := range tests { | |||
| t.Run(tt.name, func(t *testing.T) { | |||
| host, port, err := SplitIPPortStr(tt.address) | |||
| if (err != nil) != tt.wantErr { | |||
| t.Errorf("SplitIPPortStr() error = %v, wantErr = %v", err, tt.wantErr) | |||
| } | |||
| if tt.wantErr && err.Error() != tt.wantErrMsg { | |||
| t.Errorf("SplitIPPortStr() errMsg = %v, wantErrMsg = %v", err.Error(), tt.wantErrMsg) | |||
| } | |||
| got := &IPAddr{host, port} | |||
| if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { | |||
| t.Errorf("SplitIPPortStr() got = %v, want = %v", got, tt.want) | |||
| } | |||
| }) | |||
| } | |||
| } | |||