github.com/susy-go/susy-graviton@v0.0.0-20190614130430-36cddae42305/swarm/network_test.go (about) 1 // Copyleft 2018 The susy-graviton Authors 2 // This file is part of the susy-graviton library. 3 // 4 // The susy-graviton library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The susy-graviton library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MSRCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the susy-graviton library. If not, see <http://www.gnu.org/licenses/>. 16 17 package swarm 18 19 import ( 20 "context" 21 "flag" 22 "fmt" 23 "io/ioutil" 24 "math/rand" 25 "os" 26 "sync" 27 "sync/atomic" 28 "testing" 29 "time" 30 31 "github.com/susy-go/susy-graviton/crypto" 32 "github.com/susy-go/susy-graviton/log" 33 "github.com/susy-go/susy-graviton/node" 34 "github.com/susy-go/susy-graviton/p2p/enode" 35 "github.com/susy-go/susy-graviton/p2p/simulations/adapters" 36 "github.com/susy-go/susy-graviton/swarm/api" 37 "github.com/susy-go/susy-graviton/swarm/network/simulation" 38 "github.com/susy-go/susy-graviton/swarm/storage" 39 colorable "github.com/mattn/go-colorable" 40 ) 41 42 var ( 43 loglevel = flag.Int("loglevel", 2, "verbosity of logs") 44 longrunning = flag.Bool("longrunning", false, "do run long-running tests") 45 waitKademlia = flag.Bool("waitkademlia", false, "wait for healthy kademlia before checking files availability") 46 ) 47 48 func init() { 49 rand.Seed(time.Now().UnixNano()) 50 51 flag.Parse() 52 53 log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) 54 } 55 56 // TestSwarmNetwork runs a series of test simulations with 57 // static and dynamic Swarm nodes in network simulation, by 58 // uploading files to every node and retrieving them. 59 func TestSwarmNetwork(t *testing.T) { 60 for _, tc := range []struct { 61 name string 62 steps []testSwarmNetworkStep 63 options *testSwarmNetworkOptions 64 disabled bool 65 }{ 66 { 67 name: "10_nodes", 68 steps: []testSwarmNetworkStep{ 69 { 70 nodeCount: 10, 71 }, 72 }, 73 options: &testSwarmNetworkOptions{ 74 Timeout: 45 * time.Second, 75 }, 76 }, 77 { 78 name: "10_nodes_skip_check", 79 steps: []testSwarmNetworkStep{ 80 { 81 nodeCount: 10, 82 }, 83 }, 84 options: &testSwarmNetworkOptions{ 85 Timeout: 45 * time.Second, 86 SkipCheck: true, 87 }, 88 }, 89 { 90 name: "50_nodes", 91 steps: []testSwarmNetworkStep{ 92 { 93 nodeCount: 50, 94 }, 95 }, 96 options: &testSwarmNetworkOptions{ 97 Timeout: 3 * time.Minute, 98 }, 99 disabled: !*longrunning, 100 }, 101 { 102 name: "50_nodes_skip_check", 103 steps: []testSwarmNetworkStep{ 104 { 105 nodeCount: 50, 106 }, 107 }, 108 options: &testSwarmNetworkOptions{ 109 Timeout: 3 * time.Minute, 110 SkipCheck: true, 111 }, 112 disabled: !*longrunning, 113 }, 114 { 115 name: "inc_node_count", 116 steps: []testSwarmNetworkStep{ 117 { 118 nodeCount: 2, 119 }, 120 { 121 nodeCount: 5, 122 }, 123 { 124 nodeCount: 10, 125 }, 126 }, 127 options: &testSwarmNetworkOptions{ 128 Timeout: 90 * time.Second, 129 }, 130 disabled: !*longrunning, 131 }, 132 { 133 name: "dec_node_count", 134 steps: []testSwarmNetworkStep{ 135 { 136 nodeCount: 10, 137 }, 138 { 139 nodeCount: 6, 140 }, 141 { 142 nodeCount: 3, 143 }, 144 }, 145 options: &testSwarmNetworkOptions{ 146 Timeout: 90 * time.Second, 147 }, 148 disabled: !*longrunning, 149 }, 150 { 151 name: "dec_inc_node_count", 152 steps: []testSwarmNetworkStep{ 153 { 154 nodeCount: 3, 155 }, 156 { 157 nodeCount: 1, 158 }, 159 { 160 nodeCount: 5, 161 }, 162 }, 163 options: &testSwarmNetworkOptions{ 164 Timeout: 90 * time.Second, 165 }, 166 }, 167 { 168 name: "inc_dec_node_count", 169 steps: []testSwarmNetworkStep{ 170 { 171 nodeCount: 3, 172 }, 173 { 174 nodeCount: 5, 175 }, 176 { 177 nodeCount: 25, 178 }, 179 { 180 nodeCount: 10, 181 }, 182 { 183 nodeCount: 4, 184 }, 185 }, 186 options: &testSwarmNetworkOptions{ 187 Timeout: 5 * time.Minute, 188 }, 189 disabled: !*longrunning, 190 }, 191 { 192 name: "inc_dec_node_count_skip_check", 193 steps: []testSwarmNetworkStep{ 194 { 195 nodeCount: 3, 196 }, 197 { 198 nodeCount: 5, 199 }, 200 { 201 nodeCount: 25, 202 }, 203 { 204 nodeCount: 10, 205 }, 206 { 207 nodeCount: 4, 208 }, 209 }, 210 options: &testSwarmNetworkOptions{ 211 Timeout: 5 * time.Minute, 212 SkipCheck: true, 213 }, 214 disabled: !*longrunning, 215 }, 216 } { 217 if tc.disabled { 218 continue 219 } 220 t.Run(tc.name, func(t *testing.T) { 221 testSwarmNetwork(t, tc.options, tc.steps...) 222 }) 223 } 224 } 225 226 // testSwarmNetworkStep is the configuration 227 // for the state of the simulation network. 228 type testSwarmNetworkStep struct { 229 // number of swarm nodes that must be in the Up state 230 nodeCount int 231 } 232 233 // file represents the file uploaded on a particular node. 234 type file struct { 235 addr storage.Address 236 data string 237 nodeID enode.ID 238 } 239 240 // check represents a reference to a file that is retrieved 241 // from a particular node. 242 type check struct { 243 key string 244 nodeID enode.ID 245 } 246 247 // testSwarmNetworkOptions contains optional parameters for running 248 // testSwarmNetwork. 249 type testSwarmNetworkOptions struct { 250 Timeout time.Duration 251 SkipCheck bool 252 } 253 254 // testSwarmNetwork is a helper function used for testing different 255 // static and dynamic Swarm network simulations. 256 // It is responsible for: 257 // - Setting up a Swarm network simulation, and updates the number of nodes within the network on every step according to steps. 258 // - Uploading a unique file to every node on every step. 259 // - May wait for Kademlia on every node to be healthy. 260 // - Checking if a file is retrievable from all nodes. 261 func testSwarmNetwork(t *testing.T, o *testSwarmNetworkOptions, steps ...testSwarmNetworkStep) { 262 263 if o == nil { 264 o = new(testSwarmNetworkOptions) 265 } 266 267 sim := simulation.New(map[string]simulation.ServiceFunc{ 268 "swarm": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { 269 config := api.NewConfig() 270 271 dir, err := ioutil.TempDir("", "swarm-network-test-node") 272 if err != nil { 273 return nil, nil, err 274 } 275 cleanup = func() { 276 err := os.RemoveAll(dir) 277 if err != nil { 278 log.Error("cleaning up swarm temp dir", "err", err) 279 } 280 } 281 282 config.Path = dir 283 284 privkey, err := crypto.GenerateKey() 285 if err != nil { 286 return nil, cleanup, err 287 } 288 289 config.Init(privkey) 290 config.DeliverySkipCheck = o.SkipCheck 291 config.Port = "" 292 293 swarm, err := NewSwarm(config, nil) 294 if err != nil { 295 return nil, cleanup, err 296 } 297 bucket.Store(simulation.BucketKeyKademlia, swarm.bzz.Hive.Kademlia) 298 log.Info("new swarm", "bzzKey", config.BzzKey, "baseAddr", fmt.Sprintf("%x", swarm.bzz.BaseAddr())) 299 return swarm, cleanup, nil 300 }, 301 }) 302 defer sim.Close() 303 304 ctx := context.Background() 305 if o.Timeout > 0 { 306 var cancel context.CancelFunc 307 ctx, cancel = context.WithTimeout(ctx, o.Timeout) 308 defer cancel() 309 } 310 311 files := make([]file, 0) 312 313 for i, step := range steps { 314 log.Debug("test sync step", "n", i+1, "nodes", step.nodeCount) 315 316 change := step.nodeCount - len(sim.UpNodeIDs()) 317 318 if change > 0 { 319 _, err := sim.AddNodesAndConnectChain(change) 320 if err != nil { 321 t.Fatal(err) 322 } 323 } else if change < 0 { 324 _, err := sim.StopRandomNodes(-change) 325 if err != nil { 326 t.Fatal(err) 327 } 328 } else { 329 t.Logf("step %v: no change in nodes", i) 330 continue 331 } 332 333 var checkStatusM sync.Map 334 var nodeStatusM sync.Map 335 var totalFoundCount uint64 336 337 result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { 338 nodeIDs := sim.UpNodeIDs() 339 rand.Shuffle(len(nodeIDs), func(i, j int) { 340 nodeIDs[i], nodeIDs[j] = nodeIDs[j], nodeIDs[i] 341 }) 342 for _, id := range nodeIDs { 343 key, data, err := uploadFile(sim.Service("swarm", id).(*Swarm)) 344 if err != nil { 345 return err 346 } 347 log.Trace("file uploaded", "node", id, "key", key.String()) 348 files = append(files, file{ 349 addr: key, 350 data: data, 351 nodeID: id, 352 }) 353 } 354 355 if *waitKademlia { 356 if _, err := sim.WaitTillHealthy(ctx); err != nil { 357 return err 358 } 359 } 360 361 // File retrieval check is repeated until all uploaded files are retrieved from all nodes 362 // or until the timeout is reached. 363 for { 364 if retrieve(sim, files, &checkStatusM, &nodeStatusM, &totalFoundCount) == 0 { 365 return nil 366 } 367 } 368 }) 369 370 if result.Error != nil { 371 t.Fatal(result.Error) 372 } 373 log.Debug("done: test sync step", "n", i+1, "nodes", step.nodeCount) 374 } 375 } 376 377 // uploadFile, uploads a short file to the swarm instance 378 // using the api.Put method. 379 func uploadFile(swarm *Swarm) (storage.Address, string, error) { 380 b := make([]byte, 8) 381 _, err := rand.Read(b) 382 if err != nil { 383 return nil, "", err 384 } 385 // File data is very short, but it is ensured that its 386 // uniqueness is very certain. 387 data := fmt.Sprintf("test content %s %x", time.Now().Round(0), b) 388 ctx := context.TODO() 389 k, wait, err := swarm.api.Put(ctx, data, "text/plain", false) 390 if err != nil { 391 return nil, "", err 392 } 393 if wait != nil { 394 err = wait(ctx) 395 } 396 return k, data, err 397 } 398 399 // retrieve is the function that is used for checking the availability of 400 // uploaded files in testSwarmNetwork test helper function. 401 func retrieve( 402 sim *simulation.Simulation, 403 files []file, 404 checkStatusM *sync.Map, 405 nodeStatusM *sync.Map, 406 totalFoundCount *uint64, 407 ) (missing uint64) { 408 rand.Shuffle(len(files), func(i, j int) { 409 files[i], files[j] = files[j], files[i] 410 }) 411 412 var totalWg sync.WaitGroup 413 errc := make(chan error) 414 415 nodeIDs := sim.UpNodeIDs() 416 417 totalCheckCount := len(nodeIDs) * len(files) 418 419 for _, id := range nodeIDs { 420 if _, ok := nodeStatusM.Load(id); ok { 421 continue 422 } 423 start := time.Now() 424 var checkCount uint64 425 var foundCount uint64 426 427 totalWg.Add(1) 428 429 var wg sync.WaitGroup 430 431 swarm := sim.Service("swarm", id).(*Swarm) 432 for _, f := range files { 433 434 checkKey := check{ 435 key: f.addr.String(), 436 nodeID: id, 437 } 438 if n, ok := checkStatusM.Load(checkKey); ok && n.(int) == 0 { 439 continue 440 } 441 442 checkCount++ 443 wg.Add(1) 444 go func(f file, id enode.ID) { 445 defer wg.Done() 446 447 log.Debug("api get: check file", "node", id.String(), "key", f.addr.String(), "total files found", atomic.LoadUint64(totalFoundCount)) 448 449 r, _, _, _, err := swarm.api.Get(context.TODO(), api.NOOPDecrypt, f.addr, "/") 450 if err != nil { 451 errc <- fmt.Errorf("api get: node %s, key %s, kademlia %s: %v", id, f.addr, swarm.bzz.Hive, err) 452 return 453 } 454 d, err := ioutil.ReadAll(r) 455 if err != nil { 456 errc <- fmt.Errorf("api get: read response: node %s, key %s: kademlia %s: %v", id, f.addr, swarm.bzz.Hive, err) 457 return 458 } 459 data := string(d) 460 if data != f.data { 461 errc <- fmt.Errorf("file contend missmatch: node %s, key %s, expected %q, got %q", id, f.addr, f.data, data) 462 return 463 } 464 checkStatusM.Store(checkKey, 0) 465 atomic.AddUint64(&foundCount, 1) 466 log.Info("api get: file found", "node", id.String(), "key", f.addr.String(), "content", data, "files found", atomic.LoadUint64(&foundCount)) 467 }(f, id) 468 } 469 470 go func(id enode.ID) { 471 defer totalWg.Done() 472 wg.Wait() 473 474 atomic.AddUint64(totalFoundCount, foundCount) 475 476 if foundCount == checkCount { 477 log.Info("all files are found for node", "id", id.String(), "duration", time.Since(start)) 478 nodeStatusM.Store(id, 0) 479 return 480 } 481 log.Debug("files missing for node", "id", id.String(), "check", checkCount, "found", foundCount) 482 }(id) 483 484 } 485 486 go func() { 487 totalWg.Wait() 488 close(errc) 489 }() 490 491 var errCount int 492 for err := range errc { 493 if err != nil { 494 errCount++ 495 } 496 log.Warn(err.Error()) 497 } 498 499 log.Info("check stats", "total check count", totalCheckCount, "total files found", atomic.LoadUint64(totalFoundCount), "total errors", errCount) 500 501 return uint64(totalCheckCount) - atomic.LoadUint64(totalFoundCount) 502 }