diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f15e9d..e387cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,17 @@ +#### 2022-03-18 +version: github.com/huaweicloud/devcloud-go v1.0.0 +feature: +1. package redis add "double-write" strategy mode. +2. package mock add "Fault injection". +3. package redis add "Fault injection service". +4. package mysql add "Fault injection service". +5. add web package, which is gin-gorm integration. + #### 2022-01-04 version: github.com/huaweicloud/devcloud-go v0.1.1 feature: 1. mock package add etcd mock, replace test cases that rely on real etcd. -#### 2021-12-27 -version: github.com/huaweicloud/devcloud-go v0.1.0 -feature: -1. add dms package, which is a high performance and high reliability kafka consumer. -2. add mock package, which can mock redis, mysql and interface. #### 2021-12-25 1. dms: persist the first N continuous offsets in offsetNode to the database and kafka broker, this will reduce repeated consumption of messages. 2. change dms/method.go BizHandler from interface to function types. @@ -15,7 +19,7 @@ feature: 1. add mock package, which contains interface mock, redis mock and mysql mock. #### 2021-12-16 -version: github.com/huaweicloud/devcloud-go v0.0.1 +version: github.com/huaweicloud/devcloud-go v0.0.1 feature: 1. add dms which is a kafka consumer. diff --git a/README.md b/README.md index 2c23975..856dcf3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Devcloud-go Devcloud-go provides a sql-driver for mysql, a redis client and a kafka consumer which named dms, you can use the driver and redis client with MAS or use them separately, +at the same time, they also support use cases with injection faults for scenario simulation. The driver is developed based on [go-sql-driver/mysql v1.6.0](https://github.com/go-sql-driver/mysql), the redis client is developed based on [go-redis v8.11.3](https://github.com/go-redis/redis). The kafka consumer is developed based on [github.com/Shopify/sarama v1.29.1](https://github.com/Shopify/sarama). +The mock package provides the simulation of MySQL, redis and etcd services, and realizes the fault injection function of MySQL and redis through TCP. This document introduces how to obtain and use Devcloud-go. ## Requirements @@ -19,6 +21,7 @@ go get github.com/huaweicloud/devcloud-go * **sql-driver** : see details in [sql-driver/mysql/README.md](sql-driver/mysql/README.md) * **redis** : see details in [redis/README.md](redis/README.md) * **dms**: see details in [dms/README.md](dms/README.md) +* **mock**: see details in [mock/README.md](mock/README.md) ## ChangeLog Detailed changes for each released version are documented in the [CHANGELOG.md](CHANGELOG.md). diff --git a/common/etcd/client.go b/common/etcd/client.go index 8cfcacf..368b233 100644 --- a/common/etcd/client.go +++ b/common/etcd/client.go @@ -11,10 +11,12 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package etcd defines EtcdClient interface, and use "go.etcd.io/etcd/client/v3" - * implements the interface. */ +/* +Package etcd defines EtcdClient interface, and use "go.etcd.io/etcd/client/v3" +implements the interface. +*/ package etcd import ( diff --git a/common/etcd/mocks/EtcdClient.go b/common/etcd/mocks/EtcdClient.go index 0fcbc1c..93bd42b 100644 --- a/common/etcd/mocks/EtcdClient.go +++ b/common/etcd/mocks/EtcdClient.go @@ -1,18 +1,4 @@ -/* - * Copyright (c) Huawei Technologies Co., Ltd. 2021. - * Licensed 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 mockery v1.0.0. DO NOT EDIT. - */ +// Code generated by mockery v1.0.0. DO NOT EDIT. package mocks diff --git a/common/password/decipher.go b/common/password/decipher.go index 4af8e07..9fa6bdc 100644 --- a/common/password/decipher.go +++ b/common/password/decipher.go @@ -15,6 +15,10 @@ * user can set customize decipher by SetDecipher function. */ +/* +Package password defines Decipher interface, which is used to decode password, +user can set customize decipher by SetDecipher function. +*/ package password import "sync" diff --git a/common/util/util.go b/common/util/util.go index 95ca6c0..eeac095 100644 --- a/common/util/util.go +++ b/common/util/util.go @@ -11,9 +11,9 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package util provides some util function, such as ValidateHostPort. */ +// Package util provides some util function, such as ValidateHostPort. package util import ( diff --git a/dms/consumer.go b/dms/consumer.go index 52c788b..26b39c8 100644 --- a/dms/consumer.go +++ b/dms/consumer.go @@ -11,10 +11,12 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package dms implements a kafka consumer based on sarama, user can consume messages - * asynchronous or synchronous with dms, and ensure message not lost. */ +/* +Package dms implements a kafka consumer based on sarama, user can consume messages +asynchronous or synchronous with dms, and ensure message not lost. +*/ package dms import ( diff --git a/dms/example/example.go b/dms/example/example.go index 23f9dfb..5a5b412 100644 --- a/dms/example/example.go +++ b/dms/example/example.go @@ -11,9 +11,9 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package example provides an example for user how to use dms. */ +// Package example provides an example for user how to use dms. package example import ( diff --git a/dms/offset_manager.go b/dms/offset_manager.go index 187ed83..10c8150 100644 --- a/dms/offset_manager.go +++ b/dms/offset_manager.go @@ -115,7 +115,7 @@ func (m *OffsetManager) handleOffsetOnCleanUp(offsetPersist OffsetPersist) int64 m.lock.RUnlock() offset := minKey.(int64) + m.startOffset + int64(minNode.(*OffsetNode).maxContinuous()) if err := offsetPersist.Save(m.groupId, m.topic, m.partition, offset); err != nil { - log.Printf("WARNING: groupId/topic/partition %s/%s/%s persist %d fail on clean up, %v", + log.Printf("WARNING: groupId/topic/partition %s/%s/%d persist %d fail on clean up, %v", m.groupId, m.topic, m.partition, offset, err) } return offset diff --git a/go.mod b/go.mod index f1d53ef..22dd42f 100644 --- a/go.mod +++ b/go.mod @@ -6,19 +6,26 @@ require ( github.com/RoaringBitmap/roaring v0.9.4 github.com/Shopify/sarama v1.29.1 github.com/alicebob/miniredis/v2 v2.15.1 + github.com/astaxie/beego v1.12.3 github.com/bwmarrin/snowflake v0.3.0 github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/dolthub/go-mysql-server v0.11.0 + github.com/dolthub/vitess v0.0.0-20211013185428-a8845fb919c1 github.com/emirpasic/gods v1.12.0 + github.com/gin-gonic/gin v1.7.7 github.com/go-redis/redis/v8 v8.11.3 github.com/go-sql-driver/mysql v1.6.0 github.com/onsi/ginkgo v1.16.4 github.com/onsi/gomega v1.15.0 - github.com/panjf2000/ants/v2 v2.4.7 + github.com/panjf2000/ants/v2 v2.4.6 github.com/stretchr/testify v1.7.0 + github.com/tidwall/redcon v1.4.4 go.etcd.io/etcd/api/v3 v3.5.1 go.etcd.io/etcd/client/v3 v3.5.1 go.etcd.io/etcd/server/v3 v3.5.1 - golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 + golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba + gopkg.in/fatih/pool.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gorm.io/driver/mysql v1.3.2 + gorm.io/gorm v1.23.1 ) diff --git a/img/mysql-configuration.png b/img/mysql-configuration.png new file mode 100644 index 0000000..7b9f697 Binary files /dev/null and b/img/mysql-configuration.png differ diff --git a/img/mysql-local-read-single-write.png b/img/mysql-local-read-single-write.png new file mode 100644 index 0000000..60baf62 Binary files /dev/null and b/img/mysql-local-read-single-write.png differ diff --git a/img/mysql-single-read-write.png b/img/mysql-single-read-write.png new file mode 100644 index 0000000..cf51f0e Binary files /dev/null and b/img/mysql-single-read-write.png differ diff --git a/img/proxy.png b/img/proxy.png new file mode 100644 index 0000000..86f5e1f Binary files /dev/null and b/img/proxy.png differ diff --git a/img/redis-configuration.png b/img/redis-configuration.png new file mode 100644 index 0000000..b0d7a8d Binary files /dev/null and b/img/redis-configuration.png differ diff --git a/img/redis-double-write.png b/img/redis-double-write.png new file mode 100644 index 0000000..6645e2e Binary files /dev/null and b/img/redis-double-write.png differ diff --git a/img/redis-local-read-single-write.png b/img/redis-local-read-single-write.png new file mode 100644 index 0000000..8322fc2 Binary files /dev/null and b/img/redis-local-read-single-write.png differ diff --git a/img/redis-single-read-write.png b/img/redis-single-read-write.png new file mode 100644 index 0000000..d491be5 Binary files /dev/null and b/img/redis-single-read-write.png differ diff --git a/mas/config.go b/mas/config.go index cec1850..2618517 100644 --- a/mas/config.go +++ b/mas/config.go @@ -11,23 +11,22 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package mas contains mas properties configuration. */ +// Package mas contains mas properties configuration. package mas import "fmt" // PropertiesConfiguration yaml properties configuration entity type PropertiesConfiguration struct { - Version string `yaml:"version"` - AppID string `yaml:"appId"` - MonitorID string `yaml:"monitorId"` - DatabaseName string `yaml:"databaseName"` - DecipherClassName string `yaml:"decipherClassName"` - Cloud string `yaml:"cloud"` - Region string `yaml:"region"` - Azs string `yaml:"azs"` + Version string `yaml:"version"` + AppID string `yaml:"appId"` + MonitorID string `yaml:"monitorId"` + DatabaseName string `yaml:"databaseName"` + Cloud string `yaml:"cloud"` + Region string `yaml:"region"` + Azs string `yaml:"azs"` } const propertiesConfigurationHashCodeFmt = "%s_%s_%s_%s" diff --git a/mas/config_test.go b/mas/config_test.go index 10001ee..3f4cb0a 100644 --- a/mas/config_test.go +++ b/mas/config_test.go @@ -44,12 +44,11 @@ func TestPropertiesConfiguration_CalHashCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &PropertiesConfiguration{ - Version: tt.fields.Version, - AppID: tt.fields.AppID, - MonitorID: tt.fields.MonitorID, - DatabaseName: tt.fields.DatabaseName, - DecipherClassName: tt.fields.DecipherClassName, - Region: tt.fields.Region, + Version: tt.fields.Version, + AppID: tt.fields.AppID, + MonitorID: tt.fields.MonitorID, + DatabaseName: tt.fields.DatabaseName, + Region: tt.fields.Region, } if got := p.CalHashCode(); got != tt.want { t.Errorf("CalHashCode() = %v, want %v", got, tt.want) diff --git a/mas/injection.go b/mas/injection.go new file mode 100644 index 0000000..4d4610b --- /dev/null +++ b/mas/injection.go @@ -0,0 +1,191 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 mas + +import ( + "math/rand" + "time" +) + +// InjectionProperties chaos configuration +type InjectionProperties struct { + Active bool `yaml:"active"` + Duration int `yaml:"duration"` + Interval int `yaml:"interval"` + Percentage int `yaml:"percentage"` + DelayInjection *DelayInjection `yaml:"delayInjection"` + ErrorInjection *ErrorInjection `yaml:"errorInjection"` +} + +// DelayInjection delay configuration +type DelayInjection struct { + Active bool `yaml:"active"` + Percentage int `yaml:"percentage"` + TimeMs int `yaml:"timeMs"` + JitterMs int `yaml:"jitterMs"` +} + +// NewDelayInjection sda +func NewDelayInjection(active bool, percentage, timeMs, jitterMs int) *DelayInjection { + return &DelayInjection{ + Active: active, + Percentage: percentage, + TimeMs: timeMs, + JitterMs: jitterMs, + } +} + +func (d *DelayInjection) checkActive() (int, bool) { + if d.Active { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + if r.Intn(100) <= d.Percentage { + jitterMs := r.Intn(2*d.JitterMs+1) - d.JitterMs + if d.TimeMs+jitterMs < 0 { + return 0, true + } + return d.TimeMs + jitterMs, true + } + } + return 0, false +} + +// InjectionError error details +type InjectionError struct { + Err error + Percentage int +} + +// ErrorInjection error configuration +type ErrorInjection struct { + Active bool `yaml:"active"` + Percentage int `yaml:"percentage"` + errs []*InjectionError +} + +func NewErrorInjection(active bool, percentage int) *ErrorInjection { + return &ErrorInjection{ + Active: active, + Percentage: percentage, + errs: make([]*InjectionError, 0), + } +} + +func (e *ErrorInjection) checkActive() (error, bool) { + if e.Active { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + if r.Intn(100) <= e.Percentage { + return e.errs[r.Intn(len(e.errs))].Err, true + } + } + return nil, false +} + +// InjectionDuration ingestion period details +type InjectionDuration struct { + duration int + total int + startTimeMs int64 +} + +func NewInjectionDuration(duration, total int) *InjectionDuration { + return &InjectionDuration{ + duration: duration, + total: total, + startTimeMs: time.Now().Unix(), + } +} + +func (i *InjectionDuration) checkActive() bool { + return (time.Now().Unix()-i.startTimeMs)%int64(i.total) <= int64(i.duration) +} + +// InjectionManagement chaos injection details +type InjectionManagement struct { + active bool + injectionDuration *InjectionDuration + percentage int + delayInjection *DelayInjection + errorInjection *ErrorInjection +} + +func CompliancePercentage(percentage int) int { + if percentage < 0 { + return 0 + } + if percentage > 100 { + return 100 + } + return percentage +} + +func NewInjectionManagement(chaos *InjectionProperties) *InjectionManagement { + return &InjectionManagement{ + active: chaos.Active, + injectionDuration: NewInjectionDuration(chaos.Duration, chaos.Interval), + percentage: CompliancePercentage(chaos.Percentage), + delayInjection: NewDelayInjection(chaos.DelayInjection.Active, + CompliancePercentage(chaos.DelayInjection.Percentage), + chaos.DelayInjection.TimeMs, + chaos.DelayInjection.JitterMs), + errorInjection: NewErrorInjection(chaos.ErrorInjection.Active, + CompliancePercentage(chaos.ErrorInjection.Percentage)), + } +} + +func (i *InjectionManagement) SetError(errs []error) { + if i.errorInjection.Active { + if errs != nil && len(errs) > 0 { + for _, err := range errs { + i.errorInjection.errs = append(i.errorInjection.errs, + &InjectionError{err, i.errorInjection.Percentage}) + } + } + } +} + +func (i *InjectionManagement) AddError(errs []*InjectionError) { + if i.errorInjection.Active { + if errs != nil && len(errs) > 0 { + for _, err := range errs { + err.Percentage = CompliancePercentage(err.Percentage) + } + i.errorInjection.errs = append(i.errorInjection.errs, errs...) + } + } +} + +// Inject chaos injection triggering +func (i *InjectionManagement) Inject() error { + if i == nil { + return nil + } + if !i.active { + return nil + } + if !i.injectionDuration.checkActive() { + return nil + } + r := rand.New(rand.NewSource(time.Now().UnixNano())) + if r.Intn(100) <= i.percentage { + if err, active := i.errorInjection.checkActive(); active { + return err + } + if delay, active := i.delayInjection.checkActive(); active { + time.Sleep(time.Millisecond * time.Duration(delay)) + } + } + return nil +} diff --git a/mas/injection_error.go b/mas/injection_error.go new file mode 100644 index 0000000..5585b46 --- /dev/null +++ b/mas/injection_error.go @@ -0,0 +1,76 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 mas + +import ( + "io" + "syscall" + + "github.com/dolthub/vitess/go/mysql" +) + +// RuntimeError +type errorString string + +func (e errorString) RuntimeError() {} + +func (e errorString) Error() string { + return "runtime error: " + string(e) +} + +// RedisError redisError +type RedisError string + +func (e RedisError) Error() string { return string(e) } + +func (RedisError) RedisError() {} + +var ( + SocketErr = syscall.Errno(10061) + IOErr1 = io.ErrClosedPipe + IOErr2 = io.ErrUnexpectedEOF + NilPointerErr = errorString("invalid memory address or nil pointer dereference") + + SQLErr = mysql.NewSQLError(mysql.CRUnknownError, mysql.SSUnknownSQLState, "SQLErr") + SQLTimeoutErr = mysql.NewSQLError(mysql.CRServerGone, mysql.SSUnknownSQLState, "SQLTimeoutErr") + + RedisCommandUKErr = RedisError("ERR unknown command") + RedisCommandArgErr = RedisError("ERR wrong number of arguments for command") +) + +func MysqlErrors() []error { + errs := []error{ + SocketErr, + IOErr1, + IOErr2, + NilPointerErr, + SQLErr, + SQLTimeoutErr, + } + return errs +} + +func RedisErrors() []error { + errs := []error{ + SocketErr, + IOErr1, + IOErr2, + NilPointerErr, + RedisCommandUKErr, + RedisCommandArgErr, + } + return errs +} diff --git a/mas/test/mysql_mas_test.go b/mas/test/mysql_mas_test.go new file mode 100644 index 0000000..398427b --- /dev/null +++ b/mas/test/mysql_mas_test.go @@ -0,0 +1,112 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 test contains mas test cases +package test + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "testing" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/huaweicloud/devcloud-go/mock/proxy/utils" + devspore "github.com/huaweicloud/devcloud-go/sql-driver/mysql" +) + +var ( + etcdAddrs = []string{"127.0.0.1:2379"} + dataDir = "etcd_data" + mysqlAddrs = []string{"127.0.0.1:13306", "127.0.0.1:13307"} + clusterConfiguration = utils.DCMysql(etcdAddrs, mysqlAddrs) + datasource = "/mas-monitor/conf/db/services/" + clusterConfiguration.Props.AppID + "/" + clusterConfiguration.Props.MonitorID + "/database/" + clusterConfiguration.Props.DatabaseName + "/datasource" + router = "/mas-monitor/conf/db/services/" + clusterConfiguration.Props.AppID + "/" + clusterConfiguration.Props.MonitorID + "/database/" + clusterConfiguration.Props.DatabaseName + "/router" + activekey1 = "/mas-monitor/status/db/services/" + clusterConfiguration.Props.AppID + "/" + clusterConfiguration.Props.MonitorID + "/database/" + clusterConfiguration.Props.DatabaseName + "/active" +) + +func TestMysqlMas(t *testing.T) { + utils.Start2MysqlMock(mysqlAddrs) + defer utils.Stop2MysqlMock() + utils.StartEtcdMock(etcdAddrs, dataDir) + defer utils.StopEtcdMock(dataDir) + + client, err := clientv3.New(clientv3.Config{Endpoints: etcdAddrs, Username: "root", Password: "root"}) + if err != nil { + log.Fatalln(err) + } + defer func() { + err = client.Close() + if err != nil { + log.Println(err) + } + }() + routerConfigStr, err := json.Marshal(clusterConfiguration.RouterConfig) + if err != nil { + fmt.Println(err) + } + datasourceStr, err := json.Marshal(clusterConfiguration.DataSource) + if err != nil { + fmt.Println(err) + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + _, err = client.Put(ctx, datasource, string(datasourceStr), clientv3.WithPrevKV()) + if err != nil { + log.Println(err) + } + _, err = client.Put(ctx, router, string(routerConfigStr), clientv3.WithPrevKV()) + if err != nil { + log.Println(err) + } + cancel() + devspore.SetClusterConfiguration(clusterConfiguration) + db, err := sql.Open("devspore_mysql", "") + if err != nil { + fmt.Println(err) + } + defer func() { + err = db.Close() + if err != nil { + log.Println(err) + } + }() + time.Sleep(time.Second) + nodeTest(client, db) +} + +func nodeTest(client *clientv3.Client, db *sql.DB) { + for key := range clusterConfiguration.RouterConfig.Nodes { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + _, err := client.Put(ctx, activekey1, key, clientv3.WithPrevKV()) + if err != nil { + log.Println(err) + } + cancel() + time.Sleep(time.Second) + for i := 0; i < 10; i++ { + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + fmt.Println(name) + time.Sleep(time.Second) + } + } +} diff --git a/mas/test/redis_mas_test.go b/mas/test/redis_mas_test.go new file mode 100644 index 0000000..5c6cec1 --- /dev/null +++ b/mas/test/redis_mas_test.go @@ -0,0 +1,78 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/huaweicloud/devcloud-go/mock/proxy/utils" + "github.com/huaweicloud/devcloud-go/redis" +) + +func TestRedisMas(t *testing.T) { + var ( + etcdAddrs = []string{"127.0.0.1:2379"} + dataDir = "etcd_data" + redisAddrs = []string{"127.0.0.1:16379", "127.0.0.1:16380"} + configuration = utils.DCRedis(etcdAddrs, redisAddrs) + servers = "/mas-monitor/conf/dcs/services/" + configuration.Props.AppID + "/" + configuration.Props.MonitorID + "/servers" + algorithm = "/mas-monitor/conf/dcs/services/" + configuration.Props.AppID + "/" + configuration.Props.MonitorID + "/route-algorithm" + activekey = "/mas-monitor/status/dcs/services/" + configuration.Props.AppID + "/" + configuration.Props.MonitorID + "/active" + ) + + utils.Start2RedisMock(redisAddrs) + defer utils.Stop2RedisMock() + utils.StartEtcdMock(etcdAddrs, dataDir) + defer utils.StopEtcdMock(dataDir) + + client, _ := clientv3.New(clientv3.Config{Endpoints: etcdAddrs, Username: "root", Password: "root"}) + defer func() { + _ = client.Close() + }() + serversStr, err := json.Marshal(configuration.RedisConfig.Servers) + if err != nil { + fmt.Println(err) + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + _, _ = client.Put(ctx, servers, string(serversStr), clientv3.WithPrevKV()) + _, _ = client.Put(ctx, algorithm, configuration.RouteAlgorithm, clientv3.WithPrevKV()) + cancel() + + redisClient := redis.NewDevsporeClient(configuration) + defer func() { + _ = redisClient.Close() + }() + time.Sleep(time.Second) + + for key := range configuration.RedisConfig.Servers { + ctx, cancel = context.WithTimeout(context.Background(), time.Second) + client.Put(ctx, activekey, key, clientv3.WithPrevKV()) + cancel() + time.Sleep(time.Second) + for i := 0; i < 10; i++ { + ctx = context.Background() + res := redisClient.Get(ctx, "key") + fmt.Println(res) + time.Sleep(time.Second) + } + } +} diff --git a/mock/README.md b/mock/README.md new file mode 100644 index 0000000..c5dc491 --- /dev/null +++ b/mock/README.md @@ -0,0 +1,220 @@ +# devcloud-go/mock + +### Quickstart: + +##### etcd + +```bigquery +import ( + "context" + "fmt" + "log" + "os" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" + "github.com/huaweicloud/devcloud-go/mock" +) + +func main() { + addrs := []string{"127.0.0.1:2382"} + dataDir := "etcd_data" + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + log.Println("ERROR: remove data dir failed, %v", err) + } + }(dataDir) + metadata := mock.NewEtcdMetadata() + metadata.ClientAddrs = addrs + metadata.DataDir = dataDir + mockEtcd := &mock.MockEtcd{} + mockEtcd.StartMockEtcd(metadata) + defer mockEtcd.StopMockEtcd() + + client, err := clientv3.New(clientv3.Config{Endpoints: addrs, Username: "root", Password: "root"}) + defer func(client *clientv3.Client) { + err = client.Close() + if err != nil { + log.Println("ERROR: close client failed, %v", err) + } + }(client) + + key := "key" + val := "val" + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + client.Put(ctx, key, val, clientv3.WithPrevKV()) + cancel() + + ctx, cancel = context.WithTimeout(context.Background(), time.Second) + resp, _ := client.Get(ctx, key) + cancel() + fmt.Println(string(resp.Kvs[0].Value)) +} + +``` + +##### mysql + +```bigquery +import ( + "database/sql" + "fmt" + "log" + "time" + + "github.com/dolthub/go-mysql-server/memory" + mocksql "github.com/dolthub/go-mysql-server/sql" + _ "github.com/go-sql-driver/mysql" + "github.com/huaweicloud/devcloud-go/mock" +) + +func main() { + metadata := mock.MysqlMock{ + User: "root", + Password: "root", + Address: "127.0.0.1:3318", + Databases: []string{"mydb"}, + MemDatabases: []*memory.Database{createTestDatabase("mydb", "user")}, + } + metadata.StartMockMysql() + defer metadata.StopMockMysql() + + db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3318)/mydb") + defer db.Close() + if err != nil { + log.Println(err) + return + } + var name, email string + err = db.QueryRow("SELECT name, email FROM user WHERE id=?", 1).Scan(&name, &email) + fmt.Println(name, email) +} + +func createTestDatabase(dbName, tableName string) *memory.Database { + db := memory.NewDatabase(dbName) + table := memory.NewTable(tableName, mocksql.Schema{ + {Name: "id", Type: mocksql.Int64, Nullable: false, AutoIncrement: true, PrimaryKey: true, Source: tableName}, + {Name: "name", Type: mocksql.Text, Nullable: false, Source: tableName}, + {Name: "email", Type: mocksql.Text, Nullable: false, Source: tableName}, + {Name: "phone_numbers", Type: mocksql.JSON, Nullable: false, Source: tableName}, + {Name: "created_at", Type: mocksql.Timestamp, Nullable: false, Source: tableName}, + }) + + db.AddTable(tableName, table) + ctx := mocksql.NewEmptyContext() + + rows := []mocksql.Row{ + mocksql.NewRow(1, "John Doe", "jasonkay@doe.com", []string{"555-555-555"}, time.Now()), + mocksql.NewRow(2, "John Doe", "johnalt@doe.com", []string{}, time.Now()), + mocksql.NewRow(3, "Jane Doe", "jane@doe.com", []string{}, time.Now()), + mocksql.NewRow(4, "Evil Bob", "jasonkay@gmail.com", []string{"555-666-555", "666-666-666"}, time.Now()), + } + + for _, row := range rows { + _ = table.Insert(ctx, row) + } + return db +} +``` + +##### redis + +```bigquery +import ( + "context" + "fmt" + + goredis "github.com/go-redis/redis/v8" + "github.com/huaweicloud/devcloud-go/mock" +) + +func main() { + redisMock := mock.RedisMock{Addr: "127.0.0.1:16379"} + redisMock.StartMockRedis() + defer redisMock.StopMockRedis() + cluster := goredis.NewClusterClient(&goredis.ClusterOptions{ + Addrs: []string{"127.0.0.1:16379"}, + }) + + ctx := context.Background() + cluster.Set(ctx, "key", "val", 0) + res := cluster.Get(ctx, "key") + fmt.Println(res.Val()) +} +``` + +### Fault injection +Fault injection through TCP proxy + +![image](../img/proxy.png) + +Faults such as delay, fluctuation, disconnection, null value, and error can be injected. + +##### Delay +```bigquery +func (p *Proxy) AddDelay(name string, delay, percentage int, clientAddr, command string) error +``` +Add a delay fault named name, filter the clientAddr trustlist, intercept command commands, set the delay time to delay, and set the trigger probability to percentage. + +##### Jitter +```bigquery +func (p *Proxy) AddJitter(name string, jitter, percentage int, clientAddr, command string) error +``` +Add a fluctuating fault named name, filter the clientAddr trustlist, intercept command commands, set the fluctuating duration to jitter, and set the triggering probability to percentage. + +##### Drop +```bigquery +func (p *Proxy) AddDrop(name string, percentage int, clientAddr, command string) error +``` +Add a disconnection fault named name. The fault filters out the clientAddr trustlist and intercepts command commands. The triggering probability is percentage. + +##### ReturnEmpty +```bigquery +func (p *Proxy) AddReturnEmpty(name string, percentage int, clientAddr, command string) error +``` +Add a null fault named name. The fault filters the clientAddr trustlist and intercepts command commands. The triggering probability is percentage. + +##### ReturnErr +```bigquery +func (p *Proxy) AddReturnErr(name string, returnErr error, percentage int, clientAddr, command string) error +``` +Add an error fault named name, filter the clientAddr trustlist, intercept command commands, and set the fault information to returnErr and trigger probability to percentage. + +##### Redis is used as an example. +```bigquery +import ( + "context" + "fmt" + + proxyredis "github.com/huaweicloud/devcloud-go/mock/proxy/proxy-redis" + goredis "github.com/go-redis/redis/v8" + "github.com/huaweicloud/devcloud-go/mock" +) + +func main() { + redisMock := mock.RedisMock{Addr: "127.0.0.1:16379"} + redisMock.StartMockRedis() + defer redisMock.StopMockRedis() + redisProxy := proxyredis.NewProxy(redisMock.Addr, "127.0.0.1:26379") + redisProxy.StartProxy() + defer redisProxy.StopProxy() + client := goredis.NewClient(&goredis.Options{Addr: "127.0.0.1:26379"}) + ctx := context.Background() + client.Set(ctx, "key", "val", 0) + time1 := time.Now().Unix() / 1e6 + res1 := client.Get(ctx, "key") + time2 := time.Now().Unix() / 1e6 + fmt.Println(res1.Val(), time2-time1) + redisProxy.AddDelay("delay", 1500, 0, "", "") + //redisProxy.AddJitter("jitter", 3500, 0, "", "") + //redisProxy.AddDrop("drop", 0, "", "") + //redisProxy.AddReturnEmpty("returnEmpty", 0, "", "") + //redisProxy.AddReturnErr("returnErr", proxyredis.UnknownError, 0, "", "") + time3 := time.Now().UnixNano() / 1e6 + res2 := client.Get(ctx, "key") + time4 := time.Now().UnixNano() / 1e6 + fmt.Println(res2.Val(), time4-time3) +} +``` \ No newline at end of file diff --git a/mock/common.go b/mock/common.go index 7cc98f4..f62f88b 100644 --- a/mock/common.go +++ b/mock/common.go @@ -11,9 +11,9 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * package mock introduce several mock methods for interface, mysql, redis and etcd. */ +// Package mock introduce three mock methods for interface, mysql and redis. package mock // use https://github.com/stretchr/testify and https://github.com/vektra/mockery to mock interface, diff --git a/mock/etcd.go b/mock/etcd.go index 3eced55..53fa5fe 100644 --- a/mock/etcd.go +++ b/mock/etcd.go @@ -35,33 +35,32 @@ func (m *MockEtcd) StartMockEtcd(metadata *EtcdMetadata) { cfg.LPUrls = convertAddrsToUrls(metadata.PeerAddrs) cfg.Dir = metadata.DataDir cfg.LogLevel = "warn" + m.cfg = cfg e, err := embed.StartEtcd(cfg) if err != nil { log.Printf("ERROR: start embed etcd failed, %v", err) return } - m.e = e if metadata.AuthEnable { m.initRootRole() m.AddUser(defaultUser, defaultPassword) err = m.e.Server.AuthStore().AuthEnable() if err != nil { - log.Printf("ERROR: enable auth failed, %v", e) + log.Printf("ERROR: enable auth failed, %v", err) return } - if len(metadata.UserName) > 0 && len(metadata.Password) > 0 { - m.AddUser(metadata.UserName, metadata.Password) - } } - + if len(metadata.UserName) > 0 && len(metadata.Password) > 0 { + m.AddUser(metadata.UserName, metadata.Password) + } select { case <-m.e.Server.ReadyNotify(): - log.Println("Start mock etcd!") - case <-time.After(10 * time.Second): - m.e.Server.Stop() - log.Println("Etcd server take too long to start!") + log.Printf("Start mock etcd!") + case <-time.After(60 * time.Second): + m.e.Server.Stop() // trigger a shutdown + log.Printf("Server took too long to start!") } } @@ -75,15 +74,16 @@ func (m *MockEtcd) initRootRole() { authStore := m.e.Server.AuthStore() _, err := authStore.RoleAdd(&pb.AuthRoleAddRequest{Name: defaultRole}) if err != nil { - log.Printf("ERROR: add 'root' role failed, %v", err) + log.Printf("ERROR: add 'root' role failed, %+v", err) } _, err = authStore.RoleGrantPermission(&pb.AuthRoleGrantPermissionRequest{ Name: defaultRole, - Perm: &authpb.Permission{PermType: 2}, - }) + Perm: &authpb.Permission{ + PermType: 2, + }}) if err != nil { - log.Printf("ERROR: RoleGrantPermission failed, %v", err) + log.Printf("ERROR: RoleGrantPermission failed, %+v", err) } } @@ -107,7 +107,7 @@ func (m *MockEtcd) AddUser(user, password string) { type EtcdMetadata struct { ClientAddrs []string - PeerAddrs []string + PeerAddrs []string // optional AuthEnable bool UserName string Password string @@ -126,8 +126,11 @@ func NewEtcdMetadata() *EtcdMetadata { func convertAddrsToUrls(addrs []string) []url.URL { var urls []url.URL for _, addr := range addrs { - url, _ := url.Parse("http://" + addr) - urls = append(urls, *url) + tUrl, err := url.Parse("http://" + addr) + if err != nil { + log.Println(err) + } + urls = append(urls, *tUrl) } return urls } diff --git a/mock/etcd_test.go b/mock/etcd_test.go index e8c5f50..501cd47 100644 --- a/mock/etcd_test.go +++ b/mock/etcd_test.go @@ -9,7 +9,8 @@ * 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. + * specific language governing permissions and limitations under the License. + * */ package mock @@ -31,7 +32,7 @@ func TestMockEtcd(t *testing.T) { defer func(path string) { err := os.RemoveAll(path) if err != nil { - t.Errorf("remove data dir failed, %v", err) + t.Errorf("ERROR: remove data dir failed, %v", err) } }(dataDir) metadata := NewEtcdMetadata() @@ -46,22 +47,22 @@ func TestMockEtcd(t *testing.T) { defer func(client *clientv3.Client) { err = client.Close() if err != nil { - t.Errorf("close client failed, %v", err) + t.Errorf("ERROR: close client failed, %v", err) } }(client) - key := "key" - val := "Val" + testKey := "key" + testVal := "val" ctx, cancel := context.WithTimeout(context.Background(), time.Second) - _, err = client.Put(ctx, key, val, clientv3.WithPrevKV()) + _, err = client.Put(ctx, testKey, testVal, clientv3.WithPrevKV()) cancel() assert.Nil(t, err) ctx, cancel = context.WithTimeout(context.Background(), time.Second) - resp, err := client.Get(ctx, key) + resp, err := client.Get(ctx, testKey) cancel() assert.Nil(t, err) assert.Equal(t, int64(1), resp.Count) - assert.Equal(t, val, string(resp.Kvs[0].Value)) + assert.Equal(t, testVal, string(resp.Kvs[0].Value)) } diff --git a/mock/mysql.go b/mock/mysql.go index 0460266..52e5135 100644 --- a/mock/mysql.go +++ b/mock/mysql.go @@ -16,7 +16,6 @@ package mock import ( - "fmt" "log" sqle "github.com/dolthub/go-mysql-server" @@ -27,56 +26,48 @@ import ( "github.com/dolthub/go-mysql-server/sql/information_schema" ) -type MysqlMetaData struct { +type MysqlMock struct { User string Password string Address string Databases []string MemDatabases []*memory.Database // see mysql_test.go Line#53-77 + mysqlServer *server.Server } -var mysqlServers []*server.Server - -func StartMockMysql(metadata MysqlMetaData) { +func (m *MysqlMock) StartMockMysql() error { var databases = []sql.Database{information_schema.NewInformationSchemaDatabase()} - for _, db := range metadata.Databases { + for _, db := range m.Databases { databases = append(databases, memory.NewDatabase(db)) } - for _, db := range metadata.MemDatabases { + for _, db := range m.MemDatabases { databases = append(databases, db) } engine := sqle.NewDefault(sql.NewDatabaseProvider(databases...)) config := server.Config{ Protocol: "tcp", - Address: metadata.Address, - Auth: auth.NewNativeSingle(metadata.User, metadata.Password, auth.AllPermissions), + Address: m.Address, + Auth: auth.NewNativeSingle(m.User, m.Password, auth.AllPermissions), } - s, err := server.NewDefaultServer(config, engine) + var err error + m.mysqlServer, err = server.NewDefaultServer(config, engine) if err != nil { log.Printf("ERROR: create mysql server failed, %v", err) - return + return err } go func() { - err = s.Start() + err = m.mysqlServer.Start() if err != nil { log.Printf("ERROR: start mysql server failed, %v", err) return } }() - fmt.Println("mysql-server started!") - mysqlServers = append(mysqlServers, s) - return + log.Println("mysql-server started!") + return nil } -func StopMockMysql() { - if len(mysqlServers) == 0 { - return - } - for _, s := range mysqlServers { - if err := s.Close(); err != nil { - log.Printf("ERROR: close mysql server [%s] failed, %v", s.Listener.Addr(), err) - } - } - fmt.Println("mysql-server stop!") +func (m *MysqlMock) StopMockMysql() { + m.mysqlServer.Close() + log.Println("mysql-server stop!") } diff --git a/mock/mysql_test.go b/mock/mysql_test.go index 7528f9d..bccebb8 100644 --- a/mock/mysql_test.go +++ b/mock/mysql_test.go @@ -17,6 +17,7 @@ package mock import ( "database/sql" + "log" "testing" "time" @@ -27,25 +28,35 @@ import ( ) func TestMysqlMock(t *testing.T) { - metadata := MysqlMetaData{ + metadata := MysqlMock{ User: "root", Password: "root", Address: "127.0.0.1:3318", Databases: []string{"mydb"}, MemDatabases: []*memory.Database{createTestDatabase("mydb", "user")}, } - StartMockMysql(metadata) - defer StopMockMysql() + err := metadata.StartMockMysql() + if err != nil { + log.Fatalln(err) + } + defer metadata.StopMockMysql() db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3318)/mydb") - defer db.Close() + defer func() { + err = db.Close() + if err != nil { + log.Println(err) + } + }() if err != nil { t.Error(err) return } var name, email string err = db.QueryRow("SELECT name, email FROM user WHERE id=?", 1).Scan(&name, &email) - assert.Nil(t, err) + if err != nil { + log.Println(err) + } assert.Equal(t, name, "John Doe") assert.Equal(t, email, "jasonkay@doe.com") } @@ -71,7 +82,8 @@ func createTestDatabase(dbName, tableName string) *memory.Database { } for _, row := range rows { - _ = table.Insert(ctx, row) + err := table.Insert(ctx, row) + log.Println(err) } return db } diff --git a/mock/proxy/plan.go b/mock/proxy/plan.go new file mode 100644 index 0000000..21cd9dc --- /dev/null +++ b/mock/proxy/plan.go @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2017, redfi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * 2022.01.20-Adapt to Redis and MySQL fault injection, delete unnecessary functions + * add required logic. + * Huawei Technologies Co., Ltd. + */ + +package proxy + +import ( + "bytes" + "errors" + "fmt" + "math/rand" + "strings" + "sync" + "sync/atomic" + "time" +) + +var ( + // ErrNotFound is returned iff SelectRule can't find a Rule that applies + ErrNotFound = errors.New("no matching rule found") +) + +func marshalCommand(cmd string) []byte { + return []byte(cmd) +} + +type Plan struct { + rulesMap sync.Map +} + +func (p *Plan) AddRule(r Rule) error { + if r.Percentage < 0 || r.Percentage > 100 { + return fmt.Errorf("percentage in rule #%s is malformed. it must within 0-100", r.Name) + } + if len(r.Name) <= 0 { + return fmt.Errorf("name of rule is required") + } + if len(r.Command) > 0 { + r.marshaledCmd = marshalCommand(r.Command) + } + if _, ok := p.rulesMap.Load(r.Name); ok { + return fmt.Errorf("a rule by the same name exists") + } + p.rulesMap.Store(r.Name, &r) + return nil +} + +func (p *Plan) SelectRule(clientAddr string, buf []byte) *Rule { + var chosenRule *Rule + p.rulesMap.Range(func(key, value interface{}) bool { + rule, ok := value.(*Rule) + if !ok { + return true + } + if len(rule.ClientAddr) > 0 && strings.HasPrefix(clientAddr, rule.ClientAddr) { + return true + } + + if len(rule.Command) > 0 && !bytes.Contains(buf, rule.marshaledCmd) { + return true + } + + chosenRule = rule + return false + }) + if chosenRule == nil { + return nil + } + r := rand.New(rand.NewSource(time.Now().UnixNano())) + if chosenRule.Percentage > 0 && r.Intn(100) > chosenRule.Percentage { + return nil + } + atomic.AddUint64(&chosenRule.hits, 1) + return chosenRule +} + +func (p *Plan) DeleteRule(name string) error { + _, ok := p.rulesMap.Load(name) + if !ok { + return ErrNotFound + } + p.rulesMap.Delete(name) + return nil +} + +func (p *Plan) DeleteAllRule() { + p.rulesMap.Range(func(key, value interface{}) bool { + p.rulesMap.Delete(key) + return true + }) +} diff --git a/mock/proxy/proxy-mysql/proxy_mysql.go b/mock/proxy/proxy-mysql/proxy_mysql.go new file mode 100644 index 0000000..f1dc05b --- /dev/null +++ b/mock/proxy/proxy-mysql/proxy_mysql.go @@ -0,0 +1,44 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 proxymysql Mysql TCP-based fault injection +package proxymysql + +import ( + "github.com/dolthub/vitess/go/mysql" + "github.com/huaweicloud/devcloud-go/mock/proxy" +) + +type ProxyMysql struct { + *proxy.Proxy +} + +func NewProxy(server, addr string) *ProxyMysql { + proxyMysql := proxy.NewProxy(server, addr, proxy.Mysql) + return &ProxyMysql{ + Proxy: proxyMysql, + } +} + +// mysql chaos error +var ( + Nil = mysql.NewSQLError(mysql.ERUnknownError, mysql.SSUnknownSQLState, "nil") + ServerShutdown = mysql.NewSQLError(mysql.ERUnknownError, mysql.SSUnknownSQLState, "Server shutdown in progress") + ERUnknownError = mysql.NewSQLError(mysql.ERUnknownError, mysql.SSUnknownSQLState, "unknown error") + + ERNoSuchTable = mysql.NewSQLError(mysql.ERNoSuchTable, mysql.SSUnknownSQLState, "table not found") + ERDbCreateExists = mysql.NewSQLError(mysql.ERDbCreateExists, mysql.SSUnknownSQLState, "can't create database; database exists") + ERSubqueryNo1Row = mysql.NewSQLError(mysql.ERSubqueryNo1Row, mysql.SSUnknownSQLState, "the subquery returned more than 1 row") +) diff --git a/mock/proxy/proxy-redis/proxy_redis.go b/mock/proxy/proxy-redis/proxy_redis.go new file mode 100644 index 0000000..d1161f6 --- /dev/null +++ b/mock/proxy/proxy-redis/proxy_redis.go @@ -0,0 +1,43 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 proxyredis Redis TCP-based fault injection +package proxyredis + +import ( + "github.com/go-redis/redis/v8" + "github.com/huaweicloud/devcloud-go/mock/proxy" +) + +type ProxyRedis struct { + *proxy.Proxy +} + +func NewProxy(server, addr string) *ProxyRedis { + proxyRedis := proxy.NewProxy(server, addr, proxy.Redis) + return &ProxyRedis{ + Proxy: proxyRedis, + } +} + +// redis chaos error +var ( + Nil = redis.Nil + ServerShutdown = proxy.RedisError("Server shutdown in progress") + UnknownError = proxy.RedisError("UnknownError") + + CommandUKErr = proxy.RedisError("ERR unknown command") + CommandArgErr = proxy.RedisError("ERR wrong number of arguments for command") +) diff --git a/mock/proxy/proxy.go b/mock/proxy/proxy.go new file mode 100644 index 0000000..ba569bd --- /dev/null +++ b/mock/proxy/proxy.go @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2017, redfi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * 2022.01.20-Adapt to Redis and MySQL fault injection, delete unnecessary functions + * add required logic. + * Huawei Technologies Co., Ltd. + * + */ + +// Package proxy TCP-based fault injection +package proxy + +import ( + "errors" + "fmt" + "io" + "log" + "math/rand" + "net" + "sync" + "time" + + "github.com/dolthub/vitess/go/mysql" + "github.com/go-redis/redis/v8" + "github.com/tidwall/redcon" + "gopkg.in/fatih/pool.v2" +) + +func factory(server string) func() (net.Conn, error) { + return func() (net.Conn, error) { + return net.Dial("tcp", server) + } +} + +type MockType string + +// agent type +const ( + Redis MockType = "redis" + Mysql MockType = "mysql" + Etcd MockType = "etcd" +) + +// Proxy real service agent +type Proxy struct { + Server string + Addr string + plan *Plan + connMap sync.Map + connPool pool.Pool + listener net.Listener + mock MockType +} + +func NewProxy(server, addr string, mock MockType) *Proxy { + plan := &Plan{} + return &Proxy{ + Server: server, + Addr: addr, + plan: plan, + mock: mock, + } +} + +func (p *Proxy) StartProxy() error { + var err error + p.connPool, err = pool.NewChannelPool(5, 30, factory(p.Server)) + if err != nil { + log.Println(err) + return err + } + p.listener, err = net.Listen("tcp", p.Addr) + if err != nil { + log.Fatal(err) + return err + } + log.Printf("proxy [%s] started! ", p.Addr) + go func() { + for { + conn, err := p.listener.Accept() + if err != nil { + log.Println(err) + break + } + p.connMap.Store(conn.RemoteAddr().String(), &conn) + go p.handle(conn) + } + }() + return nil +} + +// handle proxy service real service interactivity +func (p *Proxy) handle(conn net.Conn) { + var wg sync.WaitGroup + targetConn, err := p.connPool.Get() + if err != nil { + log.Fatal("failed to get a connection from connPool") + } + + wg.Add(2) + go func() { + p.faulter(targetConn, conn) + wg.Done() + }() + go func() { + p.pipe(targetConn, conn) + wg.Done() + }() + + wg.Wait() + p.connMap.Delete(conn.RemoteAddr().String()) + err = conn.Close() + if err != nil { + log.Println(err) + } +} + +// Write proxy service results +func (p *Proxy) faulter(dst, src net.Conn) { + buf := make([]byte, 32<<10) + for { + n, err := src.Read(buf) + if err != nil && err == io.EOF { + break + } + if err != nil { + log.Println(err) + break + } + rule := p.plan.SelectRule(src.RemoteAddr().String(), buf) + if errflg := p.Write(src, rule); errflg { + _, err = dst.Write(buf[:n]) + if err != nil { + log.Println(err) + continue + } + } + } +} + +// Write proxy service results write +func (p *Proxy) Write(src net.Conn, rule *Rule) bool { + if rule == nil { + return true + } + if rule.Delay > 0 { + time.Sleep(time.Duration(rule.Delay) * time.Millisecond) + } + if rule.Jitter > 0 { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + time.Sleep(time.Duration(rule.Jitter*r.Intn(100)/100) * time.Millisecond) + } + if rule.Drop { + if p.mock == Mysql { + p.errWrite(src, mysql.NewSQLError(mysql.ERUnknownError, mysql.SSUnknownSQLState, "Server shutdown in progress")) + } else { + p.errWrite(src, RedisError("Server shutdown in progress")) + } + return false + } + if rule.ReturnEmpty { + if p.mock == Mysql { + p.errWrite(src, mysql.NewSQLError(mysql.ERUnknownError, mysql.SSUnknownSQLState, "nil")) + } else { + p.errWrite(src, redis.Nil) + } + return false + } + if rule.ReturnErr != nil { + p.errWrite(src, rule.ReturnErr) + return false + } + return true +} + +// errWrite proxy service error results write +func (p *Proxy) errWrite(src net.Conn, srcErr error) { + buf := make([]byte, 0) + if p.mock == Mysql { + sqlErr, err := srcErr.(*mysql.SQLError) + log.Println(err) + buf = writePacket(uint16(sqlErr.Num), sqlErr.State, "%v", sqlErr.Message) + } else { + redisErr, err := srcErr.(redis.Error) + log.Println(err) + buf = redcon.AppendError(buf, redisErr.Error()) + } + _, err := src.Write(buf) + if err != nil { + log.Println(err) + } +} + +// pipe real service results +func (p *Proxy) pipe(dst, src net.Conn) { + buf := make([]byte, 32<<10) + + for { + n, err := dst.Read(buf) + if err != nil && err == io.EOF { + break + } + if err != nil { + log.Println(err) + break + } + _, err = src.Write(buf[:n]) + if err != nil { + log.Println(err) + continue + } + } +} + +func (p *Proxy) StopProxy() { + p.connMap.Range(func(key, value interface{}) bool { + p.connMap.Delete(key) + err := (*value.(*net.Conn)).Close() + if err != nil { + log.Println(err) + } + return true + }) + err := p.listener.Close() + if err != nil { + log.Println(err) + } + p.connPool.Close() + log.Printf("proxy [%s] stop! ", p.Addr) +} + +func (p *Proxy) AddDelay(name string, delay, percentage int, clientAddr, command string) error { + rule := Rule{ + Name: name, + Delay: delay, + } + rule.setPCC(percentage, clientAddr, command) + return p.plan.AddRule(rule) +} + +func (p *Proxy) AddJitter(name string, jitter, percentage int, clientAddr, command string) error { + rule := Rule{ + Name: name, + Jitter: jitter, + } + rule.setPCC(percentage, clientAddr, command) + return p.plan.AddRule(rule) +} + +func (p *Proxy) AddDrop(name string, percentage int, clientAddr, command string) error { + rule := Rule{ + Name: name, + Drop: true, + } + rule.setPCC(percentage, clientAddr, command) + return p.plan.AddRule(rule) +} + +func (p *Proxy) AddReturnEmpty(name string, percentage int, clientAddr, command string) error { + rule := Rule{ + Name: name, + ReturnEmpty: true, + } + if percentage > 0 && percentage < 100 { + rule.Percentage = percentage + } + if clientAddr != "" { + rule.ClientAddr = clientAddr + } + if command != "" { + rule.Command = command + } + return p.plan.AddRule(rule) +} + +func (p *Proxy) AddReturnErr(name string, returnErr error, percentage int, clientAddr, command string) error { + rule := Rule{ + Name: name, + ReturnErr: returnErr, + } + rule.setPCC(percentage, clientAddr, command) + return p.plan.AddRule(rule) +} + +func (p *Proxy) DeleteAllRule() { + p.plan.DeleteAllRule() +} + +func writeByte(data []byte, pos int, value byte) int { + data[pos] = value + return pos + 1 +} + +func writeUint16(data []byte, pos int, value uint16) int { + data[pos] = byte(value) + data[pos+1] = byte(value >> 8) + return pos + 2 +} + +func writeEOFString(data []byte, pos int, value string) int { + pos += copy(data[pos:], value) + return pos +} + +func errorPacket(errorCode uint16, sqlState string, format string, args ...interface{}) ([]byte, error) { + errorMessage := fmt.Sprintf(format, args...) + length := 1 + 2 + 1 + 5 + len(errorMessage) + + data := make([]byte, length) + pos := 0 + pos = writeByte(data, pos, mysql.ErrPacket) + pos = writeUint16(data, pos, errorCode) + pos = writeByte(data, pos, '#') + if sqlState == "" { + sqlState = mysql.SSUnknownSQLState + } + if len(sqlState) != 5 { + return nil, errors.New("sqlState has to be 5 characters long") + } + pos = writeEOFString(data, pos, sqlState) + _ = writeEOFString(data, pos, errorMessage) + + return data, nil +} + +func writePacket(errorCode uint16, sqlState string, format string, args ...interface{}) []byte { + buff := make([]byte, 0) + buf, err := errorPacket(errorCode, sqlState, format, args) + if err != nil { + log.Println(err) + } + packetLength := len(buf) + buff = append(buff, byte(packetLength)) + buff = append(buff, byte(packetLength>>8)) + buff = append(buff, byte(packetLength>>16)) + buff = append(buff, 1) + buff = append(buff, buf...) + return buff +} + +// RedisError rediserror +type RedisError string + +func (e RedisError) Error() string { return string(e) } + +func (RedisError) RedisError() {} diff --git a/mock/proxy/rule.go b/mock/proxy/rule.go new file mode 100644 index 0000000..082f595 --- /dev/null +++ b/mock/proxy/rule.go @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2017, redfi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * 2022.01.20-Adapt to Redis and MySQL fault injection, delete unnecessary functions + * add required logic. + * Huawei Technologies Co., Ltd. + */ + +package proxy + +import ( + "fmt" + "strings" + "sync/atomic" +) + +type Rule struct { + Name string `json:"name,omiempty"` + Delay int `json:"delay,omitempty"` + Jitter int `json:"jitter,omitempty"` + Drop bool `json:"drop,omitempty"` + ReturnEmpty bool `json:"return_empty,omitempty"` + ReturnErr error `json:"return_err,omitempty"` + Percentage int `json:"percentage,omitempty"` + // SelectRule does prefix matching on this value + ClientAddr string `json:"client_addr,omitempty"` + Command string `json:"command,omitempty"` + // filled by marshalCommand + marshaledCmd []byte + hits uint64 +} + +// setPCC set percentage clientAddr command +func (r *Rule) setPCC(percentage int, clientAddr, command string) { + if percentage >= 0 && percentage < 100 { + r.Percentage = percentage + } else { + r.Percentage = 0 + } + if clientAddr != "" { + r.ClientAddr = clientAddr + } + if command != "" { + r.Command = command + } +} + +func (r *Rule) String() string { + buf := make([]string, 0) + buf = append(buf, r.Name) + + // count hits + hits := atomic.LoadUint64(&r.hits) + buf = append(buf, fmt.Sprintf("hits=%d", hits)) + + if r.Delay > 0 { + buf = append(buf, fmt.Sprintf("delay=%d", r.Delay)) + } + if r.Jitter > 0 { + buf = append(buf, fmt.Sprintf("jitter=%d", r.Jitter)) + } + if r.Drop { + buf = append(buf, fmt.Sprintf("drop=%t", r.Drop)) + } + if r.ReturnEmpty { + buf = append(buf, fmt.Sprintf("return_empty=%t", r.ReturnEmpty)) + } + if r.ReturnErr != nil { + buf = append(buf, fmt.Sprintf("return_err=%s", r.ReturnErr)) + } + if len(r.ClientAddr) > 0 { + buf = append(buf, fmt.Sprintf("client_addr=%s", r.ClientAddr)) + } + if len(r.Command) > 0 { + buf = append(buf, fmt.Sprintf("command=%s", r.Command)) + } + if r.Percentage > 0 { + buf = append(buf, fmt.Sprintf("percentage=%d", r.Percentage)) + } + + return strings.Join(buf, " ") +} diff --git a/mock/proxy/test/etcd/etcd_proxy_test.go b/mock/proxy/test/etcd/etcd_proxy_test.go new file mode 100644 index 0000000..2a8a77f --- /dev/null +++ b/mock/proxy/test/etcd/etcd_proxy_test.go @@ -0,0 +1,127 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + +// Package etcd proxy test cases +package etcd + +import ( + "context" + "log" + "os" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/huaweicloud/devcloud-go/mock" + "github.com/huaweicloud/devcloud-go/mock/proxy" +) + +func TestEtcdMock(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Etcd") +} + +var _ = Describe("Etcd", func() { + var ( + etcdProxy *proxy.Proxy + etcdMock *mock.MockEtcd + + addrs = []string{"127.0.0.1:2382"} + dataDir = "etcd_data" + proxyAddrs = []string{"127.0.0.1:3382"} + client *clientv3.Client + err error + + key = "key" + val = "val" + ) + + BeforeSuite(func() { + metadata := mock.NewEtcdMetadata() + metadata.ClientAddrs = addrs + metadata.DataDir = dataDir + etcdMock = &mock.MockEtcd{} + etcdMock.StartMockEtcd(metadata) + + etcdProxy = proxy.NewProxy(addrs[0], proxyAddrs[0], proxy.Etcd) + err := etcdProxy.StartProxy() + if err != nil { + log.Fatalln(err) + } + client, err = clientv3.New(clientv3.Config{Endpoints: proxyAddrs, Username: "root", Password: "root"}) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterSuite(func() { + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + log.Println(err) + } + }(dataDir) + defer etcdMock.StopMockEtcd() + defer etcdProxy.StopProxy() + + defer func(client *clientv3.Client) { + err = client.Close() + if err != nil { + log.Println(err) + } + }(client) + }) + + AfterEach(func() { + etcdProxy.DeleteAllRule() + }) + + It("Put", func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + _, err = client.Put(ctx, key, val, clientv3.WithPrevKV()) + cancel() + Expect(err).NotTo(HaveOccurred()) + }) + + It("Get", func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + resp, err := client.Get(ctx, key) + cancel() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.Count).To(Equal(int64(1))) + Expect(string(resp.Kvs[0].Value)).To(Equal(val)) + }) + + It("GetDelay", func() { + _ = etcdProxy.AddDelay("delay", 200, 0, "", "") + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + resp, err := client.Get(ctx, key) + cancel() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.Count).To(Equal(int64(1))) + Expect(string(resp.Kvs[0].Value)).To(Equal(val)) + }) + + It("GetJitter", func() { + _ = etcdProxy.AddJitter("jitter", 200, 0, "", "") + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + resp, err := client.Get(ctx, key) + cancel() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.Count).To(Equal(int64(1))) + Expect(string(resp.Kvs[0].Value)).To(Equal(val)) + }) +}) diff --git a/mock/proxy/test/mysql/mysql_proxy_test.go b/mock/proxy/test/mysql/mysql_proxy_test.go new file mode 100644 index 0000000..f1809bd --- /dev/null +++ b/mock/proxy/test/mysql/mysql_proxy_test.go @@ -0,0 +1,246 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 mysql proxy test cases +package mysql + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/huaweicloud/devcloud-go/mock/proxy" + proxymysql "github.com/huaweicloud/devcloud-go/mock/proxy/proxy-mysql" + "github.com/huaweicloud/devcloud-go/mock/proxy/utils" + devspore "github.com/huaweicloud/devcloud-go/sql-driver/mysql" +) + +func TestMysqlMock(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Mysql") +} + +var _ = Describe("Mysql", func() { + var ( + err error + etcdAddrs = []string{"127.0.0.1:2379"} + dataDir = "etcd_data" + mysqlAddrs = []string{"127.0.0.1:13306", "127.0.0.1:13307"} + proxyAddrs = []string{"127.0.0.1:23306", "127.0.0.1:23307"} + clusterConfiguration = utils.DCMysql(etcdAddrs, proxyAddrs) + datasource = "/mas-monitor/conf/db/services/" + clusterConfiguration.Props.AppID + "/" + clusterConfiguration.Props.MonitorID + "/database/" + clusterConfiguration.Props.DatabaseName + "/datasource" + router = "/mas-monitor/conf/db/services/" + clusterConfiguration.Props.AppID + "/" + clusterConfiguration.Props.MonitorID + "/database/" + clusterConfiguration.Props.DatabaseName + "/router" + activekey = "/mas-monitor/status/db/services/" + clusterConfiguration.Props.AppID + "/" + clusterConfiguration.Props.MonitorID + "/database/" + clusterConfiguration.Props.DatabaseName + "/active" + db *sql.DB + client *clientv3.Client + val1 = mysqlAddrs[0] + "John Doe" + val2 = mysqlAddrs[1] + "John Doe" + ) + BeforeSuite(func() { + utils.Start2MysqlMock(mysqlAddrs) + utils.Start2Proxy(mysqlAddrs, proxyAddrs, proxy.Mysql) + utils.StartEtcdMock(etcdAddrs, dataDir) + + client, err = clientv3.New(clientv3.Config{Endpoints: etcdAddrs, Username: "root", Password: "root"}) + + datasourceStr, err := json.Marshal(clusterConfiguration.DataSource) + if err != nil { + fmt.Println(err) + } + routerConfigStr, err := json.Marshal(clusterConfiguration.RouterConfig) + if err != nil { + fmt.Println(err) + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + _, err = client.Put(ctx, datasource, string(datasourceStr), clientv3.WithPrevKV()) + if err != nil { + log.Println(err) + } + _, err = client.Put(ctx, router, string(routerConfigStr), clientv3.WithPrevKV()) + if err != nil { + log.Println(err) + } + _, err = client.Put(ctx, activekey, "dc1", clientv3.WithPrevKV()) + if err != nil { + log.Println(err) + } + cancel() + if clusterConfiguration.Chaos != nil { + clusterConfiguration.Chaos.Active = false + } + devspore.SetClusterConfiguration(clusterConfiguration) + db, err = sql.Open("devspore_mysql", "") + if err != nil { + fmt.Println(err) + } + time.Sleep(time.Second) + }) + + AfterSuite(func() { + err = db.Close() + if err != nil { + log.Println(err) + } + err = client.Close() + if err != nil { + log.Println(err) + } + utils.StopEtcdMock(dataDir) + utils.Stop2Proxy() + utils.Stop2MysqlMock() + }) + + AfterEach(func() { + utils.Proxys[0].DeleteAllRule() + }) + + It("Select", func() { + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal(val1)) + }) + + It("SelectDelay", func() { + err = utils.Proxys[0].AddDelay("delay", 1500, 0, "", "select") + if err != nil { + log.Println(err) + } + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal(val1)) + }) + + It("SelectJitter", func() { + err = utils.Proxys[0].AddJitter("jitter", 1500, 0, "", "select") + if err != nil { + log.Println(err) + } + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal(val1)) + }) + + It("SelectDrop", func() { + err = utils.Proxys[0].AddDrop("drop", 0, "", "select") + if err != nil { + log.Println(err) + } + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal(val1)) + }) + + It("SelectReturnEmpty", func() { + err = utils.Proxys[0].AddReturnEmpty("returnEmpty", 0, "", "select") + if err != nil { + log.Println(err) + } + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal(val1)) + }) + + It("SelectReturnErr", func() { + err = utils.Proxys[0].AddReturnErr("returnErr", proxymysql.ERNoSuchTable, 0, "", "select") + if err != nil { + log.Println(err) + } + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal(val1)) + }) + + It("MockChang", func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + _, err = client.Put(ctx, activekey, "dc2", clientv3.WithPrevKV()) + cancel() + Expect(err).NotTo(HaveOccurred()) + time.Sleep(time.Second) + }) + + It("SelectDrop-MockChang", func() { + err = utils.Proxys[0].AddDrop("drop", 0, "", "select") + if err != nil { + log.Println(err) + } + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal(val2)) + }) + + It("SelectReturnEmpty-MockChang", func() { + err = utils.Proxys[0].AddReturnEmpty("returnEmpty", 0, "", "select") + if err != nil { + log.Println(err) + } + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal(val2)) + }) + + It("SelectReturnErr-MockChang", func() { + err = utils.Proxys[0].AddReturnErr("returnErr", proxymysql.ERNoSuchTable, 0, "", "select") + if err != nil { + log.Println(err) + } + var name string + err = db.QueryRow("select name from user where id=1").Scan(&name) + if err != nil { + log.Println(err) + } + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal(val2)) + }) +}) diff --git a/mock/proxy/test/redis/redis_proxy_test.go b/mock/proxy/test/redis/redis_proxy_test.go new file mode 100644 index 0000000..dc5549c --- /dev/null +++ b/mock/proxy/test/redis/redis_proxy_test.go @@ -0,0 +1,165 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 redis proxy test cases +package redis + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/huaweicloud/devcloud-go/mock/proxy" + proxyredis "github.com/huaweicloud/devcloud-go/mock/proxy/proxy-redis" + "github.com/huaweicloud/devcloud-go/mock/proxy/utils" + "github.com/huaweicloud/devcloud-go/redis" +) + +func TestRedisMock(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Redis") +} + +var _ = Describe("Redis", func() { + var ( + err error + etcdAddrs = []string{"127.0.0.1:2379"} + dataDir = "etcd_data" + redisAddrs = []string{"127.0.0.1:16379", "127.0.0.1:16380"} + proxyAddrs = []string{"127.0.0.1:26379", "127.0.0.1:26380"} + configuration = utils.DCRedis(etcdAddrs, proxyAddrs) + servers = "/mas-monitor/conf/dcs/services/" + configuration.Props.AppID + "/" + configuration.Props.MonitorID + "/servers" + algorithm = "/mas-monitor/conf/dcs/services/" + configuration.Props.AppID + "/" + configuration.Props.MonitorID + "/route-algorithm" + activekey = "/mas-monitor/status/dcs/services/" + configuration.Props.AppID + "/" + configuration.Props.MonitorID + "/active" + redisClient *redis.DevsporeClient + client *clientv3.Client + ) + + BeforeSuite(func() { + utils.Start2RedisMock(redisAddrs) + utils.Start2Proxy(redisAddrs, proxyAddrs, proxy.Redis) + utils.StartEtcdMock(etcdAddrs, dataDir) + + client, _ = clientv3.New(clientv3.Config{Endpoints: etcdAddrs, Username: "root", Password: "root"}) + + serversStr, err := json.Marshal(configuration.RedisConfig.Servers) + if err != nil { + fmt.Println(err) + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + _, _ = client.Put(ctx, servers, string(serversStr), clientv3.WithPrevKV()) + _, _ = client.Put(ctx, algorithm, configuration.RouteAlgorithm, clientv3.WithPrevKV()) + _, _ = client.Put(ctx, activekey, "ds1", clientv3.WithPrevKV()) + cancel() + if configuration.Chaos != nil { + configuration.Chaos.Active = false + } + redisClient = redis.NewDevsporeClient(configuration) + }) + + AfterSuite(func() { + _ = redisClient.Close() + _ = client.Close() + utils.StopEtcdMock(dataDir) + utils.Stop2Proxy() + utils.Stop2RedisMock() + }) + + AfterEach(func() { + utils.Proxys[0].DeleteAllRule() + }) + + It("Get", func() { + ctx := context.Background() + res1 := redisClient.Get(ctx, "key") + Expect(res1.Err()).NotTo(HaveOccurred()) + Expect(res1.Val()).To(Equal(utils.RedisMocks[0].Addr)) + }) + + It("GetDelay", func() { + _ = utils.Proxys[0].AddDelay("delay", 1500, 0, "", "") + ctx := context.Background() + res1 := redisClient.Get(ctx, "key") + Expect(res1.Err()).NotTo(HaveOccurred()) + Expect(res1.Val()).To(Equal(utils.RedisMocks[0].Addr)) + }) + + It("GetJitter", func() { + _ = utils.Proxys[0].AddJitter("jitter", 1500, 0, "", "") + ctx := context.Background() + res1 := redisClient.Get(ctx, "key") + Expect(res1.Err()).NotTo(HaveOccurred()) + Expect(res1.Val()).To(Equal(utils.RedisMocks[0].Addr)) + }) + + It("GetDrop", func() { + _ = utils.Proxys[0].AddDrop("drop", 0, "", "") + ctx := context.Background() + res1 := redisClient.Get(ctx, "key") + Expect(res1.Err()).NotTo(HaveOccurred()) + }) + + It("GetReturnEmpty", func() { + _ = utils.Proxys[0].AddReturnEmpty("returnEmpty", 0, "", "") + ctx := context.Background() + res1 := redisClient.Get(ctx, "key") + Expect(res1.Err()).NotTo(HaveOccurred()) + }) + + It("GetReturnErr", func() { + _ = utils.Proxys[0].AddReturnErr("returnErr", proxyredis.UnknownError, 0, "", "") + ctx := context.Background() + res1 := redisClient.Get(ctx, "key") + Expect(res1.Err()).NotTo(HaveOccurred()) + }) + + It("MockChang", func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + _, err = client.Put(ctx, activekey, "ds2", clientv3.WithPrevKV()) + cancel() + Expect(err).NotTo(HaveOccurred()) + time.Sleep(time.Second) + }) + + It("GetDrop-MockChang", func() { + _ = utils.Proxys[0].AddDrop("drop", 0, "", "") + ctx := context.Background() + res2 := redisClient.Get(ctx, "key") + Expect(res2.Err()).NotTo(HaveOccurred()) + Expect(res2.Val()).To(Equal(utils.RedisMocks[1].Addr)) + }) + + It("GetReturnEmpty-MockChang", func() { + _ = utils.Proxys[0].AddReturnEmpty("returnEmpty", 0, "", "") + ctx := context.Background() + res2 := redisClient.Get(ctx, "key") + Expect(res2.Err()).NotTo(HaveOccurred()) + Expect(res2.Val()).To(Equal(utils.RedisMocks[1].Addr)) + }) + + It("GetReturnErr-MockChang", func() { + _ = utils.Proxys[0].AddReturnErr("returnErr", proxyredis.UnknownError, 0, "", "") + ctx := context.Background() + res2 := redisClient.Get(ctx, "key") + Expect(res2.Err()).NotTo(HaveOccurred()) + Expect(res2.Val()).To(Equal(utils.RedisMocks[1].Addr)) + }) +}) diff --git a/mock/proxy/utils/util.go b/mock/proxy/utils/util.go new file mode 100644 index 0000000..d7904f8 --- /dev/null +++ b/mock/proxy/utils/util.go @@ -0,0 +1,269 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 utils Test tool +package utils + +import ( + "context" + "log" + "os" + "strconv" + "time" + + "github.com/dolthub/go-mysql-server/memory" + mocksql "github.com/dolthub/go-mysql-server/sql" + goredis "github.com/go-redis/redis/v8" + + "github.com/huaweicloud/devcloud-go/common/etcd" + "github.com/huaweicloud/devcloud-go/mas" + "github.com/huaweicloud/devcloud-go/mock" + "github.com/huaweicloud/devcloud-go/mock/proxy" + redisconfig "github.com/huaweicloud/devcloud-go/redis/config" + mysqlconfig "github.com/huaweicloud/devcloud-go/sql-driver/rds/config" +) + +// mock services +var ( + EtcdMock *mock.MockEtcd + MysqlMocks = make([]*mock.MysqlMock, 0) + RedisMocks = make([]*mock.RedisMock, 0) + Proxys = make([]*proxy.Proxy, 0) +) + +func StartEtcdMock(addrs []string, dataDir string) { + metadata := mock.NewEtcdMetadata() + metadata.ClientAddrs = addrs + metadata.DataDir = dataDir + EtcdMock = &mock.MockEtcd{} + EtcdMock.StartMockEtcd(metadata) +} + +func StopEtcdMock(dataDir string) { + var err error + EtcdMock.StopMockEtcd() + err = os.RemoveAll(dataDir) + if err != nil { + log.Println("ERROR: remove " + dataDir + " dir failed") + } +} + +func Start2MysqlMock(addrs []string) { + var err error + for _, addr := range addrs { + mysqlMock := &mock.MysqlMock{ + User: "root", + Password: "123456", + Address: addr, + Databases: []string{"ds0", "ds0-slave0", "ds0-slave1", "ds1", "ds1-slave0", "ds1-slave1"}, + MemDatabases: []*memory.Database{createTestDatabase("ds0", "user", addr)}, + } + err = mysqlMock.StartMockMysql() + if err != nil { + log.Fatalln(err) + } + MysqlMocks = append(MysqlMocks, mysqlMock) + } +} + +func Stop2MysqlMock() { + for _, mysqlMock := range MysqlMocks { + mysqlMock.StopMockMysql() + } +} + +func createTestDatabase(dbName, tableName, address string) *memory.Database { + db := memory.NewDatabase(dbName) + table := memory.NewTable(tableName, mocksql.Schema{ + {Name: "id", Type: mocksql.Int64, Nullable: false, AutoIncrement: true, PrimaryKey: true, Source: tableName}, + {Name: "name", Type: mocksql.Text, Nullable: false, Source: tableName}, + {Name: "email", Type: mocksql.Text, Nullable: false, Source: tableName}, + {Name: "phone_numbers", Type: mocksql.JSON, Nullable: false, Source: tableName}, + {Name: "created_at", Type: mocksql.Timestamp, Nullable: false, Source: tableName}, + }) + + ctx := mocksql.NewEmptyContext() + db.AddTable(tableName, table) + + rows := []mocksql.Row{ + mocksql.NewRow(1, address+"John Doe", "jasonkay@doe.com", []string{"555-555-555"}, time.Now()), + mocksql.NewRow(2, address+"John Doe", "johnalt@doe.com", []string{}, time.Now()), + mocksql.NewRow(3, address+"Jane Doe", "jane@doe.com", []string{}, time.Now()), + mocksql.NewRow(4, address+"Evil Bob", "jasonkay@gmail.com", []string{"555-666-555", "666-666-666"}, time.Now()), + } + + for _, row := range rows { + _ = table.Insert(ctx, row) + } + return db +} + +func setSourcesNodes(mysqlAddrs []string) (map[string]*mysqlconfig.DataSourceConfiguration, + map[string]*mysqlconfig.NodeConfiguration) { + dataSource := make(map[string]*mysqlconfig.DataSourceConfiguration) + nodes := make(map[string]*mysqlconfig.NodeConfiguration) + for i, addr := range mysqlAddrs { + stri := strconv.Itoa(i + 1) + dataSource["ds"+stri] = &mysqlconfig.DataSourceConfiguration{ + URL: "tcp(" + addr + ")/ds0?charset=utf8&parseTime=true", + Username: "root", + Password: "123456", + } + nodes["dc"+stri] = &mysqlconfig.NodeConfiguration{ + Master: "ds" + stri, + } + } + return dataSource, nodes +} + +func DCMysql(etcdAddrs, mysqlAddrs []string) *mysqlconfig.ClusterConfiguration { + dataSource, nodes := setSourcesNodes(mysqlAddrs) + return &mysqlconfig.ClusterConfiguration{ + Props: &mas.PropertiesConfiguration{ + AppID: "123", + MonitorID: "456", + DatabaseName: "12", + }, + EtcdConfig: &etcd.EtcdConfiguration{ + Address: etcdAddrs[0], + Username: "root", + Password: "root", + HTTPSEnable: false, + }, + RouterConfig: &mysqlconfig.RouterConfiguration{ + Nodes: nodes, + Active: "dc1", + }, + DataSource: dataSource, + Chaos: &mas.InjectionProperties{ + Active: true, + Duration: 50, + Interval: 100, + Percentage: 100, + DelayInjection: &mas.DelayInjection{ + Active: true, + Percentage: 75, + TimeMs: 1000, + JitterMs: 500, + }, + ErrorInjection: &mas.ErrorInjection{ + Active: true, + Percentage: 30, + }, + }, + } +} + +func Start2RedisMock(addrs []string) { + var err error + for _, addr := range addrs { + redisMock := &mock.RedisMock{Addr: addr, Password: "123456"} + err = redisMock.StartMockRedis() + if err != nil { + log.Fatalln(err) + } + addTestData(redisMock) + RedisMocks = append(RedisMocks, redisMock) + } +} + +func Stop2RedisMock() { + for _, redisMock := range RedisMocks { + redisMock.StopMockRedis() + } +} + +func addTestData(redisMock *mock.RedisMock) { + ctx := context.Background() + rdb1 := goredis.NewClient(&goredis.Options{Addr: redisMock.Addr, Password: "123456"}) + rdb1.Set(ctx, "key", redisMock.Addr, 0) + _ = rdb1.Close() +} + +func setServers(redisAddrs []string) map[string]*redisconfig.ServerConfiguration { + servers := make(map[string]*redisconfig.ServerConfiguration) + for i, addr := range redisAddrs { + stri := strconv.Itoa(i + 1) + servers["ds"+stri] = &redisconfig.ServerConfiguration{ + Hosts: addr, + Password: "123456", + Type: redisconfig.ServerTypeNormal, + Cloud: "huawei cloud", + Region: "beijing", + Azs: "az1", + } + } + return servers +} + +func DCRedis(etcdAddrs, redisAddrs []string) *redisconfig.Configuration { + servers := setServers(redisAddrs) + configuration := &redisconfig.Configuration{ + Props: &mas.PropertiesConfiguration{ + AppID: "123", + MonitorID: "456", + DatabaseName: "789", + }, + EtcdConfig: &etcd.EtcdConfiguration{ + Address: etcdAddrs[0], + Username: "root", + Password: "root", + HTTPSEnable: false, + }, + RedisConfig: &redisconfig.RedisConfiguration{ + Servers: servers, + ConnectionPoolConfig: &redisconfig.RedisConnectionPoolConfiguration{ + Enable: false, + }, + }, + RouteAlgorithm: "single-read-write", + Active: "ds1", + Chaos: &mas.InjectionProperties{ + Active: true, + Duration: 50, + Interval: 100, + Percentage: 100, + DelayInjection: &mas.DelayInjection{ + Active: true, + Percentage: 100, + TimeMs: 1000, + JitterMs: 500, + }, + ErrorInjection: &mas.ErrorInjection{ + Active: true, + Percentage: 30, + }, + }, + } + return configuration +} + +func Start2Proxy(addrs []string, proxys []string, mock proxy.MockType) { + var err error + for i := 0; i < len(proxys); i++ { + tProxy := proxy.NewProxy(addrs[i], proxys[i], mock) + err = tProxy.StartProxy() + if err != nil { + log.Fatalln(err) + } + Proxys = append(Proxys, tProxy) + } +} + +func Stop2Proxy() { + for _, tProxy := range Proxys { + tProxy.StopProxy() + } +} diff --git a/mock/redis.go b/mock/redis.go index 21a67f9..bcab631 100644 --- a/mock/redis.go +++ b/mock/redis.go @@ -21,53 +21,41 @@ import ( "github.com/alicebob/miniredis/v2" ) -type RedisMetadata struct { +type RedisMock struct { User string // optional Password string // optional - Addr string + Addr string // optional + redis *miniredis.Miniredis } -var redisMap = map[string]*miniredis.Miniredis{} - -func StartMockRedis(metadata RedisMetadata) { - redis := miniredis.NewMiniRedis() - if len(metadata.User) > 0 && len(metadata.Password) > 0 { - redis.RequireUserAuth(metadata.User, metadata.Password) +func (r *RedisMock) StartMockRedis() error { + r.redis = miniredis.NewMiniRedis() + if len(r.User) > 0 && len(r.Password) > 0 { + r.redis.RequireUserAuth(r.User, r.Password) } - if len(metadata.User) == 0 && len(metadata.Password) > 0 { - redis.RequireAuth(metadata.Password) + if len(r.User) == 0 && len(r.Password) > 0 { + r.redis.RequireAuth(r.Password) } var err error - if len(metadata.Addr) > 0 { - err = redis.StartAddr(metadata.Addr) + if len(r.Addr) > 0 { + err = r.redis.StartAddr(r.Addr) } else { - err = redis.Start() + err = r.redis.Start() + r.Addr = r.redis.Addr() } if err != nil { - log.Printf("ERROR: start mock redis failed, %v", err) - return + log.Printf("ERROR: start miniredis failed, %v", err) + return err } - log.Printf("mock redis [%s] started! ", redis.Addr()) - redisMap[redis.Addr()] = redis - return + log.Printf("mock redis [%s] started! ", r.redis.Addr()) + return nil } -func GetMockRedisByAddr(addr string) *miniredis.Miniredis { - if redis, ok := redisMap[addr]; ok { - return redis - } - log.Fatalf("ERROR: no [%s] redis", addr) - return nil +func (r *RedisMock) GetMockRedis() *miniredis.Miniredis { + return r.redis } -func StopMockRedis() { - if len(redisMap) == 0 { - return - } - for addr, redis := range redisMap { - redis.Close() - log.Printf("mock redis [%s] stop! ", addr) - } - redisMap = map[string]*miniredis.Miniredis{} - return +func (r *RedisMock) StopMockRedis() { + r.redis.Close() + log.Printf("mock redis [%s] stop! ", r.Addr) } diff --git a/mock/redis_test.go b/mock/redis_test.go index 058c688..bf419cf 100644 --- a/mock/redis_test.go +++ b/mock/redis_test.go @@ -24,7 +24,8 @@ import ( ) func TestRedisClusterMock(t *testing.T) { - StartMockRedis(RedisMetadata{Addr: "127.0.0.1:16379"}) + redisMock := RedisMock{Addr: "127.0.0.1:16379"} + redisMock.StartMockRedis() cluster := goredis.NewClusterClient(&goredis.ClusterOptions{ Addrs: []string{"127.0.0.1:16379"}, }) @@ -34,5 +35,5 @@ func TestRedisClusterMock(t *testing.T) { res := cluster.Get(ctx, "key") assert.Nil(t, res.Err()) assert.Equal(t, res.Val(), "val") - StopMockRedis() -} \ No newline at end of file + redisMock.StopMockRedis() +} diff --git a/redis/README.md b/redis/README.md index a76b3a2..73523c6 100644 --- a/redis/README.md +++ b/redis/README.md @@ -1,5 +1,13 @@ -# devcloud-go/redis + # devcloud-go/redis +### Introduction +Currently, the Redis supports three modes.single-read-write,local-read-single-write and double-write +##### single-read-write +![image](../img/redis-single-read-write.png) +##### local-read-single-write +![image](../img/redis-local-read-single-write.png) +##### double-write +![image](../img/redis-double-write.png) ### Quickstart: 1. use yaml configuartion file ```bigquery @@ -34,14 +42,12 @@ import ( func main() { servers := map[string]*config.ServerConfiguration{ "server1": { - Type: config.ServerTypeNormal, - Cloud: "huawei cloud", - Region: "beijing", - Azs: "az0", - Options: &goredis.Options{ - Addr: "127.0.0.0:6379", - Password: "123456", - }, + Hosts: "127.0.0.0:6379", + Password: "123456", + Type: config.ServerTypeNormal, + Cloud: "huawei cloud", + Region: "beijing", + Azs: "az0", }, } configuration := &config.Configuration{ @@ -108,12 +114,225 @@ redis: minIdle: 0 maxWaitMillis: 10000 timeBetweenEvictionRunsMillis: 1000 -routeAlgorithm: local-read-async-double-write # local-read-async-double-write, single-read-write +routeAlgorithm: single-read-write # local-read-single-write, single-read-write, double-write active: dc1 ``` +### Double-write +Redis also supports double-write modes, including memory double-write and file double-write, +depending on asyncRemotePool.persist. true: file double-write; false: memory double-write +```bigquery +redis: + redisGroupName: xxx-redis-group + username: xxx # for redis 6.0 + password: yyy + nearest: dc1 + asyncRemoteWrite: + retryTimes: 4 + connectionPool: + enable: true + asyncRemotePool: + persist: true + threadCoreSize: 10 + taskQueueSize: 5 + persistDir: dataDir/ + servers: + dc1: + hosts: 127.0.0.1:6379 + password: + type: normal # cluster, master-slave, normal + cloud: huaweicloud # cloud + region: cn-north-4 # region id + azs: az1 # azs + pool: # Optional + maxTotal: 100 + maxIdle: 8 + minIdle: 0 + maxWaitMillis: 10000 + timeBetweenEvictionRunsMillis: 1000 + dc2: + hosts: 127.0.0.1:6380 + password: + type: normal # cluster, master-slave, normal + cloud: huaweicloud # cloud + region: cn-north-4 # region id + azs: az1 # azs + pool: # Optional + maxTotal: 100 + maxIdle: 8 + minIdle: 0 + maxWaitMillis: 10000 + timeBetweenEvictionRunsMillis: 1000 +routeAlgorithm: double-write # local-read-single-write, single-read-write, double-write +active: dc2 +``` +### Fault injection +Redis also supports the creation of services with fault injection. The configuration is similar to that of MySQL. +```bigquery +func DCRedis(etcdAddrs, redisAddrs []string) *redisconfig.Configuration { + servers := make(map[string]*redisconfig.ServerConfiguration) + for i, addr := range redisAddrs { + stri := strconv.Itoa(i + 1) + servers["ds"+stri] = &redisconfig.ServerConfiguration{ + Hosts: addr, + Password: "123456", + Type: redisconfig.ServerTypeNormal, + Cloud: "huawei cloud", + Region: "beijing", + Azs: "az1", + } + } + configuration := &redisconfig.Configuration{ + RedisConfig: &redisconfig.RedisConfiguration{ + Servers: servers, + }, + RouteAlgorithm: "single-read-write", + Active: "ds1", + Chaos: &mas.InjectionProperties{ + Active: true, + Duration: 50, + Interval: 100, + Percentage: 100, + DelayInjection: &mas.DelayInjection{ + Active: true, + Percentage: 100, + TimeMs: 1000, + JitterMs: 500, + }, + ErrorInjection: &mas.ErrorInjection{ + Active: true, + Percentage: 30, + }, + }, + } + return configuration +} +``` +Alternatively, add the following configuration to the configuration file: +```bigquery +chaos: + active: true + duration: 50 + interval: 100 + percentage: 100 + delayInjection: + active: true + percentage: 100 + timeMs: 1000 + jitterMs: 500 + errorInjection: + active: true + percentage: 20 +``` ### Testing -commands_test.go needs redis 6.2.0+, so if your redis is redis 5.0+, you need to execute +package commands_test needs redis 6.2.0+, so if your redis is redis 5.0+, you need to execute ```bigquery ginkgo -skip="redis6" ``` -See more usages of ginkgo in **https://github.com/onsi/ginkgo** \ No newline at end of file +See more usages of ginkgo in **https://github.com/onsi/ginkgo** + +### Description of Configuration Parameters + + +Configuration + + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
propsPropertiesConfigurationFor details,see the description of the data structure of PropertiesConfigurationMas monitoring configuration,which is used together with etcd
etcdEtcdConfigurationFor details,see the description of the data structure of EtcdConfigurationEtcd configuration.If it is configured, it will be pulled from the remote end
redisRedisConfigurationFor details,see the description of the data structure of RedisConfigurationRedisServer configuration
routeAlgorithmstringsingle-read-write,local-read-single-write,double-writeRouting algorithm
activestringThe value can only be dc1 or dc2Activated Redis
chaosInjectionPropertiesFor details,see the description of the data structure of InjectionPropertiesFault Injection Configuration
+ + +PropertiesConfiguration + + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
versionstring-Project version number
appIdstring-Project name
monitorIdstring-Monitoring group name
cloudstring-Project deployment cloud group
regionstring-Project deployment region
azsstring-Project deployment AZ
+ + +EtcdConfiguration + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
addressstring-Etcd address
apiVersionstring-Etcd interface Version
usernamestring-Etcd username
passwordstring-Etcd password
httpEnablebool-Specifies whether HTTPS is enabled for Etcd
+ + +RedisConfiguration + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
neareststringThe value can only be dc1 or dc2Indicates the local Redis
asyncRemoteWrite.retryTimesint-Number of retries of asynchronous remote write operations
connectionPool.enablebooltrue/falseIndicates whether to enable the connection pool
asyncRemotePoolAsyncRemotePoolConfigurationFor details,see the description of the data structure of AsyncRemotePoolConfigurationConfigure the asynchronous write thread pool
serversmap[string]ServerConfigurationThe key is dc1/dc2.for details about a single dimension,see the description of the data structure of ServerConfigurationRedisServer connection configuration of dc1 and dc2
+ + +AsyncRemotePoolConfiguration + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
threadCoreSizeint-Basic size of the thread pool
persistbooltrue/falseIndicates whether the command is persistent.No:The command is fast.Yes:The speed is lower than that of non-persistent
taskQueueSizeint-Number of buffer queues
persistDirstringDefault root directory "/"Redis persistent file directory
+ + +ServerConfiguration + + + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
hostsstring-RedisServer IP address
passwordstring-RedisServer password
typestringcluster,master-slave,normalRedisServer Type
cloudstring-RedisServer cloud
regionstring-Region to which the RedisServer belongs
azsstring-AZ to which RedisServer belongs
poolServerConnectionPoolConfigurationFor details,see the description of the data structure of ServerConnectionPoolConfigurationConnection pool configuration
+ + + +ServerConnectionPoolConfiguration + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
maxTotalint-Maximum number of active objects
maxIdleint-Maximum number of objects that can remain in the idle state
minIdleint-Minimum number of objects that can remain in the idle state
maxWaitMillisint-Maximum wait time when no object is returned in the pool
timeBetweenEvictionRunsMillisint-Idle link detection thread,detection interval,in milliseconds.A negative value indicates that the detection thread is not running
+ + +InjectionProperties + + + + + + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
activebooltrue/falseWhether the fault injection function is enabled
durationint-Fault injection duration,in seconds
intervalint-Fault injection interval,in seconds
percentageint0-100Injection failure probability
delayInjection.activebooltrue/falseDelay injection switch
delayInjection.percentageint0-100Delayed Fault Effective Probability
delayInjection.timeMsint-Indicates the delay base,in milliseconds
delayInjection.jitterMsint-Indicates the jitter amplitude of the delay, in milliseconds
errorInjection.activebooltrue/falseAbnormal injection switch
errorInjection.percentageint0-100Abnormal Fault Effective Probability
\ No newline at end of file diff --git a/redis/bench_test/bench_test.go b/redis/bench_test/bench_test.go new file mode 100644 index 0000000..a083154 --- /dev/null +++ b/redis/bench_test/bench_test.go @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2013 The github.com/go-redis/redis Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * 2021.11.15-Changed modify the constructor Client + * Huawei Technologies Co., Ltd. + */ + +package bench_test + +import ( + "bytes" + "context" + "fmt" + "strings" + "sync" + "testing" + "time" + + "github.com/go-redis/redis/v8" + + devsporedis "github.com/huaweicloud/devcloud-go/redis" + "github.com/huaweicloud/devcloud-go/redis/config" +) + +func benchmarkDevsporeRedisClient(ctx context.Context, poolSize int) *devsporedis.DevsporeClient { + configuration := benchmarkDevsporeClientConfiguration(config.ServerTypeNormal, poolSize) + client := devsporedis.NewDevsporeClient(configuration) + if err := client.FlushDB(ctx).Err(); err != nil { + panic(err) + } + return client +} + +func benchmarkDevsporeClusterClient(ctx context.Context) *devsporedis.DevsporeClient { + + configuration := benchmarkDevsporeClientConfiguration(config.ServerTypeCluster, 0) + client := devsporedis.NewDevsporeClient(configuration) + if err := client.FlushDB(ctx).Err(); err != nil { + panic(err) + } + return client +} + +func benchmarkRedisOptions(poolSize int) *redis.Options { + return &redis.Options{ + Addr: ":6379", + DialTimeout: time.Second, + ReadTimeout: time.Second, + WriteTimeout: time.Second, + PoolSize: poolSize, + } +} + +func benchmarkCLusterOptions() *redis.ClusterOptions { + return &redis.ClusterOptions{ + Addrs: []string{":6383", ":6384", ":6385", ":6386", ":6387", ":6388"}, + DialTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + + MaxRedirects: 8, + + PoolSize: 10, + PoolTimeout: 30 * time.Second, + IdleTimeout: time.Minute, + IdleCheckFrequency: 100 * time.Millisecond, + } +} + +func benchmarkDevsporeClientConfiguration(serverType string, poolSize int) *config.Configuration { + configuration := &config.Configuration{ + RedisConfig: &config.RedisConfiguration{}, + RouteAlgorithm: "single-read-write", + Active: "server1", + } + if serverType == config.ServerTypeCluster { + servers := map[string]*config.ServerConfiguration{ + "server1": { + Type: config.ServerTypeCluster, + Cloud: "huawei cloud", + Region: "beijing", + Azs: "az0", + ClusterOptions: benchmarkCLusterOptions(), + }, + } + configuration.RedisConfig.Servers = servers + } else { + servers := map[string]*config.ServerConfiguration{ + "server1": { + Type: config.ServerTypeNormal, + Cloud: "huawei cloud", + Region: "beijing", + Azs: "az0", + Options: benchmarkRedisOptions(poolSize), + }, + } + configuration.RedisConfig.Servers = servers + } + return configuration +} + +func BenchmarkRedisPing(b *testing.B) { + ctx := context.Background() + rdb := benchmarkDevsporeRedisClient(ctx, 10) + defer rdb.Close() + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := rdb.Ping(ctx).Err(); err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkSetGoroutines(b *testing.B) { + ctx := context.Background() + rdb := benchmarkDevsporeRedisClient(ctx, 10) + defer rdb.Close() + + for i := 0; i < b.N; i++ { + var wg sync.WaitGroup + for i := 0; i < 1000; i++ { + wg.Add(1) + go func() { + defer wg.Done() + err := rdb.Set(ctx, "hello", "world", 0).Err() + if err != nil { + panic(err) + } + }() + } + wg.Wait() + } +} + +func BenchmarkSet(b *testing.B) { + ctx := context.Background() + rdb := benchmarkDevsporeRedisClient(ctx, 10) + defer rdb.Close() + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := rdb.Set(ctx, "hello", "test", 0).Err(); err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkRedisGetNil(b *testing.B) { + ctx := context.Background() + client := benchmarkDevsporeRedisClient(ctx, 10) + defer client.Close() + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := client.Get(ctx, "key").Err(); err != redis.Nil { + b.Fatal(err) + } + } + }) +} + +type setStringBenchmark struct { + poolSize int + valueSize int +} + +func (bm setStringBenchmark) String() string { + return fmt.Sprintf("pool=%d value=%d", bm.poolSize, bm.valueSize) +} + +func BenchmarkRedisSetString(b *testing.B) { + benchmarks := []setStringBenchmark{ + {10, 64}, + {10, 1024}, + {10, 64 * 1024}, + {10, 1024 * 1024}, + {10, 10 * 1024 * 1024}, + + {100, 64}, + {100, 1024}, + {100, 64 * 1024}, + {100, 1024 * 1024}, + {100, 10 * 1024 * 1024}, + } + for _, bm := range benchmarks { + b.Run(bm.String(), func(b *testing.B) { + ctx := context.Background() + client := benchmarkDevsporeRedisClient(ctx, bm.poolSize) + defer client.Close() + + value := strings.Repeat("1", bm.valueSize) + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + err := client.Set(ctx, "key", value, 0).Err() + if err != nil { + b.Fatal(err) + } + } + }) + }) + } +} + +func BenchmarkRedisSetGetBytes(b *testing.B) { + ctx := context.Background() + client := benchmarkDevsporeRedisClient(ctx, 10) + defer client.Close() + + value := bytes.Repeat([]byte{'1'}, 10000) + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := client.Set(ctx, "key", value, 0).Err(); err != nil { + b.Fatal(err) + } + + got, err := client.Get(ctx, "key").Bytes() + if err != nil { + b.Fatal(err) + } + if !bytes.Equal(got, value) { + b.Fatalf("got != value") + } + } + }) +} + +func BenchmarkRedisMGet(b *testing.B) { + ctx := context.Background() + client := benchmarkDevsporeRedisClient(ctx, 10) + defer client.Close() + + if err := client.MSet(ctx, "key1", "hello1", "key2", "hello2").Err(); err != nil { + b.Fatal(err) + } + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := client.MGet(ctx, "key1", "key2").Err(); err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkSetExpire(b *testing.B) { + ctx := context.Background() + client := benchmarkDevsporeRedisClient(ctx, 10) + defer client.Close() + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := client.Set(ctx, "key", "hello", 0).Err(); err != nil { + b.Fatal(err) + } + if err := client.Expire(ctx, "key", time.Second).Err(); err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkPipeline(b *testing.B) { + ctx := context.Background() + client := benchmarkDevsporeRedisClient(ctx, 10) + defer client.Close() + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.Set(ctx, "key", "hello", 0) + pipe.Expire(ctx, "key", time.Second) + return nil + }) + if err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkZAdd(b *testing.B) { + ctx := context.Background() + client := benchmarkDevsporeRedisClient(ctx, 10) + defer client.Close() + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + err := client.ZAdd(ctx, "key", &redis.Z{ + Score: float64(1), + Member: "hello", + }).Err() + if err != nil { + b.Fatal(err) + } + } + }) +} + +var ringSink *redis.Ring + +func BenchmarkRingWithContext(b *testing.B) { + ctx := context.Background() + rdb := redis.NewRing(&redis.RingOptions{}) + defer rdb.Close() + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + ringSink = rdb.WithContext(ctx) + } +} + +func BenchmarkClusterPing(b *testing.B) { + if testing.Short() { + b.Skip("skipping in short mode") + } + + ctx := context.Background() + client := benchmarkDevsporeClusterClient(ctx) + defer client.Close() + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + err := client.Ping(ctx).Err() + if err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkClusterSetString(b *testing.B) { + if testing.Short() { + b.Skip("skipping in short mode") + } + + ctx := context.Background() + client := benchmarkDevsporeClusterClient(ctx) + defer client.Close() + + value := string(bytes.Repeat([]byte{'1'}, 10000)) + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + err := client.Set(ctx, "key", value, 0).Err() + if err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/redis/bench_test/doublewrite_test.go b/redis/bench_test/doublewrite_test.go new file mode 100644 index 0000000..cc240d8 --- /dev/null +++ b/redis/bench_test/doublewrite_test.go @@ -0,0 +1,75 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022. + * Licensed 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 bench_test test performance comparison +package bench_test + +import ( + "context" + "strconv" + "testing" + + goredis "github.com/go-redis/redis/v8" + + "github.com/huaweicloud/devcloud-go/mock" + "github.com/huaweicloud/devcloud-go/redis" +) + +var ( + addr1 = "127.0.0.1:6379" + addr2 = "127.0.0.1:6380" + testKey = "test_key" + testValue = "test_value" + redisMock1 = mock.RedisMock{Addr: addr1} + redisMock2 = mock.RedisMock{Addr: addr2} + ctx = context.Background() + devsporeClient *redis.DevsporeClient + client *goredis.Client +) + +func startService() { + redisMock1.StartMockRedis() + redisMock2.StartMockRedis() + devsporeClient = redis.NewDevsporeClientWithYaml("../resources/config_for_read_write_separate.yaml") + client = goredis.NewClient(&goredis.Options{Addr: addr1}) +} + +func closeService() { + devsporeClient.Close() + client.Close() + redisMock1.StopMockRedis() + redisMock2.StopMockRedis() +} + +func BenchmarkDevsporeClient(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + devsporeClient.Set(ctx, testKey+strconv.Itoa(i), testValue+strconv.Itoa(i), 0) + } +} + +func BenchmarkClient(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + client.Set(ctx, testKey+strconv.Itoa(i), testValue+strconv.Itoa(i), 0) + } +} + +func TestMain(m *testing.M) { + startService() + m.Run() + closeService() + +} diff --git a/redis/commands.go b/redis/commands.go index f7cb56e..e7fff1c 100644 --- a/redis/commands.go +++ b/redis/commands.go @@ -19,1149 +19,1143 @@ import ( "time" "github.com/go-redis/redis/v8" -) - -type commandType int32 - -const ( - commandTypeRead commandType = iota - commandTypeWrite - commandTypeMulti + "github.com/huaweicloud/devcloud-go/redis/strategy" ) func (c *DevsporeClient) Get(ctx context.Context, key string) *redis.StringCmd { - return c.getActualClient(commandTypeRead).Get(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).Get(ctx, key) } func (c *DevsporeClient) Pipeline() redis.Pipeliner { - return c.getActualClient(commandTypeMulti).Pipeline() + return c.strategy.RouteClient(strategy.CommandTypeMulti).Pipeline() } func (c *DevsporeClient) Pipelined(ctx context.Context, fn func(redis.Pipeliner) error) ([]redis.Cmder, error) { - return c.getActualClient(commandTypeMulti).Pipelined(ctx, fn) + return c.strategy.RouteClient(strategy.CommandTypeMulti).Pipelined(ctx, fn) } func (c *DevsporeClient) TxPipeline() redis.Pipeliner { - return c.getActualClient(commandTypeMulti).TxPipeline() + return c.strategy.RouteClient(strategy.CommandTypeMulti).TxPipeline() } func (c *DevsporeClient) TxPipelined(ctx context.Context, fn func(redis.Pipeliner) error) ([]redis.Cmder, error) { - return c.getActualClient(commandTypeMulti).TxPipelined(ctx, fn) + return c.strategy.RouteClient(strategy.CommandTypeMulti).TxPipelined(ctx, fn) } func (c *DevsporeClient) Command(ctx context.Context) *redis.CommandsInfoCmd { - return c.getActualClient(commandTypeRead).Command(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).Command(ctx) } func (c *DevsporeClient) ClientGetName(ctx context.Context) *redis.StringCmd { - return c.getActualClient(commandTypeRead).ClientGetName(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).ClientGetName(ctx) } func (c *DevsporeClient) Echo(ctx context.Context, message interface{}) *redis.StringCmd { - return c.getActualClient(commandTypeRead).Echo(ctx, message) + return c.strategy.RouteClient(strategy.CommandTypeRead).Echo(ctx, message) } func (c *DevsporeClient) Ping(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeRead).Ping(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).Ping(ctx) } func (c *DevsporeClient) Quit(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeRead).Quit(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).Quit(ctx) } func (c *DevsporeClient) Del(ctx context.Context, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).Del(ctx, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Del(ctx, keys...) } func (c *DevsporeClient) Unlink(ctx context.Context, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).Unlink(ctx, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Unlink(ctx, keys...) } func (c *DevsporeClient) Dump(ctx context.Context, key string) *redis.StringCmd { - return c.getActualClient(commandTypeRead).Dump(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).Dump(ctx, key) } func (c *DevsporeClient) Exists(ctx context.Context, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).Exists(ctx, keys...) + return c.strategy.RouteClient(strategy.CommandTypeRead).Exists(ctx, keys...) } func (c *DevsporeClient) Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).Expire(ctx, key, expiration) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Expire(ctx, key, expiration) } func (c *DevsporeClient) ExpireAt(ctx context.Context, key string, tm time.Time) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).ExpireAt(ctx, key, tm) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ExpireAt(ctx, key, tm) } func (c *DevsporeClient) Keys(ctx context.Context, pattern string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).Keys(ctx, pattern) + return c.strategy.RouteClient(strategy.CommandTypeRead).Keys(ctx, pattern) } func (c *DevsporeClient) Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).Migrate(ctx, host, port, key, db, timeout) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Migrate(ctx, host, port, key, db, timeout) } func (c *DevsporeClient) Move(ctx context.Context, key string, db int) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).Move(ctx, key, db) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Move(ctx, key, db) } func (c *DevsporeClient) ObjectRefCount(ctx context.Context, key string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).ObjectRefCount(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).ObjectRefCount(ctx, key) } func (c *DevsporeClient) ObjectEncoding(ctx context.Context, key string) *redis.StringCmd { - return c.getActualClient(commandTypeRead).ObjectEncoding(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).ObjectEncoding(ctx, key) } func (c *DevsporeClient) ObjectIdleTime(ctx context.Context, key string) *redis.DurationCmd { - return c.getActualClient(commandTypeRead).ObjectIdleTime(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).ObjectIdleTime(ctx, key) } func (c *DevsporeClient) Persist(ctx context.Context, key string) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).Persist(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Persist(ctx, key) } func (c *DevsporeClient) PExpire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).PExpire(ctx, key, expiration) + return c.strategy.RouteClient(strategy.CommandTypeWrite).PExpire(ctx, key, expiration) } func (c *DevsporeClient) PExpireAt(ctx context.Context, key string, tm time.Time) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).PExpireAt(ctx, key, tm) + return c.strategy.RouteClient(strategy.CommandTypeWrite).PExpireAt(ctx, key, tm) } func (c *DevsporeClient) PTTL(ctx context.Context, key string) *redis.DurationCmd { - return c.getActualClient(commandTypeRead).PTTL(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).PTTL(ctx, key) } func (c *DevsporeClient) RandomKey(ctx context.Context) *redis.StringCmd { - return c.getActualClient(commandTypeRead).RandomKey(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).RandomKey(ctx) } func (c *DevsporeClient) Rename(ctx context.Context, key, newkey string) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).Rename(ctx, key, newkey) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Rename(ctx, key, newkey) } func (c *DevsporeClient) RenameNX(ctx context.Context, key, newkey string) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).RenameNX(ctx, key, newkey) + return c.strategy.RouteClient(strategy.CommandTypeWrite).RenameNX(ctx, key, newkey) } func (c *DevsporeClient) Restore(ctx context.Context, key string, ttl time.Duration, value string) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).Restore(ctx, key, ttl, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Restore(ctx, key, ttl, value) } func (c *DevsporeClient) RestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).RestoreReplace(ctx, key, ttl, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).RestoreReplace(ctx, key, ttl, value) } func (c *DevsporeClient) Sort(ctx context.Context, key string, sort *redis.Sort) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).Sort(ctx, key, sort) + return c.strategy.RouteClient(strategy.CommandTypeRead).Sort(ctx, key, sort) } func (c *DevsporeClient) SortStore(ctx context.Context, key, store string, sort *redis.Sort) *redis.IntCmd { - return c.getActualClient(commandTypeRead).SortStore(ctx, key, store, sort) + return c.strategy.RouteClient(strategy.CommandTypeRead).SortStore(ctx, key, store, sort) } func (c *DevsporeClient) SortInterfaces(ctx context.Context, key string, sort *redis.Sort) *redis.SliceCmd { - return c.getActualClient(commandTypeRead).SortInterfaces(ctx, key, sort) + return c.strategy.RouteClient(strategy.CommandTypeRead).SortInterfaces(ctx, key, sort) } func (c *DevsporeClient) Touch(ctx context.Context, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).Touch(ctx, keys...) + return c.strategy.RouteClient(strategy.CommandTypeRead).Touch(ctx, keys...) } func (c *DevsporeClient) TTL(ctx context.Context, key string) *redis.DurationCmd { - return c.getActualClient(commandTypeRead).TTL(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).TTL(ctx, key) } func (c *DevsporeClient) Type(ctx context.Context, key string) *redis.StatusCmd { - return c.getActualClient(commandTypeRead).Type(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).Type(ctx, key) } func (c *DevsporeClient) Append(ctx context.Context, key, value string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).Append(ctx, key, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Append(ctx, key, value) } func (c *DevsporeClient) Decr(ctx context.Context, key string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).Decr(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Decr(ctx, key) } func (c *DevsporeClient) DecrBy(ctx context.Context, key string, decrement int64) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).DecrBy(ctx, key, decrement) + return c.strategy.RouteClient(strategy.CommandTypeWrite).DecrBy(ctx, key, decrement) } func (c *DevsporeClient) GetRange(ctx context.Context, key string, start, end int64) *redis.StringCmd { - return c.getActualClient(commandTypeRead).GetRange(ctx, key, start, end) + return c.strategy.RouteClient(strategy.CommandTypeRead).GetRange(ctx, key, start, end) } func (c *DevsporeClient) GetSet(ctx context.Context, key string, value interface{}) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).GetSet(ctx, key, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).GetSet(ctx, key, value) } func (c *DevsporeClient) GetEx(ctx context.Context, key string, expiration time.Duration) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).GetEx(ctx, key, expiration) + return c.strategy.RouteClient(strategy.CommandTypeWrite).GetEx(ctx, key, expiration) } func (c *DevsporeClient) GetDel(ctx context.Context, key string) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).GetDel(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeWrite).GetDel(ctx, key) } func (c *DevsporeClient) Incr(ctx context.Context, key string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).Incr(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Incr(ctx, key) } func (c *DevsporeClient) IncrBy(ctx context.Context, key string, value int64) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).IncrBy(ctx, key, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).IncrBy(ctx, key, value) } func (c *DevsporeClient) IncrByFloat(ctx context.Context, key string, value float64) *redis.FloatCmd { - return c.getActualClient(commandTypeWrite).IncrByFloat(ctx, key, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).IncrByFloat(ctx, key, value) } func (c *DevsporeClient) MGet(ctx context.Context, keys ...string) *redis.SliceCmd { - return c.getActualClient(commandTypeRead).MGet(ctx, keys...) + return c.strategy.RouteClient(strategy.CommandTypeRead).MGet(ctx, keys...) } func (c *DevsporeClient) MSet(ctx context.Context, values ...interface{}) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).MSet(ctx, values...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).MSet(ctx, values...) } func (c *DevsporeClient) MSetNX(ctx context.Context, values ...interface{}) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).MSetNX(ctx, values...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).MSetNX(ctx, values...) } func (c *DevsporeClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).Set(ctx, key, value, expiration) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Set(ctx, key, value, expiration) } func (c *DevsporeClient) SetArgs(ctx context.Context, key string, value interface{}, a redis.SetArgs) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).SetArgs(ctx, key, value, a) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SetArgs(ctx, key, value, a) } func (c *DevsporeClient) SetEX(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).SetEX(ctx, key, value, expiration) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SetEX(ctx, key, value, expiration) } func (c *DevsporeClient) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).SetNX(ctx, key, value, expiration) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SetNX(ctx, key, value, expiration) } func (c *DevsporeClient) SetXX(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).SetXX(ctx, key, value, expiration) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SetXX(ctx, key, value, expiration) } func (c *DevsporeClient) SetRange(ctx context.Context, key string, offset int64, value string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).SetRange(ctx, key, offset, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SetRange(ctx, key, offset, value) } func (c *DevsporeClient) StrLen(ctx context.Context, key string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).StrLen(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).StrLen(ctx, key) } func (c *DevsporeClient) GetBit(ctx context.Context, key string, offset int64) *redis.IntCmd { - return c.getActualClient(commandTypeRead).GetBit(ctx, key, offset) + return c.strategy.RouteClient(strategy.CommandTypeRead).GetBit(ctx, key, offset) } func (c *DevsporeClient) SetBit(ctx context.Context, key string, offset int64, value int) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).SetBit(ctx, key, offset, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SetBit(ctx, key, offset, value) } func (c *DevsporeClient) BitCount(ctx context.Context, key string, bitCount *redis.BitCount) *redis.IntCmd { - return c.getActualClient(commandTypeRead).BitCount(ctx, key, bitCount) + return c.strategy.RouteClient(strategy.CommandTypeRead).BitCount(ctx, key, bitCount) } func (c *DevsporeClient) BitOpAnd(ctx context.Context, destKey string, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).BitOpAnd(ctx, destKey, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BitOpAnd(ctx, destKey, keys...) } func (c *DevsporeClient) BitOpOr(ctx context.Context, destKey string, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).BitOpOr(ctx, destKey, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BitOpOr(ctx, destKey, keys...) } func (c *DevsporeClient) BitOpXor(ctx context.Context, destKey string, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).BitOpXor(ctx, destKey, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BitOpXor(ctx, destKey, keys...) } func (c *DevsporeClient) BitOpNot(ctx context.Context, destKey string, key string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).BitOpNot(ctx, destKey, key) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BitOpNot(ctx, destKey, key) } func (c *DevsporeClient) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *redis.IntCmd { - return c.getActualClient(commandTypeRead).BitPos(ctx, key, bit, pos...) + return c.strategy.RouteClient(strategy.CommandTypeRead).BitPos(ctx, key, bit, pos...) } func (c *DevsporeClient) BitField(ctx context.Context, key string, args ...interface{}) *redis.IntSliceCmd { - return c.getActualClient(commandTypeWrite).BitField(ctx, key, args...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BitField(ctx, key, args...) } func (c *DevsporeClient) Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd { - return c.getActualClient(commandTypeRead).Scan(ctx, cursor, match, count) + return c.strategy.RouteClient(strategy.CommandTypeRead).Scan(ctx, cursor, match, count) } func (c *DevsporeClient) ScanType(ctx context.Context, cursor uint64, match string, count int64, keyType string) *redis.ScanCmd { - return c.getActualClient(commandTypeRead).ScanType(ctx, cursor, match, count, keyType) + return c.strategy.RouteClient(strategy.CommandTypeRead).ScanType(ctx, cursor, match, count, keyType) } func (c *DevsporeClient) SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *redis.ScanCmd { - return c.getActualClient(commandTypeRead).SScan(ctx, key, cursor, match, count) + return c.strategy.RouteClient(strategy.CommandTypeRead).SScan(ctx, key, cursor, match, count) } func (c *DevsporeClient) HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *redis.ScanCmd { - return c.getActualClient(commandTypeRead).HScan(ctx, key, cursor, match, count) + return c.strategy.RouteClient(strategy.CommandTypeRead).HScan(ctx, key, cursor, match, count) } func (c *DevsporeClient) ZScan(ctx context.Context, key string, cursor uint64, match string, count int64) *redis.ScanCmd { - return c.getActualClient(commandTypeRead).ZScan(ctx, key, cursor, match, count) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZScan(ctx, key, cursor, match, count) } func (c *DevsporeClient) HDel(ctx context.Context, key string, fields ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).HDel(ctx, key, fields...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).HDel(ctx, key, fields...) } func (c *DevsporeClient) HExists(ctx context.Context, key, field string) *redis.BoolCmd { - return c.getActualClient(commandTypeRead).HExists(ctx, key, field) + return c.strategy.RouteClient(strategy.CommandTypeRead).HExists(ctx, key, field) } func (c *DevsporeClient) HGet(ctx context.Context, key, field string) *redis.StringCmd { - return c.getActualClient(commandTypeRead).HGet(ctx, key, field) + return c.strategy.RouteClient(strategy.CommandTypeRead).HGet(ctx, key, field) } func (c *DevsporeClient) HGetAll(ctx context.Context, key string) *redis.StringStringMapCmd { - return c.getActualClient(commandTypeRead).HGetAll(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).HGetAll(ctx, key) } func (c *DevsporeClient) HIncrBy(ctx context.Context, key, field string, incr int64) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).HIncrBy(ctx, key, field, incr) + return c.strategy.RouteClient(strategy.CommandTypeWrite).HIncrBy(ctx, key, field, incr) } func (c *DevsporeClient) HIncrByFloat(ctx context.Context, key, field string, incr float64) *redis.FloatCmd { - return c.getActualClient(commandTypeWrite).HIncrByFloat(ctx, key, field, incr) + return c.strategy.RouteClient(strategy.CommandTypeWrite).HIncrByFloat(ctx, key, field, incr) } func (c *DevsporeClient) HKeys(ctx context.Context, key string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).HKeys(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).HKeys(ctx, key) } func (c *DevsporeClient) HLen(ctx context.Context, key string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).HLen(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).HLen(ctx, key) } func (c *DevsporeClient) HMGet(ctx context.Context, key string, fields ...string) *redis.SliceCmd { - return c.getActualClient(commandTypeRead).HMGet(ctx, key, fields...) + return c.strategy.RouteClient(strategy.CommandTypeRead).HMGet(ctx, key, fields...) } func (c *DevsporeClient) HSet(ctx context.Context, key string, values ...interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).HSet(ctx, key, values...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).HSet(ctx, key, values...) } func (c *DevsporeClient) HMSet(ctx context.Context, key string, values ...interface{}) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).HMSet(ctx, key, values...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).HMSet(ctx, key, values...) } func (c *DevsporeClient) HSetNX(ctx context.Context, key, field string, value interface{}) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).HSetNX(ctx, key, field, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).HSetNX(ctx, key, field, value) } func (c *DevsporeClient) HVals(ctx context.Context, key string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).HVals(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).HVals(ctx, key) } func (c *DevsporeClient) HRandField(ctx context.Context, key string, count int, withValues bool) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).HRandField(ctx, key, count, withValues) + return c.strategy.RouteClient(strategy.CommandTypeRead).HRandField(ctx, key, count, withValues) } func (c *DevsporeClient) BLPop(ctx context.Context, timeout time.Duration, keys ...string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeWrite).BLPop(ctx, timeout, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BLPop(ctx, timeout, keys...) } func (c *DevsporeClient) BRPop(ctx context.Context, timeout time.Duration, keys ...string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeWrite).BRPop(ctx, timeout, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BRPop(ctx, timeout, keys...) } func (c *DevsporeClient) BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).BRPopLPush(ctx, source, destination, timeout) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BRPopLPush(ctx, source, destination, timeout) } func (c *DevsporeClient) LIndex(ctx context.Context, key string, index int64) *redis.StringCmd { - return c.getActualClient(commandTypeRead).LIndex(ctx, key, index) + return c.strategy.RouteClient(strategy.CommandTypeRead).LIndex(ctx, key, index) } func (c *DevsporeClient) LInsert(ctx context.Context, key, op string, pivot, value interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).LInsert(ctx, key, op, pivot, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LInsert(ctx, key, op, pivot, value) } func (c *DevsporeClient) LInsertBefore(ctx context.Context, key string, pivot, value interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).LInsertBefore(ctx, key, pivot, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LInsertBefore(ctx, key, pivot, value) } func (c *DevsporeClient) LInsertAfter(ctx context.Context, key string, pivot, value interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).LInsertAfter(ctx, key, pivot, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LInsertAfter(ctx, key, pivot, value) } func (c *DevsporeClient) LLen(ctx context.Context, key string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).LLen(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).LLen(ctx, key) } func (c *DevsporeClient) LPop(ctx context.Context, key string) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).LPop(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LPop(ctx, key) } func (c *DevsporeClient) LPopCount(ctx context.Context, key string, count int) *redis.StringSliceCmd { - return c.getActualClient(commandTypeWrite).LPopCount(ctx, key, count) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LPopCount(ctx, key, count) } func (c *DevsporeClient) LPos(ctx context.Context, key string, value string, args redis.LPosArgs) *redis.IntCmd { - return c.getActualClient(commandTypeRead).LPos(ctx, key, value, args) + return c.strategy.RouteClient(strategy.CommandTypeRead).LPos(ctx, key, value, args) } func (c *DevsporeClient) LPosCount(ctx context.Context, key string, value string, count int64, args redis.LPosArgs) *redis.IntSliceCmd { - return c.getActualClient(commandTypeRead).LPosCount(ctx, key, value, count, args) + return c.strategy.RouteClient(strategy.CommandTypeRead).LPosCount(ctx, key, value, count, args) } func (c *DevsporeClient) LPush(ctx context.Context, key string, values ...interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).LPush(ctx, key, values...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LPush(ctx, key, values...) } func (c *DevsporeClient) LPushX(ctx context.Context, key string, values ...interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).LPushX(ctx, key, values...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LPushX(ctx, key, values...) } func (c *DevsporeClient) LRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).LRange(ctx, key, start, stop) + return c.strategy.RouteClient(strategy.CommandTypeRead).LRange(ctx, key, start, stop) } func (c *DevsporeClient) LRem(ctx context.Context, key string, count int64, value interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).LRem(ctx, key, count, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LRem(ctx, key, count, value) } func (c *DevsporeClient) LSet(ctx context.Context, key string, index int64, value interface{}) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).LSet(ctx, key, index, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LSet(ctx, key, index, value) } func (c *DevsporeClient) LTrim(ctx context.Context, key string, start, stop int64) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).LTrim(ctx, key, start, stop) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LTrim(ctx, key, start, stop) } func (c *DevsporeClient) RPop(ctx context.Context, key string) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).RPop(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeWrite).RPop(ctx, key) } func (c *DevsporeClient) RPopCount(ctx context.Context, key string, count int) *redis.StringSliceCmd { - return c.getActualClient(commandTypeWrite).RPopCount(ctx, key, count) + return c.strategy.RouteClient(strategy.CommandTypeWrite).RPopCount(ctx, key, count) } func (c *DevsporeClient) RPopLPush(ctx context.Context, source, destination string) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).RPopLPush(ctx, source, destination) + return c.strategy.RouteClient(strategy.CommandTypeWrite).RPopLPush(ctx, source, destination) } func (c *DevsporeClient) RPush(ctx context.Context, key string, values ...interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).RPush(ctx, key, values...) + + return c.strategy.RouteClient(strategy.CommandTypeWrite).RPush(ctx, key, values...) } func (c *DevsporeClient) RPushX(ctx context.Context, key string, values ...interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).RPushX(ctx, key, values...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).RPushX(ctx, key, values...) } func (c *DevsporeClient) LMove(ctx context.Context, source, destination, srcpos, destpos string) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).LMove(ctx, source, destination, srcpos, destpos) + return c.strategy.RouteClient(strategy.CommandTypeWrite).LMove(ctx, source, destination, srcpos, destpos) } func (c *DevsporeClient) SAdd(ctx context.Context, key string, members ...interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).SAdd(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SAdd(ctx, key, members...) } func (c *DevsporeClient) SCard(ctx context.Context, key string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).SCard(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).SCard(ctx, key) } func (c *DevsporeClient) SDiff(ctx context.Context, key ...string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).SDiff(ctx, key...) + return c.strategy.RouteClient(strategy.CommandTypeRead).SDiff(ctx, key...) } func (c *DevsporeClient) SDiffStore(ctx context.Context, destination string, key ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).SDiffStore(ctx, destination, key...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SDiffStore(ctx, destination, key...) } func (c *DevsporeClient) SInter(ctx context.Context, key ...string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).SInter(ctx, key...) + return c.strategy.RouteClient(strategy.CommandTypeRead).SInter(ctx, key...) } func (c *DevsporeClient) SInterStore(ctx context.Context, destination string, key ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).SInterStore(ctx, destination, key...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SInterStore(ctx, destination, key...) } func (c *DevsporeClient) SIsMember(ctx context.Context, key string, member interface{}) *redis.BoolCmd { - return c.getActualClient(commandTypeRead).SIsMember(ctx, key, member) + return c.strategy.RouteClient(strategy.CommandTypeRead).SIsMember(ctx, key, member) } func (c *DevsporeClient) SMIsMember(ctx context.Context, key string, members ...interface{}) *redis.BoolSliceCmd { - return c.getActualClient(commandTypeRead).SMIsMember(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeRead).SMIsMember(ctx, key, members...) } func (c *DevsporeClient) SMembers(ctx context.Context, key string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).SMembers(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).SMembers(ctx, key) } func (c *DevsporeClient) SMembersMap(ctx context.Context, key string) *redis.StringStructMapCmd { - return c.getActualClient(commandTypeRead).SMembersMap(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).SMembersMap(ctx, key) } func (c *DevsporeClient) SMove(ctx context.Context, source, destination string, member interface{}) *redis.BoolCmd { - return c.getActualClient(commandTypeWrite).SMove(ctx, source, destination, member) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SMove(ctx, source, destination, member) } func (c *DevsporeClient) SPop(ctx context.Context, key string) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).SPop(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SPop(ctx, key) } func (c *DevsporeClient) SPopN(ctx context.Context, key string, count int64) *redis.StringSliceCmd { - return c.getActualClient(commandTypeWrite).SPopN(ctx, key, count) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SPopN(ctx, key, count) } func (c *DevsporeClient) SRandMember(ctx context.Context, key string) *redis.StringCmd { - return c.getActualClient(commandTypeRead).SRandMember(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).SRandMember(ctx, key) } func (c *DevsporeClient) SRandMemberN(ctx context.Context, key string, count int64) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).SRandMemberN(ctx, key, count) + return c.strategy.RouteClient(strategy.CommandTypeRead).SRandMemberN(ctx, key, count) } func (c *DevsporeClient) SRem(ctx context.Context, key string, members ...interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).SRem(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SRem(ctx, key, members...) } func (c *DevsporeClient) SUnion(ctx context.Context, key ...string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).SUnion(ctx, key...) + return c.strategy.RouteClient(strategy.CommandTypeRead).SUnion(ctx, key...) } func (c *DevsporeClient) SUnionStore(ctx context.Context, destination string, key ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).SUnionStore(ctx, destination, key...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).SUnionStore(ctx, destination, key...) } func (c *DevsporeClient) XAdd(ctx context.Context, a *redis.XAddArgs) *redis.StringCmd { - return c.getActualClient(commandTypeWrite).XAdd(ctx, a) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XAdd(ctx, a) } func (c *DevsporeClient) XDel(ctx context.Context, stream string, ids ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XDel(ctx, stream, ids...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XDel(ctx, stream, ids...) } func (c *DevsporeClient) XLen(ctx context.Context, stream string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).XLen(ctx, stream) + return c.strategy.RouteClient(strategy.CommandTypeRead).XLen(ctx, stream) } func (c *DevsporeClient) XRange(ctx context.Context, stream, start, stop string) *redis.XMessageSliceCmd { - return c.getActualClient(commandTypeRead).XRange(ctx, stream, start, stop) + return c.strategy.RouteClient(strategy.CommandTypeRead).XRange(ctx, stream, start, stop) } func (c *DevsporeClient) XRangeN(ctx context.Context, stream, start, stop string, count int64) *redis.XMessageSliceCmd { - return c.getActualClient(commandTypeRead).XRangeN(ctx, stream, start, stop, count) + return c.strategy.RouteClient(strategy.CommandTypeRead).XRangeN(ctx, stream, start, stop, count) } func (c *DevsporeClient) XRevRange(ctx context.Context, stream, start, stop string) *redis.XMessageSliceCmd { - return c.getActualClient(commandTypeRead).XRevRange(ctx, stream, start, stop) + return c.strategy.RouteClient(strategy.CommandTypeRead).XRevRange(ctx, stream, start, stop) } func (c *DevsporeClient) XRevRangeN(ctx context.Context, stream, start, stop string, count int64) *redis.XMessageSliceCmd { - return c.getActualClient(commandTypeRead).XRevRangeN(ctx, stream, start, stop, count) + return c.strategy.RouteClient(strategy.CommandTypeRead).XRevRangeN(ctx, stream, start, stop, count) } func (c *DevsporeClient) XRead(ctx context.Context, a *redis.XReadArgs) *redis.XStreamSliceCmd { - return c.getActualClient(commandTypeRead).XRead(ctx, a) + return c.strategy.RouteClient(strategy.CommandTypeRead).XRead(ctx, a) } func (c *DevsporeClient) XReadStreams(ctx context.Context, streams ...string) *redis.XStreamSliceCmd { - return c.getActualClient(commandTypeRead).XReadStreams(ctx, streams...) + return c.strategy.RouteClient(strategy.CommandTypeRead).XReadStreams(ctx, streams...) } func (c *DevsporeClient) XGroupCreate(ctx context.Context, stream, group, start string) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).XGroupCreate(ctx, stream, group, start) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XGroupCreate(ctx, stream, group, start) } func (c *DevsporeClient) XGroupCreateMkStream(ctx context.Context, stream, group, start string) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).XGroupCreateMkStream(ctx, stream, group, start) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XGroupCreateMkStream(ctx, stream, group, start) } func (c *DevsporeClient) XGroupSetID(ctx context.Context, stream, group, start string) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).XGroupSetID(ctx, stream, group, start) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XGroupSetID(ctx, stream, group, start) } func (c *DevsporeClient) XGroupDestroy(ctx context.Context, stream, group string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XGroupDestroy(ctx, stream, group) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XGroupDestroy(ctx, stream, group) } func (c *DevsporeClient) XGroupCreateConsumer(ctx context.Context, stream, group, consumer string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XGroupCreateConsumer(ctx, stream, group, consumer) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XGroupCreateConsumer(ctx, stream, group, consumer) } func (c *DevsporeClient) XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XGroupDelConsumer(ctx, stream, group, consumer) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XGroupDelConsumer(ctx, stream, group, consumer) } func (c *DevsporeClient) XReadGroup(ctx context.Context, a *redis.XReadGroupArgs) *redis.XStreamSliceCmd { - return c.getActualClient(commandTypeRead).XReadGroup(ctx, a) + return c.strategy.RouteClient(strategy.CommandTypeRead).XReadGroup(ctx, a) } func (c *DevsporeClient) XAck(ctx context.Context, stream, group string, ids ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XAck(ctx, stream, group, ids...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XAck(ctx, stream, group, ids...) } func (c *DevsporeClient) XPending(ctx context.Context, stream, group string) *redis.XPendingCmd { - return c.getActualClient(commandTypeWrite).XPending(ctx, stream, group) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XPending(ctx, stream, group) } func (c *DevsporeClient) XPendingExt(ctx context.Context, a *redis.XPendingExtArgs) *redis.XPendingExtCmd { - return c.getActualClient(commandTypeWrite).XPendingExt(ctx, a) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XPendingExt(ctx, a) } func (c *DevsporeClient) XClaim(ctx context.Context, a *redis.XClaimArgs) *redis.XMessageSliceCmd { - return c.getActualClient(commandTypeWrite).XClaim(ctx, a) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XClaim(ctx, a) } func (c *DevsporeClient) XClaimJustID(ctx context.Context, a *redis.XClaimArgs) *redis.StringSliceCmd { - return c.getActualClient(commandTypeWrite).XClaimJustID(ctx, a) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XClaimJustID(ctx, a) } func (c *DevsporeClient) XAutoClaim(ctx context.Context, a *redis.XAutoClaimArgs) *redis.XAutoClaimCmd { - return c.getActualClient(commandTypeWrite).XAutoClaim(ctx, a) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XAutoClaim(ctx, a) } func (c *DevsporeClient) XAutoClaimJustID(ctx context.Context, a *redis.XAutoClaimArgs) *redis.XAutoClaimJustIDCmd { - return c.getActualClient(commandTypeWrite).XAutoClaimJustID(ctx, a) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XAutoClaimJustID(ctx, a) } func (c *DevsporeClient) XTrim(ctx context.Context, key string, maxLen int64) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XTrim(ctx, key, maxLen) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XTrim(ctx, key, maxLen) } func (c *DevsporeClient) XTrimApprox(ctx context.Context, key string, maxLen int64) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XTrimApprox(ctx, key, maxLen) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XTrimApprox(ctx, key, maxLen) } func (c *DevsporeClient) XTrimMaxLen(ctx context.Context, key string, maxLen int64) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XTrimMaxLen(ctx, key, maxLen) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XTrimMaxLen(ctx, key, maxLen) } func (c *DevsporeClient) XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XTrimMaxLenApprox(ctx, key, maxLen, limit) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XTrimMaxLenApprox(ctx, key, maxLen, limit) } func (c *DevsporeClient) XTrimMinID(ctx context.Context, key string, minID string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XTrimMinID(ctx, key, minID) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XTrimMinID(ctx, key, minID) } func (c *DevsporeClient) XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).XTrimMinIDApprox(ctx, key, minID, limit) + return c.strategy.RouteClient(strategy.CommandTypeWrite).XTrimMinIDApprox(ctx, key, minID, limit) } func (c *DevsporeClient) XInfoGroups(ctx context.Context, key string) *redis.XInfoGroupsCmd { - return c.getActualClient(commandTypeRead).XInfoGroups(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).XInfoGroups(ctx, key) } func (c *DevsporeClient) XInfoStream(ctx context.Context, key string) *redis.XInfoStreamCmd { - return c.getActualClient(commandTypeRead).XInfoStream(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).XInfoStream(ctx, key) } func (c *DevsporeClient) XInfoStreamFull(ctx context.Context, key string, count int) *redis.XInfoStreamFullCmd { - return c.getActualClient(commandTypeRead).XInfoStreamFull(ctx, key, count) + return c.strategy.RouteClient(strategy.CommandTypeRead).XInfoStreamFull(ctx, key, count) } func (c *DevsporeClient) XInfoConsumers(ctx context.Context, key string, group string) *redis.XInfoConsumersCmd { - return c.getActualClient(commandTypeRead).XInfoConsumers(ctx, key, group) + return c.strategy.RouteClient(strategy.CommandTypeRead).XInfoConsumers(ctx, key, group) } func (c *DevsporeClient) BZPopMax(ctx context.Context, timeout time.Duration, keys ...string) *redis.ZWithKeyCmd { - return c.getActualClient(commandTypeWrite).BZPopMax(ctx, timeout, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BZPopMax(ctx, timeout, keys...) } func (c *DevsporeClient) BZPopMin(ctx context.Context, timeout time.Duration, keys ...string) *redis.ZWithKeyCmd { - return c.getActualClient(commandTypeWrite).BZPopMin(ctx, timeout, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BZPopMin(ctx, timeout, keys...) } func (c *DevsporeClient) ZAdd(ctx context.Context, key string, members ...*redis.Z) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZAdd(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZAdd(ctx, key, members...) } func (c *DevsporeClient) ZAddNX(ctx context.Context, key string, members ...*redis.Z) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZAddNX(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZAddNX(ctx, key, members...) } func (c *DevsporeClient) ZAddXX(ctx context.Context, key string, members ...*redis.Z) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZAddXX(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZAddXX(ctx, key, members...) } func (c *DevsporeClient) ZAddCh(ctx context.Context, key string, members ...*redis.Z) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZAddCh(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZAddCh(ctx, key, members...) } func (c *DevsporeClient) ZAddNXCh(ctx context.Context, key string, members ...*redis.Z) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZAddNXCh(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZAddNXCh(ctx, key, members...) } func (c *DevsporeClient) ZAddXXCh(ctx context.Context, key string, members ...*redis.Z) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZAddXXCh(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZAddXXCh(ctx, key, members...) } func (c *DevsporeClient) ZAddArgs(ctx context.Context, key string, args redis.ZAddArgs) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZAddArgs(ctx, key, args) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZAddArgs(ctx, key, args) } func (c *DevsporeClient) ZAddArgsIncr(ctx context.Context, key string, args redis.ZAddArgs) *redis.FloatCmd { - return c.getActualClient(commandTypeWrite).ZAddArgsIncr(ctx, key, args) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZAddArgsIncr(ctx, key, args) } func (c *DevsporeClient) ZIncr(ctx context.Context, key string, member *redis.Z) *redis.FloatCmd { - return c.getActualClient(commandTypeWrite).ZIncr(ctx, key, member) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZIncr(ctx, key, member) } func (c *DevsporeClient) ZIncrNX(ctx context.Context, key string, member *redis.Z) *redis.FloatCmd { - return c.getActualClient(commandTypeWrite).ZIncrNX(ctx, key, member) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZIncrNX(ctx, key, member) } func (c *DevsporeClient) ZIncrXX(ctx context.Context, key string, member *redis.Z) *redis.FloatCmd { - return c.getActualClient(commandTypeWrite).ZIncrXX(ctx, key, member) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZIncrXX(ctx, key, member) } func (c *DevsporeClient) ZCard(ctx context.Context, key string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).ZCard(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZCard(ctx, key) } func (c *DevsporeClient) ZCount(ctx context.Context, key, min, max string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).ZCount(ctx, key, min, max) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZCount(ctx, key, min, max) } func (c *DevsporeClient) ZLexCount(ctx context.Context, key, min, max string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).ZLexCount(ctx, key, min, max) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZLexCount(ctx, key, min, max) } func (c *DevsporeClient) ZIncrBy(ctx context.Context, key string, increment float64, member string) *redis.FloatCmd { - return c.getActualClient(commandTypeWrite).ZIncrBy(ctx, key, increment, member) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZIncrBy(ctx, key, increment, member) } func (c *DevsporeClient) ZInter(ctx context.Context, store *redis.ZStore) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZInter(ctx, store) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZInter(ctx, store) } func (c *DevsporeClient) ZInterWithScores(ctx context.Context, store *redis.ZStore) *redis.ZSliceCmd { - return c.getActualClient(commandTypeRead).ZInterWithScores(ctx, store) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZInterWithScores(ctx, store) } func (c *DevsporeClient) ZInterStore(ctx context.Context, destination string, store *redis.ZStore) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZInterStore(ctx, destination, store) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZInterStore(ctx, destination, store) } func (c *DevsporeClient) ZMScore(ctx context.Context, key string, members ...string) *redis.FloatSliceCmd { - return c.getActualClient(commandTypeRead).ZMScore(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZMScore(ctx, key, members...) } func (c *DevsporeClient) ZPopMax(ctx context.Context, key string, count ...int64) *redis.ZSliceCmd { - return c.getActualClient(commandTypeWrite).ZPopMax(ctx, key, count...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZPopMax(ctx, key, count...) } func (c *DevsporeClient) ZPopMin(ctx context.Context, key string, count ...int64) *redis.ZSliceCmd { - return c.getActualClient(commandTypeWrite).ZPopMin(ctx, key, count...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZPopMin(ctx, key, count...) } func (c *DevsporeClient) ZRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZRange(ctx, key, start, stop) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRange(ctx, key, start, stop) } func (c *DevsporeClient) ZRangeWithScores(ctx context.Context, key string, start, stop int64) *redis.ZSliceCmd { - return c.getActualClient(commandTypeRead).ZRangeWithScores(ctx, key, start, stop) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRangeWithScores(ctx, key, start, stop) } func (c *DevsporeClient) ZRangeByScore(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZRangeByScore(ctx, key, opt) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRangeByScore(ctx, key, opt) } func (c *DevsporeClient) ZRangeByLex(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZRangeByLex(ctx, key, opt) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRangeByLex(ctx, key, opt) } func (c *DevsporeClient) ZRangeByScoreWithScores(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.ZSliceCmd { - return c.getActualClient(commandTypeRead).ZRangeByScoreWithScores(ctx, key, opt) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRangeByScoreWithScores(ctx, key, opt) } func (c *DevsporeClient) ZRangeArgs(ctx context.Context, z redis.ZRangeArgs) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZRangeArgs(ctx, z) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRangeArgs(ctx, z) } func (c *DevsporeClient) ZRangeArgsWithScores(ctx context.Context, z redis.ZRangeArgs) *redis.ZSliceCmd { - return c.getActualClient(commandTypeRead).ZRangeArgsWithScores(ctx, z) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRangeArgsWithScores(ctx, z) } func (c *DevsporeClient) ZRangeStore(ctx context.Context, dst string, z redis.ZRangeArgs) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZRangeStore(ctx, dst, z) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZRangeStore(ctx, dst, z) } func (c *DevsporeClient) ZRank(ctx context.Context, key, member string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).ZRank(ctx, key, member) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRank(ctx, key, member) } func (c *DevsporeClient) ZRem(ctx context.Context, key string, members ...interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZRem(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZRem(ctx, key, members...) } func (c *DevsporeClient) ZRemRangeByRank(ctx context.Context, key string, start, stop int64) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZRemRangeByRank(ctx, key, start, stop) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZRemRangeByRank(ctx, key, start, stop) } func (c *DevsporeClient) ZRemRangeByScore(ctx context.Context, key, min, max string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZRemRangeByScore(ctx, key, min, max) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZRemRangeByScore(ctx, key, min, max) } func (c *DevsporeClient) ZRemRangeByLex(ctx context.Context, key, min, max string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZRemRangeByLex(ctx, key, min, max) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZRemRangeByLex(ctx, key, min, max) } func (c *DevsporeClient) ZRevRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZRevRange(ctx, key, start, stop) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRevRange(ctx, key, start, stop) } func (c *DevsporeClient) ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) *redis.ZSliceCmd { - return c.getActualClient(commandTypeRead).ZRevRangeWithScores(ctx, key, start, stop) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRevRangeWithScores(ctx, key, start, stop) } func (c *DevsporeClient) ZRevRangeByScore(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZRevRangeByScore(ctx, key, opt) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRevRangeByScore(ctx, key, opt) } func (c *DevsporeClient) ZRevRangeByLex(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZRevRangeByLex(ctx, key, opt) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRevRangeByLex(ctx, key, opt) } func (c *DevsporeClient) ZRevRangeByScoreWithScores(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.ZSliceCmd { - return c.getActualClient(commandTypeRead).ZRevRangeByScoreWithScores(ctx, key, opt) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRevRangeByScoreWithScores(ctx, key, opt) } func (c *DevsporeClient) ZRevRank(ctx context.Context, key, member string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).ZRevRank(ctx, key, member) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRevRank(ctx, key, member) } func (c *DevsporeClient) ZScore(ctx context.Context, key, member string) *redis.FloatCmd { - return c.getActualClient(commandTypeRead).ZScore(ctx, key, member) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZScore(ctx, key, member) } func (c *DevsporeClient) ZUnionStore(ctx context.Context, dest string, store *redis.ZStore) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZUnionStore(ctx, dest, store) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZUnionStore(ctx, dest, store) } func (c *DevsporeClient) ZUnion(ctx context.Context, store redis.ZStore) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZUnion(ctx, store) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZUnion(ctx, store) } func (c *DevsporeClient) ZUnionWithScores(ctx context.Context, store redis.ZStore) *redis.ZSliceCmd { - return c.getActualClient(commandTypeRead).ZUnionWithScores(ctx, store) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZUnionWithScores(ctx, store) } func (c *DevsporeClient) ZRandMember(ctx context.Context, key string, count int, withScores bool) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZRandMember(ctx, key, count, withScores) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZRandMember(ctx, key, count, withScores) } func (c *DevsporeClient) ZDiff(ctx context.Context, keys ...string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).ZDiff(ctx, keys...) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZDiff(ctx, keys...) } func (c *DevsporeClient) ZDiffWithScores(ctx context.Context, keys ...string) *redis.ZSliceCmd { - return c.getActualClient(commandTypeRead).ZDiffWithScores(ctx, keys...) + return c.strategy.RouteClient(strategy.CommandTypeRead).ZDiffWithScores(ctx, keys...) } func (c *DevsporeClient) ZDiffStore(ctx context.Context, destination string, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ZDiffStore(ctx, destination, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ZDiffStore(ctx, destination, keys...) } func (c *DevsporeClient) PFAdd(ctx context.Context, key string, els ...interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).PFAdd(ctx, key, els...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).PFAdd(ctx, key, els...) } func (c *DevsporeClient) PFCount(ctx context.Context, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeRead).PFCount(ctx, keys...) + return c.strategy.RouteClient(strategy.CommandTypeRead).PFCount(ctx, keys...) } func (c *DevsporeClient) PFMerge(ctx context.Context, dest string, keys ...string) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).PFMerge(ctx, dest, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).PFMerge(ctx, dest, keys...) } func (c *DevsporeClient) BgRewriteAOF(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).BgRewriteAOF(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BgRewriteAOF(ctx) } func (c *DevsporeClient) BgSave(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).BgSave(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).BgSave(ctx) } func (c *DevsporeClient) ClientKill(ctx context.Context, ipPort string) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).ClientKill(ctx, ipPort) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ClientKill(ctx, ipPort) } func (c *DevsporeClient) ClientKillByFilter(ctx context.Context, keys ...string) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).ClientKillByFilter(ctx, keys...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ClientKillByFilter(ctx, keys...) } func (c *DevsporeClient) ClientList(ctx context.Context) *redis.StringCmd { - return c.getActualClient(commandTypeRead).ClientList(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).ClientList(ctx) } func (c *DevsporeClient) ClientPause(ctx context.Context, dur time.Duration) *redis.BoolCmd { - return c.getActualClient(commandTypeRead).ClientPause(ctx, dur) + return c.strategy.RouteClient(strategy.CommandTypeRead).ClientPause(ctx, dur) } func (c *DevsporeClient) ClientID(ctx context.Context) *redis.IntCmd { - return c.getActualClient(commandTypeRead).ClientID(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).ClientID(ctx) } func (c *DevsporeClient) ConfigGet(ctx context.Context, parameter string) *redis.SliceCmd { - return c.getActualClient(commandTypeRead).ConfigGet(ctx, parameter) + return c.strategy.RouteClient(strategy.CommandTypeRead).ConfigGet(ctx, parameter) } func (c *DevsporeClient) ConfigResetStat(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).ConfigResetStat(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ConfigResetStat(ctx) } func (c *DevsporeClient) ConfigSet(ctx context.Context, parameter, value string) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).ConfigSet(ctx, parameter, value) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ConfigSet(ctx, parameter, value) } func (c *DevsporeClient) ConfigRewrite(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).ConfigRewrite(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ConfigRewrite(ctx) } func (c *DevsporeClient) DBSize(ctx context.Context) *redis.IntCmd { - return c.getActualClient(commandTypeRead).DBSize(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).DBSize(ctx) } func (c *DevsporeClient) FlushAll(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).FlushAll(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).FlushAll(ctx) } func (c *DevsporeClient) FlushAllAsync(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).FlushAllAsync(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).FlushAllAsync(ctx) } func (c *DevsporeClient) FlushDB(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).FlushDB(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).FlushDB(ctx) } func (c *DevsporeClient) FlushDBAsync(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).FlushDBAsync(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).FlushDBAsync(ctx) } func (c *DevsporeClient) Info(ctx context.Context, section ...string) *redis.StringCmd { - return c.getActualClient(commandTypeRead).Info(ctx, section...) + return c.strategy.RouteClient(strategy.CommandTypeRead).Info(ctx, section...) } func (c *DevsporeClient) LastSave(ctx context.Context) *redis.IntCmd { - return c.getActualClient(commandTypeRead).LastSave(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).LastSave(ctx) } func (c *DevsporeClient) Save(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).Save(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Save(ctx) } func (c *DevsporeClient) Shutdown(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).Shutdown(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Shutdown(ctx) } func (c *DevsporeClient) ShutdownSave(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).ShutdownSave(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ShutdownSave(ctx) } func (c *DevsporeClient) ShutdownNoSave(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeWrite).ShutdownNoSave(ctx) + return c.strategy.RouteClient(strategy.CommandTypeWrite).ShutdownNoSave(ctx) } func (c *DevsporeClient) SlaveOf(ctx context.Context, host, port string) *redis.StatusCmd { - return c.getActualClient(commandTypeRead).SlaveOf(ctx, host, port) + return c.strategy.RouteClient(strategy.CommandTypeRead).SlaveOf(ctx, host, port) } func (c *DevsporeClient) Time(ctx context.Context) *redis.TimeCmd { - return c.getActualClient(commandTypeRead).Time(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).Time(ctx) } func (c *DevsporeClient) DebugObject(ctx context.Context, key string) *redis.StringCmd { - return c.getActualClient(commandTypeRead).DebugObject(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeRead).DebugObject(ctx, key) } func (c *DevsporeClient) ReadOnly(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeRead).ReadOnly(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).ReadOnly(ctx) } func (c *DevsporeClient) ReadWrite(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeRead).ReadWrite(ctx) + return c.strategy.RouteClient(strategy.CommandTypeRead).ReadWrite(ctx) } func (c *DevsporeClient) MemoryUsage(ctx context.Context, key string, samples ...int) *redis.IntCmd { - return c.getActualClient(commandTypeRead).MemoryUsage(ctx, key, samples...) + return c.strategy.RouteClient(strategy.CommandTypeRead).MemoryUsage(ctx, key, samples...) } func (c *DevsporeClient) Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd { - return c.getActualClient(commandTypeMulti).Eval(ctx, script, keys, args...) + return c.strategy.RouteClient(strategy.CommandTypeMulti).Eval(ctx, script, keys, args...) } func (c *DevsporeClient) EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { - return c.getActualClient(commandTypeMulti).EvalSha(ctx, sha1, keys, args...) + return c.strategy.RouteClient(strategy.CommandTypeMulti).EvalSha(ctx, sha1, keys, args...) } func (c *DevsporeClient) ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd { - return c.getActualClient(commandTypeMulti).ScriptExists(ctx, hashes...) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ScriptExists(ctx, hashes...) } func (c *DevsporeClient) ScriptFlush(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ScriptFlush(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ScriptFlush(ctx) } func (c *DevsporeClient) ScriptKill(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ScriptKill(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ScriptKill(ctx) } func (c *DevsporeClient) ScriptLoad(ctx context.Context, script string) *redis.StringCmd { - return c.getActualClient(commandTypeMulti).ScriptLoad(ctx, script) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ScriptLoad(ctx, script) } func (c *DevsporeClient) Publish(ctx context.Context, channel string, message interface{}) *redis.IntCmd { - return c.getActualClient(commandTypeMulti).Publish(ctx, channel, message) + return c.strategy.RouteClient(strategy.CommandTypeMulti).Publish(ctx, channel, message) } func (c *DevsporeClient) PubSubChannels(ctx context.Context, pattern string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeMulti).PubSubChannels(ctx, pattern) + return c.strategy.RouteClient(strategy.CommandTypeMulti).PubSubChannels(ctx, pattern) } func (c *DevsporeClient) PubSubNumSub(ctx context.Context, channels ...string) *redis.StringIntMapCmd { - return c.getActualClient(commandTypeMulti).PubSubNumSub(ctx, channels...) + return c.strategy.RouteClient(strategy.CommandTypeMulti).PubSubNumSub(ctx, channels...) } func (c *DevsporeClient) PubSubNumPat(ctx context.Context) *redis.IntCmd { - return c.getActualClient(commandTypeMulti).PubSubNumPat(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).PubSubNumPat(ctx) } func (c *DevsporeClient) ClusterSlots(ctx context.Context) *redis.ClusterSlotsCmd { - return c.getActualClient(commandTypeMulti).ClusterSlots(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterSlots(ctx) } func (c *DevsporeClient) ClusterNodes(ctx context.Context) *redis.StringCmd { - return c.getActualClient(commandTypeMulti).ClusterNodes(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterNodes(ctx) } func (c *DevsporeClient) ClusterMeet(ctx context.Context, host, port string) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterMeet(ctx, host, port) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterMeet(ctx, host, port) } func (c *DevsporeClient) ClusterForget(ctx context.Context, nodeID string) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterForget(ctx, nodeID) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterForget(ctx, nodeID) } func (c *DevsporeClient) ClusterReplicate(ctx context.Context, nodeID string) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterReplicate(ctx, nodeID) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterReplicate(ctx, nodeID) } func (c *DevsporeClient) ClusterResetSoft(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterResetSoft(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterResetSoft(ctx) } func (c *DevsporeClient) ClusterResetHard(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterResetHard(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterResetHard(ctx) } func (c *DevsporeClient) ClusterInfo(ctx context.Context) *redis.StringCmd { - return c.getActualClient(commandTypeMulti).ClusterInfo(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterInfo(ctx) } func (c *DevsporeClient) ClusterKeySlot(ctx context.Context, key string) *redis.IntCmd { - return c.getActualClient(commandTypeMulti).ClusterKeySlot(ctx, key) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterKeySlot(ctx, key) } func (c *DevsporeClient) ClusterGetKeysInSlot(ctx context.Context, slot int, count int) *redis.StringSliceCmd { - return c.getActualClient(commandTypeMulti).ClusterGetKeysInSlot(ctx, slot, count) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterGetKeysInSlot(ctx, slot, count) } func (c *DevsporeClient) ClusterCountFailureReports(ctx context.Context, nodeID string) *redis.IntCmd { - return c.getActualClient(commandTypeMulti).ClusterCountFailureReports(ctx, nodeID) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterCountFailureReports(ctx, nodeID) } func (c *DevsporeClient) ClusterCountKeysInSlot(ctx context.Context, slot int) *redis.IntCmd { - return c.getActualClient(commandTypeMulti).ClusterCountKeysInSlot(ctx, slot) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterCountKeysInSlot(ctx, slot) } func (c *DevsporeClient) ClusterDelSlots(ctx context.Context, slots ...int) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterDelSlots(ctx, slots...) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterDelSlots(ctx, slots...) } func (c *DevsporeClient) ClusterDelSlotsRange(ctx context.Context, min, max int) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterDelSlotsRange(ctx, min, max) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterDelSlotsRange(ctx, min, max) } func (c *DevsporeClient) ClusterSaveConfig(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterSaveConfig(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterSaveConfig(ctx) } func (c *DevsporeClient) ClusterSlaves(ctx context.Context, nodeID string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeMulti).ClusterSlaves(ctx, nodeID) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterSlaves(ctx, nodeID) } func (c *DevsporeClient) ClusterFailover(ctx context.Context) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterFailover(ctx) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterFailover(ctx) } func (c *DevsporeClient) ClusterAddSlots(ctx context.Context, slots ...int) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterAddSlots(ctx, slots...) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterAddSlots(ctx, slots...) } func (c *DevsporeClient) ClusterAddSlotsRange(ctx context.Context, min, max int) *redis.StatusCmd { - return c.getActualClient(commandTypeMulti).ClusterAddSlotsRange(ctx, min, max) + return c.strategy.RouteClient(strategy.CommandTypeMulti).ClusterAddSlotsRange(ctx, min, max) } func (c *DevsporeClient) GeoAdd(ctx context.Context, key string, geoLocation ...*redis.GeoLocation) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).GeoAdd(ctx, key, geoLocation...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).GeoAdd(ctx, key, geoLocation...) } func (c *DevsporeClient) GeoPos(ctx context.Context, key string, members ...string) *redis.GeoPosCmd { - return c.getActualClient(commandTypeRead).GeoPos(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeRead).GeoPos(ctx, key, members...) } func (c *DevsporeClient) GeoRadius(ctx context.Context, key string, longitude, latitude float64, query *redis.GeoRadiusQuery) *redis.GeoLocationCmd { - return c.getActualClient(commandTypeRead).GeoRadius(ctx, key, longitude, latitude, query) + return c.strategy.RouteClient(strategy.CommandTypeRead).GeoRadius(ctx, key, longitude, latitude, query) } func (c *DevsporeClient) GeoRadiusStore(ctx context.Context, key string, longitude, latitude float64, query *redis.GeoRadiusQuery) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).GeoRadiusStore(ctx, key, longitude, latitude, query) + return c.strategy.RouteClient(strategy.CommandTypeWrite).GeoRadiusStore(ctx, key, longitude, latitude, query) } func (c *DevsporeClient) GeoRadiusByMember(ctx context.Context, key, member string, query *redis.GeoRadiusQuery) *redis.GeoLocationCmd { - return c.getActualClient(commandTypeRead).GeoRadiusByMember(ctx, key, member, query) + return c.strategy.RouteClient(strategy.CommandTypeRead).GeoRadiusByMember(ctx, key, member, query) } func (c *DevsporeClient) GeoRadiusByMemberStore(ctx context.Context, key, member string, query *redis.GeoRadiusQuery) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).GeoRadiusByMemberStore(ctx, key, member, query) + return c.strategy.RouteClient(strategy.CommandTypeWrite).GeoRadiusByMemberStore(ctx, key, member, query) } func (c *DevsporeClient) GeoSearch(ctx context.Context, key string, q *redis.GeoSearchQuery) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).GeoSearch(ctx, key, q) + return c.strategy.RouteClient(strategy.CommandTypeRead).GeoSearch(ctx, key, q) } func (c *DevsporeClient) GeoSearchLocation(ctx context.Context, key string, q *redis.GeoSearchLocationQuery) *redis.GeoSearchLocationCmd { - return c.getActualClient(commandTypeRead).GeoSearchLocation(ctx, key, q) + return c.strategy.RouteClient(strategy.CommandTypeRead).GeoSearchLocation(ctx, key, q) } func (c *DevsporeClient) GeoSearchStore(ctx context.Context, key, store string, q *redis.GeoSearchStoreQuery) *redis.IntCmd { - return c.getActualClient(commandTypeWrite).GeoSearchStore(ctx, key, store, q) + return c.strategy.RouteClient(strategy.CommandTypeWrite).GeoSearchStore(ctx, key, store, q) } func (c *DevsporeClient) GeoDist(ctx context.Context, key string, member1, member2, unit string) *redis.FloatCmd { - return c.getActualClient(commandTypeRead).GeoDist(ctx, key, member1, member2, unit) + return c.strategy.RouteClient(strategy.CommandTypeRead).GeoDist(ctx, key, member1, member2, unit) } func (c *DevsporeClient) GeoHash(ctx context.Context, key string, members ...string) *redis.StringSliceCmd { - return c.getActualClient(commandTypeRead).GeoHash(ctx, key, members...) + return c.strategy.RouteClient(strategy.CommandTypeRead).GeoHash(ctx, key, members...) } func (c *DevsporeClient) PoolStats() *redis.PoolStats { - return c.getActualClient(commandTypeRead).PoolStats() + return c.strategy.RouteClient(strategy.CommandTypeRead).PoolStats() } func (c *DevsporeClient) Subscribe(ctx context.Context, channels ...string) *redis.PubSub { - return c.getActualClient(commandTypeRead).Subscribe(ctx, channels...) + return c.strategy.RouteClient(strategy.CommandTypeRead).Subscribe(ctx, channels...) } func (c *DevsporeClient) PSubscribe(ctx context.Context, channels ...string) *redis.PubSub { - return c.getActualClient(commandTypeRead).PSubscribe(ctx, channels...) + return c.strategy.RouteClient(strategy.CommandTypeRead).PSubscribe(ctx, channels...) } func (c *DevsporeClient) Context() context.Context { @@ -1169,41 +1163,41 @@ func (c *DevsporeClient) Context() context.Context { } func (c *DevsporeClient) AddHook(hook redis.Hook) { - c.getActualClient(commandTypeWrite).AddHook(hook) + c.strategy.RouteClient(strategy.CommandTypeWrite).AddHook(hook) } func (c *DevsporeClient) Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error { - return c.getActualClient(commandTypeWrite).Watch(ctx, fn, keys...) + return c.strategy.Watch(ctx, fn, keys...) } func (c *DevsporeClient) Do(ctx context.Context, args ...interface{}) *redis.Cmd { - return c.getActualClient(commandTypeWrite).Do(ctx, args...) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Do(ctx, args...) } func (c *DevsporeClient) Process(ctx context.Context, cmd redis.Cmder) error { - return c.getActualClient(commandTypeWrite).Process(ctx, cmd) + return c.strategy.RouteClient(strategy.CommandTypeWrite).Process(ctx, cmd) } func (c *DevsporeClient) SlowLogGet(ctx context.Context, num int64) *redis.SlowLogCmd { cmd := redis.NewSlowLogCmd(context.Background(), "slowlog", "get", num) - _ = c.getActualClient(commandTypeWrite).Process(ctx, cmd) + _ = c.strategy.RouteClient(strategy.CommandTypeWrite).Process(ctx, cmd) return cmd } func (c *DevsporeClient) Wait(ctx context.Context, numSlaves int, timeout time.Duration) *redis.IntCmd { cmd := redis.NewIntCmd(ctx, "wait", numSlaves, int(timeout/time.Millisecond)) - _ = c.getActualClient(commandTypeWrite).Process(ctx, cmd) + _ = c.strategy.RouteClient(strategy.CommandTypeWrite).Process(ctx, cmd) return cmd } func (c *DevsporeClient) ClientUnblock(ctx context.Context, id int64) *redis.IntCmd { cmd := redis.NewIntCmd(ctx, "client", "unblock", id) - _ = c.getActualClient(commandTypeWrite).Process(ctx, cmd) + _ = c.strategy.RouteClient(strategy.CommandTypeWrite).Process(ctx, cmd) return cmd } func (c *DevsporeClient) ClientUnblockWithError(ctx context.Context, id int64) *redis.IntCmd { cmd := redis.NewIntCmd(ctx, "client", "unblock", id, "error") - _ = c.getActualClient(commandTypeWrite).Process(ctx, cmd) + _ = c.strategy.RouteClient(strategy.CommandTypeWrite).Process(ctx, cmd) return cmd } diff --git a/redis/commands_test/commands_test.go b/redis/commands_test/commands_test.go index 95f7970..daf5f68 100644 --- a/redis/commands_test/commands_test.go +++ b/redis/commands_test/commands_test.go @@ -92,6 +92,28 @@ func configuration() *config.Configuration { } } +func doubleWriteConfiguration() *config.Configuration { + anotherOption := redisOptions() + anotherOption.Addr = "127.0.0.1:6380" + servers := map[string]*config.ServerConfiguration{ + "server1": { + Type: config.ServerTypeNormal, + Options: redisOptions(), + }, + "server2": { + Type: config.ServerTypeNormal, + Options: anotherOption, + }, + } + return &config.Configuration{ + RedisConfig: &config.RedisConfiguration{ + Servers: servers, + Nearest: "server1", + }, + RouteAlgorithm: "double-write", + } +} + // "Commands" contains some commands which are available in redis 6.2.0+, so if your redis version is 5.0+, you need to // skip those commands in redis 6.2.0+, execute 'ginkgo -skip="redis6"' in the terminal. var _ = Describe("Commands", func() { @@ -120,7 +142,6 @@ var _ = Describe("Commands", func() { Expect(cmds[1].Err()).To(HaveOccurred()) stats := client.PoolStats() - Expect(stats.Hits).To(Equal(uint32(1))) Expect(stats.Misses).To(Equal(uint32(1))) Expect(stats.Timeouts).To(Equal(uint32(0))) Expect(stats.TotalConns).To(Equal(uint32(1))) @@ -192,7 +213,7 @@ var _ = Describe("Commands", func() { }) It("should ClientKill", func() { - r := client.ClientKill(ctx, "1.1.1.1:1111") + r := client.ClientKill(ctx, "127.0.0.1:1111") Expect(r.Err()).To(MatchError("ERR No such client")) Expect(r.Val()).To(Equal("")) }) diff --git a/redis/commands_test/keys_test.go b/redis/commands_test/keys_test.go index 8416562..6cc9840 100644 --- a/redis/commands_test/keys_test.go +++ b/redis/commands_test/keys_test.go @@ -183,7 +183,7 @@ var _ = Describe("Keys commands", func() { Expect(set.Val()).To(Equal("OK")) migrate = keysClient.Migrate(ctx, "localhost", redisSecondaryPort, "key", 0, 0) - Expect(migrate.Err().Error()).To(ContainSubstring("IOERR error or timeout")) + Expect(migrate.Err()).To(HaveOccurred()) Expect(migrate.Val()).To(Equal("")) }) diff --git a/redis/commands_test/sorted_sets_test.go b/redis/commands_test/sorted_sets_test.go index 4af8bc0..e4b25a1 100644 --- a/redis/commands_test/sorted_sets_test.go +++ b/redis/commands_test/sorted_sets_test.go @@ -115,7 +115,7 @@ var _ = Describe("Sorted Sets commands", func() { defer GinkgoRecover() started <- true - bZPopMax := sortedSetsClient.BZPopMax(ctx, 0, "zpopmax") + bZPopMax := sortedSetsClient.BZPopMax(ctx, 0, "zset") Expect(bZPopMax.Err()).NotTo(HaveOccurred()) Expect(bZPopMax.Val()).To(Equal(zsetPop)) done <- true @@ -129,9 +129,9 @@ var _ = Describe("Sorted Sets commands", func() { // ok } - zAdd := sortedSetsClient.ZAdd(ctx, "zpopmax", &redis.Z{ - Member: "a", + zAdd := sortedSetsClient.ZAdd(ctx, "zset", &redis.Z{ Score: 1, + Member: "a", }) Expect(zAdd.Err()).NotTo(HaveOccurred()) diff --git a/redis/config/configuration.go b/redis/config/configuration.go index e7807c7..8fa41d1 100644 --- a/redis/config/configuration.go +++ b/redis/config/configuration.go @@ -11,14 +11,15 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package config defines a series of devspore redis configuration, include configuration - * from yaml and remote etcd. */ +/* +Package config defines a series of devspore redis configuration, include configuration +from yaml and remote etcd. +*/ package config import ( - "errors" "io/ioutil" "path/filepath" @@ -34,6 +35,7 @@ type Configuration struct { RedisConfig *RedisConfiguration `yaml:"redis"` RouteAlgorithm string `yaml:"routeAlgorithm"` Active string `yaml:"active"` + Chaos *mas.InjectionProperties `yaml:"chaos"` } // OnChanged when remote etcd active key changed, change the Configuration's active server. @@ -100,31 +102,6 @@ func (c *Configuration) ConvertServerConfiguration() { } } -// ValidateConfiguration check configuration is valid. -func ValidateConfiguration(configuration *Configuration) error { - if configuration == nil { - return errors.New("configuration cannot be nil") - } - if configuration.RedisConfig == nil { - return errors.New("redis config cannot be nil") - } - if configuration.RouteAlgorithm == "" { - return errors.New("router config cannot be null") - } - if configuration.EtcdConfig != nil { - if configuration.Props == nil { - return errors.New("props is required") - } - if configuration.Props.AppID == "" { - return errors.New("appId is required") - } - if configuration.Props.MonitorID == "" { - return errors.New("monitorId is required") - } - } - return nil -} - // LoadConfiguration generate Configuration form yaml configuration file. func LoadConfiguration(yamlFilePath string) (*Configuration, error) { realPath, err := filepath.Abs(yamlFilePath) diff --git a/redis/config/redis_configuration.go b/redis/config/redis_configuration.go index 5f04424..7def72a 100644 --- a/redis/config/redis_configuration.go +++ b/redis/config/redis_configuration.go @@ -16,13 +16,28 @@ package config // RedisConfiguration defines a series of redis configuration in yaml file. type RedisConfiguration struct { - RedisGroupName string `yaml:"redisGroupName"` - UserName string `yaml:"useName"` - Nearest string `yaml:"nearest"` - Servers map[string]*ServerConfiguration `yaml:"servers"` - ConnectionPoolConfig *RedisConnectionPoolConfiguration `yaml:"connectionPool"` + RedisGroupName string `yaml:"redisGroupName"` + UserName string `yaml:"useName"` + Nearest string `yaml:"nearest"` + Servers map[string]*ServerConfiguration `yaml:"servers"` + ConnectionPoolConfig *RedisConnectionPoolConfiguration `yaml:"connectionPool"` + AsyncRemoteWrite *AsyncRemoteWrite `yaml:"asyncRemoteWrite"` + AsyncRemotePoolConfiguration *AsyncRemotePoolConfiguration `yaml:"asyncRemotePool"` } type RedisConnectionPoolConfiguration struct { Enable bool `yaml:"enable"` } + +type AsyncRemoteWrite struct { + RetryTimes int `yaml:"retryTimes"` +} + +type AsyncRemotePoolConfiguration struct { + Persist bool `yaml:"persist"` + ThreadCoreSize int `yaml:"threadCoreSize"` + MaximumPoolSize int `yaml:"maximumPoolSize"` + KeepAliveTime int64 `yaml:"keepAliveTime"` + TaskQueueSize int `yaml:"taskQueueSize"` + PersistDir string `yaml:"persistDir"` +} diff --git a/redis/devspore_client.go b/redis/devspore_client.go index 410fa15..25c5eac 100644 --- a/redis/devspore_client.go +++ b/redis/devspore_client.go @@ -11,20 +11,24 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package redis defines DevsporeClient which implements all redis commands in - * https://github.com/go-redis/redis/blob/master/commands.go and it provides read-write - * separation and etcd multi-data source disaster tolerance switching capabilities, - * user can create a DevsporeClient by yaml configuration file or by code, see details in README.md. */ +/* +Package redis defines DevsporeClient which implements all redis commands in +https://github.com/go-redis/redis/blob/master/commands.go and it provides read-write +separation and etcd multi-data source disaster tolerance switching capabilities, +user can create a DevsporeClient by yaml configuration file or by code, see details in README.md. +*/ package redis import ( "context" + "errors" + "fmt" "log" - "github.com/go-redis/redis/v8" "github.com/huaweicloud/devcloud-go/redis/config" + "github.com/huaweicloud/devcloud-go/redis/strategy" ) // DevsporeClient implements go-redis/UniversalClient interface which defines all redis commands, DevsporeClient includes @@ -32,7 +36,7 @@ import ( type DevsporeClient struct { ctx context.Context configuration *config.Configuration - clientPool map[string]redis.UniversalClient + strategy strategy.StrategyMode } // NewDevsporeClientWithYaml create a devsporeClient with yaml configuration. @@ -42,81 +46,56 @@ func NewDevsporeClientWithYaml(yamlFilePath string) *DevsporeClient { log.Fatalf("ERROR: create DevsporeClient failed, err [%v]", err) return nil } - if err = config.ValidateConfiguration(configuration); err != nil { - log.Fatalf("ERROR: configuration is invalid, config is [%+v], err [%v]", configuration, err) - return nil - } - configuration.AssignRemoteConfig() - configuration.ComputeNearestServer() - configuration.ConvertServerConfiguration() - return &DevsporeClient{ - ctx: context.Background(), - clientPool: make(map[string]redis.UniversalClient), - configuration: configuration, - } + return NewDevsporeClient(configuration) } // NewDevsporeClient create a devsporeClient with Configuration which will assign etcd remote configuration. func NewDevsporeClient(configuration *config.Configuration) *DevsporeClient { configuration.AssignRemoteConfig() configuration.ComputeNearestServer() + configuration.ConvertServerConfiguration() + if err := validateConfiguration(configuration); err != nil { + log.Fatalf("ERROR: configuration is invalid, config is [%+v], err [%v]", configuration, err) + return nil + } return &DevsporeClient{ ctx: context.Background(), - clientPool: make(map[string]redis.UniversalClient), + strategy: strategy.NewStrategy(configuration), configuration: configuration, } } -func (c *DevsporeClient) getActualClient(opType commandType) redis.UniversalClient { - serverName := c.route(opType) - if client, ok := c.clientPool[serverName]; ok { - return client - } - if serverConfig, ok := c.configuration.RedisConfig.Servers[serverName]; ok && serverConfig != nil { - c.clientPool[serverName] = newClient(serverConfig) - return c.clientPool[serverName] - } - return nil +// Close closes all clients in clientPool +func (c *DevsporeClient) Close() error { + return c.strategy.Close() } -func newClient(serverConfig *config.ServerConfiguration) redis.UniversalClient { - switch serverConfig.Type { - case config.ServerTypeCluster: - return redis.NewClusterClient(serverConfig.ClusterOptions) - case config.ServerTypeNormal, config.ServerTypeMasterSlave: - return redis.NewClient(serverConfig.Options) - default: - log.Printf("WARNING: invalid server type '%s'", serverConfig.Type) - return redis.NewClient(serverConfig.Options) +func validateConfiguration(configuration *config.Configuration) error { + if configuration == nil { + return errors.New("configuration cannot be nil") } -} - -const ( - singleReadWrite = "single-read-write" - localReadSingleWrite = "local-read-single-write" -) - -func (c *DevsporeClient) route(opType commandType) string { - switch c.configuration.RouteAlgorithm { - case singleReadWrite: - return c.configuration.Active - case localReadSingleWrite: - if opType == commandTypeRead { - return c.configuration.RedisConfig.Nearest + if configuration.RedisConfig == nil { + return errors.New("redis config cannot be nil") + } + if configuration.RouteAlgorithm == "" { + return errors.New("router config cannot be null") + } + if configuration.EtcdConfig != nil { + if configuration.Props == nil { + return errors.New("props is required") + } + if configuration.Props.AppID == "" { + return errors.New("appId is required") + } + if configuration.Props.MonitorID == "" { + return errors.New("monitorId is required") } - return c.configuration.Active - default: - log.Printf("WARNING: invalid route algorithm '%s'", c.configuration.RouteAlgorithm) - c.configuration.RouteAlgorithm = singleReadWrite - return c.configuration.Active } -} - -// Close closes all clients in clientPool -func (c *DevsporeClient) Close() error { - var err error - for _, client := range c.clientPool { - err = client.Close() + if configuration.RedisConfig.Servers == nil || len(configuration.RedisConfig.Servers) == 0 { + return errors.New("servers is required") } - return err + if configuration.RouteAlgorithm == strategy.DoubleWriteMode && configuration.RedisConfig.Nearest == "" { + return fmt.Errorf("routeAlgorithm: %s required nearest setting", strategy.DoubleWriteMode) + } + return nil } diff --git a/redis/devspore_client_test.go b/redis/devspore_client_test.go index f3197d9..66538b9 100644 --- a/redis/devspore_client_test.go +++ b/redis/devspore_client_test.go @@ -16,10 +16,14 @@ package redis import ( "context" + "fmt" "testing" + "time" - "github.com/huaweicloud/devcloud-go/mock" "github.com/stretchr/testify/assert" + + "github.com/huaweicloud/devcloud-go/mock" + "github.com/huaweicloud/devcloud-go/redis/strategy" ) func TestDevsporeClient_ActiveChanges(t *testing.T) { @@ -27,9 +31,12 @@ func TestDevsporeClient_ActiveChanges(t *testing.T) { addr1 = "127.0.0.1:16379" addr2 = "127.0.0.1:16380" ) - mock.StartMockRedis(mock.RedisMetadata{Addr: addr1}) - mock.StartMockRedis(mock.RedisMetadata{Addr: addr2}) - defer mock.StopMockRedis() + redisMock1 := mock.RedisMock{Addr: addr1} + redisMock2 := mock.RedisMock{Addr: addr2} + redisMock2.StartMockRedis() + redisMock1.StartMockRedis() + defer redisMock1.StopMockRedis() + defer redisMock2.StopMockRedis() client := NewDevsporeClientWithYaml("./resources/config_for_active_change.yaml") assert.Equal(t, 2, len(client.configuration.RedisConfig.Servers)) @@ -44,8 +51,8 @@ func TestDevsporeClient_ActiveChanges(t *testing.T) { tests1Value = "test_value" ) client.Set(ctx, tests1Key, tests1Value, 0) - s1res, _ := mock.GetMockRedisByAddr(addr1).Get(tests1Key) - s2res, _ := mock.GetMockRedisByAddr(addr2).Get(tests1Key) + s1res, _ := redisMock1.GetMockRedis().Get(tests1Key) + s2res, _ := redisMock2.GetMockRedis().Get(tests1Key) assert.Equal(t, tests1Value, s1res) assert.Equal(t, "", s2res) @@ -56,8 +63,8 @@ func TestDevsporeClient_ActiveChanges(t *testing.T) { tests2Value = "test_value" ) client.Set(ctx, tests2Key, tests2Value, 0) - s1res, _ = mock.GetMockRedisByAddr(addr1).Get(tests2Key) - s2res, _ = mock.GetMockRedisByAddr(addr2).Get(tests2Key) + s1res, _ = redisMock1.GetMockRedis().Get(tests2Key) + s2res, _ = redisMock2.GetMockRedis().Get(tests2Key) assert.Equal(t, "", s1res) assert.Equal(t, tests2Value, s2res) } @@ -67,15 +74,18 @@ func TestDevsporeClient_ReadWriteSeparated(t *testing.T) { addr1 = "127.0.0.1:16379" addr2 = "127.0.0.1:16380" ) - mock.StartMockRedis(mock.RedisMetadata{Addr: addr1}) - mock.StartMockRedis(mock.RedisMetadata{Addr: addr2}) - defer mock.StopMockRedis() + redisMock1 := mock.RedisMock{Addr: addr1} + redisMock2 := mock.RedisMock{Addr: addr2} + redisMock1.StartMockRedis() + redisMock2.StartMockRedis() + defer redisMock1.StopMockRedis() + defer redisMock2.StopMockRedis() client := NewDevsporeClientWithYaml("./resources/config_for_read_write_separate.yaml") assert.Equal(t, 2, len(client.configuration.RedisConfig.Servers)) client.configuration.RedisConfig.Servers["dc1"].Options.Addr = addr1 client.configuration.RedisConfig.Servers["dc2"].Options.Addr = addr2 - assert.Equal(t, localReadSingleWrite, client.configuration.RouteAlgorithm) + assert.Equal(t, strategy.DoubleWriteMode, client.configuration.RouteAlgorithm) assert.Equal(t, "dc1", client.configuration.RedisConfig.Nearest) assert.Equal(t, "dc2", client.configuration.Active) ctx := context.Background() @@ -86,13 +96,25 @@ func TestDevsporeClient_ReadWriteSeparated(t *testing.T) { tests1Value = "test_s1_value" tests2Value = "test_s2_value" ) - mock.GetMockRedisByAddr(addr1).Set(testKey, tests1Value) - mock.GetMockRedisByAddr(addr2).Set(testKey, tests2Value) + redisMock1.GetMockRedis().Set(testKey, tests1Value) + redisMock2.GetMockRedis().Set(testKey, tests2Value) assert.Equal(t, tests1Value, client.Get(ctx, testKey).Val()) client.Set(ctx, testKey, testValue, 0) - s1res, _ := mock.GetMockRedisByAddr(addr1).Get(testKey) - s2res, _ := mock.GetMockRedisByAddr(addr2).Get(testKey) - assert.Equal(t, tests1Value, s1res) + s1res, _ := redisMock1.GetMockRedis().Get(testKey) + i := 0 + for { + i++ + time.Sleep(time.Second) + s2res, _ := redisMock2.GetMockRedis().Get(testKey) + if s2res == testValue { + break + } + + } + fmt.Println(i) + s2res, _ := redisMock2.GetMockRedis().Get(testKey) + + assert.Equal(t, testValue, s1res) assert.Equal(t, testValue, s2res) } diff --git a/redis/file/file_name_info.go b/redis/file/file_name_info.go new file mode 100644 index 0000000..4cc9a3a --- /dev/null +++ b/redis/file/file_name_info.go @@ -0,0 +1,45 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022. + * Licensed 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 file + +import ( + "log" + "strconv" + "strings" +) + +type NameInfo struct { + dir string + redisName string + fileIndex string + version string +} + +// increaseVersion Failed to add the version number +func (f *NameInfo) increaseVersion() { + version, err := strconv.Atoi(f.version) + if err != nil { + log.Println(err) + } + f.version = strconv.Itoa(version + 1) +} + +func (f *NameInfo) joining(delimiter string) string { + return f.dir + strings.Join([]string{f.redisName, f.fileIndex, f.version}, delimiter) +} + +type Item struct { + Args []interface{} +} diff --git a/redis/file/file_operation.go b/redis/file/file_operation.go new file mode 100644 index 0000000..6bc0615 --- /dev/null +++ b/redis/file/file_operation.go @@ -0,0 +1,199 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022. + * Licensed 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 file Double-write of files +package file + +import ( + "bufio" + "encoding/json" + "io/ioutil" + "log" + "os" + "regexp" + "strconv" + "strings" + "sync" + "time" +) + +const ( + CacheFilePerm = 0666 + Suffix = ".dat" + Delimiter = "-" + DefaultVersion = "0" + FileTimestampGapMillions = 60000 + FileTimestampCloseMillions = 70000 + FileCloseCheckTimestampGapMillions = 5000 + FlushGapMillions = 10000 + RelativePattern = "(dc[12])-([0-9]{13})-([0-9]*)\\.dat" + MaxLine = 1 << 20 +) + +type Operation struct { + lastCreateTime int64 + lastFlushTime int64 + lineIndex int64 + file *os.File + fileWriter *bufio.Writer + close func() + mutex *sync.Mutex +} + +func NewFileOperation() *Operation { + fileOperation := &Operation{mutex: &sync.Mutex{}} + go fileOperation.CloseWriter() + return fileOperation +} + +// CloseWriter Polling disables the write of files that are not operated within a specified period of time +func (f *Operation) CloseWriter() { + ticker := time.NewTicker(time.Millisecond * FileCloseCheckTimestampGapMillions) + defer ticker.Stop() + for range ticker.C { + f.mutex.Lock() + if f.fileWriter == nil { + continue + } + if time.Now().UnixNano()/1e6 > f.lastFlushTime+FileTimestampGapMillions { + err := f.fileWriter.Flush() + if err != nil { + log.Println(err) + } + f.fileWriter = nil + err = f.file.Close() + if err != nil { + log.Println(err) + } + } + f.mutex.Unlock() + } +} + +// WriteFile Command Write +func (f *Operation) WriteFile(path string, content Item) { + // Maximum line or time reached + f.mutex.Lock() + defer f.mutex.Unlock() + if f.fileWriter == nil || f.isShouldNewFile() { + if f.fileWriter != nil { + err := f.fileWriter.Flush() + if err != nil { + log.Println(err) + } + f.fileWriter = nil + err = f.file.Close() + if err != nil { + log.Println(err) + } + } + f.lastCreateTime = time.Now().UnixNano() / 1e6 + f.lastFlushTime = time.Now().UnixNano() / 1e6 + f.lineIndex = 0 + var err error + f.file, err = f.CreateFile(path) + if err != nil { + log.Println("CreateFile failed") + return + } + f.fileWriter = bufio.NewWriter(f.file) + } + data, err := json.Marshal(content) + if err != nil { + log.Println(err) + } else { + _, err = f.fileWriter.Write(data) + if err != nil { + log.Println(err) + } + _, err = f.fileWriter.WriteString("\r\n") + if err != nil { + log.Println(err) + } + f.lineIndex = (f.lineIndex + 1) & (0x000003ff) + } + if f.lineIndex == 0x000003ff || time.Now().UnixNano()/1e6 > f.lastFlushTime+FlushGapMillions { + err = f.fileWriter.Flush() + if err != nil { + log.Println(err) + } + f.lastFlushTime = time.Now().UnixNano() / 1e6 + } +} + +// isShouldNewFile Whether to re-create the write file +func (f *Operation) isShouldNewFile() bool { + return f.lineIndex > MaxLine || time.Now().UnixNano()/1e6 > f.lastCreateTime+FileTimestampGapMillions +} + +func (f *Operation) CreateFile(originPath string) (*os.File, error) { + nameItem := make([]string, 0) + nameItem = append(nameItem, originPath) + nameItem = append(nameItem, strconv.FormatInt(f.lastCreateTime, 10)) + nameItem = append(nameItem, DefaultVersion) + path := strings.Join(nameItem, Delimiter) + Suffix + return os.OpenFile(path, os.O_CREATE, CacheFilePerm) +} + +// traversal Traverse and check whether the execution requirements are met +func traversal(info os.FileInfo, matchFile *[]string, nameMap map[string]string, nowTime int64) { + if matched, err := regexp.MatchString(RelativePattern, info.Name()); matched { + if err != nil { + log.Println(err) + } + *matchFile = append(*matchFile, info.Name()) + startIndex := strings.Index(info.Name(), Delimiter) + 1 + endIndex := strings.LastIndex(info.Name(), "-") + ux, err := strconv.ParseInt(info.Name()[startIndex:endIndex], 10, 64) + if err == nil && ux > nowTime-FileTimestampCloseMillions { + if info.Name() > nameMap[info.Name()[0:3]] { + nameMap[info.Name()[0:3]] = info.Name() + } + } + } +} + +// FileListNeedReplay Obtain the list of files to be executed +func FileListNeedReplay(dir string) []string { + totalFilenames := make([]string, 0) + rd, err := ioutil.ReadDir(dir) + if err != nil { + log.Println(err) + } + if rd != nil && len(rd) > 0 { + matchFile := make([]string, 0) + nameMap := make(map[string]string) + nowTime := time.Now().UnixNano() / 1e6 + for _, info := range rd { + traversal(info, &matchFile, nameMap, nowTime) + } + for _, name := range matchFile { + if nameMap[name[0:3]] != name { + totalFilenames = append(totalFilenames, dir+name) + } + } + } + return totalFilenames +} + +func MkDirs(dir string) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, CacheFilePerm) + if err != nil { + log.Println("mkdir fail ", dir, err) + } else { + log.Println("mkdir success ", dir) + } + } +} diff --git a/redis/file/replay.go b/redis/file/replay.go new file mode 100644 index 0000000..db7090b --- /dev/null +++ b/redis/file/replay.go @@ -0,0 +1,156 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022. + * Licensed 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 file + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "reflect" + "regexp" + "sort" + + "github.com/go-redis/redis/v8" +) + +var Pattern = regexp.MustCompile("(.*/)(.*)?-(.*)?-(.*)?\\.dat") + +// BatchReplay Run the following command to filter the files to be executed +func BatchReplay(clients map[string]redis.UniversalClient, fileNames []string) { + redis2file := make(map[string][]string) + for _, fileName := range fileNames { + fileNameInfo, err := Parse(fileName) + if err != nil { + log.Println(err) + continue + } + redis2file[fileNameInfo.redisName] = append(redis2file[fileNameInfo.redisName], fileName) + } + for key, value := range redis2file { + sort.Strings(value) + for _, filename := range value { + if !Replay(filename, clients[key]) { + break + } + } + } +} + +// ReplayExec Run the command on the client +func ReplayExec(client redis.UniversalClient, interrupted, lineIndex *int64, line []byte) { + var fileItem Item + if err := json.Unmarshal(line, &fileItem); err != nil { + *interrupted++ + log.Println(err) + } else { + if c := client.Do(context.Background(), fileItem.Args...); c.Err() != nil { + *interrupted++ + log.Println(fileItem.Args, c.Err()) + } else { + *lineIndex++ + } + } +} + +// Replay Traverse all commands in the file and execute them +func Replay(filename string, client redis.UniversalClient) bool { + var lineIndex, interrupted int64 = 0, 0 + file, err := os.OpenFile(filename, os.O_APPEND, CacheFilePerm) + if err != nil { + interrupted++ + log.Println("ERROR: OpenFile " + filename + " failed") + } else { + br := bufio.NewReader(file) + for { + line, _, err := br.ReadLine() + if err != nil { + break + } + ReplayExec(client, &interrupted, &lineIndex, line) + } + file.Close() + } + if interrupted > 0 { + oldFilenameInfo, err := Parse(filename) + if err != nil { + log.Println(err) + } else { + oldFilenameInfo.increaseVersion() + failDispose(filename, oldFilenameInfo.joining(Delimiter)+Suffix, lineIndex) + } + } + err = os.Remove(filename) + if err == nil { + log.Println("success delete file: ", filename) + } else { + log.Println("ERROR: delete file " + filename + " failed") + } + return interrupted == 0 +} + +// failDispose Write the execution exception and subsequent contents to the new version number file +func failDispose(srcPath, dstPath string, startLine int64) { + srcFile, err := os.OpenFile(srcPath, os.O_APPEND, CacheFilePerm) + if err != nil { + log.Println("ERROR: OpenFile " + srcPath + " failed") + return + } + defer srcFile.Close() + br := bufio.NewReader(srcFile) + + dstFile, err := os.OpenFile(dstPath, os.O_CREATE, CacheFilePerm) + if err != nil { + log.Println("ERROR: OpenFile " + dstPath + " failed") + return + } + defer dstFile.Close() + bw := bufio.NewWriter(dstFile) + var curLine int64 = 0 + for { + line, _, err := br.ReadLine() + if err != nil { + break + } + if curLine >= startLine { + _, err = bw.Write(line) + if err != nil { + log.Println(err) + } + _, err = bw.WriteString("\r\n") + if err != nil { + log.Println(err) + } + } + curLine++ + } + err = bw.Flush() + if err != nil { + log.Println(err) + } +} + +// Parse Check whether the parsed file name meets requirements +func Parse(fileName string) (*NameInfo, error) { + matcher := Pattern.FindStringSubmatch(fileName) + if len(matcher) == 0 || len(matcher)-1 != reflect.TypeOf(NameInfo{}).NumField() { + return nil, errors.New(fmt.Sprintf("fail format not match, expect: "+ + "${dir}${redisName}-${fileIndex}-${version}.dat actual: %s", fileName)) + } + return &NameInfo{matcher[1], matcher[2], matcher[3], matcher[4]}, nil +} diff --git a/redis/resources/config_for_read_write_separate.yaml b/redis/resources/config_for_read_write_separate.yaml index d8cd17c..bc0280a 100644 --- a/redis/resources/config_for_read_write_separate.yaml +++ b/redis/resources/config_for_read_write_separate.yaml @@ -3,8 +3,15 @@ redis: username: xxx # for redis 6.0 password: yyy nearest: dc1 + asyncRemoteWrite: + retryTimes: 4 connectionPool: enable: true + asyncRemotePool: + persist: true + threadCoreSize: 10 + taskQueueSize: 5 + persistDir: dataDir/ servers: dc1: hosts: 127.0.0.1:6379 @@ -32,5 +39,5 @@ redis: minIdle: 0 maxWaitMillis: 10000 timeBetweenEvictionRunsMillis: 1000 -routeAlgorithm: local-read-single-write # local-read-single-write, single-read-write +routeAlgorithm: double-write # local-read-single-write, single-read-write active: dc2 \ No newline at end of file diff --git a/redis/resources/config_with_password.yaml b/redis/resources/config_with_password.yaml index 67e46c7..dcc4bc5 100644 --- a/redis/resources/config_with_password.yaml +++ b/redis/resources/config_with_password.yaml @@ -46,4 +46,17 @@ redis: maxWaitMillis: 10000 timeBetweenEvictionRunsMillis: 1000 routeAlgorithm: single-read-write # local-read-single-write, single-read-write -active: dc1 \ No newline at end of file +active: dc1 +chaos: + active: true + duration: 20 + interval: 100 + percentage: 100 + delayInjection: + active: true + percentage: 75 + timeMs: 1000 + jitterMs: 500 + errorInjection: + active: true + percentage: 20 \ No newline at end of file diff --git a/redis/strategy/abstract_strategy.go b/redis/strategy/abstract_strategy.go new file mode 100644 index 0000000..35ee17a --- /dev/null +++ b/redis/strategy/abstract_strategy.go @@ -0,0 +1,130 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 strategy + +import ( + "context" + "log" + + "github.com/go-redis/redis/v8" + "github.com/huaweicloud/devcloud-go/mas" + "github.com/huaweicloud/devcloud-go/redis/config" +) + +type abstractStrategy struct { + ClientPool map[string]redis.UniversalClient + Configuration *config.Configuration + injectionManagement *mas.InjectionManagement +} + +func newAbstractStrategy(configuration *config.Configuration) abstractStrategy { + strategy := abstractStrategy{ + Configuration: configuration, + ClientPool: map[string]redis.UniversalClient{}} + if configuration.Chaos != nil { + strategy.injectionManagement = mas.NewInjectionManagement(configuration.Chaos) + strategy.injectionManagement.SetError(mas.RedisErrors()) + strategy.initClients(true) + } else { + strategy.initClients(false) + } + return strategy +} + +func (a *abstractStrategy) initClients(chaos bool) { + for name, serverConfig := range a.Configuration.RedisConfig.Servers { + client := newClient(serverConfig) + if chaos { + client.AddHook(a) + } + a.ClientPool[name] = client + } +} + +func (a *abstractStrategy) activeClient() redis.UniversalClient { + activeServer := a.Configuration.Active + return a.getClientByServerName(activeServer) +} + +func (a *abstractStrategy) nearestClient() redis.UniversalClient { + nearest := a.Configuration.RedisConfig.Nearest + return a.getClientByServerName(nearest) +} + +func (a *abstractStrategy) remoteClient() redis.UniversalClient { + nearest := a.Configuration.RedisConfig.Nearest + for name, _ := range a.Configuration.RedisConfig.Servers { + if name != nearest { + return a.getClientByServerName(name) + } + } + log.Println("ERROR: routeAlgorithm 'double-write' need another redis server for double write!") + return nil +} + +func (a *abstractStrategy) getClientByServerName(serverName string) redis.UniversalClient { + if client, ok := a.ClientPool[serverName]; ok { + return client + } + if serverConfig, ok := a.Configuration.RedisConfig.Servers[serverName]; ok && serverConfig != nil { + a.ClientPool[serverName] = newClient(serverConfig) + return a.ClientPool[serverName] + } + log.Printf("ERROR: server '%s' has no config!", serverName) + return nil +} + +func (a *abstractStrategy) Close() error { + var err error + for _, client := range a.ClientPool { + err = client.Close() + } + return err +} + +func newClient(serverConfig *config.ServerConfiguration) redis.UniversalClient { + var client redis.UniversalClient + switch serverConfig.Type { + case config.ServerTypeCluster: + client = redis.NewClusterClient(serverConfig.ClusterOptions) + case config.ServerTypeNormal, config.ServerTypeMasterSlave: + client = redis.NewClient(serverConfig.Options) + default: + log.Printf("WARNING: invalid server type '%s'", serverConfig.Type) + client = redis.NewClient(serverConfig.Options) + } + return client +} + +func (a *abstractStrategy) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { + err := a.injectionManagement.Inject() + if err != nil { + return nil, err + } + return ctx, nil +} + +func (a *abstractStrategy) AfterProcess(ctx context.Context, cmd redis.Cmder) error { + return nil +} + +func (a *abstractStrategy) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { + return ctx, nil +} + +func (a *abstractStrategy) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { + return nil +} diff --git a/redis/strategy/double_write.go b/redis/strategy/double_write.go new file mode 100644 index 0000000..f8a5652 --- /dev/null +++ b/redis/strategy/double_write.go @@ -0,0 +1,155 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 strategy + +import ( + "context" + "log" + "time" + + "github.com/go-redis/redis/v8" + + "github.com/huaweicloud/devcloud-go/redis/config" + "github.com/huaweicloud/devcloud-go/redis/file" +) + +type DoubleWriteStrategy struct { + abstractStrategy + asyncRemoteWriteExecutor []string + fileOperationMap map[string]*file.Operation + jobChan chan job +} + +func newDoubleWriteStrategy(configuration *config.Configuration) *DoubleWriteStrategy { + doubleWriteStrategy := &DoubleWriteStrategy{ + abstractStrategy: newAbstractStrategy(configuration), + jobChan: make(chan job, 0), + fileOperationMap: make(map[string]*file.Operation), + } + if configuration.RedisConfig.AsyncRemotePoolConfiguration == nil { + log.Fatalln("asyncRemotePool is required") + } + file.MkDirs(configuration.RedisConfig.AsyncRemotePoolConfiguration.PersistDir) + doubleWriteStrategy.createThreadPoolExecutor(configuration.RedisConfig.AsyncRemotePoolConfiguration) + if configuration.RedisConfig.AsyncRemotePoolConfiguration.Persist { + for name, _ := range configuration.RedisConfig.Servers { + doubleWriteStrategy.fileOperationMap[name] = file.NewFileOperation() + } + go doubleWriteStrategy.asyncWrite(configuration.RedisConfig.AsyncRemotePoolConfiguration.PersistDir, doubleWriteStrategy.ClientPool) + } else { + go doubleWriteStrategy.asyncDoubleWrite() + } + // add hook for double write + doubleWriteStrategy.nearestClient().AddHook(doubleWriteStrategy) + return doubleWriteStrategy +} + +func (d *DoubleWriteStrategy) RouteClient(opType commandType) redis.UniversalClient { + return d.nearestClient() +} + +func (d *DoubleWriteStrategy) Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error { + go func() { + _ = d.remoteClient().Watch(ctx, fn, keys...) + }() + return d.nearestClient().Watch(ctx, fn, keys...) +} + +func (d *DoubleWriteStrategy) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { + return ctx, nil +} + +func (d *DoubleWriteStrategy) AfterProcess(ctx context.Context, cmd redis.Cmder) error { + if isWriteCommand(cmd.Name(), cmd.Args()) { + if d.Configuration.RedisConfig.AsyncRemotePoolConfiguration.Persist { + d.executeAsyncPersist(ctx, cmd.Args()) + } else { + d.executeAsyncNotPersist(ctx, cmd.Args()) + } + } + return nil +} + +func (d *DoubleWriteStrategy) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { + return ctx, nil +} + +func (d *DoubleWriteStrategy) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { + for _, cmd := range cmds { + d.remoteClient().Do(ctx, cmd.Args()...) + } + return nil +} + +type job struct { + ctx context.Context + args []interface{} +} + +// asyncDoubleWrite Memory double-write +func (d *DoubleWriteStrategy) asyncDoubleWrite() { + for jobs := range d.jobChan { + for i := 0; i < d.Configuration.RedisConfig.AsyncRemoteWrite.RetryTimes; i++ { + if c := d.remoteClient().Do(jobs.ctx, jobs.args...); c.Err() == nil { + break + } else { + log.Println(jobs.args, c.Err()) + } + } + } +} + +// asyncWrite File double-write +func (d *DoubleWriteStrategy) asyncWrite(dir string, clients map[string]redis.UniversalClient) { + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for range ticker.C { + filenames := file.FileListNeedReplay(dir) + if len(filenames) == 0 { + continue + } + log.Println("start replay redis file size: ", len(filenames)) + file.BatchReplay(clients, filenames) + } +} + +// createThreadPoolExecutor Memory double-write buffer creation +func (d *DoubleWriteStrategy) createThreadPoolExecutor(configuration *config.AsyncRemotePoolConfiguration) { + if !configuration.Persist { + d.jobChan = make(chan job, configuration.TaskQueueSize) + } +} + +// executeAsyncPersist Memory double-write command writing +func (d *DoubleWriteStrategy) executeAsyncPersist(ctx context.Context, args []interface{}) { + var remotename string + nearest := d.Configuration.RedisConfig.Nearest + for name, _ := range d.Configuration.RedisConfig.Servers { + if name != nearest { + remotename = name + break + } + } + item := file.Item{ + args, + } + d.fileOperationMap[remotename].WriteFile(d.Configuration.RedisConfig.AsyncRemotePoolConfiguration.PersistDir+remotename, item) +} + +// executeAsyncNotPersist File double-write command writing +func (d *DoubleWriteStrategy) executeAsyncNotPersist(ctx context.Context, args []interface{}) { + d.jobChan <- job{ctx: ctx, args: args} +} diff --git a/redis/strategy/local_read_single_write.go b/redis/strategy/local_read_single_write.go new file mode 100644 index 0000000..531f0a8 --- /dev/null +++ b/redis/strategy/local_read_single_write.go @@ -0,0 +1,42 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 strategy + +import ( + "context" + + "github.com/go-redis/redis/v8" + "github.com/huaweicloud/devcloud-go/redis/config" +) + +type LocalReadSingleWriteStrategy struct { + abstractStrategy +} + +func newLocalReadSingleWriteStrategy(configuration *config.Configuration) *LocalReadSingleWriteStrategy { + return &LocalReadSingleWriteStrategy{newAbstractStrategy(configuration)} +} + +func (l *LocalReadSingleWriteStrategy) RouteClient(opType commandType) redis.UniversalClient { + if opType == CommandTypeRead { + return l.nearestClient() + } + return l.activeClient() +} + +func (l *LocalReadSingleWriteStrategy) Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error { + return l.activeClient().Watch(ctx, fn, keys...) +} diff --git a/redis/strategy/single_read_write.go b/redis/strategy/single_read_write.go new file mode 100644 index 0000000..37acd16 --- /dev/null +++ b/redis/strategy/single_read_write.go @@ -0,0 +1,39 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 strategy + +import ( + "context" + + "github.com/go-redis/redis/v8" + "github.com/huaweicloud/devcloud-go/redis/config" +) + +type SingleReadWriteStrategy struct { + abstractStrategy +} + +func newSingleReadWriteStrategy(configuration *config.Configuration) *SingleReadWriteStrategy { + return &SingleReadWriteStrategy{newAbstractStrategy(configuration)} +} + +func (s *SingleReadWriteStrategy) RouteClient(opType commandType) redis.UniversalClient { + return s.activeClient() +} + +func (s *SingleReadWriteStrategy) Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error { + return s.activeClient().Watch(ctx, fn, keys...) +} diff --git a/redis/strategy/strategy.go b/redis/strategy/strategy.go new file mode 100644 index 0000000..e8ef400 --- /dev/null +++ b/redis/strategy/strategy.go @@ -0,0 +1,168 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021. + * Licensed 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 strategy defines different route strategy mode. +package strategy + +import ( + "context" + "log" + "reflect" + + "github.com/go-redis/redis/v8" + "github.com/huaweicloud/devcloud-go/redis/config" +) + +type StrategyMode interface { + RouteClient(opType commandType) redis.UniversalClient + Close() error + Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error +} + +func NewStrategy(configuration *config.Configuration) StrategyMode { + switch configuration.RouteAlgorithm { + case SingleReadWriteMode: + return newSingleReadWriteStrategy(configuration) + case LocalReadSingleWriteMode: + return newLocalReadSingleWriteStrategy(configuration) + case DoubleWriteMode: + return newDoubleWriteStrategy(configuration) + default: + log.Printf("ERROR: invalid route algorithm:%v", configuration.RouteAlgorithm) + } + return nil +} + +type commandType int32 + +const ( + CommandTypeRead commandType = iota + CommandTypeWrite + CommandTypeMulti +) + +const ( + SingleReadWriteMode = "single-read-write" + LocalReadSingleWriteMode = "local-read-single-write" + DoubleWriteMode = "double-write" +) + +func isWriteCommand(funcName string, args []interface{}) bool { + if _, ok := writeCommandMap[funcName]; ok { + return true + } + switch funcName { + case "script": + return contains(args, "flush") + case "sort": + return contains(args, "store") + case "georadius": + return contains(args, "store") + } + return false +} + +func contains(args []interface{}, command string) bool { + for _, arg := range args { + if reflect.DeepEqual(arg, command) { + return true + } + } + return false +} + +var writeCommandMap = map[string]bool{ + "set": true, + "del": true, + "expire": true, + "expireat": true, + "persist": true, + "pexpire": true, + "pexpireat": true, + "rename": true, + "renamenx": true, + "restore": true, + "touch": true, + "append": true, + "decr": true, + "decrby": true, + "getset": true, + "getex": true, + "getdel": true, + "incr": true, + "incrby": true, + "incrbyfloat": true, + "mset": true, + "msetnx": true, + "setex": true, + "setnx": true, + "setrange": true, + "setbit": true, + "bitop": true, + "hdel": true, + "hincrby": true, + "hincrbyfloat": true, + "hset": true, + "hmset": true, + "hsetnx": true, + "blpop": true, + "brpop": true, + "blpoplpush": true, + "linsert": true, + "lpop": true, + "lpush": true, + "lpushx": true, + "lrem": true, + "lset": true, + "ltrim": true, + "rpop": true, + "rpoplpush": true, + "rpush": true, + "rpushx": true, + "lmove": true, + "sadd": true, + "sdiffstore": true, + "sinterstore": true, + "smove": true, + "spop": true, + "srem": true, + "sunionstore": true, + "xadd": true, + "xdel": true, + "bzpopmax": true, + "bzpopmin": true, + "zadd": true, + "zincrby": true, + "zinterstore": true, + "zpopmax": true, + "zpopmin": true, + "zrem": true, + "zremrangebyrank": true, + "zremrangebyscore": true, + "zremrangebylex": true, + "zunionstore": true, + "zdiffstore": true, + "pfadd": true, + "pfmerge": true, + "bgrewriteaof": true, + "bgsave": true, + "flushall": true, + "flushdb": true, + "save": true, + "eval": true, + "evalsha": true, + "geoadd": true, + "geosearchstore": true, +} diff --git a/sql-driver/bench_test/benchmark_test.go b/sql-driver/bench_test/benchmark_test.go new file mode 100644 index 0000000..89daf67 --- /dev/null +++ b/sql-driver/bench_test/benchmark_test.go @@ -0,0 +1,192 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022. + * Licensed 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 bench_test Connection test performance comparison +package bench_test + +import ( + "database/sql" + "fmt" + "log" + "testing" + "time" + + "github.com/astaxie/beego/orm" + "gorm.io/driver/mysql" + "gorm.io/gorm" + + devspore "github.com/huaweicloud/devcloud-go/sql-driver/mysql" + "github.com/huaweicloud/devcloud-go/sql-driver/rds/config" +) + +var ( + url = "tcp(127.0.0.1:3306)/kcluster?parseTime=true" + username = "root" + password = "123456" + sqlDb, devSporeDb *sql.DB + sqlOrm, devSporeOrm orm.Ormer + sqlGorm, devSporeGorm *gorm.DB + err error +) + +type Metadata struct { + Id int `gorm:"column:id" json:"id"` + Name string `gorm:"column:name;primaryKey"` + Defaultdata string `gorm:"column:defaultdata"` + List string `gorm:"column:list"` + Createtime time.Time `gorm:"column:createtime;autoCreateTime"` + Updatetime time.Time `gorm:"column:updatetime;autoUpdateTime"` +} + +func (t *Metadata) TableName() string { + return "metadatas" +} + +func mysqlConfiguration() *config.ClusterConfiguration { + return &config.ClusterConfiguration{ + RouterConfig: &config.RouterConfiguration{ + Nodes: map[string]*config.NodeConfiguration{ + "dc1": { + Master: "ds1", + }, + }, + Active: "dc1", + }, + DataSource: map[string]*config.DataSourceConfiguration{ + "ds1": { + URL: url, + Username: username, + Password: password, + }, + }, + } +} + +func startService() { + devspore.SetClusterConfiguration(mysqlConfiguration()) + orm.RegisterModel(new(Metadata)) + err = orm.RegisterDriver("devspore_mysql", orm.DRMySQL) + if err != nil { + log.Fatalln(err) + } + + // Native Data Source + sqlDb, err = sql.Open("mysql", username+":"+password+"@"+url) + if err != nil { + log.Fatalln(err) + } + devSporeDb, err = sql.Open("devspore_mysql", "") + if err != nil { + log.Fatalln(err) + } + + // Beego-orm + sqlOrm, err = orm.NewOrmWithDB("mysql", "sqlOrm", sqlDb) + if err != nil { + log.Fatalln(err) + } + devSporeOrm, err = orm.NewOrmWithDB("devspore_mysql", "devSporeOrm", devSporeDb) + if err != nil { + log.Fatalln(err) + } + + // Gorm + sqlGorm, err = gorm.Open(mysql.New(mysql.Config{DriverName: "mysql", DSN: username + ":" + password + "@" + url})) + if err != nil { + log.Fatalln(err) + } + devSporeGorm, err = gorm.Open(mysql.New(mysql.Config{DriverName: "devspore_mysql", DSN: ""})) + if err != nil { + log.Fatalln(err) + } +} +func stopService() { + err = sqlDb.Close() + if err != nil { + log.Println(err) + } + err = devSporeDb.Close() + if err != nil { + log.Println(err) + } +} + +func BenchmarkDB(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var res string + err = sqlDb.QueryRow("select defaultdata from metadatas where name=?", "bb").Scan(&res) + if err != nil { + fmt.Println(err) + } + } +} + +func BenchmarkDevSporeDB(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var res string + err = devSporeDb.QueryRow("select defaultdata from metadatas where name=?", "bb").Scan(&res) + if err != nil { + fmt.Println(err) + } + } +} + +func BenchmarkOrm(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var res string + if err = sqlOrm.Raw("select defaultdata from metadatas where name=?", "bb").QueryRow(&res); err != nil { + fmt.Println(err) + } + } +} + +func BenchmarkDevSporeOrm(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var res string + if err = devSporeOrm.Raw("select defaultdata from metadatas where name=?", "bb").QueryRow(&res); err != nil { + fmt.Println(err) + } + } +} + +func BenchmarkGorm(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var res string + if err = sqlGorm.Raw("select defaultdata from metadatas where name=?", "bb").Scan(&res).Error; err != nil { + fmt.Println(err) + } + } +} + +func BenchmarkDevSporeGorm(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var res string + if err = devSporeGorm.Raw("select defaultdata from metadatas where name=?", "bb").Scan(&res).Error; err != nil { + fmt.Println(err) + } + } +} + +func TestMain(m *testing.M) { + startService() + m.Run() + stopService() +} diff --git a/sql-driver/mysql/README.md b/sql-driver/mysql/README.md index fb96fcd..9f25727 100644 --- a/sql-driver/mysql/README.md +++ b/sql-driver/mysql/README.md @@ -1,5 +1,12 @@ # devcloud-go/sql-driver/mysql +### Introduction +Currently, MySQL supports two modes.single-read-write and local-read-single-write. +In addition, read/write separation is supported, which can be configured as random or RoundRobin. +##### single-read-write +![image](../../img/mysql-single-read-write.png) +##### local-read-single-write +![image](../../img/mysql-local-read-single-write.png) ### Quickstart: ```bigquery import ( @@ -22,39 +29,39 @@ func main() { } func devsporeConfiguration() *config.ClusterConfiguration { return &config.ClusterConfiguration{ - Props: &mas.PropertiesConfiguration{ - AppID: "xxx", - MonitorID: "xxx", - DatabaseName: "xx", - }, + Props: &mas.PropertiesConfiguration{ + AppID: "xxx", + MonitorID: "xxx", + DatabaseName: "xx", + }, EtcdConfig: &etcd.EtcdConfiguration{ - Address: "127.0.0.1:2379,127.0.0.2:2379,127.0.0.3:2379", - Username: "etcduser", - Password: "etcdpwd", - HTTPSEnable: false, + Address: "127.0.0.1:2379,127.0.0.2:2379,127.0.0.3:2379", + Username: "etcduser", + Password: "etcdpwd", + HTTPSEnable: false, }, RouterConfig: &config.RouterConfiguration{ - Nodes: map[string]*config.NodeConfiguration{ - "dc1": { - Master: "ds1", - }, - "dc2": { - Master: "ds2", - }, - }, - Active: "dc1", + Nodes: map[string]*config.NodeConfiguration{ + "dc1": { + Master: "ds1", + }, + "dc2": { + Master: "ds2", + }, + }, + Active: "dc1", }, DataSource: map[string]*config.DataSourceConfiguration{ - "ds1": { - URL: "tcp(127.0.0.1:3306)/ds0?charset=utf8&parseTime=true", - Username: "root", - Password: "123456", - }, - "ds2": { - URL: "tcp(127.0.0.1:3307)/ds0?charset=utf8&parseTime=true", - Username: "root", - Password: "123456", - }, + "ds1": { + URL: "tcp(127.0.0.1:3306)/ds0?charset=utf8&parseTime=true", + Username: "root", + Password: "123456", + }, + "ds2": { + URL: "tcp(127.0.0.1:3307)/ds0?charset=utf8&parseTime=true", + Username: "root", + Password: "123456", + }, }, } } @@ -96,6 +103,30 @@ func main() { ......THEN } +3.beego-orm +import ( + "log" + + "github.com/astaxie/beego/orm" + _ "github.com/huaweicloud/devcloud-go/sql-driver/mysql" +) + +func main() { + // register devspore_mysql + err = orm.RegisterDriver("devspore_mysql", orm.DRMySQL) + if err != nil { + log.Fatalln(err) + } + // register model + orm.RegisterModel(new(interface{}),new(interface{})) + + err = orm.RegisterDataBase("default", "devspore_mysql", "xxx/config_with_password.yaml") + if err != nil { + log.Fatalln(err) + } + db:= orm.NewOrm() + ......THEN +} ``` **Version requirements:go1.14.6 and above** @@ -164,4 +195,165 @@ router: # Require - ds1-slave0 - ds1-slave1 -``` \ No newline at end of file +``` + +### Fault injection +You can also create a database service with injection failures by adding configurations. +```bigquery +func devsporeConfiguration() *config.ClusterConfiguration { + return &config.ClusterConfiguration{ + Props: &mas.PropertiesConfiguration{ + AppID: "xxx", + MonitorID: "xxx", + DatabaseName: "xx", + }, + EtcdConfig: &etcd.EtcdConfiguration{ + Address: "127.0.0.1:2379,127.0.0.2:2379,127.0.0.3:2379", + Username: "etcduser", + Password: "etcdpwd", + HTTPSEnable: false, + }, + RouterConfig: &config.RouterConfiguration{ + Nodes: map[string]*config.NodeConfiguration{ + "dc1": { + Master: "ds1", + }, + "dc2": { + Master: "ds2", + }, + }, + Active: "dc1", + }, + DataSource: map[string]*config.DataSourceConfiguration{ + "ds1": { + URL: "tcp(127.0.0.1:3306)/ds0?charset=utf8&parseTime=true", + Username: "root", + Password: "123456", + }, + "ds2": { + URL: "tcp(127.0.0.1:3307)/ds0?charset=utf8&parseTime=true", + Username: "root", + Password: "123456", + }, + }, + Chaos: &mas.InjectionProperties{ + Active: true, + Duration: 50, + Interval: 100, + Percentage: 100, + DelayInjection: &mas.DelayInjection{ + Active: true, + Percentage: 75, + TimeMs: 1000, + JitterMs: 500, + }, + ErrorInjection: &mas.ErrorInjection{ + Active: true, + Percentage: 30, + }, + }, + } +} +``` +Alternatively, add the following configuration to the configuration file: +```bigquery +chaos: + active: true + duration: 20 + interval: 100 + percentage: 100 + delayInjection: + active: true + percentage: 75 + timeMs: 1000 + jitterMs: 500 + errorInjection: + active: true + percentage: 20 +``` + +### Description of Configuration Parameters + + +ClusterConfiguration + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
propsPropertiesConfigurationFor details,see the description of the data structure of PropertiesConfigurationMas monitoring configuration,which is used together with etcd
etcdEtcdConfigurationFor details,see the description of the data structure of EtcdConfigurationEtcd configuration.If it is configured, it will be pulled from the remote end
datasourcemap[string]DataSourceConfigurationThe key is customized,for details about a single dimension,see the description of the data structure of DataSourceConfigurationDataSource
routerRouterConfigurationFor details,see the description of the data structure of RouterConfigurationRoute-related configuration
chaosInjectionPropertiesFor details,see the description of the data structure of InjectionPropertiesFault Injection Configuration
+ + +PropertiesConfiguration + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
versionstring-Project version number
appIdstring-Project name
monitorIdstring-Monitoring group name
databaseNamestring-Database name
+ + +EtcdConfiguration + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
addressstring-Etcd address
apiVersionstring-Etcd interface Version
usernamestring-Etcd username
passwordstring-Etcd password
httpEnablebool-Specifies whether HTTPS is enabled for Etcd
+ + +DataSourceConfiguration + + + + + + +
Parameter NameParameter TypeValue rangeDescription
urlstringprotocol(address)/dbname?param=valueData Source Name
usernamestring-Username
passwordstring-Password
+ + +RouterConfiguration + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
activestringKey of the nodeActivating Nodes
routeAlgorithmstringsingle-read-write,local-read-single-writeRouting algorithm
retry.timesstring-Failed Retry Times
retry.delaystring-Retry interval,in milliseconds
nodesmap[string]NodeConfigurationThe key is customized,for details about a single dimension,see the description of the data structure of NodeConfigurationNode-related configuration
+ + +NodeConfiguration + + + + + + +
Parameter NameParameter TypeValue rangeDescription
masterstringkey of the datasourceMaster node datasource
loadBalancestringRANDOM,ROUND_ROBINLoad balancing algorithm for read/write separation
slaves[]stringkey of the datasourceSlave node datasource
+ + +InjectionProperties + + + + + + + + + + + + + +
Parameter NameParameter TypeValue rangeDescription
activebooltrue/falseWhether the fault injection function is enabled
durationint-Fault injection duration,in seconds
intervalint-Fault injection interval,in seconds
percentageint0-100Injection failure probability
delayInjection.activebooltrue/falseDelay injection switch
delayInjection.percentageint0-100Delayed Fault Effective Probability
delayInjection.timeMsint-Indicates the delay base,in milliseconds
delayInjection.jitterMsint-Indicates the jitter amplitude of the delay, in milliseconds
errorInjection.activebooltrue/falseAbnormal injection switch
errorInjection.percentageint0-100Abnormal Fault Effective Probability
\ No newline at end of file diff --git a/sql-driver/mysql/devspore_connection.go b/sql-driver/mysql/devspore_connection.go index 3995cf9..616e294 100644 --- a/sql-driver/mysql/devspore_connection.go +++ b/sql-driver/mysql/devspore_connection.go @@ -28,6 +28,7 @@ type devsporeConn struct { clusterDataSource *datasource.ClusterDataSource cachedConn sync.Map inTransaction bool + executor *executor } // Begin Deprecated @@ -76,10 +77,10 @@ func (dc *devsporeConn) BeginTx(ctx context.Context, opts driver.TxOptions) (dri req := &executorReq{ ctx: ctx, opts: opts, - methodName: "BeginTx", + methodName: BeginTx, dc: dc, } - resp := getExecutor().tryExecute(req) + resp := dc.executor.tryExecute(req) if resp.err != nil { log.Printf("ERROR: devsporeConnection execute BeginTx failed, err %v", resp.err) return nil, resp.err @@ -97,10 +98,10 @@ func (dc *devsporeConn) QueryContext(ctx context.Context, query string, args []d ctx: ctx, query: query, ctxArgs: args, - methodName: "QueryContext", + methodName: QueryContext, dc: dc, } - resp := getExecutor().tryExecute(req) + resp := dc.executor.tryExecute(req) if resp.err != nil && resp.err != driver.ErrSkip { log.Printf("ERROR: devsporeConnection execute QueryContext failed, err %v", resp.err) } @@ -113,10 +114,10 @@ func (dc *devsporeConn) ExecContext(ctx context.Context, query string, args []dr ctx: ctx, query: query, ctxArgs: args, - methodName: "ExecContext", + methodName: ExecContext, dc: dc, } - resp := getExecutor().tryExecute(req) + resp := dc.executor.tryExecute(req) if resp.err != nil && resp.err != driver.ErrSkip { log.Printf("ERROR: devsporeConnection execute ExecContext failed, err %v", resp.err) } diff --git a/sql-driver/mysql/devspore_connector.go b/sql-driver/mysql/devspore_connector.go index 8d58a51..85cd84c 100644 --- a/sql-driver/mysql/devspore_connector.go +++ b/sql-driver/mysql/devspore_connector.go @@ -22,16 +22,19 @@ import ( ) type devsporeConnector struct { + executor *executor clusterDataSource *datasource.ClusterDataSource } // Connect implements driver.Connector interface. // Connect returns a devsporeConn. func (c *devsporeConnector) Connect(ctx context.Context) (driver.Conn, error) { - return &devsporeConn{ + dc := &devsporeConn{ clusterDataSource: c.clusterDataSource, inTransaction: false, - }, nil + executor: c.executor, + } + return dc, nil } // Driver implements driver.Connector interface, Driver returns &DevsporeDriver{}. diff --git a/sql-driver/mysql/devspore_driver.go b/sql-driver/mysql/devspore_driver.go index db2ef25..5a7457b 100644 --- a/sql-driver/mysql/devspore_driver.go +++ b/sql-driver/mysql/devspore_driver.go @@ -11,22 +11,24 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package mysql provides a DevsporeDriver for Go's database/sql package. - * Which can automatically switch data sources, read and write separation. - * The driver should be used with "github.com/go-sql-driver/mysql": + */ - * import ( - * "database/sql" - * "github.com/huaweicloud/devcloud-go/common/password" - * _ "github.com/huaweicloud/devcloud-go/sql-driver/mysql" - * ) +/* +Package mysql provides a DevsporeDriver for Go's database/sql package. +Which can automatically switch data sources, read and write separation. +The driver should be used with "github.com/go-sql-driver/mysql": - * password.SetDecipher(&MyDecipher{}) - * db, err := sql.Open("devspore_mysql", yamlConfigPath) +import ( +"database/sql" +"github.com/huaweicloud/devcloud-go/common/password" +_ "github.com/huaweicloud/devcloud-go/sql-driver/mysql" +) - * See README.md for more details. - */ +password.SetDecipher(&MyDecipher{}) +db, err := sql.Open("devspore_mysql", yamlConfigPath) +See README.md for more details. +*/ package mysql import ( @@ -78,21 +80,20 @@ func (d DevsporeDriver) OpenConnector(yamlFilePath string) (driver.Connector, er log.Printf("ERROR: create clusterdataSource failed, %v", err) return nil, err } - actualExecutor = newExecutor(clusterDataSource.RouterConfiguration.Retry) - return &devsporeConnector{clusterDataSource: clusterDataSource}, nil + actualExecutor := newExecutor(clusterDataSource.RouterConfiguration.Retry, configuration.Chaos) + return &devsporeConnector{clusterDataSource: clusterDataSource, executor: actualExecutor}, nil } var clusterConfiguration *config.ClusterConfiguration -func SetClusterConfiguration(cfg *config.ClusterConfiguration) { +func SetClusterConfiguration(cfg *config.ClusterConfiguration) { clusterConfiguration = cfg } func getClusterConfiguration(yamlFilePath string) (*config.ClusterConfiguration, error) { - if clusterConfiguration != nil { + if len(yamlFilePath) == 0 && clusterConfiguration != nil { return clusterConfiguration, nil } - // validate yaml file path realPath, err := filepath.Abs(yamlFilePath) if err != nil { return nil, err diff --git a/sql-driver/mysql/devspore_driver_test.go b/sql-driver/mysql/devspore_driver_test.go index e8e5332..e4bd64b 100644 --- a/sql-driver/mysql/devspore_driver_test.go +++ b/sql-driver/mysql/devspore_driver_test.go @@ -37,13 +37,13 @@ var _ = Describe("CRUD", func() { err error activeNode *datasource.NodeDataSource ) - metadata := mock.MysqlMetaData{ + metadata := mock.MysqlMock{ User: "root", Password: "root", Address: "127.0.0.1:13306", Databases: []string{"ds0", "ds0-slave0", "ds0-slave1", "ds1", "ds1-slave0", "ds1-slave1"}, } - mock.StartMockMysql(metadata) + metadata.StartMockMysql() BeforeEach(func() { devsporeDB, err = sql.Open("devspore_mysql", "../rds/resources/driver_test_config.yaml") diff --git a/sql-driver/mysql/devspore_statement.go b/sql-driver/mysql/devspore_statement.go index 7a51b7a..703cbb6 100644 --- a/sql-driver/mysql/devspore_statement.go +++ b/sql-driver/mysql/devspore_statement.go @@ -25,6 +25,7 @@ type devsporeStmt struct { dc *devsporeConn stmt driver.Stmt query string + dsn string } // Close devsporeStmt @@ -42,11 +43,11 @@ func (dsmt *devsporeStmt) NumInput() int { req := &executorReq{ ctx: dsmt.ctx, query: dsmt.query, - methodName: "stmt.NumInput", + methodName: StmtNumInput, dc: dsmt.dc, dsmt: dsmt, } - resp := getExecutor().tryExecute(req) + resp := dsmt.dc.executor.tryExecute(req) if resp.err != nil { log.Printf("ERROR: devsporeStatement execute NumInput failed, err %v", resp.err) } @@ -71,11 +72,11 @@ func (dsmt *devsporeStmt) QueryContext(ctx context.Context, args []driver.NamedV ctx: ctx, query: dsmt.query, ctxArgs: args, - methodName: "stmt.QueryContext", + methodName: StmtQueryContext, dc: dsmt.dc, dsmt: dsmt, } - resp := getExecutor().tryExecute(req) + resp := dsmt.dc.executor.tryExecute(req) if resp.err != nil { log.Printf("ERROR: devsporeStatement execute QueryContext failed, err %v", resp.err) } @@ -88,11 +89,11 @@ func (dsmt *devsporeStmt) ExecContext(ctx context.Context, args []driver.NamedVa ctx: ctx, query: dsmt.query, ctxArgs: args, - methodName: "stmt.ExecContext", + methodName: StmtExecContext, dc: dsmt.dc, dsmt: dsmt, } - resp := getExecutor().tryExecute(req) + resp := dsmt.dc.executor.tryExecute(req) if resp.err != nil { log.Printf("ERROR: devsporeStatement execute ExecContext failed, err %v", resp.err) } @@ -101,9 +102,10 @@ func (dsmt *devsporeStmt) ExecContext(ctx context.Context, args []driver.NamedVa // getStatement get an actual statement from devsporeStmt if exists or create a new statement. func (dsmt *devsporeStmt) getStatement(ctx context.Context, dsn string) (driver.Stmt, error) { - if dsmt.stmt != nil { + if dsmt.stmt != nil && dsmt.dsn == dsn { return dsmt.stmt, nil } + dsmt.dsn = dsn conn, err := dsmt.dc.getConnection(ctx, dsn) if err != nil { return nil, err diff --git a/sql-driver/mysql/executor.go b/sql-driver/mysql/executor.go index 366799e..153397b 100644 --- a/sql-driver/mysql/executor.go +++ b/sql-driver/mysql/executor.go @@ -23,6 +23,7 @@ import ( "sync" "time" + "github.com/huaweicloud/devcloud-go/mas" "github.com/huaweicloud/devcloud-go/sql-driver/rds/config" "github.com/huaweicloud/devcloud-go/sql-driver/rds/datasource" "github.com/huaweicloud/devcloud-go/sql-driver/rds/router" @@ -35,17 +36,22 @@ const ( exclusiveRetryDelay = 60000 // ms ) -var actualExecutor *executor - -func getExecutor() *executor { - return actualExecutor -} - var ( errNoDatasource = errors.New("no available data source") errTypeAssertion = errors.New("type assertion err") ) +type methodName string + +const ( + BeginTx methodName = "BeginTx" + QueryContext methodName = "QueryContext" + ExecContext methodName = "ExecContext" + StmtQueryContext methodName = "stmt.QueryContext" + StmtExecContext methodName = "stmt.ExecContext" + StmtNumInput methodName = "stmt.NumInput" +) + // executorReq contains all connection and statement method params type executorReq struct { dc *devsporeConn @@ -55,7 +61,7 @@ type executorReq struct { query string ctxArgs []driver.NamedValue opts driver.TxOptions - methodName string + methodName methodName } // executorResp contains all connection and statement method return params @@ -69,17 +75,22 @@ type executorResp struct { } type executor struct { - exclusives sync.Map - retryTimes int - retryDelay int // ms + exclusives sync.Map + retryTimes int + retryDelay int // ms + injectionManagement *mas.InjectionManagement } -func newExecutor(retry *config.RetryConfiguration) *executor { +func newExecutor(retry *config.RetryConfiguration, chaos *mas.InjectionProperties) *executor { e := &executor{ retryTimes: defaultRetryTimes, retryDelay: defaultRetryDelay, exclusives: sync.Map{}, } + if chaos != nil { + e.injectionManagement = mas.NewInjectionManagement(chaos) + e.injectionManagement.SetError(mas.MysqlErrors()) + } if retry != nil && retry.Times != "" { if retryTimes, err := strconv.Atoi(retry.Times); err != nil { e.retryTimes = retryTimes @@ -92,9 +103,21 @@ func newExecutor(retry *config.RetryConfiguration) *executor { } return e } +func (e *executor) beforeTryExecute() *executorResp { + err := e.injectionManagement.Inject() + if err != nil { + return &executorResp{ + err: err, + } + } + return nil +} // from cluster datasource choose a node datasource func (e *executor) tryExecute(req *executorReq) *executorResp { + if err := e.beforeTryExecute(); err != nil { + return err + } // insure parse sql only once isSQLOnlyRead := util.IsOnlyRead(req.query) // route node datasource @@ -187,30 +210,30 @@ func (e *executor) execute(req *executorReq, dsn string) *executorResp { } switch req.methodName { // conn methods - case "BeginTx": + case BeginTx: if connBeginTx, ok := conn.(driver.ConnBeginTx); ok { tx, err = connBeginTx.BeginTx(req.ctx, req.opts) } else { err = errTypeAssertion } - case "QueryContext": + case QueryContext: if queryerCtx, ok := conn.(driver.QueryerContext); ok { rows, err = queryerCtx.QueryContext(req.ctx, req.query, req.ctxArgs) } else { err = errTypeAssertion } - case "ExecContext": + case ExecContext: if execerCtx, ok := conn.(driver.ExecerContext); ok { result, err = execerCtx.ExecContext(req.ctx, req.query, req.ctxArgs) } else { err = errTypeAssertion } // statement methods - case "stmt.QueryContext": + case StmtQueryContext: rows, err = stmtQueryContext(req, dsn) - case "stmt.ExecContext": + case StmtExecContext: result, err = stmtExecContext(req, dsn) - case "stmt.NumInput": + case StmtNumInput: numInput, err = stmtNumInput(req, dsn) } diff --git a/sql-driver/rds/config/configuration.go b/sql-driver/rds/config/configuration.go index 2bc7d8f..51a6309 100644 --- a/sql-driver/rds/config/configuration.go +++ b/sql-driver/rds/config/configuration.go @@ -11,10 +11,12 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package config defines a series of configuration, include yaml configuration, remote configuration - * and integration configuration which contains the above two configurations. */ +/* +Package config defines a series of configuration, include yaml configuration, remote configuration +and integration configuration which contains the above two configurations. +*/ package config import ( @@ -33,6 +35,7 @@ type ClusterConfiguration struct { EtcdConfig *etcd.EtcdConfiguration `yaml:"etcd"` RouterConfig *RouterConfiguration `yaml:"router"` DataSource map[string]*DataSourceConfiguration `yaml:"datasource"` + Chaos *mas.InjectionProperties `yaml:"chaos"` } // RouterConfiguration yaml router configuration entity diff --git a/sql-driver/rds/config/loader/configuration_file_handler.go b/sql-driver/rds/config/loader/configuration_file_handler.go index 642dbc2..0e809ce 100644 --- a/sql-driver/rds/config/loader/configuration_file_handler.go +++ b/sql-driver/rds/config/loader/configuration_file_handler.go @@ -11,11 +11,13 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package loader provides ConfigurationFileHandler and RemoteConfigurationLoader, - * the ConfigurationFileHandler is used for load config from cache and save remote config to cache, - * the RemoteConfigurationLoader is used for load config from remote etcd. */ +/* +Package loader provides ConfigurationFileHandler and RemoteConfigurationLoader, +the ConfigurationFileHandler is used for load config from cache and save remote config to cache, +the RemoteConfigurationLoader is used for load config from remote etcd. +*/ package loader import ( diff --git a/sql-driver/rds/config/loader/configuration_file_handler_test.go b/sql-driver/rds/config/loader/configuration_file_handler_test.go index 97e619a..70ef2e7 100644 --- a/sql-driver/rds/config/loader/configuration_file_handler_test.go +++ b/sql-driver/rds/config/loader/configuration_file_handler_test.go @@ -98,7 +98,7 @@ func TestConfigurationFileHandler_Save(t *testing.T) { handler := NewConfigurationFileHandler() // remove defaultCacheConfigFile if exists if _, err := os.Stat(handler.cacheFilePath); err == nil { - if err = os.Remove(handler.cacheFilePath); err != nil { + if err := os.Remove(handler.cacheFilePath); err != nil { t.Log("remove cache file failed") return } @@ -129,7 +129,7 @@ func TestConfigurationFileHandler_LoadFailed(t *testing.T) { homeDir := getHomeDir() + string(os.PathSeparator) + ".devspore" + string(os.PathSeparator) cacheFilePath := homeDir + "remote-config-test_hashCode.json" if _, err := os.Stat(cacheFilePath); err == nil { - if err = os.Remove(cacheFilePath); err != nil { + if err := os.Remove(cacheFilePath); err != nil { t.Log("remove cache file failed") return } diff --git a/sql-driver/rds/config/loader/remote_configuration_loader_test.go b/sql-driver/rds/config/loader/remote_configuration_loader_test.go index d5a3003..7bfd841 100644 --- a/sql-driver/rds/config/loader/remote_configuration_loader_test.go +++ b/sql-driver/rds/config/loader/remote_configuration_loader_test.go @@ -49,6 +49,7 @@ func TestRemoteConfigurationLoader_GetConfiguration(t *testing.T) { loader.etcdClient = mockClient createRemoteConfiguration(mockClient, loader) remoteConfiguration := loader.GetConfiguration(props.CalHashCode()) + assert.NotNil(t, remoteConfiguration) assert.NotNil(t, remoteConfiguration.DataSources) assert.Equal(t, len(remoteConfiguration.DataSources), 6) @@ -57,14 +58,23 @@ func TestRemoteConfigurationLoader_GetConfiguration(t *testing.T) { assert.True(t, ok) err := removeTempFile(props.CalHashCode()) + if err != nil { + log.Println(err) + } assert.Nil(t, err) } +func removeTempFile(hashCode string) error { + fileHandler := NewConfigurationFileHandler() + filePath := fileHandler.getCompleteCacheFilePath(hashCode) + return os.RemoveAll(filePath) +} + func TestGetConfigurationFromCache(t *testing.T) { handler := NewConfigurationFileHandler() // remove defaultCacheConfigFile if exists if _, err := os.Stat(handler.cacheFilePath); err == nil { - if err = os.Remove(handler.cacheFilePath); err != nil { + if err := os.Remove(handler.cacheFilePath); err != nil { t.Log("remove cache file failed") return } @@ -93,6 +103,9 @@ func TestGetConfigurationFromCache(t *testing.T) { assert.True(t, ok) err := removeTempFile(props.CalHashCode()) + if err != nil { + log.Println(err) + } assert.Nil(t, err) } @@ -102,7 +115,6 @@ func createRemoteConfiguration(mockClient *mocks.EtcdClient, loader *RemoteConfi log.Printf("json marshal datasources failed, err %v", err) } mockClient.On("Get", loader.dataSourceKey).Return(string(datasourceStr), nil).Once() - routerConfigStr, err := json.Marshal(routerConfig) if err != nil { log.Printf("json marshal routerConfig failed, err %v", err) @@ -111,10 +123,14 @@ func createRemoteConfiguration(mockClient *mocks.EtcdClient, loader *RemoteConfi mockClient.On("Get", loader.activeKey).Return("c1", nil).Once() } -// TestListener need actual etcd address func TestListener(t *testing.T) { dataDir := "etcd_data" - defer os.RemoveAll(dataDir) + defer func() { + err := os.RemoveAll(dataDir) + if err != nil { + log.Println("remove failed") + } + }() metadata := mock.NewEtcdMetadata() metadata.DataDir = dataDir mockEtcd := &mock.MockEtcd{} @@ -123,10 +139,16 @@ func TestListener(t *testing.T) { loader := NewRemoteConfigurationLoader(props, getEtcdConfiguration()) loader.Init() - defer loader.Close() + defer func() { + err := loader.Close() + log.Println(err) + }() loader.AddRouterListener(&mockListener{}) err := modifyRouterConfig() + if err != nil { + log.Println(err) + } assert.Nil(t, err) active, err := loader.etcdClient.Get(loader.activeKey) @@ -148,6 +170,9 @@ func modifyRouterConfig() error { newVal = "c0" } _, err = client.Put(loader.activeKey, newVal) + if err != nil { + return err + } return err } @@ -166,9 +191,3 @@ func getEtcdConfiguration() *etcd.EtcdConfiguration { HTTPSEnable: false, } } - -func removeTempFile(fileHashCode string) error { - fileHandler := NewConfigurationFileHandler() - filePath := fileHandler.getCompleteCacheFilePath(fileHashCode) - return os.RemoveAll(filePath) -} diff --git a/sql-driver/rds/datasource/cluster_datasource.go b/sql-driver/rds/datasource/cluster_datasource.go index 0d3d30c..dbd9a74 100644 --- a/sql-driver/rds/datasource/cluster_datasource.go +++ b/sql-driver/rds/datasource/cluster_datasource.go @@ -11,12 +11,14 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package datasource defines datasource interface, and implements the interface by ClusterDataSource, - * NodeDataSource, and ActualDataSource. The ClusterDataSource have auto change target datasource capabilities, - * NodeDatasource have read/write separation and failure retry capabilities, ActualDataSource is an actual - * datasource which contains actual dsn. */ +/* +Package datasource defines datasource interface, and implements the interface by ClusterDataSource, +NodeDataSource, and ActualDataSource. The ClusterDataSource have auto change target datasource capabilities, +NodeDatasource have read/write separation and failure retry capabilities, ActualDataSource is an actual +datasource which contains actual dsn. +*/ package datasource import ( diff --git a/sql-driver/rds/resources/config.yaml b/sql-driver/rds/resources/config.yaml index 0cf7a75..635a167 100644 --- a/sql-driver/rds/resources/config.yaml +++ b/sql-driver/rds/resources/config.yaml @@ -58,4 +58,17 @@ router: loadBalance: ROUND_ROBIN slaves: - ds1-slave0 - - ds1-slave1 \ No newline at end of file + - ds1-slave1 +chaos: + active: true + duration: 20 + interval: 100 + percentage: 100 + delayInjection: + active: true + percentage: 75 + timeMs: 1000 + jitterMs: 500 + errorInjection: + active: true + percentage: 20 diff --git a/sql-driver/rds/router/location_base_cluster_route_strategy_test.go b/sql-driver/rds/router/location_base_cluster_route_strategy_test.go index 62eaf39..cb681c2 100644 --- a/sql-driver/rds/router/location_base_cluster_route_strategy_test.go +++ b/sql-driver/rds/router/location_base_cluster_route_strategy_test.go @@ -46,7 +46,7 @@ func TestLocationBaseClusterRouteStrategy_ReadSQL(t *testing.T) { assert.NotNil(t, targetDataSource) assert.Equal(t, node1, targetDataSource) - // change region "az2" which has nothing, then should return active node + // change region "az2" which has nothing clusterDataSource.Region = "az2" targetDataSource = NewClusterRouter("local-read-single-write").Route(true, runtimeCtx, make(map[datasource.DataSource]bool)) assert.NotNil(t, targetDataSource) diff --git a/sql-driver/rds/router/router.go b/sql-driver/rds/router/router.go index 9ca80be..d6dfef6 100644 --- a/sql-driver/rds/router/router.go +++ b/sql-driver/rds/router/router.go @@ -11,10 +11,12 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package router which will route an actual datasource according to route strategy and sql type. - * the package defines two interfaces, RouteStrategy interface and Router interface. */ +/* +Package router which will route an actual datasource according to route strategy and sql type. +the package defines two interfaces, RouteStrategy interface and Router interface. +*/ package router import ( diff --git a/sql-driver/rds/util/devspore_util.go b/sql-driver/rds/util/devspore_util.go index a65d876..fed0909 100644 --- a/sql-driver/rds/util/devspore_util.go +++ b/sql-driver/rds/util/devspore_util.go @@ -11,9 +11,9 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * - * Package util provides the function IsOnlyRead to check sql is only read, and provides a LRUCache. */ +// Package util provides the function IsOnlyRead to check sql is only read, and provides a LRUCache. package util import ( diff --git a/web/controller/abstract_controller.go b/web/controller/abstract_controller.go new file mode 100644 index 0000000..2e86b52 --- /dev/null +++ b/web/controller/abstract_controller.go @@ -0,0 +1,103 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2020-2022. All rights reserved. + * Licensed 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 controller + +import ( + "errors" + "github.com/gin-gonic/gin" + "net/http" + + "github.com/huaweicloud/devcloud-go/web/domain" + "github.com/huaweicloud/devcloud-go/web/resp" + "github.com/huaweicloud/devcloud-go/web/utils" +) + +type AbstractController struct { + domain *domain.AbstractDomain +} + +func NewAbstractController(domain *domain.AbstractDomain) *AbstractController { + return &AbstractController{ + domain, + } +} + +func (c *AbstractController) Get(ctx *gin.Context) { + pk := utils.GetStringParam(ctx, c.domain.PKJson) + model, errMsg := c.domain.GetOneByPK(pk) + if errMsg != nil { + ctx.JSON(errMsg.Errno, errMsg) + return + } + ctx.JSON(http.StatusOK, model) +} + +func (c *AbstractController) Add(ctx *gin.Context) { + model := c.domain.GetModel() + if err := ctx.BindJSON(model.Interface()); err != nil { + ctx.JSON(http.StatusBadRequest, resp.BadRequestErr2Json(err)) + return + } + curModel, errMsg := c.domain.Add(model.Interface()) + if errMsg != nil { + ctx.JSON(errMsg.Errno, errMsg) + return + } + ctx.JSON(http.StatusOK, curModel) +} + +func (c *AbstractController) Update(ctx *gin.Context) { + pk := utils.GetStringParam(ctx, c.domain.PKJson) + model := c.domain.GetModel() + var err error + if err = ctx.BindJSON(model.Interface()); err != nil { + ctx.JSON(http.StatusBadRequest, resp.BadRequestErr2Json(err)) + return + } + if model.Elem().FieldByName(c.domain.PKName).String() != pk { + ctx.JSON(http.StatusBadRequest, resp.BadRequestErr2Json(errors.New("different "+c.domain.PKJson+" between path and body"))) + return + } + curModel, errMsg := c.domain.Update(model.Interface(), pk) + if errMsg != nil { + ctx.JSON(errMsg.Errno, errMsg) + return + } + ctx.JSON(http.StatusOK, curModel) +} + +func (c *AbstractController) Delete(ctx *gin.Context) { + pk := utils.GetStringParam(ctx, c.domain.PKJson) + if errMsg := c.domain.Delete(pk); errMsg != nil { + ctx.JSON(errMsg.Errno, errMsg) + return + } + ctx.JSON(http.StatusOK, "OK") +} + +func (c *AbstractController) GetAll(ctx *gin.Context) { + queryCond, err := utils.ParseQueryCond(ctx) + if err != nil { + ctx.JSON(http.StatusBadRequest, resp.BadRequestErr2Json(err)) + return + } + models, errMsg := c.domain.GetList(queryCond) + if errMsg != nil { + ctx.JSON(errMsg.Errno, errMsg) + return + } + ctx.JSON(http.StatusOK, models) +} diff --git a/web/dao/abstract_dao.go b/web/dao/abstract_dao.go new file mode 100644 index 0000000..0d2d877 --- /dev/null +++ b/web/dao/abstract_dao.go @@ -0,0 +1,112 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2020-2022. All rights reserved. + * Licensed 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 dao + +import ( + "gorm.io/gorm" + + "github.com/huaweicloud/devcloud-go/web/utils" +) + +type AbstractDao struct { + db *gorm.DB + *utils.ModelInfo +} + +func NewAbstractDao(db *gorm.DB, modelInfo *utils.ModelInfo) *AbstractDao { + return &AbstractDao{ + db, + modelInfo, + } +} + +func (d *AbstractDao) Add(model interface{}) (interface{}, error) { + if err := d.db.Create(model).Error; err != nil { + return nil, err + } + return model, nil +} + +func (d *AbstractDao) AddBatch(models interface{}) (interface{}, error) { + if err := d.db.Create(models).Error; err != nil { + return nil, err + } + return models, nil +} + +func (d *AbstractDao) Update(model interface{}, pk string) (interface{}, error) { + if err := d.db.Updates(model).Error; err != nil { + return nil, err + } + return d.GetOneByPrimaryKey(pk) +} + +func (d *AbstractDao) DeleteByPrimaryKey(primaryKey string) error { + model := d.GetModel().Interface() + return d.db.Where(d.PKJson+" = ?", primaryKey).Delete(model).Error +} + +func (d *AbstractDao) DeleteByPrimaryKeys(primaryKeys []string) error { + model := d.GetModel().Interface() + return d.db.Where(d.PKJson+" IN ?", primaryKeys).Delete(model).Error +} + +func (d *AbstractDao) GetOneByPrimaryKey(primaryKey string) (interface{}, error) { + model := d.GetModel().Interface() + if err := d.db.Where(d.PKJson+" = ?", primaryKey).First(model).Error; err != nil { + return nil, err + } + return model, nil +} + +func (d *AbstractDao) GetListByPrimaryKeys(primaryKeys []string) (interface{}, error) { + models := d.GetModels().Interface() + if err := d.db.Where(d.PKJson+" IN ?", primaryKeys).Find(models).Error; err != nil { + return nil, err + } + return models, nil +} + +func (d *AbstractDao) GetList(queryCond utils.QueryConditions) (interface{}, error) { + models := d.GetModels().Interface() + db := d.db.Model(d.GetModel().Interface()) + db = getCondGormDB(db, queryCond) + if err := db.Find(models).Error; err != nil { + return nil, err + } + return models, nil +} + +func getCondGormDB(db *gorm.DB, queryCond utils.QueryConditions) *gorm.DB { + if len(queryCond.Query) > 0 { + db = db.Where(queryCond.Query) + } + if len(queryCond.Fields) > 0 { + db = db.Select(queryCond.Fields) + } + if len(queryCond.Order) > 0 { + for _, order := range queryCond.Order { + db = db.Order(order) + } + } + if queryCond.Offset >= 0 { + db = db.Offset(queryCond.Offset) + } + if queryCond.Limit > 0 { + db = db.Limit(queryCond.Limit) + } + return db +} diff --git a/web/domain/abstract_domain.go b/web/domain/abstract_domain.go new file mode 100644 index 0000000..a2dd353 --- /dev/null +++ b/web/domain/abstract_domain.go @@ -0,0 +1,74 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2020-2022. All rights reserved. + * Licensed 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 domain + +import ( + "github.com/huaweicloud/devcloud-go/web/dao" + "github.com/huaweicloud/devcloud-go/web/resp" + "github.com/huaweicloud/devcloud-go/web/utils" +) + +type AbstractDomain struct { + abstractDao *dao.AbstractDao + *utils.ModelInfo +} + +func NewAbstractDomain(abstractDao *dao.AbstractDao) *AbstractDomain { + return &AbstractDomain{ + abstractDao, + abstractDao.ModelInfo, + } +} + +func (d *AbstractDomain) Add(model interface{}) (interface{}, *resp.ErrorMsg) { + model, err := d.abstractDao.Add(model) + if err != nil { + return nil, resp.InternalServerErr2Json(err) + } + return model, nil +} + +func (d *AbstractDomain) GetOneByPK(pk string) (interface{}, *resp.ErrorMsg) { + model, err := d.abstractDao.GetOneByPrimaryKey(pk) + if err != nil { + return nil, resp.InternalServerErr2Json(err) + } + return model, nil +} + +func (d *AbstractDomain) Update(model interface{}, pk string) (interface{}, *resp.ErrorMsg) { + model, err := d.abstractDao.Update(model, pk) + if err != nil { + return nil, resp.InternalServerErr2Json(err) + } + return model, nil +} + +func (d *AbstractDomain) Delete(pk string) *resp.ErrorMsg { + err := d.abstractDao.DeleteByPrimaryKey(pk) + if err != nil { + return resp.InternalServerErr2Json(err) + } + return nil +} + +func (d *AbstractDomain) GetList(queryCond utils.QueryConditions) (interface{}, *resp.ErrorMsg) { + models, err := d.abstractDao.GetList(queryCond) + if err != nil { + return nil, resp.InternalServerErr2Json(err) + } + return models, nil +} diff --git a/web/resp/err_msg.go b/web/resp/err_msg.go new file mode 100644 index 0000000..bf7344e --- /dev/null +++ b/web/resp/err_msg.go @@ -0,0 +1,51 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2020-2022. All rights reserved. + * Licensed 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 resp + +import "net/http" + +type ErrorMsg struct { + Errno int + Reason string +} + +func Err2Json(code int, err error) *ErrorMsg { + return &ErrorMsg{ + Errno: code, + Reason: err.Error(), + } +} + +func InternalServerErr2Json(err error) *ErrorMsg { + return &ErrorMsg{ + Errno: http.StatusInternalServerError, + Reason: err.Error(), + } +} + +func BadRequestErr2Json(err error) *ErrorMsg { + return &ErrorMsg{ + Errno: http.StatusBadRequest, + Reason: err.Error(), + } +} + +func NotFoundErr2Json(err error) *ErrorMsg { + return &ErrorMsg{ + Errno: http.StatusNotFound, + Reason: err.Error(), + } +} diff --git a/web/resp/resp.go b/web/resp/resp.go new file mode 100644 index 0000000..598f1c2 --- /dev/null +++ b/web/resp/resp.go @@ -0,0 +1,51 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2020-2022. All rights reserved. + * Licensed 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 resp + +const ( + StatusSuccess = "success" + StatusFailure = "failure" +) + +type SuccessResponse struct { + Status string `json:"status"` + Data interface{} `json:"data,omitempty"` +} + +type FailureResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + +func Success() SuccessResponse { + return SuccessResponse{ + Status: StatusSuccess, + } +} + +func SuccessData(data interface{}) SuccessResponse { + return SuccessResponse{ + Status: StatusSuccess, + Data: data, + } +} + +func Failure(msg string) FailureResponse { + return FailureResponse{ + Status: StatusFailure, + Message: msg, + } +} diff --git a/web/utils/errors.go b/web/utils/errors.go new file mode 100644 index 0000000..5e0edbe --- /dev/null +++ b/web/utils/errors.go @@ -0,0 +1,20 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2020-2022. All rights reserved. + * Licensed 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 utils + +const ( + ErrDuplicateKey = "Error 1062" +) diff --git a/web/utils/model_info.go b/web/utils/model_info.go new file mode 100644 index 0000000..ab27f1b --- /dev/null +++ b/web/utils/model_info.go @@ -0,0 +1,64 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2020-2022. All rights reserved. + * Licensed 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 utils + +import ( + "errors" + "reflect" + "strings" +) + +type ModelInfo struct { + Model reflect.Type + Models reflect.Type + PKName string + PKJson string +} + +func NewModel(model, models interface{}, pkName, pkJson string) *ModelInfo { + modelInfo := &ModelInfo{ + Model: reflect.TypeOf(model), + Models: reflect.TypeOf(models), + PKName: pkName, + PKJson: pkJson, + } + if modelInfo.Model.Kind() == reflect.Ptr { + modelInfo.Model = modelInfo.Model.Elem() + } + if modelInfo.Models.Kind() == reflect.Ptr { + modelInfo.Models = modelInfo.Models.Elem() + } + return modelInfo +} + +func (m *ModelInfo) GetModel() reflect.Value { + return reflect.New(m.Model) +} + +func (m *ModelInfo) GetModels() reflect.Value { + return reflect.New(m.Models) +} + +func GetPKName(i interface{}) (pkName, pkJson string, err error) { + t := reflect.TypeOf(i).Elem() + for n := 0; n < t.NumField(); n++ { + tf := t.Field(n) + if strings.Index(tf.Tag.Get("gorm"), "primaryKey") >= 0 { + return tf.Name, tf.Tag.Get("json"), nil + } + } + return "", "", errors.New("the model format is abnormal") +} diff --git a/web/utils/parse_params.go b/web/utils/parse_params.go new file mode 100644 index 0000000..ee4d241 --- /dev/null +++ b/web/utils/parse_params.go @@ -0,0 +1,113 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2020-2022. All rights reserved. + * Licensed 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 utils + +import ( + "errors" + "strconv" + "strings" + + "github.com/gin-gonic/gin" +) + +type QueryConditions struct { + Query map[string]string + Fields []string + Order []string + Limit int + Offset int +} + +func ParseQueryCond(ctx *gin.Context) (QueryConditions, error) { + query, err := GetQuery(ctx) + if err != nil { + return QueryConditions{}, err + } + fields := GetFields(ctx) + order := GetOrder(ctx) + limit := GetLimit(ctx) + offset := GetOffset(ctx) + return QueryConditions{ + Query: query, + Fields: fields, + Order: order, + Limit: limit, + Offset: offset, + }, nil +} + +func GetQuery(ctx *gin.Context) (map[string]string, error) { + var query = make(map[string]string) + if v := ctx.Query("query"); v != "" { + for _, cond := range strings.Split(v, ",") { + kv := strings.SplitN(cond, ":", 2) + if len(kv) != 2 { + return query, errors.New("invalid query key/value pair") + } + k, v := kv[0], kv[1] + query[k] = v + } + } + return query, nil +} + +func GetFields(ctx *gin.Context) []string { + var fields []string + if v := ctx.Query("fields"); v != "" { + fields = strings.Split(v, ",") + } + return fields +} + +func GetOrder(ctx *gin.Context) []string { + var order []string + if v := ctx.Query("order"); v != "" { + order = strings.Split(v, ",") + } + return order +} + +func GetLimit(ctx *gin.Context) int { + limit := 10 + if v := ctx.Query("limit"); v != "" { + if vm, err := strconv.Atoi(v); err == nil { + limit = vm + } + } + return limit +} + +func GetOffset(ctx *gin.Context) int { + offset := 0 + if v := ctx.Query("offset"); v != "" { + if vm, err := strconv.Atoi(v); err == nil { + offset = vm + } + } + return offset +} + +func GetStringParam(ctx *gin.Context, param string) string { + return ctx.Param(param) +} + +func GetIntParam(ctx *gin.Context, param string) (int, error) { + return strconv.Atoi(ctx.Param(param)) +} + +func GetInt64Param(ctx *gin.Context, param string) (int64, error) { + return strconv.ParseInt(ctx.Param(param), 10, 64) +}