github.com/OpenFlowLabs/storage@v1.12.13/lockfile_test.go (about) 1 package storage 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "os" 8 "sync" 9 "sync/atomic" 10 "testing" 11 "time" 12 13 "github.com/containers/storage/pkg/reexec" 14 "github.com/sirupsen/logrus" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 ) 18 19 // Warning: this is not an exhaustive set of tests. 20 21 func TestMain(m *testing.M) { 22 if reexec.Init() { 23 return 24 } 25 os.Exit(m.Run()) 26 } 27 28 // subTouchMain is a child process which opens the lock file, closes stdout to 29 // indicate that it has acquired the lock, waits for stdin to get closed, 30 // updates the last-writer for the lockfile, and then unlocks the file. 31 func subTouchMain() { 32 if len(os.Args) != 2 { 33 logrus.Fatalf("expected two args, got %d", len(os.Args)) 34 } 35 tf, err := GetLockfile(os.Args[1]) 36 if err != nil { 37 logrus.Fatalf("error opening lock file %q: %v", os.Args[1], err) 38 } 39 tf.Lock() 40 os.Stdout.Close() 41 io.Copy(ioutil.Discard, os.Stdin) 42 tf.Touch() 43 tf.Unlock() 44 } 45 46 // subTouch starts a child process. If it doesn't return an error, the caller 47 // should wait for the first ReadCloser by reading it until it receives an EOF. 48 // At that point, the child will have acquired the lock. It can then signal 49 // that the child should Touch() the lock by closing the WriteCloser. The 50 // second ReadCloser will be closed when the child has finished. 51 func subTouch(l *namedLocker) (io.WriteCloser, io.ReadCloser, io.ReadCloser, error) { 52 cmd := reexec.Command("subTouch", l.name) 53 wc, err := cmd.StdinPipe() 54 if err != nil { 55 return nil, nil, nil, err 56 } 57 rc, err := cmd.StdoutPipe() 58 if err != nil { 59 return nil, nil, nil, err 60 } 61 ec, err := cmd.StderrPipe() 62 if err != nil { 63 return nil, nil, nil, err 64 } 65 go func() { 66 if err = cmd.Run(); err != nil { 67 logrus.Errorf("error running subTouch: %v", err) 68 } 69 }() 70 return wc, rc, ec, nil 71 } 72 73 // subLockMain is a child process which opens the lock file, closes stdout to 74 // indicate that it has acquired the lock, waits for stdin to get closed, and 75 // then unlocks the file. 76 func subLockMain() { 77 if len(os.Args) != 2 { 78 logrus.Fatalf("expected two args, got %d", len(os.Args)) 79 } 80 tf, err := GetLockfile(os.Args[1]) 81 if err != nil { 82 logrus.Fatalf("error opening lock file %q: %v", os.Args[1], err) 83 } 84 tf.Lock() 85 os.Stdout.Close() 86 io.Copy(ioutil.Discard, os.Stdin) 87 tf.Unlock() 88 } 89 90 // subLock starts a child process. If it doesn't return an error, the caller 91 // should wait for the first ReadCloser by reading it until it receives an EOF. 92 // At that point, the child will have acquired the lock. It can then signal 93 // that the child should release the lock by closing the WriteCloser. 94 func subLock(l *namedLocker) (io.WriteCloser, io.ReadCloser, error) { 95 cmd := reexec.Command("subLock", l.name) 96 wc, err := cmd.StdinPipe() 97 if err != nil { 98 return nil, nil, err 99 } 100 rc, err := cmd.StdoutPipe() 101 if err != nil { 102 return nil, nil, err 103 } 104 go func() { 105 if err = cmd.Run(); err != nil { 106 logrus.Errorf("error running subLock: %v", err) 107 } 108 }() 109 return wc, rc, nil 110 } 111 112 // subRecursiveLockMain is a child process which opens the lock file, closes 113 // stdout to indicate that it has acquired the lock, waits for stdin to get 114 // closed, and then unlocks the file. 115 func subRecursiveLockMain() { 116 if len(os.Args) != 2 { 117 logrus.Fatalf("expected two args, got %d", len(os.Args)) 118 } 119 tf, err := GetLockfile(os.Args[1]) 120 if err != nil { 121 logrus.Fatalf("error opening lock file %q: %v", os.Args[1], err) 122 } 123 tf.RecursiveLock() 124 os.Stdout.Close() 125 io.Copy(ioutil.Discard, os.Stdin) 126 tf.Unlock() 127 } 128 129 // subRecursiveLock starts a child process. If it doesn't return an error, the 130 // caller should wait for the first ReadCloser by reading it until it receives 131 // an EOF. At that point, the child will have acquired the lock. It can then 132 // signal that the child should release the lock by closing the WriteCloser. 133 func subRecursiveLock(l *namedLocker) (io.WriteCloser, io.ReadCloser, error) { 134 cmd := reexec.Command("subRecursiveLock", l.name) 135 wc, err := cmd.StdinPipe() 136 if err != nil { 137 return nil, nil, err 138 } 139 rc, err := cmd.StdoutPipe() 140 if err != nil { 141 return nil, nil, err 142 } 143 go func() { 144 if err = cmd.Run(); err != nil { 145 logrus.Errorf("error running subLock: %v", err) 146 } 147 }() 148 return wc, rc, nil 149 } 150 151 // subRLockMain is a child process which opens the lock file, closes stdout to 152 // indicate that it has acquired the read lock, waits for stdin to get closed, 153 // and then unlocks the file. 154 func subRLockMain() { 155 if len(os.Args) != 2 { 156 logrus.Fatalf("expected two args, got %d", len(os.Args)) 157 } 158 tf, err := GetLockfile(os.Args[1]) 159 if err != nil { 160 logrus.Fatalf("error opening lock file %q: %v", os.Args[1], err) 161 } 162 tf.RLock() 163 os.Stdout.Close() 164 io.Copy(ioutil.Discard, os.Stdin) 165 tf.Unlock() 166 } 167 168 // subRLock starts a child process. If it doesn't return an error, the caller 169 // should wait for the first ReadCloser by reading it until it receives an EOF. 170 // At that point, the child will have acquired a read lock. It can then signal 171 // that the child should release the lock by closing the WriteCloser. 172 func subRLock(l *namedLocker) (io.WriteCloser, io.ReadCloser, error) { 173 cmd := reexec.Command("subRLock", l.name) 174 wc, err := cmd.StdinPipe() 175 if err != nil { 176 return nil, nil, err 177 } 178 rc, err := cmd.StdoutPipe() 179 if err != nil { 180 return nil, nil, err 181 } 182 go func() { 183 if err = cmd.Run(); err != nil { 184 logrus.Errorf("error running subRLock: %v", err) 185 } 186 }() 187 return wc, rc, nil 188 } 189 190 func init() { 191 reexec.Register("subTouch", subTouchMain) 192 reexec.Register("subRLock", subRLockMain) 193 reexec.Register("subRecursiveLock", subRecursiveLockMain) 194 reexec.Register("subLock", subLockMain) 195 } 196 197 type namedLocker struct { 198 Locker 199 name string 200 } 201 202 func getNamedLocker(ro bool) (*namedLocker, error) { 203 var l Locker 204 tf, err := ioutil.TempFile("", "lockfile") 205 if err != nil { 206 return nil, err 207 } 208 name := tf.Name() 209 tf.Close() 210 if ro { 211 l, err = GetROLockfile(name) 212 } else { 213 l, err = GetLockfile(name) 214 } 215 if err != nil { 216 return nil, err 217 } 218 return &namedLocker{Locker: l, name: name}, nil 219 } 220 221 func getTempLockfile() (*namedLocker, error) { 222 return getNamedLocker(false) 223 } 224 225 func getTempROLockfile() (*namedLocker, error) { 226 return getNamedLocker(true) 227 } 228 229 func TestLockfileName(t *testing.T) { 230 l, err := getTempLockfile() 231 require.Nil(t, err, "error getting temporary lock file") 232 defer os.Remove(l.name) 233 234 assert.NotEmpty(t, l.name, "lockfile name should be recorded correctly") 235 236 assert.False(t, l.Locked(), "Locked() said we have a write lock") 237 238 l.RLock() 239 assert.False(t, l.Locked(), "Locked() said we have a write lock") 240 l.Unlock() 241 242 assert.NotEmpty(t, l.name, "lockfile name should be recorded correctly") 243 244 l.Lock() 245 assert.True(t, l.Locked(), "Locked() said we didn't have a write lock") 246 l.Unlock() 247 248 assert.NotEmpty(t, l.name, "lockfile name should be recorded correctly") 249 } 250 251 func TestLockfileRead(t *testing.T) { 252 l, err := getTempLockfile() 253 require.Nil(t, err, "error getting temporary lock file") 254 defer os.Remove(l.name) 255 256 l.RLock() 257 assert.False(t, l.Locked(), "Locked() said we have a write lock") 258 l.Unlock() 259 } 260 261 func TestROLockfileRead(t *testing.T) { 262 l, err := getTempROLockfile() 263 require.Nil(t, err, "error getting temporary lock file") 264 defer os.Remove(l.name) 265 266 l.Lock() 267 assert.False(t, l.Locked(), "Locked() said we have a write lock") 268 l.Unlock() 269 270 l.RLock() 271 assert.False(t, l.Locked(), "Locked() said we have a write lock") 272 l.Unlock() 273 } 274 275 func TestLockfileWrite(t *testing.T) { 276 l, err := getTempLockfile() 277 require.Nil(t, err, "error getting temporary lock file") 278 defer os.Remove(l.name) 279 280 l.Lock() 281 assert.True(t, l.Locked(), "Locked() said we didn't have a write lock") 282 l.Unlock() 283 } 284 285 func TestRecursiveLockfileWrite(t *testing.T) { 286 l, err := getTempLockfile() 287 require.Nil(t, err, "error getting temporary lock file") 288 defer os.Remove(l.name) 289 290 l.RecursiveLock() 291 assert.True(t, l.Locked(), "Locked() said we didn't have a write lock") 292 l.RecursiveLock() 293 l.Unlock() 294 l.Unlock() 295 } 296 297 func TestROLockfileWrite(t *testing.T) { 298 l, err := getTempROLockfile() 299 require.Nil(t, err, "error getting temporary lock file") 300 defer os.Remove(l.name) 301 302 l.Lock() 303 assert.False(t, l.Locked(), "Locked() said we have a write lock") 304 l.Unlock() 305 } 306 307 func TestLockfileTouch(t *testing.T) { 308 l, err := getTempLockfile() 309 require.Nil(t, err, "error getting temporary lock file") 310 defer os.Remove(l.name) 311 312 l.Lock() 313 m, err := l.Modified() 314 require.Nil(t, err, "got an error from Modified()") 315 assert.True(t, m, "new lock file does not appear to have changed") 316 317 now := time.Now() 318 assert.False(t, l.TouchedSince(now), "file timestamp was updated for no reason") 319 320 time.Sleep(2 * time.Second) 321 err = l.Touch() 322 require.Nil(t, err, "got an error from Touch()") 323 assert.True(t, l.TouchedSince(now), "file timestamp was not updated by Touch()") 324 325 m, err = l.Modified() 326 require.Nil(t, err, "got an error from Modified()") 327 assert.False(t, m, "lock file mistakenly indicated that someone else has modified it") 328 329 stdin, stdout, stderr, err := subTouch(l) 330 require.Nil(t, err, "got an error starting a subprocess to touch the lockfile") 331 l.Unlock() 332 io.Copy(ioutil.Discard, stdout) 333 stdin.Close() 334 io.Copy(ioutil.Discard, stderr) 335 l.Lock() 336 m, err = l.Modified() 337 l.Unlock() 338 require.Nil(t, err, "got an error from Modified()") 339 assert.True(t, m, "lock file failed to notice that someone else modified it") 340 } 341 342 func TestLockfileWriteConcurrent(t *testing.T) { 343 l, err := getTempLockfile() 344 require.Nil(t, err, "error getting temporary lock file") 345 defer os.Remove(l.name) 346 var wg sync.WaitGroup 347 var highestMutex sync.Mutex 348 var counter, highest int64 349 for i := 0; i < 100000; i++ { 350 wg.Add(1) 351 go func() { 352 l.Lock() 353 tmp := atomic.AddInt64(&counter, 1) 354 assert.True(t, tmp >= 0, "counter should never be less than zero") 355 highestMutex.Lock() 356 if tmp > highest { 357 // multiple writers should not be able to hold 358 // this lock at the same time, so there should 359 // be no point at which two goroutines are 360 // between the AddInt64() above and the one 361 // below 362 highest = tmp 363 } 364 highestMutex.Unlock() 365 atomic.AddInt64(&counter, -1) 366 l.Unlock() 367 wg.Done() 368 }() 369 } 370 wg.Wait() 371 assert.True(t, highest == 1, "counter should never have gone above 1, got to %d", highest) 372 } 373 374 func TestLockfileReadConcurrent(t *testing.T) { 375 l, err := getTempLockfile() 376 require.Nil(t, err, "error getting temporary lock file") 377 defer os.Remove(l.name) 378 379 // the test below is inspired by the stdlib's rwmutex tests 380 numReaders := 1000 381 locked := make(chan bool) 382 unlocked := make(chan bool) 383 done := make(chan bool) 384 385 for i := 0; i < numReaders; i++ { 386 go func() { 387 l.RLock() 388 locked <- true 389 <-unlocked 390 l.Unlock() 391 done <- true 392 }() 393 } 394 395 // Wait for all parallel locks to succeed 396 for i := 0; i < numReaders; i++ { 397 <-locked 398 } 399 // Instruct all parallel locks to unlock 400 for i := 0; i < numReaders; i++ { 401 unlocked <- true 402 } 403 // Wait for all parallel locks to be unlocked 404 for i := 0; i < numReaders; i++ { 405 <-done 406 } 407 } 408 409 func TestLockfileRecursiveWrite(t *testing.T) { 410 // NOTE: given we're in the same process space, it's effectively the same as 411 // reader lock. 412 413 l, err := getTempLockfile() 414 require.Nil(t, err, "error getting temporary lock file") 415 defer os.Remove(l.name) 416 417 // the test below is inspired by the stdlib's rwmutex tests 418 numReaders := 1000 419 locked := make(chan bool) 420 unlocked := make(chan bool) 421 done := make(chan bool) 422 423 for i := 0; i < numReaders; i++ { 424 go func() { 425 l.RecursiveLock() 426 locked <- true 427 <-unlocked 428 l.Unlock() 429 done <- true 430 }() 431 } 432 433 // Wait for all parallel locks to succeed 434 for i := 0; i < numReaders; i++ { 435 <-locked 436 } 437 // Instruct all parallel locks to unlock 438 for i := 0; i < numReaders; i++ { 439 unlocked <- true 440 } 441 // Wait for all parallel locks to be unlocked 442 for i := 0; i < numReaders; i++ { 443 <-done 444 } 445 } 446 447 func TestLockfileMixedConcurrent(t *testing.T) { 448 l, err := getTempLockfile() 449 require.Nil(t, err, "error getting temporary lock file") 450 defer os.Remove(l.name) 451 452 counter := int32(0) 453 diff := int32(10000) 454 numIterations := 10 455 numReaders := 100 456 numWriters := 50 457 458 done := make(chan bool) 459 460 // A writer always adds `diff` to the counter. Hence, `diff` is the 461 // only valid value in the critical section. 462 writer := func(c *int32) { 463 for i := 0; i < numIterations; i++ { 464 l.Lock() 465 tmp := atomic.AddInt32(c, diff) 466 assert.True(t, tmp == diff, "counter should be %d but instead is %d", diff, tmp) 467 time.Sleep(100 * time.Millisecond) 468 atomic.AddInt32(c, diff*(-1)) 469 l.Unlock() 470 } 471 done <- true 472 } 473 474 // A reader always adds `1` to the counter. Hence, 475 // [1,`numReaders*numIterations`] are valid values. 476 reader := func(c *int32) { 477 for i := 0; i < numIterations; i++ { 478 l.RLock() 479 tmp := atomic.AddInt32(c, 1) 480 assert.True(t, tmp >= 1 && tmp < diff) 481 time.Sleep(100 * time.Millisecond) 482 atomic.AddInt32(c, -1) 483 l.Unlock() 484 } 485 done <- true 486 } 487 488 for i := 0; i < numReaders; i++ { 489 go reader(&counter) 490 // schedule a writer every 2nd iteration 491 if i%2 == 1 { 492 go writer(&counter) 493 } 494 } 495 496 for i := 0; i < numReaders+numWriters; i++ { 497 <-done 498 } 499 } 500 501 func TestLockfileMixedConcurrentRecursiveWriters(t *testing.T) { 502 // It's effectively the same tests as with mixed readers & writers but calling 503 // RecursiveLocks() instead. 504 505 l, err := getTempLockfile() 506 require.Nil(t, err, "error getting temporary lock file") 507 defer os.Remove(l.name) 508 509 counter := int32(0) 510 diff := int32(10000) 511 numIterations := 10 512 numReaders := 100 513 numWriters := 50 514 515 done := make(chan bool) 516 517 // A writer always adds `diff` to the counter. Hence, `diff` is the 518 // only valid value in the critical section. 519 writer := func(c *int32) { 520 for i := 0; i < numIterations; i++ { 521 l.Lock() 522 tmp := atomic.AddInt32(c, diff) 523 assert.True(t, tmp == diff, "counter should be %d but instead is %d", diff, tmp) 524 time.Sleep(100 * time.Millisecond) 525 atomic.AddInt32(c, diff*(-1)) 526 l.Unlock() 527 } 528 done <- true 529 } 530 531 // A reader always adds `1` to the counter. Hence, 532 // [1,`numReaders*numIterations`] are valid values. 533 reader := func(c *int32) { 534 for i := 0; i < numIterations; i++ { 535 l.RecursiveLock() 536 tmp := atomic.AddInt32(c, 1) 537 assert.True(t, tmp >= 1 && tmp < diff) 538 time.Sleep(100 * time.Millisecond) 539 atomic.AddInt32(c, -1) 540 l.Unlock() 541 } 542 done <- true 543 } 544 545 for i := 0; i < numReaders; i++ { 546 go reader(&counter) 547 // schedule a writer every 2nd iteration 548 if i%2 == 1 { 549 go writer(&counter) 550 } 551 } 552 553 for i := 0; i < numReaders+numWriters; i++ { 554 <-done 555 } 556 } 557 558 func TestLockfileMultiprocessRead(t *testing.T) { 559 l, err := getTempLockfile() 560 require.Nil(t, err, "error getting temporary lock file") 561 defer os.Remove(l.name) 562 var wg sync.WaitGroup 563 var rcounter, rhighest int64 564 var highestMutex sync.Mutex 565 subs := make([]struct { 566 stdin io.WriteCloser 567 stdout io.ReadCloser 568 }, 100) 569 for i := range subs { 570 stdin, stdout, err := subRLock(l) 571 require.Nil(t, err, "error starting subprocess %d to take a read lock", i+1) 572 subs[i].stdin = stdin 573 subs[i].stdout = stdout 574 } 575 for i := range subs { 576 wg.Add(1) 577 go func(i int) { 578 io.Copy(ioutil.Discard, subs[i].stdout) 579 if testing.Verbose() { 580 fmt.Printf("\tchild %4d acquired the read lock\n", i+1) 581 } 582 atomic.AddInt64(&rcounter, 1) 583 highestMutex.Lock() 584 if rcounter > rhighest { 585 rhighest = rcounter 586 } 587 highestMutex.Unlock() 588 time.Sleep(1 * time.Second) 589 atomic.AddInt64(&rcounter, -1) 590 if testing.Verbose() { 591 fmt.Printf("\ttelling child %4d to release the read lock\n", i+1) 592 } 593 subs[i].stdin.Close() 594 wg.Done() 595 }(i) 596 } 597 wg.Wait() 598 assert.True(t, rhighest > 1, "expected to have multiple reader locks at least once, only had %d", rhighest) 599 } 600 601 func TestLockfileMultiprocessWrite(t *testing.T) { 602 l, err := getTempLockfile() 603 require.Nil(t, err, "error getting temporary lock file") 604 defer os.Remove(l.name) 605 var wg sync.WaitGroup 606 var wcounter, whighest int64 607 var highestMutex sync.Mutex 608 subs := make([]struct { 609 stdin io.WriteCloser 610 stdout io.ReadCloser 611 }, 10) 612 for i := range subs { 613 stdin, stdout, err := subLock(l) 614 require.Nil(t, err, "error starting subprocess %d to take a write lock", i+1) 615 subs[i].stdin = stdin 616 subs[i].stdout = stdout 617 } 618 for i := range subs { 619 wg.Add(1) 620 go func(i int) { 621 io.Copy(ioutil.Discard, subs[i].stdout) 622 if testing.Verbose() { 623 fmt.Printf("\tchild %4d acquired the write lock\n", i+1) 624 } 625 atomic.AddInt64(&wcounter, 1) 626 highestMutex.Lock() 627 if wcounter > whighest { 628 whighest = wcounter 629 } 630 highestMutex.Unlock() 631 time.Sleep(1 * time.Second) 632 atomic.AddInt64(&wcounter, -1) 633 if testing.Verbose() { 634 fmt.Printf("\ttelling child %4d to release the write lock\n", i+1) 635 } 636 subs[i].stdin.Close() 637 wg.Done() 638 }(i) 639 } 640 wg.Wait() 641 assert.True(t, whighest == 1, "expected to have no more than one writer lock active at a time, had %d", whighest) 642 } 643 644 func TestLockfileMultiprocessRecursiveWrite(t *testing.T) { 645 l, err := getTempLockfile() 646 require.Nil(t, err, "error getting temporary lock file") 647 defer os.Remove(l.name) 648 var wg sync.WaitGroup 649 var wcounter, whighest int64 650 var highestMutex sync.Mutex 651 subs := make([]struct { 652 stdin io.WriteCloser 653 stdout io.ReadCloser 654 }, 10) 655 for i := range subs { 656 stdin, stdout, err := subRecursiveLock(l) 657 require.Nil(t, err, "error starting subprocess %d to take a write lock", i+1) 658 subs[i].stdin = stdin 659 subs[i].stdout = stdout 660 } 661 for i := range subs { 662 wg.Add(1) 663 go func(i int) { 664 io.Copy(ioutil.Discard, subs[i].stdout) 665 if testing.Verbose() { 666 fmt.Printf("\tchild %4d acquired the recursive write lock\n", i+1) 667 } 668 atomic.AddInt64(&wcounter, 1) 669 highestMutex.Lock() 670 if wcounter > whighest { 671 whighest = wcounter 672 } 673 highestMutex.Unlock() 674 time.Sleep(1 * time.Second) 675 atomic.AddInt64(&wcounter, -1) 676 if testing.Verbose() { 677 fmt.Printf("\ttelling child %4d to release the recursive write lock\n", i+1) 678 } 679 subs[i].stdin.Close() 680 wg.Done() 681 }(i) 682 } 683 wg.Wait() 684 assert.True(t, whighest == 1, "expected to have no more than one writer lock active at a time, had %d", whighest) 685 } 686 687 func TestLockfileMultiprocessMixed(t *testing.T) { 688 l, err := getTempLockfile() 689 require.Nil(t, err, "error getting temporary lock file") 690 defer os.Remove(l.name) 691 var wg sync.WaitGroup 692 var rcounter, wcounter, rhighest, whighest int64 693 var rhighestMutex, whighestMutex sync.Mutex 694 bias_p := 1 695 bias_q := 10 696 groups := 15 697 writer := func(i int) bool { return (i % bias_q) < bias_p } 698 subs := make([]struct { 699 stdin io.WriteCloser 700 stdout io.ReadCloser 701 }, bias_q*groups) 702 for i := range subs { 703 var stdin io.WriteCloser 704 var stdout io.ReadCloser 705 if writer(i) { 706 stdin, stdout, err = subLock(l) 707 require.Nil(t, err, "error starting subprocess %d to take a write lock", i+1) 708 } else { 709 stdin, stdout, err = subRLock(l) 710 require.Nil(t, err, "error starting subprocess %d to take a read lock", i+1) 711 } 712 subs[i].stdin = stdin 713 subs[i].stdout = stdout 714 } 715 for i := range subs { 716 wg.Add(1) 717 go func(i int) { 718 // wait for the child to acquire whatever lock it wants 719 io.Copy(ioutil.Discard, subs[i].stdout) 720 if writer(i) { 721 // child acquired a write lock 722 if testing.Verbose() { 723 fmt.Printf("\tchild %4d acquired the write lock\n", i+1) 724 } 725 atomic.AddInt64(&wcounter, 1) 726 whighestMutex.Lock() 727 if wcounter > whighest { 728 whighest = wcounter 729 } 730 require.Zero(t, rcounter, "acquired a write lock while we appear to have read locks") 731 whighestMutex.Unlock() 732 } else { 733 // child acquired a read lock 734 if testing.Verbose() { 735 fmt.Printf("\tchild %4d acquired the read lock\n", i+1) 736 } 737 atomic.AddInt64(&rcounter, 1) 738 rhighestMutex.Lock() 739 if rcounter > rhighest { 740 rhighest = rcounter 741 } 742 require.Zero(t, wcounter, "acquired a read lock while we appear to have write locks") 743 rhighestMutex.Unlock() 744 } 745 time.Sleep(1 * time.Second) 746 if writer(i) { 747 atomic.AddInt64(&wcounter, -1) 748 if testing.Verbose() { 749 fmt.Printf("\ttelling child %4d to release the write lock\n", i+1) 750 } 751 } else { 752 atomic.AddInt64(&rcounter, -1) 753 if testing.Verbose() { 754 fmt.Printf("\ttelling child %4d to release the read lock\n", i+1) 755 } 756 } 757 subs[i].stdin.Close() 758 wg.Done() 759 }(i) 760 } 761 wg.Wait() 762 assert.True(t, rhighest > 1, "expected to have more than one reader lock active at a time at least once, only had %d", rhighest) 763 assert.True(t, whighest == 1, "expected to have no more than one writer lock active at a time, had %d", whighest) 764 }