All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mongodb.connection.netty.NettyStream Maven / Gradle / Ivy

Go to download

The Java operations layer for the MongoDB Java Driver. Third parties can ' + 'wrap this layer to provide custom higher-level APIs

There is a newer version: 5.3.0-beta0
Show newest version
/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb.connection.netty;

import com.mongodb.MongoClientException;
import com.mongodb.MongoException;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoInterruptedException;
import com.mongodb.MongoSocketException;
import com.mongodb.MongoSocketOpenException;
import com.mongodb.MongoSocketReadTimeoutException;
import com.mongodb.ServerAddress;
import com.mongodb.annotations.ThreadSafe;
import com.mongodb.connection.AsyncCompletionHandler;
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
import com.mongodb.connection.Stream;
import com.mongodb.lang.Nullable;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.ReadTimeoutException;
import org.bson.ByteBuf;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import java.io.IOException;
import java.net.SocketAddress;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;

import static com.mongodb.assertions.Assertions.assertNotNull;
import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.internal.connection.SslHelper.enableHostNameVerification;
import static com.mongodb.internal.connection.SslHelper.enableSni;
import static java.util.Optional.ofNullable;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * A Stream implementation based on Netty 4.0.
 * Just like it is for the {@link java.nio.channels.AsynchronousSocketChannel},
 * concurrent pending1 readers
 * (whether {@linkplain #read(int, int) synchronous} or {@linkplain #readAsync(int, AsyncCompletionHandler) asynchronous})
 * are not supported by {@link NettyStream}.
 * However, this class does not have a fail-fast mechanism checking for such situations.
 * 
* 1We cannot simply say that read methods are not allowed be run concurrently because strictly speaking they are allowed, * as explained below. *
{@code
 * NettyStream stream = ...;
 * stream.readAsync(1, new AsyncCompletionHandler() {//inv1
 *  @Override
 *  public void completed(ByteBuf o) {
 *      stream.readAsync(//inv2
 *              1, ...);//ret2
 *  }
 *
 *  @Override
 *  public void failed(Throwable t) {
 *  }
 * });//ret1
 * }
* Arrows on the diagram below represent happens-before relations. *
{@code
 * int1 -> inv2 -> ret2
 *      \--------> ret1
 * }
* As shown on the diagram, the method {@link #readAsync(int, AsyncCompletionHandler)} runs concurrently with * itself in the example above. However, there are no concurrent pending readers because the second operation * is invoked after the first operation has completed reading despite the method has not returned yet. */ final class NettyStream implements Stream { private static final byte NO_SCHEDULE_TIME = 0; private final ServerAddress address; private final SocketSettings settings; private final SslSettings sslSettings; private final EventLoopGroup workerGroup; private final Class socketChannelClass; private final ByteBufAllocator allocator; @Nullable private final SslContext sslContext; private boolean isClosed; private volatile Channel channel; private final LinkedList pendingInboundBuffers = new LinkedList<>(); /* The fields pendingReader, pendingException are always written/read inside synchronized blocks * that use the same NettyStream object, so they can be plain.*/ private PendingReader pendingReader; private Throwable pendingException; /* The fields readTimeoutTask, readTimeoutMillis are each written only in the ChannelInitializer.initChannel method * (in addition to the write of the default value and the write by variable initializers), * and read only when NettyStream users read data, or Netty event loop handles incoming data. * Since actions done by the ChannelInitializer.initChannel method * are ordered (in the happens-before order) before user read actions and before event loop actions that handle incoming data, * these fields can be plain.*/ @Nullable private ReadTimeoutTask readTimeoutTask; private long readTimeoutMillis = NO_SCHEDULE_TIME; NettyStream(final ServerAddress address, final SocketSettings settings, final SslSettings sslSettings, final EventLoopGroup workerGroup, final Class socketChannelClass, final ByteBufAllocator allocator, @Nullable final SslContext sslContext) { this.address = address; this.settings = settings; this.sslSettings = sslSettings; this.workerGroup = workerGroup; this.socketChannelClass = socketChannelClass; this.allocator = allocator; this.sslContext = sslContext; } @Override public ByteBuf getBuffer(final int size) { return new NettyByteBuf(allocator.buffer(size, size)); } @Override public void open() throws IOException { FutureAsyncCompletionHandler handler = new FutureAsyncCompletionHandler<>(); openAsync(handler); handler.get(); } @Override public void openAsync(final AsyncCompletionHandler handler) { Queue socketAddressQueue; try { socketAddressQueue = new LinkedList<>(address.getSocketAddresses()); } catch (Throwable t) { handler.failed(t); return; } initializeChannel(handler, socketAddressQueue); } private void initializeChannel(final AsyncCompletionHandler handler, final Queue socketAddressQueue) { if (socketAddressQueue.isEmpty()) { handler.failed(new MongoSocketException("Exception opening socket", getAddress())); } else { SocketAddress nextAddress = socketAddressQueue.poll(); Bootstrap bootstrap = new Bootstrap(); bootstrap.group(workerGroup); bootstrap.channel(socketChannelClass); bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, settings.getConnectTimeout(MILLISECONDS)); bootstrap.option(ChannelOption.TCP_NODELAY, true); bootstrap.option(ChannelOption.SO_KEEPALIVE, true); if (settings.getReceiveBufferSize() > 0) { bootstrap.option(ChannelOption.SO_RCVBUF, settings.getReceiveBufferSize()); } if (settings.getSendBufferSize() > 0) { bootstrap.option(ChannelOption.SO_SNDBUF, settings.getSendBufferSize()); } bootstrap.option(ChannelOption.ALLOCATOR, allocator); bootstrap.handler(new ChannelInitializer() { @Override public void initChannel(final SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); if (sslSettings.isEnabled()) { addSslHandler(ch); } int readTimeout = settings.getReadTimeout(MILLISECONDS); if (readTimeout > NO_SCHEDULE_TIME) { readTimeoutMillis = readTimeout; /* We need at least one handler before (in the inbound evaluation order) the InboundBufferHandler, * so that we can fire exception events (they are inbound events) using its context and the InboundBufferHandler * receives them. SslHandler is not always present, so adding a NOOP handler.*/ pipeline.addLast(new ChannelInboundHandlerAdapter()); readTimeoutTask = new ReadTimeoutTask(pipeline.lastContext()); } pipeline.addLast(new InboundBufferHandler()); } }); ChannelFuture channelFuture = bootstrap.connect(nextAddress); channelFuture.addListener(new OpenChannelFutureListener(socketAddressQueue, channelFuture, handler)); } } @Override public void write(final List buffers) throws IOException { FutureAsyncCompletionHandler future = new FutureAsyncCompletionHandler<>(); writeAsync(buffers, future); future.get(); } @Override public ByteBuf read(final int numBytes) throws IOException { return read(numBytes, 0); } @Override public boolean supportsAdditionalTimeout() { return true; } @Override public ByteBuf read(final int numBytes, final int additionalTimeoutMillis) throws IOException { isTrueArgument("additionalTimeoutMillis must not be negative", additionalTimeoutMillis >= 0); FutureAsyncCompletionHandler future = new FutureAsyncCompletionHandler<>(); readAsync(numBytes, future, combinedTimeout(readTimeoutMillis, additionalTimeoutMillis)); return future.get(); } @Override public void writeAsync(final List buffers, final AsyncCompletionHandler handler) { CompositeByteBuf composite = PooledByteBufAllocator.DEFAULT.compositeBuffer(); for (ByteBuf cur : buffers) { composite.addComponent(true, ((NettyByteBuf) cur).asByteBuf()); } channel.writeAndFlush(composite).addListener((ChannelFutureListener) future -> { if (!future.isSuccess()) { handler.failed(future.cause()); } else { handler.completed(null); } }); } @Override public void readAsync(final int numBytes, final AsyncCompletionHandler handler) { readAsync(numBytes, handler, readTimeoutMillis); } /** * @param numBytes Must be equal to {@link #pendingReader}{@code .numBytes} when called by a Netty channel handler. * @param handler Must be equal to {@link #pendingReader}{@code .handler} when called by a Netty channel handler. * @param readTimeoutMillis Must be equal to {@link #NO_SCHEDULE_TIME} when called by a Netty channel handler. * Timeouts may be scheduled only by the public read methods. Taking into account that concurrent pending * readers are not allowed, there must not be a situation when threads attempt to schedule a timeout * before the previous one is either cancelled or completed. */ private void readAsync(final int numBytes, final AsyncCompletionHandler handler, final long readTimeoutMillis) { ByteBuf buffer = null; Throwable exceptionResult = null; synchronized (this) { exceptionResult = pendingException; if (exceptionResult == null) { if (!hasBytesAvailable(numBytes)) { if (pendingReader == null) {//called by a public read method pendingReader = new PendingReader(numBytes, handler, scheduleReadTimeout(readTimeoutTask, readTimeoutMillis)); } } else { CompositeByteBuf composite = allocator.compositeBuffer(pendingInboundBuffers.size()); int bytesNeeded = numBytes; for (Iterator iter = pendingInboundBuffers.iterator(); iter.hasNext();) { io.netty.buffer.ByteBuf next = iter.next(); int bytesNeededFromCurrentBuffer = Math.min(next.readableBytes(), bytesNeeded); if (bytesNeededFromCurrentBuffer == next.readableBytes()) { composite.addComponent(next); iter.remove(); } else { next.retain(); composite.addComponent(next.readSlice(bytesNeededFromCurrentBuffer)); } composite.writerIndex(composite.writerIndex() + bytesNeededFromCurrentBuffer); bytesNeeded -= bytesNeededFromCurrentBuffer; if (bytesNeeded == 0) { break; } } buffer = new NettyByteBuf(composite).flip(); } } if (!(exceptionResult == null && buffer == null)//the read operation has completed && pendingReader != null) {//we need to clear the pending reader cancel(pendingReader.timeout); this.pendingReader = null; } } if (exceptionResult != null) { handler.failed(exceptionResult); } if (buffer != null) { handler.completed(buffer); } } private boolean hasBytesAvailable(final int numBytes) { int bytesAvailable = 0; for (io.netty.buffer.ByteBuf cur : pendingInboundBuffers) { bytesAvailable += cur.readableBytes(); if (bytesAvailable >= numBytes) { return true; } } return false; } private void handleReadResponse(@Nullable final io.netty.buffer.ByteBuf buffer, @Nullable final Throwable t) { PendingReader localPendingReader = null; synchronized (this) { if (buffer != null) { pendingInboundBuffers.add(buffer.retain()); } else { pendingException = t; } localPendingReader = pendingReader; } if (localPendingReader != null) { //timeouts may be scheduled only by the public read methods readAsync(localPendingReader.numBytes, localPendingReader.handler, NO_SCHEDULE_TIME); } } @Override public ServerAddress getAddress() { return address; } @Override public synchronized void close() { isClosed = true; if (channel != null) { channel.close(); channel = null; } for (Iterator iterator = pendingInboundBuffers.iterator(); iterator.hasNext();) { io.netty.buffer.ByteBuf nextByteBuf = iterator.next(); iterator.remove(); nextByteBuf.release(); } } @Override public boolean isClosed() { return isClosed; } public SocketSettings getSettings() { return settings; } public SslSettings getSslSettings() { return sslSettings; } public EventLoopGroup getWorkerGroup() { return workerGroup; } public Class getSocketChannelClass() { return socketChannelClass; } public ByteBufAllocator getAllocator() { return allocator; } private void addSslHandler(final SocketChannel channel) { SSLEngine engine; if (sslContext == null) { SSLContext sslContext; try { sslContext = ofNullable(sslSettings.getContext()).orElse(SSLContext.getDefault()); } catch (NoSuchAlgorithmException e) { throw new MongoClientException("Unable to create standard SSLContext", e); } engine = sslContext.createSSLEngine(address.getHost(), address.getPort()); } else { engine = sslContext.newEngine(channel.alloc(), address.getHost(), address.getPort()); } engine.setUseClientMode(true); SSLParameters sslParameters = engine.getSSLParameters(); enableSni(address.getHost(), sslParameters); if (!sslSettings.isInvalidHostNameAllowed()) { enableHostNameVerification(sslParameters); } engine.setSSLParameters(sslParameters); channel.pipeline().addFirst("ssl", new SslHandler(engine, false)); } private class InboundBufferHandler extends SimpleChannelInboundHandler { @Override protected void channelRead0(final ChannelHandlerContext ctx, final io.netty.buffer.ByteBuf buffer) throws Exception { handleReadResponse(buffer, null); } @Override public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable t) { if (t instanceof ReadTimeoutException) { handleReadResponse(null, new MongoSocketReadTimeoutException("Timeout while receiving message", address, t)); } else { handleReadResponse(null, t); } ctx.close(); } } private static final class PendingReader { private final int numBytes; private final AsyncCompletionHandler handler; @Nullable private final ScheduledFuture timeout; private PendingReader( final int numBytes, final AsyncCompletionHandler handler, @Nullable final ScheduledFuture timeout) { this.numBytes = numBytes; this.handler = handler; this.timeout = timeout; } } private static final class FutureAsyncCompletionHandler implements AsyncCompletionHandler { private final CountDownLatch latch = new CountDownLatch(1); private volatile T t; private volatile Throwable throwable; FutureAsyncCompletionHandler() { } @Override public void completed(@Nullable final T t) { this.t = t; latch.countDown(); } @Override public void failed(final Throwable t) { this.throwable = t; latch.countDown(); } public T get() throws IOException { try { latch.await(); if (throwable != null) { if (throwable instanceof IOException) { throw (IOException) throwable; } else if (throwable instanceof MongoException) { throw (MongoException) throwable; } else { throw new MongoInternalException("Exception thrown from Netty Stream", throwable); } } return t; } catch (InterruptedException e) { throw new MongoInterruptedException("Interrupted", e); } } } private class OpenChannelFutureListener implements ChannelFutureListener { private final Queue socketAddressQueue; private final ChannelFuture channelFuture; private final AsyncCompletionHandler handler; OpenChannelFutureListener(final Queue socketAddressQueue, final ChannelFuture channelFuture, final AsyncCompletionHandler handler) { this.socketAddressQueue = socketAddressQueue; this.channelFuture = channelFuture; this.handler = handler; } @Override public void operationComplete(final ChannelFuture future) { synchronized (NettyStream.this) { if (future.isSuccess()) { if (isClosed) { channelFuture.channel().close(); } else { channel = channelFuture.channel(); channel.closeFuture().addListener((ChannelFutureListener) future1 -> handleReadResponse(null, new IOException("The connection to the server was closed"))); } handler.completed(null); } else { if (isClosed) { handler.completed(null); } else if (socketAddressQueue.isEmpty()) { handler.failed(new MongoSocketOpenException("Exception opening socket", getAddress(), future.cause())); } else { initializeChannel(handler, socketAddressQueue); } } } } } private static void cancel(@Nullable final Future f) { if (f != null) { f.cancel(false); } } private static long combinedTimeout(final long timeout, final int additionalTimeout) { if (timeout == NO_SCHEDULE_TIME) { return NO_SCHEDULE_TIME; } else { return Math.addExact(timeout, additionalTimeout); } } @Nullable private static ScheduledFuture scheduleReadTimeout(@Nullable final ReadTimeoutTask readTimeoutTask, final long timeoutMillis) { if (timeoutMillis == NO_SCHEDULE_TIME) { return null; } else { return assertNotNull(readTimeoutTask).schedule(timeoutMillis); } } @ThreadSafe private static final class ReadTimeoutTask implements Runnable { private final ChannelHandlerContext ctx; private ReadTimeoutTask(final ChannelHandlerContext timeoutChannelHandlerContext) { ctx = timeoutChannelHandlerContext; } @Override public void run() { try { if (ctx.channel().isOpen()) { ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE); ctx.close(); } } catch (final Throwable t) { ctx.fireExceptionCaught(t); } } private ScheduledFuture schedule(final long timeoutMillis) { //assert timeoutMillis > 0 : timeoutMillis; return ctx.executor().schedule(this, timeoutMillis, MILLISECONDS); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy