mirror of
https://github.com/OpenHFT/Chronicle-Map.git
synced 2021-09-13 01:26:12 +00:00
649 lines
20 KiB
Java
649 lines
20 KiB
Java
/*
|
|
* Copyright 2014 Higher Frequency Trading
|
|
*
|
|
* http://www.higherfrequencytrading.com
|
|
*
|
|
* 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 net.openhft.chronicle.map;
|
|
|
|
import com.sun.jdi.connect.spi.ClosedConnectionException;
|
|
import net.openhft.chronicle.bytes.Bytes;
|
|
import net.openhft.chronicle.hash.RemoteCallTimeoutException;
|
|
import net.openhft.chronicle.hash.impl.util.BuildVersion;
|
|
import net.openhft.chronicle.hash.impl.util.CloseablesManager;
|
|
import net.openhft.chronicle.network2.event.EventGroup;
|
|
import net.openhft.chronicle.wire.TextWire;
|
|
import net.openhft.chronicle.wire.Wire;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.io.IOException;
|
|
import java.net.InetSocketAddress;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.channels.SocketChannel;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
|
|
import static net.openhft.chronicle.map.ClientWiredStatelessChronicleMap.EventId.APPLICATION_VERSION;
|
|
import static net.openhft.chronicle.map.MapWireHandler.SIZE_OF_SIZE;
|
|
import static net.openhft.chronicle.map.MapWireHandlerBuilder.Fields.*;
|
|
|
|
/**
|
|
* Created by Rob Austin
|
|
*/
|
|
public class ClientWiredStatelessTcpConnectionHub {
|
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(StatelessChronicleMap.class);
|
|
|
|
protected final String name;
|
|
protected final InetSocketAddress remoteAddress;
|
|
protected final long timeoutMs;
|
|
protected final int tcpBufferSize;
|
|
private final ReentrantLock inBytesLock = new ReentrantLock(true);
|
|
private final ReentrantLock outBytesLock = new ReentrantLock();
|
|
|
|
|
|
@NotNull
|
|
private final AtomicLong transactionID = new AtomicLong(0);
|
|
private final ClientWiredChronicleMapStatelessBuilder config;
|
|
@Nullable
|
|
protected CloseablesManager closeables;
|
|
|
|
private final Wire outWire = new TextWire(Bytes.elasticByteBuffer());
|
|
|
|
long largestChunkSoFar = 0;
|
|
private final Wire intWire = new TextWire(Bytes.elasticByteBuffer());
|
|
|
|
// used by the enterprise version
|
|
protected int localIdentifier;
|
|
|
|
|
|
private SocketChannel clientChannel;
|
|
// this is a transaction id and size that has been read by another thread.
|
|
private volatile long parkedTransactionId;
|
|
|
|
private volatile long parkedTransactionTimeStamp;
|
|
private long limitOfLast = 0;
|
|
|
|
// set up in the header
|
|
private long startTime;
|
|
private boolean doHandShaking;
|
|
|
|
|
|
public ClientWiredStatelessTcpConnectionHub(ClientWiredChronicleMapStatelessBuilder config, byte localIdentifier, boolean doHandShaking) {
|
|
this.localIdentifier = localIdentifier;
|
|
this.doHandShaking = doHandShaking;
|
|
this.remoteAddress = config.remoteAddress();
|
|
this.tcpBufferSize = config.tcpBufferSize();
|
|
this.config = config;
|
|
this.name = config.name();
|
|
this.timeoutMs = config.timeoutMs();
|
|
attemptConnect(remoteAddress);
|
|
checkVersion(config.channelID());
|
|
}
|
|
|
|
private synchronized void attemptConnect(final InetSocketAddress remoteAddress) {
|
|
|
|
// ensures that the excising connection are closed
|
|
closeExisting();
|
|
|
|
try {
|
|
SocketChannel socketChannel = AbstractChannelReplicator.openSocketChannel(closeables);
|
|
|
|
if (socketChannel.connect(remoteAddress)) {
|
|
|
|
clientChannel = socketChannel;
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
if (closeables != null) closeables.closeQuietly();
|
|
clientChannel = null;
|
|
}
|
|
}
|
|
|
|
ReentrantLock inBytesLock() {
|
|
return inBytesLock;
|
|
}
|
|
|
|
ReentrantLock outBytesLock() {
|
|
return outBytesLock;
|
|
}
|
|
|
|
protected void checkVersion(short channelID) {
|
|
|
|
final String serverVersion = serverApplicationVersion(channelID);
|
|
final String clientVersion = clientVersion();
|
|
|
|
if (!serverVersion.equals(clientVersion)) {
|
|
LOG.warn("DIFFERENT CHRONICLE-MAP VERSIONS: The Chronicle-Map-Server and " +
|
|
"Stateless-Client are on different " +
|
|
"versions, " +
|
|
" we suggest that you use the same version, server=" + serverApplicationVersion(channelID) + ", " +
|
|
"client=" + clientVersion);
|
|
}
|
|
}
|
|
|
|
|
|
private void checkTimeout(long timeoutTime) {
|
|
if (timeoutTime < System.currentTimeMillis())
|
|
throw new RemoteCallTimeoutException();
|
|
}
|
|
|
|
protected synchronized void lazyConnect(final long timeoutMs,
|
|
final InetSocketAddress remoteAddress) {
|
|
if (clientChannel != null)
|
|
return;
|
|
|
|
if (LOG.isDebugEnabled())
|
|
LOG.debug("attempting to connect to " + remoteAddress + " ,name=" + name);
|
|
|
|
SocketChannel result;
|
|
|
|
long timeoutAt = System.currentTimeMillis() + timeoutMs;
|
|
|
|
for (; ; ) {
|
|
checkTimeout(timeoutAt);
|
|
|
|
// ensures that the excising connection are closed
|
|
closeExisting();
|
|
|
|
try {
|
|
result = AbstractChannelReplicator.openSocketChannel(closeables);
|
|
if (!result.connect(remoteAddress)) {
|
|
try {
|
|
Thread.sleep(100);
|
|
} catch (InterruptedException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
result.socket().setTcpNoDelay(true);
|
|
if (doHandShaking)
|
|
|
|
break;
|
|
} catch (IOException e) {
|
|
if (closeables != null) closeables.closeQuietly();
|
|
} catch (Exception e) {
|
|
if (closeables != null) closeables.closeQuietly();
|
|
throw e;
|
|
}
|
|
}
|
|
clientChannel = result;
|
|
|
|
checkVersion(config.channelID());
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* closes the existing connections and establishes a new closeables
|
|
*/
|
|
protected void closeExisting() {
|
|
// ensure that any excising connection are first closed
|
|
if (closeables != null)
|
|
closeables.closeQuietly();
|
|
|
|
closeables = new CloseablesManager();
|
|
}
|
|
|
|
|
|
|
|
public synchronized void close() {
|
|
|
|
if (closeables != null)
|
|
closeables.closeQuietly();
|
|
|
|
closeables = null;
|
|
clientChannel = null;
|
|
|
|
}
|
|
|
|
/**
|
|
* the transaction id are generated as unique timestamps
|
|
*
|
|
* @param time in milliseconds
|
|
* @return a unique transactionId
|
|
*/
|
|
long nextUniqueTransaction(long time) {
|
|
long id = time;
|
|
for (; ; ) {
|
|
long old = transactionID.get();
|
|
if (old >= id) id = old + 1;
|
|
if (transactionID.compareAndSet(old, id))
|
|
break;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
@NotNull
|
|
public String serverApplicationVersion(short channelID) {
|
|
TextWire wire = new TextWire(Bytes.elasticByteBuffer());
|
|
String result = proxyReturnString(APPLICATION_VERSION.toString(), channelID, wire);
|
|
return (result == null) ? "" : result;
|
|
}
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
@NotNull
|
|
String clientVersion() {
|
|
return BuildVersion.version();
|
|
}
|
|
|
|
|
|
/**
|
|
* sends data to the server via TCP/IP
|
|
*
|
|
* @param wire the {@code wire} containing the outbound data
|
|
*/
|
|
void writeSocket(@NotNull final Wire wire) {
|
|
|
|
assert outBytesLock().isHeldByCurrentThread();
|
|
assert !inBytesLock().isHeldByCurrentThread();
|
|
|
|
|
|
final long timeoutTime = startTime + this.timeoutMs;
|
|
try {
|
|
|
|
for (; ; ) {
|
|
if (clientChannel == null)
|
|
lazyConnect(timeoutMs, remoteAddress);
|
|
try {
|
|
writeLength(wire);
|
|
|
|
// send out all the bytes
|
|
writeSocket(wire, timeoutTime);
|
|
break;
|
|
|
|
} catch (@NotNull java.nio.channels.ClosedChannelException | ClosedConnectionException e) {
|
|
checkTimeout(timeoutTime);
|
|
lazyConnect(timeoutMs, remoteAddress);
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
close();
|
|
throw new IORuntimeException(e);
|
|
} catch (Exception e) {
|
|
close();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private void writeLength(Wire outWire) {
|
|
assert outBytesLock().isHeldByCurrentThread();
|
|
long position = outWire.bytes().position();
|
|
if (position > Integer.MAX_VALUE || position < Integer.MIN_VALUE)
|
|
throw new IllegalStateException("message too large");
|
|
|
|
long pos = outWire.bytes().position();
|
|
try {
|
|
outWire.bytes().reset();
|
|
int size = (int) (position - outWire.bytes().position());
|
|
outWire.bytes().writeUnsignedShort(size - SIZE_OF_SIZE);
|
|
} finally {
|
|
outWire.bytes().position(pos);
|
|
}
|
|
}
|
|
|
|
protected Wire proxyReply(long timeoutTime, final long transactionId) {
|
|
|
|
assert inBytesLock().isHeldByCurrentThread();
|
|
|
|
try {
|
|
|
|
final Wire wire = proxyReplyThrowable(timeoutTime, transactionId);
|
|
|
|
// handle an exception if the message contains the IS_EXCEPTION field
|
|
if (wire.read(() -> "IS_EXCEPTION").bool()) {
|
|
final String text = wire.read(() -> "EXCEPTION").text();
|
|
throw new RuntimeException(text);
|
|
}
|
|
return wire;
|
|
} catch (IOException e) {
|
|
close();
|
|
throw new IORuntimeException(e);
|
|
} catch (RuntimeException e) {
|
|
close();
|
|
throw e;
|
|
} catch (Exception e) {
|
|
close();
|
|
throw new RuntimeException(e);
|
|
} catch (AssertionError e) {
|
|
LOG.error("name=" + name, e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
|
|
private Wire proxyReplyThrowable(long timeoutTime, long transactionId) throws IOException {
|
|
|
|
assert inBytesLock().isHeldByCurrentThread();
|
|
|
|
for (; ; ) {
|
|
|
|
// read the next item from the socket
|
|
if (parkedTransactionId == 0) {
|
|
|
|
assert parkedTransactionTimeStamp == 0;
|
|
|
|
// if we have processed all the bytes that we have read in
|
|
if (inWireByteBuffer().position() == intWire.bytes().position())
|
|
inWireClear();
|
|
|
|
// todo change the size to include the meta data bit
|
|
// reads just the size
|
|
readSocket(SIZE_OF_SIZE, timeoutTime);
|
|
|
|
final int messageSize = intWire.bytes().readUnsignedShort(intWire.bytes().position());
|
|
|
|
|
|
assert messageSize > 0 : "Invalid message size " + messageSize;
|
|
assert messageSize < 16 << 20 : "Invalid message size " + messageSize;
|
|
|
|
final int remainingBytes0 = messageSize;
|
|
readSocket(remainingBytes0, timeoutTime);
|
|
|
|
|
|
intWire.bytes().skip(SIZE_OF_SIZE);
|
|
intWire.bytes().limit(intWire.bytes().position() + messageSize);
|
|
|
|
System.out.println("\n--------------------------------\n" +
|
|
"client read:\n\n" + Bytes.toDebugString(intWire.bytes()));
|
|
|
|
long transactionId0 = intWire.read(() -> "TRANSACTION_ID").int64();
|
|
|
|
|
|
// check the transaction id is reasonable
|
|
/* assert transactionId0 > 1410000000000L * TcpReplicator.TIMESTAMP_FACTOR :
|
|
"TransactionId too small " + transactionId0 + " messageSize " + messageSize;
|
|
assert transactionId0 < 2100000000000L * TcpReplicator.TIMESTAMP_FACTOR :
|
|
"TransactionId too large " + transactionId0 + " messageSize " + messageSize;
|
|
*/
|
|
// if the transaction id is for this thread process it
|
|
if (transactionId0 == transactionId) {
|
|
clearParked();
|
|
return intWire;
|
|
|
|
} else {
|
|
|
|
// if the transaction id is not for this thread, park it
|
|
// and allow another thread to pick it up
|
|
parkedTransactionTimeStamp = System.currentTimeMillis();
|
|
parkedTransactionId = transactionId0;
|
|
pause();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// the transaction id was read by another thread, but is for this thread, process it
|
|
if (parkedTransactionId == transactionId) {
|
|
clearParked();
|
|
return intWire;
|
|
}
|
|
|
|
// time out the old transaction id
|
|
if (System.currentTimeMillis() - timeoutTime >
|
|
parkedTransactionTimeStamp) {
|
|
|
|
LOG.error("name=" + name, new IllegalStateException("Skipped Message with " +
|
|
"transaction-id=" +
|
|
parkedTransactionTimeStamp +
|
|
", this can occur when you have another thread which has called the " +
|
|
"stateless client and terminated abruptly before the message has been " +
|
|
"returned from the server and hence consumed by the other thread."));
|
|
|
|
// read the the next message
|
|
clearParked();
|
|
pause();
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* clears the wire and its underlying byte buffer
|
|
*/
|
|
private void inWireClear() {
|
|
inWireByteBuffer().clear();
|
|
intWire.bytes().clear();
|
|
}
|
|
|
|
private void clearParked() {
|
|
assert inBytesLock().isHeldByCurrentThread();
|
|
parkedTransactionId = 0;
|
|
parkedTransactionTimeStamp = 0;
|
|
}
|
|
|
|
private void pause() {
|
|
|
|
assert !outBytesLock().isHeldByCurrentThread();
|
|
assert inBytesLock().isHeldByCurrentThread();
|
|
|
|
/// don't call inBytesLock.isHeldByCurrentThread() as it not atomic
|
|
inBytesLock().unlock();
|
|
|
|
// allows another thread to enter hear
|
|
inBytesLock().lock();
|
|
}
|
|
|
|
/**
|
|
* reads up to the number of byte in {@code requiredNumberOfBytes} from the socket
|
|
*
|
|
* @param requiredNumberOfBytes the number of bytes to read
|
|
* @param timeoutTime timeout in milliseconds
|
|
* @return bytes read from the TCP/IP socket
|
|
* @throws java.io.IOException socket failed to read data
|
|
*/
|
|
@SuppressWarnings("UnusedReturnValue")
|
|
private void readSocket(int requiredNumberOfBytes, long timeoutTime) throws IOException {
|
|
|
|
assert inBytesLock().isHeldByCurrentThread();
|
|
ByteBuffer buffer = inWireByteBuffer();
|
|
|
|
buffer.limit(buffer.position() + requiredNumberOfBytes);
|
|
long start = buffer.position();
|
|
|
|
while (buffer.position() - start < requiredNumberOfBytes) {
|
|
|
|
int len = clientChannel.read(buffer);
|
|
|
|
if (len == -1)
|
|
throw new IORuntimeException("Disconnection to server");
|
|
|
|
checkTimeout(timeoutTime);
|
|
}
|
|
}
|
|
|
|
private ByteBuffer inWireByteBuffer() {
|
|
return (ByteBuffer) intWire.bytes().underlyingObject();
|
|
}
|
|
|
|
/**
|
|
* writes the bytes to the socket
|
|
*
|
|
* @param outWire the data that you wish to write
|
|
* @param timeoutTime how long before a we timeout
|
|
* @throws java.io.IOException
|
|
*/
|
|
private void writeSocket(Wire outWire, long timeoutTime) throws IOException {
|
|
|
|
assert outBytesLock().isHeldByCurrentThread();
|
|
assert !inBytesLock().isHeldByCurrentThread();
|
|
|
|
long outBytesPosition = outWire.bytes().position();
|
|
|
|
|
|
// if we have other threads waiting to send and the buffer is not full, let the other threads
|
|
// write to the buffer
|
|
if (outBytesLock().hasQueuedThreads() &&
|
|
outBytesPosition + largestChunkSoFar <= tcpBufferSize) {
|
|
return;
|
|
}
|
|
|
|
final ByteBuffer outBuffer = (ByteBuffer) outWire.bytes().underlyingObject();
|
|
outBuffer.limit((int) outWire.bytes().position());
|
|
outBuffer.position(SIZE_OF_SIZE);
|
|
|
|
if (EventGroup.IS_DEBUG ) {
|
|
System.out.println("\n--------------------------------------------\n" +
|
|
"client wrote:\n\n" + Bytes.toDebugString(outBuffer));
|
|
}
|
|
|
|
outBuffer.position(0);
|
|
|
|
upateLargestChunkSoFarSize(outBuffer);
|
|
|
|
while (outBuffer.remaining() > 0) {
|
|
|
|
int len = clientChannel.write(outBuffer);
|
|
|
|
if (len == -1)
|
|
throw new IORuntimeException("Disconnection to server");
|
|
|
|
|
|
// if we have queued threads then we don't have to write all the bytes as the other
|
|
// threads will write the remains bytes.
|
|
if (outBuffer.remaining() > 0 && outBytesLock().hasQueuedThreads() &&
|
|
outBuffer.remaining() + largestChunkSoFar <= tcpBufferSize) {
|
|
|
|
if (LOG.isDebugEnabled())
|
|
LOG.debug("continuing - without all the data being written to the buffer as " +
|
|
"it will be written by the next thread");
|
|
outBuffer.compact();
|
|
outWire.bytes().limit(outBuffer.limit());
|
|
outWire.bytes().position(outBuffer.position());
|
|
return;
|
|
}
|
|
|
|
checkTimeout(timeoutTime);
|
|
|
|
}
|
|
|
|
outBuffer.clear();
|
|
outWire.bytes().clear();
|
|
|
|
}
|
|
|
|
/**
|
|
* calculates the size of each chunk
|
|
*
|
|
* @param outBuffer
|
|
*/
|
|
private void upateLargestChunkSoFarSize(ByteBuffer outBuffer) {
|
|
int sizeOfThisChunk = (int) (outBuffer.limit() - limitOfLast);
|
|
if (largestChunkSoFar < sizeOfThisChunk)
|
|
largestChunkSoFar = sizeOfThisChunk;
|
|
|
|
limitOfLast = outBuffer.limit();
|
|
}
|
|
|
|
|
|
private long proxySend(@NotNull final String methodName,
|
|
final long startTime,
|
|
short channelID,
|
|
@NotNull final Wire wire) {
|
|
|
|
assert outBytesLock().isHeldByCurrentThread();
|
|
assert !inBytesLock().isHeldByCurrentThread();
|
|
|
|
// send
|
|
outBytesLock().lock();
|
|
try {
|
|
long transactionId = writeHeader(startTime, channelID, wire);
|
|
wire.write(() -> "METHOD_NAME").text(methodName);
|
|
writeSocket(wire);
|
|
return transactionId;
|
|
} finally {
|
|
outBytesLock().unlock();
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("SameParameterValue")
|
|
@Nullable
|
|
String proxyReturnString(@NotNull final String messageId, short channelID) {
|
|
return proxyReturnString(messageId, channelID, outWire);
|
|
}
|
|
|
|
@SuppressWarnings("SameParameterValue")
|
|
@Nullable
|
|
String proxyReturnString(@NotNull final String messageId, short channelID, Wire outWire) {
|
|
final long startTime = System.currentTimeMillis();
|
|
long transactionId;
|
|
|
|
outBytesLock().lock();
|
|
try {
|
|
transactionId = proxySend(messageId, startTime, channelID, outWire);
|
|
} finally {
|
|
outBytesLock().unlock();
|
|
}
|
|
|
|
long timeoutTime = startTime + this.timeoutMs;
|
|
|
|
// receive
|
|
inBytesLock().lock();
|
|
try {
|
|
return proxyReply(timeoutTime, transactionId).read(() -> "RESULT").text();
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
} finally {
|
|
inBytesLock().unlock();
|
|
}
|
|
}
|
|
|
|
|
|
Wire outWire() {
|
|
assert outBytesLock().isHeldByCurrentThread();
|
|
return outWire;
|
|
}
|
|
|
|
long writeHeader(long startTime, short channelID, Wire wire) {
|
|
|
|
assert outBytesLock().isHeldByCurrentThread();
|
|
markSize(wire);
|
|
startTime(startTime);
|
|
|
|
long transactionId = nextUniqueTransaction(startTime);
|
|
wire.write(TYPE).text("MAP");
|
|
wire.write(TRANSACTION_ID).int64(transactionId);
|
|
wire.write(TIME_STAMP).int64(startTime);
|
|
wire.write(CHANNEL_ID).int16(channelID);
|
|
|
|
return transactionId;
|
|
}
|
|
|
|
|
|
/**
|
|
* mark the location of the outWire size
|
|
*
|
|
* @param outWire
|
|
*/
|
|
void markSize(Wire outWire) {
|
|
|
|
assert outBytesLock().isHeldByCurrentThread();
|
|
|
|
// this is where the size will go
|
|
outWire.bytes().mark();
|
|
|
|
// skip the 2 bytes for the size
|
|
outWire.bytes().skip(2);
|
|
}
|
|
|
|
void startTime(long startTime) {
|
|
this.startTime = startTime;
|
|
}
|
|
}
|