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

feat(starter): starter improvements to avoid port clashes #505

Merged
merged 22 commits into from
Oct 3, 2023

Conversation

kevin-dp
Copy link
Contributor

@kevin-dp kevin-dp commented Sep 28, 2023

This PR improves the starter to enable several (independent) Electric applications to run on the same machine and thus fixes #497 and #489. Also addressed VAX-1114.

The changes are as follows:

  • Avoid volume clashes by putting project name on the app name
  • Avoid web server port clashes by supporting a custom port to be used and fix it everywhere in the template
  • Avoid Electric port clashes by supporting a custom port to be used and fix it everywhere in the template
  • Introduced 2 modes for the starter: fast mode and interactive mode
    • Fast mode: app name and ports (optional) are provided as command line arguments
    • Interactive mode: no arguments to the script, app name and ports are asked via prompts
  • Changed start to check that the chosen port for Electric is free. If not, prompt user if they want to change the port
  • Script to change the project’s configuration with other Electric and web server ports. Exposed as a new ports:configure command.
  • Changed backend:start to check for port clashes and if there is a port clash suggest to run ports:configure.
  • Changed yarn start and client:generate to check that the Electric port the app is configured to use is the app's own Electric service (e.g. catches problem where project 1 is running Electric on port 5133 and another project is accidentally configured to also connect to port 5133).
  • Modified build script to use forward requests to right esbuild server. This fixes a bug where if an esbuild server was already running, the new app would forward its requests to the esbuild server of the other app.

tldr: it should now be possible to run several Electric applications. Port clashes should always be detected and reported with a suggestion to reconfigure the app with other ports.

@linear
Copy link

linear bot commented Sep 28, 2023

VAX-1087 Prefix starter template container name with app name.

As per #489 et al, we currently have a clash of container names from multiple invocations of the starter template.

One fix is to prefix the container name in the generated docker compose with the app name. Basically to stop Docker from reusing the same container / volume.

@kevin-dp kevin-dp marked this pull request as ready for review September 28, 2023 12:23
`docker run \
-e "DATABASE_URL=${db}" \
-e "LOGICAL_PUBLISHER_HOST=localhost" \
-e "AUTH_MODE=insecure" \
-p 5133:5133 \
-p ${electricPort}:5133 \
-p 5433:5433 ${electric}`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we still need to expose this port?
The compose file is not binding to 5433 so i'm assuming it is not necessary here either and we can remove it?

Copy link
Member

Choose a reason for hiding this comment

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

5433 is the logical publisher port. I think it is being exposed here to support setup where people are running their own postgres on the host while Electric itself is run inside a Docker container.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we do need to expose it, because we need the PG to be able to connect to the electric. In this case, isn’t the script for when you’re running Electric without Postgres in the same compose?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yes, i see. Perhaps i'll leave a comment to remember.
No the script for running only Electric is this separate file that executes docker run because there are some differences (PG url can be passed as an argument, and 5433 is not exposed in compose file).

Copy link
Contributor

@thruflo thruflo left a comment

Choose a reason for hiding this comment

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

Looks great 👍

`docker run \
-e "DATABASE_URL=${db}" \
-e "LOGICAL_PUBLISHER_HOST=localhost" \
-e "AUTH_MODE=insecure" \
-p 5133:5133 \
-p ${electricPort}:5133 \
-p 5433:5433 ${electric}`
Copy link
Member

Choose a reason for hiding this comment

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

5433 is the logical publisher port. I think it is being exposed here to support setup where people are running their own postgres on the host while Electric itself is run inside a Docker container.

@kevin-dp
Copy link
Contributor Author

Btw: at least someone else than me should try this out to see that everything works fine.
Try to break it with port clashes etc. and check that they get caught and that we can fix them :-)

@samwillis
Copy link
Contributor

samwillis commented Sep 28, 2023

A few quick thoughts, yarn is not installed with vanilla node, I know that predates this PR. We should use npm run.

(Aside: I feel strongly that all starters, examples and docs need to just use plain npm. Anyone that uses yarn or pnpm knows how to rewrite a command for it.)

If we do change the starter to use Vite as discussed on Discord earlier, the code for setting the dev web server port is not needed. Vite will enumerate ports until it finds one free and print it. (still need logic for electric)

I also feel that having the change port command rewrite the package.json and other committed files, isn't right. Different dev will want to use different ports. Local config should not be in committed files.

Should we not have an optional not committed .electric file that holds things like port numbers for the local project environment?

@kevin-dp
Copy link
Contributor Author

A few quick thoughts, yarn is not installed with vanilla node, I know that predates this PR. We should use npm run.

(Aside: I feel strongly that all starters, examples and docs need to just use plain npm. Anyone that uses yarn or pnpm knows how to rewrite a command for it.)

I had a similar opinion but we decided to stick with yarn everywhere a while ago.
If needed we can re-discuss it and change this.
But that will be a separate PR.

If we do change the starter to use Vite as discussed on Discord earlier, the code for setting the dev web server port is not needed. Vite will enumerate ports until it finds one free and print it. (still need logic for electric)

Yep but should be a separate PR, so for now best to do this such that the starter works with several apps.

I also feel that having the change port command rewrite the package.json and other committed files, isn't right. Different dev will want to use different ports. Local config should not be in committed files.

Should we not have an optional not committed .electric file that holds things like port numbers for the local project environment?

This has been a bit of a rabbit hole. We have discussed and iterated on several strategies this week.
We decided to stick with this approach of fixing the ports everywhere in the files.
If there is a better solution we can discuss and implement it later but we should probably move forward with other features first.

@thruflo
Copy link
Contributor

thruflo commented Sep 28, 2023

Just to chip in, I think @samwillis's points are very valid but also @kevin-dp is right that it's probably time to merge this and we can change to npm when we change to vite in subsequent PRs.

Should we not have an optional not committed .electric file that holds things like port numbers for the local project environment?

This does make sense to me. I was assuming we would persist in some kind of env file. Could that work rather than patching package.json and builder.js? Whether it's checked in or not I think is a separate question. I can imagine you may choose to distribute with or without the config file. Because maybe you do want to use it to specify your ports.

@alco
Copy link
Member

alco commented Sep 29, 2023

Notes from my testing:

  1. The output of yarn backend:start is no longer colored. Also, there are now lines that look like this

    Network app_1_default  Creating
    Network app_1_default  Created
    

    Whereas before the line would be updated in-place. It seems that the command is now treating the terminal as a dumb one and skips employing advanced features such as line rewriting and the use of ANSI escapes for colors.

  2. After starting the backend and running yarn db:migrate, when I then run yarn client:generate it fails:

    /bin/sh: line 1: electric:check: command not found
    error Command failed with exit code 127
    

    The package.json file includes a definition for the "electric:check" script. The problem is probably with the way "client:generate" invokes it:

    "client:generate": "electric:check && npx electric-sql generate [...]"
    

    I managed to make it work by prefixing the command with yarn:

    "client:generate": "yarn electric:check && npx electric-sql generate [...]"
    
  3. The prompts shown when a port clash is detect has awkward UX. You can see my exchange with it below:

    $ create-electric-app app_2
    Port 5133 for Electric is already in use.
    prompt: Do you want to chose another port for Electric? [y/n]:  (y) 
    prompt: port:  (5133) 5134
    Port 3001 for the web server is already in use.
    prompt: Do you want to chose another port for the web server? [y/n]:  (y) 3002
    error:   Please reply with 'y' or 'n'
    prompt: Do you want to chose another port for the web server? [y/n]:  (y) 
    prompt: port:  (3001) 3002
    ⚡️ Your ElectricSQL app is ready at `./app_2`
    

    How about asking for a different port number within one prompt:

    Port 5133 for Electric is already in use.
    prompt: Hit Enter to keep it or enter a different port number: 5134
    Port 3001 for the web server is already in use.
    prompt: Hit Enter to keep it or enter a different port number: 3002
    
  4. This was the case prior to this PR but it still remains a point of confusion: when running yarn start, I would expect to get the URL to access the app printed in the output but it's not. And the app does not open a web browser tab automatically. So I'm left guessing which port it's running on. I have filed this as VAX-1114.

  5. Somewhat related to the previous point, in addition to the 3001 port, the app is also listening on port 8000. When I create app_1 and app_2 from the starter template, then whichever gets started first will be available on port 8000.

  6. I tried testing the case where I would run an app that would try to connect to Electric but the latter would be running on a different port. So I have yarn backend:start in app_1 and then run yarn client:generate or yarn start in app_2.

    Here's the output that gets produced:

    $ yarn start
    yarn run v1.22.19
    warning package.json: No license field
    warning ../../../package.json: No license field
    $ yarn electric:check && SERVE=true npm run build
    warning package.json: No license field
    warning ../../../package.json: No license field
    $ node ./check-electric-is-running.js
    /home/alco/code/tmp/app_2/db/util.js:36
        error(`${service} appears not to be running for this app.\nDocker container ${container} not running.`)
        ^
    
    ReferenceError: Cannot access 'error' before initialization
        at fetchHostPort (/home/alco/code/tmp/app_2/db/util.js:36:5)
        at fetchHostPortPG (/home/alco/code/tmp/app_2/db/util.js:23:10)
        at Object.<anonymous> (/home/alco/code/tmp/app_2/db/util.js:12:16)
        at Module._compile (node:internal/modules/cjs/loader:1256:14)
        at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
        at Module.load (node:internal/modules/cjs/loader:1119:32)
        at Module._load (node:internal/modules/cjs/loader:960:12)
        at Module.require (node:internal/modules/cjs/loader:1143:19)
        at require (node:internal/modules/cjs/helpers:119:18)
        at Object.<anonymous> (/home/alco/code/tmp/app_2/check-electric-is-running.js:1:35)
    
    Node.js v18.18.0
    error Command failed with exit code 1.
    info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
    error Command failed with exit code 1.
    info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
    
    $ yarn client:generate
    yarn run v1.22.19
    warning package.json: No license field
    warning ../../../package.json: No license field
    $ yarn electric:check && npx electric-sql generate --service http://localhost:5134
    warning package.json: No license field
    warning ../../../package.json: No license field
    $ node ./check-electric-is-running.js
    /home/alco/code/tmp/app_2/db/util.js:36
        error(`${service} appears not to be running for this app.\nDocker container ${container} not running.`)
        ^
    
    ReferenceError: Cannot access 'error' before initialization
        at fetchHostPort (/home/alco/code/tmp/app_2/db/util.js:36:5)
        at fetchHostPortPG (/home/alco/code/tmp/app_2/db/util.js:23:10)
        at Object.<anonymous> (/home/alco/code/tmp/app_2/db/util.js:12:16)
        at Module._compile (node:internal/modules/cjs/loader:1256:14)
        at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
        at Module.load (node:internal/modules/cjs/loader:1119:32)
        at Module._load (node:internal/modules/cjs/loader:960:12)
        at Module.require (node:internal/modules/cjs/loader:1143:19)
        at require (node:internal/modules/cjs/helpers:119:18)
        at Object.<anonymous> (/home/alco/code/tmp/app_2/check-electric-is-running.js:1:35)
    
    Node.js v18.18.0
    error Command failed with exit code 1.
    info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
    error Command failed with exit code 1.
    info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
    

@alco
Copy link
Member

alco commented Sep 29, 2023

Re point 1 above, here's a side-by-side diff between main and this PR:
Screenshot from 2023-09-29 12-32-16

Weirdly enough, when I run yarn backend:down, the output is colored in both versions.

@kevin-dp
Copy link
Contributor Author

kevin-dp commented Oct 2, 2023

@alco Output of yarn backend:start is no longer colored because we now invoke docker compose up from a script that uses shelljs to invoke it and shelljs still does not support coloring...

@alco
Copy link
Member

alco commented Oct 2, 2023

@alco Output of yarn backend:start is no longer colored because we now invoke docker compose up from a script that uses shelljs to invoke it and shelljs still does not support coloring...

I have found this package which is a lightweight wrapper of shelljs that supposedly adds coloring. Could we use it instead?
shelljs/shelljs#86 (comment)

@kevin-dp
Copy link
Contributor Author

kevin-dp commented Oct 3, 2023

@alco

1: Moved to using shell-js. Output is colored now.

2: Thanks! Not sure how this went unnoticed.

3: done!

4: Ok change is very small so addressed it here also.

5: Indeed, the esbuild server runs on port 8000 by default. If it is already taken it will take another available port with preference for ports in the range 8000 to 8009. Not sure why this matters though? I made sure that the app's builder.js script uses whatever port that was chosen by esbuild.

6: This is actually good! It did detect the problem and wanted to raise it as an error but somehow the error function is not yet initialized (because it is an arrow function assigned to a constant, i will turn it into a real function to fix this).

@kevin-dp
Copy link
Contributor Author

kevin-dp commented Oct 3, 2023

@alco Output of yarn backend:start is no longer colored because we now invoke docker compose up from a script that uses shelljs to invoke it and shelljs still does not support coloring...

I have found this package which is a lightweight wrapper of shelljs that supposedly adds coloring. Could we use it instead? shelljs/shelljs#86 (comment)

Yep had also seen it but was not sure about its stability and maturity though.
So I now gave it a try and it works flawlessly.

This makes backend:start/backend:stop and backend:up/backend:down more similar to
"docker compose start/stop" and "docker compose up/down", respectively.
Copy link
Member

@alco alco left a comment

Choose a reason for hiding this comment

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

Top notch! 🏆

I've pushed one commit as a suggestion, feel free to keep it or remove it.

@kevin-dp kevin-dp merged commit 0d879a8 into main Oct 3, 2023
11 checks passed
@kevin-dp kevin-dp deleted the kevindp/vax-1087-container-clashes branch October 3, 2023 11:58
alco added a commit that referenced this pull request Oct 4, 2023
…520)

This is a follow-up to #505
and #502. I'm addressing a
few different concerns in the same PR only because we don't have
automated tests for the starter and redoing the same manual testing
steps multiple times is error-prone.

Here's a brief description of changes.

## Removed `shelljs-live`

This reverts
a8764b0.

Turns out `shelljs-live` is not a drop-in replacement for `shelljs`. It
only uses shelljs's config but implements a different API for executing
a subprocess that only returns the status code to the caller. This broke
our error handling in `startCompose.js` and `startElectric.js` that
expects the return value to have the `stderr` field. I discovered this
problem when I executed `yarn backend:up` instead of `yarn
backend:start`:

$ yarn backend:up
yarn run v1.22.19
warning package.json: No license field
warning ../../../package.json: No license field
$ yarn backend:start --detach
warning package.json: No license field
warning ../../../package.json: No license field
$ node ./backend/startCompose.js --detach
[+] Running 5/5
 ✔ Network app_default         Created                                                                                                                                                                          0.1s 
 ✔ Volume "app_pg_data"        Created                                                                                                                                                                          0.0s 
 ✔ Volume "app_electric_data"  Created                                                                                                                                                                          0.0s 
 ✔ Container app-postgres-1    Started                                                                                                                                                                          0.1s 
 ✔ Container app-electric-1    Started                                                                                                                                                                          0.1s 
/home/alco/code/tmp/app/backend/startCompose.js:11
if (res.code !== 0 && res.stderr.includes('port is already allocated')) {
                                 ^

TypeError: Cannot read properties of undefined (reading 'includes')
    at Object.<anonymous> (/home/alco/code/tmp/app/backend/startCompose.js:11:34)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:86:12)
    at node:internal/main/run_main_module:23:47

Node.js v18.18.0
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

It does mean we lose colorized output from `yarn backend:start`. But I
believe it can be restored by getting rid of `shelljs` completely and
using `cross-spawn` like `shelljs-live` does.

## Replaced ad-hoc migration runner with @databases/pg-migrations

This package does everything we need: it can run multiple migrations and
keeps track of already applied migrations.

The way `@databases/pg-migrations` keeps track of applied migrations is
a bit noisy:
```
[localhost] postgres:app=# \d
                          List of relations
 Schema │                 Name                  │   Type   │  Owner   
────────┼───────────────────────────────────────┼──────────┼──────────
 public │ atdatabases_migrations_applied        │ table    │ postgres
 public │ atdatabases_migrations_applied_id_seq │ sequence │ postgres
 public │ atdatabases_migrations_version        │ table    │ postgres
 public │ foo                                   │ table    │ postgres
 public │ items                                 │ table    │ postgres
(5 rows)
```

But I don't think it's a problem for the starter template. Once we
introduce integrations with backend frameworks, we'll replace this
migrations runner with framework-specific ones.

## Reset terminal color back to default after printing an error message

Our colorized error messages where missing the "reset" ANSI escape which
was causing the output following the error message to also be colored,
namely, the `Done` line printed by `yarn`.
alco added a commit that referenced this pull request Oct 5, 2023
This PR improves the starter to enable several (independent) Electric
applications to run on the same machine and thus fixes #497 and #489.
Also addressed VAX-1114.

The changes are as follows:
- Avoid volume clashes by putting project name on the app name
- Avoid web server port clashes by supporting a custom port to be used
and fix it everywhere in the template
- Avoid Electric port clashes by supporting a custom port to be used and
fix it everywhere in the template
- Introduced 2 modes for the starter: fast mode and interactive mode
- Fast mode: app name and ports (optional) are provided as command line
arguments
- Interactive mode: no arguments to the script, app name and ports are
asked via prompts
- Changed start to check that the chosen port for Electric is free. If
not, prompt user if they want to change the port
- Script to change the project’s configuration with other Electric and
web server ports. Exposed as a new `ports:configure` command.
- Changed `backend:start` to check for port clashes and if there is a
port clash suggest to run `ports:configure`.
- Changed `yarn start` and `client:generate` to check that the Electric
port the app is configured to use is the app's own Electric service
(e.g. catches problem where project 1 is running Electric on port 5133
and another project is accidentally configured to also connect to port
5133).
- Modified build script to use forward requests to right esbuild server.
This fixes a bug where if an esbuild server was already running, the new
app would forward its requests to the esbuild server of the other app.

tldr: it should now be possible to run several Electric applications.
Port clashes should always be detected and reported with a suggestion to
reconfigure the app with other ports.

---------

Co-authored-by: Oleksii Sholik <[email protected]>
alco added a commit that referenced this pull request Oct 5, 2023
…520)

This is a follow-up to #505
and #502. I'm addressing a
few different concerns in the same PR only because we don't have
automated tests for the starter and redoing the same manual testing
steps multiple times is error-prone.

Here's a brief description of changes.

## Removed `shelljs-live`

This reverts
a8764b0.

Turns out `shelljs-live` is not a drop-in replacement for `shelljs`. It
only uses shelljs's config but implements a different API for executing
a subprocess that only returns the status code to the caller. This broke
our error handling in `startCompose.js` and `startElectric.js` that
expects the return value to have the `stderr` field. I discovered this
problem when I executed `yarn backend:up` instead of `yarn
backend:start`:

$ yarn backend:up
yarn run v1.22.19
warning package.json: No license field
warning ../../../package.json: No license field
$ yarn backend:start --detach
warning package.json: No license field
warning ../../../package.json: No license field
$ node ./backend/startCompose.js --detach
[+] Running 5/5
 ✔ Network app_default         Created                                                                                                                                                                          0.1s 
 ✔ Volume "app_pg_data"        Created                                                                                                                                                                          0.0s 
 ✔ Volume "app_electric_data"  Created                                                                                                                                                                          0.0s 
 ✔ Container app-postgres-1    Started                                                                                                                                                                          0.1s 
 ✔ Container app-electric-1    Started                                                                                                                                                                          0.1s 
/home/alco/code/tmp/app/backend/startCompose.js:11
if (res.code !== 0 && res.stderr.includes('port is already allocated')) {
                                 ^

TypeError: Cannot read properties of undefined (reading 'includes')
    at Object.<anonymous> (/home/alco/code/tmp/app/backend/startCompose.js:11:34)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:86:12)
    at node:internal/main/run_main_module:23:47

Node.js v18.18.0
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

It does mean we lose colorized output from `yarn backend:start`. But I
believe it can be restored by getting rid of `shelljs` completely and
using `cross-spawn` like `shelljs-live` does.

## Replaced ad-hoc migration runner with @databases/pg-migrations

This package does everything we need: it can run multiple migrations and
keeps track of already applied migrations.

The way `@databases/pg-migrations` keeps track of applied migrations is
a bit noisy:
```
[localhost] postgres:app=# \d
                          List of relations
 Schema │                 Name                  │   Type   │  Owner   
────────┼───────────────────────────────────────┼──────────┼──────────
 public │ atdatabases_migrations_applied        │ table    │ postgres
 public │ atdatabases_migrations_applied_id_seq │ sequence │ postgres
 public │ atdatabases_migrations_version        │ table    │ postgres
 public │ foo                                   │ table    │ postgres
 public │ items                                 │ table    │ postgres
(5 rows)
```

But I don't think it's a problem for the starter template. Once we
introduce integrations with backend frameworks, we'll replace this
migrations runner with framework-specific ones.

## Reset terminal color back to default after printing an error message

Our colorized error messages where missing the "reset" ANSI escape which
was causing the output following the error message to also be colored,
namely, the `Done` line printed by `yarn`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[VAX-1087] Prefix starter template container name with app name.
4 participants