Skip to content

Commit

Permalink
feat: implement new MultiStrategy design
Browse files Browse the repository at this point in the history
design: #559

- add: Strategy#Timeout
- add: MultiStrategy#WithDeadline
- add: NopStrategy & NopStrategyTarget
- deprecate: MultiStrategy#WithStartupTimeout
  • Loading branch information
hhsnopek committed Nov 8, 2022
1 parent 5ff899f commit a469e91
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 68 deletions.
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
}

// WithStartupTimeoutDefault sets the default timeout for all inner 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
}{
{
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 {
func (ws *ExitStrategy) WithPollInterval(pollInterval time.Duration) *ExitStrategy {
ws.PollInterval = pollInterval
return ws
Expand All @@ -52,13 +52,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
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 @@ -52,11 +51,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
26 changes: 17 additions & 9 deletions wait/host_port.go
Expand Up @@ -20,16 +20,15 @@ type HostPortStrategy struct {
// which
Port nat.Port
// all WaitStrategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
PollInterval time.Duration
timeout *time.Duration
PollInterval time.Duration
}

// NewHostPortStrategy constructs a default host port strategy
func NewHostPortStrategy(port nat.Port) *HostPortStrategy {
return &HostPortStrategy{
Port: port,
startupTimeout: defaultStartupTimeout(),
PollInterval: defaultPollInterval(),
Port: port,
PollInterval: defaultPollInterval(),
}
}

Expand All @@ -49,8 +48,9 @@ func ForExposedPort() *HostPortStrategy {
return NewHostPortStrategy("")
}

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

Expand All @@ -60,11 +60,19 @@ func (hp *HostPortStrategy) WithPollInterval(pollInterval time.Duration) *HostPo
return hp
}

func (hp *HostPortStrategy) Timeout() *time.Duration {
return hp.timeout
}

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

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

ipAddress, err := target.Host(ctx)
if err != nil {
Expand Down

0 comments on commit a469e91

Please sign in to comment.