Skip to content

Commit

Permalink
feat: implement new MultiStrategy design (#580)
Browse files Browse the repository at this point in the history
* feat: implement new MultiStrategy design

design: #559

- add: Strategy#Timeout
- add: MultiStrategy#WithDeadline
- add: NopStrategy & NopStrategyTarget
- deprecate: MultiStrategy#WithStartupTimeout

* docs: update Multi Wait Strategy

* docs: fix WithStartupTimeout references

* review: fix WithPollInterval godoc

* review: add wait/all_test.go error tests

* refactor: simplify wait/all_test.go

* review: remove noopStrategyTarget for NopStrategyTarget

* fix: resolve conflicts for NopStrategy

* review: move Strategy#Timeout to StrategyTimeout interface

Co-authored-by: Manuel de la Peña <[email protected]>
  • Loading branch information
hhsnopek and mdelapenya authored Nov 21, 2022
1 parent eb22bbd commit 9ad2a50
Show file tree
Hide file tree
Showing 14 changed files with 374 additions and 117 deletions.
17 changes: 11 additions & 6 deletions docs/features/wait/multi.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Multi Wait strategy

The Multi wait strategy will hold a list of wait strategies, in order to wait for all of them. It's possible to set the following conditions:
The Multi wait strategy holds a list of wait strategies. The execution of each strategy is first added, first executed.

- the startup timeout to be used in seconds, default is 60 seconds.
Available Options:

- `WithDeadline` - the deadline for when all strategies must complete by, default is none.
- `WithStartupTimeoutDefault` - the startup timeout default to be used for each Strategy if not defined in seconds, default is 60 seconds.

```golang
req := ContainerRequest{
Expand All @@ -12,9 +15,11 @@ req := ContainerRequest{
"MYSQL_ROOT_PASSWORD": "password",
"MYSQL_DATABASE": "database",
},
WaitingFor: wait.ForAll(
wait.ForLog("port: 3306 MySQL Community Server - GPL"),
wait.ForListeningPort("3306/tcp"),
).WithStartupTimeout(10*time.Second),
wait.ForAll(
wait.ForLog("port: 3306 MySQL Community Server - GPL"), // Timeout: 120s (from ForAll.WithStartupTimeoutDefault)
wait.ForExposedPort().WithStartupTimeout(180*time.Second), // Timeout: 180s
wait.ForListeningPort("3306/tcp").WithStartupTimeout(10*time.Second), // Timeout: 10s
).WithStartupTimeoutDefault(120*time.Second). // Applies default StartupTimeout when not explictly defined
WithDeadline(360*time.Second) // Applies deadline for all Wait Strategies
}
```
6 changes: 3 additions & 3 deletions e2e/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestContainerWithWaitForSQL(t *testing.T) {
Cmd: []string{"postgres", "-c", "fsync=off"},
Env: env,
WaitingFor: wait.ForSQL(nat.Port(port), "postgres", dbURL).
Timeout(time.Second * 5),
WithStartupTimeout(time.Second * 5),
}
container, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: req,
Expand All @@ -55,7 +55,7 @@ func TestContainerWithWaitForSQL(t *testing.T) {
Cmd: []string{"postgres", "-c", "fsync=off"},
Env: env,
WaitingFor: wait.ForSQL(nat.Port(port), "postgres", dbURL).
Timeout(time.Second * 5).
WithStartupTimeout(time.Second * 5).
WithQuery("SELECT 10"),
}
container, err := GenericContainer(ctx, GenericContainerRequest{
Expand All @@ -75,7 +75,7 @@ func TestContainerWithWaitForSQL(t *testing.T) {
Cmd: []string{"postgres", "-c", "fsync=off"},
Env: env,
WaitingFor: wait.ForSQL(nat.Port(port), "postgres", dbURL).
Timeout(time.Second * 5).
WithStartupTimeout(time.Second * 5).
WithQuery("SELECT 'a' from b"),
}
container, err := GenericContainer(ctx, GenericContainerRequest{
Expand Down
51 changes: 42 additions & 9 deletions wait/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,73 @@ import (

// Implement interface
var _ Strategy = (*MultiStrategy)(nil)
var _ StrategyTimeout = (*MultiStrategy)(nil)

type MultiStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
timeout *time.Duration
deadline *time.Duration

// additional properties
Strategies []Strategy
}

func (ms *MultiStrategy) WithStartupTimeout(startupTimeout time.Duration) *MultiStrategy {
ms.startupTimeout = startupTimeout
// WithStartupTimeoutDefault sets the default timeout for all inner wait strategies
func (ms *MultiStrategy) WithStartupTimeoutDefault(timeout time.Duration) *MultiStrategy {
ms.timeout = &timeout
return ms
}

// WithStartupTimeout sets a time.Duration which limits all wait strategies
//
// Deprecated: use WithDeadline
func (ms *MultiStrategy) WithStartupTimeout(timeout time.Duration) Strategy {
return ms.WithDeadline(timeout)
}

// WithDeadline sets a time.Duration which limits all wait strategies
func (ms *MultiStrategy) WithDeadline(deadline time.Duration) *MultiStrategy {
ms.deadline = &deadline
return ms
}

func ForAll(strategies ...Strategy) *MultiStrategy {
return &MultiStrategy{
startupTimeout: defaultStartupTimeout(),
Strategies: strategies,
Strategies: strategies,
}
}

func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
ctx, cancelContext := context.WithTimeout(ctx, ms.startupTimeout)
defer cancelContext()
func (ms *MultiStrategy) Timeout() *time.Duration {
return ms.timeout
}

func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
var cancel context.CancelFunc
if ms.deadline != nil {
ctx, cancel = context.WithTimeout(ctx, *ms.deadline)
defer cancel()
}

if len(ms.Strategies) == 0 {
return fmt.Errorf("no wait strategy supplied")
}

for _, strategy := range ms.Strategies {
err := strategy.WaitUntilReady(ctx, target)
strategyCtx := ctx

// Set default Timeout when strategy implements StrategyTimeout
if st, ok := strategy.(StrategyTimeout); ok {
if ms.Timeout() != nil && st.Timeout() == nil {
strategyCtx, cancel = context.WithTimeout(ctx, *ms.Timeout())
defer cancel()
}
}

err := strategy.WaitUntilReady(strategyCtx, target)
if err != nil {
return err
}
}

return nil
}
121 changes: 121 additions & 0 deletions wait/all_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package wait

import (
"bytes"
"context"
"errors"
"io"
"testing"
"time"
)

func TestMultiStrategy_WaitUntilReady(t *testing.T) {
t.Parallel()
type args struct {
ctx context.Context
target StrategyTarget
}
tests := []struct {
name string
strategy Strategy
args args
wantErr bool
}{
{
name: "returns error when no WaitStrategies are passed",
strategy: ForAll(),
args: args{
ctx: context.Background(),
target: NopStrategyTarget{},
},
wantErr: true,
},
{
name: "returns WaitStrategy error",
strategy: ForAll(
ForNop(
func(ctx context.Context, target StrategyTarget) error {
return errors.New("intentional failure")
},
),
),
args: args{
ctx: context.Background(),
target: NopStrategyTarget{},
},
wantErr: true,
},
{
name: "WithDeadline sets context Deadline for WaitStrategy",
strategy: ForAll(
ForNop(
func(ctx context.Context, target StrategyTarget) error {
if _, set := ctx.Deadline(); !set {
return errors.New("expected context.Deadline to be set")
}
return nil
},
),
ForLog("docker"),
).WithDeadline(1 * time.Second),
args: args{
ctx: context.Background(),
target: NopStrategyTarget{
ReaderCloser: io.NopCloser(bytes.NewReader([]byte("docker"))),
},
},
wantErr: false,
},
{
name: "WithStartupTimeoutDefault skips setting context.Deadline when WaitStrategy.Timeout is defined",
strategy: ForAll(
ForNop(
func(ctx context.Context, target StrategyTarget) error {
if _, set := ctx.Deadline(); set {
return errors.New("expected context.Deadline not to be set")
}
return nil
},
).WithStartupTimeout(2*time.Second),
ForLog("docker"),
).WithStartupTimeoutDefault(1 * time.Second),
args: args{
ctx: context.Background(),
target: NopStrategyTarget{
ReaderCloser: io.NopCloser(bytes.NewReader([]byte("docker"))),
},
},
wantErr: false,
},
{
name: "WithStartupTimeoutDefault sets context.Deadline for nil WaitStrategy.Timeout",
strategy: ForAll(
ForNop(
func(ctx context.Context, target StrategyTarget) error {
if _, set := ctx.Deadline(); !set {
return errors.New("expected context.Deadline to be set")
}
return nil
},
),
ForLog("docker"),
).WithStartupTimeoutDefault(1 * time.Second),
args: args{
ctx: context.Background(),
target: NopStrategyTarget{
ReaderCloser: io.NopCloser(bytes.NewReader([]byte("docker"))),
},
},
wantErr: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if err := tt.strategy.WaitUntilReady(tt.args.ctx, tt.args.target); (err != nil) != tt.wantErr {
t.Errorf("ForAll.WaitUntilReady() error = %v, wantErr = %v", err, tt.wantErr)
}
})
}
}
25 changes: 17 additions & 8 deletions wait/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import (

// Implement interface
var _ Strategy = (*ExecStrategy)(nil)
var _ StrategyTimeout = (*ExecStrategy)(nil)

type ExecStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
cmd []string
timeout *time.Duration
cmd []string

// additional properties
ExitCodeMatcher func(exitCode int) bool
Expand All @@ -21,7 +22,6 @@ type ExecStrategy struct {
// NewExecStrategy constructs an Exec strategy ...
func NewExecStrategy(cmd []string) *ExecStrategy {
return &ExecStrategy{
startupTimeout: defaultStartupTimeout(),
cmd: cmd,
ExitCodeMatcher: defaultExitCodeMatcher,
PollInterval: defaultPollInterval(),
Expand All @@ -32,8 +32,9 @@ func defaultExitCodeMatcher(exitCode int) bool {
return exitCode == 0
}

// WithStartupTimeout can be used to change the default startup timeout
func (ws *ExecStrategy) WithStartupTimeout(startupTimeout time.Duration) *ExecStrategy {
ws.startupTimeout = startupTimeout
ws.timeout = &startupTimeout
return ws
}

Expand All @@ -53,10 +54,18 @@ func ForExec(cmd []string) *ExecStrategy {
return NewExecStrategy(cmd)
}

func (ws ExecStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
// limit context to startupTimeout
ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout)
defer cancelContext()
func (ws *ExecStrategy) Timeout() *time.Duration {
return ws.timeout
}

func (ws *ExecStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
timeout := defaultStartupTimeout()
if ws.timeout != nil {
timeout = *ws.timeout
}

ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

for {
select {
Expand Down
18 changes: 11 additions & 7 deletions wait/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import (

// Implement interface
var _ Strategy = (*ExitStrategy)(nil)
var _ StrategyTimeout = (*ExitStrategy)(nil)

// ExitStrategy will wait until container exit
type ExitStrategy struct {
// all Strategies should have a timeout to avoid waiting infinitely
exitTimeout time.Duration
timeout *time.Duration

// additional properties
PollInterval time.Duration
Expand All @@ -32,7 +33,7 @@ func NewExitStrategy() *ExitStrategy {

// WithExitTimeout can be used to change the default exit timeout
func (ws *ExitStrategy) WithExitTimeout(exitTimeout time.Duration) *ExitStrategy {
ws.exitTimeout = exitTimeout
ws.timeout = &exitTimeout
return ws
}

Expand All @@ -53,13 +54,16 @@ func ForExit() *ExitStrategy {
return NewExitStrategy()
}

func (ws *ExitStrategy) Timeout() *time.Duration {
return ws.timeout
}

// WaitUntilReady implements Strategy.WaitUntilReady
func (ws *ExitStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
// limit context to exitTimeout
if ws.exitTimeout > 0 {
var cancelContext context.CancelFunc
ctx, cancelContext = context.WithTimeout(ctx, ws.exitTimeout)
defer cancelContext()
if ws.timeout != nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, *ws.timeout)
defer cancel()
}

for {
Expand Down
Loading

0 comments on commit 9ad2a50

Please sign in to comment.