diff --git a/.gitignore b/.gitignore index 317950b..4af65d2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ local-testnet output builder-playground **/.DS_Store -e2e-test/ \ No newline at end of file +e2e-test/ +playground/cache diff --git a/main.go b/main.go index e6e42fe..75d561b 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "context" _ "embed" "fmt" "log" @@ -450,7 +451,9 @@ func runIt(recipe playground.Recipe) error { } fmt.Println("\nWaiting for services to get healthy...") - if err := dockerRunner.WaitForReady(ctx, 20*time.Second); err != nil { + waitCtx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + if err := dockerRunner.WaitForReady(waitCtx); err != nil { dockerRunner.Stop(keepFlag) return fmt.Errorf("failed to wait for service readiness: %w", err) } diff --git a/playground/components.go b/playground/components.go index 7d7c0a6..e6b6b98 100644 --- a/playground/components.go +++ b/playground/components.go @@ -880,7 +880,14 @@ func (b *BuilderHub) Apply(manifest *Manifest) { WithTag("0.3.1-alpha1"). WithPort("http", 8888). WithEnv("TARGET", Connect("builder-hub-api", "http")). - DependsOnHealthy("builder-hub-api") + DependsOnHealthy("builder-hub-api"). + WithReady(ReadyCheck{ + QueryURL: "http://localhost:8888", + Interval: 1 * time.Second, + Timeout: 30 * time.Second, + Retries: 3, + StartPeriod: 1 * time.Second, + }) } func UseHealthmon(m *Manifest, s *Service) { diff --git a/playground/components_test.go b/playground/components_test.go index 62aa7f9..df2b656 100644 --- a/playground/components_test.go +++ b/playground/components_test.go @@ -195,7 +195,9 @@ func (tt *testFramework) test(s ServiceGen, args []string) *Manifest { err = dockerRunner.Run(context.Background()) require.NoError(t, err) - require.NoError(t, dockerRunner.WaitForReady(context.Background(), 20*time.Second)) + waitCtx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + require.NoError(t, dockerRunner.WaitForReady(waitCtx)) return svcManager } diff --git a/playground/local_runner.go b/playground/local_runner.go index 985e0c0..a08400e 100644 --- a/playground/local_runner.go +++ b/playground/local_runner.go @@ -16,7 +16,6 @@ import ( "strings" "sync" "text/template" - "time" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/events" @@ -57,8 +56,9 @@ type LocalRunner struct { exitErr chan error // tasks tracks the status of each service - tasksMtx sync.Mutex - tasks map[string]*task + tasksMtx sync.Mutex + tasks map[string]*task + allTasksReadyCh chan struct{} } type task struct { @@ -155,23 +155,21 @@ func NewLocalRunner(cfg *RunnerConfig) (*LocalRunner, error) { } d := &LocalRunner{ - config: cfg, - out: cfg.Out, - manifest: cfg.Manifest, - client: client, - reservedPorts: map[int]bool{}, - handles: []*exec.Cmd{}, - tasks: tasks, - exitErr: make(chan error, 2), + config: cfg, + out: cfg.Out, + manifest: cfg.Manifest, + client: client, + reservedPorts: map[int]bool{}, + handles: []*exec.Cmd{}, + tasks: tasks, + allTasksReadyCh: make(chan struct{}), + exitErr: make(chan error, 2), } return d, nil } -func (d *LocalRunner) AreReady() bool { - d.tasksMtx.Lock() - defer d.tasksMtx.Unlock() - +func (d *LocalRunner) checkAndUpdateReadiness() { for name, task := range d.tasks { // ensure the task is not a host service if d.isHostService(name) { @@ -180,39 +178,32 @@ func (d *LocalRunner) AreReady() bool { // first ensure the task has started if task.status != TaskStatusStarted { - return false + return } // then ensure it is ready if it has a ready function svc := d.getService(name) if svc.ReadyCheck != nil { if !task.ready { - return false + return } } } - return true + close(d.allTasksReadyCh) } -func (d *LocalRunner) WaitForReady(ctx context.Context, timeout time.Duration) error { +func (d *LocalRunner) WaitForReady(ctx context.Context) error { defer utils.StartTimer("docker.wait-for-ready")() - for { - select { - case <-ctx.Done(): - return ctx.Err() - - case <-time.After(timeout): - return fmt.Errorf("timeout") + select { + case <-ctx.Done(): + return ctx.Err() - case <-time.After(1 * time.Second): - if d.AreReady() { - return nil - } + case <-d.allTasksReadyCh: + return nil - case err := <-d.exitErr: - return err - } + case err := <-d.exitErr: + return err } } @@ -238,6 +229,7 @@ func (d *LocalRunner) updateTaskStatus(name string, status TaskStatus) { } d.emitCallback(name, status) + d.checkAndUpdateReadiness() } func (d *LocalRunner) ExitErr() <-chan error { diff --git a/playground/local_runner_test.go b/playground/local_runner_test.go index 8a9ae57..0c18746 100644 --- a/playground/local_runner_test.go +++ b/playground/local_runner_test.go @@ -55,7 +55,7 @@ func TestWaitForReady_Timeout(t *testing.T) { // Create a runner with a service that never becomes ready manifest := &Manifest{ Services: []*Service{ - {Name: "never-ready"}, + {Name: "never-ready", ReadyCheck: &ReadyCheck{}}, }, } @@ -68,17 +68,18 @@ func TestWaitForReady_Timeout(t *testing.T) { // Mark service as started but not ready runner.updateTaskStatus("never-ready", TaskStatusStarted) - ctx := context.Background() - err = runner.WaitForReady(ctx, 500*time.Millisecond) + waitCtx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond) + defer cancel() + err = runner.WaitForReady(waitCtx) require.Error(t, err) - require.Equal(t, "timeout", err.Error()) + require.ErrorIs(t, err, context.DeadlineExceeded) } func TestWaitForReady_Success(t *testing.T) { // Create a runner with a service that becomes ready manifest := &Manifest{ Services: []*Service{ - {Name: "ready-service"}, + {Name: "always-ready", ReadyCheck: &ReadyCheck{}}, }, } @@ -88,13 +89,11 @@ func TestWaitForReady_Success(t *testing.T) { runner, err := NewLocalRunner(cfg) require.NoError(t, err) - // Service becomes ready after a delay - go func() { - time.Sleep(200 * time.Millisecond) - runner.updateTaskStatus("ready-service", TaskStatusStarted) - }() + runner.updateTaskStatus("always-ready", TaskStatusStarted) + runner.updateTaskStatus("always-ready", TaskStatusHealthy) - ctx := context.Background() - err = runner.WaitForReady(ctx, 2*time.Second) + waitCtx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + err = runner.WaitForReady(waitCtx) require.NoError(t, err) }