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  }