Skip to content

Commit

Permalink
script: Add '*' prefix for retrying commands
Browse files Browse the repository at this point in the history
This adds a generic facility for retrying failing commands.

A command that is prefixed with '*' and fails will cause the whole
section (delimited by '#' comments) to be retried from the top.

Retrying is repeated until the command succeeds or the context is
cancelled. The retry interval can be set in (*Engine).RetryInterval.

Example usage:

  # Verify table contents
  db show -o=out.table my-table
  * cmp out.table expected.table

Signed-off-by: Jussi Maki <[email protected]>
  • Loading branch information
joamaki committed Oct 25, 2024
1 parent bb8f3c0 commit 424a8cb
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 21 deletions.
65 changes: 48 additions & 17 deletions script/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,18 @@ type Engine struct {
// If Quiet is true, Execute deletes log prints from the previous
// section when starting a new section.
Quiet bool

// RetryInterval for retrying commands marked with '*'. If zero, then
// retries are disabled.
RetryInterval time.Duration
}

// NewEngine returns an Engine configured with a basic set of commands and conditions.
func NewEngine() *Engine {
return &Engine{
Cmds: DefaultCmds(),
Conds: DefaultConds(),
Cmds: DefaultCmds(),
Conds: DefaultConds(),
RetryInterval: 100 * time.Millisecond,
}
}

Expand Down Expand Up @@ -167,7 +172,10 @@ func (e *Engine) Execute(s *State, file string, script *bufio.Reader, log io.Wri
defer func(prev io.Writer) { s.logOut = prev }(s.logOut)
s.logOut = log

var sectionStart time.Time
var (
sectionStart time.Time
sectionCmds []*command
)
// endSection flushes the logs for the current section from s.log to log.
// ok indicates whether all commands in the section succeeded.
endSection := func(ok bool) error {
Expand All @@ -193,6 +201,7 @@ func (e *Engine) Execute(s *State, file string, script *bufio.Reader, log io.Wri
}

sectionStart = time.Time{}
sectionCmds = nil
return err
}

Expand Down Expand Up @@ -257,6 +266,8 @@ func (e *Engine) Execute(s *State, file string, script *bufio.Reader, log io.Wri
if cmd == nil && err == nil {
continue // Ignore blank lines.
}
sectionCmds = append(sectionCmds, cmd)

s.Logf("> %s\n", line)
if err != nil {
return lineErr(err)
Expand Down Expand Up @@ -296,16 +307,34 @@ func (e *Engine) Execute(s *State, file string, script *bufio.Reader, log io.Wri
// Run the command.
err = e.runCommand(s, cmd, impl)
if err != nil {
if stop := (stopError{}); errors.As(err, &stop) {
// Since the 'stop' command halts execution of the entire script,
// log its message separately from the section in which it appears.
err = endSection(true)
s.Logf("%v\n", stop)
if err == nil {
return nil
if cmd.want == successRetryOnFailure && e.RetryInterval > 0 {
// Command wants retries. Retry the whole section
for err != nil {
select {
case <-s.Context().Done():
err = lineErr(s.Context().Err())
break
case <-time.After(e.RetryInterval):
}
for _, cmd := range sectionCmds {
impl := e.Cmds[cmd.name]
if err = e.runCommand(s, cmd, impl); err != nil {
break
}
}
}
} else {
if stop := (stopError{}); errors.As(err, &stop) {
// Since the 'stop' command halts execution of the entire script,
// log its message separately from the section in which it appears.
err = endSection(true)
s.Logf("%v\n", stop)
if err == nil {
return nil
}
}
return lineErr(err)
}
return lineErr(err)
}
}

Expand Down Expand Up @@ -394,9 +423,10 @@ type command struct {
type expectedStatus string

const (
success expectedStatus = ""
failure expectedStatus = "!"
successOrFailure expectedStatus = "?"
success expectedStatus = ""
failure expectedStatus = "!"
successOrFailure expectedStatus = "?"
successRetryOnFailure expectedStatus = "*"
)

type argFragment struct {
Expand Down Expand Up @@ -437,10 +467,11 @@ func parse(filename string, lineno int, line string) (cmd *command, err error) {
// Command prefix ! means negate the expectations about this command:
// go command should fail, match should not be found, etc.
// Prefix ? means allow either success or failure.
// Prefix * means to retry the command a few times.
switch want := expectedStatus(arg); want {
case failure, successOrFailure:
case failure, successOrFailure, successRetryOnFailure:
if cmd.want != "" {
return errors.New("duplicated '!' or '?' token")
return errors.New("duplicated '!', '?' or '*' token")
}
cmd.want = want
return nil
Expand Down Expand Up @@ -695,7 +726,7 @@ func checkStatus(cmd *command, err error) error {
return cmdError(cmd, err)
}

if cmd.want == success {
if cmd.want == success || cmd.want == successRetryOnFailure {
return cmdError(cmd, err)
}

Expand Down
8 changes: 5 additions & 3 deletions script/scripttest/scripttest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"os"
"testing"
"time"

"github.com/cilium/hive/script"
"github.com/cilium/hive/script/scripttest"
Expand All @@ -16,9 +17,10 @@ import (
func TestAll(t *testing.T) {
ctx := context.Background()
engine := &script.Engine{
Conds: scripttest.DefaultConds(),
Cmds: scripttest.DefaultCmds(),
Quiet: !testing.Verbose(),
Conds: scripttest.DefaultConds(),
Cmds: scripttest.DefaultCmds(),
Quiet: !testing.Verbose(),
RetryInterval: 10 * time.Millisecond,
}
env := os.Environ()
scripttest.Test(t, ctx, func(t testing.TB) *script.Engine { return engine }, env, "testdata/*.txt")
Expand Down
6 changes: 6 additions & 0 deletions script/scripttest/testdata/basic.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@ cat hello.txt
stdout 'hello world'
! stderr 'hello world'

exec sh -c 'sleep 0.1 && echo > out.txt' &

# Retry section test
echo hello
* exec cat out.txt

-- hello.txt --
hello world
1 change: 0 additions & 1 deletion script/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ type State struct {

ctx context.Context
cancel context.CancelFunc
file string
log bytes.Buffer
logOut io.Writer

Expand Down

0 comments on commit 424a8cb

Please sign in to comment.