/* * 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.hash.StatelessBuilder; import net.openhft.chronicle.hash.exceptions.IORuntimeException; import net.openhft.chronicle.hash.exceptions.TimeoutRuntimeException; import net.openhft.lang.io.ByteBufferBytes; import net.openhft.lang.io.Bytes; import net.openhft.lang.threadlocal.ThreadLocalCopies; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.SocketChannel; import java.util.*; import static java.nio.ByteBuffer.allocateDirect; import static net.openhft.chronicle.map.AbstractChannelReplicator.SIZE_OF_SIZE; import static net.openhft.chronicle.map.AbstractChannelReplicator.SIZE_OF_TRANSACTIONID; import static net.openhft.chronicle.map.StatelessChronicleMap.EventId.*; /** * @author Rob Austin. */ class StatelessChronicleMap implements ChronicleMap, Closeable { private static final Logger LOG = LoggerFactory.getLogger(StatelessChronicleMap.class); public static final byte STATELESS_CLIENT_IDENTIFIER = (byte) -127; private final byte[] connectionByte = new byte[1]; private final ByteBuffer connectionOutBuffer = ByteBuffer.wrap(connectionByte); private ByteBuffer buffer; private ByteBufferBytes bytes; private final KeyValueSerializer keyValueSerializer; private volatile SocketChannel clientChannel; private volatile CloseablesManager closeables; private final StatelessBuilder builder; private int maxEntrySize; private final Class kClass; private final Class vClass; private final boolean putReturnsNull; private final boolean removeReturnsNull; static enum EventId { HEARTBEAT, STATEFUL_UPDATE, LONG_SIZE, SIZE, IS_EMPTY, CONTAINS_KEY, CONTAINS_VALUE, GET, PUT, PUT_WITHOUT_ACC, REMOVE, REMOVE_WITHOUT_ACC, CLEAR, KEY_SET, VALUES, ENTRY_SET, REPLACE, REPLACE_WITH_OLD_AND_NEW_VALUE, PUT_IF_ABSENT, REMOVE_WITH_VALUE, TO_STRING, PUT_ALL, PUT_ALL_WITHOUT_ACC, HASH_CODE } private long transactionID; StatelessChronicleMap(final StatelessBuilder builder, final AbstractChronicleMapBuilder chronicleMapBuilder) throws IOException { this.builder = builder; this.keyValueSerializer = new KeyValueSerializer(chronicleMapBuilder.keyBuilder, chronicleMapBuilder.valueBuilder); this.putReturnsNull = chronicleMapBuilder.putReturnsNull(); this.removeReturnsNull = chronicleMapBuilder.removeReturnsNull(); this.maxEntrySize = chronicleMapBuilder.entrySize(); this.vClass = chronicleMapBuilder.valueBuilder.eClass; this.kClass = chronicleMapBuilder.keyBuilder.eClass; // this.putReturnsNull = putReturnsNull; // this.removeReturnsNull = removeReturnsNull; attemptConnect(builder.remoteAddress()); buffer = allocateDirect(maxEntrySize).order(ByteOrder.nativeOrder()); bytes = new ByteBufferBytes(buffer.slice()); } private SocketChannel lazyConnect(final long timeoutMs, final InetSocketAddress remoteAddress) throws IOException { if (LOG.isDebugEnabled()) LOG.debug("attempting to connect to " + remoteAddress); SocketChannel result = null; long timeoutAt = System.currentTimeMillis() + timeoutMs; for (; ; ) { checkTimeout(timeoutAt); // ensures that the excising connection are closed closeExisting(); try { result = AbstractChannelReplicator.openSocketChannel(closeables); result.connect(builder.remoteAddress()); doHandShaking(result); break; } catch (IOException e) { closeables.closeQuietly(); } catch (Exception e) { closeables.closeQuietly(); throw e; } } return result; } /** * attempts a single connect without a timeout and eat any IOException used typically in the constructor, * its possible the server is not running with this instance is created, {@link * StatelessChronicleMap#lazyConnect(long, java.net.InetSocketAddress)} will attempt to establish the * connection when the client make the first map method call. * * @param remoteAddress * @return * @throws IOException * @see StatelessChronicleMap#lazyConnect(long, java.net.InetSocketAddress) */ private void attemptConnect(final InetSocketAddress remoteAddress) throws IOException { // ensures that the excising connection are closed closeExisting(); try { clientChannel = AbstractChannelReplicator.openSocketChannel(closeables); clientChannel.connect(remoteAddress); doHandShaking(clientChannel); } catch (IOException e) { closeables.closeQuietly(); } } /** * closes the existing connections and establishes a new closeables */ private void closeExisting() { // ensure that any excising connection are first closed if (closeables != null) closeables.closeQuietly(); closeables = new CloseablesManager(); } /** * initiates a very simple level of handshaking with the remote server, we send a special ID of -127 ( * when the server receives this it knows its dealing with a stateless client, receive back an identifier * from the server * * @param clientChannel * @throws IOException */ private void doHandShaking(@NotNull final SocketChannel clientChannel) throws IOException { connectionByte[0] = STATELESS_CLIENT_IDENTIFIER; this.connectionOutBuffer.clear(); long timeoutTime = System.currentTimeMillis() + builder.timeoutMs(); // write a single byte while (connectionOutBuffer.hasRemaining()) { clientChannel.write(connectionOutBuffer); checkTimeout(timeoutTime); } this.connectionOutBuffer.clear(); // read a single byte back while (this.connectionOutBuffer.position() <= 0) { clientChannel.read(this.connectionOutBuffer); checkTimeout(timeoutTime); } byte remoteIdentifier = connectionByte[0]; if (LOG.isDebugEnabled()) LOG.debug("Attached to a map with a remote identifier=" + remoteIdentifier); } public File file() { throw new UnsupportedOperationException(); } public synchronized void close() { if (closeables != null) closeables.closeQuietly(); closeables = null; } /** * the transaction id are generated as unique timestamps * * @param time in milliseconds * @return a unique transactionId */ private long nextUniqueTransaction(long time) { transactionID = (time == transactionID) ? time + 1 : time; return transactionID; } public synchronized V putIfAbsent(K key, V value) { final long sizeLocation = writeEventAnSkip(PUT_IF_ABSENT); final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); writeKey(key, local); writeValue(value, local); return readKey(sizeLocation, local); } public synchronized boolean remove(Object key, Object value) { if (key == null) throw new NullPointerException("key can not be null"); final long sizeLocation = writeEventAnSkip(REMOVE_WITH_VALUE); final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); writeKey((K) key, local); writeValue((V) value, local); // get the data back from the server return blockingFetch(sizeLocation).readBoolean(); } public synchronized boolean replace(K key, V oldValue, V newValue) { if (key == null) throw new NullPointerException("key can not be null"); final long sizeLocation = writeEventAnSkip(REPLACE_WITH_OLD_AND_NEW_VALUE); final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); writeKey(key, local); writeValue(oldValue, local); writeValue(newValue, local); // get the data back from the server return blockingFetch(sizeLocation).readBoolean(); } public synchronized V replace(K key, V value) { if (key == null) throw new NullPointerException("key can not be null"); final long sizeLocation = writeEventAnSkip(REPLACE); final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); writeKey(key, local); writeValue(value, local); // get the data back from the server return readKey(sizeLocation, local); } public synchronized int size() { return (int) longSize(); } /** * calling this method should be avoided at all cost, as the entire {@code object} is serialized. This * equals can be used to compare map that extends ChronicleMap. So two Chronicle Maps that contain the * same data are considered equal, even if the instances of the chronicle maps were of different types * * @param object the object that you are comparing against * @return true if the contain the same data */ @Override public synchronized boolean equals(Object object) { if (this == object) return true; if (object == null || object.getClass().isAssignableFrom(Map.class)) return false; final Map that = (Map) object; final int size = size(); if (that.size() != size) return false; final Set> entries = entrySet(); return that.entrySet().equals(entries); } @Override public synchronized int hashCode() { return blockingFetch(writeEventAnSkip(HASH_CODE)).readInt(); } public synchronized String toString() { return blockingFetch(writeEventAnSkip(TO_STRING)).readObject(String.class); } public synchronized boolean isEmpty() { return blockingFetch(writeEventAnSkip(IS_EMPTY)).readBoolean(); } public synchronized boolean containsKey(Object key) { final long sizeLocation = writeEventAnSkip(CONTAINS_KEY); writeKey((K) key); // get the data back from the server return blockingFetch(sizeLocation).readBoolean(); } public synchronized boolean containsValue(Object value) { final long sizeLocation = writeEventAnSkip(CONTAINS_VALUE); writeValue((V) value); // get the data back from the server return blockingFetch(sizeLocation).readBoolean(); } public synchronized long longSize() { return blockingFetch(writeEventAnSkip(LONG_SIZE)).readLong(); } public synchronized V get(Object key) { final long sizeLocation = writeEventAnSkip(GET); final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); writeKey((K) key, local); // get the data back from the server return readKey(sizeLocation, local); } public synchronized V getUsing(K key, V usingValue) { throw new UnsupportedOperationException("getUsing() is not supported for stateless " + "clients"); } public synchronized V acquireUsing(K key, V usingValue) { throw new UnsupportedOperationException("acquireUsing() is not supported for stateless " + "clients"); } @NotNull @Override public WriteContext acquireUsingLocked(@NotNull K key, @NotNull V usingValue) { throw new UnsupportedOperationException(); } @NotNull @Override public ReadContext getUsingLocked(@NotNull K key, @NotNull V usingValue) { throw new UnsupportedOperationException(); } public synchronized V remove(Object key) { if (key == null) throw new NullPointerException("key can not be null"); final long sizeLocation = (removeReturnsNull) ? writeEvent(REMOVE_WITHOUT_ACC) : writeEventAnSkip(REMOVE); writeKey((K) key); if (removeReturnsNull) { sendWithoutAcc(bytes, sizeLocation); return null; } else // get the data back from the server return readKey(sizeLocation); } public synchronized V put(K key, V value) { final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); final long sizeLocation = (putReturnsNull) ? writeEvent(PUT_WITHOUT_ACC) : writeEventAnSkip(PUT); writeKey(key, local); writeValue(value, local); if (putReturnsNull) { sendWithoutAcc(bytes, sizeLocation); return null; } else // get the data back from the server return readKey(sizeLocation, local); } public synchronized void putAll(Map map) { final long sizeLocation = (putReturnsNull) ? writeEvent(PUT_ALL_WITHOUT_ACC) : writeEventAnSkip(PUT_ALL); writeEntries(map); if (putReturnsNull) sendWithoutAcc(bytes, sizeLocation); else blockingFetch(sizeLocation); } private void sendWithoutAcc(final Bytes bytes, long sizeLocation) { writeSizeAt(sizeLocation); long timeoutTime = builder.timeoutMs() + System.currentTimeMillis(); try { for (; ; ) { if (clientChannel == null) clientChannel = lazyConnect(builder.timeoutMs(), builder.remoteAddress()); try { send(bytes, timeoutTime); return; } catch (java.nio.channels.ClosedChannelException | ClosedConnectionException e) { checkTimeout(timeoutTime); clientChannel = lazyConnect(builder.timeoutMs(), builder.remoteAddress()); } } } catch (IOException e) { close(); throw new IORuntimeException(e); } } private void writeEntries(Map map) { final int numberOfEntries = map.size(); int numberOfEntriesReadSoFar = 0; final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); bytes.writeStopBit(numberOfEntries); assert bytes.limit() == bytes.capacity(); for (final Map.Entry e : map.entrySet()) { numberOfEntriesReadSoFar++; // putAll if a bit more complicated than the others // as the volume of data could cause us to have to resize our buffers resizeIfRequired(numberOfEntries, numberOfEntriesReadSoFar); final long start = bytes.position(); final K key = e.getKey(); final Class keyClass = key.getClass(); if (!kClass.isAssignableFrom(keyClass)) throw new ClassCastException("key=" + key + " is of type=" + keyClass + " " + "and should" + " be of type=" + kClass); writeKey(key, local); final V value = e.getValue(); final Class valueClass = value.getClass(); if (!vClass.isAssignableFrom(valueClass)) throw new ClassCastException("value=" + value + " is of type=" + valueClass + " and " + "should be of type=" + vClass); writeValue(value, local); final int len = (int) (bytes.position() - start); if (len > maxEntrySize) maxEntrySize = len; } } private void resizeIfRequired(int numberOfEntries, int numberOfEntriesReadSoFar) { final long remaining = bytes.remaining(); if (remaining < maxEntrySize) { // todo check this again as I think it may have a bug long estimatedRequiredSize = estimateSize(numberOfEntries, numberOfEntriesReadSoFar); resizeBuffer(estimatedRequiredSize + maxEntrySize); } } /** * estimates the size based on what been completed so far * * @param numberOfEntries * @param numberOfEntriesReadSoFar */ private long estimateSize(int numberOfEntries, int numberOfEntriesReadSoFar) { // we will back the size estimate on what we have so far final double percentageComplete = (double) numberOfEntriesReadSoFar / (double) numberOfEntries; return (long) ((double) bytes.position() / percentageComplete); } void resizeBuffer(long size) { if (size < buffer.capacity()) throw new IllegalStateException("it not possible to resize the buffer smaller"); assert size < Integer.MAX_VALUE; final ByteBuffer result = ByteBuffer.allocateDirect((int) size).order(ByteOrder.nativeOrder()); final int bufferPosition = buffer.position(); final int bufferLimit = buffer.limit(); final long bytesPosition = bytes.position(); bytes = new ByteBufferBytes(result.slice()); buffer.position(0); buffer.limit((int) bytesPosition); for (int i = 0; i < bytesPosition; i++) { result.put(buffer.get()); } buffer = result; buffer.limit(bufferLimit); buffer.position(bufferPosition); assert buffer.capacity() == bytes.capacity(); bytes.limit(bytes.capacity()); bytes.position(bytesPosition); assert buffer.capacity() == size; assert buffer.capacity() == bytes.capacity(); assert bytes.limit() == bytes.capacity(); } public synchronized void clear() { // get the data back from the server blockingFetch(writeEventAnSkip(CLEAR)); } @NotNull public synchronized Collection values() { final long sizeLocation = writeEventAnSkip(VALUES); final long startTime = System.currentTimeMillis(); final long transactionId = nextUniqueTransaction(startTime); final long timeoutTime = System.currentTimeMillis() + builder.timeoutMs(); // get the data back from the server Bytes in = blockingFetch0(sizeLocation, transactionId, startTime); final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); final Collection result = new ArrayList(); for (; ; ) { final boolean hasMoreEntries = in.readBoolean(); // number of entries in this chunk final long size = in.readInt(); for (int i = 0; i < size; i++) { result.add(keyValueSerializer.readValue(in, local)); } if (!hasMoreEntries) break; compact(in); in = blockingFetchReadOnly(timeoutTime, transactionId); } return result; } @NotNull public synchronized Set> entrySet() { final long sizeLocation = writeEventAnSkip(ENTRY_SET); final long startTime = System.currentTimeMillis(); final long transactionId = nextUniqueTransaction(startTime); final long timeoutTime = System.currentTimeMillis() + builder.timeoutMs(); // get the data back from the server Bytes in = blockingFetch0(sizeLocation, transactionId, startTime); final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); final Map result = new HashMap(); for (; ; ) { final boolean hasMoreEntries = in.readBoolean(); // number of entries in this chunk final long size = in.readInt(); for (int i = 0; i < size; i++) { final K k = keyValueSerializer.readKey(in, local); final V v = keyValueSerializer.readValue(in, local); result.put(k, v); } if (!hasMoreEntries) break; compact(in); in = blockingFetchReadOnly(timeoutTime, transactionId); } return result.entrySet(); } private void compact(Bytes in) { if (in.remaining() == 0) { bytes.clear(); bytes.buffer().clear(); } else { buffer.compact(); } } @NotNull public synchronized Set keySet() { final long sizeLocation = writeEventAnSkip(KEY_SET); final long startTime = System.currentTimeMillis(); final long transactionId = nextUniqueTransaction(startTime); final long timeoutTime = System.currentTimeMillis() + builder.timeoutMs(); // get the data back from the server Bytes in = blockingFetch0(sizeLocation, transactionId, startTime); final ThreadLocalCopies local = keyValueSerializer.threadLocalCopies(); final Set result = new HashSet<>(); for (; ; ) { boolean hasMoreEntries = in.readBoolean(); // number of entries in the chunk long size = in.readInt(); for (int i = 0; i < size; i++) { result.add(keyValueSerializer.readKey(in, local)); } if (!hasMoreEntries) break; compact(in); in = blockingFetchReadOnly(timeoutTime, transactionId); } return result; } private long writeEvent(EventId event) { buffer.clear(); bytes.clear(); //bytes.limit(buffer.capacity()); bytes.write((byte) event.ordinal()); long sizeLocation = markSizeLocation(); return sizeLocation; } /** * skips for the transactionid * * @param event * @return */ private long writeEventAnSkip(EventId event) { long sizeLocation = writeEvent(event); // skips for the transaction id bytes.skip(SIZE_OF_TRANSACTIONID); return sizeLocation; } class Entry implements Map.Entry { final K key; final V value; /** * Creates new entry. */ Entry(K k1, V v) { value = v; key = k1; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; StatelessChronicleMap.this.put((K) getKey(), (V) newValue); return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; final Map.Entry e = (Map.Entry) o; final Object k1 = getKey(); final Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } } private Bytes blockingFetch(long sizeLocation) { try { long startTime = System.currentTimeMillis(); return blockingFetchThrowable(sizeLocation, this.builder.timeoutMs(), nextUniqueTransaction(startTime), startTime); } catch (IOException e) { close(); throw new IORuntimeException(e); } catch (Exception e) { close(); throw e; } } private Bytes blockingFetch0(long sizeLocation, final long transactionId, long startTime) { try { return blockingFetchThrowable(sizeLocation, this.builder.timeoutMs(), transactionId, startTime); } catch (IOException e) { close(); throw new IORuntimeException(e); } catch (Exception e) { close(); throw e; } } private Bytes blockingFetchThrowable(long sizeLocation, long timeOutMs, final long transactionId, final long startTime) throws IOException { final long timeoutTime = startTime + timeOutMs; for (; ; ) { if (clientChannel == null) clientChannel = lazyConnect(builder.timeoutMs(), builder.remoteAddress()); try { if (LOG.isDebugEnabled()) LOG.debug("sending data with transactionId=" + transactionId); writeSizeAndTransactionIdAt(sizeLocation, transactionId); // send out all the bytes send(bytes, timeoutTime); bytes.clear(); bytes.buffer().clear(); return blockingFetch(timeoutTime, transactionId); } catch (java.nio.channels.ClosedChannelException | ClosedConnectionException e) { checkTimeout(timeoutTime); clientChannel = lazyConnect(builder.timeoutMs(), builder.remoteAddress()); } } } private Bytes blockingFetchReadOnly(long timeoutTime, final long transactionId) { try { return blockingFetch(timeoutTime, transactionId); } catch (IOException e) { close(); throw new IORuntimeException(e); } catch (Exception e) { close(); throw e; } } private Bytes blockingFetch(long timeoutTime, long transactionId) throws IOException { // the number of bytes in the response final int size = receive(SIZE_OF_SIZE, timeoutTime).readInt(); final int requiredSize = size + SIZE_OF_SIZE; if (bytes.capacity() < requiredSize) { bytes = new ByteBufferBytes(allocateDirect(requiredSize)); } // block until we have received all the byte in this chunk receive(size, timeoutTime); final boolean isException = bytes.readBoolean(); final long inTransactionId = bytes.readLong(); if (inTransactionId != transactionId) throw new IllegalStateException("the received transaction-id=" + inTransactionId + ", does not match the expected transaction-id=" + transactionId); if (isException) throw (RuntimeException) bytes.readObject(); return bytes; } private Bytes receive(int requiredNumberOfBytes, long timeoutTime) throws IOException { while (bytes.buffer().position() < requiredNumberOfBytes) { clientChannel.read(bytes.buffer()); checkTimeout(timeoutTime); } bytes.limit(bytes.buffer().position()); return bytes; } private void send(final Bytes out, long timeoutTime) throws IOException { buffer.limit((int) out.position()); buffer.position(0); while (buffer.remaining() > 0) { clientChannel.write(buffer); checkTimeout(timeoutTime); } out.clear(); buffer.clear(); } private void checkTimeout(long timeoutTime) { if (timeoutTime < System.currentTimeMillis()) throw new TimeoutRuntimeException(); } private void writeSizeAndTransactionIdAt(long locationOfSize, final long transactionId) { final long size = bytes.position() - locationOfSize; final long pos = bytes.position(); bytes.position(locationOfSize); bytes.writeInt((int) size - SIZE_OF_SIZE); bytes.writeLong(transactionId); bytes.position(pos); } private void writeSizeAt(long locationOfSize) { final long size = bytes.position() - locationOfSize; bytes.writeInt(locationOfSize, (int) size - SIZE_OF_SIZE); } private long markSizeLocation() { final long position = bytes.position(); bytes.skip(SIZE_OF_SIZE); return position; } private void writeKey(K key) { try { keyValueSerializer.writeKey(key, bytes); } catch (IllegalArgumentException e) { resizeBuffer(bytes.capacity() + maxEntrySize); } } private void writeKey(K key, ThreadLocalCopies local) { try { keyValueSerializer.writeKey(key, bytes, local); } catch (IllegalArgumentException e) { resizeBuffer(bytes.capacity() + maxEntrySize); } } private V readKey(final long sizeLocation) { return keyValueSerializer.readValue(blockingFetch(sizeLocation), null); } private V readKey(final long sizeLocation, ThreadLocalCopies local) { return keyValueSerializer.readValue(blockingFetch(sizeLocation), local); } private void writeValue(V value) { try { keyValueSerializer.writeValue(value, bytes, null); } catch (IllegalArgumentException e) { resizeBuffer(bytes.capacity() + maxEntrySize); } } private void writeValue(V value, ThreadLocalCopies local) { try { keyValueSerializer.writeValue(value, bytes, local); } catch (IllegalArgumentException e) { resizeBuffer(bytes.capacity() + maxEntrySize); } } }