...

Source file src/cmd/vendor/golang.org/x/telemetry/internal/counter/file.go

Documentation: cmd/vendor/golang.org/x/telemetry/internal/counter

     1  // Copyright 2023 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  package counter
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"math/rand"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"runtime"
    16  	"runtime/debug"
    17  	"sync"
    18  	"sync/atomic"
    19  	"time"
    20  	"unsafe"
    21  
    22  	"golang.org/x/telemetry/internal/mmap"
    23  	"golang.org/x/telemetry/internal/telemetry"
    24  )
    25  
    26  // A file is a counter file.
    27  type file struct {
    28  	// Linked list of all known counters.
    29  	// (Linked list insertion is easy to make lock-free,
    30  	// and we don't want the initial counters incremented
    31  	// by a program to cause significant contention.)
    32  	counters atomic.Pointer[Counter] // head of list
    33  	end      Counter                 // list ends at &end instead of nil
    34  
    35  	mu                 sync.Mutex
    36  	buildInfo          *debug.BuildInfo
    37  	timeBegin, timeEnd time.Time
    38  	err                error
    39  	// current holds the current file mapping, which may change when the file is
    40  	// rotated or extended.
    41  	//
    42  	// current may be read without holding mu, but may be nil.
    43  	//
    44  	// The cleanup logic for file mappings is complicated, because invalidating
    45  	// counter pointers is reentrant: [file.invalidateCounters] may call
    46  	// [file.lookup], which acquires mu. Therefore, writing current must be done
    47  	// as follows:
    48  	//  1. record the previous value of current
    49  	//  2. Store a new value in current
    50  	//  3. unlock mu
    51  	//  4. call invalidateCounters
    52  	//  5. close the previous mapped value from (1)
    53  	// TODO(rfindley): simplify
    54  	current atomic.Pointer[mappedFile]
    55  }
    56  
    57  var defaultFile file
    58  
    59  // register ensures that the counter c is registered with the file.
    60  func (f *file) register(c *Counter) {
    61  	debugPrintf("register %s %p\n", c.Name(), c)
    62  
    63  	// If counter is not registered with file, register it.
    64  	// Doing this lazily avoids init-time work
    65  	// as well as any execution cost at all for counters
    66  	// that are not used in a given program.
    67  	wroteNext := false
    68  	for wroteNext || c.next.Load() == nil {
    69  		head := f.counters.Load()
    70  		next := head
    71  		if next == nil {
    72  			next = &f.end
    73  		}
    74  		debugPrintf("register %s next %p\n", c.Name(), next)
    75  		if !wroteNext {
    76  			if !c.next.CompareAndSwap(nil, next) {
    77  				debugPrintf("register %s cas failed %p\n", c.Name(), c.next.Load())
    78  				continue
    79  			}
    80  			wroteNext = true
    81  		} else {
    82  			c.next.Store(next)
    83  		}
    84  		if f.counters.CompareAndSwap(head, c) {
    85  			debugPrintf("registered %s %p\n", c.Name(), f.counters.Load())
    86  			return
    87  		}
    88  		debugPrintf("register %s cas2 failed %p %p\n", c.Name(), f.counters.Load(), head)
    89  	}
    90  }
    91  
    92  // invalidateCounters marks as invalid all the pointers
    93  // held by f's counters and then refreshes them.
    94  //
    95  // invalidateCounters cannot be called while holding f.mu,
    96  // because a counter refresh may call f.lookup.
    97  func (f *file) invalidateCounters() {
    98  	// Mark every counter as needing to refresh its count pointer.
    99  	if head := f.counters.Load(); head != nil {
   100  		for c := head; c != &f.end; c = c.next.Load() {
   101  			c.invalidate()
   102  		}
   103  		for c := head; c != &f.end; c = c.next.Load() {
   104  			c.refresh()
   105  		}
   106  	}
   107  }
   108  
   109  // lookup looks up the counter with the given name in the file,
   110  // allocating it if needed, and returns a pointer to the atomic.Uint64
   111  // containing the counter data.
   112  // If the file has not been opened yet, lookup returns nil.
   113  func (f *file) lookup(name string) counterPtr {
   114  	current := f.current.Load()
   115  	if current == nil {
   116  		debugPrintf("lookup %s - no mapped file\n", name)
   117  		return counterPtr{}
   118  	}
   119  	ptr := f.newCounter(name)
   120  	if ptr == nil {
   121  		return counterPtr{}
   122  	}
   123  	return counterPtr{current, ptr}
   124  }
   125  
   126  // ErrDisabled is the error returned when telemetry is disabled.
   127  var ErrDisabled = errors.New("counter: disabled as Go telemetry is off")
   128  
   129  var (
   130  	errNoBuildInfo = errors.New("counter: missing build info")
   131  	errCorrupt     = errors.New("counter: corrupt counter file")
   132  )
   133  
   134  // weekEnd returns the day of the week on which uploads occur (and therefore
   135  // counters expire).
   136  //
   137  // Reads the weekends file, creating one if none exists.
   138  func weekEnd() (time.Weekday, error) {
   139  	// If there is no 'weekends' file create it and initialize it
   140  	// to a random day of the week. There is a short interval for
   141  	// a race.
   142  	weekends := filepath.Join(telemetry.Default.LocalDir(), "weekends")
   143  	day := fmt.Sprintf("%d\n", rand.Intn(7))
   144  	if _, err := os.ReadFile(weekends); err != nil {
   145  		if err := os.MkdirAll(telemetry.Default.LocalDir(), 0777); err != nil {
   146  			debugPrintf("%v: could not create telemetry.LocalDir %s", err, telemetry.Default.LocalDir())
   147  			return 0, err
   148  		}
   149  		if err = os.WriteFile(weekends, []byte(day), 0666); err != nil {
   150  			return 0, err
   151  		}
   152  	}
   153  
   154  	// race is over, read the file
   155  	buf, err := os.ReadFile(weekends)
   156  	// There is no reasonable way of recovering from errors
   157  	// so we just fail
   158  	if err != nil {
   159  		return 0, err
   160  	}
   161  	buf = bytes.TrimSpace(buf)
   162  	if len(buf) == 0 {
   163  		return 0, fmt.Errorf("empty weekends file")
   164  	}
   165  	weekend := time.Weekday(buf[0] - '0') // 0 is Sunday
   166  	// paranoia to make sure the value is legal
   167  	weekend %= 7
   168  	if weekend < 0 {
   169  		weekend += 7
   170  	}
   171  	return weekend, nil
   172  }
   173  
   174  // rotate checks to see whether the file f needs to be rotated,
   175  // meaning to start a new counter file with a different date in the name.
   176  // rotate is also used to open the file initially, meaning f.current can be nil.
   177  // In general rotate should be called just once for each file.
   178  // rotate will arrange a timer to call itself again when necessary.
   179  func (f *file) rotate() {
   180  	expiry := f.rotate1()
   181  	if !expiry.IsZero() {
   182  		delay := time.Until(expiry)
   183  		// Some tests set CounterTime to a time in the past, causing delay to be
   184  		// negative. Avoid infinite loops by delaying at least a short interval.
   185  		//
   186  		// TODO(rfindley): instead, just also mock AfterFunc.
   187  		const minDelay = 1 * time.Minute
   188  		if delay < minDelay {
   189  			delay = minDelay
   190  		}
   191  		// TODO(rsc): Does this do the right thing for laptops closing?
   192  		time.AfterFunc(delay, f.rotate)
   193  	}
   194  }
   195  
   196  func nop() {}
   197  
   198  // CounterTime returns the current UTC time.
   199  // Mutable for testing.
   200  var CounterTime = func() time.Time {
   201  	return time.Now().UTC()
   202  }
   203  
   204  // counterSpan returns the current time span for a counter file, as determined
   205  // by [CounterTime] and the [weekEnd].
   206  func counterSpan() (begin, end time.Time, _ error) {
   207  	year, month, day := CounterTime().Date()
   208  	begin = time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
   209  	// files always begin today, but expire on the next day of the week
   210  	// from the 'weekends' file.
   211  	weekend, err := weekEnd()
   212  	if err != nil {
   213  		return time.Time{}, time.Time{}, err
   214  	}
   215  	incr := int(weekend - begin.Weekday())
   216  	if incr <= 0 {
   217  		incr += 7 // ensure that end is later than begin
   218  	}
   219  	end = time.Date(year, month, day+incr, 0, 0, 0, 0, time.UTC)
   220  	return begin, end, nil
   221  }
   222  
   223  // rotate1 rotates the current counter file, returning its expiry, or the zero
   224  // time if rotation failed.
   225  func (f *file) rotate1() time.Time {
   226  	// Cleanup must be performed while unlocked, since invalidateCounters may
   227  	// involve calls to f.lookup.
   228  	var previous *mappedFile // read below while holding the f.mu.
   229  	defer func() {
   230  		// Counters must be invalidated whenever the mapped file changes.
   231  		if next := f.current.Load(); next != previous {
   232  			f.invalidateCounters()
   233  			// Ensure that the previous counter mapped file is closed.
   234  			if previous != nil {
   235  				previous.close() // safe to call multiple times
   236  			}
   237  		}
   238  	}()
   239  
   240  	f.mu.Lock()
   241  	defer f.mu.Unlock()
   242  
   243  	previous = f.current.Load()
   244  
   245  	if f.err != nil {
   246  		return time.Time{} // already in failed state; nothing to do
   247  	}
   248  
   249  	fail := func(err error) {
   250  		debugPrintf("rotate: %v", err)
   251  		f.err = err
   252  		f.current.Store(nil)
   253  	}
   254  
   255  	if mode, _ := telemetry.Default.Mode(); mode == "off" {
   256  		// TODO(rfindley): do we ever want to make ErrDisabled recoverable?
   257  		// Specifically, if f.err is ErrDisabled, should we check again during when
   258  		// rotating?
   259  		fail(ErrDisabled)
   260  		return time.Time{}
   261  	}
   262  
   263  	if f.buildInfo == nil {
   264  		bi, ok := debug.ReadBuildInfo()
   265  		if !ok {
   266  			fail(errNoBuildInfo)
   267  			return time.Time{}
   268  		}
   269  		f.buildInfo = bi
   270  	}
   271  
   272  	begin, end, err := counterSpan()
   273  	if err != nil {
   274  		fail(err)
   275  		return time.Time{}
   276  	}
   277  	if f.timeBegin.Equal(begin) && f.timeEnd.Equal(end) {
   278  		return f.timeEnd // nothing to do
   279  	}
   280  	f.timeBegin, f.timeEnd = begin, end
   281  
   282  	goVers, progPath, progVers := telemetry.ProgramInfo(f.buildInfo)
   283  	meta := fmt.Sprintf("TimeBegin: %s\nTimeEnd: %s\nProgram: %s\nVersion: %s\nGoVersion: %s\nGOOS: %s\nGOARCH: %s\n\n",
   284  		f.timeBegin.Format(time.RFC3339), f.timeEnd.Format(time.RFC3339),
   285  		progPath, progVers, goVers, runtime.GOOS, runtime.GOARCH)
   286  	if len(meta) > maxMetaLen { // should be impossible for our use
   287  		fail(fmt.Errorf("metadata too long"))
   288  		return time.Time{}
   289  	}
   290  
   291  	if progVers != "" {
   292  		progVers = "@" + progVers
   293  	}
   294  	baseName := fmt.Sprintf("%s%s-%s-%s-%s-%s.%s.count",
   295  		path.Base(progPath),
   296  		progVers,
   297  		goVers,
   298  		runtime.GOOS,
   299  		runtime.GOARCH,
   300  		f.timeBegin.Format(time.DateOnly),
   301  		FileVersion,
   302  	)
   303  	dir := telemetry.Default.LocalDir()
   304  	if err := os.MkdirAll(dir, 0777); err != nil {
   305  		fail(fmt.Errorf("making local dir: %v", err))
   306  		return time.Time{}
   307  	}
   308  	name := filepath.Join(dir, baseName)
   309  
   310  	m, err := openMapped(name, meta)
   311  	if err != nil {
   312  		// Mapping failed:
   313  		// If there used to be a mapped file, after cleanup
   314  		// incrementing counters will only change their internal state.
   315  		// (before cleanup the existing mapped file would be updated)
   316  		fail(fmt.Errorf("openMapped: %v", err))
   317  		return time.Time{}
   318  	}
   319  
   320  	debugPrintf("using %v", m.f.Name())
   321  	f.current.Store(m)
   322  	return f.timeEnd
   323  }
   324  
   325  func (f *file) newCounter(name string) *atomic.Uint64 {
   326  	v, cleanup := f.newCounter1(name)
   327  	cleanup()
   328  	return v
   329  }
   330  
   331  func (f *file) newCounter1(name string) (v *atomic.Uint64, cleanup func()) {
   332  	f.mu.Lock()
   333  	defer f.mu.Unlock()
   334  
   335  	current := f.current.Load()
   336  	if current == nil {
   337  		return nil, nop
   338  	}
   339  	debugPrintf("newCounter %s in %s\n", name, current.f.Name())
   340  	if v, _, _, _ := current.lookup(name); v != nil {
   341  		return v, nop
   342  	}
   343  	v, newM, err := current.newCounter(name)
   344  	if err != nil {
   345  		debugPrintf("newCounter %s: %v\n", name, err)
   346  		return nil, nop
   347  	}
   348  
   349  	cleanup = nop
   350  	if newM != nil {
   351  		f.current.Store(newM)
   352  		cleanup = func() {
   353  			f.invalidateCounters()
   354  			current.close()
   355  		}
   356  	}
   357  	return v, cleanup
   358  }
   359  
   360  var (
   361  	openOnce sync.Once
   362  	// rotating reports whether the call to Open had rotate = true.
   363  	//
   364  	// In golang/go#68497, we observed that file rotation can break runtime
   365  	// deadlock detection. To minimize the fix for 1.23, we are splitting the
   366  	// Open API into one version that rotates the counter file, and another that
   367  	// does not. The rotating variable guards against use of both APIs from the
   368  	// same process.
   369  	rotating bool
   370  )
   371  
   372  // Open associates counting with the defaultFile.
   373  // The returned function is for testing only, and should
   374  // be called after all Inc()s are finished, but before
   375  // any reports are generated.
   376  // (Otherwise expired count files will not be deleted on Windows.)
   377  func Open(rotate bool) func() {
   378  	if telemetry.DisabledOnPlatform {
   379  		return func() {}
   380  	}
   381  	close := func() {}
   382  	openOnce.Do(func() {
   383  		rotating = rotate
   384  		if mode, _ := telemetry.Default.Mode(); mode == "off" {
   385  			// Don't open the file when telemetry is off.
   386  			defaultFile.err = ErrDisabled
   387  			// No need to clean up.
   388  			return
   389  		}
   390  		debugPrintf("Open(%v)", rotate)
   391  		if rotate {
   392  			defaultFile.rotate() // calls rotate1 and schedules a rotation
   393  		} else {
   394  			defaultFile.rotate1()
   395  		}
   396  		close = func() {
   397  			// Once this has been called, the defaultFile is no longer usable.
   398  			mf := defaultFile.current.Load()
   399  			if mf == nil {
   400  				// telemetry might have been off
   401  				return
   402  			}
   403  			mf.close()
   404  		}
   405  	})
   406  	if rotating != rotate {
   407  		panic("BUG: Open called with inconsistent values for 'rotate'")
   408  	}
   409  	return close
   410  }
   411  
   412  const (
   413  	FileVersion = "v1"
   414  	hdrPrefix   = "# telemetry/counter file " + FileVersion + "\n"
   415  	recordUnit  = 32
   416  	maxMetaLen  = 512
   417  	numHash     = 512 // 2kB for hash table
   418  	maxNameLen  = 4 * 1024
   419  	limitOff    = 0
   420  	hashOff     = 4
   421  	pageSize    = 16 * 1024
   422  	minFileLen  = 16 * 1024
   423  )
   424  
   425  // A mappedFile is a counter file mmapped into memory.
   426  //
   427  // The file layout for a mappedFile m is as follows:
   428  //
   429  //	offset, byte size:                 description
   430  //	------------------                 -----------
   431  //	0, hdrLen:                         header, containing metadata; see [mappedHeader]
   432  //	hdrLen+limitOff, 4:                uint32 allocation limit (byte offset of the end of counter records)
   433  //	hdrLen+hashOff, 4*numHash:         hash table, stores uint32 heads of a linked list of records, keyed by name hash
   434  //	hdrLen+hashOff+4*numHash to limit: counter records: see record syntax below
   435  //
   436  // The record layout is as follows:
   437  //
   438  //	offset, byte size: description
   439  //	------------------ -----------
   440  //	0, 8:              uint64 counter value
   441  //	8, 12:             uint32 name length
   442  //	12, 16:            uint32 offset of next record in linked list
   443  //	16, name length:   counter name
   444  type mappedFile struct {
   445  	meta      string
   446  	hdrLen    uint32
   447  	zero      [4]byte
   448  	closeOnce sync.Once
   449  	f         *os.File
   450  	mapping   *mmap.Data
   451  }
   452  
   453  // openMapped opens and memory maps a file.
   454  //
   455  // name is the path to the file.
   456  //
   457  // meta is the file metadata, which must match the metadata of the file on disk
   458  // exactly.
   459  //
   460  // existing should be nil the first time this is called for a file,
   461  // and when remapping, should be the previous mappedFile.
   462  func openMapped(name, meta string) (_ *mappedFile, err error) {
   463  	hdr, err := mappedHeader(meta)
   464  	if err != nil {
   465  		return nil, err
   466  	}
   467  
   468  	f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  	// Note: using local variable m here, not return value,
   473  	// so that return nil, err does not set m = nil and break the code in the defer.
   474  	m := &mappedFile{
   475  		f:    f,
   476  		meta: meta,
   477  	}
   478  
   479  	defer func() {
   480  		if err != nil {
   481  			m.close()
   482  		}
   483  	}()
   484  
   485  	info, err := f.Stat()
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  
   490  	// Establish file header and initial data area if not already present.
   491  	if info.Size() < minFileLen {
   492  		if _, err := f.WriteAt(hdr, 0); err != nil {
   493  			return nil, err
   494  		}
   495  		// Write zeros at the end of the file to extend it to minFileLen.
   496  		if _, err := f.WriteAt(m.zero[:], int64(minFileLen-len(m.zero))); err != nil {
   497  			return nil, err
   498  		}
   499  		info, err = f.Stat()
   500  		if err != nil {
   501  			return nil, err
   502  		}
   503  		if info.Size() < minFileLen {
   504  			return nil, fmt.Errorf("counter: writing file did not extend it")
   505  		}
   506  	}
   507  
   508  	// Map into memory.
   509  	mapping, err := memmap(f)
   510  	if err != nil {
   511  		return nil, err
   512  	}
   513  	m.mapping = mapping
   514  	if !bytes.HasPrefix(m.mapping.Data, hdr) {
   515  		// TODO(rfindley): we can and should do better here, reading the mapped
   516  		// header length and comparing headers exactly.
   517  		return nil, fmt.Errorf("counter: header mismatch")
   518  	}
   519  	m.hdrLen = uint32(len(hdr))
   520  
   521  	return m, nil
   522  }
   523  
   524  func mappedHeader(meta string) ([]byte, error) {
   525  	if len(meta) > maxMetaLen {
   526  		return nil, fmt.Errorf("counter: metadata too large")
   527  	}
   528  	np := round(len(hdrPrefix), 4)
   529  	n := round(np+4+len(meta), 32)
   530  	hdr := make([]byte, n)
   531  	copy(hdr, hdrPrefix)
   532  	*(*uint32)(unsafe.Pointer(&hdr[np])) = uint32(n)
   533  	copy(hdr[np+4:], meta)
   534  	return hdr, nil
   535  }
   536  
   537  func (m *mappedFile) place(limit uint32, name string) (start, end uint32) {
   538  	if limit == 0 {
   539  		// first record in file
   540  		limit = m.hdrLen + hashOff + 4*numHash
   541  	}
   542  	n := round(uint32(16+len(name)), recordUnit)
   543  	start = round(limit, recordUnit) // should already be rounded but just in case
   544  	// Note: Checking for crossing a page boundary would be
   545  	// start/pageSize != (start+n-1)/pageSize,
   546  	// but we are checking for reaching the page end, so no -1.
   547  	// The page end is reserved for use by extend.
   548  	// See the comment in m.extend.
   549  	if start/pageSize != (start+n)/pageSize {
   550  		// bump start to next page
   551  		start = round(limit, pageSize)
   552  	}
   553  	return start, start + n
   554  }
   555  
   556  var memmap = mmap.Mmap
   557  var munmap = mmap.Munmap
   558  
   559  func (m *mappedFile) close() {
   560  	m.closeOnce.Do(func() {
   561  		if m.mapping != nil {
   562  			munmap(m.mapping)
   563  			m.mapping = nil
   564  		}
   565  		if m.f != nil {
   566  			m.f.Close() // best effort
   567  			m.f = nil
   568  		}
   569  	})
   570  }
   571  
   572  // hash returns the hash code for name.
   573  // The implementation is FNV-1a.
   574  // This hash function is a fixed detail of the file format.
   575  // It cannot be changed without also changing the file format version.
   576  func hash(name string) uint32 {
   577  	const (
   578  		offset32 = 2166136261
   579  		prime32  = 16777619
   580  	)
   581  	h := uint32(offset32)
   582  	for i := 0; i < len(name); i++ {
   583  		c := name[i]
   584  		h = (h ^ uint32(c)) * prime32
   585  	}
   586  	return (h ^ (h >> 16)) % numHash
   587  }
   588  
   589  func (m *mappedFile) load32(off uint32) uint32 {
   590  	if int64(off) >= int64(len(m.mapping.Data)) {
   591  		return 0
   592  	}
   593  	return (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off])).Load()
   594  }
   595  
   596  func (m *mappedFile) cas32(off, old, new uint32) bool {
   597  	if int64(off) >= int64(len(m.mapping.Data)) {
   598  		panic("bad cas32") // return false would probably loop
   599  	}
   600  	return (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off])).CompareAndSwap(old, new)
   601  }
   602  
   603  // entryAt reads a counter record at the given byte offset.
   604  //
   605  // See the documentation for [mappedFile] for a description of the counter record layout.
   606  func (m *mappedFile) entryAt(off uint32) (name []byte, next uint32, v *atomic.Uint64, ok bool) {
   607  	if off < m.hdrLen+hashOff || int64(off)+16 > int64(len(m.mapping.Data)) {
   608  		return nil, 0, nil, false
   609  	}
   610  	nameLen := m.load32(off+8) & 0x00ffffff
   611  	if nameLen == 0 || int64(off)+16+int64(nameLen) > int64(len(m.mapping.Data)) {
   612  		return nil, 0, nil, false
   613  	}
   614  	name = m.mapping.Data[off+16 : off+16+nameLen]
   615  	next = m.load32(off + 12)
   616  	v = (*atomic.Uint64)(unsafe.Pointer(&m.mapping.Data[off]))
   617  	return name, next, v, true
   618  }
   619  
   620  // writeEntryAt writes a new counter record at the given offset.
   621  //
   622  // See the documentation for [mappedFile] for a description of the counter record layout.
   623  //
   624  // writeEntryAt only returns false in the presence of some form of corruption:
   625  // an offset outside the bounds of the record region in the mapped file.
   626  func (m *mappedFile) writeEntryAt(off uint32, name string) (next *atomic.Uint32, v *atomic.Uint64, ok bool) {
   627  	// TODO(rfindley): shouldn't this first condition be off < m.hdrLen+hashOff+4*numHash?
   628  	if off < m.hdrLen+hashOff || int64(off)+16+int64(len(name)) > int64(len(m.mapping.Data)) {
   629  		return nil, nil, false
   630  	}
   631  	copy(m.mapping.Data[off+16:], name)
   632  	atomic.StoreUint32((*uint32)(unsafe.Pointer(&m.mapping.Data[off+8])), uint32(len(name))|0xff000000)
   633  	next = (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off+12]))
   634  	v = (*atomic.Uint64)(unsafe.Pointer(&m.mapping.Data[off]))
   635  	return next, v, true
   636  }
   637  
   638  // lookup searches the mapped file for a counter record with the given name, returning:
   639  //   - v: the mapped counter value
   640  //   - headOff: the offset of the head pointer (see [mappedFile])
   641  //   - head: the value of the head pointer
   642  //   - ok: whether lookup succeeded
   643  func (m *mappedFile) lookup(name string) (v *atomic.Uint64, headOff, head uint32, ok bool) {
   644  	h := hash(name)
   645  	headOff = m.hdrLen + hashOff + h*4
   646  	head = m.load32(headOff)
   647  	off := head
   648  	for off != 0 {
   649  		ename, next, v, ok := m.entryAt(off)
   650  		if !ok {
   651  			return nil, 0, 0, false
   652  		}
   653  		if string(ename) == name {
   654  			return v, headOff, head, true
   655  		}
   656  		off = next
   657  	}
   658  	return nil, headOff, head, true
   659  }
   660  
   661  // newCounter allocates and writes a new counter record with the given name.
   662  //
   663  // If name is already recorded in the file, newCounter returns the existing counter.
   664  func (m *mappedFile) newCounter(name string) (v *atomic.Uint64, m1 *mappedFile, err error) {
   665  	if len(name) > maxNameLen {
   666  		return nil, nil, fmt.Errorf("counter name too long")
   667  	}
   668  	orig := m
   669  	defer func() {
   670  		if m != orig {
   671  			if err != nil {
   672  				m.close()
   673  			} else {
   674  				m1 = m
   675  			}
   676  		}
   677  	}()
   678  
   679  	v, headOff, head, ok := m.lookup(name)
   680  	for tries := 0; !ok; tries++ {
   681  		if tries >= 10 {
   682  			debugFatalf("corrupt: failed to remap after 10 tries")
   683  			return nil, nil, errCorrupt
   684  		}
   685  		// Lookup found an invalid pointer,
   686  		// perhaps because the file has grown larger than the mapping.
   687  		limit := m.load32(m.hdrLen + limitOff)
   688  		if limit, datalen := int64(limit), int64(len(m.mapping.Data)); limit <= datalen {
   689  			// Mapping doesn't need to grow, so lookup found actual corruption,
   690  			// in the form of an entry pointer that exceeds the recorded allocation
   691  			// limit. This should never happen, unless the actual file contents are
   692  			// corrupt.
   693  			debugFatalf("corrupt: limit %d is within mapping length %d", limit, datalen)
   694  			return nil, nil, errCorrupt
   695  		}
   696  		// That the recorded limit is greater than the mapped data indicates that
   697  		// an external process has extended the file. Re-map to pick up this extension.
   698  		newM, err := openMapped(m.f.Name(), m.meta)
   699  		if err != nil {
   700  			return nil, nil, err
   701  		}
   702  		if limit, datalen := int64(limit), int64(len(newM.mapping.Data)); limit > datalen {
   703  			// We've re-mapped, yet limit still exceeds the data length. This
   704  			// indicates that the underlying file was somehow truncated, or the
   705  			// recorded limit is corrupt.
   706  			debugFatalf("corrupt: limit %d exceeds file size %d", limit, datalen)
   707  			return nil, nil, errCorrupt
   708  		}
   709  		// If m != orig, this is at least the second time around the loop
   710  		// trying to open the mapping. Close the previous attempt.
   711  		if m != orig {
   712  			m.close()
   713  		}
   714  		m = newM
   715  		v, headOff, head, ok = m.lookup(name)
   716  	}
   717  	if v != nil {
   718  		return v, nil, nil
   719  	}
   720  
   721  	// Reserve space for new record.
   722  	// We are competing against other programs using the same file,
   723  	// so we use a compare-and-swap on the allocation limit in the header.
   724  	var start, end uint32
   725  	for {
   726  		// Determine where record should end, and grow file if needed.
   727  		limit := m.load32(m.hdrLen + limitOff)
   728  		start, end = m.place(limit, name)
   729  		debugPrintf("place %s at %#x-%#x\n", name, start, end)
   730  		if int64(end) > int64(len(m.mapping.Data)) {
   731  			newM, err := m.extend(end)
   732  			if err != nil {
   733  				return nil, nil, err
   734  			}
   735  			if m != orig {
   736  				m.close()
   737  			}
   738  			m = newM
   739  			continue
   740  		}
   741  
   742  		// Attempt to reserve that space for our record.
   743  		if m.cas32(m.hdrLen+limitOff, limit, end) {
   744  			break
   745  		}
   746  	}
   747  
   748  	// Write record.
   749  	next, v, ok := m.writeEntryAt(start, name)
   750  	if !ok {
   751  		debugFatalf("corrupt: failed to write entry: %#x+%d vs %#x\n", start, len(name), len(m.mapping.Data))
   752  		return nil, nil, errCorrupt // more likely our math is wrong
   753  	}
   754  
   755  	// Link record into hash chain, making sure not to introduce a duplicate.
   756  	// We know name does not appear in the chain starting at head.
   757  	for {
   758  		next.Store(head)
   759  		if m.cas32(headOff, head, start) {
   760  			return v, nil, nil
   761  		}
   762  
   763  		// Check new elements in chain for duplicates.
   764  		old := head
   765  		head = m.load32(headOff)
   766  		for off := head; off != old; {
   767  			ename, enext, v, ok := m.entryAt(off)
   768  			if !ok {
   769  				return nil, nil, errCorrupt
   770  			}
   771  			if string(ename) == name {
   772  				next.Store(^uint32(0)) // mark ours as dead
   773  				return v, nil, nil
   774  			}
   775  			off = enext
   776  		}
   777  	}
   778  }
   779  
   780  func (m *mappedFile) extend(end uint32) (*mappedFile, error) {
   781  	end = round(end, pageSize)
   782  	info, err := m.f.Stat()
   783  	if err != nil {
   784  		return nil, err
   785  	}
   786  	if info.Size() < int64(end) {
   787  		// Note: multiple processes could be calling extend at the same time,
   788  		// but this write only writes the last 4 bytes of the page.
   789  		// The last 4 bytes of the page are reserved for this purpose and hold no data.
   790  		// (In m.place, if a new record would extend to the very end of the page,
   791  		// it is placed in the next page instead.)
   792  		// So it is fine if multiple processes extend at the same time.
   793  		if _, err := m.f.WriteAt(m.zero[:], int64(end)-int64(len(m.zero))); err != nil {
   794  			return nil, err
   795  		}
   796  	}
   797  	newM, err := openMapped(m.f.Name(), m.meta)
   798  	if err != nil {
   799  		return nil, err
   800  	}
   801  	if int64(len(newM.mapping.Data)) < int64(end) {
   802  		// File system or logic bug: new file is somehow not extended.
   803  		// See go.dev/issue/68311, where this appears to have been happening.
   804  		newM.close()
   805  		return nil, errCorrupt
   806  	}
   807  	return newM, err
   808  }
   809  
   810  // round returns x rounded up to the next multiple of unit,
   811  // which must be a power of two.
   812  func round[T int | uint32](x T, unit T) T {
   813  	return (x + unit - 1) &^ (unit - 1)
   814  }
   815  

View as plain text