| // Copyright 2019 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // +build !plan9 |
| |
| package renameio |
| |
| import ( |
| "encoding/binary" |
| "errors" |
| "internal/testenv" |
| "io/ioutil" |
| "math/rand" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "sync" |
| "sync/atomic" |
| "syscall" |
| "testing" |
| "time" |
| |
| "cmd/go/internal/robustio" |
| ) |
| |
| func TestConcurrentReadsAndWrites(t *testing.T) { |
| if runtime.GOOS == "darwin" && strings.HasSuffix(testenv.Builder(), "-10_14") { |
| testenv.SkipFlaky(t, 33041) |
| } |
| |
| dir, err := ioutil.TempDir("", "renameio") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(dir) |
| path := filepath.Join(dir, "blob.bin") |
| |
| const chunkWords = 8 << 10 |
| buf := make([]byte, 2*chunkWords*8) |
| for i := uint64(0); i < 2*chunkWords; i++ { |
| binary.LittleEndian.PutUint64(buf[i*8:], i) |
| } |
| |
| var attempts int64 = 128 |
| if !testing.Short() { |
| attempts *= 16 |
| } |
| const parallel = 32 |
| |
| var sem = make(chan bool, parallel) |
| |
| var ( |
| writeSuccesses, readSuccesses int64 // atomic |
| writeErrnoSeen, readErrnoSeen sync.Map |
| ) |
| |
| for n := attempts; n > 0; n-- { |
| sem <- true |
| go func() { |
| defer func() { <-sem }() |
| |
| time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond) |
| offset := rand.Intn(chunkWords) |
| chunk := buf[offset*8 : (offset+chunkWords)*8] |
| if err := WriteFile(path, chunk, 0666); err == nil { |
| atomic.AddInt64(&writeSuccesses, 1) |
| } else if robustio.IsEphemeralError(err) { |
| var ( |
| errno syscall.Errno |
| dup bool |
| ) |
| if errors.As(err, &errno) { |
| _, dup = writeErrnoSeen.LoadOrStore(errno, true) |
| } |
| if !dup { |
| t.Logf("ephemeral error: %v", err) |
| } |
| } else { |
| t.Errorf("unexpected error: %v", err) |
| } |
| |
| time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond) |
| data, err := ReadFile(path) |
| if err == nil { |
| atomic.AddInt64(&readSuccesses, 1) |
| } else if robustio.IsEphemeralError(err) { |
| var ( |
| errno syscall.Errno |
| dup bool |
| ) |
| if errors.As(err, &errno) { |
| _, dup = readErrnoSeen.LoadOrStore(errno, true) |
| } |
| if !dup { |
| t.Logf("ephemeral error: %v", err) |
| } |
| return |
| } else { |
| t.Errorf("unexpected error: %v", err) |
| return |
| } |
| |
| if len(data) != 8*chunkWords { |
| t.Errorf("read %d bytes, but each write is a %d-byte file", len(data), 8*chunkWords) |
| return |
| } |
| |
| u := binary.LittleEndian.Uint64(data) |
| for i := 1; i < chunkWords; i++ { |
| next := binary.LittleEndian.Uint64(data[i*8:]) |
| if next != u+1 { |
| t.Errorf("wrote sequential integers, but read integer out of sequence at offset %d", i) |
| return |
| } |
| u = next |
| } |
| }() |
| } |
| |
| for n := parallel; n > 0; n-- { |
| sem <- true |
| } |
| |
| var minWriteSuccesses int64 = attempts |
| if runtime.GOOS == "windows" { |
| // Windows produces frequent "Access is denied" errors under heavy rename load. |
| // As long as those are the only errors and *some* of the writes succeed, we're happy. |
| minWriteSuccesses = attempts / 4 |
| } |
| |
| if writeSuccesses < minWriteSuccesses { |
| t.Errorf("%d (of %d) writes succeeded; want ≥ %d", writeSuccesses, attempts, minWriteSuccesses) |
| } else { |
| t.Logf("%d (of %d) writes succeeded (ok: ≥ %d)", writeSuccesses, attempts, minWriteSuccesses) |
| } |
| |
| var minReadSuccesses int64 = attempts |
| |
| switch runtime.GOOS { |
| case "windows": |
| // Windows produces frequent "Access is denied" errors under heavy rename load. |
| // As long as those are the only errors and *some* of the reads succeed, we're happy. |
| minReadSuccesses = attempts / 4 |
| |
| case "darwin": |
| // The filesystem on macOS 10.14 occasionally fails with "no such file or |
| // directory" errors. See https://golang.org/issue/33041. The flake rate is |
| // fairly low, so ensure that at least 75% of attempts succeed. |
| minReadSuccesses = attempts - (attempts / 4) |
| } |
| |
| if readSuccesses < minReadSuccesses { |
| t.Errorf("%d (of %d) reads succeeded; want ≥ %d", readSuccesses, attempts, minReadSuccesses) |
| } else { |
| t.Logf("%d (of %d) reads succeeded (ok: ≥ %d)", readSuccesses, attempts, minReadSuccesses) |
| } |
| } |