Browse Source

Fixes issues in HealthCheckServiceInstanceListSupplier (#685)

- WebClient response leaks
- potential Scheduled task leak
- rework polling to plain reactor operators
- remove non atomic operations on ServiceInstance lists

related to gh-629
fixes issues from gh-683
pull/705/head
robotmrv 5 years ago committed by GitHub
parent
commit
6db6a81d82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      spring-cloud-loadbalancer/pom.xml
  2. 119
      spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplier.java
  3. 404
      spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java

5
spring-cloud-loadbalancer/pom.xml

@ -88,5 +88,10 @@
<artifactId>reactor-test</artifactId> <artifactId>reactor-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

119
spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplier.java

@ -16,18 +16,19 @@
package org.springframework.cloud.loadbalancer.core; package org.springframework.cloud.loadbalancer.core;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import reactor.core.Disposable;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -40,10 +41,11 @@ import org.springframework.web.util.UriComponentsBuilder;
* {@link WebClient} to ping the <code>health</code> endpoint of the instances. * {@link WebClient} to ping the <code>health</code> endpoint of the instances.
* *
* @author Olga Maciaszek-Sharma * @author Olga Maciaszek-Sharma
* @author Roman Matiushchenko
* @since 2.2.0 * @since 2.2.0
*/ */
public class HealthCheckServiceInstanceListSupplier public class HealthCheckServiceInstanceListSupplier
implements ServiceInstanceListSupplier { implements ServiceInstanceListSupplier, InitializingBean, DisposableBean {
private static final Log LOG = LogFactory private static final Log LOG = LogFactory
.getLog(HealthCheckServiceInstanceListSupplier.class); .getLog(HealthCheckServiceInstanceListSupplier.class);
@ -56,50 +58,72 @@ public class HealthCheckServiceInstanceListSupplier
private final String defaultHealthCheckPath; private final String defaultHealthCheckPath;
private List<ServiceInstance> instances = Collections private final Flux<List<ServiceInstance>> aliveInstancesReplay;
.synchronizedList(new ArrayList<>());
private List<ServiceInstance> healthyInstances = Collections private Disposable healthCheckDisposable;
.synchronizedList(new ArrayList<>());
public HealthCheckServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, public HealthCheckServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
LoadBalancerProperties.HealthCheck healthCheck, WebClient webClient) { LoadBalancerProperties.HealthCheck healthCheck, WebClient webClient) {
this.delegate = delegate; this.delegate = delegate;
this.healthCheck = healthCheck; this.healthCheck = healthCheck;
defaultHealthCheckPath = healthCheck.getPath().getOrDefault("default", this.defaultHealthCheckPath = healthCheck.getPath().getOrDefault("default",
"/actuator/health"); "/actuator/health");
this.webClient = webClient; this.webClient = webClient;
initInstances(); this.aliveInstancesReplay = Flux.defer(delegate)
.delaySubscription(Duration.ofMillis(this.healthCheck.getInitialDelay()))
.switchMap(serviceInstances -> healthCheckFlux(serviceInstances)
.map(alive -> Collections.unmodifiableList(new ArrayList<>(alive)))
)
.replay(1)
.refCount(1);
} }
private void initInstances() { @Override
delegate.get().subscribe(delegateInstances -> { public void afterPropertiesSet() {
instances.clear(); Disposable healthCheckDisposable = this.healthCheckDisposable;
instances.addAll(delegateInstances); if (healthCheckDisposable != null) {
}); healthCheckDisposable.dispose();
}
Flux<List<ServiceInstance>> healthCheckFlux = healthCheckFlux(); this.healthCheckDisposable = aliveInstancesReplay.subscribe();
healthCheckFlux.subscribe(verifiedInstances -> {
healthyInstances.clear();
healthyInstances.addAll(verifiedInstances);
});
} }
protected Flux<List<ServiceInstance>> healthCheckFlux() { protected Flux<List<ServiceInstance>> healthCheckFlux(List<ServiceInstance> instances) {
return Flux.create(emitter -> Schedulers return Flux.defer(() -> {
.newSingle("Health Check Verifier: " + getServiceId(), true) List<Mono<ServiceInstance>> checks = new ArrayList<>(instances.size());
.schedulePeriodically(() -> { for (ServiceInstance instance : instances) {
List<ServiceInstance> verifiedInstances = new ArrayList<>(); Mono<ServiceInstance> alive = isAlive(instance).onErrorResume(error -> {
Flux.fromIterable(instances).filterWhen(this::isAlive) if (LOG.isDebugEnabled()) {
.subscribe(serviceInstance -> { LOG.debug(String.format(
verifiedInstances.add(serviceInstance); "Exception occurred during health check of the instance for service %s: %s",
emitter.next(verifiedInstances); instance.getServiceId(), instance.getUri()), error);
}); }
}, healthCheck.getInitialDelay(), healthCheck.getInterval().toMillis(), return Mono.empty();
TimeUnit.MILLISECONDS), })
FluxSink.OverflowStrategy.LATEST); .timeout(this.healthCheck.getInterval(), Mono.defer(() -> {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format(
"The instance for service %s: %s did not respond for %s during health check",
instance.getServiceId(), instance.getUri(),
this.healthCheck.getInterval()));
}
return Mono.empty();
}))
.handle((isHealthy, sink) -> {
if (isHealthy) {
sink.next(instance);
}
});
checks.add(alive);
}
List<ServiceInstance> result = new ArrayList<>();
return Flux.merge(checks).map(alive -> {
result.add(alive);
return result;
})
.defaultIfEmpty(result);
})
.repeatWhen(restart -> restart.delayElements(this.healthCheck.getInterval()));
} }
@Override @Override
@ -109,16 +133,7 @@ public class HealthCheckServiceInstanceListSupplier
@Override @Override
public Flux<List<ServiceInstance>> get() { public Flux<List<ServiceInstance>> get() {
if (!healthyInstances.isEmpty()) { return aliveInstancesReplay;
return Flux.defer(() -> Flux.fromIterable(healthyInstances).collectList());
}
// If there are no healthy instances, it might be better to still retry on all of
// them
if (LOG.isWarnEnabled()) {
LOG.warn(
"No verified healthy instances were found, returning all listed instances.");
}
return Flux.defer(() -> Flux.fromIterable(instances).collectList());
} }
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) { protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
@ -130,7 +145,17 @@ public class HealthCheckServiceInstanceListSupplier
.uri(UriComponentsBuilder.fromUri(serviceInstance.getUri()) .uri(UriComponentsBuilder.fromUri(serviceInstance.getUri())
.path(healthCheckPath).build().toUri()) .path(healthCheckPath).build().toUri())
.exchange() .exchange()
.map(clientResponse -> HttpStatus.OK.equals(clientResponse.statusCode())); .flatMap(clientResponse -> clientResponse.releaseBody()
.thenReturn(HttpStatus.OK.value() == clientResponse.rawStatusCode())
);
}
@Override
public void destroy() {
Disposable healthCheckDisposable = this.healthCheckDisposable;
if (healthCheckDisposable != null) {
healthCheckDisposable.dispose();
}
} }
} }

404
spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java

@ -16,8 +16,21 @@
package org.springframework.cloud.loadbalancer.core; package org.springframework.cloud.loadbalancer.core;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.assertj.core.api.Assertions;
import org.assertj.core.util.Lists;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -39,6 +52,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link HealthCheckServiceInstanceListSupplier}. * Tests for {@link HealthCheckServiceInstanceListSupplier}.
* *
* @author Olga Maciaszek-Sharma * @author Olga Maciaszek-Sharma
* @author Roman Matiushchenko
*/ */
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@SpringBootTest( @SpringBootTest(
@ -46,18 +60,37 @@ import static org.assertj.core.api.Assertions.assertThat;
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HealthCheckServiceInstanceListSupplierTests { class HealthCheckServiceInstanceListSupplierTests {
private static final String SERVICE_ID = "ignored-service";
private static final Duration VERIFY_TIMEOUT = Duration.ofSeconds(5);
@LocalServerPort @LocalServerPort
private int port; private int port;
private final WebClient webClient = WebClient.create(); private final WebClient webClient = WebClient.create();
private LoadBalancerProperties.HealthCheck healthCheck = new LoadBalancerProperties.HealthCheck(); private LoadBalancerProperties.HealthCheck healthCheck;
private HealthCheckServiceInstanceListSupplier listSupplier;
@BeforeEach
void setUp() {
healthCheck = new LoadBalancerProperties.HealthCheck();
}
@AfterEach
void tearDown() throws Exception {
if (listSupplier != null) {
listSupplier.destroy();
listSupplier = null;
}
}
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@Test @Test
void shouldCheckInstanceWithProvidedHealthCheckPath() { void shouldCheckInstanceWithProvidedHealthCheckPath() {
healthCheck.getPath().put("ignored-service", "/health"); healthCheck.getPath().put("ignored-service", "/health");
HealthCheckServiceInstanceListSupplier listSupplier = new HealthCheckServiceInstanceListSupplier( listSupplier = new HealthCheckServiceInstanceListSupplier(
ServiceInstanceListSupplier.FixedServiceInstanceListSupplier ServiceInstanceListSupplier.FixedServiceInstanceListSupplier
.with(new MockEnvironment()).build(), .with(new MockEnvironment()).build(),
healthCheck, webClient); healthCheck, webClient);
@ -72,7 +105,7 @@ class HealthCheckServiceInstanceListSupplierTests {
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@Test @Test
void shouldCheckInstanceWithDefaultHealthCheckPath() { void shouldCheckInstanceWithDefaultHealthCheckPath() {
HealthCheckServiceInstanceListSupplier listSupplier = new HealthCheckServiceInstanceListSupplier( listSupplier = new HealthCheckServiceInstanceListSupplier(
ServiceInstanceListSupplier.FixedServiceInstanceListSupplier ServiceInstanceListSupplier.FixedServiceInstanceListSupplier
.with(new MockEnvironment()).build(), .with(new MockEnvironment()).build(),
healthCheck, webClient); healthCheck, webClient);
@ -88,7 +121,7 @@ class HealthCheckServiceInstanceListSupplierTests {
@Test @Test
void shouldReturnFalseIfEndpointNotFound() { void shouldReturnFalseIfEndpointNotFound() {
healthCheck.getPath().put("ignored-service", "/test"); healthCheck.getPath().put("ignored-service", "/test");
HealthCheckServiceInstanceListSupplier listSupplier = new HealthCheckServiceInstanceListSupplier( listSupplier = new HealthCheckServiceInstanceListSupplier(
ServiceInstanceListSupplier.FixedServiceInstanceListSupplier ServiceInstanceListSupplier.FixedServiceInstanceListSupplier
.with(new MockEnvironment()).build(), .with(new MockEnvironment()).build(),
healthCheck, webClient); healthCheck, webClient);
@ -100,6 +133,369 @@ class HealthCheckServiceInstanceListSupplierTests {
assertThat(alive).isFalse(); assertThat(alive).isFalse();
} }
@Test
void shouldReturnOnlyAliveService() {
healthCheck.setInitialDelay(1000);
ServiceInstance si1 = new DefaultServiceInstance("ignored-service-1", SERVICE_ID,
"127.0.0.1", port, false);
ServiceInstance si2 = new DefaultServiceInstance("ignored-service-2", SERVICE_ID,
"127.0.0.2", port, false);
StepVerifier.withVirtualTime(() -> {
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID);
Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(si1, si2)));
HealthCheckServiceInstanceListSupplier mock = Mockito
.mock(HealthCheckServiceInstanceListSupplier.class);
Mockito.doReturn(Mono.just(true)).when(mock).isAlive(si1);
Mockito.doReturn(Mono.just(false)).when(mock).isAlive(si2);
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate,
healthCheck, webClient) {
@Override
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
return mock.isAlive(serviceInstance);
}
};
return listSupplier.get();
})
.expectSubscription()
.expectNoEvent(Duration.ofMillis(healthCheck.getInitialDelay()))
.expectNext(Lists.list(si1))
.expectNoEvent(healthCheck.getInterval())
.thenCancel()
.verify(VERIFY_TIMEOUT);
}
@Test
void shouldEmitOnEachAliveServiceInBatch() {
healthCheck.setInitialDelay(1000);
ServiceInstance si1 = new DefaultServiceInstance("ignored-service-1", SERVICE_ID,
"127.0.0.1", port, false);
ServiceInstance si2 = new DefaultServiceInstance("ignored-service-2", SERVICE_ID,
"127.0.0.2", port, false);
StepVerifier.withVirtualTime(() -> {
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID);
Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(si1, si2)));
HealthCheckServiceInstanceListSupplier mock = Mockito
.mock(HealthCheckServiceInstanceListSupplier.class);
Mockito.doReturn(Mono.just(true)).when(mock).isAlive(si1);
Mockito.doReturn(Mono.just(true)).when(mock).isAlive(si2);
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate,
healthCheck, webClient) {
@Override
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
return mock.isAlive(serviceInstance);
}
};
return listSupplier.get();
})
.expectSubscription()
.expectNoEvent(Duration.ofMillis(healthCheck.getInitialDelay()))
.expectNext(Lists.list(si1))
.expectNext(Lists.list(si1, si2))
.expectNoEvent(healthCheck.getInterval())
.thenCancel()
.verify(VERIFY_TIMEOUT);
}
@Test
void shouldNotFailIfIsAliveReturnsError() {
healthCheck.setInitialDelay(1000);
ServiceInstance si1 = new DefaultServiceInstance("ignored-service-1", SERVICE_ID,
"127.0.0.1", port, false);
ServiceInstance si2 = new DefaultServiceInstance("ignored-service-2", SERVICE_ID,
"127.0.0.2", port, false);
StepVerifier.withVirtualTime(() -> {
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID);
Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(si1, si2)));
HealthCheckServiceInstanceListSupplier mock = Mockito
.mock(HealthCheckServiceInstanceListSupplier.class);
Mockito.doReturn(Mono.just(true)).when(mock).isAlive(si1);
Mockito.doReturn(Mono.error(new RuntimeException("boom"))).when(mock)
.isAlive(si2);
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate,
healthCheck, webClient) {
@Override
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
return mock.isAlive(serviceInstance);
}
};
return listSupplier.get();
})
.expectSubscription()
.expectNoEvent(Duration.ofMillis(healthCheck.getInitialDelay()))
.expectNext(Lists.list(si1))
.expectNoEvent(healthCheck.getInterval())
.thenCancel()
.verify(VERIFY_TIMEOUT);
}
@Test
void shouldEmitAllInstancesIfAllIsAliveChecksFailed() {
healthCheck.setInitialDelay(1000);
ServiceInstance si1 = new DefaultServiceInstance("ignored-service-1", SERVICE_ID,
"127.0.0.1", port, false);
ServiceInstance si2 = new DefaultServiceInstance("ignored-service-2", SERVICE_ID,
"127.0.0.2", port, false);
StepVerifier.withVirtualTime(() -> {
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID);
Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(si1, si2)));
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate,
healthCheck, webClient) {
@Override
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
if (serviceInstance == si1) {
return Mono.just(false);
}
else {
return Mono.error(new RuntimeException("boom"));
}
}
};
return listSupplier.get();
})
.expectSubscription()
.expectNoEvent(Duration.ofMillis(healthCheck.getInitialDelay()))
.expectNext(Lists.list())
.expectNoEvent(healthCheck.getInterval())
.thenCancel()
.verify(VERIFY_TIMEOUT);
}
@Test
void shouldMakeInitialDaleyAfterPropertiesSet() {
healthCheck.setInitialDelay(1000);
ServiceInstance si1 = new DefaultServiceInstance("ignored-service-1", SERVICE_ID,
"127.0.0.1", port, false);
StepVerifier.withVirtualTime(() -> {
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID);
Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(si1)));
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate,
healthCheck, webClient) {
@Override
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
return Mono.just(true);
}
};
listSupplier.afterPropertiesSet();
return listSupplier.get();
})
.expectSubscription()
.expectNoEvent(Duration.ofMillis(healthCheck.getInitialDelay()))
.expectNext(Lists.list(si1))
.expectNoEvent(healthCheck.getInterval())
.thenCancel()
.verify(VERIFY_TIMEOUT);
}
@Test
void shouldRepeatIsAliveChecksIndefinitely() {
healthCheck.setInitialDelay(1000);
ServiceInstance si1 = new DefaultServiceInstance("ignored-service-1", SERVICE_ID,
"127.0.0.1", port, false);
ServiceInstance si2 = new DefaultServiceInstance("ignored-service-2", SERVICE_ID,
"127.0.0.2", port, false);
StepVerifier.withVirtualTime(() -> {
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID);
Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(si1, si2)));
HealthCheckServiceInstanceListSupplier mock = Mockito
.mock(HealthCheckServiceInstanceListSupplier.class);
Mockito.doReturn(Mono.just(false), Mono.just(true)).when(mock).isAlive(si1);
Mockito.doReturn(Mono.error(new RuntimeException("boom"))).when(mock)
.isAlive(si2);
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate,
healthCheck, webClient) {
@Override
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
return mock.isAlive(serviceInstance);
}
};
return listSupplier.get();
})
.expectSubscription()
.expectNoEvent(Duration.ofMillis(healthCheck.getInitialDelay()))
.expectNext(Lists.list())
.expectNoEvent(healthCheck.getInterval())
.expectNext(Lists.list(si1))
.expectNoEvent(healthCheck.getInterval())
.expectNext(Lists.list(si1))
.thenCancel().verify(VERIFY_TIMEOUT);
}
@Test
void shouldTimeoutIsAliveCheck() {
healthCheck.setInitialDelay(1000);
ServiceInstance si1 = new DefaultServiceInstance("ignored-service-1", SERVICE_ID,
"127.0.0.1", port, false);
StepVerifier.withVirtualTime(() -> {
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID);
Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(si1)));
HealthCheckServiceInstanceListSupplier mock = Mockito
.mock(HealthCheckServiceInstanceListSupplier.class);
Mockito.when(mock.isAlive(si1)).thenReturn(Mono.never(), Mono.just(true));
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate,
healthCheck, webClient) {
@Override
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
return mock.isAlive(serviceInstance);
}
};
return listSupplier.get();
})
.expectSubscription()
.expectNoEvent(Duration.ofMillis(healthCheck.getInitialDelay()))
.expectNoEvent(healthCheck.getInterval())
.expectNext(Lists.list())
.expectNoEvent(healthCheck.getInterval())
.expectNext(Lists.list(si1))
.expectNoEvent(healthCheck.getInterval())
.expectNext(Lists.list(si1))
.thenCancel().verify(VERIFY_TIMEOUT);
}
@Test
void shouldUpdateInstances() {
healthCheck.setInitialDelay(1000);
ServiceInstance si1 = new DefaultServiceInstance("ignored-service-1", SERVICE_ID,
"127.0.0.1", port, false);
ServiceInstance si2 = new DefaultServiceInstance("ignored-service-2", SERVICE_ID,
"127.0.0.2", port, false);
StepVerifier.withVirtualTime(() -> {
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID);
Flux<List<ServiceInstance>> instances = Flux.just(Lists.list(si1))
.concatWith(Flux.just(Lists.list(si1, si2))
.delayElements(healthCheck.getInterval().dividedBy(2)));
Mockito.when(delegate.get()).thenReturn(instances);
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate,
healthCheck, webClient) {
@Override
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
return Mono.just(true);
}
};
return listSupplier.get();
})
.expectSubscription()
.expectNoEvent(Duration.ofMillis(healthCheck.getInitialDelay()))
.expectNext(Lists.list(si1))
.thenAwait(healthCheck.getInterval().dividedBy(2))
.expectNext(Lists.list(si1))
.expectNext(Lists.list(si1, si2))
.expectNoEvent(healthCheck.getInterval())
.expectNext(Lists.list(si1))
.expectNext(Lists.list(si1, si2))
.thenCancel()
.verify(VERIFY_TIMEOUT);
}
@Test
void shouldCacheResultIfAfterPropertiesSetInvoked() {
healthCheck.setInitialDelay(1000);
ServiceInstance si1 = new DefaultServiceInstance("ignored-service-1", SERVICE_ID,
"127.0.0.1", port, false);
AtomicInteger emitCounter = new AtomicInteger();
StepVerifier.withVirtualTime(() -> {
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID);
Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(si1)));
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate,
healthCheck, webClient) {
@Override
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
return Mono.just(true);
}
@Override
protected Flux<List<ServiceInstance>> healthCheckFlux(List<ServiceInstance> instances) {
return super.healthCheckFlux(instances).doOnNext(it -> emitCounter.incrementAndGet());
}
};
listSupplier.afterPropertiesSet();
return listSupplier.get().take(1).concatWith(listSupplier.get().take(1));
})
.expectSubscription()
.expectNoEvent(Duration.ofMillis(healthCheck.getInitialDelay()))
.expectNext(Lists.list(si1))
.expectNext(Lists.list(si1))
.thenCancel()
.verify(VERIFY_TIMEOUT);
Assertions.assertThat(emitCounter).hasValue(1);
}
@Test
void shouldCancelSubscription() {
final AtomicInteger instancesCanceled = new AtomicInteger();
ServiceInstanceListSupplier delegate = Mockito
.mock(ServiceInstanceListSupplier.class);
Mockito.when(delegate.get())
.thenReturn(Flux.<List<ServiceInstance>>never()
.log("test")
.doOnCancel(instancesCanceled::incrementAndGet));
listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, healthCheck, webClient);
listSupplier.afterPropertiesSet();
Assertions.assertThat(instancesCanceled).hasValue(0);
listSupplier.destroy();
Awaitility.await()
.pollDelay(Duration.ofMillis(100)).atMost(VERIFY_TIMEOUT).untilAsserted(
() -> Assertions.assertThat(instancesCanceled).hasValue(1));
}
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration @EnableAutoConfiguration
@RestController @RestController

Loading…
Cancel
Save