Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement new MultiStrategy design #580

Merged
merged 12 commits into from Nov 21, 2022
17 changes: 11 additions & 6 deletions docs/features/wait/multi.md
@@ -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
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
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
@@ -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
}{
{
hhsnopek marked this conversation as resolved.
Show resolved Hide resolved
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
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
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 {
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, *ws.timeout)
defer cancel()
}

for {
Expand Down