Initial version
This commit is contained in:
commit
7bd17c2dbd
5 changed files with 200 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
backoff
|
31
README.md
Normal file
31
README.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Luzifer / backoff
|
||||||
|
|
||||||
|
`backoff` is a small CLI util wrapping [`github.com/Luzifer/go_helpers/v2/backoff`](https://pkg.go.dev/github.com/Luzifer/go_helpers/v2@v2.20.0/backoff) to be used in shell scripts
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```console
|
||||||
|
# backoff --help
|
||||||
|
Usage of backoff:
|
||||||
|
--log-level string Log level (debug, info, warn, error, fatal) (default "info")
|
||||||
|
--max-iteration-time duration How long to wait at most between iterations (default 1m0s)
|
||||||
|
-i, --max-iterations uint Maximum number of retries (0 = infinite)
|
||||||
|
-t, --max-total-time duration Deadline for overall executions (0 = infinite)
|
||||||
|
--min-iteration-time duration How long to wait before first retry (default 100ms)
|
||||||
|
--mulitplier float Mulitplier to apply to the wait-time after each retry (1.0 = constant backoff) (default 1.5)
|
||||||
|
--stdin Pass stdin to command, to do so stdin will be fully buffered to memory before starting the command, enabling without input wil hang forever
|
||||||
|
--version Prints current version and exits
|
||||||
|
|
||||||
|
# backoff -i 10 --log-level=debug -- false
|
||||||
|
time="2023-07-22T14:38:28+02:00" level=debug msg="starting execution" try=1
|
||||||
|
time="2023-07-22T14:38:28+02:00" level=debug msg="starting execution" try=2
|
||||||
|
time="2023-07-22T14:38:28+02:00" level=debug msg="starting execution" try=3
|
||||||
|
time="2023-07-22T14:38:28+02:00" level=debug msg="starting execution" try=4
|
||||||
|
time="2023-07-22T14:38:28+02:00" level=debug msg="starting execution" try=5
|
||||||
|
time="2023-07-22T14:38:29+02:00" level=debug msg="starting execution" try=6
|
||||||
|
time="2023-07-22T14:38:30+02:00" level=debug msg="starting execution" try=7
|
||||||
|
time="2023-07-22T14:38:31+02:00" level=debug msg="starting execution" try=8
|
||||||
|
time="2023-07-22T14:38:33+02:00" level=debug msg="starting execution" try=9
|
||||||
|
time="2023-07-22T14:38:35+02:00" level=debug msg="starting execution" try=10
|
||||||
|
time="2023-07-22T14:38:35+02:00" level=fatal msg="retrying command" error="Maximum iterations reached: executing command: exit status 1"
|
||||||
|
```
|
18
go.mod
Normal file
18
go.mod
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module git.luzifer.io/luzifer/backoff
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Luzifer/go_helpers/v2 v2.20.0
|
||||||
|
github.com/Luzifer/rconfig/v2 v2.4.0
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
golang.org/x/sys v0.9.0 // indirect
|
||||||
|
gopkg.in/validator.v2 v2.0.0-20210331031555-b37d688a7fb0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
)
|
37
go.sum
Normal file
37
go.sum
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
github.com/Luzifer/go_helpers/v2 v2.20.0 h1:OyCUs7TFGwfJpGqD21KEKKOXy92jetw2l7dlmG7HZnA=
|
||||||
|
github.com/Luzifer/go_helpers/v2 v2.20.0/go.mod h1:KPGjImwm51SmOTZMd9XUsT241gHYJuEyLrS/omQ4/Dw=
|
||||||
|
github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o=
|
||||||
|
github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||||
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/validator.v2 v2.0.0-20210331031555-b37d688a7fb0 h1:EFLtLCwd8tGN+r/ePz3cvRtdsfYNhDEdt/vp6qsT+0A=
|
||||||
|
gopkg.in/validator.v2 v2.0.0-20210331031555-b37d688a7fb0/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
113
main.go
Normal file
113
main.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/backoff"
|
||||||
|
"github.com/Luzifer/rconfig/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfg = struct {
|
||||||
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||||
|
MaxIterations uint64 `flag:"max-iterations,i" vardefault:"maxIterations" description:"Maximum number of retries (0 = infinite)"`
|
||||||
|
MaxIterationTime time.Duration `flag:"max-iteration-time" vardefault:"maxIterationTime" description:"How long to wait at most between iterations"`
|
||||||
|
MaxTotalTime time.Duration `flag:"max-total-time,t" vardefault:"maxTotalTime" description:"Deadline for overall executions (0 = infinite)"`
|
||||||
|
MinIterationTime time.Duration `flag:"min-iteration-time" vardefault:"minIterationTime" description:"How long to wait before first retry"`
|
||||||
|
Multiplier float64 `flag:"mulitplier" vardefault:"mulitplier" description:"Mulitplier to apply to the wait-time after each retry (1.0 = constant backoff)"`
|
||||||
|
Stdin bool `flag:"stdin" default:"false" description:"Pass stdin to command, to do so stdin will be fully buffered to memory before starting the command, enabling without input wil hang forever"`
|
||||||
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
version = "dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initApp() error {
|
||||||
|
rconfig.SetVariableDefaults(map[string]string{
|
||||||
|
"maxIterations": strconv.FormatUint(backoff.DefaultMaxIterations, 10),
|
||||||
|
"maxIterationTime": backoff.DefaultMaxIterationTime.String(),
|
||||||
|
"maxTotalTime": backoff.DefaultMaxTotalTime.String(),
|
||||||
|
"minIterationTime": backoff.DefaultMinIterationTime.String(),
|
||||||
|
"mulitplier": strconv.FormatFloat(backoff.DefaultMultipler, 'f', -1, 64),
|
||||||
|
})
|
||||||
|
rconfig.AutoEnv(true)
|
||||||
|
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing cli options")
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := logrus.ParseLevel(cfg.LogLevel)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "parsing log-level")
|
||||||
|
}
|
||||||
|
logrus.SetLevel(l)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var err error
|
||||||
|
if err = initApp(); err != nil {
|
||||||
|
logrus.WithError(err).Fatal("initializing app")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.VersionAndExit {
|
||||||
|
logrus.WithField("version", version).Info("backoff")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rconfig.Args()[1:]) < 1 {
|
||||||
|
logrus.Fatal("Usage: backoff [options] -- <command / args>")
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdin io.ReadSeeker
|
||||||
|
if cfg.Stdin {
|
||||||
|
sbuf := new(bytes.Buffer)
|
||||||
|
if _, err = io.Copy(sbuf, os.Stdin); err != nil {
|
||||||
|
logrus.WithError(err).Fatal("reading stdin to buffer")
|
||||||
|
}
|
||||||
|
stdin = bytes.NewReader(sbuf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
bo = backoff.NewBackoff().
|
||||||
|
WithMaxIterations(cfg.MaxIterations).
|
||||||
|
WithMaxIterationTime(cfg.MaxIterationTime).
|
||||||
|
WithMaxTotalTime(cfg.MaxTotalTime).
|
||||||
|
WithMinIterationTime(cfg.MinIterationTime).
|
||||||
|
WithMultiplier(cfg.Multiplier)
|
||||||
|
try int
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = bo.Retry(func() error {
|
||||||
|
try++
|
||||||
|
|
||||||
|
logrus.WithField("try", try).Debug("starting execution")
|
||||||
|
|
||||||
|
if cfg.Stdin {
|
||||||
|
if _, err = stdin.Seek(0, io.SeekStart); err != nil {
|
||||||
|
logrus.WithError(err).Fatal("resetting seek position in stdin buffer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(rconfig.Args()[1], rconfig.Args()[2:]...)
|
||||||
|
cmd.Env = os.Environ() // Env-Passthrough
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if cfg.Stdin {
|
||||||
|
cmd.Stdin = stdin
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrap(cmd.Run(), "executing command")
|
||||||
|
}); err != nil {
|
||||||
|
logrus.WithError(err).Fatal("retrying command")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue