Skip to content

Commit dd47088

Browse files
authored
Multi-instance Process Compose (#873)
## Summary This PR allows users to run process-compose for multiple devbox projects at the same time, without any collisions or undesirable behavior. This does _not_ provide any isolation between services in different projects, but it does let a user effectively manage them on a per project basis ## How it works When starting process-compose in a devbox project, we create a UUID for the project, and then store the port + PID of the process-compose instance in a global `process-compose.json` file, with the UUID as the key. When making client requests to process-compose, a devbox project looks up the UUID for the project in the .devbox directory, gets the right port, and then makes the request. This ensures each project is communicating with the correct process-compose instance to start and stop services. Devbox also uses the same lookup to get the PID of a project's process-compose instance, so it can stop the right instance. ## Questions: * Should the project UUID be permanent or set somewhere else? Is there a better way to identify a project that won't be affected by the user moving it? ## Future features: * Let users specify their own process-compose port in `devbox services up` * ~Add `devbox services stop --all`, which stops all running process-compose files and resets the global config. This could help with situations where a developer accidentally leaves their projects running.~ ## How was it tested? Ran Example Tests to check backward compatibility, tested by running postgres + jekyll simultaneously, starting and stopping them
1 parent 7269168 commit dd47088

File tree

6 files changed

+351
-73
lines changed

6 files changed

+351
-73
lines changed

devbox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ type Devbox interface {
4747
ShellPlan() (*plansdk.ShellPlan, error)
4848
StartProcessManager(ctx context.Context, requestedServices []string, background bool, processComposeFileOrDir string) error
4949
StartServices(ctx context.Context, services ...string) error
50-
StopServices(ctx context.Context, services ...string) error
50+
StopServices(ctx context.Context, allProjects bool, services ...string) error
5151
ListServices(ctx context.Context) error
5252
}
5353

internal/boxcli/services.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ type serviceUpFlags struct {
1919
processComposeFile string
2020
}
2121

22+
type serviceStopFlags struct {
23+
configFlags
24+
allProjects bool
25+
}
26+
2227
func (flags *serviceUpFlags) register(cmd *cobra.Command) {
2328
flags.configFlags.register(cmd)
2429
cmd.Flags().StringVar(
@@ -32,9 +37,16 @@ func (flags *serviceUpFlags) register(cmd *cobra.Command) {
3237
&flags.background, "background", "b", false, "Run service in background")
3338
}
3439

40+
func (flags *serviceStopFlags) register(cmd *cobra.Command) {
41+
flags.configFlags.register(cmd)
42+
cmd.Flags().BoolVar(
43+
&flags.allProjects, "all-projects", false, "Stop all running services across all your projects.\nThis flag cannot be used simultaneously with the [services] argument")
44+
}
45+
3546
func servicesCmd() *cobra.Command {
3647
flags := servicesCmdFlags{}
3748
serviceUpFlags := serviceUpFlags{}
49+
serviceStopFlags := serviceStopFlags{}
3850
servicesCommand := &cobra.Command{
3951
Use: "services",
4052
Short: "Interact with devbox services",
@@ -59,9 +71,10 @@ func servicesCmd() *cobra.Command {
5971

6072
stopCommand := &cobra.Command{
6173
Use: "stop [service]...",
62-
Short: "Stop service. If no service is specified, stops all services",
74+
Short: "Stop one or more services in the current project. If no service is specified, stops all services in the current project.",
75+
Long: `Stop one or more services in the current project. If no service is specified, stops all services in the current project. \nIf the --all-projects flag is specified, stops all running services across all your projects. This flag cannot be used with [service] arguments.`,
6376
RunE: func(cmd *cobra.Command, args []string) error {
64-
return stopServices(cmd, args, flags)
77+
return stopServices(cmd, args, serviceStopFlags)
6578
},
6679
}
6780

@@ -83,6 +96,7 @@ func servicesCmd() *cobra.Command {
8396

8497
flags.config.register(servicesCommand)
8598
serviceUpFlags.register(upCommand)
99+
serviceStopFlags.register(stopCommand)
86100
servicesCommand.AddCommand(lsCommand)
87101
servicesCommand.AddCommand(upCommand)
88102
servicesCommand.AddCommand(restartCommand)
@@ -110,12 +124,15 @@ func startServices(cmd *cobra.Command, services []string, flags servicesCmdFlags
110124
return box.StartServices(cmd.Context(), services...)
111125
}
112126

113-
func stopServices(cmd *cobra.Command, services []string, flags servicesCmdFlags) error {
114-
box, err := devbox.Open(flags.config.path, cmd.ErrOrStderr())
127+
func stopServices(cmd *cobra.Command, services []string, flags serviceStopFlags) error {
128+
box, err := devbox.Open(flags.configFlags.path, cmd.ErrOrStderr())
115129
if err != nil {
116130
return errors.WithStack(err)
117131
}
118-
return box.StopServices(cmd.Context(), services...)
132+
if len(services) > 0 && flags.allProjects {
133+
return errors.New("cannot use both services and --all-projects arguments simultaneously")
134+
}
135+
return box.StopServices(cmd.Context(), flags.allProjects, services...)
119136
}
120137

121138
func restartServices(

internal/impl/devbox.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ func (d *Devbox) StartServices(ctx context.Context, serviceNames ...string) erro
449449
return d.RunScript("devbox", append([]string{"services", "start"}, serviceNames...))
450450
}
451451

452-
if !services.ProcessManagerIsRunning() {
452+
if !services.ProcessManagerIsRunning(d.projectDir) {
453453
fmt.Fprintln(d.writer, "Process-compose is not running. Starting it now...")
454454
fmt.Fprintln(d.writer, "\nNOTE: We recommend using `devbox services up` to start process-compose and your services")
455455
return d.StartProcessManager(ctx, serviceNames, true, "")
@@ -473,25 +473,34 @@ func (d *Devbox) StartServices(ctx context.Context, serviceNames ...string) erro
473473
for _, s := range serviceNames {
474474
err := services.StartServices(ctx, d.writer, s, d.projectDir)
475475
if err != nil {
476-
fmt.Printf("Error starting service %s: %s", s, err)
476+
fmt.Fprintf(d.writer, "Error starting service %s: %s", s, err)
477477
} else {
478-
fmt.Printf("Service %s started successfully", s)
478+
fmt.Fprintf(d.writer, "Service %s started successfully", s)
479479
}
480480
}
481481
return nil
482482
}
483483

484-
func (d *Devbox) StopServices(ctx context.Context, serviceNames ...string) error {
484+
func (d *Devbox) StopServices(ctx context.Context, allProjects bool, serviceNames ...string) error {
485485
if !IsDevboxShellEnabled() {
486-
return d.RunScript("devbox", append([]string{"services", "stop"}, serviceNames...))
486+
args := []string{"services", "stop"}
487+
args = append(args, serviceNames...)
488+
if allProjects {
489+
args = append(args, "--all-projects")
490+
}
491+
return d.RunScript("devbox", args)
487492
}
488493

489-
if !services.ProcessManagerIsRunning() {
494+
if allProjects {
495+
return services.StopAllProcessManagers(ctx, d.writer)
496+
}
497+
498+
if !services.ProcessManagerIsRunning(d.projectDir) {
490499
return usererr.New("Process manager is not running. Run `devbox services up` to start it.")
491500
}
492501

493502
if len(serviceNames) == 0 {
494-
return services.StopProcessManager(ctx, d.writer)
503+
return services.StopProcessManager(ctx, d.projectDir, d.writer)
495504
}
496505

497506
svcSet, err := d.Services()
@@ -526,7 +535,7 @@ func (d *Devbox) ListServices(ctx context.Context) error {
526535
return nil
527536
}
528537

529-
if !services.ProcessManagerIsRunning() {
538+
if !services.ProcessManagerIsRunning(d.projectDir) {
530539
fmt.Fprintln(d.writer, "No services currently running. Run `devbox services up` to start them:")
531540
fmt.Fprintln(d.writer, "")
532541
for _, s := range svcSet {
@@ -554,7 +563,7 @@ func (d *Devbox) RestartServices(ctx context.Context, serviceNames ...string) er
554563
return d.RunScript("devbox", append([]string{"services", "restart"}, serviceNames...))
555564
}
556565

557-
if !services.ProcessManagerIsRunning() {
566+
if !services.ProcessManagerIsRunning(d.projectDir) {
558567
fmt.Fprintln(d.writer, "Process-compose is not running. Starting it now...")
559568
fmt.Fprintln(d.writer, "\nTip: We recommend using `devbox services up` to start process-compose and your services")
560569
return d.StartProcessManager(ctx, serviceNames, true, "")
@@ -596,7 +605,11 @@ func (d *Devbox) StartProcessManager(
596605
return usererr.New("No services found in your project")
597606
}
598607

599-
// processCompose := services.LookupProcessCompose(d.projectDir, processComposeFileOrDir)
608+
for _, s := range requestedServices {
609+
if _, ok := svcs[s]; !ok {
610+
return usererr.New(fmt.Sprintf("Service %s not found in your project", s))
611+
}
612+
}
600613

601614
processComposePath, err := utilityLookPath("process-compose")
602615
if err != nil {
@@ -626,7 +639,15 @@ func (d *Devbox) StartProcessManager(
626639

627640
// Start the process manager
628641

629-
return services.StartProcessManager(ctx, requestedServices, svcs, d.projectDir, processComposePath, processComposeFileOrDir, background)
642+
return services.StartProcessManager(
643+
ctx,
644+
d.writer,
645+
requestedServices,
646+
svcs,
647+
d.projectDir,
648+
processComposePath, processComposeFileOrDir,
649+
background,
650+
)
630651
}
631652

632653
// computeNixEnv computes the set of environment variables that define a Devbox

internal/services/client.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313

1414
type processStates = types.ProcessStates
1515

16-
type ProcessSummary struct {
16+
type Process struct {
1717
Name string
1818
Status string
1919
ExitCode int
@@ -23,7 +23,7 @@ func StartServices(ctx context.Context, w io.Writer, serviceName string, project
2323
path := fmt.Sprintf("/process/start/%s", serviceName)
2424
method := "POST"
2525

26-
body, status, err := clientRequest(path, method)
26+
body, status, err := clientRequest(path, method, projectDir)
2727
if err != nil {
2828
return err
2929
}
@@ -42,7 +42,7 @@ func StopServices(ctx context.Context, serviceName string, projectDir string, w
4242
path := fmt.Sprintf("/process/stop/%s", serviceName)
4343
method := "PATCH"
4444

45-
body, status, err := clientRequest(path, method)
45+
body, status, err := clientRequest(path, method, projectDir)
4646
if err != nil {
4747
return err
4848
}
@@ -60,7 +60,7 @@ func RestartServices(ctx context.Context, serviceName string, projectDir string,
6060
path := fmt.Sprintf("/process/restart/%s", serviceName)
6161
method := "POST"
6262

63-
body, status, err := clientRequest(path, method)
63+
body, status, err := clientRequest(path, method, projectDir)
6464
if err != nil {
6565
return err
6666
}
@@ -74,12 +74,12 @@ func RestartServices(ctx context.Context, serviceName string, projectDir string,
7474
}
7575
}
7676

77-
func ListServices(ctx context.Context, projectDir string, w io.Writer) ([]ProcessSummary, error) {
77+
func ListServices(ctx context.Context, projectDir string, w io.Writer) ([]Process, error) {
7878
path := "/processes"
7979
method := "GET"
80-
results := []ProcessSummary{}
80+
results := []Process{}
8181

82-
body, status, err := clientRequest(path, method)
82+
body, status, err := clientRequest(path, method, projectDir)
8383
if err != nil {
8484
return results, err
8585
}
@@ -92,7 +92,7 @@ func ListServices(ctx context.Context, projectDir string, w io.Writer) ([]Proces
9292
return results, err
9393
}
9494
for _, process := range processes.States {
95-
results = append(results, ProcessSummary{
95+
results = append(results, Process{
9696
Name: process.Name,
9797
Status: process.Status,
9898
ExitCode: process.ExitCode,
@@ -105,9 +105,14 @@ func ListServices(ctx context.Context, projectDir string, w io.Writer) ([]Proces
105105
}
106106
}
107107

108-
func clientRequest(path string, method string) (string, int, error) {
109-
port := "8280"
110-
req, err := http.NewRequest(method, fmt.Sprintf("http://localhost:%s%s", port, path), nil)
108+
func clientRequest(path string, method string, projectDir string) (string, int, error) {
109+
port, err := GetProcessManagerPort(projectDir)
110+
if err != nil {
111+
err := fmt.Errorf("unable to connect to process-compose server: %s", err.Error())
112+
return "", 0, err
113+
}
114+
115+
req, err := http.NewRequest(method, fmt.Sprintf("http://localhost:%d%s", port, path), nil)
111116
if err != nil {
112117
return "", 0, err
113118
}

0 commit comments

Comments
 (0)