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

com.hazelcast.internal.tpcengine.nio.NioAsyncSocket Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2008-2024, Hazelcast, Inc. All Rights Reserved.
 *
 * 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.hazelcast.internal.tpcengine.nio;

import com.hazelcast.internal.tpcengine.iobuffer.IOBuffer;
import com.hazelcast.internal.tpcengine.net.AsyncSocket;
import com.hazelcast.internal.tpcengine.net.AsyncSocketMetrics;
import com.hazelcast.internal.tpcengine.net.AsyncSocketOptions;
import com.hazelcast.internal.tpcengine.net.AsyncSocketReader;
import com.hazelcast.internal.tpcengine.net.AsyncSocketWriter;
import com.hazelcast.internal.tpcengine.util.CircularQueue;
import com.hazelcast.shaded.org.jctools.queues.MpmcArrayQueue;

import java.io.EOFException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;

import static com.hazelcast.internal.tpcengine.net.AsyncSocketOptions.SO_RCVBUF;
import static com.hazelcast.internal.tpcengine.net.AsyncSocketOptions.SO_SNDBUF;
import static com.hazelcast.internal.tpcengine.util.BufferUtil.allocateBuffer;
import static com.hazelcast.internal.tpcengine.util.BufferUtil.compactOrClear;
import static com.hazelcast.internal.tpcengine.util.CloseUtil.closeQuietly;
import static com.hazelcast.internal.tpcengine.util.ExceptionUtil.sneakyThrow;
import static com.hazelcast.internal.tpcengine.util.Preconditions.checkNotNull;
import static java.lang.Thread.currentThread;
import static java.nio.channels.SelectionKey.OP_CONNECT;
import static java.nio.channels.SelectionKey.OP_READ;
import static java.nio.channels.SelectionKey.OP_WRITE;

/**
 * Nio implementation of the {@link AsyncSocket}.
 */
@SuppressWarnings({"checkstyle:DeclarationOrder", "checkstyle:VisibilityOrder", "checkstyle:MethodCount", "java:S1181"})
public final class NioAsyncSocket extends AsyncSocket {

    private final NioAsyncSocketOptions options;
    private final AtomicReference flushThread = new AtomicReference<>(currentThread());
    private final MpmcArrayQueue writeQueue;
    private final Handler handler;
    private final SocketChannel socketChannel;
    private final NioReactor reactor;
    private final Thread eventloopThread;
    private final SelectionKey key;
    private final IOVector ioVector;
    private final AsyncSocketReader reader;
    private final CircularQueue localTaskQueue;
    private final AsyncSocketWriter writer;
    private boolean ioVectorWriteAllowed;

    // only accessed from eventloop thread
    private boolean started;
    // only accessed from eventloop thread
    private boolean connecting;
    private volatile CompletableFuture connectFuture;

    @SuppressWarnings("checkstyle:executablestatementcount")
    NioAsyncSocket(NioAsyncSocketBuilder builder) {
        super(builder.clientSide);

        assert currentThread() == builder.reactor.eventloopThread();

        try {
            this.reactor = builder.reactor;
            this.localTaskQueue = builder.reactor.eventloop().localTaskQueue;
            this.options = builder.options;
            this.eventloopThread = reactor.eventloopThread();
            this.socketChannel = builder.socketChannel;
            if (!clientSide) {
                this.localAddress = socketChannel.getLocalAddress();
                this.remoteAddress = socketChannel.getRemoteAddress();
            }
            this.writeQueue = new MpmcArrayQueue<>(builder.writeQueueCapacity);
            this.handler = new Handler(builder);
            this.key = socketChannel.register(reactor.selector, 0, handler);
            this.reader = builder.reader;
            reader.init(this);
            this.writer = builder.writer;
            if (writer != null) {
                this.writer.init(this, writeQueue);
                this.ioVector = null;
            } else {
                this.ioVector = new IOVector();
            }
            this.ioVectorWriteAllowed = ioVector != null;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public AsyncSocketOptions options() {
        return options;
    }

    @Override
    public NioReactor reactor() {
        return reactor;
    }

    @Override
    public void setReadable(boolean readable) {
        if (currentThread() == eventloopThread) {
            setReadable0(readable);
        } else {
            CompletableFuture future = new CompletableFuture();
            reactor.execute(() -> {
                try {
                    setReadable0(readable);
                    future.complete(null);
                } catch (Throwable t) {
                    future.completeExceptionally(t);
                    throw sneakyThrow(t);
                }
            });

            future.join();
        }
    }

    private void setReadable0(boolean readable) {
        if (readable) {
            // Signal that we are interested in OP_READ events.
            key.interestOpsOr(OP_READ);
        } else {
            // Signal that we are not interested in OP_READ events.
            // So even if data is received or still available on the socket,
            // we will not get further events.
            key.interestOpsAnd(~OP_READ);
        }

        // We are not running on the eventloop thread. We need to notify the
        // reactor because a change in the interest set isn't picked up while
        // the reactor is waiting on the selectionKey
        reactor.wakeup();
    }

    @Override
    public boolean isReadable() {
        if (currentThread() == eventloopThread) {
            return isReadable0();
        } else {
            CompletableFuture future = new CompletableFuture<>();
            reactor.execute(() -> {
                try {
                    future.complete(isReadable0());
                } catch (Throwable t) {
                    future.completeExceptionally(t);
                    throw sneakyThrow(t);
                }
            });

            return future.join();
        }
    }

    private boolean isReadable0() {
        return (key.interestOps() & OP_READ) != 0;
    }

    @Override
    public void start() {
        if (currentThread() == reactor.eventloopThread()) {
            start0();
        } else {
            CompletableFuture future = new CompletableFuture();
            reactor.execute(() -> {
                try {
                    start0();
                    future.complete(null);
                } catch (Throwable e) {
                    future.completeExceptionally(e);
                    throw sneakyThrow(e);
                }
            });
            future.join();
        }
    }

    private void start0() {
        if (started) {
            throw new IllegalStateException(this + " is already started");
        }
        started = true;

        assert flushThread.get() == reactor.eventloopThread();

        if (!clientSide) {
            // on the server side we immediately start reading.
            key.interestOps(key.interestOps() | OP_READ);
            // and on the server side we can immediately start sending
            resetFlushed();
        }
    }

    @Override
    public CompletableFuture connect(SocketAddress address) {
        checkNotNull(address, "address");

        if (logger.isInfoEnabled()) {
            logger.info("Connecting to address:" + address);
        }

        CompletableFuture future = new CompletableFuture<>();
        if (currentThread() == eventloopThread) {
            connect0(address, future);
        } else {
            reactor.execute(() -> connect0(address, future));
        }

        return future;
    }

    private void connect0(SocketAddress address, CompletableFuture future) {
        try {
            if (!started) {
                throw new IllegalStateException(this + " can't connect when socket not yet started");
            }

            if (connecting) {
                throw new IllegalStateException(this + " is already trying to connect");
            }

            assert flushThread.get() == reactor.eventloopThread();

            connecting = true;
            connectFuture = future;
            key.interestOps(key.interestOps() | OP_CONNECT);
            if (socketChannel.connect(address)) {
                // We got lucky, the connection was immediately established which can
                // happen with local connections.
                onConnectFinished();
            }
        } catch (Throwable e) {
            future.completeExceptionally(e);
            throw sneakyThrow(e);
        }
    }

    private void onConnectFinished() throws IOException {
        assert connecting;
        assert flushThread.get() == reactor.eventloopThread();

        remoteAddress = socketChannel.getRemoteAddress();
        localAddress = socketChannel.getLocalAddress();
        if (logger.isInfoEnabled()) {
            logger.info("Connection established " + NioAsyncSocket.this);
        }

        key.interestOps(OP_READ);

        connectFuture.complete(null);
        connectFuture = null;

        // From this point on, the socket is willing to send data.
        resetFlushed();
    }

    @SuppressWarnings("java:S1135")
    @Override
    public void flush() {
        Thread currentThread = currentThread();

        if (flushThread.get() != null) {
            // the socket is already flushed, we are done.
            return;
        }

        // The socket is not flushed, so we are going to try to flush it.
        if (!flushThread.compareAndSet(null, currentThread)) {
            // A different thread triggered a flush, we are done.
            return;
        }

        if (currentThread == eventloopThread) {
            localTaskQueue.add(handler);
        } else {
            reactor.offer(handler);
        }
    }

    @SuppressWarnings({"java:S3398", "java:S1066"})
    private void resetFlushed() {
        flushThread.set(null);

        if (!writeQueue.isEmpty()) {
            if (flushThread.compareAndSet(null, currentThread())) {
                reactor.offer(handler);
            }
        }
    }

    @Override
    public boolean write(Object msg) {
        checkNotNull(msg, "msg");

        if (writer == null && !(msg instanceof IOBuffer)) {
            throw new IllegalArgumentException("Message needs to be an IOBuffer if no writer is configured.");
        }

        if (writeQueue.add(msg)) {
            return true;
        } else {
            // lets trigger a flush since the writeQueue is full.
            flush();
            return false;
        }
    }

    @Override
    public boolean writeAndFlush(Object msg) {
        boolean result = write(msg);
        flush();
        return result;
    }

    @Override
    public boolean unsafeWriteAndFlush(Object msg) {
        checkNotNull(msg, "msg");

        if (writer == null && !(msg instanceof IOBuffer)) {
            throw new IllegalArgumentException(
                    "Only accepting IOBuffers if writer isn't set.");
        }

        Thread currentThread = currentThread();
        if (currentThread != eventloopThread) {
            throw new IllegalStateException(
                    "insideWriteAndFlush can only be made from eventloop thread, "
                            + "found " + currentThread);
        }

        boolean triggeredFlush;

        Thread currentFlushThread = flushThread.get();
        if (currentFlushThread == null) {
            // the socket isn't flushed, lets try to flush it.
            triggeredFlush = flushThread.compareAndSet(null, currentThread);
            // At this point we know for sure that the socket was flushed; either
            // by the current thread or by a different one.
        } else {
            // the socket was already flushed
            triggeredFlush = false;
        }

        boolean offered = unsafeWrite(msg);

        if (triggeredFlush && offered) {
            reactor.execute(handler);
        }

        return offered;
    }

    private boolean unsafeWrite(Object msg) {
        if (ioVectorWriteAllowed) {
            if (ioVector.offer((IOBuffer) msg)) {
                return true;
            } else {
                ioVectorWriteAllowed = false;
                return writeQueue.offer(msg);
            }
        } else {
            return writeQueue.offer(msg);
        }
    }

    @Override
    protected void close0() throws IOException {
        closeQuietly(socketChannel);
        key.cancel();
        super.close0();
    }

    @SuppressWarnings("java:S125")
    private final class Handler implements NioHandler, Runnable {
        private final ByteBuffer rcvBuffer;
        private final AsyncSocketMetrics metrics = NioAsyncSocket.this.metrics;
        private final ByteBuffer sndBuffer;

        private Handler(NioAsyncSocketBuilder builder) throws SocketException {
            this.rcvBuffer = allocateBuffer(builder.directBuffers, builder.options.get(SO_RCVBUF));

            if (ioVector == null) {
                this.sndBuffer = allocateBuffer(builder.directBuffers, builder.options.get(SO_SNDBUF));
            } else {
                this.sndBuffer = null;
            }
        }

        @Override
        public void run() {
            try {
                handleWrite();
            } catch (Throwable e) {
                close(null, e);
                throw sneakyThrow(e);
            }
        }

        @Override
        public void close(String reason, Throwable cause) {
            if (cause instanceof EOFException) {
                // The stacktrace of an EOFException isn't important. It just means that the
                // Exception is closed by the remote side.
                NioAsyncSocket.this.close(reason != null ? reason : cause.getMessage(), null);
            } else {
                NioAsyncSocket.this.close(reason, cause);
            }
        }

        @Override
        public void handle() throws IOException {
            if (!key.isValid()) {
                throw new CancelledKeyException();
            }

            int readyOps = key.readyOps();

            if ((readyOps & OP_READ) != 0) {
                handleRead();
            }

            if ((readyOps & OP_WRITE) != 0) {
                handleWrite();
            }

            if ((readyOps & OP_CONNECT) != 0) {
                handleConnect();
            }
        }

        private void handleRead() throws IOException {
            metrics.incReadEvents();

            int read = socketChannel.read(rcvBuffer);
            //System.out.println(NioAsyncSocket.this + " bytes read: " + read);

            if (read == -1) {
                throw new EOFException("Socket closed by peer");
            }

            metrics.incBytesRead(read);
            rcvBuffer.flip();
            reader.onRead(rcvBuffer);
            compactOrClear(rcvBuffer);
        }

        // todo: temp notes.
        // In netty there is logic in the NioSocketChannel.doWrite
        // 1) all the collected messages are IOBUffers, do a writev.
        // 2) if there is only 1 message and it an IOBuffer, then do a write
        // 3) if at least one of the messages is a non IOBuffer, then fallback
        // to a normal write.
        private void handleWrite() throws IOException {
            metrics.incWriteEvents();

            // todo: Netty has a write spin option we need to investigate.

            long bytesWritten;
            boolean clean;
            if (writer == null) {
                // the writeQueue is guaranteed to have only IOBuffers
                // if the writer isn't set.
                ioVector.populate(writeQueue);

                if (writeQueue.isEmpty()) {
                    ioVectorWriteAllowed = true;
                }

                int ioVectorLength = ioVector.length();
                ByteBuffer[] srcs = ioVector.array();
                bytesWritten = ioVectorLength == 1
                        ? socketChannel.write(srcs[0])
                        : socketChannel.write(srcs, 0, ioVectorLength);
                ioVector.compact(bytesWritten);
                clean = ioVector.isEmpty();
            } else {
                boolean writerClean = writer.onWrite(sndBuffer);
                sndBuffer.flip();
                bytesWritten = socketChannel.write(sndBuffer);
                boolean sndBufferClean = !sndBuffer.hasRemaining();
                clean = writerClean && sndBufferClean;
                compactOrClear(sndBuffer);
            }

            metrics.incBytesWritten(bytesWritten);
            //System.out.println(socket + " bytes written:" + bytesWritten);

            if (clean) {
                // everything got written
                int interestOps = key.interestOps();

                // clear the OP_WRITE flag if it was set
                if ((interestOps & OP_WRITE) != 0) {
                    key.interestOps(interestOps & ~OP_WRITE);
                }

                resetFlushed();
            } else {
                // not everything got written, therefor we need to register
                // for the OP_WRITE so that we get an event as soon as space
                // is available in the send buffer of the socket.
                key.interestOps(key.interestOps() | OP_WRITE);
            }
        }


        // Is called when side of the socket that initiates the connect
        // gets the event that the connection is completed.
        private void handleConnect() {
            try {
                assert flushThread.get() != null;

                if (!socketChannel.finishConnect()) {
                    throw new IllegalStateException();
                }
                onConnectFinished();
            } catch (Throwable e) {
                if (connectFuture != null) {
                    connectFuture.completeExceptionally(e);
                }
                throw sneakyThrow(e);
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy