diff --git a/wait/all.go b/wait/all.go index 468a5a1932..1958360d9a 100644 --- a/wait/all.go +++ b/wait/all.go @@ -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 } diff --git a/wait/all_test.go b/wait/all_test.go new file mode 100644 index 0000000000..a1f40f5d67 --- /dev/null +++ b/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) + } + }) + } +} diff --git a/wait/exec.go b/wait/exec.go index c034b3c166..21fed88cb4 100644 --- a/wait/exec.go +++ b/wait/exec.go @@ -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 @@ -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(), @@ -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 } @@ -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 { diff --git a/wait/exit.go b/wait/exit.go index eace6702ac..39536a587f 100644 --- a/wait/exit.go +++ b/wait/exit.go @@ -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 @@ -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 @@ -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 { diff --git a/wait/health.go b/wait/health.go index 73885c2e81..fd55849bbe 100644 --- a/wait/health.go +++ b/wait/health.go @@ -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 @@ -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(), } } @@ -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 } @@ -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 { diff --git a/wait/host_port.go b/wait/host_port.go index 53de3405ba..ca33908708 100644 --- a/wait/host_port.go +++ b/wait/host_port.go @@ -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(), } } @@ -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 } @@ -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 { diff --git a/wait/http.go b/wait/http.go index 9315991c51..65cf11d2be 100644 --- a/wait/http.go +++ b/wait/http.go @@ -21,7 +21,7 @@ var _ Strategy = (*HTTPStrategy)(nil) type HTTPStrategy struct { // all Strategies should have a startupTimeout to avoid waiting infinitely - startupTimeout time.Duration + timeout *time.Duration // additional properties Port nat.Port @@ -39,7 +39,6 @@ type HTTPStrategy struct { // NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200 func NewHTTPStrategy(path string) *HTTPStrategy { return &HTTPStrategy{ - startupTimeout: defaultStartupTimeout(), Port: "80/tcp", Path: path, StatusCodeMatcher: defaultStatusCodeMatcher, @@ -60,8 +59,9 @@ func defaultStatusCodeMatcher(status int) bool { // since go has neither covariance nor generics, the return type must be the type of the concrete implementation // this is true for all properties, even the "shared" ones like startupTimeout -func (ws *HTTPStrategy) WithStartupTimeout(startupTimeout time.Duration) *HTTPStrategy { - ws.startupTimeout = startupTimeout +// WithStartupTimeout can be used to change the default startup timeout +func (ws *HTTPStrategy) WithStartupTimeout(timeout time.Duration) *HTTPStrategy { + ws.timeout = &timeout return ws } @@ -115,11 +115,19 @@ func ForHTTP(path string) *HTTPStrategy { return NewHTTPStrategy(path) } +func (ws *HTTPStrategy) Timeout() *time.Duration { + return ws.timeout +} + // WaitUntilReady implements Strategy.WaitUntilReady func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - // limit context to startupTimeout - 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() ipAddress, err := target.Host(ctx) if err != nil { diff --git a/wait/log.go b/wait/log.go index d16b3d1b82..e6f76ec8c1 100644 --- a/wait/log.go +++ b/wait/log.go @@ -13,7 +13,7 @@ var _ Strategy = (*LogStrategy)(nil) // LogStrategy will wait until a given log entry shows up in the docker logs type LogStrategy struct { // all Strategies should have a startupTimeout to avoid waiting infinitely - startupTimeout time.Duration + timeout *time.Duration // additional properties Log string @@ -24,12 +24,10 @@ type LogStrategy struct { // NewLogStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default func NewLogStrategy(log string) *LogStrategy { return &LogStrategy{ - startupTimeout: defaultStartupTimeout(), - Log: log, - Occurrence: 1, - PollInterval: defaultPollInterval(), + Log: log, + Occurrence: 1, + PollInterval: defaultPollInterval(), } - } // fluent builders for each property @@ -37,8 +35,8 @@ func NewLogStrategy(log string) *LogStrategy { // this is true for all properties, even the "shared" ones like startupTimeout // WithStartupTimeout can be used to change the default startup timeout -func (ws *LogStrategy) WithStartupTimeout(startupTimeout time.Duration) *LogStrategy { - ws.startupTimeout = startupTimeout +func (ws *LogStrategy) WithStartupTimeout(timeout time.Duration) *LogStrategy { + ws.timeout = &timeout return ws } @@ -67,11 +65,19 @@ func ForLog(log string) *LogStrategy { return NewLogStrategy(log) } +func (ws *LogStrategy) Timeout() *time.Duration { + return ws.timeout +} + // WaitUntilReady implements Strategy.WaitUntilReady func (ws *LogStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - // limit context to startupTimeout - 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() LOOP: for { diff --git a/wait/nop.go b/wait/nop.go new file mode 100644 index 0000000000..0c12430753 --- /dev/null +++ b/wait/nop.go @@ -0,0 +1,65 @@ +package wait + +import ( + "context" + "io" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/go-connections/nat" +) + +type NopStrategy struct { + timeout *time.Duration + waitUntilReady func(context.Context, StrategyTarget) error +} + +func ForNop( + waitUntilReady func(context.Context, StrategyTarget) error, +) *NopStrategy { + return &NopStrategy{ + waitUntilReady: waitUntilReady, + } +} + +func (ws *NopStrategy) Timeout() *time.Duration { + return ws.timeout +} + +func (ws *NopStrategy) WithStartupTimeout(timeout time.Duration) *NopStrategy { + ws.timeout = &timeout + return ws +} + +func (ws *NopStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + return ws.waitUntilReady(ctx, target) +} + +type NopStrategyTarget struct { + ReaderCloser io.ReadCloser + ContainerState types.ContainerState +} + +func (st NopStrategyTarget) Host(_ context.Context) (string, error) { + return "", nil +} + +func (st NopStrategyTarget) Ports(_ context.Context) (nat.PortMap, error) { + return nil, nil +} + +func (st NopStrategyTarget) MappedPort(_ context.Context, n nat.Port) (nat.Port, error) { + return n, nil +} + +func (st NopStrategyTarget) Logs(_ context.Context) (io.ReadCloser, error) { + return st.ReaderCloser, nil +} + +func (st NopStrategyTarget) Exec(_ context.Context, cmd []string) (int, io.Reader, error) { + return 0, nil, nil +} + +func (st NopStrategyTarget) State(_ context.Context) (*types.ContainerState, error) { + return &st.ContainerState, nil +} diff --git a/wait/sql.go b/wait/sql.go index 4ef6da25ea..538a831e79 100644 --- a/wait/sql.go +++ b/wait/sql.go @@ -24,6 +24,8 @@ func ForSQL(port nat.Port, driver string, url func(host string, port nat.Port) s } type waitForSql struct { + timeout *time.Duration + URL func(host string, port nat.Port) string Driver string Port nat.Port @@ -32,15 +34,9 @@ type waitForSql struct { query string } -//Timeout sets the maximum waiting time for the strategy after which it'll give up and return an error -// Deprecated: Use WithStartupTimeout -func (w *waitForSql) Timeout(duration time.Duration) *waitForSql { - return w.WithStartupTimeout(duration) -} - // WithStartupTimeout can be used to change the default startup timeout -func (w *waitForSql) WithStartupTimeout(startupTimeout time.Duration) *waitForSql { - w.startupTimeout = startupTimeout +func (w *waitForSql) WithStartupTimeout(timeout time.Duration) *waitForSql { + w.timeout = &timeout return w } @@ -56,11 +52,20 @@ func (w *waitForSql) WithQuery(query string) *waitForSql { return w } -//WaitUntilReady repeatedly tries to run "SELECT 1" or user defined query on the given port using sql and driver. +func (w *waitForSql) Timeout() *time.Duration { + return w.timeout +} + +// WaitUntilReady repeatedly tries to run "SELECT 1" or user defined query on the given port using sql and driver. // // If it doesn't succeed until the timeout value which defaults to 60 seconds, it will return an error. func (w *waitForSql) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - ctx, cancel := context.WithTimeout(ctx, w.startupTimeout) + timeout := defaultStartupTimeout() + if w.timeout != nil { + timeout = *w.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() host, err := target.Host(ctx) diff --git a/wait/wait.go b/wait/wait.go index fedee44d5f..eac622a661 100644 --- a/wait/wait.go +++ b/wait/wait.go @@ -11,6 +11,7 @@ import ( type Strategy interface { WaitUntilReady(context.Context, StrategyTarget) error + Timeout() *time.Duration } type StrategyTarget interface {