github.com/zuoyebang/bitalosdb@v1.1.1-0.20240516111551-79a8c4d8ce20/internal/vfs/disk_health_fs.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 "time" 20 ) 21 22 const ( 23 // defaultTickInterval is the default interval between two ticks of each 24 // diskHealthCheckingFile loop iteration. 25 defaultTickInterval = 2 * time.Second 26 ) 27 28 // diskHealthCheckingFile is a File wrapper to detect slow disk operations, and 29 // call onSlowDisk if a disk operation is seen to exceed diskSlowThreshold. 30 // 31 // This struct creates a goroutine (in startTicker()) that, at every tick 32 // interval, sees if there's a disk operation taking longer than the specified 33 // duration. This setup is preferable to creating a new timer at every disk 34 // operation, as it reduces overhead per disk operation. 35 type diskHealthCheckingFile struct { 36 File 37 38 onSlowDisk func(time.Duration) 39 diskSlowThreshold time.Duration 40 tickInterval time.Duration 41 42 stopper chan struct{} 43 lastWriteNanos int64 44 } 45 46 // newDiskHealthCheckingFile instantiates a new diskHealthCheckingFile, with the 47 // specified time threshold and event listener. 48 func newDiskHealthCheckingFile( 49 file File, diskSlowThreshold time.Duration, onSlowDisk func(time.Duration), 50 ) *diskHealthCheckingFile { 51 return &diskHealthCheckingFile{ 52 File: file, 53 onSlowDisk: onSlowDisk, 54 diskSlowThreshold: diskSlowThreshold, 55 tickInterval: defaultTickInterval, 56 57 stopper: make(chan struct{}), 58 } 59 } 60 61 // startTicker starts a new goroutine with a ticker to monitor disk operations. 62 // Can only be called if the ticker goroutine isn't running already. 63 func (d *diskHealthCheckingFile) startTicker() { 64 if d.diskSlowThreshold == 0 { 65 return 66 } 67 68 go func() { 69 ticker := time.NewTicker(d.tickInterval) 70 defer ticker.Stop() 71 72 for { 73 select { 74 case <-d.stopper: 75 return 76 77 case <-ticker.C: 78 lastWriteNanos := atomic.LoadInt64(&d.lastWriteNanos) 79 if lastWriteNanos == 0 { 80 continue 81 } 82 lastWrite := time.Unix(0, lastWriteNanos) 83 now := time.Now() 84 if lastWrite.Add(d.diskSlowThreshold).Before(now) { 85 // diskSlowThreshold was exceeded. Call the passed-in 86 // listener. 87 d.onSlowDisk(now.Sub(lastWrite)) 88 } 89 } 90 } 91 }() 92 } 93 94 // stopTicker stops the goroutine started in startTicker. 95 func (d *diskHealthCheckingFile) stopTicker() { 96 close(d.stopper) 97 } 98 99 // Write implements the io.Writer interface. 100 func (d *diskHealthCheckingFile) Write(p []byte) (n int, err error) { 101 d.timeDiskOp(func() { 102 n, err = d.File.Write(p) 103 }) 104 return n, err 105 } 106 107 // Seek implements the io.Seeker interface. 108 func (d *diskHealthCheckingFile) Seek(offset int64, whence int) (n int64, err error) { 109 d.timeDiskOp(func() { 110 n, err = d.File.Seek(offset, whence) 111 }) 112 return n, err 113 } 114 115 // Close implements the io.Closer interface. 116 func (d *diskHealthCheckingFile) Close() error { 117 d.stopTicker() 118 return d.File.Close() 119 } 120 121 // Sync implements the io.Syncer interface. 122 func (d *diskHealthCheckingFile) Sync() (err error) { 123 d.timeDiskOp(func() { 124 err = d.File.Sync() 125 }) 126 return err 127 } 128 129 // timeDiskOp runs the specified closure and makes its timing visible to the 130 // monitoring goroutine, in case it exceeds one of the slow disk durations. 131 func (d *diskHealthCheckingFile) timeDiskOp(op func()) { 132 if d == nil { 133 op() 134 return 135 } 136 137 atomic.StoreInt64(&d.lastWriteNanos, time.Now().UnixNano()) 138 defer func() { 139 atomic.StoreInt64(&d.lastWriteNanos, 0) 140 }() 141 op() 142 } 143 144 type diskHealthCheckingFS struct { 145 FS 146 147 diskSlowThreshold time.Duration 148 onSlowDisk func(string, time.Duration) 149 } 150 151 // WithDiskHealthChecks wraps an FS and ensures that all 152 // write-oriented created with that FS are wrapped with disk health detection 153 // checks. Disk operations that are observed to take longer than 154 // diskSlowThreshold trigger an onSlowDisk call. 155 func WithDiskHealthChecks( 156 fs FS, diskSlowThreshold time.Duration, onSlowDisk func(string, time.Duration), 157 ) FS { 158 return diskHealthCheckingFS{ 159 FS: fs, 160 diskSlowThreshold: diskSlowThreshold, 161 onSlowDisk: onSlowDisk, 162 } 163 } 164 165 // Create implements the vfs.FS interface. 166 func (d diskHealthCheckingFS) Create(name string) (File, error) { 167 f, err := d.FS.Create(name) 168 if err != nil { 169 return f, err 170 } 171 if d.diskSlowThreshold == 0 { 172 return f, nil 173 } 174 checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(duration time.Duration) { 175 d.onSlowDisk(name, duration) 176 }) 177 checkingFile.startTicker() 178 return WithFd(f, checkingFile), nil 179 } 180 181 // ReuseForWrite implements the vfs.FS interface. 182 func (d diskHealthCheckingFS) ReuseForWrite(oldname, newname string) (File, error) { 183 f, err := d.FS.ReuseForWrite(oldname, newname) 184 if err != nil { 185 return f, err 186 } 187 if d.diskSlowThreshold == 0 { 188 return f, nil 189 } 190 checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(duration time.Duration) { 191 d.onSlowDisk(newname, duration) 192 }) 193 checkingFile.startTicker() 194 return WithFd(f, checkingFile), nil 195 } 196 197 // OpenForWrite implements the vfs.FS interface. 198 func (d diskHealthCheckingFS) OpenForWrite(name string) (File, error) { 199 f, err := d.FS.OpenForWrite(name) 200 if err != nil { 201 return f, err 202 } 203 if d.diskSlowThreshold == 0 { 204 return f, nil 205 } 206 checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(duration time.Duration) { 207 d.onSlowDisk(name, duration) 208 }) 209 checkingFile.startTicker() 210 return WithFd(f, checkingFile), nil 211 }