Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ local-testnet
output
builder-playground
**/.DS_Store
e2e-test/
e2e-test/
playground/cache
5 changes: 4 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
_ "embed"
"fmt"
"log"
Expand Down Expand Up @@ -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)
}
Expand Down
9 changes: 8 additions & 1 deletion playground/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion playground/components_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
58 changes: 25 additions & 33 deletions playground/local_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"strings"
"sync"
"text/template"
"time"

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
}

Expand All @@ -238,6 +229,7 @@ func (d *LocalRunner) updateTaskStatus(name string, status TaskStatus) {
}

d.emitCallback(name, status)
d.checkAndUpdateReadiness()
}

func (d *LocalRunner) ExitErr() <-chan error {
Expand Down
23 changes: 11 additions & 12 deletions playground/local_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}},
},
}

Expand All @@ -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{}},
},
}

Expand All @@ -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)
}