Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-instance Process Compose #873

Merged
merged 12 commits into from
Apr 12, 2023
Merged

Conversation

Lagoja
Copy link
Contributor

@Lagoja Lagoja commented Apr 6, 2023

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

@Lagoja Lagoja requested review from mikeland73 and savil April 6, 2023 21:52
Copy link
Collaborator

@savil savil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I like the approach!

but I'm afraid I have strong reservations about the UUID. Please see my comment below. What do you think?

internal/impl/devbox.go Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
@Lagoja Lagoja requested a review from savil April 10, 2023 18:01
Copy link
Collaborator

@savil savil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking great so far! I have to pause for morning-routine, and will resume review once back.

internal/boxcli/services.go Show resolved Hide resolved
internal/boxcli/services.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@savil savil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Left a few more comments.

One main concern is: should we fail if we get an error with locking or unlocking? I was leaning towards "yes" to avoid weird issues with multiple processing clobbering the shared file.

internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved

lockFile(config.File.Fd())
defer config.File.Close()
defer unlockFile(config.File.Fd())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does first unlock, and then file-close. 🤔 I think this is problematic because:

  1. process#1: unlock happens
  2. process#2: starts writing to the same file
  3. process#1: closes file, which flushes any system buffers to disk. This clobbers writes of process#2.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-u, --unlock
Drop a lock. This is usually not required, since a lock is
automatically dropped when the file is closed.
source: https://man7.org/linux/man-pages/man1/flock.1.html

So, this makes me think that you just want defer config.File.Close().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also suggest checking the error return values of config.File.Close() and doing:

defer func() {
     err := config.File.Close()
     if err  != nil {
         // prefer writer since we manage the --quiet flag using that, but if its hard to get a writer here
         // for now, you could do io.Stderr
          fmt.Fprintf(writer /* or io.Stderr */, "failed to close file (%s). Got error %v\n", someFilePath, err)
     } 

}

This way we may get alerted to any errors from conflicts, etc. with our file-locking-and-writing code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, maybe move the defer file.config.Close() to be after the getGlobalConfig's error check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that makes sense, if I can unlock the file by closing it, then I can just defer that and only call unlock specifically when I need it

}

defer os.Remove(processComposePidfile)
return cmd.Wait()
err = cmd.Wait()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to get the output of the command, (i.e. out, err := cmd.Output()) which we could then print in the error message below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the command really outputs anything, since this starts the TUI.

internal/services/manager.go Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
internal/services/manager.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@savil savil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great!


config := readGlobalProcessComposeJSON(configFile)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment on line 294 can be moved up or removed

}
return nil

case <-time.After(fileLockTimeout):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, this is a nice way to do a timeout

if err != nil {
return err
return globalConfigFile, fmt.Errorf("failed to open config file: %w", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd return nil in these error cases.

}

cmd := exec.Command(processComposeBinPath, flags...)
return runProcessManagerInForeground(cmd, port, projectDir, w)
return runProcessManagerInForeground(cmd, config, port, projectDir, w)
Copy link
Collaborator

@savil savil Apr 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels a bit problematic:

  1. above we are doing defer configFile.Close()
  2. Inside runProcessManagerInForeground, we are closing the file, waiting for the command, and re-opening a file.

I worry that since the step (1) defer runs after step (2),then it (step 1) will error because it is closing a file that's already closed.

Oh, but we are implicitly discarding the error from file.Close() in the defer block, so all good I think!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess another option is to defer the entire cleanup block after cmd.Wait call + error check in runProcessManagerInForeground to ensure that it runs -- but I agree that discarding the error from defer configFile.Close() avoids the issue as well.

@Lagoja Lagoja merged commit dd47088 into main Apr 12, 2023
9 checks passed
@Lagoja Lagoja deleted the jl/multi-instance-processcompose branch April 12, 2023 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants