* 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 ( | import ( | ||||
"context" | "context" | ||||
"fmt" | "fmt" | ||||
etcd3 "go.etcd.io/etcd/client/v3" | |||||
"seata.apache.org/seata-go/pkg/util/log" | |||||
"strconv" | |||||
"strings" | "strings" | ||||
"sync" | "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 ( | const ( | ||||
@@ -189,12 +191,7 @@ func getClusterName(key []byte) (string, error) { | |||||
func getServerInstance(value []byte) (*ServiceInstance, error) { | func getServerInstance(value []byte) (*ServiceInstance, error) { | ||||
stringValue := string(value) | 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 { | if err != nil { | ||||
return nil, fmt.Errorf("etcd port has an incorrect format. err: %w", err) | 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) | return "", "", 0, fmt.Errorf("etcd key has an incorrect format. key: %s", stringKey) | ||||
} | } | ||||
cluster := keySplit[2] | 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 { | if err != nil { | ||||
return "", "", 0, fmt.Errorf("etcd port has an incorrect format. err: %w", err) | 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", | name: "use watch update ServiceInstances", | ||||
getResp: nil, | getResp: nil, | ||||
@@ -19,15 +19,14 @@ package discovery | |||||
import ( | import ( | ||||
"fmt" | "fmt" | ||||
"strconv" | |||||
"strings" | "strings" | ||||
"seata.apache.org/seata-go/pkg/util/log" | "seata.apache.org/seata-go/pkg/util/log" | ||||
"seata.apache.org/seata-go/pkg/util/net" | |||||
) | ) | ||||
const ( | const ( | ||||
endPointSplitChar = ";" | endPointSplitChar = ";" | ||||
ipPortSplitChar = ":" | |||||
) | ) | ||||
type FileRegistryService struct { | type FileRegistryService struct { | ||||
@@ -66,17 +65,13 @@ func (s *FileRegistryService) Lookup(key string) ([]*ServiceInstance, error) { | |||||
addrs := strings.Split(addrStr, endPointSplitChar) | addrs := strings.Split(addrStr, endPointSplitChar) | ||||
instances := make([]*ServiceInstance, 0) | instances := make([]*ServiceInstance, 0) | ||||
for _, addr := range addrs { | 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 { | if err != nil { | ||||
log.Errorf("endpoint err. endpoint: %s", addr) | |||||
return nil, err | return nil, err | ||||
} | } | ||||
instances = append(instances, &ServiceInstance{ | instances = append(instances, &ServiceInstance{ | ||||
Addr: ip, | |||||
Addr: host, | |||||
Port: port, | Port: port, | ||||
}) | }) | ||||
} | } | ||||
@@ -136,7 +136,7 @@ func TestFileRegistryService_Lookup(t *testing.T) { | |||||
}, | }, | ||||
want: nil, | want: nil, | ||||
wantErr: true, | 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", | name: "port is not number", | ||||
@@ -157,6 +157,75 @@ func TestFileRegistryService_Lookup(t *testing.T) { | |||||
wantErr: true, | wantErr: true, | ||||
wantErrMsg: "strconv.Atoi: parsing \"abc\": invalid syntax", | 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 { | for _, tt := range tests { | ||||
t.Run(tt.name, func(t *testing.T) { | t.Run(tt.name, func(t *testing.T) { | ||||
@@ -22,6 +22,7 @@ import ( | |||||
"fmt" | "fmt" | ||||
"net" | "net" | ||||
"reflect" | "reflect" | ||||
"strconv" | |||||
"sync" | "sync" | ||||
"sync/atomic" | "sync/atomic" | ||||
"time" | "time" | ||||
@@ -76,7 +77,7 @@ func (g *SessionManager) init() { | |||||
} | } | ||||
for _, address := range addressList { | for _, address := range addressList { | ||||
gettyClient := getty.NewTCPClient( | 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 | // todo if read c.gettyConf.ConnectionNum, will cause the connect to fail | ||||
getty.WithConnectionNumber(1), | getty.WithConnectionNumber(1), | ||||
getty.WithReconnectInterval(g.gettyConf.ReconnectInterval), | getty.WithReconnectInterval(g.gettyConf.ReconnectInterval), | ||||
@@ -18,34 +18,46 @@ | |||||
package loadbalance | package loadbalance | ||||
import ( | import ( | ||||
"fmt" | |||||
"strings" | "strings" | ||||
"sync" | "sync" | ||||
getty "github.com/apache/dubbo-getty" | 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 { | func XidLoadBalance(sessions *sync.Map, xid string) getty.Session { | ||||
var session 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 | return true | ||||
} | |||||
connectedIpPort := tmpSession.RemoteAddr() | |||||
if ipPort == connectedIpPort { | |||||
session = tmpSession | |||||
return false | |||||
} | |||||
return true | |||||
}) | |||||
}) | |||||
} | |||||
} | } | ||||
if session == nil { | if session == nil { | ||||
@@ -84,6 +84,12 @@ func TestXidLoadBalance(t *testing.T) { | |||||
xid: "127.0.0.1:9000:111", | xid: "127.0.0.1:9000:111", | ||||
returnAddrs: []string{"127.0.0.1:8000", "127.0.0.1:8002"}, | 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 { | for _, test := range testCases { | ||||
session := XidLoadBalance(test.sessions, test.xid) | 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) | |||||
} | |||||
}) | |||||
} | |||||
} |