since it was closed without any understanding of the actual problem (oops, I guess I was on the wrong path). I'm writing a client library around an GraphQL server, in order to test, I'm spinning up my own GraphQL server in java. If I was using WebClient this wouldn't be a problem, but since it's graphql, I'm using apollo-android, as a base.
The facade injects the http client into a repository implementation that uses Spring Data like interfaces. This works if you know the server url ahead of time, but if you're configured to use a RANDOM_PORT it seems impossible.
I expected this to work,
@Configuration // @TestConfiguration also tried without profiles
class ApolloConfig {
@Bean
@Profile( "!test" )
static ApolloClient prodClient( @Value( "${phdb.endpoint}" ) String graphqlEndpoint ) {
return ApolloClient.builder().serverUrl( graphqlEndpoint ).build();
}
@Bean
@Profile( "test" )
static ApolloClient testClient(@Value( "${local.server.port}" ) int port ) {
return ApolloClient.builder().serverUrl( "http://localhost:" + port + "/graphql" ).build();
}
}
@Component
class EMRRepoImpl implements EMRRepo {
private final ApolloClient client;
EMRRepoImpl( ApolloClient client ) {
this.client = Objects.requireNonNull( client );
}
@Override
public Single<EmrSecureInsert> save( @NonNull EmrSecureInsert entity ) {
return Rx2Apollo.from( client.mutate( from( entity ) ) )
.singleOrError()
.map( Response::data )
.map( CreateEmrSecureInputMutation.Data::createEmrSecureInsert )
.map( CreateEmrSecureInputMutation.CreateEmrSecureInsert::emrSecureInsert )
.map( EMRRepoImpl::to );
}
}
@SpringJUnitConfig
@SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT )
class EMRRepoTest {
private final EMRRepo repo;
@Autowired
EMRRepoTest( EMRRepo repo ) {
this.repo = repo;
}
}
but it won't because of when local.server.port is created, now I think it may be possible to create that value sooner with some refactoring... but presuming it's not... then I believe another method is needed so that one can inject client implementations properly. I actually don't expect that my code should have to be significantly magnified. I'm using Apollo in this case, but you could replace it with the Apache Commons HTTP Components, or the Java Http Implementation.
prior issue, but thinking local.server.port should work differently #14877
current workaround is to use a static port
@SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = {"server.port=18080"} )
I don't think the snippets that you've provided change anything about the responses already given in #14877. We can't provide the actual port until the server has started. We also can't reserve a port earlier than that because the OS can assign it to someone else (we've had many many issues with SocketUtils.findAvailableTcpPort().
If you want to know the actual running port you need to wait until the container has started before calling Environment.getProperty("local.server.port") or alternatively use listen for a ServletWebServerInitializedEvent and call getWebServer().getPort() on the event.
Hi, thanks for that explanation, it actually explains why it can't work, and would give me a place to start looking if I wanted to take the time to implement it as it helps me understand current decisions.
It might be possible to make it easier to inject, but I'm not sure how. Perhaps we could offer some kind of LocalServerPort supplier that can be injected early but doesn't actually get the value until .get() is called. Then again, that might just end up looking like the Environment.
It's unfortunately one of those problems that's harder to solve than it first appears. Most people assume that you can reserve a port early and just use it later. Unfortunately that's not the case and you get port clashes (especially in Docker).
well... on top of that, I haven't found a great way to make it so that ApolloClient isn't actually built until later... I tried making it a @Lazy Proxy, but that didn't work (It doesn't want to be proxied...) (you get port clashes in docker? that's confusing... as 2 containers allow you to have the same port inside each, or are you referring to exposed ports)
It was actually our CI machine, perhaps it wasn't Docker. I can't quite remember. What would happen is we'd ask for a port using SocketUtils.findAvailableTcpPort(). This would give a port, but obviously nothing is actually running on it. Then we'd give that port to Tomcat to use, but in the meantime something else on the VM has called SocketUtils.findAvailableTcpPort() and the OS has given out the same port again. By the time Tomcat starts, the port is in use.
This may be an OS specific question or involve details, but why can't you just "reserve" the port? like before tomcat takes it? find port, open socket, hand off to reader (similar to what is suggested the behavior of daemons should be by systemd (love it or hate it))
There's no API (or at least no API that I've found in Java) that says "give me a port, and don't let anyone else use it for X minutes". The only way I know of to stop a port being reused is to keep it open and in use. If the port is in use, Tomcat can't get it. That's why you must tell Tomcat itself to grab an open port.
hmm...
@TestConfiguration
static class ApolloConfig {
@Bean
@Lazy
@Scope( proxyMode = ScopedProxyMode.INTERFACES)
static ApolloClient client( Environment env ) {
return ApolloClient.builder()
.serverUrl( "http://localhost:" + env.getProperty( "local.server.port" ) + "/graphql" )
.build();
}
}
that may be a functional enough solution for now... maybe if I go gripe at the client guys... or spring core upstream... the concrete class is final, and interfaces only work by virtue of the fact that I only need mutation atm... though I'm not sure that spring-core could do anything as long as the class is final...
The bean 'client' could not be injected as a 'com.apollographql.apollo.ApolloClient' because it is a JDK dynamic proxy that implements:
com.apollographql.apollo.ApolloQueryCall$Factory
com.apollographql.apollo.ApolloMutationCall$Factory
com.apollographql.apollo.ApolloPrefetch$Factory
com.apollographql.apollo.ApolloSubscriptionCall$Factory
And of course Tomcat (etc) won't allow you to hand them an open socket, thereby inverting the dependency...
For those who also suffered from this accident. This works for me:
@Configuration
public class Config {
@Bean
public FactoryBean<ApolloClient> apolloClientFactory(Environment env) {
return new FactoryBean<>() {
@Override
public ApolloClient getObject() {
return ApolloClient.builder()
.okHttpClient(new OkHttpClient.Builder().build())
.serverUrl(String.format("http://localhost:%s/graphql", env.getProperty("local.server.port")))
.build();
}
@Override
public Class<?> getObjectType() {
return ApolloClient.class;
}
};
}
}
@Component
@RequiredArgsConstructor
public class EventApiGraphqlStepDef {
private final ApolloClient apolloClient;
}
Most helpful comment
For those who also suffered from this accident. This works for me: