github.com/zuoyebang/bitalosdb@v1.1.1-0.20240516111551-79a8c4d8ce20/internal/vfs/syncing_file.go (about)

     1  // Copyright 2021 The Bitalosdb author(hustxrb@163.com) and other contributors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package vfs
    16  
    17  import (
    18  	"sync/atomic"
    19  
    20  	"github.com/cockroachdb/errors"
    21  )
    22  
    23  // SyncingFileOptions holds the options for a syncingFile.
    24  type SyncingFileOptions struct {
    25  	BytesPerSync    int
    26  	PreallocateSize int
    27  }
    28  
    29  type syncingFile struct {
    30  	File
    31  	fd              uintptr
    32  	useSyncRange    bool
    33  	bytesPerSync    int64
    34  	preallocateSize int64
    35  	atomic          struct {
    36  		// The offset at which dirty data has been written.
    37  		offset int64
    38  		// The offset at which data has been synced. Note that if SyncFileRange is
    39  		// being used, the periodic syncing of data during writing will only ever
    40  		// sync up to offset-1MB. This is done to avoid rewriting the tail of the
    41  		// file multiple times, but has the side effect of ensuring that Close will
    42  		// sync the file's metadata.
    43  		syncOffset int64
    44  	}
    45  	preallocatedBlocks int64
    46  	syncData           func() error
    47  	syncTo             func(offset int64) error
    48  	timeDiskOp         func(op func())
    49  }
    50  
    51  // NewSyncingFile wraps a writable file and ensures that data is synced
    52  // periodically as it is written. The syncing does not provide persistency
    53  // guarantees for these periodic syncs, but is used to avoid latency spikes if
    54  // the OS automatically decides to write out a large chunk of dirty filesystem
    55  // buffers. The underlying file is fully synced upon close.
    56  func NewSyncingFile(f File, opts SyncingFileOptions) File {
    57  	s := &syncingFile{
    58  		File:            f,
    59  		bytesPerSync:    int64(opts.BytesPerSync),
    60  		preallocateSize: int64(opts.PreallocateSize),
    61  	}
    62  	// Ensure a file that is opened and then closed will be synced, even if no
    63  	// data has been written to it.
    64  	s.atomic.syncOffset = -1
    65  
    66  	type fd interface {
    67  		Fd() uintptr
    68  	}
    69  	if d, ok := f.(fd); ok {
    70  		s.fd = d.Fd()
    71  	}
    72  	type dhChecker interface {
    73  		timeDiskOp(op func())
    74  	}
    75  	if d, ok := f.(dhChecker); ok {
    76  		s.timeDiskOp = d.timeDiskOp
    77  	} else {
    78  		s.timeDiskOp = func(op func()) {
    79  			op()
    80  		}
    81  	}
    82  
    83  	s.init()
    84  
    85  	if s.syncData == nil {
    86  		s.syncData = s.File.Sync
    87  	}
    88  	return WithFd(f, s)
    89  }
    90  
    91  // NB: syncingFile.Write is unsafe for concurrent use!
    92  func (f *syncingFile) Write(p []byte) (n int, err error) {
    93  	_ = f.preallocate(atomic.LoadInt64(&f.atomic.offset))
    94  
    95  	n, err = f.File.Write(p)
    96  	if err != nil {
    97  		return n, errors.WithStack(err)
    98  	}
    99  	// The offset is updated atomically so that it can be accessed safely from
   100  	// Sync.
   101  	atomic.AddInt64(&f.atomic.offset, int64(n))
   102  	if err := f.maybeSync(); err != nil {
   103  		return 0, err
   104  	}
   105  	return n, nil
   106  }
   107  
   108  func (f *syncingFile) preallocate(offset int64) error {
   109  	if f.fd == 0 || f.preallocateSize == 0 {
   110  		return nil
   111  	}
   112  
   113  	newPreallocatedBlocks := (offset + f.preallocateSize - 1) / f.preallocateSize
   114  	if newPreallocatedBlocks <= f.preallocatedBlocks {
   115  		return nil
   116  	}
   117  
   118  	length := f.preallocateSize * (newPreallocatedBlocks - f.preallocatedBlocks)
   119  	offset = f.preallocateSize * f.preallocatedBlocks
   120  	f.preallocatedBlocks = newPreallocatedBlocks
   121  	return preallocExtend(f.fd, offset, length)
   122  }
   123  
   124  func (f *syncingFile) ratchetSyncOffset(offset int64) {
   125  	for {
   126  		syncOffset := atomic.LoadInt64(&f.atomic.syncOffset)
   127  		if syncOffset >= offset {
   128  			return
   129  		}
   130  		if atomic.CompareAndSwapInt64(&f.atomic.syncOffset, syncOffset, offset) {
   131  			return
   132  		}
   133  	}
   134  }
   135  
   136  func (f *syncingFile) Sync() error {
   137  	// We update syncOffset (atomically) in order to avoid spurious syncs in
   138  	// maybeSync. Note that even if syncOffset is larger than the current file
   139  	// offset, we still need to call the underlying file's sync for persistence
   140  	// guarantees (which are not provided by sync_file_range).
   141  	f.ratchetSyncOffset(atomic.LoadInt64(&f.atomic.offset))
   142  	return f.syncData()
   143  }
   144  
   145  func (f *syncingFile) maybeSync() error {
   146  	if f.bytesPerSync <= 0 {
   147  		return nil
   148  	}
   149  
   150  	const syncRangeBuffer = 1 << 20 // 1 MB
   151  	offset := atomic.LoadInt64(&f.atomic.offset)
   152  	if offset <= syncRangeBuffer {
   153  		return nil
   154  	}
   155  
   156  	const syncRangeAlignment = 4 << 10 // 4 KB
   157  	syncToOffset := offset - syncRangeBuffer
   158  	syncToOffset -= syncToOffset % syncRangeAlignment
   159  	syncOffset := atomic.LoadInt64(&f.atomic.syncOffset)
   160  	if syncToOffset < 0 || (syncToOffset-syncOffset) < f.bytesPerSync {
   161  		return nil
   162  	}
   163  
   164  	if f.fd == 0 {
   165  		return errors.WithStack(f.Sync())
   166  	}
   167  
   168  	// Note that syncTo will always be called with an offset < atomic.offset. The
   169  	// syncTo implementation may choose to sync the entire file (i.e. on OSes
   170  	// which do not support syncing a portion of the file). The syncTo
   171  	// implementation must call ratchetSyncOffset with as much of the file as it
   172  	// has synced.
   173  	return errors.WithStack(f.syncTo(syncToOffset))
   174  }
   175  
   176  func (f *syncingFile) Close() error {
   177  	// Sync any data that has been written but not yet synced. Note that if
   178  	// SyncFileRange was used, atomic.syncOffset will be less than
   179  	// atomic.offset. See syncingFile.syncToRange.
   180  	if atomic.LoadInt64(&f.atomic.offset) > atomic.LoadInt64(&f.atomic.syncOffset) {
   181  		if err := f.Sync(); err != nil {
   182  			return errors.WithStack(err)
   183  		}
   184  	}
   185  	return errors.WithStack(f.File.Close())
   186  }