├── .gitignore ├── ChangeLog ├── LICENSE ├── Makefile ├── README.md ├── build.sh ├── nimo-full-check ├── checker │ ├── checker.go │ ├── document-checker.go │ └── sample.go ├── common │ └── common.go ├── configure │ └── conf.go ├── go.mod ├── go.sum ├── main │ └── main.go └── run │ └── run.go ├── nimo-shake ├── checkpoint │ ├── fileWriter.go │ ├── manager.go │ ├── mongoWriter.go │ ├── struct.go │ ├── writer.go │ └── writer_test.go ├── common │ ├── callback.go │ ├── common.go │ ├── dynamodb.go │ ├── error.go │ ├── fcv.go │ ├── http.go │ ├── math.go │ ├── metric.go │ ├── mix.go │ ├── mongodb_community.go │ ├── mongodb_mgo.go │ ├── operator.go │ ├── shard.go │ ├── unsafe.go │ └── utils_test.go ├── conf │ └── nimo-shake.conf ├── configure │ ├── check.go │ └── conf.go ├── filter │ ├── filter.go │ └── filter_test.go ├── full-sync │ ├── document-syncer.go │ ├── document-syncer_test.go │ ├── syncer.go │ └── table-syncer.go ├── go.mod ├── go.sum ├── incr-sync │ ├── fetcher.go │ ├── syncer.go │ └── syncer_test.go ├── main │ └── main.go ├── protocal │ ├── converter_test.go │ ├── mtype_converter.go │ ├── protocal.go │ ├── raw_converter.go │ ├── same_converter.go │ └── type_converter.go ├── qps │ └── qps.go ├── run │ └── run.go ├── unit_test_common │ └── include.go └── writer │ ├── dynamo_proxy.go │ ├── mongodb_community_driver.go │ ├── mongodb_mgo_driver.go │ ├── writer.go │ └── writer_test.go └── scripts ├── hypervisor.c ├── run_ut_test.py ├── start.sh └── stop.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .gopath 2 | .idea 3 | *.iml 4 | logs 5 | *.pprof 6 | *.output 7 | *.data 8 | *.sw[ap] 9 | *.yml 10 | *.pid 11 | *.tar.gz 12 | *.log 13 | tags 14 | bin/* 15 | conf/* 16 | !conf/nimo-shake.conf 17 | 18 | runtime.trace 19 | 20 | .DS_Store 21 | 22 | data 23 | nimo-shake-v* 24 | 25 | .cache/ 26 | diagnostic/ 27 | *.pid 28 | src/vendor/* 29 | !src/vendor/vendor.json 30 | test_checkpoint_db 31 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2021-12-07 2 | * VERSION: 1.0.13 3 | * IMPROVE: deprecated mongo.go.driver 4 | * IMPROVE: add convert._id, add a prefix to _id key name from DynamoDB 5 | * IMPROVE: add type mchange in convert.type, use function from aws-sdk-go to convert data, better performance 6 | * IMPROVE: adapt to LastEvaluatedStreamArn and LastEvaluatedShardId not nil 7 | * IMPROVE: add incr_sync_parallel to do parallel full&incr 8 | 9 | 2021-05-18 10 | * VERSION: 1.0.12 11 | * BUGFIX: parse int value empty. 12 | 13 | 2021-04-07 14 | * VERSION: 1.0.11 15 | * IMPROVE: data source support endpoint_url. 16 | 17 | 2021-03-02 18 | * VERSION: 1.0.10 19 | * IMPROVE: replace mgo driver with mongo.go.driver 20 | 21 | 2021-03-01 22 | * VERSION: 1.0.9 23 | * IMPROVE: add full.document.write.batch. 24 | * IMPROVE: enable MongoDB unordered bulk write. 25 | * IMPROVE: create range index instead of hash index when target mongodb 26 | type is replcaSet. 27 | * BUGFIX: create union index for gsi and lsi. 28 | 29 | 2021-02-25 30 | * VERSION: 1.0.8 31 | * IMPROVE: add pprof. 32 | * IMPROVE: support parallel scan. 33 | 34 | 2021-01-28 35 | * VERSION: 1.0.7 36 | * BUGFIX: some corner cases. 37 | 38 | 2021-01-25 39 | * VERSION: 1.0.6 40 | * IMPROVE: add metric for both full sync and incr sync. 41 | 42 | 2020-12-22 43 | * VERSION: 1.0.5 44 | * BUGFIX: shard iterator expired based on full sync timeout. 45 | 46 | 2020-12-18 47 | * VERSION: 1.0.4 48 | * BUGFIX: duplicate value fetched in document-syncer. 49 | * BUGFIX: primary key is not passed to writer. 50 | * IMPROVE: store N as decimal into mongodb. 51 | 52 | 2020-10-20 53 | * VERSION: 1.0.3 54 | * IMPROVE: support migrating dynamodb meta info. 55 | * IMPROVE: support migrating gsi and lsi. 56 | 57 | 2020-07-15 58 | * VERSION: 1.0.2 59 | * IMPROVE: support aliyun-dynamo-proxy 60 | * IMPROVE: add checkpoint address with type file 61 | 62 | 2019-10-15 Alibaba Cloud. 63 | * VERSION: 1.0.0 64 | * FEATURE: first release 65 | 66 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | ./build.sh 3 | 4 | clean: 5 | rm -rf bin 6 | rm -rf *.pprof 7 | rm -rf *.output 8 | rm -rf logs 9 | rm -rf diagnostic/ 10 | rm -rf *.pid 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NimoShake: sync from dynamodb to mongodb, including full sync and increase sync.
2 | NimoFullCheck: check data consistency after syncing.
3 | These two tools are developed and maintained by NoSQL Team in Alibaba-Cloud Database department.
4 | 5 | * [中文文档介绍](https://yq.aliyun.com/articles/717439) 6 | * [binary download](https://github.com/alibaba/NimoShake/releases) 7 | 8 | #Usage 9 | --- 10 | NimoShake: 11 | `./nimo-shake -conf=nimo-shake.conf`
12 | NimoFullCheck: 13 | `./nimo-full-check --sourceAccessKeyID="xxx" --sourceSecretAccessKey="xxx" --sourceRegion="us-east-2" --filterTablesWhiteList="table list" -t="target mongodb address"` 14 | 15 | # Shake series tool 16 | --- 17 | We also provide some tools for synchronization in Shake series.
18 | 19 | * [MongoShake](https://github.com/aliyun/MongoShake): mongodb data synchronization tool. 20 | * [RedisShake](https://github.com/aliyun/RedisShake): redis data synchronization tool. 21 | * [RedisFullCheck](https://github.com/aliyun/RedisFullCheck): redis data synchronization verification tool. 22 | * [NimoShake](https://github.com/alibaba/NimoShake): sync dynamodb to mongodb. 23 | 24 | 25 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | 5 | # older version Git don't support --short ! 6 | if [ -d ".git" ];then 7 | #branch=`git symbolic-ref --short -q HEAD` 8 | branch=$(git symbolic-ref -q HEAD | awk -F'/' '{print $3;}') 9 | cid=$(git rev-parse HEAD) 10 | else 11 | branch="unknown" 12 | cid="0.0" 13 | fi 14 | branch=$branch","$cid 15 | 16 | output=./bin/ 17 | rm -rf ${output} 18 | mkdir ${output} 19 | 20 | # make sure we're in the directory where the script lives 21 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 22 | cd "$SCRIPT_DIR" 23 | 24 | GOPATH=$(pwd) 25 | export GOPATH 26 | 27 | # golang version 28 | goversion=$(go version | awk -F' ' '{print $3;}') 29 | bigVersion=$(echo $goversion | awk -F'[o.]' '{print $2}') 30 | midVersion=$(echo $goversion | awk -F'[o.]' '{print $3}') 31 | if [ $bigVersion -lt "1" -o $bigVersion -eq "1" -a $midVersion -lt "6" ]; then 32 | echo "go version[$goversion] must >= 1.6" 33 | exit 1 34 | fi 35 | 36 | t=$(date "+%Y-%m-%d_%H:%M:%S") 37 | 38 | echo "[ BUILD RELEASE ]" 39 | run_builder='go build -v' 40 | 41 | modules=(nimo-shake nimo-full-check) 42 | #modules=(nimo-shake) 43 | 44 | goos=(linux darwin) 45 | for g in "${goos[@]}"; do 46 | export GOOS=$g 47 | export GOARCH=amd64 48 | echo "try build goos=$g" 49 | for i in "${modules[@]}" ; do 50 | echo "build "$i 51 | cd $i 52 | info="$i/common.Version=$branch" 53 | info=$info","$goversion 54 | info=$info","$t 55 | $run_builder -ldflags "-X $info" -o "${output}/$i.$g" "./main/main.go" 56 | echo "build $i successfully!" 57 | cd .. 58 | cp "$i/${output}/$i.$g" ${output}/ 59 | done 60 | echo "build goos=$g: all modules successfully!" 61 | done 62 | 63 | # copy scripts 64 | cp scripts/start.sh ${output}/ 65 | cp scripts/stop.sh ${output}/ 66 | -------------------------------------------------------------------------------- /nimo-full-check/checker/checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | conf "nimo-full-check/configure" 9 | shakeUtils "nimo-shake/common" 10 | shakeFilter "nimo-shake/filter" 11 | 12 | "github.com/aws/aws-sdk-go/service/dynamodb" 13 | LOG "github.com/vinllen/log4go" 14 | "go.mongodb.org/mongo-driver/bson" 15 | ) 16 | 17 | type Checker struct { 18 | dynamoSession *dynamodb.DynamoDB 19 | mongoClient *shakeUtils.MongoCommunityConn 20 | } 21 | 22 | func NewChecker(dynamoSession *dynamodb.DynamoDB, mongoClient *shakeUtils.MongoCommunityConn) *Checker { 23 | return &Checker{ 24 | dynamoSession: dynamoSession, 25 | mongoClient: mongoClient, 26 | } 27 | } 28 | 29 | func (c *Checker) Run() error { 30 | // fetch all tables 31 | LOG.Info("start fetching table list") 32 | rawTableList, err := shakeUtils.FetchTableList(c.dynamoSession) 33 | if err != nil { 34 | return fmt.Errorf("fetch table list failed[%v]", err) 35 | } 36 | LOG.Info("finish fetching table list: %v", rawTableList) 37 | 38 | tableList := shakeFilter.FilterList(rawTableList) 39 | LOG.Info("filter table list: %v", tableList) 40 | 41 | // check table exist 42 | if err := c.checkTableExist(tableList); err != nil { 43 | return fmt.Errorf("check table exist failed[%v]", err) 44 | } 45 | 46 | // reset parallel if needed 47 | parallel := conf.Opts.Parallel 48 | if parallel > len(tableList) { 49 | parallel = len(tableList) 50 | } 51 | 52 | execChan := make(chan string, len(tableList)) 53 | for _, table := range tableList { 54 | execChan <- table 55 | } 56 | 57 | var wg sync.WaitGroup 58 | wg.Add(len(tableList)) 59 | for i := 0; i < parallel; i++ { 60 | go func(id int) { 61 | for { 62 | table, ok := <-execChan 63 | if !ok { 64 | break 65 | } 66 | 67 | LOG.Info("documentChecker[%v] starts checking table[%v]", id, table) 68 | dc := NewDocumentChecker(id, table, c.dynamoSession) 69 | dc.Run() 70 | 71 | LOG.Info("documentChecker[%v] finishes checking table[%v]", id, table) 72 | wg.Done() 73 | } 74 | }(i) 75 | } 76 | wg.Wait() 77 | 78 | LOG.Info("all documentCheckers finish") 79 | return nil 80 | } 81 | 82 | func (c *Checker) checkTableExist(tableList []string) error { 83 | collections, err := c.mongoClient.Client.Database(conf.Opts.Id).ListCollectionNames(context.TODO(), bson.M{}) 84 | if err != nil { 85 | return fmt.Errorf("get target collection names error[%v]", err) 86 | } 87 | 88 | LOG.Info("all table: %v", collections) 89 | 90 | collectionsMp := shakeUtils.StringListToMap(collections) 91 | notExist := make([]string, 0) 92 | for _, table := range tableList { 93 | if _, ok := collectionsMp[table]; !ok { 94 | notExist = append(notExist, table) 95 | } 96 | } 97 | 98 | if len(notExist) != 0 { 99 | return fmt.Errorf("table not exist on the target side: %v", notExist) 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /nimo-full-check/checker/document-checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | conf "nimo-full-check/configure" 8 | "os" 9 | "reflect" 10 | 11 | shakeUtils "nimo-shake/common" 12 | "nimo-shake/protocal" 13 | shakeQps "nimo-shake/qps" 14 | 15 | "github.com/aws/aws-sdk-go/aws" 16 | "github.com/aws/aws-sdk-go/service/dynamodb" 17 | "github.com/google/go-cmp/cmp" 18 | LOG "github.com/vinllen/log4go" 19 | "go.mongodb.org/mongo-driver/bson" 20 | "go.mongodb.org/mongo-driver/bson/primitive" 21 | ) 22 | 23 | func convertToMap(data interface{}) interface{} { 24 | switch v := data.(type) { 25 | case primitive.D: 26 | result := make(map[string]interface{}) 27 | for _, elem := range v { 28 | result[elem.Key] = convertToMap(elem.Value) 29 | } 30 | return result 31 | 32 | case primitive.ObjectID: 33 | return v.Hex() 34 | 35 | case []interface{}: 36 | var newSlice []interface{} 37 | for _, item := range v { 38 | newSlice = append(newSlice, convertToMap(item)) 39 | } 40 | return newSlice 41 | 42 | default: 43 | return v 44 | } 45 | } 46 | 47 | func interIsEqual(dynamoData, convertedMongo interface{}) bool { 48 | 49 | //convertedMongo := convertToMap(mongoData) 50 | if m, ok := convertedMongo.(map[string]interface{}); ok { 51 | delete(m, "_id") 52 | } else { 53 | LOG.Warn("don't have _id in mongodb document") 54 | return false 55 | } 56 | 57 | opts := cmp.Options{ 58 | cmp.FilterPath(func(p cmp.Path) bool { 59 | if len(p) > 0 { 60 | switch p.Last().(type) { 61 | case cmp.MapIndex, cmp.SliceIndex: 62 | return true 63 | } 64 | } 65 | return false 66 | }, cmp.Transformer("NormalizeNumbers", func(in interface{}) interface{} { 67 | switch v := in.(type) { 68 | case int, int32, int64, float32, float64: 69 | return reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0))).Float() 70 | default: 71 | return in 72 | } 73 | })), 74 | } 75 | // LOG.Warn("tmp2 %v", cmp.Diff(dynamoData, convertedMongo, opts)) 76 | 77 | return cmp.Equal(dynamoData, convertedMongo, opts) 78 | } 79 | 80 | const ( 81 | fetcherChanSize = 512 82 | parserChanSize = 4096 83 | ) 84 | 85 | type KeyUnion struct { 86 | name string 87 | tp string 88 | union string 89 | } 90 | 91 | type DocumentChecker struct { 92 | id int 93 | ns shakeUtils.NS 94 | sourceConn *dynamodb.DynamoDB 95 | mongoClient *shakeUtils.MongoCommunityConn 96 | fetcherChan chan *dynamodb.ScanOutput // chan between fetcher and parser 97 | parserChan chan protocal.RawData // chan between parser and writer 98 | converter protocal.Converter // converter 99 | sampler *Sample // use to sample 100 | primaryKeyWithType KeyUnion 101 | sortKeyWithType KeyUnion 102 | } 103 | 104 | func NewDocumentChecker(id int, table string, dynamoSession *dynamodb.DynamoDB) *DocumentChecker { 105 | // check mongodb connection 106 | mongoClient, err := shakeUtils.NewMongoCommunityConn(conf.Opts.TargetAddress, shakeUtils.ConnectModePrimary, true) 107 | if err != nil { 108 | LOG.Crashf("documentChecker[%v] with table[%v] connect mongodb[%v] failed[%v]", id, table, 109 | conf.Opts.TargetAddress, err) 110 | } 111 | 112 | converter := protocal.NewConverter(conf.Opts.ConvertType) 113 | if converter == nil { 114 | LOG.Error("documentChecker[%v] with table[%v] create converter failed", id, table) 115 | return nil 116 | } 117 | 118 | return &DocumentChecker{ 119 | id: id, 120 | sourceConn: dynamoSession, 121 | mongoClient: mongoClient, 122 | converter: converter, 123 | ns: shakeUtils.NS{ 124 | Collection: table, 125 | Database: conf.Opts.Id, 126 | }, 127 | } 128 | } 129 | 130 | func (dc *DocumentChecker) String() string { 131 | return fmt.Sprintf("documentChecker[%v] with table[%s]", dc.id, dc.ns) 132 | } 133 | 134 | func (dc *DocumentChecker) Run() { 135 | // check outline 136 | if err := dc.checkOutline(); err != nil { 137 | LOG.Crashf("%s check outline failed[%v]", dc.String(), err) 138 | } 139 | 140 | LOG.Info("%s check outline finish, starts checking details", dc.String()) 141 | 142 | dc.fetcherChan = make(chan *dynamodb.ScanOutput, fetcherChanSize) 143 | dc.parserChan = make(chan protocal.RawData, parserChanSize) 144 | 145 | // start fetcher to fetch all data from DynamoDB 146 | go dc.fetcher() 147 | 148 | // start parser to get data from fetcher and write into exector. 149 | go dc.parser() 150 | 151 | // start executor to check 152 | dc.executor() 153 | } 154 | 155 | func (dc *DocumentChecker) fetcher() { 156 | LOG.Info("%s start fetcher", dc.String()) 157 | 158 | qos := shakeQps.StartQoS(int(conf.Opts.QpsFull)) 159 | defer qos.Close() 160 | 161 | // init nil 162 | var previousKey map[string]*dynamodb.AttributeValue 163 | for { 164 | <-qos.Bucket 165 | 166 | out, err := dc.sourceConn.Scan(&dynamodb.ScanInput{ 167 | TableName: aws.String(dc.ns.Collection), 168 | ExclusiveStartKey: previousKey, 169 | Limit: aws.Int64(conf.Opts.QpsFullBatchNum), 170 | }) 171 | if err != nil { 172 | // TODO check network error and retry 173 | LOG.Crashf("%s fetcher scan failed[%v]", dc.String(), err) 174 | } 175 | 176 | // pass result to parser 177 | dc.fetcherChan <- out 178 | 179 | previousKey = out.LastEvaluatedKey 180 | if previousKey == nil { 181 | // complete 182 | break 183 | } 184 | } 185 | 186 | LOG.Info("%s close fetcher", dc.String()) 187 | close(dc.fetcherChan) 188 | } 189 | 190 | func (dc *DocumentChecker) parser() { 191 | LOG.Info("%s start parser", dc.String()) 192 | 193 | for { 194 | data, ok := <-dc.fetcherChan 195 | if !ok { 196 | break 197 | } 198 | 199 | LOG.Debug("%s reads data[%v]", dc.String(), data) 200 | 201 | list := data.Items 202 | for _, ele := range list { 203 | out, err := dc.converter.Run(ele) 204 | if err != nil { 205 | LOG.Crashf("%s parses ele[%v] failed[%v]", dc.String(), ele, err) 206 | } 207 | 208 | // sample 209 | if dc.sampler.Hit() == false { 210 | continue 211 | } 212 | 213 | dc.parserChan <- out.(protocal.RawData) 214 | } 215 | } 216 | 217 | LOG.Info("%s close parser", dc.String()) 218 | close(dc.parserChan) 219 | } 220 | 221 | func (dc *DocumentChecker) executor() { 222 | LOG.Info("%s start executor", dc.String()) 223 | 224 | diffFile := fmt.Sprintf("%s/%s", conf.Opts.DiffOutputFile, dc.ns.Collection) 225 | f, err := os.Create(diffFile) 226 | if err != nil { 227 | LOG.Crashf("%s create diff output file[%v] failed", dc.String(), diffFile) 228 | return 229 | } 230 | 231 | for { 232 | data, ok := <-dc.parserChan 233 | if !ok { 234 | break 235 | } 236 | 237 | //var query map[string]interface{} 238 | query := make(map[string]interface{}) 239 | if dc.primaryKeyWithType.name != "" { 240 | // find by union key 241 | if conf.Opts.ConvertType == shakeUtils.ConvertMTypeChange { 242 | query[dc.primaryKeyWithType.name] = data.Data.(map[string]interface{})[dc.primaryKeyWithType.name] 243 | } else { 244 | LOG.Crashf("unknown convert type[%v]", conf.Opts.ConvertType) 245 | } 246 | } 247 | if dc.sortKeyWithType.name != "" { 248 | if conf.Opts.ConvertType == shakeUtils.ConvertMTypeChange { 249 | query[dc.sortKeyWithType.name] = data.Data.(map[string]interface{})[dc.sortKeyWithType.name] 250 | } else { 251 | LOG.Crashf("unknown convert type[%v]", conf.Opts.ConvertType) 252 | } 253 | } 254 | 255 | LOG.Info("query: %v", query) 256 | 257 | // query 258 | var output, outputMap interface{} 259 | isSame := true 260 | err := dc.mongoClient.Client.Database(dc.ns.Database).Collection(dc.ns.Collection). 261 | FindOne(context.TODO(), query).Decode(&output) 262 | if err != nil { 263 | err = fmt.Errorf("target query failed[%v][%v][%v]", err, output, query) 264 | LOG.Error("%s %v", dc.String(), err) 265 | } else { 266 | outputMap = convertToMap(output) 267 | isSame = interIsEqual(data.Data, outputMap) 268 | } 269 | 270 | inputJson, _ := json.Marshal(data.Data) 271 | outputJson, _ := json.Marshal(outputMap) 272 | if err != nil { 273 | f.WriteString(fmt.Sprintf("compare src[%s] to dst[%s] failed: %v\n", inputJson, outputJson, err)) 274 | } else if isSame == false { 275 | LOG.Warn("compare src[%s] and dst[%s] failed", inputJson, outputJson) 276 | f.WriteString(fmt.Sprintf("src[%s] != dst[%s]\n", inputJson, outputJson)) 277 | } 278 | } 279 | 280 | LOG.Info("%s close executor", dc.String()) 281 | f.Close() 282 | 283 | // remove file if size == 0 284 | if fi, err := os.Stat(diffFile); err != nil { 285 | LOG.Warn("stat diffFile[%v] failed[%v]", diffFile, err) 286 | return 287 | } else if fi.Size() == 0 { 288 | if err := os.Remove(diffFile); err != nil { 289 | LOG.Warn("remove diffFile[%v] failed[%v]", diffFile, err) 290 | } 291 | } 292 | } 293 | 294 | func (dc *DocumentChecker) checkOutline() error { 295 | // describe dynamodb table 296 | out, err := dc.sourceConn.DescribeTable(&dynamodb.DescribeTableInput{ 297 | TableName: aws.String(dc.ns.Collection), 298 | }) 299 | if err != nil { 300 | return fmt.Errorf("describe table failed[%v]", err) 301 | } 302 | 303 | LOG.Info("describe table[%v] result: %v", dc.ns.Collection, out) 304 | 305 | // 1. check total number 306 | // dynamo count 307 | dynamoCount := out.Table.ItemCount 308 | 309 | // mongo count 310 | cnt, err := dc.mongoClient.Client.Database(dc.ns.Database).Collection(dc.ns.Collection).CountDocuments(context.Background(), bson.M{}) 311 | if err != nil { 312 | return fmt.Errorf("get mongo count failed[%v]", err) 313 | } 314 | 315 | if *dynamoCount != cnt { 316 | // return fmt.Errorf("dynamo count[%v] != mongo count[%v]", *dynamoCount, cnt) 317 | LOG.Warn("dynamo count[%v] != mongo count[%v]", *dynamoCount, cnt) 318 | } 319 | 320 | // set sampler 321 | dc.sampler = NewSample(conf.Opts.Sample, cnt) 322 | 323 | // 2. check index 324 | // TODO 325 | 326 | // parse index 327 | // parse primary key with sort key 328 | allIndexes := out.Table.AttributeDefinitions 329 | primaryIndexes := out.Table.KeySchema 330 | 331 | // parse index type 332 | parseMap := shakeUtils.ParseIndexType(allIndexes) 333 | primaryKey, sortKey, err := shakeUtils.ParsePrimaryAndSortKey(primaryIndexes, parseMap) 334 | if err != nil { 335 | return fmt.Errorf("parse primary and sort key failed[%v]", err) 336 | } 337 | dc.primaryKeyWithType = KeyUnion{ 338 | name: primaryKey, 339 | tp: parseMap[primaryKey], 340 | union: fmt.Sprintf("%s.%s", primaryKey, parseMap[primaryKey]), 341 | } 342 | dc.sortKeyWithType = KeyUnion{ 343 | name: sortKey, 344 | tp: parseMap[sortKey], 345 | union: fmt.Sprintf("%s.%s", sortKey, parseMap[sortKey]), 346 | } 347 | 348 | return nil 349 | } 350 | -------------------------------------------------------------------------------- /nimo-full-check/checker/sample.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import "math/rand" 4 | 5 | const ( 6 | SEED = 1 7 | ) 8 | 9 | type Sample struct { 10 | sampleCnt int64 11 | totalCnt int64 12 | source *rand.Rand 13 | } 14 | 15 | func NewSample(sampleCnt, totalCnt int64) *Sample { 16 | return &Sample{ 17 | sampleCnt: sampleCnt, 18 | totalCnt: totalCnt, 19 | source: rand.New(rand.NewSource(SEED)), 20 | } 21 | } 22 | 23 | func (s *Sample) Hit() bool { 24 | if s.sampleCnt >= s.totalCnt { 25 | return true 26 | } 27 | if s.sampleCnt == 0 { 28 | return false 29 | } 30 | 31 | return s.source.Int63n(s.totalCnt) < s.sampleCnt 32 | } 33 | -------------------------------------------------------------------------------- /nimo-full-check/common/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | var ( 4 | Version = "$" 5 | ) -------------------------------------------------------------------------------- /nimo-full-check/configure/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | var Opts struct { 4 | Id string `short:"i" long:"id" default:"nimo-shake" description:"target database collection name"` 5 | LogLevel string `short:"l" long:"logLevel" default:"info"` 6 | SourceAccessKeyID string `short:"s" long:"sourceAccessKeyID" description:"dynamodb source access key id"` 7 | SourceSecretAccessKey string `long:"sourceSecretAccessKey" description:"dynamodb source secret access key"` 8 | SourceSessionToken string `long:"sourceSessionToken" default:"" description:"dynamodb source session token"` 9 | SourceRegion string `long:"sourceRegion" default:"" description:"dynamodb source region"` 10 | SourceEndpointUrl string `long:"sourceEndpointUrl" default:"" description:"dynamodb source endpoint_url"` 11 | QpsFull int `long:"qpsFull" default:"10000" description:"qps of scan command, default is 10000"` 12 | QpsFullBatchNum int64 `long:"qpsFullBatchNum" default:"128" description:"batch number in each scan command, default is 128"` 13 | TargetAddress string `short:"t" long:"targetAddress" description:"mongodb target address"` 14 | DiffOutputFile string `short:"d" long:"diffOutputFile" default:"nimo-full-check-diff" description:"diff output file name"` 15 | Parallel int `short:"p" long:"parallel" default:"16" description:"how many threads used to compare, default is 16"` 16 | Sample int64 `short:"e" long:"sample" default:"1000" description:"comparison sample number for each table, 0 means disable"` 17 | //IndexPrimary bool `short:"m" long:"indexPrimary" description:"enable compare primary index"` 18 | //IndexUser bool `long:"indexUser" description:"enable compare user index"` 19 | FilterCollectionWhite string `long:"filterCollectionWhite" default:"" description:"only compare the given tables, split by ';'"` 20 | FilterCollectionBlack string `long:"filterCollectionBlack" default:"" description:"do not compare the given tables, split by ';'"` 21 | ConvertType string `short:"c" long:"convertType" default:"change" description:"convert type"` 22 | Version bool `short:"v" long:"version" description:"print version"` 23 | } 24 | -------------------------------------------------------------------------------- /nimo-full-check/go.mod: -------------------------------------------------------------------------------- 1 | module nimo-full-check 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.44.61 7 | github.com/google/go-cmp v0.6.0 8 | github.com/jessevdk/go-flags v1.5.0 // indirect 9 | github.com/vinllen/log4go v0.0.0-20180514124125-3848a366df9d 10 | go.mongodb.org/mongo-driver v1.16.1 11 | nimo-shake v0.0.0-00010101000000-000000000000 12 | ) 13 | 14 | replace nimo-shake => ../nimo-shake 15 | -------------------------------------------------------------------------------- /nimo-full-check/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | utils "nimo-full-check/common" 9 | conf "nimo-full-check/configure" 10 | "nimo-full-check/run" 11 | shakeUtils "nimo-shake/common" 12 | shakeFilter "nimo-shake/filter" 13 | 14 | "github.com/jessevdk/go-flags" 15 | LOG "github.com/vinllen/log4go" 16 | ) 17 | 18 | type Exit struct{ Code int } 19 | 20 | func main() { 21 | defer LOG.Close() 22 | 23 | // parse conf.Opts 24 | args, err := flags.Parse(&conf.Opts) 25 | 26 | if conf.Opts.Version { 27 | fmt.Println(utils.Version) 28 | os.Exit(0) 29 | } 30 | 31 | // 若err != nil, 会自动打印错误到 stderr 32 | if err != nil { 33 | if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { 34 | os.Exit(0) 35 | } else { 36 | fmt.Fprintf(os.Stderr, "flag err %s\n", flagsErr) 37 | os.Exit(1) 38 | } 39 | } 40 | 41 | if len(args) != 0 { 42 | fmt.Fprintf(os.Stderr, "unexpected args %+v", args) 43 | os.Exit(1) 44 | } 45 | 46 | shakeUtils.InitialLogger("", conf.Opts.LogLevel, false) 47 | 48 | if err := sanitizeOptions(); err != nil { 49 | crash(fmt.Sprintf("Conf.Options check failed: %s", err.Error()), -4) 50 | } 51 | 52 | LOG.Info("full-check starts") 53 | run.Start() 54 | LOG.Info("full-check completes!") 55 | } 56 | 57 | func sanitizeOptions() error { 58 | if len(conf.Opts.SourceAccessKeyID) == 0 || len(conf.Opts.SourceSecretAccessKey) == 0 { 59 | return fmt.Errorf("sourceAccessKeyID and sourceSecretAccessKey can't be empty") 60 | } 61 | 62 | if len(conf.Opts.TargetAddress) == 0 { 63 | return fmt.Errorf("targetAddress can't be empty") 64 | } 65 | 66 | if conf.Opts.Parallel <= 0 { 67 | return fmt.Errorf("parallel should >= 1, default is 16") 68 | } 69 | 70 | if conf.Opts.QpsFull == 0 { 71 | conf.Opts.QpsFull = 10000 72 | } else if conf.Opts.QpsFull < 0 { 73 | return fmt.Errorf("qps.full should > 0, default is 10000") 74 | } 75 | 76 | if conf.Opts.QpsFullBatchNum <= 0 { 77 | conf.Opts.QpsFullBatchNum = 128 78 | } 79 | 80 | shakeFilter.Init(conf.Opts.FilterCollectionWhite, conf.Opts.FilterCollectionBlack) 81 | 82 | if len(conf.Opts.DiffOutputFile) == 0 { 83 | return fmt.Errorf("diff output file shouldn't be empty") 84 | } else { 85 | _, err := os.Stat(conf.Opts.DiffOutputFile) 86 | if os.IsNotExist(err) { 87 | if err = os.Mkdir(conf.Opts.DiffOutputFile, os.ModePerm); err != nil { 88 | return fmt.Errorf("mkdir diffOutputFile[%v] failed[%v]", conf.Opts.DiffOutputFile, err) 89 | } 90 | } else { 91 | newName := fmt.Sprintf("%v-%v", conf.Opts.DiffOutputFile, time.Now().Format(shakeUtils.GolangSecurityTime)) 92 | if err := os.Rename(conf.Opts.DiffOutputFile, newName); err != nil { 93 | return fmt.Errorf("diffOutputFile dir[%v] rename to newFile[%v] failed[%v], need to be remvoed manullay", 94 | conf.Opts.DiffOutputFile, newName, err) 95 | } 96 | if err = os.Mkdir(conf.Opts.DiffOutputFile, os.ModePerm); err != nil { 97 | return fmt.Errorf("mkdir diffOutputFile[%v] again failed[%v]", conf.Opts.DiffOutputFile, err) 98 | } 99 | } 100 | } 101 | 102 | if conf.Opts.ConvertType == "" { 103 | conf.Opts.ConvertType = shakeUtils.ConvertMTypeChange 104 | } else if conf.Opts.ConvertType != shakeUtils.ConvertMTypeChange { 105 | return fmt.Errorf("convertType[%v] illegal, only support %v", 106 | conf.Opts.ConvertType, shakeUtils.ConvertMTypeChange) 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func crash(msg string, errCode int) { 113 | fmt.Println(msg) 114 | panic(Exit{errCode}) 115 | } 116 | -------------------------------------------------------------------------------- /nimo-full-check/run/run.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "nimo-full-check/checker" 5 | conf "nimo-full-check/configure" 6 | shakeUtils "nimo-shake/common" 7 | 8 | LOG "github.com/vinllen/log4go" 9 | ) 10 | 11 | func Start() { 12 | if err := shakeUtils.InitSession(conf.Opts.SourceAccessKeyID, conf.Opts.SourceSecretAccessKey, 13 | conf.Opts.SourceSessionToken, conf.Opts.SourceRegion, conf.Opts.SourceEndpointUrl, 14 | 3, 5000); err != nil { 15 | LOG.Crashf("init global session failed[%v]", err) 16 | } 17 | 18 | // create dynamo session 19 | dynamoSession, err := shakeUtils.CreateDynamoSession("info") 20 | if err != nil { 21 | LOG.Crashf("create dynamodb session failed[%v]", err) 22 | } 23 | 24 | // check mongodb connection 25 | mongoClient, err := shakeUtils.NewMongoCommunityConn(conf.Opts.TargetAddress, shakeUtils.ConnectModePrimary, true) 26 | if err != nil { 27 | LOG.Crashf("connect mongodb[%v] failed[%v]", conf.Opts.TargetAddress, err) 28 | } 29 | 30 | c := checker.NewChecker(dynamoSession, mongoClient) 31 | if c == nil { 32 | LOG.Crashf("create checker failed") 33 | } 34 | 35 | if err := c.Run(); err != nil { 36 | LOG.Crashf("checker runs failed[%v]", err) 37 | } 38 | 39 | LOG.Info("checker finishes!") 40 | } 41 | -------------------------------------------------------------------------------- /nimo-shake/checkpoint/fileWriter.go: -------------------------------------------------------------------------------- 1 | package checkpoint 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "bytes" 8 | "encoding/json" 9 | "fmt" 10 | LOG "github.com/vinllen/log4go" 11 | "io/ioutil" 12 | "path/filepath" 13 | "reflect" 14 | "strings" 15 | ) 16 | 17 | // marshal in json 18 | type FileWriter struct { 19 | dir string 20 | fileHandler *sync.Map // file name -> fd 21 | fileLock *sync.Map // file name -> lock 22 | } 23 | 24 | func NewFileWriter(dir string) *FileWriter { 25 | // create dir if not exist 26 | if _, err := os.Stat(dir); err != nil { 27 | if os.IsNotExist(err) { 28 | // create dir 29 | if err = os.Mkdir(dir, 0755); err != nil { 30 | LOG.Crashf("create dir[%v] failed[%v]", dir, err) 31 | return nil 32 | } 33 | } else { 34 | LOG.Crashf("stat dir[%v] failed[%v]", dir, err) 35 | return nil 36 | } 37 | } 38 | 39 | return &FileWriter{ 40 | dir: dir, 41 | fileHandler: new(sync.Map), 42 | fileLock: new(sync.Map), 43 | } 44 | } 45 | 46 | // find current status 47 | func (fw *FileWriter) FindStatus() (string, error) { 48 | // lock file 49 | fw.lockFile(CheckpointStatusTable) 50 | defer fw.unlockFile(CheckpointStatusTable) 51 | 52 | file := fmt.Sprintf("%s/%s", fw.dir, CheckpointStatusTable) 53 | if _, err := os.Stat(file); err != nil { 54 | if os.IsNotExist(err) { 55 | return CheckpointStatusValueEmpty, nil 56 | } 57 | } 58 | 59 | jsonFile, err := os.Open(file) 60 | if err != nil { 61 | return "", err 62 | } 63 | defer jsonFile.Close() 64 | 65 | byteValue, err := ioutil.ReadAll(jsonFile) 66 | if err != nil { 67 | return "", err 68 | } 69 | 70 | var ret Status 71 | if err := json.Unmarshal(byteValue, &ret); err != nil { 72 | return "", err 73 | } 74 | 75 | return ret.Value, nil 76 | } 77 | 78 | // update status 79 | func (fw *FileWriter) UpdateStatus(status string) error { 80 | // lock file 81 | fw.lockFile(CheckpointStatusTable) 82 | defer fw.unlockFile(CheckpointStatusTable) 83 | 84 | file := fmt.Sprintf("%s/%s", fw.dir, CheckpointStatusTable) 85 | input := &Status{ 86 | Key: CheckpointStatusKey, 87 | Value: status, 88 | } 89 | 90 | val, err := json.Marshal(input) 91 | if err != nil { 92 | return nil 93 | } 94 | 95 | f, err := os.Create(file) 96 | if err != nil { 97 | return err 98 | } 99 | defer f.Close() 100 | 101 | _, err = f.Write(val) 102 | return err 103 | } 104 | 105 | // extract all checkpoint 106 | func (fw *FileWriter) ExtractCheckpoint() (map[string]map[string]*Checkpoint, error) { 107 | ckptMap := make(map[string]map[string]*Checkpoint) 108 | // fileList isn't include directory 109 | var fileList []string 110 | err := filepath.Walk(fw.dir, func(path string, info os.FileInfo, err error) error { 111 | if path != fw.dir { 112 | pathList := strings.Split(path, "/") 113 | fileList = append(fileList, pathList[len(pathList)-1]) 114 | } 115 | return nil 116 | }) 117 | 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | for _, file := range fileList { 123 | if FilterCkptCollection(file) { 124 | continue 125 | } 126 | 127 | innerMap, err := fw.ExtractSingleCheckpoint(file) 128 | if err != nil { 129 | return nil, err 130 | } 131 | ckptMap[file] = innerMap 132 | } 133 | 134 | return ckptMap, nil 135 | } 136 | 137 | // extract single checkpoint 138 | func (fw *FileWriter) ExtractSingleCheckpoint(table string) (map[string]*Checkpoint, error) { 139 | fw.lockFile(table) 140 | defer fw.unlockFile(table) 141 | 142 | file := fmt.Sprintf("%s/%s", fw.dir, table) 143 | jsonFile, err := os.Open(file) 144 | if err != nil { 145 | return nil, err 146 | } 147 | defer jsonFile.Close() 148 | 149 | data, err := fw.readJsonList(jsonFile) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | innerMap := make(map[string]*Checkpoint) 155 | for _, ele := range data { 156 | innerMap[ele.ShardId] = ele 157 | } 158 | 159 | return innerMap, nil 160 | } 161 | 162 | // insert checkpoint 163 | func (fw *FileWriter) Insert(ckpt *Checkpoint, table string) error { 164 | fw.lockFile(table) 165 | defer fw.unlockFile(table) 166 | 167 | file := fmt.Sprintf("%s/%s", fw.dir, table) 168 | jsonFile, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 169 | if err != nil { 170 | return err 171 | } 172 | defer jsonFile.Close() 173 | 174 | LOG.Debug("file[%s] insert data: %v", file, *ckpt) 175 | 176 | return fw.writeJsonList(jsonFile, []*Checkpoint{ckpt}) 177 | } 178 | 179 | // update checkpoint 180 | func (fw *FileWriter) Update(shardId string, ckpt *Checkpoint, table string) error { 181 | fw.lockFile(table) 182 | defer fw.unlockFile(table) 183 | 184 | file := fmt.Sprintf("%s/%s", fw.dir, table) 185 | jsonFile, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0666) 186 | if err != nil { 187 | return err 188 | } 189 | defer jsonFile.Close() 190 | 191 | data, err := fw.readJsonList(jsonFile) 192 | if err != nil { 193 | return err 194 | } 195 | 196 | if len(data) == 0 { 197 | return fmt.Errorf("empty data") 198 | } 199 | 200 | match := false 201 | for i := range data { 202 | if data[i].ShardId == shardId { 203 | match = true 204 | data[i] = ckpt 205 | break 206 | } 207 | } 208 | if !match { 209 | return fmt.Errorf("shardId[%v] not exists", shardId) 210 | } 211 | 212 | // truncate file 213 | jsonFile.Truncate(0) 214 | jsonFile.Seek(0, 0) 215 | 216 | // write 217 | return fw.writeJsonList(jsonFile, data) 218 | } 219 | 220 | // update with set 221 | func (fw *FileWriter) UpdateWithSet(shardId string, input map[string]interface{}, table string) error { 222 | fw.lockFile(table) 223 | defer fw.unlockFile(table) 224 | 225 | file := fmt.Sprintf("%s/%s", fw.dir, table) 226 | jsonFile, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0666) 227 | if err != nil { 228 | return err 229 | } 230 | defer jsonFile.Close() 231 | 232 | data, err := fw.readJsonList(jsonFile) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | if len(data) == 0 { 238 | return fmt.Errorf("empty data") 239 | } 240 | 241 | match := false 242 | for i := range data { 243 | if data[i].ShardId == shardId { 244 | match = true 245 | // set partial 246 | for key, val := range input { 247 | field := reflect.ValueOf(data[i]).Elem().FieldByName(key) 248 | switch field.Kind() { 249 | case reflect.String: 250 | v, _ := val.(string) 251 | field.SetString(v) 252 | case reflect.Invalid: 253 | printData, _ := json.Marshal(data[i]) 254 | return fmt.Errorf("invalid field[%v], current checkpoint[%s], input checkpoint[%v]", 255 | key, printData, input) 256 | default: 257 | printData, _ := json.Marshal(data[i]) 258 | return fmt.Errorf("unknown type[%v] of field[%v], current checkpoint[%s], input checkpoint[%v]", 259 | field.Kind(), key, printData, input) 260 | } 261 | } 262 | 263 | break 264 | } 265 | } 266 | if !match { 267 | return fmt.Errorf("shardId[%v] not exists", shardId) 268 | } 269 | 270 | // truncate file 271 | jsonFile.Truncate(0) 272 | jsonFile.Seek(0, 0) 273 | 274 | // write 275 | return fw.writeJsonList(jsonFile, data) 276 | } 277 | 278 | // query 279 | func (fw *FileWriter) Query(shardId string, table string) (*Checkpoint, error) { 280 | fw.lockFile(table) 281 | defer fw.unlockFile(table) 282 | 283 | file := fmt.Sprintf("%s/%s", fw.dir, table) 284 | jsonFile, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0666) 285 | if err != nil { 286 | return nil, err 287 | } 288 | defer jsonFile.Close() 289 | 290 | data, err := fw.readJsonList(jsonFile) 291 | if err != nil { 292 | return nil, err 293 | } 294 | 295 | for _, ele := range data { 296 | if ele.ShardId == shardId { 297 | return ele, nil 298 | } 299 | } 300 | 301 | return nil, fmt.Errorf("not found") 302 | } 303 | 304 | // drop 305 | func (fw *FileWriter) DropAll() error { 306 | var fileList []string 307 | err := filepath.Walk(fw.dir, func(path string, info os.FileInfo, err error) error { 308 | if path != fw.dir { 309 | fileList = append(fileList, path) 310 | } 311 | return nil 312 | }) 313 | 314 | if err != nil { 315 | return err 316 | } 317 | 318 | LOG.Info("drop file list: %v", fileList) 319 | 320 | for _, file := range fileList { 321 | fw.lockFile(file) 322 | if err := os.Remove(file); err != nil { 323 | fw.unlockFile(file) 324 | return err 325 | } 326 | fw.unlockFile(file) 327 | } 328 | 329 | return nil 330 | } 331 | 332 | func (fw *FileWriter) lockFile(table string) { 333 | val, ok := fw.fileLock.Load(CheckpointStatusTable) 334 | if !ok { 335 | val = new(sync.Mutex) 336 | fw.fileLock.Store(CheckpointStatusTable, val) 337 | } 338 | 339 | lock := val.(*sync.Mutex) 340 | lock.Lock() 341 | } 342 | 343 | func (fw *FileWriter) unlockFile(table string) { 344 | val, ok := fw.fileLock.Load(CheckpointStatusTable) 345 | if !ok { 346 | val = new(sync.Mutex) 347 | fw.fileLock.Store(CheckpointStatusTable, val) 348 | } 349 | 350 | lock := val.(*sync.Mutex) 351 | lock.Unlock() 352 | } 353 | 354 | func (fw *FileWriter) readJsonList(f *os.File) ([]*Checkpoint, error) { 355 | byteValue, err := ioutil.ReadAll(f) 356 | if err != nil { 357 | return nil, err 358 | } 359 | 360 | byteList := bytes.Split(byteValue, []byte{10}) 361 | ret := make([]*Checkpoint, 0, len(byteList)) 362 | for i := 0; i < len(byteList)-1; i++ { 363 | var ele Checkpoint 364 | if err := json.Unmarshal(byteList[i], &ele); err != nil { 365 | return nil, err 366 | } 367 | ret = append(ret, &ele) 368 | } 369 | 370 | return ret, nil 371 | } 372 | 373 | func (fw *FileWriter) writeJsonList(f *os.File, input []*Checkpoint) error { 374 | for _, single := range input { 375 | val, err := json.Marshal(single) 376 | if err != nil { 377 | return nil 378 | } 379 | 380 | val = append(val, byte(10)) // suffix 381 | if _, err := f.Write(val); err != nil { 382 | return err 383 | } 384 | } 385 | return nil 386 | } 387 | 388 | func (fw *FileWriter) IncrCacheFileInsert(table string, shardId string, fileName string, 389 | lastSequenceNumber string, time string) error { 390 | return nil 391 | } 392 | -------------------------------------------------------------------------------- /nimo-shake/checkpoint/mongoWriter.go: -------------------------------------------------------------------------------- 1 | package checkpoint 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | utils "nimo-shake/common" 8 | 9 | LOG "github.com/vinllen/log4go" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | ) 14 | 15 | type MongoWriter struct { 16 | address string 17 | //conn *utils.MongoConn 18 | nconn *utils.MongoCommunityConn 19 | db string 20 | } 21 | 22 | func NewMongoWriter(address, db string) *MongoWriter { 23 | targetConn, err := utils.NewMongoCommunityConn(address, utils.ConnectModePrimary, true) 24 | if err != nil { 25 | LOG.Error("create mongodb with address[%v] db[%v] connection error[%v]", address, db, err) 26 | return nil 27 | } 28 | 29 | return &MongoWriter{ 30 | address: address, 31 | nconn: targetConn, 32 | db: db, 33 | } 34 | } 35 | 36 | func (mw *MongoWriter) FindStatus() (string, error) { 37 | var query Status 38 | if err := mw.nconn.Client.Database(mw.db).Collection(CheckpointStatusTable).FindOne(context.TODO(), 39 | bson.M{"Key": CheckpointStatusKey}).Decode(&query); err != nil { 40 | if err == mongo.ErrNoDocuments { 41 | return CheckpointStatusValueEmpty, nil 42 | } 43 | return "", err 44 | } else { 45 | return query.Value, nil 46 | } 47 | } 48 | 49 | func (mw *MongoWriter) UpdateStatus(status string) error { 50 | update := Status{ 51 | Key: CheckpointStatusKey, 52 | Value: status, 53 | } 54 | 55 | opts := options.Update().SetUpsert(true) 56 | filter := bson.M{"Key": CheckpointStatusKey} 57 | updateStr := bson.M{"$set": update} 58 | _, err := mw.nconn.Client.Database(mw.db).Collection(CheckpointStatusTable).UpdateOne(context.TODO(), filter, updateStr, opts) 59 | return err 60 | } 61 | 62 | func (mw *MongoWriter) ExtractCheckpoint() (map[string]map[string]*Checkpoint, error) { 63 | // extract checkpoint from mongodb, every collection checkpoint have independent collection(table) 64 | ckptMap := make(map[string]map[string]*Checkpoint) 65 | 66 | collectionList, err := mw.nconn.Client.Database(mw.db).ListCollectionNames(context.TODO(), bson.M{}) 67 | if err != nil { 68 | return nil, fmt.Errorf("fetch checkpoint collection list failed[%v]", err) 69 | } 70 | for _, table := range collectionList { 71 | if FilterCkptCollection(table) { 72 | continue 73 | } 74 | 75 | innerMap, err := mw.ExtractSingleCheckpoint(table) 76 | if err != nil { 77 | return nil, err 78 | } 79 | ckptMap[table] = innerMap 80 | } 81 | 82 | return ckptMap, nil 83 | } 84 | 85 | func (mw *MongoWriter) ExtractSingleCheckpoint(table string) (map[string]*Checkpoint, error) { 86 | innerMap := make(map[string]*Checkpoint) 87 | data := make([]*Checkpoint, 0) 88 | 89 | cursor, err := mw.nconn.Client.Database(mw.db).Collection(table).Find(context.TODO(), bson.M{}) 90 | if err != nil { 91 | return nil, err 92 | } 93 | defer cursor.Close(context.TODO()) 94 | 95 | for cursor.Next(context.TODO()) { 96 | var elem Checkpoint 97 | if err := cursor.Decode(&elem); err != nil { 98 | return nil, err 99 | } 100 | data = append(data, &elem) 101 | } 102 | 103 | for _, ele := range data { 104 | innerMap[ele.ShardId] = ele 105 | } 106 | 107 | return innerMap, nil 108 | } 109 | 110 | func (mw *MongoWriter) Insert(ckpt *Checkpoint, table string) error { 111 | _, err := mw.nconn.Client.Database(mw.db).Collection(table).InsertOne(context.TODO(), ckpt) 112 | 113 | return err 114 | } 115 | 116 | func (mw *MongoWriter) Update(shardId string, ckpt *Checkpoint, table string) error { 117 | 118 | filter := bson.M{"ShardId": shardId} 119 | updateStr := bson.M{"$set": ckpt} 120 | _, err := mw.nconn.Client.Database(mw.db).Collection(table).UpdateOne(context.TODO(), filter, updateStr) 121 | return err 122 | } 123 | 124 | func (mw *MongoWriter) UpdateWithSet(shardId string, input map[string]interface{}, table string) error { 125 | 126 | filter := bson.M{"ShardId": shardId} 127 | updateStr := bson.M{"$set": input} 128 | _, err := mw.nconn.Client.Database(mw.db).Collection(table).UpdateOne(context.TODO(), filter, updateStr) 129 | return err 130 | } 131 | 132 | func (mw *MongoWriter) Query(shardId string, table string) (*Checkpoint, error) { 133 | var res Checkpoint 134 | err := mw.nconn.Client.Database(mw.db).Collection(table).FindOne(context.TODO(), bson.M{"ShardId": shardId}).Decode(&res) 135 | 136 | return &res, err 137 | } 138 | 139 | func (mw *MongoWriter) DropAll() error { 140 | return mw.nconn.Client.Database(mw.db).Drop(context.TODO()) 141 | } 142 | 143 | func (fw *MongoWriter) IncrCacheFileInsert(table string, shardId string, fileName string, 144 | lastSequenceNumber string, time string) error { 145 | 146 | // write cachefile struct to db 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /nimo-shake/checkpoint/struct.go: -------------------------------------------------------------------------------- 1 | package checkpoint 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "nimo-shake/filter" 7 | "sync" 8 | ) 9 | 10 | const ( 11 | CheckpointWriterTypeMongo = "mongodb" 12 | CheckpointWriterTypeFile = "file" 13 | CheckpointStatusTable = "status_table" 14 | CheckpointStatusKey = "status_key" 15 | CheckpointStatusValueEmpty = "" 16 | CheckpointStatusValueFullSync = "full_sync" 17 | CheckpointStatusValueIncrSync = "incr_sync" 18 | 19 | // 0: not process; 1: no need to process; 2: prepare stage 3: in processing; 4: wait father finish, 5: done 20 | StatusNotProcess = "not process" 21 | StatusNoNeedProcess = "no need to process" 22 | StatusPrepareProcess = "prepare stage" 23 | StatusInProcessing = "in processing" 24 | StatusWaitFather = "wait father finish" 25 | StatusDone = "done" 26 | 27 | IteratorTypeLatest = "LATEST" 28 | IteratorTypeAtSequence = "AT_SEQUENCE_NUMBER" 29 | IteratorTypeAfterSequence = "AFTER_SEQUENCE_NUMBER" 30 | IteratorTypeTrimHorizon = "TRIM_HORIZON" 31 | 32 | InitShardIt = "see sequence number" 33 | 34 | StreamViewType = "NEW_AND_OLD_IMAGES" 35 | 36 | FieldShardId = "ShardId" 37 | FieldShardIt = "ShardIt" 38 | FieldStatus = "Status" 39 | FieldSeqNum = "SequenceNumber" 40 | FieldIteratorType = "IteratorType" 41 | FieldTimestamp = "UpdateDate" 42 | FieldApproximateTime = "ApproximateTime" 43 | ) 44 | 45 | type Checkpoint struct { 46 | ShardId string `bson:"ShardId" json:"ShardId"` // shard id 47 | FatherId string `bson:"FatherId" json:"FatherId"` // father id 48 | SequenceNumber string `bson:"SequenceNumber" json:"SequenceNumber"` // checkpoint 49 | Status string `bson:"Status" json:"Status"` // status 50 | WorkerId string `bson:"WorkerId" json:"WorkerId"` // thread number 51 | IteratorType string `bson:"IteratorType" json:"IteratorType"` // "LATEST" or "AT_SEQUENCE_NUMBER" 52 | ShardIt string `bson:"ShardIt" json:"ShardIt"` // only used when IteratorType == "LATEST" 53 | UpdateDate string `bson:"UpdateDate" json:"UpdateDate"` // update checkpoint time 54 | ApproximateTime string `bson:"ApproximateTime" json:"ApproximateTime"` // approximate time of records 55 | } 56 | 57 | func (c *Checkpoint) String() string { 58 | out, _ := json.Marshal(c) 59 | return fmt.Sprintf("%s", out) 60 | } 61 | 62 | type Status struct { 63 | Key string `bson:"Key" json:"Key"` // key -> CheckpointStatusKey 64 | Value string `bson:"StatusValue" json:"StatusValue"` // CheckpointStatusValueFullSync or CheckpointStatusValueIncrSync 65 | } 66 | 67 | /*---------------------------------------*/ 68 | 69 | var ( 70 | GlobalShardIteratorMap = ShardIteratorMap{ 71 | mp: make(map[string]string), 72 | } 73 | ) 74 | 75 | type ShardIteratorMap struct { 76 | mp map[string]string 77 | lock sync.Mutex 78 | } 79 | 80 | func (sim *ShardIteratorMap) Set(key, iterator string) bool { 81 | sim.lock.Lock() 82 | defer sim.lock.Unlock() 83 | 84 | if _, ok := sim.mp[key]; ok { 85 | return false 86 | } 87 | 88 | sim.mp[key] = iterator 89 | return false 90 | } 91 | 92 | func (sim *ShardIteratorMap) Get(key string) (string, bool) { 93 | sim.lock.Lock() 94 | defer sim.lock.Unlock() 95 | 96 | it, ok := sim.mp[key] 97 | return it, ok 98 | } 99 | 100 | func (sim *ShardIteratorMap) Delete(key string) bool { 101 | sim.lock.Lock() 102 | defer sim.lock.Unlock() 103 | 104 | if _, ok := sim.mp[key]; ok { 105 | delete(sim.mp, key) 106 | return true 107 | } 108 | return false 109 | } 110 | 111 | /*---------------------------------------*/ 112 | 113 | func FilterCkptCollection(collection string) bool { 114 | return collection == CheckpointStatusTable || filter.IsFilter(collection) 115 | } 116 | 117 | func IsStatusProcessing(status string) bool { 118 | return status == StatusPrepareProcess || status == StatusInProcessing || status == StatusWaitFather 119 | } 120 | 121 | func IsStatusNoNeedProcess(status string) bool { 122 | return status == StatusDone || status == StatusNoNeedProcess 123 | } 124 | -------------------------------------------------------------------------------- /nimo-shake/checkpoint/writer.go: -------------------------------------------------------------------------------- 1 | package checkpoint 2 | 3 | import ( 4 | LOG "github.com/vinllen/log4go" 5 | ) 6 | 7 | type Writer interface { 8 | // find current status 9 | FindStatus() (string, error) 10 | 11 | // update status 12 | UpdateStatus(status string) error 13 | 14 | // extract all checkpoint 15 | ExtractCheckpoint() (map[string]map[string]*Checkpoint, error) 16 | 17 | // extract single checkpoint 18 | ExtractSingleCheckpoint(table string) (map[string]*Checkpoint, error) 19 | 20 | // insert checkpoint 21 | Insert(ckpt *Checkpoint, table string) error 22 | 23 | // update checkpoint 24 | Update(shardId string, ckpt *Checkpoint, table string) error 25 | 26 | // update with set 27 | UpdateWithSet(shardId string, input map[string]interface{}, table string) error 28 | 29 | // query 30 | Query(shardId string, table string) (*Checkpoint, error) 31 | 32 | // insert incrSyncCacheFile 33 | IncrCacheFileInsert(table string, shardId string, fileName string, lastSequenceNumber string, time string) error 34 | 35 | // drop 36 | DropAll() error 37 | } 38 | 39 | func NewWriter(name, address, db string) Writer { 40 | var w Writer 41 | switch name { 42 | case CheckpointWriterTypeMongo: 43 | w = NewMongoWriter(address, db) 44 | case CheckpointWriterTypeFile: 45 | w = NewFileWriter(db) 46 | default: 47 | LOG.Crashf("unknown checkpoint writer[%v]", name) 48 | } 49 | if w == nil { 50 | LOG.Crashf("create checkpoint writer[%v] failed", name) 51 | return nil 52 | } 53 | return w 54 | } 55 | -------------------------------------------------------------------------------- /nimo-shake/checkpoint/writer_test.go: -------------------------------------------------------------------------------- 1 | package checkpoint 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | utils "nimo-shake/common" 10 | ) 11 | 12 | const ( 13 | TestMongoAddress = "mongodb://100.81.164.181:18901" 14 | TestCheckpointDb = "test_checkpoint_db" 15 | TestCheckpointTable = "test_checkpoint_table" 16 | ) 17 | 18 | func TestStatus(t *testing.T) { 19 | 20 | utils.InitialLogger("", "debug", false) 21 | 22 | var err error 23 | mongoWriter := NewWriter(CheckpointWriterTypeMongo, TestMongoAddress, TestCheckpointDb) 24 | assert.Equal(t, true, mongoWriter != nil, "should be equal") 25 | 26 | fileWriter := NewWriter(CheckpointWriterTypeFile, TestMongoAddress, TestCheckpointDb) 27 | assert.Equal(t, true, fileWriter != nil, "should be equal") 28 | 29 | var nr int 30 | // test status: mongo 31 | { 32 | fmt.Printf("TestStatus case %d.\n", nr) 33 | nr++ 34 | 35 | err = mongoWriter.DropAll() 36 | assert.Equal(t, nil, err, "should be equal") 37 | 38 | status, err := mongoWriter.FindStatus() 39 | assert.Equal(t, nil, err, "should be equal") 40 | assert.Equal(t, CheckpointStatusValueEmpty, status, "should be equal") 41 | 42 | err = mongoWriter.UpdateStatus(CheckpointStatusValueIncrSync) 43 | assert.Equal(t, nil, err, "should be equal") 44 | 45 | status, err = mongoWriter.FindStatus() 46 | assert.Equal(t, nil, err, "should be equal") 47 | assert.Equal(t, CheckpointStatusValueIncrSync, status, "should be equal") 48 | } 49 | 50 | // test status: file 51 | { 52 | fmt.Printf("TestStatus case %d.\n", nr) 53 | nr++ 54 | 55 | err = fileWriter.DropAll() 56 | assert.Equal(t, nil, err, "should be equal") 57 | 58 | status, err := fileWriter.FindStatus() 59 | assert.Equal(t, nil, err, "should be equal") 60 | assert.Equal(t, CheckpointStatusValueEmpty, status, "should be equal") 61 | 62 | err = fileWriter.UpdateStatus(CheckpointStatusValueIncrSync) 63 | assert.Equal(t, nil, err, "should be equal") 64 | 65 | status, err = fileWriter.FindStatus() 66 | assert.Equal(t, nil, err, "should be equal") 67 | assert.Equal(t, CheckpointStatusValueIncrSync, status, "should be equal") 68 | } 69 | } 70 | 71 | func TestCheckpointCRUD(t *testing.T) { 72 | var err error 73 | mongoWriter := NewWriter(CheckpointWriterTypeMongo, TestMongoAddress, TestCheckpointDb) 74 | assert.Equal(t, true, mongoWriter != nil, "should be equal") 75 | 76 | fileWriter := NewWriter(CheckpointWriterTypeFile, TestMongoAddress, TestCheckpointDb) 77 | assert.Equal(t, true, fileWriter != nil, "should be equal") 78 | 79 | // utils.InitialLogger("", "info", false) 80 | 81 | var nr int 82 | // test CRUD: mongo 83 | { 84 | fmt.Printf("TestCheckpointCRUD case %d.\n", nr) 85 | nr++ 86 | 87 | err = mongoWriter.DropAll() 88 | assert.Equal(t, nil, err, "should be equal") 89 | 90 | cpkt := &Checkpoint{ 91 | ShardId: "test_id", 92 | FatherId: "test_father", 93 | Status: StatusNotProcess, 94 | } 95 | 96 | err = mongoWriter.Update("test_id", cpkt, TestCheckpointTable) 97 | assert.Equal(t, nil, err, "should be equal") 98 | 99 | err = mongoWriter.UpdateWithSet("test_id", map[string]interface{}{ 100 | "Status": StatusNotProcess, 101 | }, TestCheckpointTable) 102 | assert.Equal(t, nil, err, "should be equal") 103 | 104 | err = mongoWriter.Insert(cpkt, TestCheckpointTable) 105 | assert.Equal(t, nil, err, "should be equal") 106 | 107 | ckptRet, err := mongoWriter.Query("test_id", TestCheckpointTable) 108 | assert.Equal(t, nil, err, "should be equal") 109 | assert.Equal(t, cpkt.ShardId, ckptRet.ShardId, "should be equal") 110 | assert.Equal(t, cpkt.FatherId, ckptRet.FatherId, "should be equal") 111 | assert.Equal(t, cpkt.Status, ckptRet.Status, "should be equal") 112 | 113 | err = mongoWriter.UpdateWithSet("test_id", map[string]interface{}{ 114 | "Status": StatusInProcessing, 115 | }, TestCheckpointTable) 116 | assert.Equal(t, nil, err, "should be equal") 117 | 118 | ckptRet, err = mongoWriter.Query("test_id", TestCheckpointTable) 119 | assert.Equal(t, nil, err, "should be equal") 120 | assert.Equal(t, cpkt.ShardId, ckptRet.ShardId, "should be equal") 121 | assert.Equal(t, cpkt.FatherId, ckptRet.FatherId, "should be equal") 122 | assert.Equal(t, StatusInProcessing, ckptRet.Status, "should be equal") 123 | } 124 | 125 | // test CRUD: file 126 | { 127 | fmt.Printf("TestCheckpointCRUD case %d.\n", nr) 128 | nr++ 129 | 130 | err = fileWriter.DropAll() 131 | assert.Equal(t, nil, err, "should be equal") 132 | 133 | cpkt := &Checkpoint{ 134 | ShardId: "test_id", 135 | FatherId: "test_father", 136 | Status: StatusNotProcess, 137 | SequenceNumber: "seq-123", 138 | } 139 | 140 | err = fileWriter.Update("test_id", cpkt, TestCheckpointTable) 141 | assert.Equal(t, true, err != nil, "should be equal") 142 | fmt.Println(err) 143 | 144 | err = fileWriter.UpdateWithSet("test_id", map[string]interface{}{ 145 | "Status": StatusNotProcess, 146 | }, TestCheckpointTable) 147 | assert.Equal(t, true, err != nil, "should be equal") 148 | fmt.Println(err) 149 | 150 | err = fileWriter.Insert(cpkt, TestCheckpointTable) 151 | assert.Equal(t, nil, err, "should be equal") 152 | 153 | ckptRet, err := fileWriter.Query("test_id", TestCheckpointTable) 154 | assert.Equal(t, nil, err, "should be equal") 155 | assert.Equal(t, cpkt.ShardId, ckptRet.ShardId, "should be equal") 156 | assert.Equal(t, cpkt.FatherId, ckptRet.FatherId, "should be equal") 157 | assert.Equal(t, cpkt.Status, ckptRet.Status, "should be equal") 158 | assert.Equal(t, cpkt.SequenceNumber, "seq-123", "should be equal") 159 | 160 | err = fileWriter.UpdateWithSet("test_id", map[string]interface{}{ 161 | "Status": StatusInProcessing, 162 | "SequenceNumber": "seq-456", 163 | }, TestCheckpointTable) 164 | assert.Equal(t, nil, err, "should be equal") 165 | 166 | ckptRet, err = fileWriter.Query("test_id", TestCheckpointTable) 167 | assert.Equal(t, nil, err, "should be equal") 168 | assert.Equal(t, cpkt.ShardId, ckptRet.ShardId, "should be equal") 169 | assert.Equal(t, cpkt.FatherId, ckptRet.FatherId, "should be equal") 170 | assert.Equal(t, StatusInProcessing, ckptRet.Status, "should be equal") 171 | assert.Equal(t, "seq-456", ckptRet.SequenceNumber, "should be equal") 172 | } 173 | } 174 | 175 | func TestExtractCheckpoint(t *testing.T) { 176 | var err error 177 | mongoWriter := NewWriter(CheckpointWriterTypeMongo, TestMongoAddress, TestCheckpointDb) 178 | assert.Equal(t, true, mongoWriter != nil, "should be equal") 179 | 180 | fileWriter := NewWriter(CheckpointWriterTypeFile, TestMongoAddress, TestCheckpointDb) 181 | assert.Equal(t, true, fileWriter != nil, "should be equal") 182 | 183 | // utils.InitialLogger("", "info", false) 184 | 185 | var nr int 186 | // test CRUD: mongo 187 | { 188 | fmt.Printf("TestExtractCheckpoint case %d.\n", nr) 189 | nr++ 190 | 191 | err = mongoWriter.DropAll() 192 | assert.Equal(t, nil, err, "should be equal") 193 | 194 | err = mongoWriter.Insert(&Checkpoint{ 195 | ShardId: "id1", 196 | Status: StatusNotProcess, 197 | }, "table1") 198 | assert.Equal(t, nil, err, "should be equal") 199 | 200 | err = mongoWriter.Insert(&Checkpoint{ 201 | ShardId: "id2", 202 | Status: StatusInProcessing, 203 | }, "table1") 204 | assert.Equal(t, nil, err, "should be equal") 205 | 206 | err = mongoWriter.Insert(&Checkpoint{ 207 | ShardId: "id3", 208 | Status: StatusPrepareProcess, 209 | }, "table1") 210 | assert.Equal(t, nil, err, "should be equal") 211 | 212 | err = mongoWriter.Insert(&Checkpoint{ 213 | ShardId: "id1", 214 | Status: StatusDone, 215 | }, "table2") 216 | assert.Equal(t, nil, err, "should be equal") 217 | 218 | err = mongoWriter.Insert(&Checkpoint{ 219 | ShardId: "id10", 220 | Status: StatusWaitFather, 221 | }, "table2") 222 | assert.Equal(t, nil, err, "should be equal") 223 | 224 | mp, err := mongoWriter.ExtractCheckpoint() 225 | assert.Equal(t, nil, err, "should be equal") 226 | assert.Equal(t, 2, len(mp), "should be equal") 227 | assert.Equal(t, 3, len(mp["table1"]), "should be equal") 228 | assert.Equal(t, 2, len(mp["table2"]), "should be equal") 229 | assert.Equal(t, StatusNotProcess, mp["table1"]["id1"].Status, "should be equal") 230 | assert.Equal(t, StatusInProcessing, mp["table1"]["id2"].Status, "should be equal") 231 | assert.Equal(t, StatusPrepareProcess, mp["table1"]["id3"].Status, "should be equal") 232 | assert.Equal(t, StatusDone, mp["table2"]["id1"].Status, "should be equal") 233 | assert.Equal(t, StatusWaitFather, mp["table2"]["id10"].Status, "should be equal") 234 | } 235 | 236 | // test CRUD: file 237 | { 238 | fmt.Printf("TestExtractCheckpoint case %d.\n", nr) 239 | nr++ 240 | 241 | err = fileWriter.DropAll() 242 | assert.Equal(t, nil, err, "should be equal") 243 | 244 | err = fileWriter.Insert(&Checkpoint{ 245 | ShardId: "id1", 246 | Status: StatusNotProcess, 247 | }, "table1") 248 | assert.Equal(t, nil, err, "should be equal") 249 | 250 | err = fileWriter.Insert(&Checkpoint{ 251 | ShardId: "id2", 252 | Status: StatusInProcessing, 253 | }, "table1") 254 | assert.Equal(t, nil, err, "should be equal") 255 | 256 | err = fileWriter.Insert(&Checkpoint{ 257 | ShardId: "id3", 258 | Status: StatusPrepareProcess, 259 | }, "table1") 260 | assert.Equal(t, nil, err, "should be equal") 261 | 262 | err = fileWriter.Insert(&Checkpoint{ 263 | ShardId: "id1", 264 | Status: StatusDone, 265 | }, "table2") 266 | assert.Equal(t, nil, err, "should be equal") 267 | 268 | err = fileWriter.Insert(&Checkpoint{ 269 | ShardId: "id10", 270 | Status: StatusWaitFather, 271 | }, "table2") 272 | assert.Equal(t, nil, err, "should be equal") 273 | 274 | mp, err := fileWriter.ExtractCheckpoint() 275 | assert.Equal(t, nil, err, "should be equal") 276 | assert.Equal(t, 2, len(mp), "should be equal") 277 | assert.Equal(t, 3, len(mp["table1"]), "should be equal") 278 | assert.Equal(t, 2, len(mp["table2"]), "should be equal") 279 | assert.Equal(t, StatusNotProcess, mp["table1"]["id1"].Status, "should be equal") 280 | assert.Equal(t, StatusInProcessing, mp["table1"]["id2"].Status, "should be equal") 281 | assert.Equal(t, StatusPrepareProcess, mp["table1"]["id3"].Status, "should be equal") 282 | assert.Equal(t, StatusDone, mp["table2"]["id1"].Status, "should be equal") 283 | assert.Equal(t, StatusWaitFather, mp["table2"]["id10"].Status, "should be equal") 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /nimo-shake/common/callback.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | /* 6 | * retry the callback function until successfully or overpass the threshold. 7 | * @input: 8 | * times: retry times 9 | * sleep: sleep time by ms interval 10 | * cb: callback 11 | * the callback should return true means need retry. 12 | */ 13 | func CallbackRetry(times int, sleep int, cb func() bool) bool { 14 | for i := 0; i < times; i++ { 15 | if cb() == false { // callback, true means retry 16 | return true 17 | } 18 | time.Sleep(time.Duration(sleep) * time.Millisecond) 19 | } 20 | return false 21 | } -------------------------------------------------------------------------------- /nimo-shake/common/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | LOG "github.com/vinllen/log4go" 9 | ) 10 | 11 | const ( 12 | GolangSecurityTime = "2006-01-02T15:04:05Z" 13 | 14 | ConvertTypeRaw = "raw" 15 | ConvertTypeChange = "change" 16 | ConvertMTypeChange = "mchange" 17 | ConvertTypeSame = "same" // used in dynamodb -> dynamo-proxy 18 | 19 | SyncModeAll = "all" 20 | SyncModeFull = "full" 21 | SyncModeIncr = "incr" 22 | 23 | TargetTypeMongo = "mongodb" 24 | TargetTypeAliyunDynamoProxy = "aliyun_dynamo_proxy" 25 | 26 | TargetMongoDBTypeReplica = "replica" 27 | TargetMongoDBTypeSharding = "sharding" 28 | 29 | TargetDBExistRename = "rename" 30 | TargetDBExistDrop = "drop" 31 | 32 | SIGNALPROFILE = 31 33 | SIGNALSTACK = 30 34 | ) 35 | 36 | var ( 37 | Version = "$" 38 | StartTime string 39 | ) 40 | 41 | func InitialLogger(logFile string, level string, logBuffer bool) bool { 42 | logLevel := parseLogLevel(level) 43 | if len(logFile) != 0 { 44 | // create logs folder for log4go. because of its mistake that doesn't create ! 45 | if err := os.MkdirAll("logs", os.ModeDir|os.ModePerm); err != nil { 46 | return false 47 | } 48 | if logBuffer { 49 | LOG.LogBufferLength = 32 50 | } else { 51 | LOG.LogBufferLength = 0 52 | } 53 | fileLogger := LOG.NewFileLogWriter(fmt.Sprintf("logs/%s", logFile), true) 54 | //fileLogger.SetRotateDaily(true) 55 | fileLogger.SetRotateSize(500 * 1024 * 1024) 56 | // fileLogger.SetFormat("[%D %T] [%L] [%s] %M") 57 | fileLogger.SetFormat("[%D %T] [%L] %M") 58 | fileLogger.SetRotateMaxBackup(100) 59 | LOG.AddFilter("file", logLevel, fileLogger) 60 | } else { 61 | LOG.AddFilter("console", logLevel, LOG.NewConsoleLogWriter()) 62 | } 63 | return true 64 | } 65 | 66 | func parseLogLevel(level string) LOG.Level { 67 | switch strings.ToLower(level) { 68 | case "debug": 69 | return LOG.DEBUG 70 | case "info": 71 | return LOG.INFO 72 | case "warning": 73 | return LOG.WARNING 74 | case "error": 75 | return LOG.ERROR 76 | default: 77 | return LOG.DEBUG 78 | } 79 | } 80 | 81 | /** 82 | * block password in mongo_urls: 83 | * two kind mongo_urls: 84 | * 1. mongodb://username:password@address 85 | * 2. username:password@address 86 | */ 87 | func BlockMongoUrlPassword(url, replace string) string { 88 | colon := strings.Index(url, ":") 89 | if colon == -1 || colon == len(url)-1 { 90 | return url 91 | } else if url[colon+1] == '/' { 92 | // find the second '/' 93 | for colon++; colon < len(url); colon++ { 94 | if url[colon] == ':' { 95 | break 96 | } 97 | } 98 | 99 | if colon == len(url) { 100 | return url 101 | } 102 | } 103 | 104 | at := strings.Index(url, "@") 105 | if at == -1 || at == len(url)-1 || at <= colon { 106 | return url 107 | } 108 | 109 | newUrl := make([]byte, 0, len(url)) 110 | for i := 0; i < len(url); i++ { 111 | if i <= colon || i > at { 112 | newUrl = append(newUrl, byte(url[i])) 113 | } else if i == at { 114 | newUrl = append(newUrl, []byte(replace)...) 115 | newUrl = append(newUrl, byte(url[i])) 116 | } 117 | } 118 | return string(newUrl) 119 | } 120 | -------------------------------------------------------------------------------- /nimo-shake/common/dynamodb.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/credentials" 11 | "github.com/aws/aws-sdk-go/aws/session" 12 | "github.com/aws/aws-sdk-go/service/dynamodb" 13 | "github.com/aws/aws-sdk-go/service/dynamodbstreams" 14 | ) 15 | 16 | var ( 17 | globalSession *session.Session 18 | ) 19 | 20 | /* 21 | * all client share the same session. 22 | * Sessions can be shared across all service clients that share the same base configuration 23 | * refer: https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ 24 | */ 25 | func InitSession(accessKeyID, secretAccessKey, sessionToken, region, endpoint string, maxRetries, timeout uint) error { 26 | config := &aws.Config{ 27 | Region: aws.String(region), 28 | Credentials: credentials.NewStaticCredentials(accessKeyID, secretAccessKey, sessionToken), 29 | MaxRetries: aws.Int(int(maxRetries)), 30 | HTTPClient: &http.Client{ 31 | Timeout: time.Duration(timeout) * time.Millisecond, 32 | }, 33 | } 34 | 35 | if endpoint != "" { 36 | config.Endpoint = aws.String(endpoint) 37 | } 38 | 39 | var err error 40 | globalSession, err = session.NewSession(config) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func CreateDynamoSession(logLevel string) (*dynamodb.DynamoDB, error) { 49 | if logLevel == "debug" { 50 | svc := dynamodb.New(globalSession, aws.NewConfig().WithLogLevel(aws.LogDebugWithHTTPBody)) 51 | return svc, nil 52 | } 53 | svc := dynamodb.New(globalSession) 54 | return svc, nil 55 | } 56 | 57 | func CreateDynamoStreamSession(logLevel string) (*dynamodbstreams.DynamoDBStreams, error) { 58 | if logLevel == "debug" { 59 | svc := dynamodbstreams.New(globalSession, aws.NewConfig().WithLogLevel(aws.LogDebugWithHTTPBody)) 60 | return svc, nil 61 | } 62 | svc := dynamodbstreams.New(globalSession) 63 | return svc, nil 64 | } 65 | 66 | func ParseIndexType(input []*dynamodb.AttributeDefinition) map[string]string { 67 | mp := make(map[string]string, len(input)) 68 | 69 | for _, ele := range input { 70 | mp[*ele.AttributeName] = *ele.AttributeType 71 | } 72 | 73 | return mp 74 | } 75 | 76 | // fetch dynamodb table list 77 | func FetchTableList(dynamoSession *dynamodb.DynamoDB) ([]string, error) { 78 | ans := make([]string, 0) 79 | var lastEvaluatedTableName *string 80 | 81 | for { 82 | out, err := dynamoSession.ListTables(&dynamodb.ListTablesInput{ 83 | ExclusiveStartTableName: lastEvaluatedTableName, 84 | }) 85 | 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | ans = AppendStringList(ans, out.TableNames) 91 | if out.LastEvaluatedTableName == nil { 92 | // finish 93 | break 94 | } 95 | lastEvaluatedTableName = out.LastEvaluatedTableName 96 | } 97 | 98 | return ans, nil 99 | } 100 | 101 | func ParsePrimaryAndSortKey(primaryIndexes []*dynamodb.KeySchemaElement, parseMap map[string]string) (string, string, error) { 102 | var primaryKey string 103 | var sortKey string 104 | for _, index := range primaryIndexes { 105 | if *(index.KeyType) == "HASH" { 106 | if primaryKey != "" { 107 | return "", "", fmt.Errorf("duplicate primary key type[%v]", *(index.AttributeName)) 108 | } 109 | primaryKey = *(index.AttributeName) 110 | } else if *(index.KeyType) == "RANGE" { 111 | if sortKey != "" { 112 | return "", "", fmt.Errorf("duplicate sort key type[%v]", *(index.AttributeName)) 113 | } 114 | sortKey = *(index.AttributeName) 115 | } else { 116 | return "", "", fmt.Errorf("unknonw key type[%v]", *(index.KeyType)) 117 | } 118 | } 119 | return primaryKey, sortKey, nil 120 | } 121 | 122 | /* 123 | input: 124 | 125 | "begin```N```1646724207280~~~end```S```1646724207283" 126 | 127 | output: 128 | 129 | map[string]*dynamodb.AttributeValue{ 130 | ":begin": &dynamodb.AttributeValue{N: aws.String("1646724207280")}, 131 | ":end": &dynamodb.AttributeValue{S: aws.String("1646724207283")}, 132 | } 133 | */ 134 | func ParseAttributes(input string) map[string]*dynamodb.AttributeValue { 135 | result := make(map[string]*dynamodb.AttributeValue) 136 | pairs := strings.Split(input, "~~~") 137 | 138 | for _, pair := range pairs { 139 | parts := strings.Split(pair, "```") 140 | if len(parts) != 3 { 141 | continue 142 | } 143 | 144 | key := ":" + parts[0] 145 | dataType := parts[1] 146 | value := parts[2] 147 | 148 | switch dataType { 149 | case "N": 150 | result[key] = &dynamodb.AttributeValue{N: aws.String(value)} 151 | case "S": 152 | result[key] = &dynamodb.AttributeValue{S: aws.String(value)} 153 | } 154 | } 155 | 156 | return result 157 | } 158 | -------------------------------------------------------------------------------- /nimo-shake/common/error.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/vinllen/mgo" 5 | ) 6 | 7 | // true means error can be ignored 8 | // https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.yml 9 | func MongodbIgnoreError(err error, op string, isFullSyncStage bool) bool { 10 | if err == nil { 11 | return true 12 | } 13 | 14 | errorCode := mgo.ErrorCodeList(err) 15 | if err != nil && len(errorCode) == 0 { 16 | return false 17 | } 18 | 19 | for _, err := range errorCode { 20 | switch op { 21 | case "i": 22 | case "u": 23 | if isFullSyncStage { 24 | if err == 28 { // PathNotViable 25 | continue 26 | } 27 | } 28 | case "d": 29 | if err == 26 { // NamespaceNotFound 30 | continue 31 | } 32 | case "c": 33 | if err == 26 { // NamespaceNotFound 34 | continue 35 | } 36 | default: 37 | return false 38 | } 39 | return false 40 | } 41 | 42 | return true 43 | } 44 | 45 | func DynamoIgnoreError(err error, op string, isFullSyncStage bool) bool { 46 | if err == nil { 47 | return true 48 | } 49 | 50 | switch op { 51 | case "i": 52 | case "u": 53 | if isFullSyncStage { 54 | if err.Error() == "xxxx" { // PathNotViable 55 | return true 56 | } 57 | } 58 | case "d": 59 | if err.Error() == "xxxx" { // NamespaceNotFound 60 | return true 61 | } 62 | case "c": 63 | if err.Error() == "xxxx" { // NamespaceNotFound 64 | return true 65 | } 66 | default: 67 | return false 68 | } 69 | return false 70 | } -------------------------------------------------------------------------------- /nimo-shake/common/fcv.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | var ( 4 | FcvCheckpoint = Checkpoint{ 5 | CurrentVersion: 0, 6 | FeatureCompatibleVersion: 0, 7 | } 8 | FcvConfiguration = Configuration{ 9 | CurrentVersion: 4, 10 | FeatureCompatibleVersion: 1, 11 | } 12 | 13 | LowestCheckpointVersion = map[int]string{ 14 | 0: "1.0.0", 15 | } 16 | LowestConfigurationVersion = map[int]string{ 17 | 0: "1.0.0", 18 | 1: "1.0.6", // add full sync and incr sync http port 19 | 2: "1.0.8", // add full.read.concurrency 20 | 3: "1.0.9", // add full.document.write.batch 21 | 4: "1.0.11", // add source.endpoint_url 22 | } 23 | ) 24 | 25 | type Fcv interface { 26 | IsCompatible(int) bool 27 | } 28 | 29 | // for checkpoint 30 | type Checkpoint struct { 31 | /* 32 | * version: 0(or set not), MongoShake < 2.4, fcv == 0 33 | * version: 1, MongoShake == 2.4, 0 < fcv <= 1 34 | */ 35 | CurrentVersion int 36 | FeatureCompatibleVersion int 37 | } 38 | 39 | func (c Checkpoint) IsCompatible(v int) bool { 40 | return v >= c.FeatureCompatibleVersion && v <= c.CurrentVersion 41 | } 42 | 43 | // for configuration 44 | type Configuration struct { 45 | /* 46 | * version: 0(or set not), MongoShake < 2.4.0, fcv == 0 47 | * version: 1, MongoShake == 2.4.0, 0 <= fcv <= 1 48 | */ 49 | CurrentVersion int 50 | FeatureCompatibleVersion int 51 | } 52 | 53 | func (c Configuration) IsCompatible(v int) bool { 54 | return v >= c.FeatureCompatibleVersion && v <= c.CurrentVersion 55 | } 56 | -------------------------------------------------------------------------------- /nimo-shake/common/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | nimo "github.com/gugemichael/nimo4go" 5 | ) 6 | 7 | var ( 8 | FullSyncHttpApi *nimo.HttpRestProvider 9 | IncrSyncHttpApi *nimo.HttpRestProvider 10 | ) 11 | 12 | func FullSyncInitHttpApi(port int) { 13 | FullSyncHttpApi = nimo.NewHttpRestProvider(port) 14 | } 15 | 16 | func IncrSyncInitHttpApi(port int) { 17 | IncrSyncHttpApi = nimo.NewHttpRestProvider(port) 18 | } 19 | -------------------------------------------------------------------------------- /nimo-shake/common/math.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/binary" 6 | ) 7 | 8 | /* 9 | // === FUNCTION ====================================================================== 10 | // Name: Md5 11 | // Description: 128位md5 12 | // ===================================================================================== 13 | */ 14 | func Md5(data []byte) [16]byte { 15 | return md5.Sum(data) 16 | } 17 | 18 | /* 19 | // === FUNCTION ====================================================================== 20 | // Name: Md5 21 | // Description: 64位md5 22 | // ===================================================================================== 23 | */ 24 | func Md5In64(data []byte) uint64 { 25 | var md5 = md5.Sum(data) 26 | var lowMd5 = md5[0:8] 27 | return binary.LittleEndian.Uint64(lowMd5) 28 | } 29 | -------------------------------------------------------------------------------- /nimo-shake/common/metric.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "time" 7 | "sync" 8 | 9 | LOG "github.com/vinllen/log4go" 10 | ) 11 | 12 | const ( 13 | TypeFull = "full" 14 | TypeIncr = "incr" 15 | ) 16 | 17 | type Status string 18 | 19 | // full sync 20 | const ( 21 | StatusWaitStart Status = "wait start" 22 | StatusProcessing Status = "in processing" 23 | StatusFinish Status = "finish" 24 | ) 25 | 26 | type CollectionMetric struct { 27 | CollectionStatus Status 28 | TotalCount uint64 29 | FinishCount uint64 30 | } 31 | 32 | func NewCollectionMetric() *CollectionMetric { 33 | return &CollectionMetric{ 34 | CollectionStatus: StatusWaitStart, 35 | } 36 | } 37 | 38 | func (cm *CollectionMetric) String() string { 39 | if cm.CollectionStatus == StatusWaitStart { 40 | return fmt.Sprintf("-") 41 | } 42 | 43 | if cm.TotalCount == 0 || cm.FinishCount >= cm.TotalCount { 44 | return fmt.Sprintf("100%% (%v/%v)", cm.FinishCount, cm.TotalCount) 45 | } else { 46 | return fmt.Sprintf("%.2f%% (%v/%v)", float64(cm.FinishCount) / float64(cm.TotalCount) * 100, 47 | cm.FinishCount, cm.TotalCount) 48 | } 49 | } 50 | 51 | /*************************************************************************/ 52 | // incr sync 53 | 54 | const ( 55 | FrequentInSeconds = 5 56 | TimeFormat string = "2006-01-02 15:04:05" 57 | ) 58 | 59 | const ( 60 | KB = 1024 61 | MB = 1024 * KB 62 | GB = 1024 * MB 63 | TB = 1024 * GB 64 | PB = 1024 * TB 65 | ) 66 | 67 | // struct used to mark the metric delta. 68 | // Value: current value 69 | // Delta: the difference between current value and previous value 70 | // previous: store the previous value 71 | type MetricDelta struct { 72 | Value uint64 73 | Delta uint64 74 | previous uint64 75 | } 76 | 77 | func (o *MetricDelta) Update() { 78 | current := atomic.LoadUint64(&o.Value) 79 | o.Delta, o.previous = current-o.previous, current 80 | } 81 | 82 | type ReplicationStatus uint64 83 | 84 | const ( 85 | METRIC_NONE = 0x0000000000000000 86 | METRIC_CKPT_TIMES = 0x0000000000000001 87 | METRIC_TUNNEL_TRAFFIC = 0x0000000000000010 88 | METRIC_LSN = 0x0000000000000100 89 | METRIC_TPS = 0x0000000000010000 90 | METRIC_SUCCESS = 0x0000000000100000 91 | METRIC_FULLSYNC_WRITE = 0x0000000010000000 // full sync writer 92 | METRIC_FILTER = 0x0000000100000000 93 | ) 94 | 95 | type ReplicationMetric struct { 96 | // NAME string 97 | STAGE string 98 | SUBSCRIBE uint64 99 | 100 | OplogFilter MetricDelta 101 | OplogGet MetricDelta 102 | OplogConsume MetricDelta 103 | OplogApply MetricDelta 104 | OplogSuccess MetricDelta 105 | OplogFail MetricDelta 106 | OplogWriteFail MetricDelta // full: write failed. currently, only used in full sync stage. 107 | CheckpointTimes uint64 108 | Retransmission uint64 109 | TunnelTraffic uint64 110 | LSN int64 111 | LSNAck int64 112 | LSNCheckpoint int64 113 | 114 | OplogMaxSize int64 115 | OplogAvgSize int64 116 | 117 | TableOperations *TableOps 118 | 119 | // replication status 120 | ReplStatus ReplicationStatus 121 | 122 | isClosed bool 123 | } 124 | 125 | //var Metric *ReplicationMetric 126 | 127 | func NewMetric(stage string, subscribe uint64) *ReplicationMetric { 128 | metric := &ReplicationMetric{} 129 | // metric.NAME = name 130 | metric.STAGE = stage 131 | metric.SUBSCRIBE = subscribe 132 | metric.startup() 133 | return metric 134 | } 135 | 136 | func (metric *ReplicationMetric) init() { 137 | metric.TableOperations = NewTableOps() 138 | } 139 | 140 | func (metric *ReplicationMetric) Close() { 141 | metric.isClosed = true 142 | } 143 | 144 | func (metric *ReplicationMetric) String() string { 145 | // return fmt.Sprintf("name[%v] stage[%v]", metric.NAME, metric.STAGE) 146 | return fmt.Sprintf("stage[%v]", metric.STAGE) 147 | } 148 | 149 | func (metric *ReplicationMetric) resetEverySecond(items []*MetricDelta) { 150 | for _, item := range items { 151 | item.Update() 152 | } 153 | } 154 | 155 | func (metric *ReplicationMetric) startup() { 156 | metric.init() 157 | go func() { 158 | tick := 0 159 | // items that need be reset 160 | resetItems := []*MetricDelta{&metric.OplogSuccess} 161 | for range time.NewTicker(1 * time.Second).C { 162 | if metric.isClosed { 163 | break 164 | } 165 | 166 | tick++ 167 | metric.resetEverySecond(resetItems) 168 | if tick%FrequentInSeconds != 0 { 169 | continue 170 | } 171 | 172 | ckpt := atomic.LoadUint64(&metric.CheckpointTimes) 173 | tps := atomic.LoadUint64(&metric.OplogSuccess.Delta) 174 | success := atomic.LoadUint64(&metric.OplogSuccess.Value) 175 | 176 | verbose := "[stage=%s, get=%d" 177 | if metric.SUBSCRIBE&METRIC_FILTER != 0 { 178 | verbose += fmt.Sprintf(", filter=%d", atomic.LoadUint64(&metric.OplogFilter.Value)) 179 | } 180 | if metric.SUBSCRIBE&METRIC_SUCCESS != 0 { 181 | verbose += fmt.Sprintf(", write_success=%d", success) 182 | } 183 | if metric.SUBSCRIBE&METRIC_TPS != 0 { 184 | verbose += fmt.Sprintf(", tps=%d", tps) 185 | } 186 | if metric.SUBSCRIBE&METRIC_CKPT_TIMES != 0 { 187 | verbose += fmt.Sprintf(", ckpt_times=%d", ckpt) 188 | } 189 | if metric.SUBSCRIBE&METRIC_TUNNEL_TRAFFIC != 0 { 190 | verbose += fmt.Sprintf(", tunnel_traffic=%s", metric.getTunnelTraffic()) 191 | } 192 | if metric.SUBSCRIBE&METRIC_FULLSYNC_WRITE != 0 { 193 | verbose += fmt.Sprintf(", fail=%d", atomic.LoadUint64(&metric.OplogWriteFail.Value)) 194 | } 195 | verbose += "]" 196 | 197 | // LOG.Info(verbose, metric.NAME, metric.STAGE, atomic.LoadUint64(&metric.OplogGet.Value)) 198 | LOG.Info(verbose, metric.STAGE, atomic.LoadUint64(&metric.OplogGet.Value)) 199 | } 200 | 201 | LOG.Info("metric[%v] exit", metric) 202 | }() 203 | } 204 | 205 | func (metric *ReplicationMetric) getTunnelTraffic() string { 206 | traffic := atomic.LoadUint64(&metric.TunnelTraffic) 207 | return GetMetricWithSize(traffic) 208 | } 209 | 210 | func (metric *ReplicationMetric) Get() uint64 { 211 | return atomic.LoadUint64(&metric.OplogGet.Value) 212 | } 213 | 214 | func (metric *ReplicationMetric) Apply() uint64 { 215 | return atomic.LoadUint64(&metric.OplogApply.Value) 216 | } 217 | 218 | func (metric *ReplicationMetric) Success() uint64 { 219 | return atomic.LoadUint64(&metric.OplogSuccess.Value) 220 | } 221 | 222 | func (metric *ReplicationMetric) Tps() uint64 { 223 | return atomic.LoadUint64(&metric.OplogSuccess.Delta) 224 | } 225 | 226 | func (metric *ReplicationMetric) AddSuccess(incr uint64) { 227 | atomic.AddUint64(&metric.OplogSuccess.Value, incr) 228 | } 229 | 230 | func (metric *ReplicationMetric) AddGet(incr uint64) { 231 | atomic.AddUint64(&metric.OplogGet.Value, incr) 232 | } 233 | 234 | func (metric *ReplicationMetric) AddCheckpoint(number uint64) { 235 | atomic.AddUint64(&metric.CheckpointTimes, number) 236 | } 237 | 238 | func (metric *ReplicationMetric) AddRetransmission(number uint64) { 239 | atomic.AddUint64(&metric.Retransmission, number) 240 | } 241 | 242 | func (metric *ReplicationMetric) AddTunnelTraffic(number uint64) { 243 | atomic.AddUint64(&metric.TunnelTraffic, number) 244 | } 245 | 246 | func (metric *ReplicationMetric) AddFilter(incr uint64) { 247 | atomic.AddUint64(&metric.OplogFilter.Value, incr) 248 | } 249 | 250 | func (metric *ReplicationMetric) AddApply(incr uint64) { 251 | atomic.AddUint64(&metric.OplogApply.Value, incr) 252 | } 253 | 254 | func (metric *ReplicationMetric) AddFailed(incr uint64) { 255 | atomic.AddUint64(&metric.OplogFail.Value, incr) 256 | } 257 | 258 | func (metric *ReplicationMetric) AddConsume(incr uint64) { 259 | atomic.AddUint64(&metric.OplogConsume.Value, incr) 260 | } 261 | 262 | func (metric *ReplicationMetric) SetOplogMax(max int64) { 263 | forwardCas(&metric.OplogMaxSize, max) 264 | } 265 | 266 | func (metric *ReplicationMetric) SetOplogAvg(size int64) { 267 | // not atomic update ! acceptable 268 | avg := (atomic.LoadInt64(&metric.OplogAvgSize) + size) / 2 269 | atomic.StoreInt64(&metric.OplogAvgSize, avg) 270 | } 271 | 272 | func (metric *ReplicationMetric) SetLSNCheckpoint(ckpt int64) { 273 | forwardCas(&metric.LSNCheckpoint, ckpt) 274 | } 275 | 276 | func (metric *ReplicationMetric) SetLSN(lsn int64) { 277 | forwardCas(&metric.LSN, lsn) 278 | } 279 | 280 | func (metric *ReplicationMetric) SetLSNACK(ack int64) { 281 | forwardCas(&metric.LSNAck, ack) 282 | } 283 | 284 | func (metric *ReplicationMetric) AddTableOps(table string, n uint64) { 285 | metric.TableOperations.Incr(table, n) 286 | } 287 | 288 | func (metric *ReplicationMetric) TableOps() map[string]uint64 { 289 | return metric.TableOperations.MakeCopy() 290 | } 291 | 292 | func (metric *ReplicationMetric) AddWriteFailed(incr uint64) { 293 | atomic.AddUint64(&metric.OplogWriteFail.Value, incr) 294 | } 295 | 296 | /************************************************************/ 297 | 298 | func forwardCas(v *int64, new int64) { 299 | var current int64 300 | for current = atomic.LoadInt64(v); new > current; { 301 | if atomic.CompareAndSwapInt64(v, current, new) { 302 | break 303 | } 304 | current = atomic.LoadInt64(v) 305 | } 306 | } 307 | 308 | func (status *ReplicationStatus) Update(s uint64) { 309 | atomic.StoreUint64((*uint64)(status), s) 310 | } 311 | 312 | // TableOps, count collection operations 313 | type TableOps struct { 314 | sync.Mutex 315 | ops map[string]uint64 316 | } 317 | 318 | func NewTableOps() *TableOps { 319 | return &TableOps{ops: make(map[string]uint64)} 320 | } 321 | 322 | func (t *TableOps) Incr(table string, n uint64) { 323 | t.Lock() 324 | defer t.Unlock() 325 | t.ops[table] += n 326 | } 327 | 328 | func (t *TableOps) MakeCopy() map[string]uint64 { 329 | t.Lock() 330 | defer t.Unlock() 331 | c := make(map[string]uint64, len(t.ops)) 332 | for k, v := range t.ops { 333 | c[k] = v 334 | } 335 | return c 336 | } 337 | 338 | func GetMetricWithSize(input interface{}) string { 339 | var val float64 340 | switch v := input.(type) { 341 | case uint64: 342 | val = float64(v) 343 | case uint32: 344 | val = float64(v) 345 | case uint16: 346 | val = float64(v) 347 | case uint: 348 | val = float64(v) 349 | case int64: 350 | val = float64(v) 351 | case int32: 352 | val = float64(v) 353 | case int16: 354 | val = float64(v) 355 | case int: 356 | val = float64(v) 357 | default: 358 | return "unknown type" 359 | } 360 | 361 | switch { 362 | case val > PB: 363 | return fmt.Sprintf("%.2fPB", val/PB) 364 | case val > TB: 365 | return fmt.Sprintf("%.2fTB", val/TB) 366 | case val > GB: 367 | return fmt.Sprintf("%.2fGB", val/GB) 368 | case val > MB: 369 | return fmt.Sprintf("%.2fMB", val/MB) 370 | case val > KB: 371 | return fmt.Sprintf("%.2fKB", val/KB) 372 | default: 373 | return fmt.Sprintf("%.2fB", val) 374 | } 375 | } -------------------------------------------------------------------------------- /nimo-shake/common/mix.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/nightlyone/lockfile" 8 | LOG "github.com/vinllen/log4go" 9 | ) 10 | 11 | func WritePid(id string) (err error) { 12 | var lock lockfile.Lockfile 13 | lock, err = lockfile.New(id) 14 | if err != nil { 15 | return err 16 | } 17 | if err = lock.TryLock(); err != nil { 18 | return err 19 | } 20 | 21 | return nil 22 | } 23 | 24 | func WritePidById(id string, path string) error { 25 | var dir string 26 | var err error 27 | if path == "" { 28 | if dir, err = os.Getwd(); err != nil { 29 | return err 30 | } 31 | } else { 32 | dir = path 33 | if _, err := os.Stat(dir); os.IsNotExist(err) { 34 | os.Mkdir(dir, os.ModePerm) 35 | } 36 | } 37 | 38 | if dir, err = filepath.Abs(dir); err != nil { 39 | return err 40 | } 41 | 42 | pidfile := filepath.Join(dir, id) + ".pid" 43 | if err := WritePid(pidfile); err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | func Welcome() { 50 | welcome := 51 | `______________________________ 52 | \ \ _ ______ | 53 | \ \ / \___-=O'/|O'/__| 54 | \ NimoShake, here we go !! \_______\ / | / ) 55 | / / '/-==__ _/__|/__=-| -GM 56 | / Alibaba Cloud / * \ | | 57 | / zhuzhao / (o) 58 | ------------------------------ 59 | ` 60 | startMsg := "if you have any problem, call aliyun" 61 | LOG.Warn("\n%s%s\n\n", welcome, startMsg) 62 | } -------------------------------------------------------------------------------- /nimo-shake/common/mongodb_community.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | LOG "github.com/vinllen/log4go" 8 | "go.mongodb.org/mongo-driver/mongo" 9 | "go.mongodb.org/mongo-driver/mongo/options" 10 | "go.mongodb.org/mongo-driver/mongo/readconcern" 11 | "go.mongodb.org/mongo-driver/mongo/readpref" 12 | ) 13 | 14 | type MongoCommunityConn struct { 15 | Client *mongo.Client 16 | URL string 17 | ctx context.Context 18 | } 19 | 20 | func NewMongoCommunityConn(url string, connectMode string, timeout bool) (*MongoCommunityConn, error) { 21 | clientOps := options.Client().ApplyURI(url) 22 | 23 | clientOps.SetReadConcern(readconcern.New(readconcern.Level("local"))) 24 | 25 | // read pref 26 | if mode, err := readpref.ModeFromString(connectMode); err != nil { 27 | return nil, fmt.Errorf("create connectMode[%v] failed: %v", connectMode, err) 28 | } else if opts, err := readpref.New(mode); err != nil { 29 | return nil, fmt.Errorf("new mode with connectMode[%v] failed: %v", connectMode, err) 30 | } else { 31 | clientOps.SetReadPreference(opts) 32 | } 33 | 34 | // set timeout 35 | if !timeout { 36 | clientOps.SetConnectTimeout(0) 37 | } 38 | 39 | // create default context 40 | ctx := context.Background() 41 | 42 | // connect 43 | client, err := mongo.NewClient(clientOps) 44 | if err != nil { 45 | return nil, fmt.Errorf("new client failed: %v", err) 46 | } 47 | if err := client.Connect(ctx); err != nil { 48 | return nil, fmt.Errorf("connect to %s failed: %v", BlockMongoUrlPassword(url, "***"), err) 49 | } 50 | 51 | // ping 52 | if err = client.Ping(ctx, clientOps.ReadPreference); err != nil { 53 | return nil, fmt.Errorf("ping to %v failed: %v", BlockMongoUrlPassword(url, "***"), err) 54 | } 55 | 56 | LOG.Info("New session to %s successfully", BlockMongoUrlPassword(url, "***")) 57 | return &MongoCommunityConn{ 58 | Client: client, 59 | URL: url, 60 | }, nil 61 | } 62 | 63 | func (conn *MongoCommunityConn) Close() { 64 | LOG.Info("Close client with %s", BlockMongoUrlPassword(conn.URL, "***")) 65 | conn.Client.Disconnect(conn.ctx) 66 | } 67 | -------------------------------------------------------------------------------- /nimo-shake/common/mongodb_mgo.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "time" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/vinllen/mgo" 9 | "github.com/vinllen/mgo/bson" 10 | LOG "github.com/vinllen/log4go" 11 | "github.com/jinzhu/copier" 12 | ) 13 | 14 | const ( 15 | ConnectModePrimary = "primary" 16 | ConnectModeSecondaryPreferred = "secondaryPreferred" 17 | ConnectModeStandalone = "standalone" 18 | 19 | OplogNS = "oplog.rs" 20 | ) 21 | 22 | var ( 23 | NotFountErr = "not found" 24 | NsNotFountErr = "ns not found" 25 | ) 26 | 27 | type NS struct { 28 | Database string 29 | Collection string 30 | } 31 | 32 | func (ns NS) Str() string { 33 | return fmt.Sprintf("%s.%s", ns.Database, ns.Collection) 34 | } 35 | 36 | type MongoConn struct { 37 | Session *mgo.Session 38 | URL string 39 | } 40 | 41 | func NewMongoConn(url string, connectMode string, timeout bool) (*MongoConn, error) { 42 | if connectMode == ConnectModeStandalone { 43 | url += "?connect=direct" 44 | } 45 | 46 | session, err := mgo.Dial(url) 47 | if err != nil { 48 | LOG.Critical("Connect to [%s] failed. %v", url, err) 49 | return nil, err 50 | } 51 | // maximum pooled connections. the overall established sockets 52 | // should be lower than this value(will block otherwise) 53 | session.SetPoolLimit(256) 54 | if timeout { 55 | session.SetSocketTimeout(10 * time.Minute) 56 | } else { 57 | session.SetSocketTimeout(0) 58 | } 59 | 60 | // already ping in the session 61 | /*if err := session.Ping(); err != nil { 62 | LOG.Critical("Verify ping command to %s failed. %v", url, err) 63 | return nil, err 64 | }*/ 65 | 66 | // Switch the session to a eventually behavior. In that case session 67 | // may read for any secondary node. default mode is mgo.Strong 68 | switch connectMode { 69 | case ConnectModePrimary: 70 | session.SetMode(mgo.Primary, true) 71 | case ConnectModeSecondaryPreferred: 72 | session.SetMode(mgo.SecondaryPreferred, true) 73 | case ConnectModeStandalone: 74 | session.SetMode(mgo.Monotonic, true) 75 | default: 76 | err = fmt.Errorf("unknown connect mode[%v]", connectMode) 77 | return nil, err 78 | } 79 | 80 | LOG.Info("New session to %s successfully", url) 81 | return &MongoConn{Session: session, URL: url}, nil 82 | } 83 | 84 | func (conn *MongoConn) Close() { 85 | LOG.Info("Close session with %s", conn.URL) 86 | conn.Session.Close() 87 | } 88 | 89 | func (conn *MongoConn) IsGood() bool { 90 | if err := conn.Session.Ping(); err != nil { 91 | return false 92 | } 93 | 94 | return true 95 | } 96 | 97 | func (conn *MongoConn) AcquireReplicaSetName() string { 98 | var replicaset struct { 99 | Id string `bson:"set"` 100 | } 101 | if err := conn.Session.DB("admin").Run(bson.M{"replSetGetStatus": 1}, &replicaset); err != nil { 102 | LOG.Warn("Replica set name not found in system.replset, %v", err) 103 | } 104 | return replicaset.Id 105 | } 106 | 107 | func (conn *MongoConn) HasOplogNs() bool { 108 | if ns, err := conn.Session.DB("local").CollectionNames(); err == nil { 109 | for _, table := range ns { 110 | if table == OplogNS { 111 | return true 112 | } 113 | } 114 | } 115 | return false 116 | } 117 | 118 | func (conn *MongoConn) HasUniqueIndex() bool { 119 | checkNs := make([]NS, 0, 128) 120 | var databases []string 121 | var err error 122 | if databases, err = conn.Session.DatabaseNames(); err != nil { 123 | LOG.Critical("Couldn't get databases from remote server %v", err) 124 | return false 125 | } 126 | 127 | for _, db := range databases { 128 | if db != "admin" && db != "local" { 129 | coll, _ := conn.Session.DB(db).CollectionNames() 130 | for _, c := range coll { 131 | if c != "system.profile" { 132 | // push all collections 133 | checkNs = append(checkNs, NS{Database: db, Collection: c}) 134 | } 135 | } 136 | } 137 | } 138 | 139 | for _, ns := range checkNs { 140 | indexes, _ := conn.Session.DB(ns.Database).C(ns.Collection).Indexes() 141 | for _, idx := range indexes { 142 | // has unique index 143 | if idx.Unique { 144 | LOG.Info("Found unique index %s on %s.%s in auto shard mode", idx.Name, ns.Database, ns.Collection) 145 | return true 146 | } 147 | } 148 | } 149 | 150 | return false 151 | } 152 | 153 | // first is from dynamo, second is from mongo 154 | func CompareBson(first, second bson.M) (bool, error) { 155 | v2 := make(bson.M, 0) 156 | if err := copier.Copy(&v2, &second); err != nil { 157 | return false, fmt.Errorf("copy[%v] failed[%v]", second, err) 158 | } 159 | 160 | if _, ok := v2["_id"]; ok { 161 | delete(v2, "_id") 162 | } 163 | 164 | return reflect.DeepEqual(first, v2), nil 165 | // return DeepEqual(first, v2), nil 166 | } 167 | -------------------------------------------------------------------------------- /nimo-shake/common/operator.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func AppendStringList(input []string, list []*string) ([]string) { 8 | for _, ele := range list { 9 | input = append(input, *ele) 10 | } 11 | return input 12 | } 13 | 14 | func StringListToMap(input []string) map[string]struct{} { 15 | mp := make(map[string]struct{}, len(input)) 16 | for _, ele := range input { 17 | mp[ele] = struct{}{} 18 | } 19 | return mp 20 | } 21 | 22 | // @vinllen. see BulkError.Error. -1 means not found 23 | func FindFirstErrorIndexAndMessage(error string) (int, string, bool) { 24 | subIndex := "index[" 25 | subMsg := "msg[" 26 | subDup := "dup[" 27 | index := strings.Index(error, subIndex) 28 | if index == -1 { 29 | return index, "", false 30 | } 31 | 32 | indexVal := 0 33 | for i := index + len(subIndex); i < len(error) && error[i] != ']'; i++ { 34 | // fmt.Printf("%c %d\n", rune(error[i]), int(error[i] - '0')) 35 | indexVal = indexVal * 10 + int(error[i] - '0') 36 | } 37 | 38 | index = strings.Index(error, subMsg) 39 | if index == -1 { 40 | return indexVal, "", false 41 | } 42 | 43 | i := index + len(subMsg) 44 | stack := 0 45 | for ; i < len(error); i++ { 46 | if error[i] == ']' { 47 | if stack == 0 { 48 | break 49 | } else { 50 | stack -= 1 51 | } 52 | } else if error[i] == '[' { 53 | stack += 1 54 | } 55 | } 56 | msg := error[index + len(subMsg): i] 57 | 58 | index = strings.Index(error, subDup) 59 | if index == -1 { 60 | return indexVal, msg, false 61 | } 62 | i = index + len(subMsg) 63 | for ; i < len(error) && error[i] != ']'; i++ {} 64 | dupVal := error[index + len(subMsg):i] 65 | 66 | return indexVal, msg, dupVal == "true" 67 | } -------------------------------------------------------------------------------- /nimo-shake/common/shard.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go/service/dynamodbstreams" 9 | ) 10 | 11 | var ( 12 | StopTraverseSonErr = fmt.Errorf("stop traverse") 13 | ) 14 | 15 | type ShardNode struct { 16 | Shard *dynamodbstreams.Shard 17 | ShardArn string 18 | Sons map[string]*ShardNode 19 | Table string 20 | } 21 | 22 | // build shard tree base on the input shard list, return root node 23 | func BuildShardTree(shards []*dynamodbstreams.Shard, table string, ShardArn string) *ShardNode { 24 | pathMap := make(map[string]*ShardNode, len(shards)) // store the inheritance relationship 25 | inDegree := make(map[string]bool, len(shards)) 26 | // initial 27 | for _, shard := range shards { 28 | pathMap[*shard.ShardId] = &ShardNode{ 29 | Shard: shard, 30 | ShardArn: ShardArn, 31 | Sons: make(map[string]*ShardNode), 32 | Table: table, 33 | } 34 | 35 | inDegree[*shard.ShardId] = false 36 | } 37 | 38 | // build path 39 | for _, shard := range shards { 40 | node := pathMap[*shard.ShardId] 41 | father := shard.ParentShardId 42 | if father != nil { 43 | if _, ok := pathMap[*father]; !ok { 44 | // father node isn't exist on the pathMap, which means deleted 45 | continue 46 | } 47 | inDegree[*shard.ShardId] = true 48 | pathMap[*father].Sons[*shard.ShardId] = node 49 | } 50 | } 51 | 52 | // root is fake node 53 | rootNode := &ShardNode{ 54 | Shard: nil, 55 | ShardArn: "", 56 | Sons: make(map[string]*ShardNode), 57 | Table: table, 58 | } 59 | for key, val := range inDegree { 60 | if val == false { 61 | rootNode.Sons[key] = pathMap[key] 62 | // fmt.Printf("root->%s\n", key) 63 | } 64 | } 65 | return rootNode 66 | } 67 | 68 | // dfs 69 | func TraverseShard(node *ShardNode, callback func(node *ShardNode) error) error { 70 | if node == nil { 71 | return nil 72 | } 73 | 74 | if node.Shard != nil { 75 | // skip root node 76 | if err := callback(node); err != nil { 77 | if err != StopTraverseSonErr { 78 | return err 79 | } else { 80 | // return if error == StopTraverseSonErr 81 | return nil 82 | } 83 | } 84 | // fmt.Printf("%s->%s\n", *node.Shard.ParentShardId, *node.Shard.ShardId) 85 | } 86 | 87 | for _, son := range node.Sons { 88 | if err := TraverseShard(son, callback); err != nil { 89 | return err 90 | } 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // calculate md5. TODO: add UT 97 | func CalMd5(root *ShardNode) uint64 { 98 | if root == nil { 99 | return 0 100 | } 101 | 102 | list := make([]string, 0, len(root.Sons)) 103 | for _, son := range root.Sons { 104 | ret := CalMd5(son) 105 | list = append(list, fmt.Sprintf("%s->%d", *son.Shard.ShardId, ret)) 106 | } 107 | 108 | sort.Strings(list) 109 | concat := strings.Join(list, ";") 110 | // fmt.Println("concat: ", concat) 111 | return Md5In64(String2Bytes(concat)) 112 | } 113 | 114 | func PrintShardTree(node *ShardNode) (string, error) { 115 | newSet := make([]string, 0) 116 | err := TraverseShard(node, func(node *ShardNode) error { 117 | var father string 118 | if node.Shard.ParentShardId != nil { 119 | father = *node.Shard.ParentShardId 120 | } else { 121 | father = "nil" 122 | } 123 | inheritance := strings.Join([]string{father, *node.Shard.ShardId}, "->") 124 | newSet = append(newSet, inheritance) 125 | 126 | return nil 127 | }) 128 | if err != nil { 129 | return "", err 130 | } 131 | 132 | return strings.Join(newSet, "\n"), nil 133 | } -------------------------------------------------------------------------------- /nimo-shake/common/unsafe.go: -------------------------------------------------------------------------------- 1 | /* 2 | // ===================================================================================== 3 | // 4 | // Filename: BytesString.go 5 | // 6 | // Description: ref from fast http 7 | // 8 | // Version: 1.0 9 | // Created: 06/23/2018 02:34:41 PM 10 | // Revision: none 11 | // Compiler: go1.10.3 12 | // 13 | // Author: boyi.gw, boyi.gw@alibaba-inc.com 14 | // Company: Alibaba Group 15 | // 16 | // ===================================================================================== 17 | */ 18 | 19 | package utils 20 | 21 | import ( 22 | "reflect" 23 | "unsafe" 24 | ) 25 | 26 | /* 27 | // === FUNCTION ====================================================================== 28 | // Name: String2Bytes 29 | // Description: return GoString's buffer slice(enable modify string) 30 | // ===================================================================================== 31 | */ 32 | func String2Bytes(s string) []byte { 33 | sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) 34 | bh := reflect.SliceHeader{ 35 | Data: sh.Data, 36 | Len: sh.Len, 37 | Cap: sh.Len, 38 | } 39 | return *(*[]byte)(unsafe.Pointer(&bh)) 40 | } 41 | 42 | /* 43 | // === FUNCTION ====================================================================== 44 | // Name: Bytes2String 45 | // Description: convert b to string without copy 46 | // ===================================================================================== 47 | */ 48 | func Bytes2String(b []byte) string { 49 | return *(*string)(unsafe.Pointer(&b)) 50 | } 51 | 52 | /* 53 | // === FUNCTION ====================================================================== 54 | // Name: StringPointer 55 | // Description: returns &s[0] 56 | // ===================================================================================== 57 | */ 58 | func StringPointer(s string) unsafe.Pointer { 59 | p := (*reflect.StringHeader)(unsafe.Pointer(&s)) 60 | return unsafe.Pointer(p.Data) 61 | } 62 | 63 | /* 64 | // === FUNCTION ====================================================================== 65 | // Name: BytesPointer 66 | // Description: returns &b[0] 67 | // ===================================================================================== 68 | */ 69 | func BytesPointer(b []byte) unsafe.Pointer { 70 | p := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 71 | return unsafe.Pointer(p.Data) 72 | } 73 | -------------------------------------------------------------------------------- /nimo-shake/conf/nimo-shake.conf: -------------------------------------------------------------------------------- 1 | # current configuration version, do not modify. 2 | # 当前配置文件的版本号,请不要修改该值。 3 | conf.version = 3 4 | 5 | # id 6 | id = nimo-shake 7 | 8 | # log file name,all log will be printed in stdout if log.file is empty 9 | # 日志文件,不配置将打印到stdout (e.g. dynamo-shake.log ) 10 | log.file = 11 | # log level: "none", "error", "warn", "info", "debug". default is "info". 12 | log.level = info 13 | # log buffer,enabling it to make every log print but performance maybe decrease. 14 | # log buffer选项,不启用将会降低性能但保证退出时每条log都被打印,否则,退出时可能有最后几条log丢失 15 | log.buffer = true 16 | 17 | # pprof port. 18 | system_profile = 9330 19 | # restful port, not used currently. 查看metric 20 | # 全量和增量的restful监控端口,可以用curl查看内部监控metric统计情况。详见wiki。 21 | full_sync.http_port = 9341 22 | incr_sync.http_port = 9340 23 | 24 | # sync mode. currently, only support "full". 25 | # all: full sync and increase sync. 26 | # full: full sync only. 27 | # incr: increase sync only. 28 | # 同步的类型。 29 | # all: 全量+增量同步 30 | # full: 全量同步 31 | # incr: 增量同步 32 | sync_mode = all 33 | 34 | # incr sync parallel 35 | # 是否并行做增量同步: true为开启,false为关闭。开启后会消耗更多内存 36 | incr_sync_parallel = false 37 | 38 | # dynamodb configuration. leave empty if not set. 39 | # 源端dynamodb的账号信息配置,source.session_token和source.region没有可以留空 40 | source.access_key_id = 41 | source.secret_access_key = 42 | source.session_token = 43 | source.region = 44 | # 源端如果是endpoint类型,可以配置该参数,启用该参数则上述source参数失效 45 | # 例如:http://100.123.124.125:1010 46 | source.endpoint_url = 47 | # max_retries in session once failed 48 | source.session.max_retries = 3 49 | # session timeout, 0 means disable. unit: ms. 50 | source.session.timeout = 3000 51 | 52 | # filter collection split by semicolon(;). at most one of these two parameters can be given. 53 | # if the filter.collection.black is not empty, the given collection will be filtered while others collection passed. 54 | # if the filter.collection.white is not empty, the given collection will be passed while others collection filtered. 55 | # all the namespace will be passed if no condition given. 56 | # E.g., "filter.collection.white = c1;c2" means only c1 and c2 passed while the others filtered. 57 | # 表粒度黑白名单过滤,白名单表示通过,黑名单表示过滤,这两个参数不能同时指定,都指定表示全部通过。分号分隔不同表。 58 | # 举例:"filter.collection.white = c1;c2"表示c1和c2表通过,剩下的都被过滤掉。 59 | filter.collection.white = 60 | filter.collection.black = 61 | 62 | # qps limit for each table. 63 | # 对表级别限速 64 | # the scan call(Scan) per second. 65 | # 全量阶段,我们调用的是scan命令,这个参数表示一秒钟最多调用的scan个数 66 | qps.full = 1000 67 | # the limit batch number in one query. default is 128. 68 | # 1次query内部的条数大小 69 | qps.full.batch_num = 128 70 | # the query call(GetRecords) per second. 71 | # 增量阶段,我们调用的是GetRecords命令,这个参数表示一秒钟最多调用的GetRecords个数 72 | qps.incr = 1000 73 | # the limit batch number in one query. default is 128. 74 | # 1次query内部的条数大小 75 | qps.incr.batch_num = 128 76 | 77 | # target mongodb configuration, currently, only supports sync to mongodb. 78 | # 目的端配置, 目前支持mongodb和aliyun_dynamo_proxy 79 | target.type = mongodb 80 | # target mongodb address, e.g., mongodb://username:password@10.1.1.1:3791,10.1.1.2:3792 81 | # 如果是mongodb,此处需要配置目的mongodb的连接串,否则请配置阿里云dynamodb的连接串 82 | # 例如:http://dds-xxxx:3717 83 | target.address = 84 | # target moongodb type, replica or sharding. 85 | # 目的mongodb类型, 副本集选择replica,分片集群请选择sharding 86 | target.mongodb.type = sharding 87 | # how to solve if target mongodb has the same name table. 88 | # "drop" means drop the table before syncing. 89 | # "rename" means rename current table which timestamp suffix, e.g., c1 -> c1.2019-07-01Z12:10:11 90 | # rename仅支持target.type=mongodb的情况 91 | # 如果目的端已经有重名的表,rename将会对原来的表进行重命名,添加 92 | # 时间戳后缀,比如c1变为c1.2019-07-01Z12:10:11;drop表示删除目的表;留空表示不处理。 93 | target.db.exist = drop 94 | 95 | # only sync schema without any data. 96 | # 仅同步数据结构,不同步任何数据。 97 | sync_schema_only = false 98 | 99 | # full sync configuration. 100 | # 全量同步参数 101 | # how many tables will be synced at the same time. 102 | # 表级别并发度,1次最多同步表的数目 103 | full.concurrency = 4 104 | # how many reading threads working in one table. 105 | # 表内文档的并发度,1个表最多有几个线程同时并发读取源端,对应Scan接口的TotalSegments 106 | full.read.concurrency = 1 107 | # how many writing threads working in one table. 108 | # 表内文档的并发度,1个表最多有几个线程同时并发写入目的端 109 | full.document.concurrency = 4 110 | # how many doc batched in one writing request 111 | # 一次聚合写入多少条数据,如果目的端是DynamoDB协议最大配置25 112 | full.document.write.batch = 25 113 | # the number of parsers which do parse dynamodb to mongodb. 114 | # 表内解析线程个数,用户转换dynamo协议到目的端对应协议 115 | full.document.parser = 2 116 | # enable sync user created indexes? 117 | # 主键索引primaryKey会默认创建索引,除此以外是否同步用户自建的索引, 118 | full.enable_index.user = true 119 | # change insert to update when duplicate key error found 120 | # 全量同步,目的端碰到相同的key,是否将insert改为update 121 | full.executor.insert_on_dup_update = true 122 | 123 | # increase sync configuration. 124 | # 增量同步参数 125 | 126 | # 增量同步并发参数,1个表最多有几个线程来读取这个表的shard数据,1个线程同时抓取一个shard 127 | increase.concurrency = 16 128 | # 增量同步,insert语句目的端碰到相同的key,是否将insert改为update 129 | increase.executor.insert_on_dup_update = true 130 | # 增量同步,update语句目的端不存在key,是否将update改为upsert 131 | increase.executor.upsert = true 132 | 133 | # checkpoint存储的类型,可以是mongodb(target.type必须是mongodb),也可以是 134 | # file,本地落盘 135 | checkpoint.type = mongodb 136 | # checkpoint存储的地址,如果是目的端是mongodb则存储目的端的mongodb的地址,不配置默认目的mongodb 137 | # 如果是file则填写相对路径(例如checkpoint),不配置默认名为checkpoint 138 | checkpoint.address = 139 | # checkpoint存储在目的mongodb中的db名,默认以"$id-checkpoint"存储 140 | checkpoint.db = 141 | 142 | # 给DynamoDB中的_id字段增加前缀,不和MongoDB的_id冲突 143 | convert._id = pre 144 | 145 | # :begin 和 :end,这两个冒号开头的是变量,实际的值在 filter_attributevalues 中 146 | # begin:N:1646724207280,begin 是变量名,N 为类型,1646724207280 为值内容 147 | # N 为 Number, S 为 String 148 | full.read.filter_expression = create_time > :begin AND create_time < :end 149 | full.read.filter_attributevalues = begin```N```1646724207280~~~end```N```1646724207283 -------------------------------------------------------------------------------- /nimo-shake/configure/check.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "bufio" 5 | "strings" 6 | "strconv" 7 | "fmt" 8 | "os" 9 | 10 | "nimo-shake/common" 11 | ) 12 | 13 | // read the given file and parse the fcv do comparison 14 | func CheckFcv(file string, fcv int) (int, error) { 15 | // read line by line and parse the version 16 | 17 | f, err := os.Open(file) 18 | if err != nil { 19 | return -1, err 20 | } 21 | 22 | scanner := bufio.NewScanner(f) 23 | versionName := "conf.version" 24 | version := 0 25 | for scanner.Scan() { 26 | field := strings.Split(scanner.Text(), "=") 27 | if len(field) >= 2 && strings.HasPrefix(field[0], versionName) { 28 | if value, err := strconv.Atoi(strings.Trim(field[1], " ")); err != nil { 29 | return 0, fmt.Errorf("illegal value[%v]", field[1]) 30 | } else { 31 | version = value 32 | break 33 | } 34 | } 35 | } 36 | 37 | if version < fcv { 38 | return version, fmt.Errorf("current required configuration version[%v] > input[%v], please upgrade NimoShake to version >= %v", 39 | fcv, version, utils.LowestConfigurationVersion[fcv]) 40 | } 41 | return version, nil 42 | } -------------------------------------------------------------------------------- /nimo-shake/configure/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | type Configuration struct { 4 | ConfVersion uint `config:"conf.version"` // do not modify the tag name 5 | Id string `config:"id"` 6 | LogFile string `config:"log.file"` 7 | LogLevel string `config:"log.level"` 8 | LogBuffer bool `config:"log.buffer"` 9 | PidPath string `config:"pid_path"` 10 | SystemProfile int `config:"system_profile"` 11 | FullSyncHTTPListenPort int `config:"full_sync.http_port"` 12 | IncrSyncHTTPListenPort int `config:"incr_sync.http_port"` 13 | SyncMode string `config:"sync_mode"` 14 | SourceAccessKeyID string `config:"source.access_key_id"` 15 | SourceSecretAccessKey string `config:"source.secret_access_key"` 16 | SourceSessionToken string `config:"source.session_token"` 17 | SourceRegion string `config:"source.region"` 18 | SourceEndpointUrl string `config:"source.endpoint_url"` 19 | SourceSessionMaxRetries uint `config:"source.session.max_retries"` 20 | SourceSessionTimeout uint `config:"source.session.timeout"` 21 | QpsFull uint `config:"qps.full"` 22 | QpsFullBatchNum int64 `config:"qps.full.batch_num"` 23 | QpsIncr uint `config:"qps.incr"` 24 | QpsIncrBatchNum int64 `config:"qps.incr.batch_num"` 25 | FilterCollectionWhite string `config:"filter.collection.white"` 26 | FilterCollectionBlack string `config:"filter.collection.black"` 27 | TargetType string `config:"target.type"` 28 | TargetAddress string `config:"target.address"` 29 | TargetMongoDBType string `config:"target.mongodb.type"` 30 | TargetDBExist string `config:"target.db.exist"` 31 | SyncSchemaOnly bool `config:"sync_schema_only"` 32 | FullConcurrency uint `config:"full.concurrency"` 33 | FullReadConcurrency uint `config:"full.read.concurrency"` 34 | FullDocumentConcurrency uint `config:"full.document.concurrency"` 35 | FullDocumentWriteBatch uint `config:"full.document.write.batch"` 36 | FullDocumentParser uint `config:"full.document.parser"` 37 | FullEnableIndexPrimary bool `config:"full.enable_index.primary"` 38 | FullEnableIndexUser bool `config:"full.enable_index.user"` 39 | FullExecutorInsertOnDupUpdate bool `config:"full.executor.insert_on_dup_update"` 40 | FullFilterExpression string `config:"full.read.filter_expression"` 41 | FullFilterAttributeValues string `config:"full.read.filter_attributevalues"` 42 | ConvertType string `config:"convert.type"` 43 | ConvertId string `config:"convert._id"` 44 | IncreaseConcurrency uint `config:"increase.concurrency"` 45 | IncreaseExecutorInsertOnDupUpdate bool `config:"increase.executor.insert_on_dup_update"` 46 | IncreaseExecutorUpsert bool `config:"increase.executor.upsert"` 47 | IncreasePersistDir string `config:"increase.persist.dir"` 48 | CheckpointType string `config:"checkpoint.type"` 49 | CheckpointAddress string `config:"checkpoint.address"` 50 | CheckpointDb string `config:"checkpoint.db"` 51 | IncrSyncParallel bool `config:"incr_sync_parallel"` 52 | 53 | /*---------------------------------------------------------*/ 54 | // generated variables 55 | Version string // version 56 | } 57 | 58 | var Options Configuration 59 | 60 | func ConvertIdFunc(name string) string { 61 | if Options.ConvertId != "" && name == "_id" { 62 | return Options.ConvertId + name 63 | } 64 | 65 | return name 66 | } 67 | -------------------------------------------------------------------------------- /nimo-shake/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const ( 8 | Sep = ";" 9 | ) 10 | 11 | var ( 12 | f = &Filter{ 13 | collectionWhiteMap: make(map[string]bool), 14 | collectionBlackMap: make(map[string]bool), 15 | } 16 | ) 17 | 18 | type Filter struct { 19 | collectionWhiteMap map[string]bool 20 | collectionBlackMap map[string]bool 21 | } 22 | 23 | func Init(collectionWhite, collectBlack string) { 24 | var collectionWhiteList, collectionBlackList []string 25 | if collectionWhite != "" { 26 | collectionWhiteList = strings.Split(collectionWhite, Sep) 27 | } 28 | if collectBlack != "" { 29 | collectionBlackList = strings.Split(collectBlack, Sep) 30 | } 31 | 32 | for _, ele := range collectionWhiteList { 33 | f.collectionWhiteMap[ele] = true 34 | } 35 | for _, ele := range collectionBlackList { 36 | f.collectionBlackMap[ele] = true 37 | } 38 | } 39 | 40 | func IsFilter(collection string) bool { 41 | // fmt.Println(f.collectionWhiteMap, collection, f.collectionWhiteMap[collection]) 42 | if len(f.collectionWhiteMap) != 0 { 43 | return !f.collectionWhiteMap[collection] 44 | } else if len(f.collectionBlackMap) != 0 { 45 | return f.collectionBlackMap[collection] 46 | } 47 | return false 48 | } 49 | 50 | func FilterList(collectionList []string) []string { 51 | ret := make([]string, 0, len(collectionList)) 52 | for _, ele := range collectionList { 53 | if !IsFilter(ele) { 54 | ret = append(ret, ele) 55 | } 56 | } 57 | return ret 58 | } -------------------------------------------------------------------------------- /nimo-shake/filter/filter_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "testing" 5 | "fmt" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFilter(t *testing.T) { 11 | // test InsertCkpt, UpdateCkpt, QueryCkpt, DropCheckpoint 12 | 13 | var nr int 14 | { 15 | fmt.Printf("TestCheckpointCRUD case %d.\n", nr) 16 | nr++ 17 | 18 | f = &Filter{ 19 | collectionWhiteMap: make(map[string]bool), 20 | collectionBlackMap: make(map[string]bool), 21 | } 22 | Init("abc;efg;a", "") 23 | 24 | assert.Equal(t, true, IsFilter("anc"), "should be equal") 25 | assert.Equal(t, true, IsFilter("ab"), "should be equal") 26 | assert.Equal(t, false, IsFilter("abc"), "should be equal") 27 | assert.Equal(t, false, IsFilter("efg"), "should be equal") 28 | } 29 | 30 | { 31 | fmt.Printf("TestCheckpointCRUD case %d.\n", nr) 32 | nr++ 33 | 34 | f = &Filter{ 35 | collectionWhiteMap: make(map[string]bool), 36 | collectionBlackMap: make(map[string]bool), 37 | } 38 | Init("","abc;efg;a") 39 | 40 | assert.Equal(t, false, IsFilter("anc"), "should be equal") 41 | assert.Equal(t, false, IsFilter("ab"), "should be equal") 42 | assert.Equal(t, true, IsFilter("abc"), "should be equal") 43 | assert.Equal(t, true, IsFilter("efg"), "should be equal") 44 | } 45 | 46 | { 47 | fmt.Printf("TestCheckpointCRUD case %d.\n", nr) 48 | nr++ 49 | 50 | f = &Filter{ 51 | collectionWhiteMap: make(map[string]bool), 52 | collectionBlackMap: make(map[string]bool), 53 | } 54 | Init("","") 55 | 56 | assert.Equal(t, false, IsFilter("anc"), "should be equal") 57 | assert.Equal(t, false, IsFilter("ab"), "should be equal") 58 | assert.Equal(t, false, IsFilter("abc"), "should be equal") 59 | assert.Equal(t, false, IsFilter("efg"), "should be equal") 60 | } 61 | } -------------------------------------------------------------------------------- /nimo-shake/full-sync/document-syncer.go: -------------------------------------------------------------------------------- 1 | package full_sync 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "nimo-shake/common" 8 | "nimo-shake/configure" 9 | "nimo-shake/writer" 10 | 11 | "github.com/aws/aws-sdk-go/service/dynamodb" 12 | LOG "github.com/vinllen/log4go" 13 | "nimo-shake/protocal" 14 | "sync/atomic" 15 | ) 16 | 17 | const ( 18 | batchSize = 2 * utils.MB // mongodb limit: 16MB 19 | batchTimeout = 1 // seconds 20 | ) 21 | 22 | var ( 23 | UT_TestDocumentSyncer = false 24 | UT_TestDocumentSyncer_Chan chan []interface{} 25 | ) 26 | 27 | /*------------------------------------------------------*/ 28 | // one document link corresponding to one documentSyncer 29 | type documentSyncer struct { 30 | tableSyncerId int 31 | id int // documentSyncer id 32 | ns utils.NS 33 | inputChan chan interface{} // parserChan in table-syncer 34 | writer writer.Writer 35 | collectionMetric *utils.CollectionMetric 36 | } 37 | 38 | func NewDocumentSyncer(tableSyncerId int, table string, id int, inputChan chan interface{}, 39 | tableDescribe *dynamodb.TableDescription, collectionMetric *utils.CollectionMetric) *documentSyncer { 40 | ns := utils.NS{ 41 | Database: conf.Options.Id, 42 | Collection: table, 43 | } 44 | 45 | w := writer.NewWriter(conf.Options.TargetType, conf.Options.TargetAddress, ns, conf.Options.LogLevel) 46 | if w == nil { 47 | LOG.Crashf("tableSyncer[%v] documentSyncer[%v] create writer failed", tableSyncerId, table) 48 | } 49 | 50 | w.PassTableDesc(tableDescribe) 51 | 52 | return &documentSyncer{ 53 | tableSyncerId: tableSyncerId, 54 | id: id, 55 | inputChan: inputChan, 56 | writer: w, 57 | ns: ns, 58 | collectionMetric: collectionMetric, 59 | } 60 | } 61 | 62 | func (ds *documentSyncer) String() string { 63 | return fmt.Sprintf("tableSyncer[%v] documentSyncer[%v] ns[%v]", ds.tableSyncerId, ds.id, ds.ns) 64 | } 65 | 66 | func (ds *documentSyncer) Close() { 67 | ds.writer.Close() 68 | } 69 | 70 | func (ds *documentSyncer) Run() { 71 | batchNumber := int(conf.Options.FullDocumentWriteBatch) 72 | LOG.Info("%s start with batchSize[%v]", ds.String(), batchNumber) 73 | 74 | var data interface{} 75 | var ok bool 76 | batchGroup := make([]interface{}, 0, batchNumber) 77 | timeout := false 78 | batchGroupSize := 0 79 | exit := false 80 | for { 81 | StartT := time.Now() 82 | select { 83 | case data, ok = <-ds.inputChan: 84 | if !ok { 85 | exit = true 86 | LOG.Info("%s channel already closed, flushing cache and exiting...", ds.String()) 87 | } 88 | case <-time.After(time.Second * batchTimeout): 89 | // timeout 90 | timeout = true 91 | data = nil 92 | } 93 | readParserChanDuration := time.Since(StartT) 94 | 95 | LOG.Debug("exit[%v], timeout[%v], len(batchGroup)[%v], batchGroupSize[%v], data[%v]", exit, timeout, 96 | len(batchGroup), batchGroupSize, data) 97 | 98 | if data != nil { 99 | if UT_TestDocumentSyncer { 100 | batchGroup = append(batchGroup, data) 101 | } else { 102 | switch v := data.(type) { 103 | case protocal.RawData: 104 | if v.Size > 0 { 105 | batchGroup = append(batchGroup, v.Data) 106 | batchGroupSize += v.Size 107 | } 108 | case map[string]*dynamodb.AttributeValue: 109 | batchGroup = append(batchGroup, v) 110 | // meaningless batchGroupSize 111 | } 112 | } 113 | } 114 | 115 | if exit || timeout || len(batchGroup) >= batchNumber || batchGroupSize >= batchSize { 116 | StartT = time.Now() 117 | batchGroupLen := len(batchGroup) 118 | if len(batchGroup) != 0 { 119 | if err := ds.write(batchGroup); err != nil { 120 | LOG.Crashf("%s write data failed[%v]", ds.String(), err) 121 | } 122 | 123 | batchGroup = make([]interface{}, 0, batchNumber) 124 | batchGroupSize = 0 125 | } 126 | writeDestDBDuration := time.Since(StartT) 127 | LOG.Info("%s write db batch[%v] parserChan.len[%v] readParserChanTime[%v] writeDestDbTime[%v]", 128 | ds.String(), batchGroupLen, len(ds.inputChan), readParserChanDuration, writeDestDBDuration) 129 | 130 | if exit { 131 | break 132 | } 133 | timeout = false 134 | } 135 | } 136 | 137 | go func() { 138 | <-time.NewTimer(time.Minute * 5).C 139 | ds.writer.Close() 140 | LOG.Info("%s full-sync writer close", ds.String()) 141 | }() 142 | LOG.Info("%s finish writing", ds.String()) 143 | } 144 | 145 | func (ds *documentSyncer) write(input []interface{}) error { 146 | LOG.Debug("%s writing data with length[%v]", ds.String(), len(input)) 147 | if len(input) == 0 { 148 | return nil 149 | } 150 | 151 | if UT_TestDocumentSyncer { 152 | UT_TestDocumentSyncer_Chan <- input 153 | return nil 154 | } 155 | 156 | defer atomic.AddUint64(&ds.collectionMetric.FinishCount, uint64(len(input))) 157 | return ds.writer.WriteBulk(input) 158 | } 159 | -------------------------------------------------------------------------------- /nimo-shake/full-sync/document-syncer_test.go: -------------------------------------------------------------------------------- 1 | package full_sync 2 | 3 | import ( 4 | "testing" 5 | "fmt" 6 | "github.com/stretchr/testify/assert" 7 | "time" 8 | "nimo-shake/configure" 9 | ) 10 | 11 | func TestDocumentSyncer(t *testing.T) { 12 | // test documentSyncer main function 13 | 14 | conf.Options.FullDocumentWriteBatch = 25 15 | batchNumber := int(conf.Options.FullDocumentWriteBatch) 16 | 17 | var nr int 18 | { 19 | fmt.Printf("TestDocumentSyncer case %d.\n", nr) 20 | nr++ 21 | 22 | UT_TestDocumentSyncer = true 23 | UT_TestDocumentSyncer_Chan = make(chan []interface{}, 1000) 24 | 25 | docSyncer := &documentSyncer{ 26 | inputChan: make(chan interface{}, 10), 27 | } 28 | go docSyncer.Run() 29 | 30 | for i := 0; i < batchNumber - 5; i++ { 31 | docSyncer.inputChan<-i 32 | } 33 | 34 | out := <-UT_TestDocumentSyncer_Chan 35 | assert.Equal(t, batchNumber - 5, len(out), "should be equal") 36 | for i := 0; i < len(out); i++ { 37 | assert.Equal(t, i, out[i].(int), "should be equal") 38 | } 39 | } 40 | 41 | // output length > batchNumber 42 | { 43 | fmt.Printf("TestDocumentSyncer case %d.\n", nr) 44 | nr++ 45 | 46 | UT_TestDocumentSyncer = true 47 | UT_TestDocumentSyncer_Chan = make(chan []interface{}, 1000) 48 | 49 | docSyncer := &documentSyncer{ 50 | inputChan: make(chan interface{}, 10), 51 | } 52 | go docSyncer.Run() 53 | 54 | for i := 0; i < batchNumber + 5; i++ { 55 | docSyncer.inputChan<-i 56 | } 57 | 58 | out := make([]interface{}, 0) 59 | out1 := <-UT_TestDocumentSyncer_Chan 60 | assert.Equal(t, batchNumber, len(out1), "should be equal") 61 | out = append(out, out1...) 62 | 63 | out2 := <-UT_TestDocumentSyncer_Chan 64 | assert.Equal(t, 5, len(out2), "should be equal") 65 | out = append(out, out2...) 66 | 67 | for i := 0; i < len(out); i++ { 68 | assert.Equal(t, i, out[i].(int), "should be equal") 69 | } 70 | } 71 | 72 | // output timeout 73 | { 74 | fmt.Printf("TestDocumentSyncer case %d.\n", nr) 75 | nr++ 76 | 77 | UT_TestDocumentSyncer = true 78 | UT_TestDocumentSyncer_Chan = make(chan []interface{}, 1000) 79 | 80 | docSyncer := &documentSyncer{ 81 | inputChan: make(chan interface{}, 10), 82 | } 83 | go docSyncer.Run() 84 | 85 | for i := 0; i < batchNumber - 10; i++ { 86 | docSyncer.inputChan<-i 87 | } 88 | time.Sleep((batchTimeout + 2) * time.Second) 89 | for i := batchNumber - 10; i < batchNumber + 5; i++ { 90 | docSyncer.inputChan<-i 91 | } 92 | 93 | out := make([]interface{}, 0) 94 | out1 := <-UT_TestDocumentSyncer_Chan 95 | assert.Equal(t, batchNumber - 10, len(out1), "should be equal") 96 | out = append(out, out1...) 97 | 98 | out2 := <-UT_TestDocumentSyncer_Chan 99 | assert.Equal(t, 15, len(out2), "should be equal") 100 | out = append(out, out2...) 101 | fmt.Println(out) 102 | 103 | for i := 0; i < len(out); i++ { 104 | assert.Equal(t, i, out[i].(int), "should be equal") 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /nimo-shake/full-sync/syncer.go: -------------------------------------------------------------------------------- 1 | package full_sync 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | utils "nimo-shake/common" 9 | conf "nimo-shake/configure" 10 | "nimo-shake/filter" 11 | "nimo-shake/writer" 12 | 13 | "time" 14 | 15 | "github.com/aws/aws-sdk-go/aws" 16 | "github.com/aws/aws-sdk-go/service/dynamodb" 17 | nimo "github.com/gugemichael/nimo4go" 18 | LOG "github.com/vinllen/log4go" 19 | bson2 "go.mongodb.org/mongo-driver/bson" 20 | "go.mongodb.org/mongo-driver/mongo" 21 | ) 22 | 23 | var ( 24 | metricNsMapLock sync.Mutex 25 | metricNsMap = make(map[string]*utils.CollectionMetric) // namespace map: db.collection -> collection metric 26 | ) 27 | 28 | func Start(dynamoSession *dynamodb.DynamoDB, w writer.Writer) { 29 | // fetch all tables 30 | LOG.Info("start fetching table list") 31 | tableList, err := utils.FetchTableList(dynamoSession) 32 | if err != nil { 33 | LOG.Crashf("fetch table list failed[%v]", err) 34 | } 35 | LOG.Info("finish fetching table list: %v", tableList) 36 | 37 | tableList = filter.FilterList(tableList) 38 | 39 | if err := checkTableExists(tableList, w); err != nil { 40 | if !strings.Contains(err.Error(), "ResourceNotFoundException") { 41 | LOG.Crashf("check table exists failed[%v]", err) 42 | return 43 | } 44 | } 45 | 46 | LOG.Info("start syncing: %v", tableList) 47 | 48 | metricNsMapLock.Lock() 49 | for _, table := range tableList { 50 | metricNsMap[table] = utils.NewCollectionMetric() 51 | } 52 | metricNsMapLock.Unlock() 53 | 54 | fullChan := make(chan string, len(tableList)) 55 | for _, table := range tableList { 56 | fullChan <- table 57 | } 58 | 59 | var wg sync.WaitGroup 60 | wg.Add(len(tableList)) 61 | for i := 0; i < int(conf.Options.FullConcurrency); i++ { 62 | go func(id int) { 63 | for { 64 | table, ok := <-fullChan 65 | if !ok { 66 | // chan closed 67 | break 68 | } 69 | 70 | // no need to lock map because the map size won't change 71 | ts := NewTableSyncer(id, table, metricNsMap[table]) 72 | if ts == nil { 73 | LOG.Crashf("tableSyncer[%v] create failed", id) 74 | } 75 | 76 | LOG.Info("tableSyncer[%v] starts sync table[%v]", id, table) 77 | ts.Sync() 78 | LOG.Info("tableSyncer[%v] finish sync table[%v]", id, table) 79 | ts.Close() 80 | 81 | wg.Done() 82 | } 83 | }(i) 84 | } 85 | 86 | wg.Wait() 87 | close(fullChan) 88 | 89 | LOG.Info("finish syncing all tables and indexes!") 90 | } 91 | 92 | func checkTableExists(tableList []string, w writer.Writer) error { 93 | LOG.Info("target.db.exist is set[%v]", conf.Options.TargetDBExist) 94 | switch conf.Options.TargetType { 95 | case utils.TargetTypeMongo: 96 | 97 | sess := w.GetSession().(*mongo.Client) 98 | 99 | now := time.Now().Format(utils.GolangSecurityTime) 100 | collections, err := sess.Database(conf.Options.Id).ListCollectionNames(nil, bson2.M{}) 101 | if err != nil { 102 | return fmt.Errorf("get target collection names error[%v]", err) 103 | } 104 | 105 | collectionsMp := utils.StringListToMap(collections) 106 | for _, table := range tableList { 107 | // check exist on the target mongodb 108 | if _, ok := collectionsMp[table]; ok { 109 | // exist 110 | LOG.Info("table[%v] exists", table) 111 | if conf.Options.TargetDBExist == utils.TargetDBExistDrop { 112 | if err := sess.Database(conf.Options.Id).Collection(table).Drop(nil); err != nil { 113 | return fmt.Errorf("drop target collection[%v] failed[%v]", table, err) 114 | } 115 | } else if conf.Options.TargetDBExist == utils.TargetDBExistRename { 116 | fromCollection := fmt.Sprintf("%s.%s", conf.Options.Id, table) 117 | toCollection := fmt.Sprintf("%s.%s_%v", conf.Options.Id, table, now) 118 | res := sess.Database("admin").RunCommand(nil, bson2.D{ 119 | {"renameCollection", fromCollection}, 120 | {"to", toCollection}, 121 | {"dropTarget", false}, 122 | }) 123 | if err := res.Err(); err != nil { 124 | return fmt.Errorf("rename target collection[%v] failed[%v]", table, err) 125 | } 126 | } else { 127 | //return fmt.Errorf("collection[%v] exists on the target", table) 128 | } 129 | } 130 | } 131 | case utils.TargetTypeAliyunDynamoProxy: 132 | sess := w.GetSession().(*dynamodb.DynamoDB) 133 | 134 | // query table list 135 | collections := make([]string, 0, 16) 136 | 137 | // dynamo-proxy is not support Limit and ExclusiveStartTableName 138 | /*lastTableName := aws.String("") 139 | var count int64 = 100 140 | for i := 0; ; i++ { 141 | LOG.Debug("list table round[%v]", i) 142 | var input *dynamodb.ListTablesInput 143 | if i == 0 { 144 | input = &dynamodb.ListTablesInput{ 145 | Limit: aws.Int64(count), 146 | } 147 | } else { 148 | input = &dynamodb.ListTablesInput{ 149 | ExclusiveStartTableName: lastTableName, 150 | Limit: aws.Int64(count), 151 | } 152 | } 153 | out, err := sess.ListTables(input) 154 | if err != nil { 155 | return fmt.Errorf("list table failed: %v", err) 156 | } 157 | 158 | for _, collection := range out.TableNames { 159 | collections = append(collections, *collection) 160 | } 161 | 162 | lastTableName = out.LastEvaluatedTableName 163 | if len(out.TableNames) < int(count) { 164 | break 165 | } 166 | }*/ 167 | out, err := sess.ListTables(&dynamodb.ListTablesInput{}) 168 | if err != nil { 169 | return fmt.Errorf("list table failed: %v", err) 170 | } 171 | for _, collection := range out.TableNames { 172 | collections = append(collections, *collection) 173 | } 174 | 175 | collectionsMp := utils.StringListToMap(collections) 176 | LOG.Info("target exit db list: %v", collections) 177 | for _, table := range tableList { 178 | // check exist on the target 179 | if _, ok := collectionsMp[table]; ok { 180 | // exist 181 | LOG.Info("table[%v] exists, try [%v]", table, conf.Options.TargetDBExist) 182 | if conf.Options.TargetDBExist == utils.TargetDBExistDrop { 183 | if _, err := sess.DeleteTable(&dynamodb.DeleteTableInput{ 184 | TableName: aws.String(table), 185 | }); err != nil { 186 | return fmt.Errorf("drop target collection[%v] failed[%v]", table, err) 187 | } 188 | } else { 189 | return fmt.Errorf("collection[%v] exists on the target", table) 190 | } 191 | } 192 | } 193 | } 194 | 195 | LOG.Info("finish handling table exists") 196 | 197 | return nil 198 | } 199 | 200 | func RestAPI() { 201 | type FullSyncInfo struct { 202 | Progress string `json:"progress"` // synced_collection_number / total_collection_number 203 | TotalCollection int `json:"total_collection_number"` // total collection 204 | FinishedCollection int `json:"finished_collection_number"` // finished 205 | ProcessingCollection int `json:"processing_collection_number"` // in processing 206 | WaitCollection int `json:"wait_collection_number"` // wait start 207 | CollectionMetric map[string]string `json:"collection_metric"` // collection_name -> process 208 | } 209 | 210 | utils.FullSyncHttpApi.RegisterAPI("/progress", nimo.HttpGet, func([]byte) interface{} { 211 | ret := FullSyncInfo{ 212 | CollectionMetric: make(map[string]string), 213 | } 214 | 215 | metricNsMapLock.Lock() 216 | defer metricNsMapLock.Unlock() 217 | 218 | ret.TotalCollection = len(metricNsMap) 219 | for ns, collectionMetric := range metricNsMap { 220 | ret.CollectionMetric[ns] = collectionMetric.String() 221 | switch collectionMetric.CollectionStatus { 222 | case utils.StatusWaitStart: 223 | ret.WaitCollection += 1 224 | case utils.StatusProcessing: 225 | ret.ProcessingCollection += 1 226 | case utils.StatusFinish: 227 | ret.FinishedCollection += 1 228 | } 229 | } 230 | 231 | if ret.TotalCollection == 0 { 232 | ret.Progress = "-%" 233 | } else { 234 | ret.Progress = fmt.Sprintf("%.2f%%", float64(ret.FinishedCollection)/float64(ret.TotalCollection)*100) 235 | } 236 | 237 | return ret 238 | }) 239 | 240 | } 241 | -------------------------------------------------------------------------------- /nimo-shake/full-sync/table-syncer.go: -------------------------------------------------------------------------------- 1 | package full_sync 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/aws/awserr" 9 | "github.com/aws/aws-sdk-go/aws/request" 10 | 11 | utils "nimo-shake/common" 12 | conf "nimo-shake/configure" 13 | "nimo-shake/protocal" 14 | "nimo-shake/qps" 15 | "nimo-shake/writer" 16 | 17 | "github.com/aws/aws-sdk-go/aws" 18 | "github.com/aws/aws-sdk-go/service/dynamodb" 19 | LOG "github.com/vinllen/log4go" 20 | ) 21 | 22 | const ( 23 | fetcherChanSize = 1024 24 | parserChanSize = 81920 25 | ) 26 | 27 | type tableSyncer struct { 28 | id int 29 | ns utils.NS 30 | sourceConn *dynamodb.DynamoDB 31 | sourceTableDescribe *dynamodb.TableDescription 32 | fetcherChan chan *dynamodb.ScanOutput // chan between fetcher and parser 33 | parserChan chan interface{} // chan between parser and writer 34 | converter protocal.Converter // converter 35 | collectionMetric *utils.CollectionMetric 36 | } 37 | 38 | func NewTableSyncer(id int, table string, collectionMetric *utils.CollectionMetric) *tableSyncer { 39 | sourceConn, err := utils.CreateDynamoSession(conf.Options.LogLevel) 40 | if err != nil { 41 | LOG.Error("tableSyncer[%v] with table[%v] create dynamodb session error[%v]", id, table, err) 42 | return nil 43 | } 44 | 45 | // describe source table 46 | tableDescription, err := sourceConn.DescribeTable(&dynamodb.DescribeTableInput{ 47 | TableName: aws.String(table), 48 | }) 49 | if err != nil { 50 | LOG.Error("tableSyncer[%v] with table[%v] describe failed[%v]", id, table, err) 51 | return nil 52 | } 53 | 54 | converter := protocal.NewConverter(conf.Options.ConvertType) 55 | if converter == nil { 56 | LOG.Error("tableSyncer[%v] with table[%v] create converter failed", id, table) 57 | return nil 58 | } 59 | 60 | return &tableSyncer{ 61 | id: id, 62 | sourceConn: sourceConn, 63 | sourceTableDescribe: tableDescription.Table, 64 | converter: converter, 65 | ns: utils.NS{ 66 | Database: conf.Options.Id, 67 | Collection: table, 68 | }, 69 | collectionMetric: collectionMetric, 70 | } 71 | } 72 | 73 | func (ts *tableSyncer) String() string { 74 | return fmt.Sprintf("tableSyncer[%v] with table[%v]", ts.id, ts.ns.Collection) 75 | } 76 | 77 | func (ts *tableSyncer) Sync() { 78 | ts.fetcherChan = make(chan *dynamodb.ScanOutput, fetcherChanSize) 79 | ts.parserChan = make(chan interface{}, parserChanSize) 80 | 81 | targetWriter := writer.NewWriter(conf.Options.TargetType, conf.Options.TargetAddress, ts.ns, conf.Options.LogLevel) 82 | if targetWriter == nil { 83 | LOG.Crashf("%s create writer failed", ts) 84 | return 85 | } 86 | // create table and index with description 87 | if err := targetWriter.CreateTable(ts.sourceTableDescribe); err != nil { 88 | LOG.Crashf("%s create table failed: %v", ts, err) 89 | return 90 | } 91 | 92 | // wait dynamo proxy to sync cache 93 | time.Sleep(10 * time.Second) 94 | 95 | if conf.Options.SyncSchemaOnly { 96 | LOG.Info("sync_schema_only enabled, %s exits", ts) 97 | return 98 | } 99 | 100 | // total table item count 101 | totalCount := ts.count() 102 | 103 | ts.collectionMetric.CollectionStatus = utils.StatusProcessing 104 | ts.collectionMetric.TotalCount = totalCount 105 | 106 | // start fetcher to fetch all data from DynamoDB 107 | go ts.fetcher() 108 | 109 | // start parser to get data from fetcher and write into writer. 110 | // we can also start several parsers to accelerate 111 | var wgParser sync.WaitGroup 112 | wgParser.Add(int(conf.Options.FullDocumentParser)) 113 | for i := 0; i < int(conf.Options.FullDocumentParser); i++ { 114 | go func(id int) { 115 | ts.parser(id) 116 | wgParser.Done() 117 | }(i) 118 | } 119 | 120 | // start writer 121 | var wgWriter sync.WaitGroup 122 | wgWriter.Add(int(conf.Options.FullDocumentConcurrency)) 123 | for i := 0; i < int(conf.Options.FullDocumentConcurrency); i++ { 124 | go func(id int) { 125 | LOG.Info("%s create document syncer with id[%v]", ts, id) 126 | ds := NewDocumentSyncer(ts.id, ts.ns.Collection, id, ts.parserChan, ts.sourceTableDescribe, 127 | ts.collectionMetric) 128 | ds.Run() 129 | LOG.Info("%s document syncer with id[%v] exit", ts, id) 130 | wgWriter.Done() 131 | }(i) 132 | } 133 | 134 | LOG.Info("%s wait all parsers exiting", ts.String()) 135 | wgParser.Wait() // wait all parser exit 136 | close(ts.parserChan) // close parser channel 137 | 138 | LOG.Info("%s all parsers exited, wait all writers exiting", ts.String()) 139 | wgWriter.Wait() // wait all writer exit 140 | 141 | ts.collectionMetric.CollectionStatus = utils.StatusFinish 142 | LOG.Info("%s finish syncing table", ts.String()) 143 | } 144 | 145 | func (ts *tableSyncer) Close() { 146 | // TODO, dynamo-session doesn't have close function? 147 | } 148 | 149 | func (ts *tableSyncer) fetcher() { 150 | LOG.Info("%s start fetcher with %v reader", ts.String(), conf.Options.FullReadConcurrency) 151 | 152 | qos := qps.StartQoS(int(conf.Options.QpsFull)) 153 | defer qos.Close() 154 | 155 | var wg sync.WaitGroup 156 | wg.Add(int(conf.Options.FullReadConcurrency)) 157 | for i := 0; i < int(conf.Options.FullReadConcurrency); i++ { 158 | go func(segmentId int64) { 159 | LOG.Info("%s start reader[%v]", ts.String(), segmentId) 160 | defer LOG.Info("%s stop reader[%v]", ts.String(), segmentId) 161 | 162 | // init nil 163 | var previousKey map[string]*dynamodb.AttributeValue 164 | for { 165 | <-qos.Bucket 166 | 167 | startT := time.Now() 168 | scanInput := &dynamodb.ScanInput{ 169 | TableName: aws.String(ts.ns.Collection), 170 | TotalSegments: aws.Int64(int64(conf.Options.FullReadConcurrency)), 171 | Segment: aws.Int64(segmentId), 172 | ExclusiveStartKey: previousKey, 173 | Limit: aws.Int64(conf.Options.QpsFullBatchNum), 174 | } 175 | if len(conf.Options.FullFilterExpression) > 0 { 176 | scanInput.FilterExpression = aws.String(conf.Options.FullFilterExpression) 177 | scanInput.ExpressionAttributeValues = utils.ParseAttributes(conf.Options.FullFilterAttributeValues) 178 | } 179 | out, err := ts.sourceConn.Scan(scanInput) 180 | if err != nil { 181 | // TODO check network error and retry 182 | if aerr, ok := err.(awserr.Error); ok { 183 | 184 | switch aerr.Code() { 185 | case dynamodb.ErrCodeProvisionedThroughputExceededException: 186 | LOG.Warn("%s fetcher reader[%v] recv ProvisionedThroughputExceededException continue", 187 | ts.String(), segmentId) 188 | time.Sleep(5 * time.Second) 189 | continue 190 | 191 | case request.ErrCodeSerialization: 192 | LOG.Warn("%s fetcher reader[%v] recv SerializationError[%v] continue", 193 | ts.String(), segmentId, err) 194 | time.Sleep(5 * time.Second) 195 | continue 196 | 197 | case request.ErrCodeRequestError, request.CanceledErrorCode, 198 | request.ErrCodeResponseTimeout, request.HandlerResponseTimeout, 199 | request.WaiterResourceNotReadyErrorCode, request.ErrCodeRead: 200 | LOG.Warn("%s fetcher reader[%v] recv Error[%v] continue", 201 | ts.String(), segmentId, err) 202 | time.Sleep(5 * time.Second) 203 | continue 204 | 205 | default: 206 | LOG.Crashf("%s fetcher scan failed[%v] errcode[%v]", ts.String(), err, aerr.Code()) 207 | } 208 | } else { 209 | LOG.Crashf("%s fetcher scan failed[%v]", ts.String(), err) 210 | } 211 | } 212 | scanDuration := time.Since(startT) 213 | 214 | // LOG.Info(*out.Count) 215 | 216 | // pass result to parser 217 | startT = time.Now() 218 | ts.fetcherChan <- out 219 | writeFetcherChan := time.Since(startT) 220 | 221 | LOG.Info("%s fetcher reader[%v] ts.fetcherChan.len[%v] "+ 222 | "scanTime[%v] scanCount[%v] writeFetcherChanTime[%v]", 223 | ts.String(), segmentId, len(ts.fetcherChan), scanDuration, *out.Count, writeFetcherChan) 224 | 225 | previousKey = out.LastEvaluatedKey 226 | if previousKey == nil { 227 | // complete 228 | break 229 | } 230 | } 231 | wg.Done() 232 | }(int64(i)) 233 | } 234 | wg.Wait() 235 | 236 | LOG.Info("%s close fetcher", ts.String()) 237 | close(ts.fetcherChan) 238 | } 239 | 240 | func (ts *tableSyncer) parser(id int) { 241 | LOG.Info("%s start parser[%v]", ts.String(), id) 242 | 243 | for { 244 | startT := time.Now() 245 | data, ok := <-ts.fetcherChan 246 | if !ok { 247 | break 248 | } 249 | readFetcherChanDuration := time.Since(startT) 250 | 251 | LOG.Debug("%s parser[%v] read data[%v]", ts.String(), id, data) 252 | 253 | var parserDuration, writeParseChanDuration time.Duration = 0, 0 254 | 255 | list := data.Items 256 | for _, ele := range list { 257 | startT = time.Now() 258 | out, err := ts.converter.Run(ele) 259 | parserDuration = parserDuration + time.Since(startT) 260 | if err != nil { 261 | LOG.Crashf("%s parser[%v] parse ele[%v] failed[%v]", ts.String(), id, ele, err) 262 | } 263 | 264 | startT = time.Now() 265 | ts.parserChan <- out 266 | writeParseChanDuration = writeParseChanDuration + time.Since(startT) 267 | } 268 | 269 | LOG.Info("%s parser parser[%v] readFetcherChanTime[%v] parserTime[%v]"+ 270 | " writeParseChantime[%v] parserChan.len[%v]", 271 | ts.String(), id, readFetcherChanDuration, parserDuration, writeParseChanDuration, len(ts.parserChan)) 272 | 273 | } 274 | LOG.Info("%s close parser", ts.String()) 275 | } 276 | 277 | func (ts *tableSyncer) count() uint64 { 278 | return uint64(*ts.sourceTableDescribe.ItemCount) 279 | } 280 | -------------------------------------------------------------------------------- /nimo-shake/go.mod: -------------------------------------------------------------------------------- 1 | module nimo-shake 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.44.61 7 | github.com/gugemichael/nimo4go v0.0.0-20210413043712-ccb2ff0d7b40 8 | github.com/jinzhu/copier v0.3.5 9 | github.com/nightlyone/lockfile v1.0.0 10 | github.com/stretchr/testify v1.6.1 11 | github.com/vinllen/log4go v0.0.0-20180514124125-3848a366df9d 12 | github.com/vinllen/mgo v0.0.0-20220329061231-e5ecea62f194 13 | go.mongodb.org/mongo-driver v1.16.1 14 | ) 15 | -------------------------------------------------------------------------------- /nimo-shake/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.44.61 h1:NcpLSS3Z0MiVQIYugx4I40vSIEEAXT0baO684ExNRco= 2 | github.com/aws/aws-sdk-go v1.44.61/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 7 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 8 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 9 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/gugemichael/nimo4go v0.0.0-20210413043712-ccb2ff0d7b40 h1:6TWAiHVyKs75ZHEn7XtVv7SO7M4rHwvY/5Tf7xdJBkc= 11 | github.com/gugemichael/nimo4go v0.0.0-20210413043712-ccb2ff0d7b40/go.mod h1:ibO7uKpO8fOH/bKD4trmwm5tHhHKiAjC0u288Rd+GnI= 12 | github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= 13 | github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 14 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 15 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 16 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 17 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 18 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 19 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 20 | github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= 21 | github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 22 | github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= 23 | github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= 24 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 29 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 30 | github.com/vinllen/log4go v0.0.0-20180514124125-3848a366df9d h1:V+5NxRH9I8rVJyazHI25yXaszF4JA64Isr4ilU+nnck= 31 | github.com/vinllen/log4go v0.0.0-20180514124125-3848a366df9d/go.mod h1:F8d+yTmuUlynKo7Wn26s+Q8bCZBfcymR5bYSydymIlc= 32 | github.com/vinllen/mgo v0.0.0-20220329061231-e5ecea62f194 h1:UlCPbcl8pHR5vAFP3qPx7HItWNa7+wjxRVySHstMsec= 33 | github.com/vinllen/mgo v0.0.0-20220329061231-e5ecea62f194/go.mod h1:nwebO37lLx+dkv3Dzp24vuzrapu+JudOSYpue46unDk= 34 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 35 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 36 | github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= 37 | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= 38 | github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= 39 | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= 40 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= 41 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 42 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 43 | go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= 44 | go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= 45 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 46 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 47 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 48 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= 49 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 50 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 51 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 52 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 53 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 54 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 55 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 56 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 57 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 58 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 59 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 60 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 62 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 64 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 65 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 74 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 75 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 76 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 77 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 78 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 79 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 80 | golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= 81 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 82 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 83 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 84 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 85 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 86 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 87 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 88 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 89 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 90 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 91 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 92 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 93 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 95 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 97 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 98 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 99 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | -------------------------------------------------------------------------------- /nimo-shake/incr-sync/fetcher.go: -------------------------------------------------------------------------------- 1 | package incr_sync 2 | 3 | import ( 4 | "time" 5 | 6 | "nimo-shake/checkpoint" 7 | utils "nimo-shake/common" 8 | conf "nimo-shake/configure" 9 | "nimo-shake/qps" 10 | 11 | "fmt" 12 | 13 | "github.com/aws/aws-sdk-go/service/dynamodbstreams" 14 | LOG "github.com/vinllen/log4go" 15 | ) 16 | 17 | const ( 18 | FetcherInterval = 60 // seconds 19 | ) 20 | 21 | type Fetcher struct { 22 | dynamoClient *dynamodbstreams.DynamoDBStreams 23 | table string 24 | stream *dynamodbstreams.Stream 25 | shardChan chan *utils.ShardNode 26 | ckptWriter checkpoint.Writer 27 | metric *utils.ReplicationMetric 28 | } 29 | 30 | func NewFetcher(table string, stream *dynamodbstreams.Stream, shardChan chan *utils.ShardNode, ckptWriter checkpoint.Writer, metric *utils.ReplicationMetric) *Fetcher { 31 | // create dynamo stream client 32 | dynamoStreamSession, err := utils.CreateDynamoStreamSession(conf.Options.LogLevel) 33 | if err != nil { 34 | LOG.Error("create dynamodb stream session failed[%v]", err) 35 | return nil 36 | } 37 | 38 | return &Fetcher{ 39 | dynamoClient: dynamoStreamSession, 40 | table: table, 41 | stream: stream, 42 | shardChan: shardChan, 43 | ckptWriter: ckptWriter, 44 | metric: metric, 45 | } 46 | } 47 | 48 | func (f *Fetcher) Run() { 49 | md5Map := make(map[string]uint64) 50 | tableEpoch := make(map[string]int) // GlobalFetcherMoreFlag, restore previous epoch 51 | 52 | qos := qps.StartQoS(10) 53 | defer qos.Close() 54 | 55 | for range time.NewTicker(FetcherInterval * time.Second).C { 56 | shardList := make([]*utils.ShardNode, 0) 57 | // LOG.Debug("fetch table[%v] stream", table) 58 | 59 | preEpoch, ok := tableEpoch[f.table] 60 | if !ok { 61 | tableEpoch[f.table] = 0 62 | } 63 | 64 | var allShards []*dynamodbstreams.Shard 65 | var lastShardIdString *string = nil 66 | for { 67 | var describeStreamInput *dynamodbstreams.DescribeStreamInput 68 | if lastShardIdString != nil { 69 | describeStreamInput = &dynamodbstreams.DescribeStreamInput{ 70 | StreamArn: f.stream.StreamArn, 71 | ExclusiveStartShardId: lastShardIdString, 72 | } 73 | } else { 74 | describeStreamInput = &dynamodbstreams.DescribeStreamInput{ 75 | StreamArn: f.stream.StreamArn, 76 | } 77 | } 78 | 79 | // limit qos of api DescribeStreamInput 80 | <-qos.Bucket 81 | 82 | desStream, err := f.dynamoClient.DescribeStream(describeStreamInput) 83 | if err != nil { 84 | LOG.Crashf("describe table[%v] with stream[%v] failed[%v]", f.table, *f.stream.StreamArn, err) 85 | } 86 | if *desStream.StreamDescription.StreamStatus == "DISABLED" { 87 | LOG.Crashf("table[%v] with stream[%v] has already been disabled", f.table, *f.stream.StreamArn) 88 | } 89 | 90 | allShards = append(allShards, desStream.StreamDescription.Shards...) 91 | 92 | if desStream.StreamDescription.LastEvaluatedShardId == nil { 93 | break 94 | } else { 95 | lastShardIdString = desStream.StreamDescription.LastEvaluatedShardId 96 | LOG.Info("table[%v] have next shardId,LastEvaluatedShardId[%v]", 97 | f.table, *desStream.StreamDescription.LastEvaluatedShardId) 98 | } 99 | } 100 | LOG.Info("fetch.Run table[%v] allShards(len:%d)[%v]", f.table, len(allShards), allShards) 101 | 102 | rootNode := utils.BuildShardTree(allShards, f.table, *f.stream.StreamArn) 103 | md5 := utils.CalMd5(rootNode) 104 | 105 | GlobalFetcherLock.Lock() 106 | curEpoch := GlobalFetcherMoreFlag[f.table] 107 | GlobalFetcherLock.Unlock() 108 | 109 | if val, ok := md5Map[f.table]; !ok || val != md5 { 110 | // shards is changed 111 | LOG.Info("table[%v] md5 changed from old[%v] to new[%v], need fetch shard", f.table, val, md5) 112 | md5Map[f.table] = md5 113 | } else if preEpoch != curEpoch { 114 | // old shard has already been finished 115 | LOG.Info("table[%v] curEpoch[%v] != preEpoch[%v]", f.table, curEpoch, preEpoch) 116 | tableEpoch[f.table] = curEpoch 117 | } else { 118 | LOG.Info("table[%v] md5-old[%v] md5-new[%v]", f.table, val, md5) 119 | 120 | continue 121 | } 122 | 123 | // extract checkpoint from mongodb 124 | ckptSingleMap, err := f.ckptWriter.ExtractSingleCheckpoint(f.table) 125 | if err != nil { 126 | LOG.Crashf("extract checkpoint failed[%v]", err) 127 | } else { 128 | LOG.Info("table:[%v] ckptSingleMap:[%v]", f.table, ckptSingleMap) 129 | } 130 | 131 | if tree, err := utils.PrintShardTree(rootNode); err != nil { 132 | LOG.Info("table[%v] traverse to print tree failed[%v]", f.table, err) 133 | } else { 134 | LOG.Info("traverse stream tree for table[%v](father->child): \n-----\n%v\n-----", f.table, tree) 135 | } 136 | 137 | // traverse shards 138 | err = utils.TraverseShard(rootNode, func(node *utils.ShardNode) error { 139 | LOG.Info("traverse shard[%v]", *node.Shard.ShardId) 140 | id := *node.Shard.ShardId 141 | var father string 142 | if node.Shard.ParentShardId != nil { 143 | father = *node.Shard.ParentShardId 144 | } 145 | 146 | ckpt, ok := ckptSingleMap[id] 147 | if !ok { 148 | // insert checkpoint 149 | newCkpt := &checkpoint.Checkpoint{ 150 | ShardId: id, 151 | SequenceNumber: *node.Shard.SequenceNumberRange.StartingSequenceNumber, 152 | Status: checkpoint.StatusPrepareProcess, 153 | WorkerId: "unknown", 154 | FatherId: father, 155 | IteratorType: checkpoint.IteratorTypeTrimHorizon, 156 | UpdateDate: "", // empty at first 157 | } 158 | f.ckptWriter.Insert(newCkpt, f.table) 159 | shardList = append(shardList, node) 160 | LOG.Info("insert new checkpoint: %v ckptSingleMap[id]:%v", *newCkpt, ckptSingleMap[id]) 161 | return utils.StopTraverseSonErr 162 | } 163 | switch ckpt.Status { 164 | case checkpoint.StatusNoNeedProcess: 165 | LOG.Info("no need to process: %v", *ckpt) 166 | return nil 167 | case checkpoint.StatusPrepareProcess: 168 | LOG.Info("status already in prepare: %v", *ckpt) 169 | shardList = append(shardList, node) 170 | return utils.StopTraverseSonErr 171 | case checkpoint.StatusInProcessing: 172 | LOG.Info("status already in processing: %v", *ckpt) 173 | shardList = append(shardList, node) 174 | return utils.StopTraverseSonErr 175 | case checkpoint.StatusNotProcess: 176 | fallthrough 177 | case checkpoint.StatusWaitFather: 178 | LOG.Info("status need to process: %v", *ckpt) 179 | ckpt.SequenceNumber = *node.Shard.SequenceNumberRange.StartingSequenceNumber 180 | ckpt.Status = checkpoint.StatusPrepareProcess 181 | ckpt.IteratorType = checkpoint.IteratorTypeTrimHorizon 182 | f.ckptWriter.Update(ckpt.ShardId, ckpt, f.table) 183 | shardList = append(shardList, node) 184 | return utils.StopTraverseSonErr 185 | case checkpoint.StatusDone: 186 | LOG.Info("already done: %v", *ckpt) 187 | return nil 188 | default: 189 | return fmt.Errorf("unknown checkpoint status[%v]", ckpt.Status) 190 | } 191 | 192 | return nil 193 | }) 194 | if err != nil { 195 | LOG.Crashf("traverse shard tree failed[%v]", err) 196 | } 197 | 198 | // dispatch shard list 199 | for _, shard := range shardList { 200 | LOG.Info("need to dispatch shard[%v]", *shard.Shard.ShardId) 201 | f.shardChan <- shard 202 | } 203 | } 204 | LOG.Crashf("can't see me!") 205 | } 206 | -------------------------------------------------------------------------------- /nimo-shake/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | _ "net/http/pprof" 8 | "os" 9 | "runtime" 10 | "syscall" 11 | "time" 12 | 13 | "nimo-shake/checkpoint" 14 | "nimo-shake/common" 15 | "nimo-shake/configure" 16 | "nimo-shake/run" 17 | 18 | "github.com/gugemichael/nimo4go" 19 | LOG "github.com/vinllen/log4go" 20 | ) 21 | 22 | type Exit struct{ Code int } 23 | 24 | func main() { 25 | defer LOG.Close() 26 | 27 | //http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) 28 | //go func() { 29 | // http.ListenAndServe(":6060", nil) 30 | //}() 31 | 32 | runtime.GOMAXPROCS(256) 33 | fmt.Println("max process:", runtime.GOMAXPROCS(0)) 34 | 35 | var err error 36 | // argument options 37 | configuration := flag.String("conf", "", "configuration path") 38 | version := flag.Bool("version", false, "show version") 39 | flag.Parse() 40 | 41 | if *version { 42 | fmt.Println(utils.Version) 43 | return 44 | } 45 | 46 | if *configuration == "" { 47 | fmt.Println(utils.Version) 48 | flag.PrintDefaults() 49 | return 50 | } 51 | 52 | conf.Options.Version = utils.Version 53 | 54 | var file *os.File 55 | if file, err = os.Open(*configuration); err != nil { 56 | crash(fmt.Sprintf("Configure file open failed. %v", err), -1) 57 | } 58 | 59 | configure := nimo.NewConfigLoader(file) 60 | configure.SetDateFormat(utils.GolangSecurityTime) 61 | if err := configure.Load(&conf.Options); err != nil { 62 | crash(fmt.Sprintf("Configure file %s parse failed. %v", *configuration, err), -2) 63 | } 64 | 65 | utils.InitialLogger(conf.Options.LogFile, conf.Options.LogLevel, conf.Options.LogBuffer) 66 | 67 | // sanitize options 68 | if err := sanitizeOptions(); err != nil { 69 | crash(fmt.Sprintf("Conf.Options check failed: %s", err.Error()), -4) 70 | } 71 | 72 | // read fcv and do comparison 73 | if _, err := conf.CheckFcv(*configuration, utils.FcvConfiguration.FeatureCompatibleVersion); err != nil { 74 | crash(err.Error(), -5) 75 | } 76 | 77 | utils.Welcome() 78 | utils.StartTime = fmt.Sprintf("%v", time.Now().Format(utils.GolangSecurityTime)) 79 | 80 | // write pid 81 | if err = utils.WritePidById(conf.Options.Id, "."); err != nil { 82 | crash(fmt.Sprintf("write pid failed. %v", err), -5) 83 | } 84 | 85 | // print configuration 86 | if opts, err := json.Marshal(conf.Options); err != nil { 87 | crash(fmt.Sprintf("marshal configuration failed[%v]", err), -6) 88 | } else { 89 | LOG.Info("%v configuration: %s", conf.Options.Id, string(opts)) 90 | } 91 | 92 | nimo.Profiling(int(conf.Options.SystemProfile)) 93 | nimo.RegisterSignalForProfiling(syscall.Signal(utils.SIGNALPROFILE)) // syscall.SIGUSR2 94 | nimo.RegisterSignalForPrintStack(syscall.Signal(utils.SIGNALSTACK), func(bytes []byte) { // syscall.SIGUSR1 95 | LOG.Info(string(bytes)) 96 | }) 97 | 98 | run.Start() 99 | 100 | LOG.Info("sync complete!") 101 | } 102 | 103 | func sanitizeOptions() error { 104 | if len(conf.Options.Id) == 0 { 105 | return fmt.Errorf("id[%v] shouldn't be empty", conf.Options.Id) 106 | } 107 | 108 | if conf.Options.SyncMode != utils.SyncModeAll && conf.Options.SyncMode != utils.SyncModeFull { 109 | return fmt.Errorf("sync_mode[%v] illegal, should in {all, full}", conf.Options.SyncMode) 110 | } 111 | 112 | if conf.Options.IncrSyncParallel != true { 113 | conf.Options.IncrSyncParallel = false 114 | } else { 115 | if conf.Options.SyncMode != utils.SyncModeAll { 116 | return fmt.Errorf("sync_mode must be all when incr_sync_parallel is true") 117 | } 118 | } 119 | if conf.Options.IncreasePersistDir == "" { 120 | conf.Options.IncreasePersistDir = "/tmp/" 121 | } 122 | 123 | if conf.Options.SourceAccessKeyID == "" { 124 | return fmt.Errorf("source.access_key_id shouldn't be empty") 125 | } 126 | 127 | if conf.Options.SourceSecretAccessKey == "" { 128 | return fmt.Errorf("source.secret_access_key shouldn't be empty") 129 | } 130 | 131 | if conf.Options.FilterCollectionBlack != "" && conf.Options.FilterCollectionWhite != "" { 132 | return fmt.Errorf("filter.collection.white and filter.collection.black can't both be given") 133 | } 134 | 135 | if conf.Options.QpsFull <= 0 || conf.Options.QpsIncr <= 0 { 136 | return fmt.Errorf("qps should > 0") 137 | } 138 | 139 | if conf.Options.QpsFullBatchNum <= 0 { 140 | conf.Options.QpsFullBatchNum = 128 141 | } 142 | if conf.Options.QpsIncrBatchNum <= 0 { 143 | conf.Options.QpsIncrBatchNum = 128 144 | } 145 | 146 | if conf.Options.TargetType != utils.TargetTypeMongo && conf.Options.TargetType != utils.TargetTypeAliyunDynamoProxy { 147 | return fmt.Errorf("conf.Options.TargetType[%v] supports {mongodb, aliyun_dynamo_proxy} currently", conf.Options.TargetType) 148 | } 149 | 150 | if len(conf.Options.TargetAddress) == 0 { 151 | return fmt.Errorf("target.address[%v] illegal", conf.Options.TargetAddress) 152 | } 153 | 154 | if conf.Options.FullConcurrency > 4096 || conf.Options.FullConcurrency == 0 { 155 | return fmt.Errorf("full.concurrency[%v] should in (0, 4096]", conf.Options.FullConcurrency) 156 | } 157 | 158 | if conf.Options.FullDocumentConcurrency > 4096 || conf.Options.FullDocumentConcurrency == 0 { 159 | return fmt.Errorf("full.document.concurrency[%v] should in (0, 4096]", conf.Options.FullDocumentConcurrency) 160 | } 161 | 162 | if conf.Options.FullDocumentWriteBatch <= 0 { 163 | if conf.Options.TargetType == utils.TargetTypeAliyunDynamoProxy { 164 | conf.Options.FullDocumentWriteBatch = 25 165 | } else { 166 | conf.Options.FullDocumentWriteBatch = 128 167 | } 168 | } else if conf.Options.FullDocumentWriteBatch > 25 && conf.Options.TargetType == utils.TargetTypeAliyunDynamoProxy { 169 | conf.Options.FullDocumentWriteBatch = 25 170 | } 171 | 172 | if conf.Options.FullReadConcurrency <= 0 { 173 | conf.Options.FullReadConcurrency = 1 174 | } else if conf.Options.FullReadConcurrency > 8192 { 175 | return fmt.Errorf("full.read.concurrency[%v] should in (0, 8192]", conf.Options.FullReadConcurrency) 176 | } 177 | 178 | if conf.Options.FullDocumentParser > 4096 || conf.Options.FullDocumentParser == 0 { 179 | return fmt.Errorf("full.document.parser[%v] should in (0, 4096]", conf.Options.FullDocumentParser) 180 | } 181 | 182 | // always enable 183 | conf.Options.FullEnableIndexPrimary = true 184 | 185 | if conf.Options.ConvertType == "" { 186 | conf.Options.ConvertType = utils.ConvertMTypeChange 187 | } 188 | if conf.Options.ConvertType != utils.ConvertTypeRaw && 189 | conf.Options.ConvertType != utils.ConvertTypeChange && 190 | conf.Options.ConvertType != utils.ConvertMTypeChange { 191 | return fmt.Errorf("convert.type[%v] illegal", conf.Options.ConvertType) 192 | } 193 | 194 | if conf.Options.IncreaseConcurrency == 0 { 195 | return fmt.Errorf("increase.concurrency should > 0") 196 | } 197 | 198 | if conf.Options.TargetMongoDBType != "" && conf.Options.TargetMongoDBType != utils.TargetMongoDBTypeReplica && 199 | conf.Options.TargetMongoDBType != utils.TargetMongoDBTypeSharding { 200 | return fmt.Errorf("illegal target.mongodb.type[%v]", conf.Options.TargetMongoDBType) 201 | } 202 | 203 | if conf.Options.TargetType == utils.TargetTypeMongo && conf.Options.TargetDBExist != "" && 204 | conf.Options.TargetDBExist != utils.TargetDBExistRename && 205 | conf.Options.TargetDBExist != utils.TargetDBExistDrop || 206 | conf.Options.TargetType == utils.TargetTypeAliyunDynamoProxy && conf.Options.TargetDBExist != "" && 207 | conf.Options.TargetDBExist != utils.TargetDBExistDrop { 208 | return fmt.Errorf("target.mongodb.exist[%v] should be 'drop' when target.type=%v", 209 | conf.Options.TargetDBExist, conf.Options.TargetType) 210 | } 211 | // set ConvertType 212 | if conf.Options.TargetType == utils.TargetTypeAliyunDynamoProxy { 213 | conf.Options.ConvertType = utils.ConvertTypeSame 214 | } 215 | 216 | // checkpoint 217 | if conf.Options.CheckpointType == "" { 218 | conf.Options.CheckpointType = checkpoint.CheckpointWriterTypeFile 219 | } 220 | if conf.Options.CheckpointType == checkpoint.CheckpointWriterTypeMongo && 221 | conf.Options.CheckpointAddress == "" && 222 | conf.Options.TargetType != utils.TargetTypeMongo { 223 | return fmt.Errorf("checkpoint.type should == file when checkpoint.address is empty and target.type != mongodb") 224 | } 225 | 226 | if conf.Options.CheckpointAddress == "" { 227 | if conf.Options.TargetType == utils.TargetTypeMongo { 228 | conf.Options.CheckpointAddress = conf.Options.TargetAddress 229 | } else { 230 | conf.Options.CheckpointAddress = "checkpoint" 231 | } 232 | } 233 | if conf.Options.CheckpointDb == "" { 234 | conf.Options.CheckpointDb = fmt.Sprintf("%s-%s", conf.Options.Id, "checkpoint") 235 | } 236 | 237 | if conf.Options.TargetType == utils.TargetTypeAliyunDynamoProxy && 238 | (!conf.Options.IncreaseExecutorUpsert || !conf.Options.IncreaseExecutorInsertOnDupUpdate) { 239 | return fmt.Errorf("increase.executor.upsert and increase.executor.insert_on_dup_update should be "+ 240 | "enable when target type is %v", utils.TargetTypeAliyunDynamoProxy) 241 | } 242 | 243 | if conf.Options.SourceEndpointUrl != "" && conf.Options.SyncMode != "full" { 244 | return fmt.Errorf("only support sync.mode=full when source.endpoint_url is set") 245 | } 246 | 247 | return nil 248 | } 249 | 250 | func crash(msg string, errCode int) { 251 | fmt.Println(msg) 252 | panic(Exit{errCode}) 253 | } 254 | -------------------------------------------------------------------------------- /nimo-shake/protocal/converter_test.go: -------------------------------------------------------------------------------- 1 | package protocal 2 | 3 | import ( 4 | "testing" 5 | "fmt" 6 | 7 | "github.com/aws/aws-sdk-go/service/dynamodb" 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/vinllen/mgo/bson" 11 | "strconv" 12 | ) 13 | 14 | func TestRawConverter(t *testing.T) { 15 | // test RawConverter 16 | 17 | var nr int 18 | { 19 | fmt.Printf("TestRawConverter case %d.\n", nr) 20 | nr++ 21 | 22 | src := map[string]*dynamodb.AttributeValue { 23 | "test": { 24 | N: aws.String("12345"), 25 | }, 26 | } 27 | 28 | rc := new(RawConverter) 29 | out, err := rc.Run(src) 30 | assert.Equal(t, nil, err, "should be equal") 31 | assert.Equal(t, bson.M{ 32 | "test": bson.M{ 33 | "N": "12345", 34 | }, 35 | }, out.(RawData).Data, "should be equal") 36 | } 37 | 38 | { 39 | fmt.Printf("TestRawConverter case %d.\n", nr) 40 | nr++ 41 | 42 | src := map[string]*dynamodb.AttributeValue { 43 | "test": { 44 | N: aws.String("12345"), 45 | }, 46 | "fuck": { 47 | S: aws.String("hello"), 48 | }, 49 | } 50 | 51 | rc := new(RawConverter) 52 | out, err := rc.Run(src) 53 | assert.Equal(t, nil, err, "should be equal") 54 | assert.Equal(t, bson.M{ 55 | "test": bson.M { 56 | "N": "12345", 57 | }, 58 | "fuck": bson.M { 59 | "S": "hello", 60 | }, 61 | }, out.(RawData).Data, "should be equal") 62 | } 63 | 64 | { 65 | fmt.Printf("TestRawConverter case %d.\n", nr) 66 | nr++ 67 | 68 | src := map[string]*dynamodb.AttributeValue { 69 | "test": { 70 | N: aws.String("12345"), 71 | }, 72 | "fuck": { 73 | S: aws.String("hello"), 74 | }, 75 | "test-string-list": { 76 | SS: []*string{aws.String("z1"), aws.String("z2"), aws.String("z3")}, 77 | }, 78 | "test-number-list": { 79 | NS: []*string{aws.String("123"), aws.String("456"), aws.String("78999999999999999999999999999")}, 80 | }, 81 | "test-bool": { 82 | BOOL: aws.Bool(true), 83 | }, 84 | "test-byte": { 85 | B: []byte{123, 45, 78, 0, 12}, 86 | }, 87 | "test-byte-list": { 88 | BS: [][]byte{ 89 | {123, 33, 44, 0, 55}, 90 | {0, 1, 2, 0, 5}, 91 | }, 92 | }, 93 | } 94 | 95 | rc := new(RawConverter) 96 | out, err := rc.Run(src) 97 | assert.Equal(t, nil, err, "should be equal") 98 | assert.Equal(t, bson.M{ 99 | "test": bson.M { 100 | "N": "12345", 101 | }, 102 | "fuck": bson.M { 103 | "S": "hello", 104 | }, 105 | "test-string-list": bson.M { 106 | "SS": []interface{}{"z1", "z2", "z3"}, 107 | }, 108 | "test-number-list": bson.M { 109 | "NS": []interface{}{"123", "456", "78999999999999999999999999999"}, 110 | }, 111 | "test-bool": bson.M { 112 | "BOOL": true, 113 | }, 114 | "test-byte": bson.M { 115 | "B": []byte{123, 45, 78, 0, 12}, 116 | }, 117 | "test-byte-list": bson.M{ 118 | "BS": [][]byte{ 119 | {123, 33, 44, 0, 55}, 120 | {0, 1, 2, 0, 5}, 121 | }, 122 | }, 123 | }, out.(RawData).Data, "should be equal") 124 | } 125 | 126 | { 127 | fmt.Printf("TestRawConverter case %d.\n", nr) 128 | nr++ 129 | 130 | src := map[string]*dynamodb.AttributeValue { 131 | "test": { 132 | N: aws.String("12345"), 133 | }, 134 | "test-inner-struct": { 135 | L: []*dynamodb.AttributeValue { 136 | { 137 | S: aws.String("hello-inner"), 138 | N: aws.String("12345"), 139 | }, 140 | { 141 | SS: []*string{aws.String("zi1"), aws.String("zi2"), aws.String("zi3")}, 142 | }, 143 | }, 144 | }, 145 | "test-inner-map": { 146 | M: map[string]*dynamodb.AttributeValue{ 147 | "test": { 148 | N: aws.String("12345000"), 149 | }, 150 | }, 151 | }, 152 | "test-NULL": { 153 | NULL: aws.Bool(false), 154 | }, 155 | } 156 | 157 | rc := new(RawConverter) 158 | out, err := rc.Run(src) 159 | assert.Equal(t, nil, err, "should be equal") 160 | assert.Equal(t, bson.M{ 161 | "test": bson.M { 162 | "N": "12345", 163 | }, 164 | "test-inner-struct": bson.M { 165 | "L": []interface{} { 166 | bson.M{ 167 | "S": "hello-inner", 168 | "N": "12345", 169 | }, 170 | bson.M{ 171 | "SS": []interface{}{"zi1", "zi2", "zi3"}, 172 | }, 173 | }, 174 | }, 175 | "test-inner-map": bson.M { 176 | "M": bson.M { 177 | "test": bson.M{ 178 | "N": "12345000", 179 | }, 180 | }, 181 | }, 182 | "test-NULL": bson.M { 183 | "NULL": false, 184 | }, 185 | }, out.(RawData).Data, "should be equal") 186 | } 187 | } 188 | 189 | func TestTypeConverter(t *testing.T) { 190 | // test TypeConverter 191 | 192 | var nr int 193 | { 194 | fmt.Printf("TestTypeConverter case %d.\n", nr) 195 | nr++ 196 | 197 | src := map[string]*dynamodb.AttributeValue{ 198 | "test": { 199 | N: aws.String("12345"), 200 | }, 201 | } 202 | 203 | rc := new(TypeConverter) 204 | out, err := rc.Run(src) 205 | assert.Equal(t, nil, err, "should be equal") 206 | val, err := bson.ParseDecimal128("12345") 207 | assert.Equal(t, nil, err, "should be equal") 208 | assert.Equal(t, bson.M{ 209 | "test": val, 210 | }, out.(RawData).Data, "should be equal") 211 | } 212 | 213 | { 214 | fmt.Printf("TestTypeConverter case %d.\n", nr) 215 | nr++ 216 | 217 | src := map[string]*dynamodb.AttributeValue{ 218 | "test": { 219 | N: aws.String("123456789101112131415161718192021"), 220 | }, 221 | "test2": { 222 | N: aws.String("3.141592653589793238462643383279"), 223 | }, 224 | "test3": { 225 | N: aws.String("3.1415926535897932384626433832795012345"), 226 | }, 227 | } 228 | 229 | rc := new(TypeConverter) 230 | out, err := rc.Run(src) 231 | assert.Equal(t, nil, err, "should be equal") 232 | val, err := bson.ParseDecimal128("123456789101112131415161718192021") 233 | assert.Equal(t, nil, err, "should be equal") 234 | val2, err := bson.ParseDecimal128("3.141592653589793238462643383279") 235 | assert.Equal(t, nil, err, "should be equal") 236 | val3, err := strconv.ParseFloat("3.1415926535897932384626433832795012345", 64) 237 | assert.Equal(t, nil, err, "should be equal") 238 | val3_2, err := bson.ParseDecimal128(fmt.Sprintf("%v", val3)) 239 | assert.Equal(t, nil, err, "should be equal") 240 | assert.Equal(t, bson.M{ 241 | "test": val, 242 | "test2": val2, 243 | "test3": val3_2, 244 | }, out.(RawData).Data, "should be equal") 245 | } 246 | 247 | { 248 | fmt.Printf("TestTypeConverter case %d.\n", nr) 249 | nr++ 250 | 251 | src := map[string]*dynamodb.AttributeValue { 252 | "test": { 253 | N: aws.String("12345"), 254 | }, 255 | "fuck": { 256 | S: aws.String("hello"), 257 | }, 258 | } 259 | 260 | rc := new(TypeConverter) 261 | out, err := rc.Run(src) 262 | assert.Equal(t, nil, err, "should be equal") 263 | val, err := bson.ParseDecimal128("12345") 264 | assert.Equal(t, nil, err, "should be equal") 265 | assert.Equal(t, bson.M{ 266 | "test": val, 267 | "fuck": "hello", 268 | }, out.(RawData).Data, "should be equal") 269 | } 270 | 271 | { 272 | fmt.Printf("TestTypeConverter case %d.\n", nr) 273 | nr++ 274 | 275 | src := map[string]*dynamodb.AttributeValue { 276 | "test": { 277 | N: aws.String("12345"), 278 | }, 279 | "fuck": { 280 | S: aws.String("hello"), 281 | }, 282 | "test-string-list": { 283 | SS: []*string{aws.String("z1"), aws.String("z2"), aws.String("z3")}, 284 | }, 285 | "test-number-list": { 286 | NS: []*string{aws.String("123"), aws.String("456"), aws.String("789999999999")}, 287 | }, 288 | "test-bool": { 289 | BOOL: aws.Bool(true), 290 | }, 291 | "test-byte": { 292 | B: []byte{123, 45, 78, 0, 12}, 293 | }, 294 | "test-byte-list": { 295 | BS: [][]byte{ 296 | {123, 33, 44, 0, 55}, 297 | {0, 1, 2, 0, 5}, 298 | }, 299 | }, 300 | } 301 | 302 | rc := new(TypeConverter) 303 | out, err := rc.Run(src) 304 | assert.Equal(t, nil, err, "should be equal") 305 | val, err := bson.ParseDecimal128("12345") 306 | assert.Equal(t, nil, err, "should be equal") 307 | val2, err := bson.ParseDecimal128("123") 308 | assert.Equal(t, nil, err, "should be equal") 309 | val3, err := bson.ParseDecimal128("456") 310 | assert.Equal(t, nil, err, "should be equal") 311 | val4, err := bson.ParseDecimal128("789999999999") 312 | assert.Equal(t, nil, err, "should be equal") 313 | assert.Equal(t, bson.M{ 314 | "test": val, 315 | "fuck": "hello", 316 | "test-string-list": []string{"z1", "z2", "z3"}, 317 | "test-number-list": []bson.Decimal128{val2, val3, val4}, 318 | "test-bool": true, 319 | "test-byte": []byte{123, 45, 78, 0, 12}, 320 | "test-byte-list": [][]byte{ 321 | {123, 33, 44, 0, 55}, 322 | {0, 1, 2, 0, 5}, 323 | }, 324 | }, out.(RawData).Data, "should be equal") 325 | } 326 | 327 | { 328 | fmt.Printf("TestTypeConverter case %d.\n", nr) 329 | nr++ 330 | 331 | src := map[string]*dynamodb.AttributeValue { 332 | "test": { 333 | N: aws.String("12345"), 334 | }, 335 | "test-inner-struct": { 336 | L: []*dynamodb.AttributeValue { 337 | { 338 | S: aws.String("hello-inner"), 339 | // N: aws.String("12345"), 340 | }, 341 | { 342 | SS: []*string{aws.String("zi1"), aws.String("zi2"), aws.String("zi3")}, 343 | }, 344 | }, 345 | }, 346 | "test-inner-map": { 347 | M: map[string]*dynamodb.AttributeValue{ 348 | "test": { 349 | N: aws.String("12345000"), 350 | }, 351 | }, 352 | }, 353 | "test-NULL": { 354 | NULL: aws.Bool(false), 355 | }, 356 | "N": { 357 | M:map[string]*dynamodb.AttributeValue{ 358 | "NN": { 359 | N: aws.String("567"), 360 | }, 361 | "M": { 362 | S: aws.String("899"), 363 | }, 364 | }, 365 | }, 366 | } 367 | 368 | rc := new(TypeConverter) 369 | out, err := rc.Run(src) 370 | assert.Equal(t, nil, err, "should be equal") 371 | val, err := bson.ParseDecimal128("12345") 372 | assert.Equal(t, nil, err, "should be equal") 373 | val2, err := bson.ParseDecimal128("12345000") 374 | assert.Equal(t, nil, err, "should be equal") 375 | val3, err := bson.ParseDecimal128("567") 376 | assert.Equal(t, nil, err, "should be equal") 377 | assert.Equal(t, bson.M{ 378 | "test": val, 379 | "test-inner-struct": []interface{} { 380 | "hello-inner", 381 | []string{"zi1", "zi2", "zi3"}, 382 | }, 383 | "test-inner-map": bson.M { 384 | "test": val2, 385 | }, 386 | "test-NULL": false, 387 | "N": bson.M { 388 | "NN": val3, 389 | "M": "899", 390 | }, 391 | }, out.(RawData).Data, "should be equal") 392 | } 393 | } -------------------------------------------------------------------------------- /nimo-shake/protocal/mtype_converter.go: -------------------------------------------------------------------------------- 1 | package protocal 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-sdk-go/service/dynamodb" 6 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 7 | LOG "github.com/vinllen/log4go" 8 | conf "nimo-shake/configure" 9 | "time" 10 | ) 11 | 12 | type MTypeConverter struct { 13 | } 14 | 15 | 16 | func (tc *MTypeConverter) Run(input map[string]*dynamodb.AttributeValue) (interface{}, error) { 17 | funcStartT := time.Now() 18 | 19 | outLen := 0 20 | for key, value := range input { 21 | outLen = outLen + len(key) + len(value.String()) 22 | } 23 | 24 | out := new(interface{}) 25 | if err := dynamodbattribute.UnmarshalMap(input, out); err == nil { 26 | 27 | for key, value := range (*out).(map[string]interface {}) { 28 | if key == "_id" { 29 | delete((*out).(map[string]interface {}), key) 30 | ((*out).(map[string]interface {}))[conf.ConvertIdFunc(key)] = value 31 | } 32 | } 33 | 34 | LOG.Debug("Run_func input[%v] out[%v] out_len[%v] duration[%v]", 35 | input, *out, outLen, time.Since(funcStartT)) 36 | 37 | return RawData{outLen, *out}, nil 38 | } else { 39 | LOG.Debug("Run_func input[%v] out[%v] err[%v]", input, *out, err) 40 | } 41 | 42 | return RawData{}, fmt.Errorf("parse failed, return nil") 43 | } -------------------------------------------------------------------------------- /nimo-shake/protocal/protocal.go: -------------------------------------------------------------------------------- 1 | package protocal 2 | 3 | import ( 4 | "nimo-shake/common" 5 | 6 | "github.com/aws/aws-sdk-go/service/dynamodb" 7 | ) 8 | 9 | // convert DynamoDB attributeValue to bson 10 | type Converter interface { 11 | // run 12 | Run(input map[string]*dynamodb.AttributeValue) (interface{}, error) 13 | } 14 | 15 | func NewConverter(tp string) Converter { 16 | switch tp { 17 | case utils.ConvertTypeRaw: 18 | return new(RawConverter) 19 | case utils.ConvertTypeChange: 20 | return new(TypeConverter) 21 | case utils.ConvertMTypeChange: 22 | return new(MTypeConverter) 23 | case utils.ConvertTypeSame: 24 | return new(SameConverter) 25 | default: 26 | return nil 27 | } 28 | } 29 | 30 | type RawData struct { 31 | Size int // fake size, only calculate real data 32 | Data interface{} // real data 33 | } 34 | -------------------------------------------------------------------------------- /nimo-shake/protocal/raw_converter.go: -------------------------------------------------------------------------------- 1 | package protocal 2 | 3 | import ( 4 | "fmt" 5 | conf "nimo-shake/configure" 6 | "reflect" 7 | 8 | bson2 "go.mongodb.org/mongo-driver/bson" 9 | 10 | "github.com/aws/aws-sdk-go/service/dynamodb" 11 | LOG "github.com/vinllen/log4go" 12 | ) 13 | 14 | type RawConverter struct { 15 | } 16 | 17 | // use dfs to convert to bson.M 18 | func (rc *RawConverter) Run(input map[string]*dynamodb.AttributeValue) (interface{}, error) { 19 | v := reflect.ValueOf(input) 20 | if output := rc.dfs(v); output == nil { 21 | return RawData{}, fmt.Errorf("parse failed, return nil") 22 | } else if out, ok := output.(RawData); !ok { 23 | return RawData{}, fmt.Errorf("parse failed, return type isn't RawData") 24 | } else if _, ok := out.Data.(bson2.M); !ok { 25 | return RawData{}, fmt.Errorf("parse failed, return data isn't bson.M") 26 | } else { 27 | return out, nil 28 | } 29 | } 30 | 31 | func (rc *RawConverter) dfs(v reflect.Value) interface{} { 32 | switch v.Kind() { 33 | case reflect.Invalid: 34 | return nil 35 | case reflect.Slice, reflect.Array: 36 | if v.Len() == 0 { 37 | return nil 38 | } 39 | 40 | size := 0 41 | ret := make([]interface{}, 0, v.Len()) 42 | for i := 0; i < v.Len(); i++ { 43 | out := rc.dfs(v.Index(i)) 44 | md := out.(RawData) 45 | size += md.Size 46 | ret = append(ret, md.Data) 47 | } 48 | return RawData{size, ret} 49 | case reflect.Struct: 50 | if v.NumField() == 0 { 51 | return nil 52 | } 53 | 54 | size := 0 55 | ret := make(bson2.M) 56 | for i := 0; i < v.NumField(); i++ { 57 | name := v.Type().Field(i).Name 58 | if out := rc.dfs(v.Field(i)); out != nil { 59 | md := out.(RawData) 60 | size += md.Size 61 | size += len(name) 62 | if _, ok := md.Data.([]interface{}); ok { 63 | // is type array 64 | md.Data = rc.convertListToDetailList(name, md.Data) 65 | } 66 | ret[name] = md.Data 67 | } 68 | } 69 | return RawData{size, ret} 70 | case reflect.Map: 71 | if len(v.MapKeys()) == 0 { 72 | return nil 73 | } 74 | 75 | size := 0 76 | ret := make(bson2.M) 77 | for _, key := range v.MapKeys() { 78 | name := key.String() 79 | name = conf.ConvertIdFunc(name) 80 | if out := rc.dfs(v.MapIndex(key)); out != nil { 81 | md := out.(RawData) 82 | size += md.Size 83 | size += len(name) 84 | if _, ok := md.Data.([]interface{}); ok { 85 | // is type array 86 | out = rc.convertListToDetailList(name, md.Data) 87 | } 88 | ret[name] = md.Data 89 | } 90 | } 91 | return RawData{size, ret} 92 | case reflect.Ptr: 93 | if v.IsNil() { 94 | return nil 95 | } else { 96 | return rc.dfs(v.Elem()) 97 | } 98 | case reflect.Interface: 99 | if v.IsNil() { 100 | return nil 101 | } else { 102 | return rc.dfs(v.Elem()) 103 | } 104 | case reflect.String: 105 | out := v.String() 106 | return RawData{len(out), out} 107 | case reflect.Int: 108 | fallthrough 109 | case reflect.Int64: 110 | return RawData{8, v.Int()} 111 | case reflect.Int8: 112 | return RawData{1, int8(v.Int())} 113 | case reflect.Int16: 114 | return RawData{2, int16(v.Int())} 115 | case reflect.Int32: 116 | return RawData{4, int32(v.Int())} 117 | case reflect.Uint: 118 | fallthrough 119 | case reflect.Uint64: 120 | return RawData{8, v.Uint()} 121 | case reflect.Uint8: 122 | return RawData{1, uint8(v.Uint())} 123 | case reflect.Uint16: 124 | return RawData{2, uint16(v.Uint())} 125 | case reflect.Uint32: 126 | return RawData{4, uint32(v.Uint())} 127 | case reflect.Bool: 128 | // fake size 129 | return RawData{1, v.Bool()} 130 | default: 131 | // not support 132 | LOG.Error("unknown type[%v]", v.Kind()) 133 | return nil 134 | } 135 | } 136 | 137 | func (rc *RawConverter) convertListToDetailList(name string, input interface{}) interface{} { 138 | list := input.([]interface{}) 139 | switch name { 140 | case "B": 141 | output := make([]byte, 0, len(list)) 142 | for _, ele := range list { 143 | output = append(output, ele.(byte)) 144 | } 145 | return output 146 | case "BS": 147 | output := make([][]byte, 0, len(list)) 148 | for _, ele := range list { 149 | inner := rc.convertListToDetailList("B", ele) 150 | output = append(output, inner.([]byte)) 151 | } 152 | return output 153 | case "NS": 154 | fallthrough 155 | case "SS": 156 | output := make([]interface{}, 0, len(list)) 157 | for _, ele := range list { 158 | output = append(output, ele.(string)) 159 | } 160 | return output 161 | case "L": 162 | output := make([]interface{}, 0, len(list)) 163 | for _, ele := range list { 164 | output = append(output, ele.(bson2.M)) 165 | } 166 | return output 167 | } 168 | return list 169 | } 170 | -------------------------------------------------------------------------------- /nimo-shake/protocal/same_converter.go: -------------------------------------------------------------------------------- 1 | package protocal 2 | 3 | import "github.com/aws/aws-sdk-go/service/dynamodb" 4 | 5 | type SameConverter struct { 6 | } 7 | 8 | func (sc *SameConverter) Run(input map[string]*dynamodb.AttributeValue) (interface{}, error) { 9 | return input, nil 10 | } -------------------------------------------------------------------------------- /nimo-shake/protocal/type_converter.go: -------------------------------------------------------------------------------- 1 | package protocal 2 | 3 | import ( 4 | "fmt" 5 | conf "nimo-shake/configure" 6 | "reflect" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go/service/dynamodb" 11 | LOG "github.com/vinllen/log4go" 12 | bson2 "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/bson/primitive" 14 | ) 15 | 16 | type TypeConverter struct { 17 | } 18 | 19 | // use dfs to convert to bson.M 20 | func (tc *TypeConverter) Run(input map[string]*dynamodb.AttributeValue) (interface{}, error) { 21 | v := reflect.ValueOf(input) 22 | if output := tc.dfs(v); output == nil { 23 | return RawData{}, fmt.Errorf("parse failed, return nil") 24 | } else if out, ok := output.(RawData); !ok { 25 | return RawData{}, fmt.Errorf("parse failed, return type isn't RawData") 26 | } else if _, ok := out.Data.(bson2.M); !ok { 27 | return RawData{}, fmt.Errorf("parse failed, return data isn't bson.M") 28 | } else { 29 | return out, nil 30 | } 31 | } 32 | 33 | func (tc *TypeConverter) dfs(v reflect.Value) interface{} { 34 | 35 | funcStartT := time.Now() 36 | defer LOG.Debug("dfs_func kind[%v] value[%v] duration[%v]", 37 | v.Kind().String(), v, time.Since(funcStartT)) 38 | 39 | switch v.Kind() { 40 | case reflect.Invalid: 41 | return nil 42 | case reflect.Slice, reflect.Array: 43 | if v.Len() == 0 { 44 | return nil 45 | } 46 | 47 | size := 0 48 | ret := make([]interface{}, 0, v.Len()) 49 | for i := 0; i < v.Len(); i++ { 50 | out := tc.dfs(v.Index(i)) 51 | md := out.(RawData) 52 | size += md.Size 53 | ret = append(ret, md.Data) 54 | } 55 | return RawData{size, ret} 56 | case reflect.Struct: 57 | if v.NumField() == 0 { 58 | return nil 59 | } 60 | if v.Type().Name() == "bson.Decimal128" { 61 | return RawData{16, v} 62 | } 63 | 64 | size := 0 65 | var ret interface{} 66 | cnt := 0 67 | // at most one field in AttributeValue 68 | for i := 0; i < v.NumField(); i++ { 69 | name := v.Type().Field(i).Name 70 | if out := tc.dfs(v.Field(i)); out != nil { 71 | cnt++ 72 | if cnt > 2 { 73 | LOG.Crashf("illegal struct field number") 74 | } 75 | 76 | md := out.(RawData) 77 | size += md.Size 78 | md.Data = tc.convertToDetail(name, md.Data) 79 | ret = md.Data 80 | } 81 | } 82 | return RawData{size, ret} 83 | case reflect.Map: 84 | if len(v.MapKeys()) == 0 { 85 | return nil 86 | } 87 | 88 | size := 0 89 | ret := make(bson2.M) 90 | for _, key := range v.MapKeys() { 91 | name := key.String() 92 | name = conf.ConvertIdFunc(name) 93 | if out := tc.dfs(v.MapIndex(key)); out != nil { 94 | md := out.(RawData) 95 | size += md.Size 96 | size += len(name) 97 | // out = tc.convertToDetail(name, md.Data, false) 98 | ret[name] = md.Data 99 | } 100 | } 101 | return RawData{size, ret} 102 | case reflect.Ptr: 103 | if v.IsNil() { 104 | return nil 105 | } else { 106 | return tc.dfs(v.Elem()) 107 | } 108 | case reflect.Interface: 109 | if v.IsNil() { 110 | return nil 111 | } else { 112 | return tc.dfs(v.Elem()) 113 | } 114 | case reflect.String: 115 | out := v.String() 116 | return RawData{len(out), out} 117 | case reflect.Int: 118 | fallthrough 119 | case reflect.Int64: 120 | return RawData{8, v.Int()} 121 | case reflect.Int8: 122 | return RawData{1, int8(v.Int())} 123 | case reflect.Int16: 124 | return RawData{2, int16(v.Int())} 125 | case reflect.Int32: 126 | return RawData{4, int32(v.Int())} 127 | case reflect.Uint: 128 | fallthrough 129 | case reflect.Uint64: 130 | return RawData{8, v.Uint()} 131 | case reflect.Uint8: 132 | return RawData{1, uint8(v.Uint())} 133 | case reflect.Uint16: 134 | return RawData{2, uint16(v.Uint())} 135 | case reflect.Uint32: 136 | return RawData{4, uint32(v.Uint())} 137 | case reflect.Bool: 138 | // fake size 139 | return RawData{1, v.Bool()} 140 | default: 141 | // not support 142 | LOG.Error("unknown type[%v]", v.Kind()) 143 | return nil 144 | } 145 | } 146 | 147 | func (tc *TypeConverter) convertToDetail(name string, input interface{}) interface{} { 148 | 149 | funcStartT := time.Now() 150 | defer LOG.Debug("convertToDetail_func name[%v] input[%v] duration[%v]", name, input, time.Since(funcStartT)) 151 | 152 | switch name { 153 | case "B": 154 | list := input.([]interface{}) 155 | output := make([]byte, 0, len(list)) 156 | for _, ele := range list { 157 | output = append(output, ele.(byte)) 158 | } 159 | return output 160 | case "BS": 161 | list := input.([]interface{}) 162 | output := make([][]byte, 0, len(list)) 163 | for _, ele := range list { 164 | inner := tc.convertToDetail("B", ele) 165 | output = append(output, inner.([]byte)) 166 | } 167 | return output 168 | case "NS": 169 | list := input.([]interface{}) 170 | 171 | var nType reflect.Type 172 | for _, ele := range list { 173 | inner := tc.convertToDetail("N", ele) 174 | nType = reflect.TypeOf(inner) 175 | break 176 | } 177 | 178 | if nType.Name() == "int" { 179 | 180 | output := make([]int, 0, len(list)) 181 | for _, ele := range list { 182 | inner := tc.convertToDetail("N", ele) 183 | output = append(output, inner.(int)) 184 | } 185 | 186 | return output 187 | } else { 188 | output := make([]primitive.Decimal128, 0, len(list)) 189 | for _, ele := range list { 190 | inner := tc.convertToDetail("N", ele) 191 | output = append(output, inner.(primitive.Decimal128)) 192 | } 193 | 194 | return output 195 | } 196 | 197 | case "SS": 198 | list := input.([]interface{}) 199 | output := make([]string, 0, len(list)) 200 | for _, ele := range list { 201 | inner := tc.convertToDetail("S", ele) 202 | output = append(output, inner.(string)) 203 | } 204 | return output 205 | case "L": 206 | list := input.([]interface{}) 207 | output := make([]interface{}, 0, len(list)) 208 | for _, ele := range list { 209 | output = append(output, ele) 210 | } 211 | return output 212 | case "BOOL": 213 | fallthrough 214 | case "NULL": 215 | return input.(bool) 216 | case "N": 217 | v := input.(string) 218 | 219 | val_int, err := strconv.Atoi(v) 220 | if err == nil { 221 | return val_int 222 | } 223 | 224 | val, err := primitive.ParseDecimal128(v) 225 | if err != nil { 226 | LOG.Error("convert N to decimal128 failed[%v]", err) 227 | val2, err := strconv.ParseFloat(v, 64) 228 | if err != nil { 229 | LOG.Crashf("convert N to decimal128 and float64 both failed[%v]", err) 230 | } 231 | 232 | val, _ = primitive.ParseDecimal128(fmt.Sprintf("%v", val2)) 233 | return val 234 | } 235 | return val 236 | case "S": 237 | return input.(string) 238 | } 239 | 240 | // "M" 241 | return input 242 | } 243 | -------------------------------------------------------------------------------- /nimo-shake/qps/qps.go: -------------------------------------------------------------------------------- 1 | package qps 2 | 3 | import "time" 4 | 5 | type Qos struct { 6 | Bucket chan struct{} 7 | 8 | limit int // qps 9 | close bool 10 | } 11 | 12 | func StartQoS(limit int) *Qos { 13 | q := new(Qos) 14 | q.limit = limit 15 | q.Bucket = make(chan struct{}, limit) 16 | 17 | go q.timer() 18 | return q 19 | } 20 | 21 | func (q *Qos) timer() { 22 | for range time.NewTicker(1 * time.Second).C { 23 | if q.close { 24 | return 25 | } 26 | for i := 0; i < q.limit; i++ { 27 | select { 28 | case q.Bucket <- struct{}{}: 29 | default: 30 | // break if bucket if full 31 | break 32 | } 33 | } 34 | } 35 | } 36 | 37 | func (q *Qos) Close() { 38 | q.close = true 39 | } 40 | -------------------------------------------------------------------------------- /nimo-shake/run/run.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "nimo-shake/checkpoint" 5 | "nimo-shake/common" 6 | "nimo-shake/configure" 7 | "nimo-shake/filter" 8 | "nimo-shake/full-sync" 9 | "nimo-shake/incr-sync" 10 | "nimo-shake/writer" 11 | 12 | "github.com/aws/aws-sdk-go/service/dynamodbstreams" 13 | "github.com/gugemichael/nimo4go" 14 | LOG "github.com/vinllen/log4go" 15 | ) 16 | 17 | func incrStart(streamMap map[string]*dynamodbstreams.Stream, ckptWriter checkpoint.Writer) { 18 | LOG.Info("start increase sync") 19 | 20 | // register restful api 21 | incr_sync.RestAPI() 22 | 23 | // start http server. 24 | nimo.GoRoutine(func() { 25 | // before starting, we must register all interface 26 | if err := utils.IncrSyncHttpApi.Listen(); err != nil { 27 | LOG.Critical("start incr sync server with port[%v] failed: %v", conf.Options.IncrSyncHTTPListenPort, 28 | err) 29 | } 30 | }) 31 | 32 | LOG.Info("------------------------start incr sync------------------------") 33 | incr_sync.Start(streamMap, ckptWriter) 34 | LOG.Info("------------------------end incr sync------------------------") 35 | } 36 | 37 | func Start() { 38 | LOG.Info("check connections") 39 | 40 | utils.FullSyncInitHttpApi(conf.Options.FullSyncHTTPListenPort) 41 | utils.IncrSyncInitHttpApi(conf.Options.IncrSyncHTTPListenPort) 42 | 43 | // init filter 44 | filter.Init(conf.Options.FilterCollectionWhite, conf.Options.FilterCollectionBlack) 45 | 46 | if err := utils.InitSession(conf.Options.SourceAccessKeyID, conf.Options.SourceSecretAccessKey, 47 | conf.Options.SourceSessionToken, conf.Options.SourceRegion, conf.Options.SourceEndpointUrl, 48 | conf.Options.SourceSessionMaxRetries, conf.Options.SourceSessionTimeout); err != nil { 49 | LOG.Crashf("init global session failed[%v]", err) 50 | } 51 | 52 | // check writer connection 53 | w := writer.NewWriter(conf.Options.TargetType, conf.Options.TargetAddress, 54 | utils.NS{"nimo-shake", "shake_writer_test"}, conf.Options.LogLevel) 55 | if w == nil { 56 | LOG.Crashf("connect type[%v] address[%v] failed[%v]", 57 | conf.Options.TargetType, conf.Options.TargetAddress) 58 | } 59 | 60 | // create dynamo session 61 | dynamoSession, err := utils.CreateDynamoSession(conf.Options.LogLevel) 62 | if err != nil { 63 | LOG.Crashf("create dynamodb session failed[%v]", err) 64 | } 65 | 66 | // create dynamo stream client 67 | dynamoStreamSession, err := utils.CreateDynamoStreamSession(conf.Options.LogLevel) 68 | if err != nil { 69 | LOG.Crashf("create dynamodb stream session failed[%v]", err) 70 | } 71 | 72 | LOG.Info("create checkpoint writer: type=%v", conf.Options.CheckpointType) 73 | ckptWriter := checkpoint.NewWriter(conf.Options.CheckpointType, conf.Options.CheckpointAddress, 74 | conf.Options.CheckpointDb) 75 | 76 | var skipFull bool 77 | var streamMap map[string]*dynamodbstreams.Stream 78 | if conf.Options.SyncMode == utils.SyncModeAll { 79 | LOG.Info("------------------------check checkpoint------------------------") 80 | skipFull, streamMap, err = checkpoint.CheckCkpt(ckptWriter, dynamoStreamSession) 81 | if err != nil { 82 | LOG.Crashf("check checkpoint failed[%v]", err) 83 | } 84 | LOG.Info("------------------------end check checkpoint------------------------") 85 | } 86 | 87 | // full sync 88 | skipIncrSync := false 89 | if skipFull == false { 90 | // register restful api 91 | full_sync.RestAPI() 92 | 93 | // start http server. 94 | nimo.GoRoutine(func() { 95 | // before starting, we must register all interface 96 | if err := utils.FullSyncHttpApi.Listen(); err != nil { 97 | LOG.Critical("start full sync server with port[%v] failed: %v", conf.Options.FullSyncHTTPListenPort, 98 | err) 99 | } 100 | }) 101 | 102 | if conf.Options.SyncMode == utils.SyncModeAll { 103 | LOG.Info("------------------------drop old checkpoint------------------------") 104 | if err := ckptWriter.DropAll(); err != nil && err.Error() != utils.NotFountErr { 105 | LOG.Crashf("drop checkpoint failed[%v]", err) 106 | } 107 | 108 | LOG.Info("------------------------prepare checkpoint start------------------------") 109 | streamMap, err = checkpoint.PrepareFullSyncCkpt(ckptWriter, dynamoSession, dynamoStreamSession) 110 | if err != nil { 111 | LOG.Crashf("prepare checkpoint failed[%v]", err) 112 | } 113 | LOG.Info("------------------------prepare checkpoint done------------------------") 114 | 115 | // select{} 116 | } else { 117 | LOG.Info("sync.mode is 'full', no need to check checkpoint") 118 | } 119 | 120 | if conf.Options.IncrSyncParallel == true { 121 | skipIncrSync = true 122 | go incrStart(streamMap, ckptWriter) 123 | } 124 | 125 | // update checkpoint 126 | if err := ckptWriter.UpdateStatus(checkpoint.CheckpointStatusValueFullSync); err != nil { 127 | LOG.Crashf("set checkpoint to [%v] failed[%v]", checkpoint.CheckpointStatusValueFullSync, err) 128 | } 129 | 130 | LOG.Info("------------------------start full sync------------------------") 131 | full_sync.Start(dynamoSession, w) 132 | LOG.Info("------------------------full sync done!------------------------") 133 | } 134 | 135 | if conf.Options.SyncMode == utils.SyncModeFull { 136 | LOG.Info("sync.mode is 'full', finish") 137 | return 138 | } 139 | 140 | if conf.Options.SyncSchemaOnly { 141 | LOG.Info("sync_schema_only enabled, finish") 142 | return 143 | } 144 | 145 | // update checkpoint 146 | if err := ckptWriter.UpdateStatus(checkpoint.CheckpointStatusValueIncrSync); err != nil { 147 | LOG.Crashf("set checkpoint to [%v] failed[%v]", checkpoint.CheckpointStatusValueIncrSync, err) 148 | } 149 | 150 | if skipIncrSync == false { 151 | go incrStart(streamMap, ckptWriter) 152 | } 153 | 154 | select {} 155 | } 156 | -------------------------------------------------------------------------------- /nimo-shake/unit_test_common/include.go: -------------------------------------------------------------------------------- 1 | package unit_test_common 2 | 3 | const ( 4 | TestUrl = "mongodb://100.81.164.186:31881,100.81.164.186:31882,100.81.164.186:31883" 5 | TestUrlServerless = "mongodb://100.81.164.181:32155" 6 | TestUrlServerlessTenant = "mongodb://100.81.164.181:36106" // sharding mongos with tenant 7 | TestUrlSharding = "mongodb://100.81.164.181:33010" 8 | ) 9 | -------------------------------------------------------------------------------- /nimo-shake/writer/dynamo_proxy.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "fmt" 5 | "bytes" 6 | "net/http" 7 | "time" 8 | 9 | "nimo-shake/common" 10 | "nimo-shake/configure" 11 | 12 | "github.com/aws/aws-sdk-go/aws" 13 | "github.com/aws/aws-sdk-go/aws/session" 14 | LOG "github.com/vinllen/log4go" 15 | "github.com/aws/aws-sdk-go/service/dynamodb" 16 | ) 17 | 18 | type DynamoProxyWriter struct { 19 | Name string 20 | svc *dynamodb.DynamoDB 21 | ns utils.NS 22 | } 23 | 24 | func NewDynamoProxyWriter(name, address string, ns utils.NS, logLevel string) *DynamoProxyWriter { 25 | config := &aws.Config{ 26 | Region: aws.String("us-east-2"), // meaningless 27 | Endpoint: aws.String(address), 28 | MaxRetries: aws.Int(3), 29 | DisableSSL: aws.Bool(true), 30 | HTTPClient: &http.Client{ 31 | Timeout: time.Duration(5000) * time.Millisecond, 32 | }, 33 | } 34 | 35 | var err error 36 | sess, err := session.NewSession(config) 37 | if err != nil { 38 | LOG.Crashf("create dynamo connection error[%v]", err) 39 | return nil 40 | } 41 | 42 | var svc *dynamodb.DynamoDB 43 | if logLevel == "debug" { 44 | svc = dynamodb.New(sess, aws.NewConfig().WithLogLevel(aws.LogDebugWithHTTPBody)) 45 | } else { 46 | svc = dynamodb.New(sess) 47 | } 48 | 49 | return &DynamoProxyWriter{ 50 | Name: name, 51 | svc: svc, 52 | ns: ns, 53 | } 54 | } 55 | 56 | func (dpw *DynamoProxyWriter) String() string { 57 | return dpw.Name 58 | } 59 | 60 | func (dpw *DynamoProxyWriter) GetSession() interface{} { 61 | return dpw.svc 62 | } 63 | 64 | func (dpw *DynamoProxyWriter) PassTableDesc(tableDescribe *dynamodb.TableDescription) { 65 | } 66 | 67 | func (dpw *DynamoProxyWriter) CreateTable(tableDescribe *dynamodb.TableDescription) error { 68 | createTableInput := &dynamodb.CreateTableInput{ 69 | AttributeDefinitions: tableDescribe.AttributeDefinitions, 70 | KeySchema: tableDescribe.KeySchema, 71 | TableName: tableDescribe.TableName, 72 | } 73 | 74 | LOG.Info("try create table: %v", *tableDescribe) 75 | 76 | if conf.Options.FullEnableIndexUser { 77 | // convert []*GlobalSecondaryIndexDescription => []*GlobalSecondaryIndex 78 | gsiList := make([]*dynamodb.GlobalSecondaryIndex, 0, len(tableDescribe.GlobalSecondaryIndexes)) 79 | for _, gsiDesc := range tableDescribe.GlobalSecondaryIndexes { 80 | gsiIndex := &dynamodb.GlobalSecondaryIndex{ 81 | IndexName: gsiDesc.IndexName, 82 | KeySchema: gsiDesc.KeySchema, 83 | Projection: gsiDesc.Projection, 84 | // ProvisionedThroughput: gsiDesc.ProvisionedThroughput, 85 | } 86 | 87 | // meaningless, support aliyun_dynamodb 88 | if gsiDesc.Projection == nil { 89 | gsiIndex.Projection = &dynamodb.Projection{} 90 | } 91 | gsiList = append(gsiList, gsiIndex) 92 | } 93 | createTableInput.SetGlobalSecondaryIndexes(gsiList) 94 | 95 | // convert []*LocalSecondaryIndexDescription => []*LocalSecondaryIndex 96 | lsiList := make([]*dynamodb.LocalSecondaryIndex, 0, len(tableDescribe.LocalSecondaryIndexes)) 97 | for _, lsiDesc := range tableDescribe.LocalSecondaryIndexes { 98 | lsiIndex := &dynamodb.LocalSecondaryIndex{ 99 | IndexName: lsiDesc.IndexName, 100 | KeySchema: lsiDesc.KeySchema, 101 | Projection: lsiDesc.Projection, 102 | } 103 | 104 | // meaningless, support aliyun_dynamodb 105 | if lsiDesc.Projection == nil { 106 | lsiIndex.Projection = &dynamodb.Projection{} 107 | } 108 | lsiList = append(lsiList, lsiIndex) 109 | } 110 | createTableInput.SetLocalSecondaryIndexes(lsiList) 111 | } 112 | 113 | _, err := dpw.svc.CreateTable(createTableInput) 114 | if err != nil { 115 | LOG.Error("create table[%v] fail: %v", *tableDescribe.TableName, err) 116 | return err 117 | } 118 | 119 | checkReady := func() bool { 120 | // check table is ready 121 | out, err := dpw.svc.DescribeTable(&dynamodb.DescribeTableInput{ 122 | TableName: tableDescribe.TableName, 123 | }) 124 | if err != nil { 125 | LOG.Warn("create table[%v] ok but describe failed: %v", *tableDescribe.TableName, err) 126 | return true 127 | } 128 | 129 | if *out.Table.TableStatus != "ACTIVE" { 130 | LOG.Warn("create table[%v] ok but describe not ready: %v", *tableDescribe.TableName, *out.Table.TableStatus) 131 | return true 132 | } 133 | 134 | return false 135 | } 136 | 137 | // check with retry 5 times and 1s gap 138 | ok := utils.CallbackRetry(5, 1000, checkReady) 139 | if !ok { 140 | return fmt.Errorf("create table[%v] fail: check ready fail", dpw.ns.Collection) 141 | } 142 | 143 | return nil 144 | } 145 | 146 | func (dpw *DynamoProxyWriter) DropTable() error { 147 | _, err := dpw.svc.DeleteTable(&dynamodb.DeleteTableInput{ 148 | TableName: aws.String(dpw.ns.Collection), 149 | }) 150 | return err 151 | } 152 | 153 | func (dpw *DynamoProxyWriter) WriteBulk(input []interface{}) error { 154 | if len(input) == 0 { 155 | return nil 156 | } 157 | 158 | // convert to WriteRequest 159 | request := make([]*dynamodb.WriteRequest, len(input)) 160 | for i, ele := range input { 161 | request[i] = &dynamodb.WriteRequest{ 162 | PutRequest: &dynamodb.PutRequest{ 163 | Item: ele.(map[string]*dynamodb.AttributeValue), 164 | }, 165 | } 166 | } 167 | 168 | _, err := dpw.svc.BatchWriteItem(&dynamodb.BatchWriteItemInput{ 169 | RequestItems: map[string][]*dynamodb.WriteRequest{ 170 | dpw.ns.Collection: request, 171 | }, 172 | }) 173 | return err 174 | } 175 | 176 | func (dpw *DynamoProxyWriter) Close() { 177 | 178 | } 179 | 180 | // input type is map[string]*dynamodb.AttributeValue 181 | func (dpw *DynamoProxyWriter) Insert(input []interface{}, index []interface{}) error { 182 | if len(input) == 0 { 183 | return nil 184 | } 185 | 186 | request := make([]*dynamodb.WriteRequest, len(index)) 187 | for i, ele := range input { 188 | request[i] = &dynamodb.WriteRequest{ 189 | PutRequest: &dynamodb.PutRequest{ 190 | Item: ele.(map[string]*dynamodb.AttributeValue), 191 | }, 192 | } 193 | } 194 | 195 | _, err := dpw.svc.BatchWriteItem(&dynamodb.BatchWriteItemInput{ 196 | RequestItems: map[string][]*dynamodb.WriteRequest{ 197 | dpw.ns.Collection: request, 198 | }, 199 | }) 200 | 201 | if err != nil && utils.DynamoIgnoreError(err, "i", true) { 202 | LOG.Warn("%s ignore error[%v] when insert", dpw, err) 203 | return nil 204 | } 205 | 206 | return err 207 | } 208 | 209 | func (dpw *DynamoProxyWriter) Delete(index []interface{}) error { 210 | if len(index) == 0 { 211 | return nil 212 | } 213 | 214 | request := make([]*dynamodb.WriteRequest, len(index)) 215 | for i, ele := range index { 216 | request[i] = &dynamodb.WriteRequest{ 217 | DeleteRequest: &dynamodb.DeleteRequest{ 218 | Key: ele.(map[string]*dynamodb.AttributeValue), 219 | }, 220 | } 221 | } 222 | 223 | _, err := dpw.svc.BatchWriteItem(&dynamodb.BatchWriteItemInput{ 224 | RequestItems: map[string][]*dynamodb.WriteRequest{ 225 | dpw.ns.Collection: request, 226 | }, 227 | }) 228 | 229 | if utils.DynamoIgnoreError(err, "d", true) { 230 | LOG.Warn("%s ignore error[%v] when delete", dpw, err) 231 | return nil 232 | } 233 | 234 | return err 235 | } 236 | 237 | // upsert 238 | func (dpw *DynamoProxyWriter) Update(input []interface{}, index []interface{}) error { 239 | if len(input) == 0 { 240 | return nil 241 | } 242 | 243 | // fmt.Println(input, index) 244 | 245 | for i := range input { 246 | val := input[i].(map[string]*dynamodb.AttributeValue) 247 | key := index[i].(map[string]*dynamodb.AttributeValue) 248 | 249 | // why no update interface like BatchWriteItem !!!! 250 | // generate new map(expression-attribute-values) and expression(update-expression) 251 | newMap := make(map[string]*dynamodb.AttributeValue, len(val)) 252 | expressionBuffer := new(bytes.Buffer) 253 | expressionBuffer.WriteString("SET") 254 | cnt := 1 255 | for k, v := range val { 256 | newKey := fmt.Sprintf(":v%d", cnt) 257 | newMap[newKey] = v 258 | 259 | if cnt == 1 { 260 | expressionBuffer.WriteString(fmt.Sprintf(" %s=%s", k, newKey)) 261 | } else { 262 | expressionBuffer.WriteString(fmt.Sprintf(",%s=%s", k, newKey)) 263 | } 264 | 265 | cnt++ 266 | } 267 | 268 | // fmt.Println(newMap) 269 | _, err := dpw.svc.UpdateItem(&dynamodb.UpdateItemInput{ 270 | TableName: aws.String(dpw.ns.Collection), 271 | Key: key, 272 | UpdateExpression: aws.String(expressionBuffer.String()), 273 | ExpressionAttributeValues: newMap, 274 | }) 275 | if err != nil && utils.DynamoIgnoreError(err, "u", true) { 276 | LOG.Warn("%s ignore error[%v] when insert", dpw, err) 277 | return nil 278 | } 279 | } 280 | 281 | return nil 282 | } 283 | -------------------------------------------------------------------------------- /nimo-shake/writer/mongodb_mgo_driver.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "nimo-shake/common" 5 | "fmt" 6 | 7 | LOG "github.com/vinllen/log4go" 8 | "github.com/aws/aws-sdk-go/service/dynamodb" 9 | "github.com/vinllen/mgo" 10 | "github.com/vinllen/mgo/bson" 11 | "nimo-shake/configure" 12 | "strings" 13 | ) 14 | 15 | // deprecated 16 | type MongoWriter struct { 17 | Name string 18 | ns utils.NS 19 | conn *utils.MongoConn 20 | primaryIndexes []*dynamodb.KeySchemaElement 21 | } 22 | 23 | func NewMongoWriter(name, address string, ns utils.NS) *MongoWriter { 24 | targetConn, err := utils.NewMongoConn(address, utils.ConnectModePrimary, true) 25 | if err != nil { 26 | LOG.Error("create mongodb connection error[%v]", err) 27 | return nil 28 | } 29 | 30 | return &MongoWriter{ 31 | Name: name, 32 | ns: ns, 33 | conn: targetConn, 34 | } 35 | } 36 | 37 | func (mw *MongoWriter) String() string { 38 | return mw.Name 39 | } 40 | 41 | func (mw *MongoWriter) GetSession() interface{} { 42 | return mw.conn.Session 43 | } 44 | 45 | func (mw *MongoWriter) PassTableDesc(tableDescribe *dynamodb.TableDescription) { 46 | mw.primaryIndexes = tableDescribe.KeySchema 47 | } 48 | 49 | func (mw *MongoWriter) CreateTable(tableDescribe *dynamodb.TableDescription) error { 50 | // parse primary key with sort key 51 | allIndexes := tableDescribe.AttributeDefinitions 52 | primaryIndexes := tableDescribe.KeySchema 53 | globalSecondaryIndexes := tableDescribe.GlobalSecondaryIndexes 54 | 55 | mw.primaryIndexes = primaryIndexes 56 | LOG.Info("%s table[%s] primary index length: %v", mw.String(), *tableDescribe.TableName, len(mw.primaryIndexes)) 57 | 58 | // parse index type 59 | parseMap := utils.ParseIndexType(allIndexes) 60 | 61 | // create primary key if has 62 | if len(primaryIndexes) == 0 { 63 | LOG.Info("%s no index found", mw) 64 | return nil 65 | } 66 | 67 | // check if legal 68 | if len(primaryIndexes) > 2 { 69 | return fmt.Errorf("%s illegal primary index[%v] number, should <= 2", mw, len(primaryIndexes)) 70 | } 71 | 72 | if conf.Options.FullEnableIndexPrimary { 73 | LOG.Info("%s try create primary index", mw) 74 | // create primary index 75 | if err := mw.createPrimaryIndex(primaryIndexes, parseMap); err != nil { 76 | return err 77 | } 78 | 79 | // create user index 80 | if conf.Options.FullEnableIndexUser { 81 | LOG.Info("%s try create user index", mw) 82 | // create user index 83 | if err := mw.createUserIndex(globalSecondaryIndexes, parseMap); err != nil { 84 | return err 85 | } 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func (mw *MongoWriter) DropTable() error { 93 | err := mw.conn.Session.DB(mw.ns.Database).C(mw.ns.Collection).DropCollection() 94 | if err != nil && err.Error() == utils.NsNotFountErr { 95 | return nil 96 | } 97 | return err 98 | } 99 | 100 | func (mw *MongoWriter) WriteBulk(input []interface{}) error { 101 | if len(input) == 0 { 102 | return nil 103 | } 104 | 105 | bulk := mw.conn.Session.DB(mw.ns.Database).C(mw.ns.Collection).Bulk() 106 | bulk.Unordered() 107 | bulk.Insert(input...) 108 | if _, err := bulk.Run(); err != nil { 109 | if mgo.IsDup(err) { 110 | LOG.Warn("%s duplicated document found[%v]. reinsert or update", err, mw) 111 | if !conf.Options.FullExecutorInsertOnDupUpdate || len(mw.primaryIndexes) == 0 { 112 | LOG.Error("full.executor.insert_on_dup_update==[%v], primaryIndexes length[%v]", conf.Options.FullExecutorInsertOnDupUpdate, 113 | len(mw.primaryIndexes)) 114 | return err 115 | } 116 | 117 | // 1. generate index list 118 | indexList := make([]interface{}, len(input)) 119 | for i, ele := range input { 120 | inputData := ele.(bson.M) 121 | index := make(bson.M, len(mw.primaryIndexes)) 122 | for _, primaryIndex := range mw.primaryIndexes { 123 | // currently, we only support convert type == 'convert', so there is no type inside 124 | key := *primaryIndex.AttributeName 125 | if _, ok := inputData[key]; !ok { 126 | LOG.Error("primary key[%v] is not exists on input data[%v]", 127 | *primaryIndex.AttributeName, inputData) 128 | } else { 129 | index[key] = inputData[key] 130 | } 131 | } 132 | indexList[i] = index 133 | } 134 | 135 | LOG.Debug(indexList) 136 | 137 | return mw.updateOnInsert(input, indexList) 138 | } 139 | return fmt.Errorf("%s insert docs with length[%v] into ns[%s] of dest mongo failed[%v]. first doc: %v", 140 | mw, len(input), mw.ns, err, input[0]) 141 | } 142 | return nil 143 | } 144 | 145 | func (mw *MongoWriter) Close() { 146 | mw.conn.Close() 147 | } 148 | 149 | func (mw *MongoWriter) Insert(input []interface{}, index []interface{}) error { 150 | bulk := mw.conn.Session.DB(mw.ns.Database).C(mw.ns.Collection).Bulk() 151 | bulk.Unordered() 152 | bulk.Insert(input...) 153 | 154 | if _, err := bulk.Run(); err != nil { 155 | if utils.MongodbIgnoreError(err, "i", false) { 156 | LOG.Warn("%s ignore error[%v] when insert", mw, err) 157 | return nil 158 | } 159 | 160 | // duplicate key 161 | if mgo.IsDup(err) { 162 | if conf.Options.IncreaseExecutorInsertOnDupUpdate { 163 | LOG.Warn("%s duplicated document found. reinsert or update", mw) 164 | return mw.updateOnInsert(input, index) 165 | } 166 | } 167 | return err 168 | } 169 | return nil 170 | } 171 | 172 | func (mw *MongoWriter) updateOnInsert(input []interface{}, index []interface{}) error { 173 | // upsert one by one 174 | for i := range input { 175 | LOG.Debug("upsert: selector[%v] update[%v]", index[i], input[i]) 176 | _, err := mw.conn.Session.DB(mw.ns.Database).C(mw.ns.Collection).Upsert(index[i], input[i]) 177 | if err != nil { 178 | if utils.MongodbIgnoreError(err, "u", true) { 179 | LOG.Warn("%s ignore error[%v] when upsert", mw, err) 180 | return nil 181 | } 182 | 183 | return err 184 | } 185 | } 186 | return nil 187 | } 188 | 189 | func (mw *MongoWriter) Delete(index []interface{}) error { 190 | bulk := mw.conn.Session.DB(mw.ns.Database).C(mw.ns.Collection).Bulk() 191 | bulk.Unordered() 192 | bulk.Remove(index...) 193 | 194 | if _, err := bulk.Run(); err != nil { 195 | LOG.Warn(err) 196 | // always ignore ns not found error 197 | if utils.MongodbIgnoreError(err, "d", true) { 198 | LOG.Warn("%s ignore error[%v] when delete", mw, err) 199 | return nil 200 | } 201 | 202 | return err 203 | } 204 | 205 | return nil 206 | } 207 | 208 | func (mw *MongoWriter) Update(input []interface{}, index []interface{}) error { 209 | updates := make([]interface{}, 0, len(input)*2) 210 | for i := range input { 211 | updates = append(updates, index[i]) 212 | updates = append(updates, input[i]) 213 | } 214 | 215 | bulk := mw.conn.Session.DB(mw.ns.Database).C(mw.ns.Collection).Bulk() 216 | if conf.Options.IncreaseExecutorUpsert { 217 | bulk.Upsert(updates...) 218 | } else { 219 | bulk.Update(updates...) 220 | } 221 | 222 | if _, err := bulk.Run(); err != nil { 223 | LOG.Warn(err) 224 | // parse error 225 | idx, _, _ := utils.FindFirstErrorIndexAndMessage(err.Error()) 226 | if idx == -1 { 227 | return err 228 | } 229 | 230 | // always upsert data 231 | if utils.MongodbIgnoreError(err, "u", true) { 232 | return mw.updateOnInsert(input[idx:], index[idx:]) 233 | } 234 | 235 | if mgo.IsDup(err) { 236 | LOG.Info("error[%v] is dup, ignore", err) 237 | return mw.updateOnInsert(input[idx+1:], index[idx+1:]) 238 | } 239 | return err 240 | } 241 | 242 | return nil 243 | } 244 | 245 | func (mw *MongoWriter) createPrimaryIndex(primaryIndexes []*dynamodb.KeySchemaElement, parseMap map[string]string) error { 246 | primaryKeyWithType, err := mw.createSingleIndex(primaryIndexes, parseMap, true) 247 | if err != nil { 248 | return err 249 | } 250 | 251 | // write shard key if target mongodb is sharding 252 | if conf.Options.TargetMongoDBType == utils.TargetMongoDBTypeSharding { 253 | err := mw.conn.Session.DB("admin").Run(bson.D{ 254 | {Name: "enablesharding", Value: mw.ns.Database}, 255 | }, nil) 256 | if err != nil { 257 | if strings.Contains(err.Error(), "sharding already enabled") == false { 258 | return fmt.Errorf("enable sharding failed[%v]", err) 259 | } 260 | LOG.Warn("ns[%s] sharding already enabled: %v", mw.ns, err) 261 | } 262 | 263 | err = mw.conn.Session.DB("admin").Run(bson.D{ 264 | {Name: "shardCollection", Value: mw.ns.Str()}, 265 | {Name: "key", Value: bson.M{primaryKeyWithType: "hashed"}}, 266 | {Name: "options", Value: bson.M{"numInitialChunks": NumInitialChunks}}, 267 | }, nil) 268 | if err != nil { 269 | return fmt.Errorf("shard collection[%s] failed[%v]", mw.ns, err) 270 | } 271 | } 272 | 273 | return nil 274 | } 275 | 276 | func (mw *MongoWriter) createUserIndex(globalSecondaryIndexes []*dynamodb.GlobalSecondaryIndexDescription, parseMap map[string]string) error { 277 | for _, gsi := range globalSecondaryIndexes { 278 | primaryIndexes := gsi.KeySchema 279 | // duplicate index will be ignored by MongoDB 280 | if _, err := mw.createSingleIndex(primaryIndexes, parseMap, false); err != nil { 281 | LOG.Error("ns[%s] create users' single index failed[%v]", mw.ns, err) 282 | return err 283 | } 284 | } 285 | return nil 286 | } 287 | 288 | func (mw *MongoWriter) createSingleIndex(primaryIndexes []*dynamodb.KeySchemaElement, parseMap map[string]string, 289 | isPrimaryKey bool) (string, error) { 290 | primaryKey, sortKey, err := utils.ParsePrimaryAndSortKey(primaryIndexes, parseMap) 291 | if err != nil { 292 | return "", fmt.Errorf("parse primary and sort key failed[%v]", err) 293 | } 294 | 295 | primaryKeyWithType := mw.fetchKey(primaryKey, parseMap[primaryKey]) 296 | indexList := make([]string, 0, 2) 297 | indexList = append(indexList, primaryKeyWithType) 298 | if sortKey != "" { 299 | indexList = append(indexList, mw.fetchKey(sortKey, parseMap[sortKey])) 300 | } 301 | 302 | LOG.Info("ns[%s] single index[%v] list[%v]", mw.ns, primaryKeyWithType, indexList) 303 | 304 | // primary key should be unique 305 | unique := isPrimaryKey 306 | 307 | // create union unique index 308 | if len(indexList) >= 2 { 309 | // write index 310 | index := mgo.Index{ 311 | Key: indexList, 312 | Background: true, 313 | Unique: unique, 314 | } 315 | 316 | LOG.Info("create union-index isPrimary[%v]: %v", isPrimaryKey, index.Key) 317 | 318 | if err := mw.conn.Session.DB(mw.ns.Database).C(mw.ns.Collection).EnsureIndex(index); err != nil { 319 | return "", fmt.Errorf("create primary union unique[%v] index failed[%v]", unique, err) 320 | } 321 | } 322 | 323 | var indexType interface{} 324 | indexType = "hashed" 325 | if conf.Options.TargetMongoDBType == utils.TargetMongoDBTypeReplica { 326 | indexType = 1 327 | } 328 | if len(indexList) >= 2 { 329 | // unique has already be set on the above index 330 | unique = false 331 | } else if unique { 332 | // must be range if only has 1 key 333 | indexType = 1 334 | } 335 | 336 | doc := bson.D{ 337 | {Name: "createIndexes", Value: mw.ns.Collection}, 338 | {Name: "indexes", Value: []bson.M{ 339 | { 340 | "key": bson.M{ 341 | primaryKeyWithType: indexType, 342 | }, 343 | "name": fmt.Sprintf("%s_%v", primaryKeyWithType, indexType), 344 | "background": true, 345 | "unique": unique, 346 | }, 347 | }}, 348 | } 349 | LOG.Info("create index isPrimary[%v]: %v", isPrimaryKey, doc) 350 | // create hash key only 351 | if err := mw.conn.Session.DB(mw.ns.Database).Run(doc, nil); err != nil { 352 | return "", fmt.Errorf("create primary[%v] %v index failed[%v]", isPrimaryKey, indexType, err) 353 | } 354 | 355 | return primaryKeyWithType, nil 356 | } 357 | 358 | func (mw *MongoWriter) fetchKey(key, tp string) string { 359 | switch conf.Options.ConvertType { 360 | case utils.ConvertTypeChange: 361 | fallthrough 362 | case utils.ConvertTypeSame: 363 | return key 364 | case utils.ConvertTypeRaw: 365 | return fmt.Sprintf("%s.%s", key, tp) 366 | } 367 | return "" 368 | } 369 | -------------------------------------------------------------------------------- /nimo-shake/writer/writer.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "nimo-shake/common" 5 | 6 | LOG "github.com/vinllen/log4go" 7 | "github.com/aws/aws-sdk-go/service/dynamodb" 8 | ) 9 | 10 | type Writer interface{ 11 | // create table 12 | CreateTable(tableDescribe *dynamodb.TableDescription) error 13 | // pass table description 14 | PassTableDesc(tableDescribe *dynamodb.TableDescription) 15 | // drop table 16 | DropTable() error 17 | // write bulk data, used in full sync 18 | WriteBulk(input []interface{}) error 19 | // insert 20 | Insert(input []interface{}, index []interface{}) error 21 | // delete 22 | Delete(input []interface{}) error 23 | // update 24 | Update(input []interface{}, index []interface{}) error 25 | // close 26 | Close() 27 | // get session 28 | GetSession() interface{} 29 | } 30 | 31 | func NewWriter(name, address string, ns utils.NS, logLevel string) Writer { 32 | switch name { 33 | case utils.TargetTypeMongo: 34 | // return NewMongoWriter(name, address, ns) 35 | return NewMongoCommunityWriter(name, address, ns) 36 | case utils.TargetTypeAliyunDynamoProxy: 37 | return NewDynamoProxyWriter(name, address, ns, logLevel) 38 | default: 39 | LOG.Crashf("unknown writer[%v]", name) 40 | } 41 | return nil 42 | } -------------------------------------------------------------------------------- /scripts/hypervisor.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Compilation: 3 | * gcc -Wall -O3 hypervisor.c -o hypervisor 4 | * 5 | * */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #ifdef __linux__ 13 | #include 14 | #else 15 | #include 16 | #endif 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | 26 | #define MAXOPT 256 27 | #define INTERVAL 5 28 | #define MAXINTERVAL 180 29 | 30 | #define USAGE() do{ \ 31 | fprintf(stderr, "Usage : %s [--daemon] --exec=\"command arg1 arg2 arg3 ...\"\n", argv[0]); \ 32 | }while(0) 33 | 34 | static char *cmd, *cmdopt[MAXOPT + 1]; 35 | static int daemonize; 36 | 37 | static int parseopt(int argc, char *argv[]); 38 | static int set_nonblock(int fd); 39 | static int getstatus(char *buf, int size, int status); 40 | 41 | static int parseopt(int argc, char *argv[]) 42 | { 43 | int ch, i; 44 | char *token, *tmpptr, *cmdstr; 45 | 46 | cmdstr = cmd = NULL; 47 | daemonize = 0; 48 | for(i = 0; i < MAXOPT + 1; i++){ 49 | cmdopt[i] = NULL; 50 | } 51 | 52 | struct option long_options[] = { 53 | {"daemon",optional_argument,NULL,'d'}, 54 | {"exec",required_argument,NULL,'e'}, 55 | {0,0,0,0}, 56 | }; 57 | 58 | while((ch=getopt_long(argc, argv, "dec:", long_options, NULL)) != -1) { 59 | switch(ch) 60 | { 61 | case 'e': 62 | if((cmdstr = strdup(optarg)) == NULL ) 63 | return -1; 64 | break; 65 | case 'd': 66 | daemonize = 1; 67 | break; 68 | default: 69 | USAGE(); 70 | return -1; 71 | } 72 | } 73 | 74 | if(cmdstr == NULL){ 75 | USAGE(); 76 | return -1; 77 | } 78 | 79 | for(i = 0;i < MAXOPT + 1;cmdstr = NULL, i++){ 80 | token = strtok_r(cmdstr, " \t\n", &tmpptr); 81 | if(token == NULL){ 82 | break; 83 | } else { 84 | cmdopt[i] = strdup(token); 85 | 86 | if(i == 0){ 87 | cmd = strdup(token); 88 | } 89 | } 90 | } 91 | 92 | if( (cmd == NULL) || (strlen(cmd) == 0) ){ 93 | fprintf(stderr, "Error, cmd should not be empty.\n"); 94 | return -1; 95 | } 96 | 97 | if(i == MAXOPT + 1){ 98 | fprintf(stderr, "Argument too long\n"); 99 | return -1; 100 | } 101 | 102 | cmdopt[i] = NULL; 103 | 104 | return 0; 105 | } 106 | 107 | static int set_nonblock(int fd) 108 | { 109 | int flags = fcntl(fd, F_GETFL, 0); 110 | if (flags == -1) { 111 | return -1; 112 | } 113 | return fcntl(fd, F_SETFL, flags | O_NONBLOCK); 114 | } 115 | 116 | static int getstatus(char *buf, int size, int status) 117 | { 118 | int n, len; 119 | 120 | len = size; 121 | 122 | if(WIFEXITED(status)){ 123 | n = snprintf(buf, len, "- normal termination, exit status = %d\n", WEXITSTATUS(status)); 124 | } else if(WIFSIGNALED(status)) { 125 | n = snprintf(buf, len, "- abnormal termination, signal number = %d%s", 126 | WTERMSIG(status), 127 | #ifdef WCOREDUMP 128 | WCOREDUMP(status) ? "-> core file generated" : ""); 129 | #else 130 | ""); 131 | #endif 132 | } else if(WIFSTOPPED(status)) { 133 | n = snprintf(buf, len, "child stopped, signal number = %d\n", WSTOPSIG(status)); 134 | } 135 | 136 | return n; 137 | } 138 | 139 | 140 | 141 | void go_daemon() { 142 | int fd; 143 | 144 | if (fork() != 0) exit(0); /* parent exits */ 145 | setsid(); /* create a new session */ 146 | 147 | /* Every output goes to /dev/null. If Redis is daemonized but 148 | * * the 'logfile' is set to 'stdout' in the configuration file 149 | * * it will not log at all. */ 150 | if ((fd = open("/tmp/mongo4bls.output", O_RDWR, 0)) != -1) { 151 | dup2(fd, STDIN_FILENO); 152 | dup2(fd, STDOUT_FILENO); 153 | dup2(fd, STDERR_FILENO); 154 | if (fd > STDERR_FILENO) close(fd); 155 | } 156 | } 157 | 158 | 159 | int main(int argc, char *argv[]) 160 | { 161 | int ssec = INTERVAL, ret, status; 162 | int first_start = 1; 163 | int pipefd[2], waited, alive, isdaemon; 164 | char buf[1024], info[4096]; 165 | pid_t pid; 166 | time_t now, last = time(NULL); 167 | 168 | if((ret = parseopt(argc, argv)) < 0 ) 169 | exit(ret); 170 | 171 | daemonize ? go_daemon() : 0; 172 | 173 | while(1){ 174 | if(pipe(pipefd) < 0){ 175 | fprintf(stderr, "- make pipe error : %s\n", strerror(errno)); 176 | exit(-1); 177 | } 178 | 179 | if( (ret = set_nonblock(pipefd[0])) < 0 ){ 180 | fprintf(stderr, "- set read nonblock error : %s\n", strerror(errno)); 181 | exit(-1); 182 | } 183 | 184 | if((pid = fork()) < 0){ 185 | fprintf(stderr, "- call fork() error : %s\n", strerror(errno)); 186 | exit(-1); 187 | } else if (pid > 0){ 188 | close(pipefd[1]); 189 | alive = waited = 1; 190 | isdaemon = 0; 191 | while(alive){ 192 | if(waited){ 193 | if(pid != waitpid(pid, &status, 0)){ 194 | sleep(INTERVAL); 195 | continue; 196 | } else { 197 | fprintf(stderr, "- child process[%d] terminated .\n",pid); 198 | if (first_start && (time(NULL)-last)<=5) { 199 | fprintf(stderr,"- child process killed in %ld seconds , may wrong ! exit !\n",(time(NULL)-last)); 200 | exit(-1); 201 | } else 202 | first_start = 0; 203 | waited = 0; 204 | } 205 | } 206 | 207 | ret = read(pipefd[0], buf, sizeof(buf)); 208 | if(ret < 0){ 209 | if(errno == EAGAIN){ 210 | if(isdaemon == 0){ 211 | fprintf(stderr, "- this daemon process has no output !.\n"); 212 | isdaemon = 1; 213 | } 214 | sleep(INTERVAL); 215 | continue; 216 | } else { 217 | fprintf(stderr, "- read pipe error : %s\n", strerror(errno)); 218 | exit(-1); 219 | } 220 | } else if(ret == 0) { 221 | alive = 0; 222 | close(pipefd[0]); 223 | fprintf(stderr, "- read zero from pipe of children.\n"); 224 | if(isdaemon == 0){ 225 | getstatus(info, sizeof(info), status); 226 | fprintf(stderr, "- extra info: %s\n", info); 227 | } else { 228 | strcpy(info, ""); 229 | } 230 | continue; 231 | } else { 232 | fprintf(stderr, " - read pipe return: %d bytes\n", ret); 233 | exit(-1); 234 | } 235 | } 236 | 237 | fprintf(stderr, "- process: \"%s\" exit, restart it\n", cmd); 238 | 239 | sleep(ssec); 240 | 241 | now = time(NULL); 242 | if(now - last > 3600){ 243 | ssec = INTERVAL; 244 | last = now; 245 | } else { 246 | ssec = (ssec << 1) < MAXINTERVAL ? (ssec << 1) : MAXINTERVAL; 247 | } 248 | } else { 249 | close(pipefd[0]); 250 | fprintf(stderr, "- execute: \"%s\"\n", cmd); 251 | if(execvp(cmd, cmdopt) < 0){ 252 | fprintf(stderr, "- execute: \"%s\" error, %s\n", cmd, strerror(errno)); 253 | exit(-1); 254 | } 255 | 256 | } 257 | } 258 | 259 | return 0; 260 | } 261 | -------------------------------------------------------------------------------- /scripts/run_ut_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # -*- coding:utf-8 -*- 4 | 5 | import os 6 | from os import listdir 7 | from os.path import isfile, join, isdir 8 | import subprocess 9 | 10 | go_path="." 11 | """ 12 | run unit test in recursive 13 | """ 14 | def run_ut(cur_path): 15 | print(os.path.abspath(os.path.curdir), cur_path) 16 | 17 | only_files = [f for f in listdir(".") if isfile(join(".", f))] 18 | only_dirs = [f for f in listdir(".") if isdir(join(".", f))] 19 | ut_files = [f for f in only_files if "_test.go" in f] 20 | print(only_files, only_dirs, ut_files) 21 | 22 | if len(ut_files) != 0: 23 | # with ut file, need run ut test 24 | print("----------- run ut test on dir[%s] -----------" % os.path.abspath(os.path.curdir)) 25 | ret = subprocess.call(["go", "test"]) 26 | # subprocess.check_output(["/bin/sh", "-c", "go", "test"]) 27 | 28 | print("********************************** %s *************************************" % ("OK" if ret == 0 else "FAIL")) 29 | if ret != 0: 30 | print("run failed") 31 | exit(ret) 32 | 33 | for dir in only_dirs: 34 | print("cd dir[%s]" % dir) 35 | 36 | # dfs 37 | os.chdir(dir) 38 | run_ut(dir) 39 | 40 | # backtracking 41 | os.chdir("..") 42 | 43 | if __name__ == "__main__": 44 | root_path = os.path.join("..", "src/nimo-shake") 45 | os.chdir(root_path) 46 | go_path=os.path.abspath("../..") 47 | print("GOPATH=%s" % go_path) 48 | #subprocess.call(["export GOPATH=%s" % go_path]) 49 | os.environ['GOPATH'] = go_path 50 | run_ut(".") 51 | 52 | print("-----------------------------------") 53 | print("all is well ^_^") 54 | -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | catalog=$(dirname "$0") 4 | 5 | cd "${catalog}" || exit 1 6 | 7 | if [ $# != 2 ] ; then 8 | echo "USAGE: $0 [conf] [mode]" 9 | exit 0 10 | fi 11 | 12 | name="dynamo-shake" 13 | 14 | if [ "Darwin" == "$(uname -s)" ];then 15 | printf "\\nWARNING !!! MacOs doesn't supply to use this script, please use \"./%s -conf=config_file_name\" manual command to run\\n" "$name" 16 | exit 1 17 | fi 18 | 19 | ./hypervisor --daemon --exec="./$name -conf=$1 -type=$2 1>>$name.output 2>&1" 1>>hypervisor.output 2>&1 20 | -------------------------------------------------------------------------------- /scripts/stop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #kill -9 "$(cat "$1")" 3 | if [ $# != 1 ] ; then 4 | echo "USAGE: $0 [pid filename which by default is 'dynamo-shake.pid']" 5 | exit 0 6 | fi 7 | ppid=$(ps -ef | awk '{if ($2=='`cat $1`') print $3}') 8 | [ -z $ppid ] && echo "[Fail] No process number for $(cat "$1")." && exit 1 9 | if [ $ppid -eq 1 ];then 10 | kill -9 "$(cat "$1")" 11 | else 12 | kill -9 "$ppid" "$(cat "$1")" 13 | fi 14 | --------------------------------------------------------------------------------