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

org.rapidoidx.net.impl.RapidoidWorker Maven / Gradle / Ivy

The newest version!
package org.rapidoidx.net.impl;

/*
 * #%L
 * rapidoid-x-net
 * %%
 * Copyright (C) 2014 - 2015 Nikolche Mihajlovski and contributors
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 * #L%
 */

import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;

import org.rapidoid.Insights;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.config.Conf;
import org.rapidoid.ctx.Ctxs;
import org.rapidoid.log.Log;
import org.rapidoid.measure.StatsMeasure;
import org.rapidoid.pool.Pool;
import org.rapidoid.pool.Pools;
import org.rapidoid.u.U;
import org.rapidoid.util.SimpleList;
import org.rapidoidx.buffer.BufGroup;
import org.rapidoidx.buffer.IncompleteReadException;
import org.rapidoidx.net.Protocol;

@Authors("Nikolche Mihajlovski")
@Since("3.0.0")
public class RapidoidWorker extends AbstractEventLoop {

	public static boolean EXTRA_SAFE = false;

	private final Queue restarting;

	private final Queue connecting;

	private final Queue connected;

	private final SimpleList waitingToWrite;

	private final Pool connections;

	private final long maxPipelineSize;

	private final int selectorTimeout;

	final Protocol serverProtocol;

	final RapidoidHelper helper;

	private final int bufSize;

	private final long bufSizeLimit;

	private final boolean noDelay;

	private final BufGroup bufs;

	private volatile long messagesProcessed;

	private final StatsMeasure dataIn;

	private final StatsMeasure dataOut;

	public RapidoidWorker(String name, final BufGroup bufs, final Protocol protocol, final RapidoidHelper helper,
			int bufSizeKB, boolean noNelay) {
		super(name);
		this.bufs = bufs;

		this.serverProtocol = protocol;
		this.helper = helper;
		this.maxPipelineSize = Conf.option("pipeline-max", 1000000L);
		this.selectorTimeout = Conf.option("selector-timeout", 5);
		this.bufSizeLimit = Conf.option("buffer-limit", 1024L * 1024); // 1 MB

		final int queueSize = Conf.micro() ? 1000 : 1000000;
		final int growFactor = Conf.micro() ? 2 : 10;

		this.restarting = new ArrayBlockingQueue(queueSize);
		this.connecting = new ArrayBlockingQueue(queueSize);
		this.connected = new ArrayBlockingQueue(queueSize);
		this.waitingToWrite = new SimpleList(queueSize / 10, growFactor);

		this.dataIn = Insights.stats(name + ":datain");
		this.dataOut = Insights.stats(name + ":dataout");

		connections = Pools.create("connections", new Callable() {
			@Override
			public RapidoidConnection call() throws Exception {
				return newConnection();
			}
		}, 100000);

		this.bufSize = bufSizeKB * 1024;
		this.noDelay = noNelay;
	}

	public void accept(SocketChannel socketChannel) throws IOException {

		configureSocket(socketChannel);

		connected.add(new RapidoidChannel(socketChannel, false, serverProtocol));
		selector.wakeup();
	}

	public void connect(ConnectionTarget target) throws IOException {

		configureSocket(target.socketChannel);

		connecting.add(target);

		if (target.socketChannel.connect(target.addr)) {
			Log.info("Opened socket, connected", "address", target.addr);
		} else {
			Log.info("Opened socket, connecting...", "address", target.addr);
		}

		selector.wakeup();
	}

	private void configureSocket(SocketChannel socketChannel) throws IOException, SocketException {
		socketChannel.configureBlocking(false);

		Socket socket = socketChannel.socket();
		socket.setTcpNoDelay(noDelay);
		socket.setReceiveBufferSize(bufSize);
		socket.setSendBufferSize(bufSize);
		socket.setReuseAddress(true);
	}

	@Override
	protected void connectOP(SelectionKey key) throws IOException {
		U.must(key.isConnectable());

		SocketChannel socketChannel = (SocketChannel) key.channel();
		if (!socketChannel.isConnectionPending()) {
			// not ready to retrieve the connection status
			return;
		}

		ConnectionTarget target = (ConnectionTarget) key.attachment();

		boolean ready;
		try {
			ready = socketChannel.finishConnect();
			U.rteIf(!ready, "Expected an established connection!");

			Log.info("Connected", "address", target.addr);

			connected.add(new RapidoidChannel(socketChannel, true, target.protocol, target.holder,
					target.autoreconnecting, target.state));
		} catch (ConnectException e) {
			retryConnecting(target);
		}
	}

	private void retryConnecting(ConnectionTarget target) throws IOException {
		Log.warn("Reconnecting...", "address", target.addr);
		target.socketChannel = SocketChannel.open();
		target.retryAfter = U.time() + 1000;
		connect(target);
	}

	@Override
	protected void readOP(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();
		RapidoidConnection conn = (RapidoidConnection) key.attachment();

		if (conn.input.size() < bufSizeLimit) {
			long read;
			try {
				read = conn.input.append(socketChannel);
			} catch (Exception e) {
				read = -1;
			}

			if (read == -1) {
				// the other end closed the connection
				Log.debug("The other end closed the connection!");
				conn.closing = true;
			}

			dataIn.value(read);
		}

		process(conn);

		if (conn.closing) {
			if (conn.shouldReconnect()) {
				reconnect(conn);
			} else {
				close(key);
			}
		}
	}

	private void reconnect(RapidoidConnection conn) throws IOException {
		SelectionKey key = conn.key;
		InetSocketAddress addr = conn.getAddress();
		Protocol protocol = conn.getProtocol();
		ChannelHolderImpl holder = conn.getHolder();
		ConnState state = conn.state().copy();

		holder.closed();
		close(key);

		retryConnecting(new ConnectionTarget(null, addr, protocol, holder, true, state));
	}

	public void process(RapidoidConnection conn) {
		messagesProcessed += processMsgs(conn);

		conn.completedInputPos = conn.input.position();
	}

	private long processMsgs(RapidoidConnection conn) {
		long reqN = 0;

		while (reqN < maxPipelineSize && conn.input().hasRemaining() && processNext(conn, false, false)) {
			reqN++;
		}

		return reqN;
	}

	private boolean processNext(RapidoidConnection conn, boolean initial, boolean write) {

		conn.log(initial ? "<< INIT >>" : "<< PROCESS >>");

		U.must(initial || write || conn.input().hasRemaining());

		// prepare for a rollback in case the message isn't complete yet
		conn.input().checkpoint(conn.input().position());
		long limit = conn.input().limit();
		long osize = conn.output().size();

		ConnState state = conn.state();
		long stateN = state.n;
		Object stateObj = state.obj;

		try {

			conn.done = false;

			if (EXTRA_SAFE) {
				processNextExtraSafe(conn);
			} else {
				conn.getProtocol().process(conn);
			}

			if (!conn.closed && !conn.isAsync()) {
				conn.done();
			}

			conn.input().deleteBefore(conn.input().checkpoint());

			Log.debug("Completed message processing");
			return true;

		} catch (IncompleteReadException e) {

			Log.debug("Incomplete message");
			conn.log("<< ROLLBACK >>");

			// input not complete, so rollback
			conn.input().position(conn.input().checkpoint());
			conn.input().limit(limit);

			// TODO use checkpoint for the write
			conn.output().deleteAfter(osize);

			state.n = stateN;
			state.obj = stateObj;

		} catch (ProtocolException e) {

			conn.log("<< PROTOCOL ERROR >>");
			Log.warn("Protocol error", "error", e);
			conn.output().deleteAfter(osize);
			conn.write(U.or(e.getMessage(), "Protocol error!"));
			conn.error();
			conn.close(true);

		} catch (Throwable e) {

			conn.log("<< ERROR >>");
			Log.error("Failed to process message!", e);
			conn.close(true);
		} finally {
			conn.input().checkpoint(-1);
		}

		return false;
	}

	private void processNextExtraSafe(RapidoidConnection conn) {
		if (Ctxs.hasContext()) {
			Log.warn("Detected unclosed context before processing message!");
			Ctxs.close();
		}

		try {
			conn.getProtocol().process(conn);
		} finally {
			if (Ctxs.hasContext()) {
				Log.warn("Detected unclosed context after processing message!");
				Ctxs.close();
			}
		}
	}

	public void close(RapidoidConnection conn) {
		close(conn.key);
	}

	private void close(SelectionKey key) {
		try {
			Object attachment = key.attachment();

			clearKey(key);

			if (attachment instanceof RapidoidConnection) {
				RapidoidConnection conn = (RapidoidConnection) attachment;

				if (conn != null) {
					if (!conn.closed) {
						Log.trace("Closing connection", "connection", conn);
						assert conn.key == key;
						conn.reset();
						connections.release(conn);
					}
				}
			}

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void clearKey(SelectionKey key) throws IOException {
		if (key.isValid()) {
			SocketChannel socketChannel = (SocketChannel) key.channel();
			socketChannel.close();
			key.attach(null);
			key.cancel();
		}
	}

	@Override
	protected void writeOP(SelectionKey key) throws IOException {
		RapidoidConnection conn = (RapidoidConnection) key.attachment();
		SocketChannel socketChannel = (SocketChannel) key.channel();

		checkOnSameThread();

		try {
			long wrote = conn.output.writeTo(socketChannel);
			assert wrote >= 0;

			conn.output.deleteBefore(wrote);
			dataOut.value(wrote);

			boolean complete = conn.output.size() == 0;

			if (conn.closeAfterWrite() && complete) {
				close(conn);
			} else {
				if (complete) {
					key.interestOps(conn.mode != 0 ? conn.mode : conn.nextOp);
					processNext(conn, false, true);
				} else {
					key.interestOps(conn.mode != 0 ? conn.mode : (SelectionKey.OP_READ + SelectionKey.OP_WRITE));
				}
				conn.wrote(complete);
			}
		} catch (IOException e) {
			close(conn);
		}
	}

	public void wantToWrite(RapidoidConnection conn) {
		U.must(conn.mode != SelectionKey.OP_READ);

		if (onSameThread()) {
			conn.key.interestOps(SelectionKey.OP_WRITE);
		} else {
			wantToWriteAsync(conn);
		}
	}

	private void wantToWriteAsync(RapidoidConnection conn) {
		synchronized (waitingToWrite) {
			waitingToWrite.add(conn);
		}

		selector.wakeup();
	}

	@Override
	protected void doProcessing() {

		long now = U.time();
		int connectingN = connecting.size();

		for (int i = 0; i < connectingN; i++) {
			ConnectionTarget target = connecting.poll();
			assert target != null;

			if (target.retryAfter < now) {
				Log.debug("connecting", "address", target.addr);

				try {
					SelectionKey newKey = target.socketChannel.register(selector, SelectionKey.OP_CONNECT);
					newKey.attach(target);
				} catch (ClosedChannelException e) {
					Log.warn("Closed channel", e);
				}
			} else {
				connecting.add(target);
			}
		}

		RapidoidChannel channel;

		while ((channel = connected.poll()) != null) {

			SocketChannel socketChannel = channel.socketChannel;
			Log.debug("connected", "address", socketChannel.socket().getRemoteSocketAddress());

			try {
				SelectionKey newKey = socketChannel.register(selector, SelectionKey.OP_READ);
				U.notNull(channel.protocol, "protocol");
				RapidoidConnection conn = attachConn(newKey, channel.protocol);

				conn.setClient(channel.isClient);
				conn.setAutoReconnect(channel.autoreconnecting);

				bindChannelToHolder(conn, channel.holder);

				if (channel.state != null) {
					conn.state().copyFrom(channel.state);
				}

				try {
					processNext(conn, true, false);
				} finally {
					conn.setInitial(false);
				}

			} catch (ClosedChannelException e) {
				Log.warn("Closed channel", e);
			}
		}

		RapidoidConnection restartedConn;
		while ((restartedConn = restarting.poll()) != null) {
			Log.debug("restarting", "connection", restartedConn);

			processNext(restartedConn, true, false);
		}

		synchronized (waitingToWrite) {
			for (int i = 0; i < waitingToWrite.size(); i++) {
				RapidoidConnection conn = waitingToWrite.get(i);
				if (conn.key != null && conn.key.isValid()) {
					conn.key.interestOps(SelectionKey.OP_WRITE);
				}
			}
			waitingToWrite.clear();
		}
	}

	private void bindChannelToHolder(RapidoidConnection conn, ChannelHolderImpl holder) {
		conn.setHolder(holder);
		if (holder != null) {
			holder.setChannel(conn);
		}
	}

	private RapidoidConnection attachConn(SelectionKey key, Protocol protocol) {
		U.notNull(key, "protocol");
		U.notNull(protocol, "protocol");

		Object attachment = key.attachment();
		assert attachment == null || attachment instanceof ConnectionTarget;

		RapidoidConnection conn = connections.get();

		// the connection is reset when closed
		// but a protocol can modify the connection after closing it
		// so it is reset again before reuse
		conn.reset();

		U.must(conn.closed);
		conn.closed = false;

		conn.key = key;
		conn.setProtocol(protocol);

		if (protocol instanceof CtxListener) {
			conn.setListener((CtxListener) protocol);
		}

		key.attach(conn);

		return conn;
	}

	@Override
	protected void failedOP(SelectionKey key, Throwable e) {
		Log.error("Network error", e);
		close(key);
	}

	public void restart(RapidoidConnection conn) {
		restarting.add(conn);
	}

	public RapidoidConnection newConnection() {
		return new RapidoidConnection(RapidoidWorker.this, bufs);
	}

	public long getMessagesProcessed() {
		return messagesProcessed;
	}

	@Override
	protected long getSelectorTimeout() {
		return selectorTimeout;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy