Skip to content

Commit 007d4f1

Browse files
authored
Improve restarts (#247)
- Creating and mounting specific Docker-managed volumes for container data instead of bind mounting - Mounting validator logs to a Docker-managed volume so that we don't have removal issues on restart - Identifying Docker Compose projects by the ID in manifest at start - Taking down Docker Compose projects by the ID with automatic container and volume removal (and getting rid of removal based on the container list) - `errgroup` use for starting services on the host Fixes #181
1 parent 477a842 commit 007d4f1

File tree

5 files changed

+65
-76
lines changed

5 files changed

+65
-76
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ build: ## Build the CLI
2525

2626
.PHONY: test
2727
test: ## Run tests
28-
go test ./...
28+
go test -v -count=1 ./...
2929

3030

3131
.PHONY: lint

main.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212

1313
"github.com/flashbots/builder-playground/playground"
1414
"github.com/flashbots/builder-playground/utils/mainctx"
15-
"github.com/google/uuid"
1615
"github.com/spf13/cobra"
1716
)
1817

@@ -198,7 +197,6 @@ func runIt(recipe playground.Recipe) error {
198197
}
199198

200199
svcManager := playground.NewManifest(exCtx, artifacts.Out)
201-
svcManager.ID = uuid.New().String()
202200

203201
recipe.Apply(svcManager)
204202
if err := svcManager.Validate(); err != nil {

playground/components.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,9 @@ func (l *LighthouseValidator) Apply(manifest *Manifest) {
581581
"--prefer-builder-proposals",
582582
).
583583
WithArtifact("/data/validator", "data_validator").
584-
WithArtifact("/data/testnet-dir", "testnet")
584+
WithArtifact("/data/testnet-dir", "testnet").
585+
// HACK: Mount a Docker-managed volume to avoid permission issues with removing logs.
586+
WithVolume("validator-logs", "/data/validator/validators/logs")
585587
}
586588

587589
type ClProxy struct {

playground/local_runner.go

Lines changed: 54 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -368,53 +368,26 @@ func (d *LocalRunner) ExitErr() <-chan error {
368368
}
369369

370370
func (d *LocalRunner) Stop() error {
371-
// only stop the containers that belong to this session
372-
containers, err := d.client.ContainerList(context.Background(), container.ListOptions{
373-
Filters: filters.NewArgs(filters.Arg("label", fmt.Sprintf("playground.session=%s", d.manifest.ID))),
374-
})
375-
if err != nil {
376-
return fmt.Errorf("error getting container list: %w", err)
377-
}
378-
379-
var wg sync.WaitGroup
380-
wg.Add(len(containers))
381-
382-
var errCh chan error
383-
errCh = make(chan error, len(containers))
384-
385-
for _, cont := range containers {
386-
go func(contID string) {
387-
defer wg.Done()
388-
if err := d.client.ContainerRemove(context.Background(), contID, container.RemoveOptions{
389-
RemoveVolumes: true,
390-
RemoveLinks: false,
391-
Force: true,
392-
}); err != nil {
393-
errCh <- fmt.Errorf("error removing container: %w", err)
394-
}
395-
}(cont.ID)
396-
}
397-
398-
wg.Wait()
399-
400371
// stop all the handles
401372
for _, handle := range d.handles {
402373
handle.Process.Kill()
403374
}
404375

405-
close(errCh)
376+
// stop the docker-compose
377+
cmd := exec.CommandContext(
378+
context.Background(), "docker", "compose",
379+
"-p", d.manifest.ID,
380+
"down",
381+
"-v", // removes containers and volumes
382+
)
383+
var outBuf bytes.Buffer
384+
cmd.Stdout = &outBuf
385+
cmd.Stderr = &outBuf
406386

407-
for err := range errCh {
408-
if err != nil {
409-
return err
410-
}
387+
if err := cmd.Run(); err != nil {
388+
return fmt.Errorf("error taking docker-compose down: %w\n%s", err, outBuf.String())
411389
}
412390

413-
if d.cleanupNetwork {
414-
if err := d.client.NetworkRemove(context.Background(), d.config.NetworkName); err != nil {
415-
return err
416-
}
417-
}
418391
return nil
419392
}
420393

@@ -586,26 +559,26 @@ func (d *LocalRunner) validateImageExists(image string) error {
586559
return fmt.Errorf("image %s not found", image)
587560
}
588561

589-
func (d *LocalRunner) toDockerComposeService(s *Service) (map[string]interface{}, error) {
562+
func (d *LocalRunner) toDockerComposeService(s *Service) (map[string]interface{}, []string, error) {
590563
// apply the template again on the arguments to figure out the connections
591564
// at this point all of them are valid, we just have to resolve them again. We assume for now
592565
// everyone is going to be on docker at the same network.
593566
args, envs, err := d.applyTemplate(s)
594567
if err != nil {
595-
return nil, fmt.Errorf("failed to apply template, err: %w", err)
568+
return nil, nil, fmt.Errorf("failed to apply template, err: %w", err)
596569
}
597570

598571
// The containers have access to the full set of artifacts on the /artifacts folder
599572
// so, we have to bind it as a volume on the container.
600573
outputFolder, err := d.out.AbsoluteDstPath()
601574
if err != nil {
602-
return nil, fmt.Errorf("failed to get absolute path for output folder: %w", err)
575+
return nil, nil, fmt.Errorf("failed to get absolute path for output folder: %w", err)
603576
}
604577

605578
// Validate that the image exists
606579
imageName := fmt.Sprintf("%s:%s", s.Image, s.Tag)
607580
if err := d.validateImageExists(imageName); err != nil {
608-
return nil, fmt.Errorf("failed to validate image %s: %w", imageName, err)
581+
return nil, nil, fmt.Errorf("failed to validate image %s: %w", imageName, err)
609582
}
610583

611584
labels := map[string]string{
@@ -635,12 +608,11 @@ func (d *LocalRunner) toDockerComposeService(s *Service) (map[string]interface{}
635608
}
636609

637610
// create the bind volumes
611+
var createdVolumes []string
638612
for localPath, volumeName := range s.VolumesMapped {
639-
volumeDirAbsPath, err := d.createVolume(s.Name, volumeName)
640-
if err != nil {
641-
return nil, err
642-
}
643-
volumes[volumeDirAbsPath] = localPath
613+
dockerVolumeName := d.createVolumeName(s.Name, volumeName)
614+
volumes[dockerVolumeName] = localPath
615+
createdVolumes = append(createdVolumes, dockerVolumeName)
644616
}
645617

646618
volumesInLine := []string{}
@@ -674,7 +646,7 @@ func (d *LocalRunner) toDockerComposeService(s *Service) (map[string]interface{}
674646
if s.ReadyCheck.UseNC {
675647
u, err := url.Parse(s.ReadyCheck.QueryURL)
676648
if err != nil {
677-
return nil, fmt.Errorf("failed to parse ready check url '%s': %v", s.ReadyCheck.QueryURL, err)
649+
return nil, nil, fmt.Errorf("failed to parse ready check url '%s': %v", s.ReadyCheck.QueryURL, err)
678650
}
679651
test = []string{"CMD-SHELL", "nc -z localhost " + u.Port()}
680652
} else {
@@ -739,7 +711,7 @@ func (d *LocalRunner) toDockerComposeService(s *Service) (map[string]interface{}
739711
service["ports"] = ports
740712
}
741713

742-
return service, nil
714+
return service, createdVolumes, nil
743715
}
744716

745717
func (d *LocalRunner) isHostService(name string) bool {
@@ -769,18 +741,27 @@ func (d *LocalRunner) generateDockerCompose() ([]byte, error) {
769741
}
770742
}
771743

744+
volumes := map[string]struct{}{}
772745
for _, svc := range d.manifest.Services {
773746
if d.isHostService(svc.Name) {
774747
// skip services that are going to be launched on host
775748
continue
776749
}
777-
var err error
778-
if services[svc.Name], err = d.toDockerComposeService(svc); err != nil {
750+
var (
751+
err error
752+
dockerVolumes []string
753+
)
754+
services[svc.Name], dockerVolumes, err = d.toDockerComposeService(svc)
755+
if err != nil {
779756
return nil, fmt.Errorf("failed to convert service %s to docker compose service: %w", svc.Name, err)
780757
}
758+
for _, volumeName := range dockerVolumes {
759+
volumes[volumeName] = struct{}{}
760+
}
781761
}
782762

783763
compose["services"] = services
764+
compose["volumes"] = volumes
784765
yamlData, err := yaml.Marshal(compose)
785766
if err != nil {
786767
return nil, fmt.Errorf("failed to marshal docker compose: %w", err)
@@ -789,7 +770,11 @@ func (d *LocalRunner) generateDockerCompose() ([]byte, error) {
789770
return yamlData, nil
790771
}
791772

792-
func (d *LocalRunner) createVolume(service, volumeName string) (string, error) {
773+
func (d *LocalRunner) createVolumeName(service, volumeName string) string {
774+
return fmt.Sprintf("volume-%s-%s", service, volumeName)
775+
}
776+
777+
func (d *LocalRunner) createVolumeDir(service, volumeName string) (string, error) {
793778
// create the volume in the output folder
794779
volumeDirAbsPath, err := d.out.CreateDir(fmt.Sprintf("volume-%s-%s", service, volumeName))
795780
if err != nil {
@@ -809,7 +794,7 @@ func (d *LocalRunner) runOnHost(ss *Service) error {
809794
// Create the volumes for this service
810795
volumesMapped := map[string]string{}
811796
for pathInDocker, volumeName := range ss.VolumesMapped {
812-
volumeDirAbsPath, err := d.createVolume(ss.Name, volumeName)
797+
volumeDirAbsPath, err := d.createVolumeDir(ss.Name, volumeName)
813798
if err != nil {
814799
return err
815800
}
@@ -1069,7 +1054,13 @@ func (d *LocalRunner) Run(ctx context.Context) error {
10691054
}
10701055

10711056
// First start the services that are running in docker-compose
1072-
cmd := exec.CommandContext(ctx, "docker", "compose", "-f", d.out.dst+"/docker-compose.yaml", "up", "-d")
1057+
cmd := exec.CommandContext(
1058+
ctx, "docker", "compose",
1059+
"-p", d.manifest.ID, // identify project with id for doing "docker compose down" on it later
1060+
"-f", d.out.dst+"/docker-compose.yaml",
1061+
"up",
1062+
"-d",
1063+
)
10731064

10741065
var errOut bytes.Buffer
10751066
cmd.Stderr = &errOut
@@ -1083,24 +1074,16 @@ func (d *LocalRunner) Run(ctx context.Context) error {
10831074
}
10841075

10851076
// Second, start the services that are running on the host machine
1086-
errCh := make(chan error)
1087-
go func() {
1088-
for _, svc := range d.manifest.Services {
1089-
if d.isHostService(svc.Name) {
1090-
if err := d.runOnHost(svc); err != nil {
1091-
errCh <- err
1092-
}
1093-
}
1094-
}
1095-
close(errCh)
1096-
}()
1097-
1098-
for err := range errCh {
1099-
if err != nil {
1100-
return err
1077+
g := new(errgroup.Group)
1078+
for _, svc := range d.manifest.Services {
1079+
if d.isHostService(svc.Name) {
1080+
g.Go(func() error {
1081+
return d.runOnHost(svc)
1082+
})
11011083
}
11021084
}
1103-
return nil
1085+
1086+
return g.Wait()
11041087
}
11051088

11061089
// StopContainersBySessionID removes all Docker containers associated with a specific playground session ID.

playground/manifest.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"text/template"
1212
"time"
1313

14+
"github.com/google/uuid"
1415
flag "github.com/spf13/pflag"
1516
)
1617

@@ -42,7 +43,12 @@ type Manifest struct {
4243

4344
func NewManifest(ctx *ExContext, out *output) *Manifest {
4445
ctx.Output = out
45-
return &Manifest{ctx: ctx, out: out, overrides: make(map[string]string)}
46+
return &Manifest{
47+
ID: uuid.New().String(),
48+
ctx: ctx,
49+
out: out,
50+
overrides: make(map[string]string),
51+
}
4652
}
4753

4854
type LogLevel string

0 commit comments

Comments
 (0)