github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/sync/threadgroup_test.go (about) 1 package sync 2 3 import ( 4 "net" 5 "os" 6 "path/filepath" 7 "sync" 8 "testing" 9 "time" 10 11 "SiaPrime/build" 12 ) 13 14 // TestThreadGroupStopEarly tests that a thread group can correctly interrupt 15 // an ongoing process. 16 func TestThreadGroupStopEarly(t *testing.T) { 17 if testing.Short() { 18 t.SkipNow() 19 } 20 t.Parallel() 21 22 var tg ThreadGroup 23 for i := 0; i < 10; i++ { 24 err := tg.Add() 25 if err != nil { 26 t.Fatal(err) 27 } 28 29 go func() { 30 defer tg.Done() 31 select { 32 case <-time.After(1 * time.Second): 33 case <-tg.StopChan(): 34 } 35 }() 36 } 37 start := time.Now() 38 err := tg.Stop() 39 elapsed := time.Since(start) 40 if err != nil { 41 t.Fatal(err) 42 } else if elapsed > 100*time.Millisecond { 43 t.Fatal("Stop did not interrupt goroutines") 44 } 45 } 46 47 // TestThreadGroupWait tests that a thread group will correctly wait for 48 // existing processes to halt. 49 func TestThreadGroupWait(t *testing.T) { 50 if testing.Short() { 51 t.SkipNow() 52 } 53 t.Parallel() 54 55 var tg ThreadGroup 56 for i := 0; i < 10; i++ { 57 err := tg.Add() 58 if err != nil { 59 t.Fatal(err) 60 } 61 62 go func() { 63 defer tg.Done() 64 time.Sleep(time.Second) 65 }() 66 } 67 start := time.Now() 68 err := tg.Stop() 69 elapsed := time.Since(start) 70 if err != nil { 71 t.Fatal(err) 72 } else if elapsed < time.Millisecond*950 { 73 t.Fatal("Stop did not wait for goroutines:", elapsed) 74 } 75 } 76 77 // TestThreadGroupStop tests the behavior of a ThreadGroup after Stop has been 78 // called. 79 func TestThreadGroupStop(t *testing.T) { 80 // Create a thread group and stop it. 81 var tg ThreadGroup 82 // Create an array to track the order of execution for OnStop and AfterStop 83 // calls. 84 var stopCalls []int 85 86 // isStopped should return false 87 if tg.isStopped() { 88 t.Error("isStopped returns true on unstopped ThreadGroup") 89 } 90 // The cannel provided by StopChan should be open. 91 select { 92 case <-tg.StopChan(): 93 t.Error("stop chan appears to be closed") 94 default: 95 } 96 97 // OnStop and AfterStop should queue their functions, but not call them. 98 // 'Add' and 'Done' are setup around the OnStop functions, to make sure 99 // that the OnStop functions are called before waiting for all calls to 100 // 'Done' to come through. 101 // 102 // Note: the practice of calling Add outside of OnStop and Done inside of 103 // OnStop is a bad one - any call to tg.Flush() will cause a deadlock 104 // because the stop functions will not be called but tg.Flush will be 105 // waiting for the thread group counter to reach zero. 106 err := tg.Add() 107 if err != nil { 108 t.Fatal(err) 109 } 110 err = tg.Add() 111 if err != nil { 112 t.Fatal(err) 113 } 114 tg.OnStop(func() { 115 tg.Done() 116 stopCalls = append(stopCalls, 1) 117 }) 118 tg.OnStop(func() { 119 tg.Done() 120 stopCalls = append(stopCalls, 2) 121 }) 122 tg.AfterStop(func() { 123 stopCalls = append(stopCalls, 10) 124 }) 125 tg.AfterStop(func() { 126 stopCalls = append(stopCalls, 20) 127 }) 128 // None of the stop calls should have been called yet. 129 if len(stopCalls) != 0 { 130 t.Fatal("Stop calls were called too early") 131 } 132 133 // Stop the thread group. 134 err = tg.Stop() 135 if err != nil { 136 t.Fatal(err) 137 } 138 // isStopped should return true. 139 if !tg.isStopped() { 140 t.Error("isStopped returns false on stopped ThreadGroup") 141 } 142 // The cannel provided by StopChan should be closed. 143 select { 144 case <-tg.StopChan(): 145 default: 146 t.Error("stop chan appears to be closed") 147 } 148 // The OnStop calls should have been called first, in reverse order, and 149 // the AfterStop calls should have been called second, in reverse order. 150 if len(stopCalls) != 4 { 151 t.Fatal("Stop did not call the stopping functions correctly") 152 } 153 if stopCalls[0] != 2 { 154 t.Error("Stop called the stopping functions in the wrong order") 155 } 156 if stopCalls[1] != 1 { 157 t.Error("Stop called the stopping functions in the wrong order") 158 } 159 if stopCalls[2] != 20 { 160 t.Error("Stop called the stopping functions in the wrong order") 161 } 162 if stopCalls[3] != 10 { 163 t.Error("Stop called the stopping functions in the wrong order") 164 } 165 166 // Add and Stop should return errors. 167 err = tg.Add() 168 if err != ErrStopped { 169 t.Error("expected ErrStopped, got", err) 170 } 171 err = tg.Stop() 172 if err != ErrStopped { 173 t.Error("expected ErrStopped, got", err) 174 } 175 176 // OnStop and AfterStop should call their functions immediately now that 177 // the thread group has stopped. 178 onStopCalled := false 179 tg.OnStop(func() { 180 onStopCalled = true 181 }) 182 if !onStopCalled { 183 t.Error("OnStop function not called immediately despite the thread group being closed already.") 184 } 185 afterStopCalled := false 186 tg.AfterStop(func() { 187 afterStopCalled = true 188 }) 189 if !afterStopCalled { 190 t.Error("AfterStop function not called immediately despite the thread group being closed already.") 191 } 192 } 193 194 // TestThreadGroupConcurrentAdd tests that Add can be called concurrently with Stop. 195 func TestThreadGroupConcurrentAdd(t *testing.T) { 196 if testing.Short() { 197 t.SkipNow() 198 } 199 var tg ThreadGroup 200 for i := 0; i < 10; i++ { 201 go func() { 202 err := tg.Add() 203 if err != nil { 204 return 205 } 206 defer tg.Done() 207 208 select { 209 case <-time.After(1 * time.Second): 210 case <-tg.StopChan(): 211 } 212 }() 213 } 214 time.Sleep(10 * time.Millisecond) // wait for at least one Add 215 err := tg.Stop() 216 if err != nil { 217 t.Fatal(err) 218 } 219 } 220 221 // TestThreadGroupOnce tests that a zero-valued ThreadGroup's stopChan is 222 // properly initialized. 223 func TestThreadGroupOnce(t *testing.T) { 224 tg := new(ThreadGroup) 225 if tg.stopChan != nil { 226 t.Error("expected nil stopChan") 227 } 228 229 // these methods should cause stopChan to be initialized 230 tg.StopChan() 231 if tg.stopChan == nil { 232 t.Error("stopChan should have been initialized by StopChan") 233 } 234 235 tg = new(ThreadGroup) 236 tg.isStopped() 237 if tg.stopChan == nil { 238 t.Error("stopChan should have been initialized by isStopped") 239 } 240 241 tg = new(ThreadGroup) 242 tg.Add() 243 if tg.stopChan == nil { 244 t.Error("stopChan should have been initialized by Add") 245 } 246 247 tg = new(ThreadGroup) 248 tg.Stop() 249 if tg.stopChan == nil { 250 t.Error("stopChan should have been initialized by Stop") 251 } 252 } 253 254 // TestThreadGroupOnStop tests that Stop calls functions registered with 255 // OnStop. 256 func TestThreadGroupOnStop(t *testing.T) { 257 if testing.Short() { 258 t.SkipNow() 259 } 260 l, err := net.Listen("tcp", "localhost:0") 261 if err != nil { 262 t.Fatal(err) 263 } 264 265 // create ThreadGroup and register the closer 266 var tg ThreadGroup 267 tg.OnStop(func() { l.Close() }) 268 269 // send on channel when listener is closed 270 var closed bool 271 tg.Add() 272 go func() { 273 defer tg.Done() 274 _, err := l.Accept() 275 closed = err != nil 276 }() 277 278 tg.Stop() 279 if !closed { 280 t.Fatal("Stop did not close listener") 281 } 282 } 283 284 // TestThreadGroupRace tests that calling ThreadGroup methods concurrently 285 // does not trigger the race detector. 286 func TestThreadGroupRace(t *testing.T) { 287 var tg ThreadGroup 288 go tg.StopChan() 289 go func() { 290 if tg.Add() == nil { 291 tg.Done() 292 } 293 }() 294 err := tg.Stop() 295 if err != nil { 296 t.Fatal(err) 297 } 298 } 299 300 // TestThreadGroupCloseAfterStop checks that an AfterStop function is 301 // correctly called after the thread is stopped. 302 func TestThreadGroupClosedAfterStop(t *testing.T) { 303 var tg ThreadGroup 304 var closed bool 305 tg.AfterStop(func() { closed = true }) 306 if closed { 307 t.Fatal("close function should not have been called yet") 308 } 309 if err := tg.Stop(); err != nil { 310 t.Fatal(err) 311 } 312 if !closed { 313 t.Fatal("close function should have been called") 314 } 315 316 // Stop has already been called, so the close function should be called 317 // immediately 318 closed = false 319 tg.AfterStop(func() { closed = true }) 320 if !closed { 321 t.Fatal("close function should have been called immediately") 322 } 323 } 324 325 // TestThreadGroupSiaExample tries to use a thread group as it might be 326 // expected to be used by a module of Sia. 327 func TestThreadGroupSiaExample(t *testing.T) { 328 if testing.Short() { 329 t.SkipNow() 330 } 331 t.Parallel() 332 testDir := build.TempDir("sync", t.Name()) 333 err := os.MkdirAll(testDir, 0700) 334 if err != nil { 335 t.Fatal(err) 336 } 337 var tg ThreadGroup 338 339 // Open an example file. The file is expected to be used throughout the 340 // lifetime of the module, and should not be closed until 'AfterStop' is 341 // called. 342 fileClosed := false 343 file, err := os.Create(filepath.Join(testDir, "exampleFile.txt")) 344 if err != nil { 345 t.Fatal(err) 346 } 347 tg.AfterStop(func() { 348 fileClosed = true 349 err := file.Close() 350 if err != nil { 351 t.Fatal(err) 352 } 353 }) 354 355 // Open a listener. The listener and handler thread should be closed before 356 // the file is closed. 357 listenerCleanedUp := false 358 listener, err := net.Listen("tcp", "localhost:0") 359 if err != nil { 360 t.Fatal(err) 361 } 362 // Open a thread to accept calls from the listener. 363 handlerFinishedChan := make(chan struct{}) 364 go func() { 365 for { 366 _, err := listener.Accept() 367 if err != nil { 368 break 369 } 370 } 371 handlerFinishedChan <- struct{}{} 372 }() 373 tg.OnStop(func() { 374 err := listener.Close() 375 if err != nil { 376 t.Fatal(err) 377 } 378 <-handlerFinishedChan 379 380 if fileClosed { 381 t.Error("file should be open while the listener is shutting down") 382 } 383 listenerCleanedUp = true 384 }) 385 386 // Create a thread that does some stuff which takes time, and then closes. 387 // Use Flush to clear out the process without closing the resources. 388 threadFinished := false 389 err = tg.Add() 390 if err != nil { 391 t.Fatal(err) 392 } 393 go func() { 394 time.Sleep(time.Second) 395 threadFinished = true 396 tg.Done() 397 }() 398 tg.Flush() 399 if !threadFinished { 400 t.Error("call to Flush should have allowed the working thread to finish") 401 } 402 if listenerCleanedUp || fileClosed { 403 t.Error("call to Flush resulted in permanent resources being closed") 404 } 405 406 // Create a thread that does some stuff which takes time, and then closes. 407 // Use Stop to wait for the threead to finish and then check that all 408 // resources have closed. 409 threadFinished2 := false 410 err = tg.Add() 411 if err != nil { 412 t.Fatal(err) 413 } 414 go func() { 415 time.Sleep(time.Second) 416 threadFinished2 = true 417 tg.Done() 418 }() 419 tg.Stop() 420 if !threadFinished2 || !listenerCleanedUp || !fileClosed { 421 t.Error("stop did not block until all running resources had closed") 422 } 423 } 424 425 // TestAddOnStop checks that you can safely call OnStop from under the 426 // protection of an Add call. 427 func TestAddOnStop(t *testing.T) { 428 if testing.Short() { 429 t.SkipNow() 430 } 431 t.Parallel() 432 433 var tg ThreadGroup 434 var data int 435 addChan := make(chan struct{}) 436 stopChan := make(chan struct{}) 437 tg.OnStop(func() { 438 close(stopChan) 439 }) 440 go func() { 441 err := tg.Add() 442 if err != nil { 443 t.Fatal(err) 444 } 445 close(addChan) 446 447 // Wait for the call to 'Stop' to be called in the parent thread, and 448 // then queue a bunch of 'OnStop' and 'AfterStop' functions before 449 // calling 'Done'. 450 <-stopChan 451 for i := 0; i < 10; i++ { 452 tg.OnStop(func() { 453 data++ 454 }) 455 tg.AfterStop(func() { 456 data++ 457 }) 458 } 459 tg.Done() 460 }() 461 462 // Wait for 'Add' to be called in the above thread, to guarantee that 463 // OnStop and AfterStop will be called after 'Add' and 'Stop' have been 464 // called together. 465 <-addChan 466 err := tg.Stop() 467 if err != nil { 468 t.Fatal(err) 469 } 470 471 if data != 20 { 472 t.Error("20 calls were made to increment data, but value is", data) 473 } 474 } 475 476 // BenchmarkThreadGroup times how long it takes to add a ton of threads and 477 // trigger goroutines that call Done. 478 func BenchmarkThreadGroup(b *testing.B) { 479 var tg ThreadGroup 480 for i := 0; i < b.N; i++ { 481 tg.Add() 482 go tg.Done() 483 } 484 tg.Stop() 485 } 486 487 // BenchmarkWaitGroup times how long it takes to add a ton of threads to a wait 488 // group and trigger goroutines that call Done. 489 func BenchmarkWaitGroup(b *testing.B) { 490 var wg sync.WaitGroup 491 for i := 0; i < b.N; i++ { 492 wg.Add(1) 493 go wg.Done() 494 } 495 wg.Wait() 496 }