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 5package counter 6 7import ( 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. 27type 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 57var defaultFile file 58 59// register ensures that the counter c is registered with the file. 60func (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. 97func (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. 113func (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. 127var ErrDisabled = errors.New("counter: disabled as Go telemetry is off") 128 129var ( 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. 138func 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. 179func (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 196func nop() {} 197 198// CounterTime returns the current UTC time. 199// Mutable for testing. 200var 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]. 206func 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. 225func (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 325func (f *file) newCounter(name string) *atomic.Uint64 { 326 v, cleanup := f.newCounter1(name) 327 cleanup() 328 return v 329} 330 331func (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 360var ( 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.) 377func 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 412const ( 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 444type 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. 462func 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 524func 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 537func (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 556var memmap = mmap.Mmap 557var munmap = mmap.Munmap 558 559func (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. 576func 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 589func (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 596func (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. 606func (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. 626func (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 643func (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. 664func (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 780func (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. 812func round[T int | uint32](x T, unit T) T { 813 return (x + unit - 1) &^ (unit - 1) 814} 815