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

Bind PackageServer to ephemeral port to avoid port conflicts #227

Merged
merged 1 commit into from
Mar 13, 2024

Conversation

translatenix
Copy link
Contributor

@translatenix translatenix commented Feb 22, 2024

Here is my take on how to best solve the "flaky PackageServer tests" problem.
It is ready to be merged after #217 .
See commit message for details.
If you prefer a different solution, perhaps this can serve as inspiration.

@translatenix
Copy link
Contributor Author

@bioball Rebased and ready for review.

Copy link
Contributor

@bioball bioball left a comment

Choose a reason for hiding this comment

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

Overall, this is looking great! I have some minor comments.

.single()
.int()
.default(-1)
.validate { it in 1024..65535 }
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't seem very useful to validate given it's a test flag, and it's also provisioned by the OS. Do we know it's going to be in this range?

Suggested change
.validate { it in 1024..65535 }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not terribly important to have this validated, but ephemeral ports are guaranteed to be high ports (1024-65535).
https://en.wikipedia.org/wiki/Ephemeral_port#Range

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha, thanks for the link. Let's still remove this, though; it adds some unnecessary brittleness to our tests.

*
* <p>If set, requests that specify port 12110 are rewritten to use the given port. This enables
* users of this client to connect to a test server without having to know its (ephemeral)
* listening port.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* listening port.
* listening port.
*
* <p>This is an internal option used for testing purposes only.

Copy link
Contributor Author

@translatenix translatenix Mar 8, 2024

Choose a reason for hiding this comment

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

Calling this option "internal" felt strange because HttpClient is a public API and we don't have a way to hide testPort. It's also harmless. Should replace 12210 with 0 in a follow-up PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's the same with org.pkl.commons.cli.CliBaseOptions; both are public APIs but expose flags that are only meant for test purposes (e.g. testMode, testPort added by this PR) and we call them "internal" in the doc comments.

uri.getQuery(),
uri.getFragment());
} catch (URISyntaxException e) {
throw new AssertionError(e); // only port changed
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
throw new AssertionError(e); // only port changed
throw PklBugException.unreachableCode(); // only port changed

Copy link
Contributor Author

@translatenix translatenix Mar 8, 2024

Choose a reason for hiding this comment

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

Done. org.pkl.core.util depending on org.pkl.core seems problematic. But since PklBugException is already used in org.pkl.core.util, it's not making things worse.

@@ -49,6 +50,7 @@ public class ExecutorOptions2 extends ExecutorOptions {
* @param projectDir API equivalent of the {@code --project-dir} CLI option.
* @param certificateFiles API equivalent of the {@code --ca-certificates} CLI option
* @param certificateUris API equivalent of the {@code --ca-certificates} CLI option
* @param testPort API equivalent of the {@code --test-port} CLI option
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @param testPort API equivalent of the {@code --test-port} CLI option
* @param testPort Internal option used for testing purposes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed this param from the public API.

@@ -62,7 +64,8 @@ public ExecutorOptions2(
/* @Nullable */ Path moduleCacheDir,
/* @Nullable */ Path projectDir,
List<Path> certificateFiles,
List<URI> certificateUris) {
List<URI> certificateUris,
int testPort) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since testPort is an internal test-only option, we should have another constructor that doesn't have it as an option, and sets testPort to -1.

Copy link
Contributor Author

@translatenix translatenix Mar 8, 2024

Choose a reason for hiding this comment

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

Should probably address the root cause and provide a builder for ExecutorOptions2. For example, almost nobody will want to pass certificates either.

I thought about modernizing the pkl-executor API to look more like Evaluator and language bindings. But then I realized that my time is better spent on Java language bindings, which have the potential to make pkl-executor unnecessary.

Copy link
Contributor Author

@translatenix translatenix Mar 8, 2024

Choose a reason for hiding this comment

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

Added a builder and removed testPort from public API. See commit message for details.

@translatenix
Copy link
Contributor Author

@bioball Ready from my side. See latest commit message for details. Quite happy with the cleanup.

Copy link
Contributor

@bioball bioball left a comment

Choose a reason for hiding this comment

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

Just a couple minor comments, but this is pretty close!

Comment on lines +66 to +69
public static Builder builder() {
return new Builder();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Good idea with the builder!

We have an existing pattern of having unconfigured() and preconfigured() builders; see EvaluatorBuilder, ConfigEvaluatorBuilder, ValueMapperBuilder, etc. I think let's follow that.

Suggested change
public static Builder builder() {
return new Builder();
}
// keep in sync with SecurityManagers.defaultAllowedModules
/// Returns the allowed module patterns that the CLI uses by default.
public static final List<String> defaultAllowedModules =
List.of(
"repl:",
"file:",
"jar:file:",
"modulepath:",
"https:",
"pkl:",
"package:",
"projectpackage:"
);
// keep in sync with SecurityManagers.defaultAllowedResources
/// Returns the allowed resources patterns that the CLI uses by default.
public static final List<String> defaultAllowedResources =
List.of(
"prop:",
"env:",
"file:",
"modulepath:",
"package:",
"projectpackage:",
"https:");
/**
* Creates a builder that has no options set.
*/
public static Builder unconfigured() {
return new Builder();
}
/**
* Creates a builder configured with:
*
* <ul>
* <li>{@link #defaultAllowedModules}</li>
* <li>{@link #defaultAllowedResources}</li>
* <li>System environment variables</li>
* <li>System properties</li>
* </ul>
*/
public static Builder preconfigured() {
//noinspection unchecked,rawtypes
return unconfigured()
.allowedModules(defaultAllowedModules)
.allowedResources(defaultAllowedResources)
.environmentVariables(System.getenv())
.externalProperties((Map) System.getProperties());
}

.single()
.int()
.default(-1)
.validate { it in 1024..65535 }
Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha, thanks for the link. Let's still remove this, though; it adds some unnecessary brittleness to our tests.

*
* <p>If set, requests that specify port 12110 are rewritten to use the given port. This enables
* users of this client to connect to a test server without having to know its (ephemeral)
* listening port.
Copy link
Contributor

Choose a reason for hiding this comment

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

It's the same with org.pkl.commons.cli.CliBaseOptions; both are public APIs but expose flags that are only meant for test purposes (e.g. testMode, testPort added by this PR) and we call them "internal" in the doc comments.

@translatenix
Copy link
Contributor Author

We have an existing pattern of having unconfigured() and preconfigured() builders; see EvaluatorBuilder, ConfigEvaluatorBuilder, ValueMapperBuilder, etc. I think let's follow that.

According to the Javadoc, Executor "evaluates Pkl modules in a sandbox".
Does preconfiguring with System.getenv() and System.getProperties() make sense here?
And shouldn't granting file system and network access be an explicit decision?

Apart from adding unconfigured()/preconfigured(),
we could add builder().grantFileSystemAccess(rootDir).grantNetworkAccess().
Or we could stick to a basic builder for now.
Let me know how to proceed.

Let's still remove this, though; it adds some unnecessary brittleness to our tests.

Removed.
Just to clarify, this validation can't possibly cause brittleness. It prevents brittleness by catching the mistake of picking a well-known port as test port. Operating systems would never pick a well-known port as ephemeral port.

both are public APIs but expose flags that are only meant for test purposes (e.g. testMode, testPort added by this PR) and we call them "internal" in the doc comments.

There is a big difference: testMode is only useful for testing Pkl itself, whereas testPort is equally useful for testing third-party code that uses CliBaseOptions or HttpClient. But I won't fight over this one.

@bioball
Copy link
Contributor

bioball commented Mar 13, 2024

According to the Javadoc, Executor "evaluates Pkl modules in a sandbox".
Does preconfiguring with System.getenv() and System.getProperties() make sense here?
And shouldn't granting file system and network access be an explicit decision?

I don't see any issues with the set of options provided by preconfigured(). This is an opt-in behavior, and users are expected to know what preconfigured() means. It's actually not that different from using our evaluator in pkl-core (which is also a sandbox).

@translatenix
Copy link
Contributor Author

translatenix commented Mar 13, 2024

I don't see any issues with the set of options provided by preconfigured(). This is an opt-in behavior, and users are expected to know what preconfigured() means. It's actually not that different from using our evaluator in pkl-core (which is also a sandbox).

What is Executor used for at Apple? It seems to be intended for different use cases than (say) ConfigEvaluator, where exposing process env vars is a sensible default. What also worries me about preconfigured()/unconfigured() is that it's all or nothing, and that without adding many additional methods, preconfigured() values are impossible to customize. On the other hand, a method such as grantNetworkAccess() is naturally incremental.

@translatenix translatenix force-pushed the package-server branch 2 times, most recently from d32d02c to 0cb0030 Compare March 13, 2024 04:37
This is a comprehensive solution to the "flaky PackageServer tests"
problem. It rules out port conflicts and imposes no limits on test
parallelism. The same solution can be used for other test servers
in the future.

Major changes:
- Turn `PackageServer` from a singleton into a class that is
  instantiated per test class or test method.
- Start the server the first time its `port` property is read.
  Bind the server to an ephemeral port instead of port 12110.
- For every test that uses `PackageServer`, pass the server port to
  `--test-port`, `HttpClient.Builder.setTestPort`, the `CliBaseOptions`
  or `ExecutorOptions` constructor, or the Gradle plugin's `testPort` property.
  Wire all of these to `RequestRewritingClient`'s `testPort` constructor parameter.
- Enhance `RequestRewritingClient` to replace port 12110 with `testPort`
  in request URIs unless `testPort` is -1 (its default).
- Introduce `ExecutorOptions.Builder`.
  This makes executor options more comfortable to create
  and allows to hide options such as `testPort`.
- Deprecate the `ExecutorOptions` constructor to steer users towards the builder.
- Get rid of `ExecutorOptions2`, which is no longer needed.
- Clean up `EmbeddedExecutorTest` with the help of the builder.
@translatenix
Copy link
Contributor Author

translatenix commented Mar 13, 2024

I've made 2 out of 3 requested changes and have squashed my commits. I'll leave any builder changes to you.

I've also rebased my Gradle 8.6 PR, which I'd love to land by the end of the week.

@bioball
Copy link
Contributor

bioball commented Mar 13, 2024

The executor API is typically meant for services that need to evaluate 3rd party Pkl code. For instance, if you are a service that accepts Pkl files as configuration. And, like you said, in those cases, you wouldn't want to pass your own env vars or system properties to Pkl.

The core thing that the executor provides that ConfigEvaluator (or just Evaluator) don't is the ability to use multiple different Pkl versions for evaluation. The rest of the sandboxing is already part of Evaluator/ConfigEvaluator (e.g. allowed modules, allowed resources).

Thought about this a little more--yeah, there's probably not much of a use-case for ExecutorOptions#preconfigured(). I'm okay merging this as-is.

@bioball bioball merged commit 014b3a8 into apple:main Mar 13, 2024
5 checks passed
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.

2 participants