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
43 changes: 35 additions & 8 deletions wait/all.go
Expand Up @@ -11,34 +11,61 @@ var _ Strategy = (*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) Timeout() *time.Duration {
return ms.timeout
}

func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
ctx, cancelContext := context.WithTimeout(ctx, ms.startupTimeout)
defer cancelContext()
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
if ms.Timeout() != nil && strategy.Timeout() == nil {
strategyCtx, cancel = context.WithTimeout(ctx, *ms.Timeout())
defer cancel()
}

err := strategy.WaitUntilReady(strategyCtx, target)
if err != nil {
return err
}
Expand Down
97 changes: 97 additions & 0 deletions wait/all_test.go
@@ -0,0 +1,97 @@
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: "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("unexpected context.Deadline 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)
}
})
}
}
24 changes: 16 additions & 8 deletions wait/exec.go
Expand Up @@ -10,8 +10,8 @@ var _ Strategy = (*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 +21,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 +31,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 +53,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
19 changes: 11 additions & 8 deletions wait/exit.go
Expand Up @@ -12,7 +12,7 @@ var _ Strategy = (*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,11 +32,11 @@ 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
}

// WithPollInterval can be used to override the default polling interval of 100 milliseconds
// WithPollInterval can be used to override the default polling interval of 100 milliseconds func (ws *ExitStrategy) WithPollInterval(pollInterval time.Duration) *ExitStrategy {
hhsnopek marked this conversation as resolved.
Show resolved Hide resolved
func (ws *ExitStrategy) WithPollInterval(pollInterval time.Duration) *ExitStrategy {
ws.PollInterval = pollInterval
return ws
Expand All @@ -53,13 +53,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
21 changes: 14 additions & 7 deletions wait/health.go
Expand Up @@ -11,7 +11,7 @@ var _ Strategy = (*HealthStrategy)(nil)
// HealthStrategy will wait until the container becomes healthy
type HealthStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
timeout *time.Duration

// additional properties
PollInterval time.Duration
Expand All @@ -20,8 +20,7 @@ type HealthStrategy struct {
// NewHealthStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default
func NewHealthStrategy() *HealthStrategy {
return &HealthStrategy{
startupTimeout: defaultStartupTimeout(),
PollInterval: defaultPollInterval(),
PollInterval: defaultPollInterval(),
}

}
Expand All @@ -32,7 +31,7 @@ func NewHealthStrategy() *HealthStrategy {

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

Expand All @@ -53,11 +52,19 @@ func ForHealthCheck() *HealthStrategy {
return NewHealthStrategy()
}

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

// WaitUntilReady implements Strategy.WaitUntilReady
func (ws *HealthStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
// limit context to exitTimeout
ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout)
defer cancelContext()
timeout := defaultStartupTimeout()
if ws.timeout != nil {
timeout = *ws.timeout
}

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

for {
select {
Expand Down