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 }