/* * Copyright (c) 2008-2017, 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.networking.nio; import com.hazelcast.internal.metrics.Probe; import com.hazelcast.internal.networking.ChannelConnection; import com.hazelcast.internal.networking.ChannelInitializer; import com.hazelcast.internal.networking.ChannelOutboundHandler; import com.hazelcast.internal.networking.ChannelWriter; import com.hazelcast.internal.networking.InitResult; import com.hazelcast.internal.networking.OutboundFrame; import com.hazelcast.internal.networking.nio.iobalancer.IOBalancer; import com.hazelcast.internal.util.counters.SwCounter; import com.hazelcast.logging.ILogger; import com.hazelcast.nio.Packet; import com.hazelcast.nio.tcp.TcpIpConnection; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static com.hazelcast.internal.metrics.ProbeLevel.DEBUG; import static com.hazelcast.internal.util.counters.SwCounter.newSwCounter; import static com.hazelcast.nio.Protocols.CLUSTER; import static com.hazelcast.util.EmptyStatement.ignore; import static java.lang.Math.max; import static java.lang.System.currentTimeMillis; import static java.nio.channels.SelectionKey.OP_WRITE; /** * The writing side of the {@link TcpIpConnection}. */ public final class NioChannelWriter extends AbstractHandler implements Runnable, ChannelWriter { private static final long TIMEOUT = 3; @SuppressWarnings("checkstyle:visibilitymodifier") @Probe(name = "writeQueueSize") public final Queue<OutboundFrame> writeQueue = new ConcurrentLinkedQueue<OutboundFrame>(); @SuppressWarnings("checkstyle:visibilitymodifier") @Probe(name = "priorityWriteQueueSize") public final Queue<OutboundFrame> urgentWriteQueue = new ConcurrentLinkedQueue<OutboundFrame>(); private final ChannelInitializer initializer; private ByteBuffer outputBuffer; private final AtomicBoolean scheduled = new AtomicBoolean(false); @Probe(name = "bytesWritten") private final SwCounter bytesWritten = newSwCounter(); @Probe(name = "normalFramesWritten") private final SwCounter normalFramesWritten = newSwCounter(); @Probe(name = "priorityFramesWritten") private final SwCounter priorityFramesWritten = newSwCounter(); private ChannelOutboundHandler outboundHandler; private OutboundFrame currentFrame; private volatile long lastWriteTime; // this field will be accessed by the NioThread or // it is accessed by any other thread but only that thread managed to cas the scheduled flag to true. // This prevents running into an NioThread that is migrating. private NioThread newOwner; private long bytesReadLastPublish; private long normalFramesReadLastPublish; private long priorityFramesReadLastPublish; private long eventsLastPublish; public NioChannelWriter(ChannelConnection connection, NioThread ioThread, ILogger logger, IOBalancer balancer, ChannelInitializer initializer) { super(connection, ioThread, OP_WRITE, logger, balancer); this.initializer = initializer; } @Override public long getLoad() { switch (LOAD_TYPE) { case 0: return handleCount.get(); case 1: return bytesWritten.get() + priorityFramesWritten.get(); case 2: return normalFramesWritten.get() + priorityFramesWritten.get(); default: throw new RuntimeException(); } } @Override public int totalFramesPending() { return writeQueue.size() + urgentWriteQueue.size(); } @Override public long lastWriteTimeMillis() { return lastWriteTime; } @Override public ChannelOutboundHandler getOutboundHandler() { return outboundHandler; } @Probe(name = "writeQueuePendingBytes", level = DEBUG) public long bytesPending() { return bytesPending(writeQueue); } @Probe(name = "priorityWriteQueuePendingBytes", level = DEBUG) public long priorityBytesPending() { return bytesPending(urgentWriteQueue); } private long bytesPending(Queue<OutboundFrame> writeQueue) { long bytesPending = 0; for (OutboundFrame frame : writeQueue) { if (frame instanceof Packet) { bytesPending += ((Packet) frame).packetSize(); } } return bytesPending; } @Probe private long idleTimeMs() { return max(currentTimeMillis() - lastWriteTime, 0); } @Probe(level = DEBUG) private long isScheduled() { return scheduled.get() ? 1 : 0; } // accessed from ChannelInboundHandler and SocketConnector @Override public void setProtocol(final String protocol) { final CountDownLatch latch = new CountDownLatch(1); ioThread.addTaskAndWakeup(new Runnable() { @Override public void run() { try { if (outboundHandler == null) { InitResult<ChannelOutboundHandler> init = initializer.initOutbound(connection, NioChannelWriter.this, protocol); outputBuffer = init.getByteBuffer(); outboundHandler = init.getHandler(); } } catch (Throwable t) { onFailure(t); } finally { latch.countDown(); } } }); try { latch.await(TIMEOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { logger.finest("CountDownLatch::await interrupted", e); } } @Override public void write(OutboundFrame frame) { if (frame.isUrgent()) { urgentWriteQueue.offer(frame); } else { writeQueue.offer(frame); } schedule(); } private OutboundFrame poll() { for (; ; ) { boolean urgent = true; OutboundFrame frame = urgentWriteQueue.poll(); if (frame == null) { urgent = false; frame = writeQueue.poll(); } if (frame == null) { return null; } if (frame.getClass() == TaskFrame.class) { TaskFrame taskFrame = (TaskFrame) frame; taskFrame.task.run(); continue; } if (urgent) { priorityFramesWritten.inc(); } else { normalFramesWritten.inc(); } return frame; } } /** * Makes sure this ChannelOutboundHandler is scheduled to be executed by the IO thread. * <p/> * This call is made by 'outside' threads that interact with the connection. For example when a frame is placed * on the connection to be written. It will never be made by an IO thread. * <p/> * If the ChannelOutboundHandler already is scheduled, the call is ignored. */ private void schedule() { if (scheduled.get()) { // So this ChannelOutboundHandler is still scheduled, we don't need to schedule it again return; } if (!scheduled.compareAndSet(false, true)) { // Another thread already has scheduled this ChannelOutboundHandler, we are done. It // doesn't matter which thread does the scheduling, as long as it happens. return; } // We managed to schedule this ChannelOutboundHandler. This means we need to add a task to // the ioThread and give it a kick so that it processes our frames. ioThread.addTaskAndWakeup(this); } /** * Tries to unschedule this ChannelOutboundHandler. * <p/> * It will only be unscheduled if: * - the outputBuffer is empty * - there are no pending frames. * <p/> * If the outputBuffer is dirty then it will register itself for an OP_WRITE since we are interested in knowing * if there is more space in the socket output buffer. * If the outputBuffer is not dirty, then it will unregister itself from an OP_WRITE since it isn't interested * in space in the socket outputBuffer. * <p/> * This call is only made by the IO thread. */ private void unschedule() throws IOException { if (dirtyOutputBuffer() || currentFrame != null) { // Because not all data was written to the socket, we need to register for OP_WRITE so we get // notified when the channel is ready for more data. registerOp(OP_WRITE); // If the outputBuffer is not empty, we don't need to unschedule ourselves. This is because the // ChannelOutboundHandler will be triggered by a nio write event to continue sending data. return; } // since everything is written, we are not interested anymore in write-events, so lets unsubscribe unregisterOp(OP_WRITE); // So the outputBuffer is empty, so we are going to unschedule ourselves. scheduled.set(false); if (writeQueue.isEmpty() && urgentWriteQueue.isEmpty()) { // there are no remaining frames, so we are done. return; } // So there are frames, but we just unscheduled ourselves. If we don't try to reschedule, then these // Frames are at risk not to be send. if (!scheduled.compareAndSet(false, true)) { //someone else managed to schedule this ChannelOutboundHandler, so we are done. return; } // We managed to reschedule. So lets add ourselves to the ioThread so we are processed again. // We don't need to call wakeup because the current thread is the IO-thread and the selectionQueue will be processed // till it is empty. So it will also pick up tasks that are added while it is processing the selectionQueue. ioThread.addTask(this); } @Override @SuppressWarnings("unchecked") public void handle() throws Exception { handleCount.inc(); lastWriteTime = currentTimeMillis(); if (outboundHandler == null) { InitResult<ChannelOutboundHandler> init = initializer.initOutbound(connection, this, CLUSTER); this.outputBuffer = init.getByteBuffer(); this.outboundHandler = init.getHandler(); registerOp(OP_WRITE); } fillOutputBuffer(); if (dirtyOutputBuffer()) { writeOutputBufferToSocket(); } if (newOwner == null) { unschedule(); } else { startMigration(); } } private void startMigration() throws IOException { NioThread newOwner = this.newOwner; this.newOwner = null; startMigration(newOwner); } /** * Checks of the outputBuffer is dirty. * * @return true if dirty, false otherwise. */ private boolean dirtyOutputBuffer() { return outputBuffer.position() > 0; } /** * Writes to content of the outputBuffer to the socket. */ private void writeOutputBufferToSocket() throws IOException { // So there is data for writing, so lets prepare the buffer for writing and then write it to the channel. outputBuffer.flip(); int written = channel.write(outputBuffer); bytesWritten.inc(written); // Now we verify if all data is written. if (outputBuffer.hasRemaining()) { // We did not manage to write all data to the socket. So lets compact the buffer so new data can be added at the end. outputBuffer.compact(); } else { // We managed to fully write the outputBuffer to the socket, so we are done. outputBuffer.clear(); } } /** * Fills the outBuffer with frames. This is done till there are no more frames or till there is no more space in the * outputBuffer. */ private void fillOutputBuffer() throws Exception { if (currentFrame == null) { // there is no pending frame, lets poll one. currentFrame = poll(); } while (currentFrame != null) { // Lets write the currentFrame to the outputBuffer. if (!outboundHandler.onWrite(currentFrame, outputBuffer)) { // We are done for this round because not all data of the currentFrame fits in the outputBuffer return; } // The current frame has been written completely. So lets poll for another one. currentFrame = poll(); } } @Override public void run() { try { handle(); } catch (Throwable t) { onFailure(t); } } @Override public void close() { writeQueue.clear(); urgentWriteQueue.clear(); CloseTask closeTask = new CloseTask(); write(new TaskFrame(closeTask)); closeTask.awaitCompletion(); } @Override public void requestMigration(NioThread newOwner) { write(new TaskFrame(new StartMigrationTask(newOwner))); } @Override public String toString() { return connection + ".channelWriter"; } /** * The TaskFrame is not really a Frame. It is a way to put a task on one of the frame-queues. Using this approach we * can lift on top of the Frame scheduling mechanism and we can prevent having: * - multiple NioThread-tasks for a ChannelWriter on multiple NioThread * - multiple NioThread-tasks for a ChannelWriter on the same NioThread. */ private static final class TaskFrame implements OutboundFrame { private final Runnable task; private TaskFrame(Runnable task) { this.task = task; } @Override public boolean isUrgent() { return true; } } /** * Triggers the migration when executed by setting the ChannelWriter.newOwner field. When the handle method completes, it * checks if this field if set, if so, the migration starts. * * If the current ioThread is the same as 'theNewOwner' then the call is ignored. */ private final class StartMigrationTask implements Runnable { // field is called 'theNewOwner' to prevent any ambiguity problems with the outboundHandler.newOwner. // Else you get a lot of ugly ChannelOutboundHandler.this.newOwner is ... private final NioThread theNewOwner; StartMigrationTask(NioThread theNewOwner) { this.theNewOwner = theNewOwner; } @Override public void run() { assert newOwner == null : "No migration can be in progress"; if (ioThread == theNewOwner) { // if there is no change, we are done return; } newOwner = theNewOwner; } } @Override protected void publish() { ioThread.bytesTransceived += bytesWritten.get() - bytesReadLastPublish; ioThread.framesTransceived += normalFramesWritten.get() - normalFramesReadLastPublish; ioThread.priorityFramesTransceived += priorityFramesWritten.get() - priorityFramesReadLastPublish; ioThread.handleCount += handleCount.get() - eventsLastPublish; bytesReadLastPublish = bytesWritten.get(); normalFramesReadLastPublish = normalFramesWritten.get(); priorityFramesReadLastPublish = priorityFramesWritten.get(); eventsLastPublish = handleCount.get(); } private class CloseTask implements Runnable { private final CountDownLatch latch = new CountDownLatch(1); @Override public void run() { try { channel.closeOutbound(); } catch (IOException e) { logger.finest("Error while closing outbound", e); } finally { latch.countDown(); } } void awaitCompletion() { try { latch.await(TIMEOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { ignore(e); } } } }