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 }