use of com.couchbase.client.core.deps.io.netty.channel.Channel in project couchbase-jvm-clients by couchbase.
the class BaseEndpointIntegrationTest method mustReconnectWhenChannelCloses.
/**
* When the underlying channel closes, the endpoint must continue to reconnect until being instructed
* to stop with an explicit disconnect command.
*/
@Test
void mustReconnectWhenChannelCloses() {
LocalServerController localServerController = startLocalServer(eventLoopGroup);
ServiceContext serviceContext = new ServiceContext(new CoreContext(null, 1, env, authenticator()), "127.0.0.1", 1234, ServiceType.KV, Optional.empty());
BaseEndpoint endpoint = new BaseEndpoint("127.0.0.1", 1234, eventLoopGroup, serviceContext, CircuitBreakerConfig.enabled(false).build(), ServiceType.QUERY, false) {
@Override
protected PipelineInitializer pipelineInitializer() {
return (endpoint, pipeline) -> {
};
}
@Override
protected SocketAddress remoteAddress() {
return new LocalAddress("server");
}
};
List<EndpointState> transitions = Collections.synchronizedList(new ArrayList<>());
endpoint.states().subscribe(transitions::add);
assertEquals(0, localServerController.connectAttempts.get());
assertNull(localServerController.channel.get());
endpoint.connect();
waitUntilCondition(() -> endpoint.state() == EndpointState.CONNECTED);
waitUntilCondition(() -> localServerController.connectAttempts.get() == 1);
assertNotNull(localServerController.channel.get());
localServerController.channel.get().close().awaitUninterruptibly();
List<EndpointState> expectedTransitions = Arrays.asList(// initial state
EndpointState.DISCONNECTED, // initial connect attempt
EndpointState.CONNECTING, // properly connected the first time
EndpointState.CONNECTED, // disconnected when we kill the channel from the server side
EndpointState.DISCONNECTED, // endpoint should be reconnecting now
EndpointState.CONNECTING, // finally, we are able to reconnect completely
EndpointState.CONNECTED);
waitUntilCondition(() -> transitions.size() == expectedTransitions.size());
assertEquals(expectedTransitions, transitions);
waitUntilCondition(() -> localServerController.connectAttempts.get() >= 2);
endpoint.disconnect();
waitUntilCondition(() -> endpoint.state() == EndpointState.DISCONNECTED);
boolean hasDisconnectEvent = false;
for (Event event : eventBus.publishedEvents()) {
if (event instanceof UnexpectedEndpointDisconnectedEvent) {
hasDisconnectEvent = true;
break;
}
}
assertTrue(hasDisconnectEvent);
}
use of com.couchbase.client.core.deps.io.netty.channel.Channel in project couchbase-jvm-clients by couchbase.
the class BaseEndpointTest method disconnectOverridesConnectCompletion.
/**
* This test makes sure that even if we connect successfully, if there has been a
* disconnect signal in the meantime we need to properly close it all and not end
* in a connect-disconnect limbo.
*/
@Test
void disconnectOverridesConnectCompletion() {
final CompletableFuture<Channel> cf = new CompletableFuture<>();
InstrumentedEndpoint endpoint = InstrumentedEndpoint.create(eventLoopGroup, ctx, () -> Mono.fromFuture(cf));
endpoint.connect();
waitUntilCondition(() -> endpoint.state() == EndpointState.CONNECTING);
endpoint.disconnect();
EmbeddedChannel channel = new EmbeddedChannel();
cf.complete(channel);
waitUntilCondition(() -> endpoint.state() == EndpointState.DISCONNECTED);
assertTrue(eventBus.publishedEvents().size() >= 2);
assertTrue(eventBus.publishedEvents().get(0) instanceof EndpointConnectionIgnoredEvent);
assertTrue(eventBus.publishedEvents().get(1) instanceof EndpointDisconnectedEvent);
assertEquals("Endpoint disconnected successfully", eventBus.publishedEvents().get(1).description());
}
use of com.couchbase.client.core.deps.io.netty.channel.Channel in project couchbase-jvm-clients by couchbase.
the class BaseEndpointTest method retryOnTimeoutUntilEventuallyConnected.
/**
* This test fakes a situation where the channel future from netty would simply not return
* at all and time out, and the client would resubscribe. Then at some future attempt the
* future returns fine and we should end up in a connected state and ready to go.
*/
@Test
void retryOnTimeoutUntilEventuallyConnected() {
SimpleEventBus eventBus = new SimpleEventBus(true);
CoreEnvironment env = CoreEnvironment.builder().eventBus(eventBus).timeoutConfig(TimeoutConfig.connectTimeout(Duration.ofMillis(10))).build();
CoreContext coreContext = new CoreContext(mock(Core.class), 1, env, authenticator);
ServiceContext ctx = new ServiceContext(coreContext, LOCALHOST, 1234, ServiceType.KV, Optional.empty());
try {
final CompletableFuture<Channel> cf = new CompletableFuture<>();
InstrumentedEndpoint endpoint = InstrumentedEndpoint.create(eventLoopGroup, ctx, () -> Mono.fromFuture(cf));
endpoint.connect();
waitUntilCondition(() -> eventBus.publishedEvents().size() >= 3);
EmbeddedChannel channel = new EmbeddedChannel();
cf.complete(channel);
waitUntilCondition(() -> endpoint.state() == EndpointState.CONNECTED);
assertTrue(eventBus.publishedEvents().size() >= 3);
boolean failedFound = false;
boolean successFound = false;
for (Event event : eventBus.publishedEvents()) {
if (event instanceof EndpointConnectionFailedEvent) {
assertEquals(Event.Severity.WARN, event.severity());
assertEquals(Duration.ofMillis(10), event.duration());
failedFound = true;
} else if (event instanceof EndpointConnectedEvent) {
assertEquals(Event.Severity.DEBUG, event.severity());
assertTrue(event.duration().toNanos() > 0);
successFound = true;
}
}
assertTrue(failedFound);
assertTrue(successFound);
} finally {
env.shutdown();
}
}
use of com.couchbase.client.core.deps.io.netty.channel.Channel in project couchbase-jvm-clients by couchbase.
the class BaseEndpoint method reconnect.
/**
* This method performs the actual connecting logic.
*
* <p>It is called reconnect since it works both in the case where an initial attempt is made
* but also when the underlying channel is closed or the previous connect attempt was
* unsuccessful.</p>
*/
private void reconnect() {
if (disconnect.get()) {
return;
}
state.transition(EndpointState.CONNECTING);
final EndpointContext endpointContext = this.endpointContext.get();
final AtomicLong attemptStart = new AtomicLong();
Mono.defer((Supplier<Mono<Channel>>) () -> {
CoreEnvironment env = endpointContext.environment();
long connectTimeoutMs = env.timeoutConfig().connectTimeout().toMillis();
if (eventLoopGroup.isShutdown()) {
throw new IllegalStateException("Event Loop is already shut down, not pursuing connect attempt!");
}
final Bootstrap channelBootstrap = new Bootstrap().remoteAddress(remoteAddress()).group(eventLoopGroup).channel(channelFrom(eventLoopGroup)).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectTimeoutMs).handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(final Channel ch) {
ChannelPipeline pipeline = ch.pipeline();
SecurityConfig config = env.securityConfig();
if (config.tlsEnabled()) {
try {
pipeline.addFirst(SslHandlerFactory.get(ch.alloc(), config, endpointContext));
} catch (Exception e) {
throw new SecurityException("Could not instantiate SSL Handler", e);
}
}
if (env.ioConfig().servicesToCapture().contains(serviceType)) {
pipeline.addLast(new TrafficCaptureHandler(endpointContext));
}
pipelineInitializer().init(BaseEndpoint.this, pipeline);
pipeline.addLast(new PipelineErrorHandler(BaseEndpoint.this));
}
});
if (env.ioConfig().tcpKeepAlivesEnabled() && !(eventLoopGroup instanceof DefaultEventLoopGroup)) {
channelBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
if (eventLoopGroup instanceof EpollEventLoopGroup) {
channelBootstrap.option(EpollChannelOption.TCP_KEEPIDLE, (int) TimeUnit.MILLISECONDS.toSeconds(env.ioConfig().tcpKeepAliveTime().toMillis()));
}
}
state.transition(EndpointState.CONNECTING);
attemptStart.set(System.nanoTime());
return channelFutureIntoMono(channelBootstrap.connect());
}).timeout(endpointContext.environment().timeoutConfig().connectTimeout()).onErrorResume(throwable -> {
state.transition(EndpointState.DISCONNECTED);
if (disconnect.get()) {
endpointContext.environment().eventBus().publish(new EndpointConnectionAbortedEvent(Duration.ofNanos(System.nanoTime() - attemptStart.get()), endpointContext, ConnectTimings.toMap(channel)));
return Mono.empty();
} else {
return Mono.error(throwable);
}
}).retryWhen(Retry.any().exponentialBackoff(Duration.ofMillis(32), Duration.ofMillis(4096)).retryMax(Long.MAX_VALUE).doOnRetry(retryContext -> {
Throwable ex = retryContext.exception();
// We drop the severity for the BucketNotFoundException because it shows up when
// bootstrapping against MDS clusters and nodes with no kv service enabled on it
// that is bucket aware. If a bucket really does not exist we'll get an auth
// exception instead.
Event.Severity severity = ex instanceof BucketNotFoundException ? Event.Severity.DEBUG : Event.Severity.WARN;
Duration duration = ex instanceof TimeoutException ? endpointContext.environment().timeoutConfig().connectTimeout() : Duration.ofNanos(System.nanoTime() - attemptStart.get());
ex = annotateConnectException(ex);
endpointContext.environment().eventBus().publish(new EndpointConnectionFailedEvent(severity, duration, endpointContext, retryContext.iteration(), trimNettyFromStackTrace(ex)));
}).toReactorRetry()).subscribe(channel -> {
long now = System.nanoTime();
if (disconnect.get()) {
this.channel = null;
endpointContext.environment().eventBus().publish(new EndpointConnectionIgnoredEvent(Duration.ofNanos(now - attemptStart.get()), endpointContext, ConnectTimings.toMap(channel)));
closeChannel(channel);
} else {
this.channel = channel;
Optional<HostAndPort> localSocket = Optional.empty();
if (channel.localAddress() instanceof InetSocketAddress) {
// it will always be an inet socket address, but to safeguard for testing mocks...
InetSocketAddress so = (InetSocketAddress) channel.localAddress();
localSocket = Optional.of(new HostAndPort(so.getHostString(), so.getPort()));
}
EndpointContext newContext = new EndpointContext(endpointContext, endpointContext.remoteSocket(), endpointContext.circuitBreaker(), endpointContext.serviceType(), localSocket, endpointContext.bucket(), Optional.ofNullable(channel.attr(ChannelAttributes.CHANNEL_ID_KEY).get()));
this.endpointContext.get().environment().eventBus().publish(new EndpointConnectedEvent(Duration.ofNanos(now - attemptStart.get()), newContext, ConnectTimings.toMap(channel)));
this.endpointContext.set(newContext);
this.circuitBreaker.reset();
lastConnectedAt = now;
state.transition(EndpointState.CONNECTED);
}
}, error -> endpointContext.environment().eventBus().publish(new UnexpectedEndpointConnectionFailedEvent(Duration.ofNanos(System.nanoTime() - attemptStart.get()), endpointContext, error)));
}
use of com.couchbase.client.core.deps.io.netty.channel.Channel in project couchbase-jvm-clients by couchbase.
the class BaseEndpointIntegrationTest method startLocalServer.
private LocalServerController startLocalServer(final DefaultEventLoopGroup eventLoopGroup) {
final LocalServerController localServerController = new LocalServerController();
ServerBootstrap bootstrap = new ServerBootstrap().group(eventLoopGroup).localAddress(new LocalAddress("server")).childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
localServerController.channel.set(ctx.channel());
localServerController.connectAttempts.incrementAndGet();
ctx.fireChannelActive();
}
});
}
}).channel(LocalServerChannel.class);
bootstrap.bind().awaitUninterruptibly();
return localServerController;
}
Aggregations