github.com/wtsi-ssg/wrstat@v1.1.4-0.20221008232152-3030622a8cf8/stat/stat.go (about)

     1  /*******************************************************************************
     2   * Copyright (c) 2021 Genome Research Ltd.
     3   *
     4   * Author: Sendu Bala <sb10@sanger.ac.uk>
     5   *
     6   * Permission is hereby granted, free of charge, to any person obtaining
     7   * a copy of this software and associated documentation files (the
     8   * "Software"), to deal in the Software without restriction, including
     9   * without limitation the rights to use, copy, modify, merge, publish,
    10   * distribute, sublicense, and/or sell copies of the Software, and to
    11   * permit persons to whom the Software is furnished to do so, subject to
    12   * the following conditions:
    13   *
    14   * The above copyright notice and this permission notice shall be included
    15   * in all copies or substantial portions of the Software.
    16   *
    17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    18   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    19   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    20   * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    21   * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    22   * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    23   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    24   ******************************************************************************/
    25  
    26  package stat
    27  
    28  import (
    29  	"io/fs"
    30  	"os"
    31  	"time"
    32  
    33  	"github.com/inconshreveable/log15"
    34  )
    35  
    36  type Error string
    37  
    38  func (e Error) Error() string { return string(e) }
    39  
    40  const errLstatSlow = Error("lstat exceeded timeout")
    41  
    42  // Statter is something you use to get stats of files on disk.
    43  type Statter interface {
    44  	// Lstat calls os.Lstat() on the given path, returning the FileInfo.
    45  	Lstat(path string) (info fs.FileInfo, err error)
    46  }
    47  
    48  // StatterWithTimeout is is a Statter implementation. NB: this is NOT thread
    49  // safe; you should only call Lstat() one at a time.
    50  type StatterWithTimeout struct {
    51  	timeout         time.Duration
    52  	maxAttempts     int
    53  	currentAttempts int
    54  	logger          log15.Logger
    55  }
    56  
    57  // WithTimeout returns a Statter with the given timeout and maxAttempts
    58  // configured. Timeouts are logged with the given logger.
    59  func WithTimeout(timeout time.Duration, maxAttempts int, logger log15.Logger) *StatterWithTimeout {
    60  	return &StatterWithTimeout{
    61  		timeout:     timeout,
    62  		maxAttempts: maxAttempts,
    63  		logger:      logger,
    64  	}
    65  }
    66  
    67  // Lstat calls os.Lstat() on the given path, but times it out after our
    68  // configured timeout, retrying until we've hit our maxAttempts. NB: this is NOT
    69  // thread safe, don't call this concurrently.
    70  func (s *StatterWithTimeout) Lstat(path string) (info fs.FileInfo, err error) {
    71  	infoCh := make(chan fs.FileInfo, 1)
    72  	errCh := make(chan error, 1)
    73  	s.currentAttempts++
    74  
    75  	timer := time.NewTimer(s.timeout)
    76  
    77  	go s.doLstat(path, infoCh, errCh)
    78  
    79  	select {
    80  	case err = <-errCh:
    81  		info = <-infoCh
    82  		s.currentAttempts = 0
    83  
    84  		timer.Stop()
    85  
    86  		return
    87  	case <-timer.C:
    88  		if s.currentAttempts <= s.maxAttempts {
    89  			s.logger.Warn("an lstat call exceeded timeout, will retry", "path", path, "attempts", s.currentAttempts)
    90  
    91  			return s.Lstat(path)
    92  		}
    93  
    94  		s.logger.Warn("an lstat call exceeded timeout, giving up", "path", path, "attempts", s.currentAttempts)
    95  
    96  		err = errLstatSlow
    97  		s.currentAttempts = 0
    98  
    99  		return
   100  	}
   101  }
   102  
   103  // doLstat does the actual Lstat call and sends results on the given channels.
   104  func (s *StatterWithTimeout) doLstat(path string, infoCh chan fs.FileInfo, errCh chan error) {
   105  	if os.Getenv("WRSTAT_TEST_LSTAT") != "" {
   106  		<-time.After(1 * time.Millisecond)
   107  	}
   108  
   109  	info, err := os.Lstat(path)
   110  	infoCh <- info
   111  	errCh <- err
   112  }