github.com/thanos-io/thanos@v0.32.5/pkg/runutil/runutil.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  // Package runutil provides helpers to advanced function scheduling control like repeat or retry.
     5  //
     6  // It's very often the case when you need to excutes some code every fixed intervals or have it retried automatically.
     7  // To make it reliably with proper timeout, you need to carefully arrange some boilerplate for this.
     8  // Below function does it for you.
     9  //
    10  // For repeat executes, use Repeat:
    11  //
    12  //	err := runutil.Repeat(10*time.Second, stopc, func() error {
    13  //		// ...
    14  //	})
    15  //
    16  // Retry starts executing closure function f until no error is returned from f:
    17  //
    18  //	err := runutil.Retry(10*time.Second, stopc, func() error {
    19  //		// ...
    20  //	})
    21  //
    22  // For logging an error on each f error, use RetryWithLog:
    23  //
    24  //	err := runutil.RetryWithLog(logger, 10*time.Second, stopc, func() error {
    25  //		// ...
    26  //	})
    27  //
    28  // Another use case for runutil package is when you want to close a `Closer` interface. As we all know, we should close all implements of `Closer`, such as *os.File. Commonly we will use:
    29  //
    30  //	defer closer.Close()
    31  //
    32  // The problem is that Close() usually can return important error e.g for os.File the actual file flush might happen (and fail) on `Close` method. It's important to *always* check error. Thanos provides utility functions to log every error like those, allowing to put them in convenient `defer`:
    33  //
    34  //	defer runutil.CloseWithLogOnErr(logger, closer, "log format message")
    35  //
    36  // For capturing error, use CloseWithErrCapture:
    37  //
    38  //	var err error
    39  //	defer runutil.CloseWithErrCapture(&err, closer, "log format message")
    40  //
    41  //	// ...
    42  //
    43  // If Close() returns error, err will capture it and return by argument.
    44  //
    45  // The rununtil.Exhaust* family of functions provide the same functionality but
    46  // they take an io.ReadCloser and they exhaust the whole reader before closing
    47  // them. They are useful when trying to use http keep-alive connections because
    48  // for the same connection to be re-used the whole response body needs to be
    49  // exhausted.
    50  package runutil
    51  
    52  import (
    53  	"fmt"
    54  	"io"
    55  	"os"
    56  	"path/filepath"
    57  	"strings"
    58  	"time"
    59  
    60  	"github.com/go-kit/log"
    61  	"github.com/go-kit/log/level"
    62  	"github.com/pkg/errors"
    63  
    64  	"github.com/thanos-io/thanos/pkg/errutil"
    65  )
    66  
    67  // Repeat executes f every interval seconds until stopc is closed or f returns an error.
    68  // It executes f once right after being called.
    69  func Repeat(interval time.Duration, stopc <-chan struct{}, f func() error) error {
    70  	tick := time.NewTicker(interval)
    71  	defer tick.Stop()
    72  
    73  	for {
    74  		if err := f(); err != nil {
    75  			return err
    76  		}
    77  		select {
    78  		case <-stopc:
    79  			return nil
    80  		case <-tick.C:
    81  		}
    82  	}
    83  }
    84  
    85  // Retry executes f every interval seconds until timeout or no error is returned from f.
    86  func Retry(interval time.Duration, stopc <-chan struct{}, f func() error) error {
    87  	return RetryWithLog(log.NewNopLogger(), interval, stopc, f)
    88  }
    89  
    90  // RetryWithLog executes f every interval seconds until timeout or no error is returned from f. It logs an error on each f error.
    91  func RetryWithLog(logger log.Logger, interval time.Duration, stopc <-chan struct{}, f func() error) error {
    92  	tick := time.NewTicker(interval)
    93  	defer tick.Stop()
    94  
    95  	var err error
    96  	for {
    97  		if err = f(); err == nil {
    98  			return nil
    99  		}
   100  		level.Error(logger).Log("msg", "function failed. Retrying in next tick", "err", err)
   101  		select {
   102  		case <-stopc:
   103  			return err
   104  		case <-tick.C:
   105  		}
   106  	}
   107  }
   108  
   109  // CloseWithLogOnErr is making sure we log every error, even those from best effort tiny closers.
   110  func CloseWithLogOnErr(logger log.Logger, closer io.Closer, format string, a ...interface{}) {
   111  	err := closer.Close()
   112  	if err == nil {
   113  		return
   114  	}
   115  
   116  	// Not a problem if it has been closed already.
   117  	if errors.Is(err, os.ErrClosed) {
   118  		return
   119  	}
   120  
   121  	if logger == nil {
   122  		logger = log.NewLogfmtLogger(os.Stderr)
   123  	}
   124  
   125  	level.Warn(logger).Log("msg", "detected close error", "err", errors.Wrap(err, fmt.Sprintf(format, a...)))
   126  }
   127  
   128  // ExhaustCloseWithLogOnErr closes the io.ReadCloser with a log message on error but exhausts the reader before.
   129  func ExhaustCloseWithLogOnErr(logger log.Logger, r io.ReadCloser, format string, a ...interface{}) {
   130  	_, err := io.Copy(io.Discard, r)
   131  	if err != nil {
   132  		level.Warn(logger).Log("msg", "failed to exhaust reader, performance may be impeded", "err", err)
   133  	}
   134  
   135  	CloseWithLogOnErr(logger, r, format, a...)
   136  }
   137  
   138  // CloseWithErrCapture runs function and on error return error by argument including the given error (usually
   139  // from caller function).
   140  func CloseWithErrCapture(err *error, closer io.Closer, format string, a ...interface{}) {
   141  	merr := errutil.MultiError{}
   142  
   143  	merr.Add(*err)
   144  	merr.Add(errors.Wrapf(closer.Close(), format, a...))
   145  
   146  	*err = merr.Err()
   147  }
   148  
   149  // ExhaustCloseWithErrCapture closes the io.ReadCloser with error capture but exhausts the reader before.
   150  func ExhaustCloseWithErrCapture(err *error, r io.ReadCloser, format string, a ...interface{}) {
   151  	_, copyErr := io.Copy(io.Discard, r)
   152  
   153  	CloseWithErrCapture(err, r, format, a...)
   154  
   155  	// Prepend the io.Copy error.
   156  	merr := errutil.MultiError{}
   157  	merr.Add(copyErr)
   158  	merr.Add(*err)
   159  
   160  	*err = merr.Err()
   161  }
   162  
   163  // DeleteAll deletes all files and directories inside the given
   164  // dir except for the ignoreDirs directories.
   165  // NOTE: DeleteAll is not idempotent.
   166  func DeleteAll(dir string, ignoreDirs ...string) error {
   167  	entries, err := os.ReadDir(dir)
   168  	if os.IsNotExist(err) {
   169  		return nil
   170  	}
   171  	if err != nil {
   172  		return errors.Wrap(err, "read dir")
   173  	}
   174  	var groupErrs errutil.MultiError
   175  
   176  	var matchingIgnores []string
   177  	for _, d := range entries {
   178  		if !d.IsDir() {
   179  			if err := os.RemoveAll(filepath.Join(dir, d.Name())); err != nil {
   180  				groupErrs.Add(err)
   181  			}
   182  			continue
   183  		}
   184  
   185  		// ignoreDirs might be multi-directory paths.
   186  		matchingIgnores = matchingIgnores[:0]
   187  		ignore := false
   188  		for _, ignoreDir := range ignoreDirs {
   189  			id := strings.Split(ignoreDir, "/")
   190  			if id[0] == d.Name() {
   191  				if len(id) == 1 {
   192  					ignore = true
   193  					break
   194  				}
   195  				matchingIgnores = append(matchingIgnores, filepath.Join(id[1:]...))
   196  			}
   197  		}
   198  
   199  		if ignore {
   200  			continue
   201  		}
   202  
   203  		if len(matchingIgnores) == 0 {
   204  			if err := os.RemoveAll(filepath.Join(dir, d.Name())); err != nil {
   205  				groupErrs.Add(err)
   206  			}
   207  			continue
   208  		}
   209  		if err := DeleteAll(filepath.Join(dir, d.Name()), matchingIgnores...); err != nil {
   210  			groupErrs.Add(err)
   211  		}
   212  	}
   213  
   214  	if groupErrs.Err() != nil {
   215  		return errors.Wrap(groupErrs.Err(), "delete file/dir")
   216  	}
   217  	return nil
   218  }