-
Notifications
You must be signed in to change notification settings - Fork 291
adding custom port in cli opt-in #3182
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
Conversation
|
I came across this error after running error: 1 target failed:
`-p spin-factor-outbound-http --test factor_test`
make: *** [test-unit] Error 101All tests passed. So I don't know it this is something that can be ignored |
|
@seun-ja Could you please take a look at https://spinframework.dev/v3/contributing-spin#committing-and-pushing-your-changes? This project requires commit signing and sign-off (two entirely separate things). |
07d6d80 to
f9d5295
Compare
itowlson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this addresses #2470 after all - that issue asks for Spin to automatically find a free port number (which we are dubious about wanting as default), whereas this PR provides another way to select a fixed port number.
Perhaps it would be useful to say more about the motivation for this change?
crates/trigger-http/src/lib.rs
Outdated
|
|
||
| /// The port to listen on | ||
| #[clap(long = "port", env = "SPIN_HTTP_LISTEN_PORT")] | ||
| pub port: Option<u16>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you expand on how you see this interacting with --listen? The way I usually override the port is e.g. spin up --listen 127.0.0.1:4000 - is this envisaged as a shortcut to that? Or a way to set the address and port separately?
crates/trigger-http/src/server.rs
Outdated
|
|
||
| if let Some(tls_config) = self.tls_config.clone() { | ||
| self.serve_https(listener, tls_config).await?; | ||
| self.serve_https(listener?, tls_config).await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I love having to ? the listener everywhere it's used. Why not stick with the previous pattern of failing errors before assigning the listener variable?
|
In hindsight, this isn't an ideal approach. I didn't take into consideration that |
|
IMO if you'd be happy with a random port then you should just listen on Based on the discussion in the issue I'd suggest something like this:
|
|
Would be a good idea to prompt the user if we are on an interactive CLI where we can have a confirm prompt with the user if they want to use a different port? |
fbed697 to
f2b9734
Compare
|
@karthik2804 Because multi-trigger-type applications exist, I'd be inclined to adopt a philosophy that triggers should not prompt for input. Otherwise we risk having to manage several child processes all prompting at once, which is likely to be a bit chaotic. Or partial starts where one trigger is processing events but another is stuck at a prompt. (Sure, we could do a little dance to inform a trigger that it's the only one so it's allowed to prompt, but this makes both |
itowlson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some style and wording nits, but my main concern is if the loop is fully doing what you intend. That may be a code reading fail on my part though! Great to see this progressing!
crates/trigger-http/src/server.rs
Outdated
| } | ||
| Err(err) => { | ||
| if err.kind() == ErrorKind::AddrInUse { | ||
| continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This routine gets into some quite deep nesting (a match inside a loop inside an if inside a match) and for me it was hard to follow. Is it feasible to break out a helper function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be.. would look into it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's still value in breaking out a helper. The long and complicated if and loop make it very hard (at least for me) to see what is basically simple "if it succeeds, use it; if it doesn't, search or bail." Consider e.g.
let listener = match TcpListener::bind(self.listen_addr).await {
Ok(listener) => listener,
Err(err) if self.find_free_port && err.kind() == ErrorKind::AddrInUse => self.search_for_free_port().await?,
Err(err) => anyhow::bail!(...)
}
fn search_for_free_port(&self) -> anyhow::Result<TcpListener> {
// declare mutables
// loop
// ok or error
}
There's probably more breaking out you could do but to me this greatly clarifies the top-level logic, and gives a meaningful name to that long chunk for which I currently have to infer the intention from the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I've never found a nice way to do retry loops without a separate function so you can return from the middle of the loop.
crates/trigger-http/src/server.rs
Outdated
| if err.kind() == ErrorKind::AddrInUse { | ||
| continue; | ||
| } | ||
| return Err(anyhow::anyhow!("Unable to listen on {}", addr)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's a non-AddrInUse error on one of the scanned ports, this will produce a strange error message e.g. the user said to listen on port 4000 and gets an error about port 4007.
crates/trigger-http/src/server.rs
Outdated
| if err.kind() == ErrorKind::AddrInUse { | ||
| continue; | ||
| } | ||
| return Err(anyhow::anyhow!("Unable to listen on {}", addr)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct that we bail here? Why would an error on port 3002 stop us from trying port 3003? (Is it, perhaps, that errors other than AddrInUse imply we've gone to a bad place?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It only bails if the error kind isn't AddrInUse, so it doesn't stop us from trying if 3002 isn't available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that it continues on AddrInUse. I am asking why it bails on other errors. Is the assumption that other errors are unrecoverable and there is no point searching further?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are probably other recoverable errors. I only focused on AddrInUse for the scope of this issue.
Should I add the ones that could be recoverable here, or just create a new issue for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's going to turn into a faff then don't worry about it - stick with what you have and we can always re-evaluate if something comes up (i.e. no need to raise an issue either).
crates/trigger-http/src/server.rs
Outdated
| if self.find_free_port && err.kind() == ErrorKind::AddrInUse { | ||
| let mut found_listener = None; | ||
| for _ in 1..=9 { | ||
| let mut addr = self.listen_addr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reads as if you are resetting addr each time through the loop, which means that each iteration is trying self.listen_addr with +1 to its port. Have you confirmed that if e.g. 3000-3002 are all in use then this does find 3003? I may be misreading for sure
crates/trigger-http/src/server.rs
Outdated
| } | ||
| } | ||
| } else { | ||
| return Err(anyhow::anyhow!("Unable to listen on {}", self.listen_addr)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider mentioning the --find-free-port option e.g.
| return Err(anyhow::anyhow!("Unable to listen on {}", self.listen_addr)); | |
| return Err(anyhow::anyhow!("Unable to listen on {}. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr)); |
|
|
||
| let app = spin_app::App::new("my-app", locked_app); | ||
| let trigger = HttpTrigger::new(&app, "127.0.0.1:80".parse().unwrap(), None)?; | ||
| let trigger = HttpTrigger::new(&app, "127.0.0.1:80".parse().unwrap(), None, true)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to end up on a random port here? I am not sure if the testing infra will be happy with that. Although I guess it should never happen. But it might be better for it to fail here if it did.
crates/trigger-http/src/server.rs
Outdated
| Some(listener) => listener, | ||
| None => { | ||
| return Err(anyhow::anyhow!( | ||
| "All retries failed. Unable to bind to a free port" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the user point of view, they told us to find a free port, not to "retry" anything, so I'd suggest we re-word this error message in those terms e.g.
| "All retries failed. Unable to bind to a free port" | |
| "Couldn't find a free port in the range {min}-{max}. Consider retrying with a different base port." |
or something like that?
crates/trigger-http/src/server.rs
Outdated
| } | ||
| } | ||
|
|
||
| match found_listener { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can achieve the same effect with found_listener.ok_or_else(...)? - your call which you think is more readable!
crates/trigger-http/src/server.rs
Outdated
| for _ in 1..=9 { | ||
| addr.set_port(addr.port() + 1); | ||
|
|
||
| if !addr.port() == u16::MAX { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ! operator here is bitwise complement. Is that what you intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I missed that, was initially trying a while loop. Would fix and push a fix now
itowlson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the updates! I think the logic is sound (except for one nitty detail of an error message which is not a blocker), but I remain really troubled by the long nameless block containing the search logic. It is long enough and nested enough that I find it really hard to be confident about following the logic, and in particular to visually match open and close braces to know what scope any given line is in, especially towards the end of the block. I've suggested a way that seems a lot clearer to me (and that, I think, helps with the nitty error thing), but I'm not wedded to it, and it doesn't fully solve the problem: if you have better ideas then go for it. But I do think it would be desirable to do something unless it turns out impractical. Which it sometimes does, so please push back if it's going to be a big pain!
crates/trigger-http/src/server.rs
Outdated
| } | ||
| Err(err) => { | ||
| if err.kind() == ErrorKind::AddrInUse { | ||
| continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's still value in breaking out a helper. The long and complicated if and loop make it very hard (at least for me) to see what is basically simple "if it succeeds, use it; if it doesn't, search or bail." Consider e.g.
let listener = match TcpListener::bind(self.listen_addr).await {
Ok(listener) => listener,
Err(err) if self.find_free_port && err.kind() == ErrorKind::AddrInUse => self.search_for_free_port().await?,
Err(err) => anyhow::bail!(...)
}
fn search_for_free_port(&self) -> anyhow::Result<TcpListener> {
// declare mutables
// loop
// ok or error
}
There's probably more breaking out you could do but to me this greatly clarifies the top-level logic, and gives a meaningful name to that long chunk for which I currently have to infer the intention from the code.
crates/trigger-http/src/server.rs
Outdated
| found_listener.ok_or_else(|| anyhow::anyhow!( | ||
| "Couldn't find a free port in the range {}-{}. Consider retrying with a different base port.", | ||
| self.listen_addr.port(), | ||
| self.listen_addr.port() + 9 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate magic number (with line 157). Make this a const, or if you adopt the helper function, it could even be a parameter passed to the helper.
crates/trigger-http/src/server.rs
Outdated
| self.listen_addr.port() + 9 | ||
| ))? | ||
| } else { | ||
| anyhow::bail!("Unable to listen on {}. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It occurs to me this can happen even if they passed --find-free-port (if the error was something other than AddrInUse) (at least I think it can, I'm finding it tricky to visually match which if or case we're in here). So we probably need to distinguish those cases I'm afraid - sorry for the bad steer.
Also, we should surface what the actual error is, because there are few things more maddening than being told
"computer says no" and not being told why.
If you decide to go with my suggestion about breaking out the loop logic, this might be fairly easy e.g.
match TcpListener::bind(self.listen_addr).await {
Ok(listener) => listener,
Err(err) if err.kind() == ErrorKind::AddrInUse && self.find_free_port => self.search_for_free_port().await?,
Err(err) if err.kind() == ErrorKind::AddrInUse => anyhow::bail!(/* use --find-free-port */),
Err(err) => anyhow::bail!(/* --find-free-port wouldn't have helped, just report the error */)
}
but not sure, would need some testing and another pair of eyes for sure!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be easier to follow if the find_free_port check moved to the outside, e.g.
let listener = if self.find_free_port {
// try 10 times here
} else {
// only try once here
TcpListener::bind(self.listen_addr)...
};That would make it easier to have the error messages make sense in the two different cases.
crates/trigger-http/src/server.rs
Outdated
| Err(err) => { | ||
| if err.kind() == ErrorKind::AddrInUse { | ||
| continue; | ||
| } | ||
| anyhow::bail!("Unable to listen on {}", self.listen_addr); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little confusing to use the original port in this error message and we should include the source error.
Separately/optionally, you could move the if/continue out to a match guard clause:
| Err(err) => { | |
| if err.kind() == ErrorKind::AddrInUse { | |
| continue; | |
| } | |
| anyhow::bail!("Unable to listen on {}", self.listen_addr); | |
| } | |
| Err(err) if err.kind() == ErrorKind::AddrInUse => { | |
| // Try the next port | |
| } | |
| Err(err) => { | |
| anyhow::bail!("Unable to listen on {addr}: {err:?}"); | |
| } |
crates/trigger-http/src/server.rs
Outdated
| if err.kind() == ErrorKind::AddrInUse { | ||
| anyhow::anyhow!("Unable to listen on {}. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr) | ||
| } else { | ||
| anyhow::anyhow!("Unable to listen on {}", self.listen_addr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still feel like we are dropping the actual error here - am I missing something?
crates/trigger-http/src/server.rs
Outdated
| ); | ||
| } | ||
|
|
||
| addr.set_port(addr.port() + 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the ordering here will cause us to start searching at requested port plus 1, rather than at the requested port, even if the requested port is free. Am I misreading?
crates/trigger-http/src/server.rs
Outdated
| } else { | ||
| TcpListener::bind(self.listen_addr).await.map_err(|err| { | ||
| if err.kind() == ErrorKind::AddrInUse { | ||
| anyhow::anyhow!("Unable to listen on {}. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It occurs to me that since we now know the kind of error we can reword this to e.g.
| anyhow::anyhow!("Unable to listen on {}. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr) | |
| anyhow::anyhow!("{} is already in use. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr) |
itowlson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for persevering with this! Looks good!
c589818 to
3f9c2ae
Compare
Signed-off-by: Aminu 'Seun Joshua <[email protected]> Signed-off-by: Aminu 'Seun Joshua <[email protected]> approach revamp using --find-free-port flag Signed-off-by: Aminu 'Seun Joshua <[email protected]> Signed-off-by: Aminu 'Seun Joshua <[email protected]> improvement: readability and refactor Signed-off-by: Aminu 'Seun Joshua <[email protected]> using bail instead Signed-off-by: Aminu 'Seun Joshua <[email protected]> fix: bitwise logic error Signed-off-by: Aminu 'Seun Joshua <[email protected]> refactor Signed-off-by: Aminu 'Seun Joshua <[email protected]> refactor: cleaner logic Signed-off-by: Aminu 'Seun Joshua <[email protected]> refactor: re-wording + logic Signed-off-by: Aminu 'Seun Joshua <[email protected]>
3f9c2ae to
dfbef34
Compare
Aims to resolve issue #2470
Custom port can be provided in CLI, optional though