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