github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/internal/renameio/renameio_test.go (about)

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build !plan9
     6  // +build !plan9
     7  
     8  package renameio
     9  
    10  import (
    11  	"encoding/binary"
    12  	"errors"
    13  	"math/rand"
    14  	"path/filepath"
    15  	"runtime"
    16  	"sync"
    17  	"sync/atomic"
    18  	"syscall"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/amarpal/go-tools/internal/robustio"
    23  )
    24  
    25  func TestConcurrentReadsAndWrites(t *testing.T) {
    26  	dir := t.TempDir()
    27  	path := filepath.Join(dir, "blob.bin")
    28  
    29  	const chunkWords = 8 << 10
    30  	buf := make([]byte, 2*chunkWords*8)
    31  	for i := uint64(0); i < 2*chunkWords; i++ {
    32  		binary.LittleEndian.PutUint64(buf[i*8:], i)
    33  	}
    34  
    35  	var attempts int64 = 128
    36  	if !testing.Short() {
    37  		attempts *= 16
    38  	}
    39  	const parallel = 32
    40  
    41  	var sem = make(chan bool, parallel)
    42  
    43  	var (
    44  		writeSuccesses, readSuccesses int64 // atomic
    45  		writeErrnoSeen, readErrnoSeen sync.Map
    46  	)
    47  
    48  	for n := attempts; n > 0; n-- {
    49  		sem <- true
    50  		go func() {
    51  			defer func() { <-sem }()
    52  
    53  			time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
    54  			offset := rand.Intn(chunkWords)
    55  			chunk := buf[offset*8 : (offset+chunkWords)*8]
    56  			if err := WriteFile(path, chunk, 0666); err == nil {
    57  				atomic.AddInt64(&writeSuccesses, 1)
    58  			} else if robustio.IsEphemeralError(err) {
    59  				var (
    60  					errno syscall.Errno
    61  					dup   bool
    62  				)
    63  				if errors.As(err, &errno) {
    64  					_, dup = writeErrnoSeen.LoadOrStore(errno, true)
    65  				}
    66  				if !dup {
    67  					t.Logf("ephemeral error: %v", err)
    68  				}
    69  			} else {
    70  				t.Errorf("unexpected error: %v", err)
    71  			}
    72  
    73  			time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
    74  			data, err := ReadFile(path)
    75  			if err == nil {
    76  				atomic.AddInt64(&readSuccesses, 1)
    77  			} else if robustio.IsEphemeralError(err) {
    78  				var (
    79  					errno syscall.Errno
    80  					dup   bool
    81  				)
    82  				if errors.As(err, &errno) {
    83  					_, dup = readErrnoSeen.LoadOrStore(errno, true)
    84  				}
    85  				if !dup {
    86  					t.Logf("ephemeral error: %v", err)
    87  				}
    88  				return
    89  			} else {
    90  				t.Errorf("unexpected error: %v", err)
    91  				return
    92  			}
    93  
    94  			if len(data) != 8*chunkWords {
    95  				t.Errorf("read %d bytes, but each write is a %d-byte file", len(data), 8*chunkWords)
    96  				return
    97  			}
    98  
    99  			u := binary.LittleEndian.Uint64(data)
   100  			for i := 1; i < chunkWords; i++ {
   101  				next := binary.LittleEndian.Uint64(data[i*8:])
   102  				if next != u+1 {
   103  					t.Errorf("wrote sequential integers, but read integer out of sequence at offset %d", i)
   104  					return
   105  				}
   106  				u = next
   107  			}
   108  		}()
   109  	}
   110  
   111  	for n := parallel; n > 0; n-- {
   112  		sem <- true
   113  	}
   114  
   115  	var minWriteSuccesses int64 = attempts
   116  	if runtime.GOOS == "windows" {
   117  		// Windows produces frequent "Access is denied" errors under heavy rename load.
   118  		// As long as those are the only errors and *some* of the writes succeed, we're happy.
   119  		minWriteSuccesses = attempts / 4
   120  	}
   121  
   122  	if writeSuccesses < minWriteSuccesses {
   123  		t.Errorf("%d (of %d) writes succeeded; want ≥ %d", writeSuccesses, attempts, minWriteSuccesses)
   124  	} else {
   125  		t.Logf("%d (of %d) writes succeeded (ok: ≥ %d)", writeSuccesses, attempts, minWriteSuccesses)
   126  	}
   127  
   128  	var minReadSuccesses int64 = attempts
   129  
   130  	switch runtime.GOOS {
   131  	case "windows":
   132  		// Windows produces frequent "Access is denied" errors under heavy rename load.
   133  		// As long as those are the only errors and *some* of the reads succeed, we're happy.
   134  		minReadSuccesses = attempts / 4
   135  
   136  	case "darwin":
   137  		// The filesystem on macOS 10.14 occasionally fails with "no such file or
   138  		// directory" errors. See https://golang.org/issue/33041. The flake rate is
   139  		// fairly low, so ensure that at least 75% of attempts succeed.
   140  		minReadSuccesses = attempts - (attempts / 4)
   141  	}
   142  
   143  	if readSuccesses < minReadSuccesses {
   144  		t.Errorf("%d (of %d) reads succeeded; want ≥ %d", readSuccesses, attempts, minReadSuccesses)
   145  	} else {
   146  		t.Logf("%d (of %d) reads succeeded (ok: ≥ %d)", readSuccesses, attempts, minReadSuccesses)
   147  	}
   148  }