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

Fix port conflict when running multiple unit tests in parallel #220

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

cretzel
Copy link

@cretzel cretzel commented Oct 2, 2019

The "random" port selection can still lead to port conflicts, when running multiple unit tests in parallel.

This is because port selection and check is done within the config validation phase, but the actual port binding is done later when the hoverfly process is started. Example:

  1. Thread A selects free port, eg. 49999 (Port is not bound yet)
  2. Thread B selects same port
  3. Thread A starts hoverfly process on port 49999 (Port is bound now)
  4. Thread B starts hoverfly process on port 49999 -> IllegalStateException

Fixed by moving port selection from validation to Hoverfly.startHoverflyProcess and synchronizing on Hoverfly.class.

This required some refactoring, mainly because the HoverflyClient cannot be instantiated in constructor, because the actuall port is not known yet. HoverflyClient instantiation moved to startHoverflyProcess. Introduced HoverflyClientFactory for testability.

@tommysitu tommysitu self-requested a review October 4, 2019 09:41
Copy link
Member

@tommysitu tommysitu left a comment

Choose a reason for hiding this comment

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

Thank you for the PR! However, the solution can be simplified and without breaking the existing API.

Comment on lines +722 to +724
HoverflyClientFactory clientFactory = mock(HoverflyClientFactory.class);
when(clientFactory.createHoverflyClient(any(HoverflyConfiguration.class))).thenReturn(hoverflyClient);
Whitebox.setInternalState(hoverfly, "hoverflyClientFactory", clientFactory);
Copy link
Member

Choose a reason for hiding this comment

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

Mocking hoverflyClientFactory seems redundant because you are not verifying its behaviours, and complicating some unit tests which now requires setting up:

when(hoverflyClient.getHealth()).thenReturn(true);
hoverfly.start();

HoverflyClient hoverflyClient = createMockHoverflyClient(hoverfly);
when(hoverflyClient.getHealth()).thenReturn(true);
Copy link
Member

@tommysitu tommysitu Oct 4, 2019

Choose a reason for hiding this comment

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

This test becomes harder to understand and is testing both hoverfly.start() and hoverfly.resetMode(). We should just mock HoverflyClient only to keep test simple and less coupled to implementation details:

Whitebox.setInternalState(hoverfly, "hoverflyClient", hoverflyClient);

Comment on lines -97 to -154
/**
* Static factory method for creating a {@link Builder}
* @return a builder for HoverflyClient
*/
static Builder custom() {
return new Builder();
}

/**
* Static factory method for default Hoverfly client
* @return a default HoverflyClient
*/
static HoverflyClient createDefault() {
return new Builder().build();
}

/**
* HTTP client builder for Hoverfly admin API
*/
class Builder {

private String scheme = HoverflyConstants.HTTP;
private String host = HoverflyConstants.LOCALHOST;
private int port = HoverflyConstants.DEFAULT_ADMIN_PORT;
private String authToken = null;

Builder() {
}

public Builder scheme(String scheme) {
this.scheme = scheme;
return this;
}

public Builder host(String host) {
this.host = host;
return this;
}

public Builder port(int port) {
this.port = port;
return this;
}

/**
* Get token from environment variable "HOVERFLY_AUTH_TOKEN" to authenticate with admin API
* @return this Builder for further customizations
*/
public Builder withAuthToken() {
this.authToken = System.getenv(HoverflyConstants.HOVERFLY_AUTH_TOKEN);
return this;
}

public HoverflyClient build() {
return new OkHttpHoverflyClient(scheme, host, port, authToken);
}
}

Copy link
Member

@tommysitu tommysitu Oct 4, 2019

Choose a reason for hiding this comment

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

We should try not to remove public method hoverfly-java users depends on. For example, people may do the following to create a hoverfly client:

HoverflyClient.createDefault()

Why not create a new builder method to set config from a HoverflyConfiguration object, something like this:

HoverflyClient.custom().withConfig(configuration);

.scheme(hoverflyConfig.getScheme())
.host(hoverflyConfig.getHost())
.port(hoverflyConfig.getAdminPort())
.withAuthToken()
Copy link
Member

Choose a reason for hiding this comment

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

withAuthToken() is optional. It's better to expose the builder for the user to customize the client:

HoverflyClient.custom().withConfig(configuration).withAuthToken();

Comment on lines +169 to +176
if (hoverflyConfig.getProxyPort() == 0) {
hoverflyConfig.setProxyPort(findUnusedPort());
}
if (hoverflyConfig.getAdminPort() == 0) {
hoverflyConfig.setAdminPort(findUnusedPort());
}
checkPortInUse(hoverflyConfig.getProxyPort());
checkPortInUse(hoverflyConfig.getAdminPort());
Copy link
Member

Choose a reason for hiding this comment

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

We should move any port checking and setting right before the start of hoverfly, to avoid synchronizing the whole method, only synchronizing the following is what you wanted (in pseudocode):

allOtherSetup()

synchronized() {
    findAndSetPort()
    checkPortInUse()
    setPortToCommandArgs()
    startHoverfly()
}

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.

3 participants