diff --git a/.gitignore b/.gitignore index 0122f1ca..1aaf8978 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +databases/* out/* generate_bundles.rb .cache diff --git a/.travis.yml b/.travis.yml index 53e614cf..2c1a7a84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,19 @@ language: scala scala: - 2.10.4 - - 2.11.0 + - 2.11.7 jdk: - oraclejdk7 - - openjdk7 + - oraclejdk8 services: - postgresql - mysql +cache: + directories: + - vendor/bundle + - $HOME/.m2 + - $HOME/.ivy2 + - $HOME/.sbt before_script: - ./script/prepare_build.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index c80603d2..6ab0d079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,14 @@ # Changelog -## 0.2.17 - in progresss +## 0.2.18 - 2015-08-08 + +* Timeouts implemented queries for MySQL and PostgreSQL - @lifey - #147 + +## 0.2.17 - 2015-07-13 + +* Fixed pool leak issue - @haski +* Fixed date time formatting issue - #142 ## 0.2.16 - 2015-01-04 diff --git a/Procfile b/Procfile index 6c1b0717..1288bcfe 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ -postgresql: postgres -D /Users/mauricio/databases/postgresql +postgresql: postgres -D databases/postgresql mysql: mysqld --log-warnings --console \ No newline at end of file diff --git a/copynetty.sh b/copynetty.sh index 8b412180..391156eb 100755 --- a/copynetty.sh +++ b/copynetty.sh @@ -16,4 +16,7 @@ rm -rf db-async-common/src/main/java/com/github/mauricio/netty/bootstrap cp -r netty/transport/src/main/java/io/netty/bootstrap db-async-common/src/main/java/io/netty rm -rf db-async-common/src/main/java/com/github/mauricio/netty/channel -cp -r netty/transport/src/main/java/io/netty/channel db-async-common/src/main/java/io/netty \ No newline at end of file +cp -r netty/transport/src/main/java/io/netty/channel db-async-common/src/main/java/io/netty + +cp -r db-async-common/src/main/java/io/netty/* db-async-common/src/main/java/com/github/mauricio/netty +rm -rf db-async-common/src/main/java/io/netty \ No newline at end of file diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/AbstractBootstrap.java b/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/AbstractBootstrap.java index 37a4c382..ca2258eb 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/AbstractBootstrap.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/AbstractBootstrap.java @@ -46,7 +46,7 @@ */ public abstract class AbstractBootstrap, C extends Channel> implements Cloneable { - private volatile EventLoopGroup group; + volatile EventLoopGroup group; private volatile ChannelFactory channelFactory; private volatile SocketAddress localAddress; private final Map, Object> options = new LinkedHashMap, Object>(); @@ -71,7 +71,7 @@ public abstract class AbstractBootstrap, C ext } /** - * The {@link EventLoopGroup} which is used to handle all the events for the to-be-creates + * The {@link EventLoopGroup} which is used to handle all the events for the to-be-created * {@link Channel} */ @SuppressWarnings("unchecked") @@ -174,6 +174,7 @@ public B option(ChannelOption option, T value) { * Allow to specify an initial attribute of the newly created {@link Channel}. If the {@code value} is * {@code null}, the attribute of the specified {@code key} is removed. */ + @SuppressWarnings("unchecked") public B attr(AttributeKey key, T value) { if (key == null) { throw new NullPointerException("key"); @@ -187,10 +188,7 @@ public B attr(AttributeKey key, T value) { attrs.put(key, value); } } - - @SuppressWarnings("unchecked") - B b = (B) this; - return b; + return (B) this; } /** @@ -277,7 +275,7 @@ private ChannelFuture doBind(final SocketAddress localAddress) { } if (regFuture.isDone()) { - // At this point we know that the registration was complete and succesful. + // At this point we know that the registration was complete and successful. ChannelPromise promise = channel.newPromise(); doBind0(regFuture, channel, localAddress, promise); return promise; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/Bootstrap.java b/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/Bootstrap.java index 123abc54..13c3e446 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/Bootstrap.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/Bootstrap.java @@ -21,6 +21,7 @@ import com.github.mauricio.netty.channel.ChannelOption; import com.github.mauricio.netty.channel.ChannelPipeline; import com.github.mauricio.netty.channel.ChannelPromise; +import com.github.mauricio.netty.channel.EventLoopGroup; import com.github.mauricio.netty.util.AttributeKey; import com.github.mauricio.netty.util.internal.logging.InternalLogger; import com.github.mauricio.netty.util.internal.logging.InternalLoggerFactory; @@ -216,6 +217,17 @@ public Bootstrap clone() { return new Bootstrap(this); } + /** + * Returns a deep clone of this bootstrap which has the identical configuration except that it uses + * the given {@link EventLoopGroup}. This method is useful when making multiple {@link Channel}s with similar + * settings. + */ + public Bootstrap clone(EventLoopGroup group) { + Bootstrap bs = new Bootstrap(this); + bs.group = group; + return bs; + } + @Override public String toString() { if (remoteAddress == null) { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/ServerBootstrap.java b/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/ServerBootstrap.java index a2316966..8de3b7aa 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/ServerBootstrap.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/bootstrap/ServerBootstrap.java @@ -75,7 +75,7 @@ public ServerBootstrap group(EventLoopGroup group) { /** * Set the {@link EventLoopGroup} for the parent (acceptor) and the child (client). These - * {@link EventLoopGroup}'s are used to handle all the events and IO for {@link SocketChannel} and + * {@link EventLoopGroup}'s are used to handle all the events and IO for {@link ServerChannel} and * {@link Channel}'s. */ public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/AbstractByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/AbstractByteBuf.java index d42246e9..06d8e2ea 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/AbstractByteBuf.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/AbstractByteBuf.java @@ -1178,4 +1178,8 @@ protected final void ensureAccessible() { throw new IllegalReferenceCountException(0); } } + + void discardMarks() { + markedReaderIndex = markedWriterIndex = 0; + } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/AbstractDerivedByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/AbstractDerivedByteBuf.java index 772e4c54..30d3b588 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/AbstractDerivedByteBuf.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/AbstractDerivedByteBuf.java @@ -47,12 +47,20 @@ public final ByteBuf retain(int increment) { @Override public final boolean release() { - return unwrap().release(); + if (unwrap().release()) { + deallocate(); + return true; + } + return false; } @Override public final boolean release(int decrement) { - return unwrap().release(decrement); + if (unwrap().release(decrement)) { + deallocate(); + return true; + } + return false; } @Override @@ -64,4 +72,9 @@ public ByteBuffer internalNioBuffer(int index, int length) { public ByteBuffer nioBuffer(int index, int length) { return unwrap().nioBuffer(index, length); } + + /** + * Called when the wrapped {@link ByteBuf} was released due calling of {@link #release()} or {@link #release(int)}. + */ + protected void deallocate() { } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/ByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/ByteBuf.java index a6f3f2a6..318dcce5 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/ByteBuf.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/ByteBuf.java @@ -200,6 +200,9 @@ *

* In case a completely fresh copy of an existing buffer is required, please * call {@link #copy()} method instead. + *

+ * Also be aware that obtaining derived buffers will NOT call {@link #retain()} and so the + * reference count will NOT be increased. * *

Conversion to existing JDK types

* @@ -1190,6 +1193,9 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable { * Returns a new slice of this buffer's sub-region starting at the current * {@code readerIndex} and increases the {@code readerIndex} by the size * of the new slice (= {@code length}). + *

+ * Also be aware that this method will NOT call {@link #retain()} and so the + * reference count will NOT be increased. * * @param length the size of the new slice * @@ -1660,6 +1666,9 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable { * identical to {@code buf.slice(buf.readerIndex(), buf.readableBytes())}. * This method does not modify {@code readerIndex} or {@code writerIndex} of * this buffer. + *

+ * Also be aware that this method will NOT call {@link #retain()} and so the + * reference count will NOT be increased. */ public abstract ByteBuf slice(); @@ -1669,6 +1678,9 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable { * they maintain separate indexes and marks. * This method does not modify {@code readerIndex} or {@code writerIndex} of * this buffer. + *

+ * Also be aware that this method will NOT call {@link #retain()} and so the + * reference count will NOT be increased. */ public abstract ByteBuf slice(int index, int length); @@ -1679,6 +1691,9 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable { * This method is identical to {@code buf.slice(0, buf.capacity())}. * This method does not modify {@code readerIndex} or {@code writerIndex} of * this buffer. + *

+ * The reader and writer marks will not be duplicated. Also be aware that this method will + * NOT call {@link #retain()} and so the reference count will NOT be increased. */ public abstract ByteBuf duplicate(); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/ByteBufUtil.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/ByteBufUtil.java index a6edf0a5..2b2589bd 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/ByteBufUtil.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/ByteBufUtil.java @@ -18,7 +18,9 @@ import com.github.mauricio.netty.util.CharsetUtil; import com.github.mauricio.netty.util.Recycler; import com.github.mauricio.netty.util.Recycler.Handle; +import com.github.mauricio.netty.util.internal.ObjectUtil; import com.github.mauricio.netty.util.internal.PlatformDependent; +import com.github.mauricio.netty.util.internal.StringUtil; import com.github.mauricio.netty.util.internal.SystemPropertyUtil; import com.github.mauricio.netty.util.internal.logging.InternalLogger; import com.github.mauricio.netty.util.internal.logging.InternalLoggerFactory; @@ -34,13 +36,20 @@ import java.util.Locale; /** - * A collection of utility methods that is related with handling {@link ByteBuf}. + * A collection of utility methods that is related with handling {@link ByteBuf}, + * such as the generation of hex dump and swapping an integer's byte order. */ public final class ByteBufUtil { private static final InternalLogger logger = InternalLoggerFactory.getInstance(ByteBufUtil.class); private static final char[] HEXDUMP_TABLE = new char[256 * 4]; + private static final String NEWLINE = StringUtil.NEWLINE; + private static final String[] BYTE2HEX = new String[256]; + private static final String[] HEXPADDING = new String[16]; + private static final String[] BYTEPADDING = new String[16]; + private static final char[] BYTE2CHAR = new char[256]; + private static final String[] HEXDUMP_ROWPREFIXES = new String[65536 >>> 4]; static final ByteBufAllocator DEFAULT_ALLOCATOR; @@ -53,23 +62,69 @@ public final class ByteBufUtil { HEXDUMP_TABLE[(i << 1) + 1] = DIGITS[i & 0x0F]; } - String allocType = SystemPropertyUtil.get("com.github.mauricio.netty.allocator.type", "unpooled").toLowerCase(Locale.US).trim(); + int i; + + // Generate the lookup table for byte-to-hex-dump conversion + for (i = 0; i < BYTE2HEX.length; i ++) { + BYTE2HEX[i] = ' ' + StringUtil.byteToHexStringPadded(i); + } + + // Generate the lookup table for hex dump paddings + for (i = 0; i < HEXPADDING.length; i ++) { + int padding = HEXPADDING.length - i; + StringBuilder buf = new StringBuilder(padding * 3); + for (int j = 0; j < padding; j ++) { + buf.append(" "); + } + HEXPADDING[i] = buf.toString(); + } + + // Generate the lookup table for byte dump paddings + for (i = 0; i < BYTEPADDING.length; i ++) { + int padding = BYTEPADDING.length - i; + StringBuilder buf = new StringBuilder(padding); + for (int j = 0; j < padding; j ++) { + buf.append(' '); + } + BYTEPADDING[i] = buf.toString(); + } + + // Generate the lookup table for byte-to-char conversion + for (i = 0; i < BYTE2CHAR.length; i ++) { + if (i <= 0x1f || i >= 0x7f) { + BYTE2CHAR[i] = '.'; + } else { + BYTE2CHAR[i] = (char) i; + } + } + + // Generate the lookup table for the start-offset header in each row (up to 64KiB). + for (i = 0; i < HEXDUMP_ROWPREFIXES.length; i ++) { + StringBuilder buf = new StringBuilder(12); + buf.append(NEWLINE); + buf.append(Long.toHexString(i << 4 & 0xFFFFFFFFL | 0x100000000L)); + buf.setCharAt(buf.length() - 9, '|'); + buf.append('|'); + HEXDUMP_ROWPREFIXES[i] = buf.toString(); + } + + String allocType = SystemPropertyUtil.get("io.netty.allocator.type", "unpooled").toLowerCase(Locale.US).trim(); ByteBufAllocator alloc; if ("unpooled".equals(allocType)) { alloc = UnpooledByteBufAllocator.DEFAULT; - logger.debug("-Dcom.github.mauricio.netty.allocator.type: {}", allocType); + logger.debug("-Dio.netty.allocator.type: {}", allocType); } else if ("pooled".equals(allocType)) { alloc = PooledByteBufAllocator.DEFAULT; - logger.debug("-Dcom.github.mauricio.netty.allocator.type: {}", allocType); + logger.debug("-Dio.netty.allocator.type: {}", allocType); } else { alloc = UnpooledByteBufAllocator.DEFAULT; - logger.debug("-Dcom.github.mauricio.netty.allocator.type: unpooled (unknown: {})", allocType); + logger.debug("-Dio.netty.allocator.type: unpooled (unknown: {})", allocType); } DEFAULT_ALLOCATOR = alloc; - THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("com.github.mauricio.netty.threadLocalDirectBufferSize", 64 * 1024); - logger.debug("-Dcom.github.mauricio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE); + THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 64 * 1024); + logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE); } /** @@ -311,13 +366,7 @@ private static int firstIndexOf(ByteBuf buffer, int fromIndex, int toIndex, byte return -1; } - for (int i = fromIndex; i < toIndex; i ++) { - if (buffer.getByte(i) == value) { - return i; - } - } - - return -1; + return buffer.forEachByte(fromIndex, toIndex - fromIndex, new IndexOfProcessor(value)); } private static int lastIndexOf(ByteBuf buffer, int fromIndex, int toIndex, byte value) { @@ -326,13 +375,7 @@ private static int lastIndexOf(ByteBuf buffer, int fromIndex, int toIndex, byte return -1; } - for (int i = fromIndex - 1; i >= toIndex; i --) { - if (buffer.getByte(i) == value) { - return i; - } - } - - return -1; + return buffer.forEachByteDesc(toIndex, fromIndex - toIndex, new IndexOfProcessor(value)); } /** @@ -481,6 +524,118 @@ static String decodeString(ByteBuffer src, Charset charset) { return dst.flip().toString(); } + /** + * Returns a multi-line hexadecimal dump of the specified {@link ByteBuf} that is easy to read by humans. + */ + public static String prettyHexDump(ByteBuf buffer) { + return prettyHexDump(buffer, buffer.readerIndex(), buffer.readableBytes()); + } + + /** + * Returns a multi-line hexadecimal dump of the specified {@link ByteBuf} that is easy to read by humans, + * starting at the given {@code offset} using the given {@code length}. + */ + public static String prettyHexDump(ByteBuf buffer, int offset, int length) { + if (length == 0) { + return StringUtil.EMPTY_STRING; + } else { + int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4; + StringBuilder buf = new StringBuilder(rows * 80); + appendPrettyHexDump(buf, buffer, offset, length); + return buf.toString(); + } + } + + /** + * Appends the prettified multi-line hexadecimal dump of the specified {@link ByteBuf} to the specified + * {@link StringBuilder} that is easy to read by humans. + */ + public static void appendPrettyHexDump(StringBuilder dump, ByteBuf buf) { + appendPrettyHexDump(dump, buf, buf.readerIndex(), buf.readableBytes()); + } + + /** + * Appends the prettified multi-line hexadecimal dump of the specified {@link ByteBuf} to the specified + * {@link StringBuilder} that is easy to read by humans, starting at the given {@code offset} using + * the given {@code length}. + */ + public static void appendPrettyHexDump(StringBuilder dump, ByteBuf buf, int offset, int length) { + if (offset < 0 || length > ObjectUtil.checkNotNull(buf, "buf").capacity() - offset) { + throw new IndexOutOfBoundsException( + "expected: " + "0 <= offset(" + offset + ") <= offset + length(" + length + + ") <= " + "buf.capacity(" + buf.capacity() + ')'); + } + if (length == 0) { + return; + } + dump.append( + " +-------------------------------------------------+" + + NEWLINE + " | 0 1 2 3 4 5 6 7 8 9 a b c d e f |" + + NEWLINE + "+--------+-------------------------------------------------+----------------+"); + + final int startIndex = offset; + final int fullRows = length >>> 4; + final int remainder = length & 0xF; + + // Dump the rows which have 16 bytes. + for (int row = 0; row < fullRows; row ++) { + int rowStartIndex = (row << 4) + startIndex; + + // Per-row prefix. + appendHexDumpRowPrefix(dump, row, rowStartIndex); + + // Hex dump + int rowEndIndex = rowStartIndex + 16; + for (int j = rowStartIndex; j < rowEndIndex; j ++) { + dump.append(BYTE2HEX[buf.getUnsignedByte(j)]); + } + dump.append(" |"); + + // ASCII dump + for (int j = rowStartIndex; j < rowEndIndex; j ++) { + dump.append(BYTE2CHAR[buf.getUnsignedByte(j)]); + } + dump.append('|'); + } + + // Dump the last row which has less than 16 bytes. + if (remainder != 0) { + int rowStartIndex = (fullRows << 4) + startIndex; + appendHexDumpRowPrefix(dump, fullRows, rowStartIndex); + + // Hex dump + int rowEndIndex = rowStartIndex + remainder; + for (int j = rowStartIndex; j < rowEndIndex; j ++) { + dump.append(BYTE2HEX[buf.getUnsignedByte(j)]); + } + dump.append(HEXPADDING[remainder]); + dump.append(" |"); + + // Ascii dump + for (int j = rowStartIndex; j < rowEndIndex; j ++) { + dump.append(BYTE2CHAR[buf.getUnsignedByte(j)]); + } + dump.append(BYTEPADDING[remainder]); + dump.append('|'); + } + + dump.append(NEWLINE + "+--------+-------------------------------------------------+----------------+"); + } + + /** + * Appends the prefix of each hex dump row. Uses the look-up table for the buffer <= 64 KiB. + */ + private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) { + if (row < HEXDUMP_ROWPREFIXES.length) { + dump.append(HEXDUMP_ROWPREFIXES[row]); + } else { + dump.append(NEWLINE); + dump.append(Long.toHexString(rowStartIndex & 0xFFFFFFFFL | 0x100000000L)); + dump.setCharAt(dump.length() - 9, '|'); + dump.append('|'); + } + } + /** * Returns a cached thread-local direct buffer, if available. * @@ -565,5 +720,18 @@ protected void deallocate() { } } + private static class IndexOfProcessor implements ByteBufProcessor { + private final byte byteToFind; + + public IndexOfProcessor(byte byteToFind) { + this.byteToFind = byteToFind; + } + + @Override + public boolean process(byte value) { + return value != byteToFind; + } + } + private ByteBufUtil() { } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/CompositeByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/CompositeByteBuf.java index f2b48a6e..97de6c25 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/CompositeByteBuf.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/CompositeByteBuf.java @@ -28,18 +28,21 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import java.util.NoSuchElementException; /** * A virtual buffer which shows multiple buffers as a single merged buffer. It is recommended to use * {@link ByteBufAllocator#compositeBuffer()} or {@link Unpooled#wrappedBuffer(ByteBuf...)} instead of calling the * constructor explicitly. */ -public class CompositeByteBuf extends AbstractReferenceCountedByteBuf { +public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements Iterable { private static final ByteBuffer EMPTY_NIO_BUFFER = Unpooled.EMPTY_BUFFER.nioBuffer(); + private static final Iterator EMPTY_ITERATOR = Collections.emptyList().iterator(); private final ResourceLeak leak; private final ByteBufAllocator alloc; @@ -165,9 +168,6 @@ private int addComponent0(int cIndex, ByteBuf buffer) { } int readableBytes = buffer.readableBytes(); - if (readableBytes == 0) { - return cIndex; - } // No need to consolidate - just add a component to the list. Component c = new Component(buffer.order(ByteOrder.BIG_ENDIAN).slice()); @@ -182,7 +182,9 @@ private int addComponent0(int cIndex, ByteBuf buffer) { } } else { components.add(cIndex, c); - updateComponentOffsets(cIndex); + if (readableBytes != 0) { + updateComponentOffsets(cIndex); + } } return cIndex; } @@ -209,31 +211,15 @@ private int addComponents0(int cIndex, ByteBuf... buffers) { throw new NullPointerException("buffers"); } - int readableBytes = 0; - for (ByteBuf b: buffers) { - if (b == null) { - break; - } - readableBytes += b.readableBytes(); - } - - if (readableBytes == 0) { - return cIndex; - } - // No need for consolidation for (ByteBuf b: buffers) { if (b == null) { break; } - if (b.isReadable()) { - cIndex = addComponent0(cIndex, b) + 1; - int size = components.size(); - if (cIndex > size) { - cIndex = size; - } - } else { - b.release(); + cIndex = addComponent0(cIndex, b) + 1; + int size = components.size(); + if (cIndex > size) { + cIndex = size; } } return cIndex; @@ -350,8 +336,12 @@ private void updateComponentOffsets(int cIndex) { */ public CompositeByteBuf removeComponent(int cIndex) { checkComponentIndex(cIndex); - components.remove(cIndex).freeIfNecessary(); - updateComponentOffsets(cIndex); + Component comp = components.remove(cIndex); + comp.freeIfNecessary(); + if (comp.length > 0) { + // Only need to call updateComponentOffsets if the length was > 0 + updateComponentOffsets(cIndex); + } return this; } @@ -364,23 +354,33 @@ public CompositeByteBuf removeComponent(int cIndex) { public CompositeByteBuf removeComponents(int cIndex, int numComponents) { checkComponentIndex(cIndex, numComponents); + if (numComponents == 0) { + return this; + } List toRemove = components.subList(cIndex, cIndex + numComponents); + boolean needsUpdate = false; for (Component c: toRemove) { + if (c.length > 0) { + needsUpdate = true; + } c.freeIfNecessary(); } toRemove.clear(); - updateComponentOffsets(cIndex); + if (needsUpdate) { + // Only need to call updateComponentOffsets if the length was > 0 + updateComponentOffsets(cIndex); + } return this; } + @Override public Iterator iterator() { ensureAccessible(); - List list = new ArrayList(components.size()); - for (Component c: components) { - list.add(c.buf); + if (components.isEmpty()) { + return EMPTY_ITERATOR; } - return list.iterator(); + return new CompositeByteBufIterator(); } /** @@ -1105,6 +1105,7 @@ private Component findComponent(int offset) { } else if (offset < c.offset) { high = mid - 1; } else { + assert c.length != 0; return c; } } @@ -1633,4 +1634,34 @@ protected void deallocate() { public ByteBuf unwrap() { return null; } + + private final class CompositeByteBufIterator implements Iterator { + private final int size = components.size(); + private int index; + + @Override + public boolean hasNext() { + return size > index; + } + + @Override + public ByteBuf next() { + if (size != components.size()) { + throw new ConcurrentModificationException(); + } + if (!hasNext()) { + throw new NoSuchElementException(); + } + try { + return components.get(index++).buf; + } catch (IndexOutOfBoundsException e) { + throw new ConcurrentModificationException(); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read-Only"); + } + } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/DuplicatedByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/DuplicatedByteBuf.java index cf3c1bc1..cfb8c4e5 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/DuplicatedByteBuf.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/DuplicatedByteBuf.java @@ -31,11 +31,19 @@ */ public class DuplicatedByteBuf extends AbstractDerivedByteBuf { - private final ByteBuf buffer; + private ByteBuf buffer; public DuplicatedByteBuf(ByteBuf buffer) { super(buffer.maxCapacity()); + init(buffer); + } + + DuplicatedByteBuf(int maxCapacity) { + super(maxCapacity); + } + final void init(ByteBuf buffer) { + maxCapacity(buffer.maxCapacity()); if (buffer instanceof DuplicatedByteBuf) { this.buffer = ((DuplicatedByteBuf) buffer).buffer; } else { @@ -43,6 +51,8 @@ public DuplicatedByteBuf(ByteBuf buffer) { } setIndex(buffer.readerIndex(), buffer.writerIndex()); + markReaderIndex(); + markWriterIndex(); } @Override diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolArena.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolArena.java index 3e0a7825..c874adea 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolArena.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolArena.java @@ -16,12 +16,22 @@ package com.github.mauricio.netty.buffer; +import com.github.mauricio.netty.util.internal.LongCounter; import com.github.mauricio.netty.util.internal.PlatformDependent; import com.github.mauricio.netty.util.internal.StringUtil; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; -abstract class PoolArena { +abstract class PoolArena implements PoolArenaMetric { + + enum SizeClass { + Tiny, + Small, + Normal + } static final int numTinySubpagePools = 512 >>> 4; @@ -43,6 +53,21 @@ abstract class PoolArena { private final PoolChunkList q075; private final PoolChunkList q100; + private final List chunkListMetrics; + + // Metrics for allocations and deallocations + private long allocationsTiny; + private long allocationsSmall; + private long allocationsNormal; + // We need to use the LongCounter here as this is not guarded via synchronized block. + private final LongCounter allocationsHuge = PlatformDependent.newLongCounter(); + + private long deallocationsTiny; + private long deallocationsSmall; + private long deallocationsNormal; + // We need to use the LongCounter here as this is not guarded via synchronized block. + private final LongCounter deallocationsHuge = PlatformDependent.newLongCounter(); + // TODO: Test if adding padding helps under contention //private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7; @@ -64,19 +89,28 @@ protected PoolArena(PooledByteBufAllocator parent, int pageSize, int maxOrder, i smallSubpagePools[i] = newSubpagePoolHead(pageSize); } - q100 = new PoolChunkList(this, null, 100, Integer.MAX_VALUE); - q075 = new PoolChunkList(this, q100, 75, 100); - q050 = new PoolChunkList(this, q075, 50, 100); - q025 = new PoolChunkList(this, q050, 25, 75); - q000 = new PoolChunkList(this, q025, 1, 50); - qInit = new PoolChunkList(this, q000, Integer.MIN_VALUE, 25); - - q100.prevList = q075; - q075.prevList = q050; - q050.prevList = q025; - q025.prevList = q000; - q000.prevList = null; - qInit.prevList = qInit; + q100 = new PoolChunkList(null, 100, Integer.MAX_VALUE); + q075 = new PoolChunkList(q100, 75, 100); + q050 = new PoolChunkList(q075, 50, 100); + q025 = new PoolChunkList(q050, 25, 75); + q000 = new PoolChunkList(q025, 1, 50); + qInit = new PoolChunkList(q000, Integer.MIN_VALUE, 25); + + q100.prevList(q075); + q075.prevList(q050); + q050.prevList(q025); + q025.prevList(q000); + q000.prevList(null); + qInit.prevList(qInit); + + List metrics = new ArrayList(6); + metrics.add(qInit); + metrics.add(q000); + metrics.add(q025); + metrics.add(q050); + metrics.add(q075); + metrics.add(q100); + chunkListMetrics = Collections.unmodifiableList(metrics); } private PoolSubpage newSubpagePoolHead(int pageSize) { @@ -128,7 +162,8 @@ private void allocate(PoolThreadCache cache, PooledByteBuf buf, final int req if (isTinyOrSmall(normCapacity)) { // capacity < pageSize int tableIdx; PoolSubpage[] table; - if (isTiny(normCapacity)) { // < 512 + boolean tiny = isTiny(normCapacity); + if (tiny) { // < 512 if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) { // was able to allocate out of the cache so move on return; @@ -144,31 +179,45 @@ private void allocate(PoolThreadCache cache, PooledByteBuf buf, final int req table = smallSubpagePools; } - synchronized (this) { - final PoolSubpage head = table[tableIdx]; + final PoolSubpage head = table[tableIdx]; + + /** + * Synchronize on the head. This is needed as {@link PoolSubpage#allocate()} and + * {@link PoolSubpage#free(int)} may modify the doubly linked list as well. + */ + synchronized (head) { final PoolSubpage s = head.next; if (s != head) { assert s.doNotDestroy && s.elemSize == normCapacity; long handle = s.allocate(); assert handle >= 0; s.chunk.initBufWithSubpage(buf, handle, reqCapacity); + + if (tiny) { + ++allocationsTiny; + } else { + ++allocationsSmall; + } return; } } - } else if (normCapacity <= chunkSize) { + allocateNormal(buf, reqCapacity, normCapacity); + return; + } + if (normCapacity <= chunkSize) { if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) { // was able to allocate out of the cache so move on return; } + allocateNormal(buf, reqCapacity, normCapacity); } else { // Huge allocations are never served via the cache so just call allocateHuge allocateHuge(buf, reqCapacity); - return; } - allocateNormal(buf, reqCapacity, normCapacity); } private synchronized void allocateNormal(PooledByteBuf buf, int reqCapacity, int normCapacity) { + ++allocationsNormal; if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) || q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) || q075.allocate(buf, reqCapacity, normCapacity) || q100.allocate(buf, reqCapacity, normCapacity)) { @@ -184,24 +233,53 @@ private synchronized void allocateNormal(PooledByteBuf buf, int reqCapacity, } private void allocateHuge(PooledByteBuf buf, int reqCapacity) { + allocationsHuge.increment(); buf.initUnpooled(newUnpooledChunk(reqCapacity), reqCapacity); } - void free(PoolChunk chunk, long handle, int normCapacity, boolean sameThreads) { + void free(PoolChunk chunk, long handle, int normCapacity, PoolThreadCache cache) { if (chunk.unpooled) { + allocationsHuge.decrement(); destroyChunk(chunk); } else { - if (sameThreads) { - PoolThreadCache cache = parent.threadCache.get(); - if (cache.add(this, chunk, handle, normCapacity)) { - // cached so not free it. - return; - } + SizeClass sizeClass = sizeClass(normCapacity); + if (cache != null && cache.add(this, chunk, handle, normCapacity, sizeClass)) { + // cached so not free it. + return; } - synchronized (this) { - chunk.parent.free(chunk, handle); + freeChunk(chunk, handle, sizeClass); + } + } + + private SizeClass sizeClass(int normCapacity) { + if (!isTinyOrSmall(normCapacity)) { + return SizeClass.Normal; + } + return isTiny(normCapacity) ? SizeClass.Tiny : SizeClass.Small; + } + + void freeChunk(PoolChunk chunk, long handle, SizeClass sizeClass) { + final boolean destroyChunk; + synchronized (this) { + switch (sizeClass) { + case Normal: + ++deallocationsNormal; + break; + case Small: + ++deallocationsSmall; + break; + case Tiny: + ++deallocationsTiny; + break; + default: + throw new Error(); } + destroyChunk = !chunk.parent.free(chunk, handle); + } + if (destroyChunk) { + // destroyChunk not need to be called while holding the synchronized lock. + destroyChunk(chunk); } } @@ -277,7 +355,7 @@ void reallocate(PooledByteBuf buf, int newCapacity, boolean freeOldMemory) { int readerIndex = buf.readerIndex(); int writerIndex = buf.writerIndex(); - allocate(parent.threadCache.get(), buf, newCapacity); + allocate(parent.threadCache(), buf, newCapacity); if (newCapacity > oldCapacity) { memoryCopy( oldMemory, oldOffset, @@ -298,8 +376,137 @@ void reallocate(PooledByteBuf buf, int newCapacity, boolean freeOldMemory) { buf.setIndex(readerIndex, writerIndex); if (freeOldMemory) { - free(oldChunk, oldHandle, oldMaxLength, buf.initThread == Thread.currentThread()); + free(oldChunk, oldHandle, oldMaxLength, buf.cache); + } + } + + @Override + public int numTinySubpages() { + return tinySubpagePools.length; + } + + @Override + public int numSmallSubpages() { + return smallSubpagePools.length; + } + + @Override + public int numChunkLists() { + return chunkListMetrics.size(); + } + + @Override + public List tinySubpages() { + return subPageMetricList(tinySubpagePools); + } + + @Override + public List smallSubpages() { + return subPageMetricList(smallSubpagePools); + } + + @Override + public List chunkLists() { + return chunkListMetrics; + } + + private static List subPageMetricList(PoolSubpage[] pages) { + List metrics = new ArrayList(); + for (int i = 1; i < pages.length; i ++) { + PoolSubpage head = pages[i]; + if (head.next == head) { + continue; + } + PoolSubpage s = head.next; + for (;;) { + metrics.add(s); + s = s.next; + if (s == head) { + break; + } + } } + return metrics; + } + + @Override + public long numAllocations() { + return allocationsTiny + allocationsSmall + allocationsNormal + allocationsHuge.value(); + } + + @Override + public long numTinyAllocations() { + return allocationsTiny; + } + + @Override + public long numSmallAllocations() { + return allocationsSmall; + } + + @Override + public long numNormalAllocations() { + return allocationsNormal; + } + + @Override + public long numDeallocations() { + return deallocationsTiny + deallocationsSmall + allocationsNormal + deallocationsHuge.value(); + } + + @Override + public long numTinyDeallocations() { + return deallocationsTiny; + } + + @Override + public long numSmallDeallocations() { + return deallocationsSmall; + } + + @Override + public long numNormalDeallocations() { + return deallocationsNormal; + } + + @Override + public long numHugeAllocations() { + return allocationsHuge.value(); + } + + @Override + public long numHugeDeallocations() { + return deallocationsHuge.value(); + } + + @Override + public long numActiveAllocations() { + long val = numAllocations() - numDeallocations(); + return val >= 0 ? val : 0; + } + + @Override + public long numActiveTinyAllocations() { + long val = numTinyAllocations() - numTinyDeallocations(); + return val >= 0 ? val : 0; + } + + @Override + public long numActiveSmallAllocations() { + long val = numSmallAllocations() - numSmallDeallocations(); + return val >= 0 ? val : 0; + } + + @Override + public long numActiveNormalAllocations() { + long val = numNormalAllocations() - numNormalDeallocations(); + return val >= 0 ? val : 0; + } + + @Override + public long numActiveHugeAllocations() { + long val = numHugeAllocations() - numHugeDeallocations(); + return val >= 0 ? val : 0; } protected abstract PoolChunk newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize); @@ -308,6 +515,7 @@ void reallocate(PooledByteBuf buf, int newCapacity, boolean freeOldMemory) { protected abstract void memoryCopy(T src, int srcOffset, T dst, int dstOffset, int length); protected abstract void destroyChunk(PoolChunk chunk); + @Override public synchronized String toString() { StringBuilder buf = new StringBuilder() .append("Chunk(s) at 0~25%:") diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolArenaMetric.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolArenaMetric.java new file mode 100644 index 00000000..776848ee --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolArenaMetric.java @@ -0,0 +1,130 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.buffer; + +import java.util.List; + +/** + * Expose metrics for an arena. + */ +public interface PoolArenaMetric { + + /** + * Returns the number of tiny sub-pages for the arena. + */ + int numTinySubpages(); + + /** + * Returns the number of small sub-pages for the arena. + */ + int numSmallSubpages(); + + /** + * Returns the number of chunk lists for the arena. + */ + int numChunkLists(); + + /** + * Returns an unmodifiable {@link List} which holds {@link PoolSubpageMetric}s for tiny sub-pages. + */ + List tinySubpages(); + + /** + * Returns an unmodifiable {@link List} which holds {@link PoolSubpageMetric}s for small sub-pages. + */ + List smallSubpages(); + + /** + * Returns an unmodifiable {@link List} which holds {@link PoolChunkListMetric}s. + */ + List chunkLists(); + + /** + * Return the number of allocations done via the arena. This includes all sizes. + */ + long numAllocations(); + + /** + * Return the number of tiny allocations done via the arena. + */ + long numTinyAllocations(); + + /** + * Return the number of small allocations done via the arena. + */ + long numSmallAllocations(); + + /** + * Return the number of normal allocations done via the arena. + */ + long numNormalAllocations(); + + /** + * Return the number of huge allocations done via the arena. + */ + long numHugeAllocations(); + + /** + * Return the number of deallocations done via the arena. This includes all sizes. + */ + long numDeallocations(); + + /** + * Return the number of tiny deallocations done via the arena. + */ + long numTinyDeallocations(); + + /** + * Return the number of small deallocations done via the arena. + */ + long numSmallDeallocations(); + + /** + * Return the number of normal deallocations done via the arena. + */ + long numNormalDeallocations(); + + /** + * Return the number of huge deallocations done via the arena. + */ + long numHugeDeallocations(); + + /** + * Return the number of currently active allocations. + */ + long numActiveAllocations(); + + /** + * Return the number of currently active tiny allocations. + */ + long numActiveTinyAllocations(); + + /** + * Return the number of currently active small allocations. + */ + long numActiveSmallAllocations(); + + /** + * Return the number of currently active normal allocations. + */ + long numActiveNormalAllocations(); + + /** + * Return the number of currently active huge allocations. + */ + long numActiveHugeAllocations(); +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunk.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunk.java index 364c99bc..09c19d5b 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunk.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunk.java @@ -100,8 +100,7 @@ * where as per convention defined above * the second value (i.e, x) indicates that the first node which is free to be allocated is at depth x (from root) */ - -final class PoolChunk { +final class PoolChunk implements PoolChunkMetric { final PoolArena arena; final T memory; @@ -186,7 +185,8 @@ private PoolSubpage[] newSubpageArray(int size) { return new PoolSubpage[size]; } - int usage() { + @Override + public int usage() { final int freeBytes = this.freeBytes; if (freeBytes == 0) { return 100; @@ -358,7 +358,8 @@ void initBuf(PooledByteBuf buf, long handle, int reqCapacity) { if (bitmapIdx == 0) { byte val = value(memoryMapIdx); assert val == unusable : String.valueOf(val); - buf.init(this, handle, runOffset(memoryMapIdx), reqCapacity, runLength(memoryMapIdx)); + buf.init(this, handle, runOffset(memoryMapIdx), reqCapacity, runLength(memoryMapIdx), + arena.parent.threadCache()); } else { initBufWithSubpage(buf, handle, bitmapIdx, reqCapacity); } @@ -379,7 +380,8 @@ private void initBufWithSubpage(PooledByteBuf buf, long handle, int bitmapIdx buf.init( this, handle, - runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize, reqCapacity, subpage.elemSize); + runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize, reqCapacity, subpage.elemSize, + arena.parent.threadCache()); } private byte value(int id) { @@ -414,6 +416,16 @@ private int subpageIdx(int memoryMapIdx) { return memoryMapIdx ^ maxSubpageAllocs; // remove highest set bit, to get offset } + @Override + public int chunkSize() { + return chunkSize; + } + + @Override + public int freeBytes() { + return freeBytes; + } + @Override public String toString() { return new StringBuilder() diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkList.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkList.java index 4a35acc2..d29f5eea 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkList.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkList.java @@ -18,26 +18,36 @@ import com.github.mauricio.netty.util.internal.StringUtil; -final class PoolChunkList { - private final PoolArena arena; - private final PoolChunkList nextList; - PoolChunkList prevList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +final class PoolChunkList implements PoolChunkListMetric { + private static final Iterator EMPTY_METRICS = Collections.emptyList().iterator(); + private final PoolChunkList nextList; private final int minUsage; private final int maxUsage; private PoolChunk head; + // This is only update once when create the linked like list of PoolChunkList in PoolArena constructor. + private PoolChunkList prevList; + // TODO: Test if adding padding helps under contention //private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7; - PoolChunkList(PoolArena arena, PoolChunkList nextList, int minUsage, int maxUsage) { - this.arena = arena; + PoolChunkList(PoolChunkList nextList, int minUsage, int maxUsage) { this.nextList = nextList; this.minUsage = minUsage; this.maxUsage = maxUsage; } + void prevList(PoolChunkList prevList) { + assert this.prevList == null; + this.prevList = prevList; + } + boolean allocate(PooledByteBuf buf, int reqCapacity, int normCapacity) { if (head == null) { return false; @@ -61,17 +71,19 @@ boolean allocate(PooledByteBuf buf, int reqCapacity, int normCapacity) { } } - void free(PoolChunk chunk, long handle) { + boolean free(PoolChunk chunk, long handle) { chunk.free(handle); if (chunk.usage() < minUsage) { remove(chunk); if (prevList == null) { assert chunk.usage() == 0; - arena.destroyChunk(chunk); + return false; } else { prevList.add(chunk); + return true; } } + return true; } void add(PoolChunk chunk) { @@ -108,6 +120,32 @@ private void remove(PoolChunk cur) { } } + @Override + public int minUsage() { + return minUsage; + } + + @Override + public int maxUsage() { + return maxUsage; + } + + @Override + public Iterator iterator() { + if (head == null) { + return EMPTY_METRICS; + } + List metrics = new ArrayList(); + for (PoolChunk cur = head;;) { + metrics.add(cur); + cur = cur.next; + if (cur == null) { + break; + } + } + return metrics.iterator(); + } + @Override public String toString() { if (head == null) { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkListMetric.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkListMetric.java new file mode 100644 index 00000000..a28e736e --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkListMetric.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.buffer; + +/** + * Metrics for a list of chunks. + */ +public interface PoolChunkListMetric extends Iterable { + + /** + * Return the minum usage of the chunk list before which chunks are promoted to the previous list. + */ + int minUsage(); + + /** + * Return the minum usage of the chunk list after which chunks are promoted to the next list. + */ + int maxUsage(); +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkMetric.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkMetric.java new file mode 100644 index 00000000..a8cea3bb --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolChunkMetric.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.buffer; + +/** + * Metrics for a chunk. + */ +public interface PoolChunkMetric { + + /** + * Return the percentage of the current usage of the chunk. + */ + int usage(); + + /** + * Return the size of the chunk in bytes, this is the maximum of bytes that can be served out of the chunk. + */ + int chunkSize(); + + /** + * Return the number of free bytes in the chunk. + */ + int freeBytes(); +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolSubpage.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolSubpage.java index 0e08bb4d..60193969 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolSubpage.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolSubpage.java @@ -16,7 +16,7 @@ package com.github.mauricio.netty.buffer; -final class PoolSubpage { +final class PoolSubpage implements PoolSubpageMetric { final PoolChunk chunk; private final int memoryMapIdx; @@ -72,7 +72,10 @@ void init(int elemSize) { } } - addToPool(); + PoolSubpage head = chunk.arena.findSubpagePoolHead(elemSize); + synchronized (head) { + addToPool(head); + } } /** @@ -83,21 +86,29 @@ long allocate() { return toHandle(0); } - if (numAvail == 0 || !doNotDestroy) { - return -1; - } + /** + * Synchronize on the head of the SubpagePool stored in the {@link PoolArena. This is needed as we synchronize + * on it when calling {@link PoolArena#allocate(PoolThreadCache, int, int)} und try to allocate out of the + * {@link PoolSubpage} pool for a given size. + */ + PoolSubpage head = chunk.arena.findSubpagePoolHead(elemSize); + synchronized (head) { + if (numAvail == 0 || !doNotDestroy) { + return -1; + } - final int bitmapIdx = getNextAvail(); - int q = bitmapIdx >>> 6; - int r = bitmapIdx & 63; - assert (bitmap[q] >>> r & 1) == 0; - bitmap[q] |= 1L << r; + final int bitmapIdx = getNextAvail(); + int q = bitmapIdx >>> 6; + int r = bitmapIdx & 63; + assert (bitmap[q] >>> r & 1) == 0; + bitmap[q] |= 1L << r; - if (-- numAvail == 0) { - removeFromPool(); - } + if (-- numAvail == 0) { + removeFromPool(); + } - return toHandle(bitmapIdx); + return toHandle(bitmapIdx); + } } /** @@ -110,36 +121,44 @@ boolean free(int bitmapIdx) { return true; } - int q = bitmapIdx >>> 6; - int r = bitmapIdx & 63; - assert (bitmap[q] >>> r & 1) != 0; - bitmap[q] ^= 1L << r; + /** + * Synchronize on the head of the SubpagePool stored in the {@link PoolArena. This is needed as we synchronize + * on it when calling {@link PoolArena#allocate(PoolThreadCache, int, int)} und try to allocate out of the + * {@link PoolSubpage} pool for a given size. + */ + PoolSubpage head = chunk.arena.findSubpagePoolHead(elemSize); - setNextAvail(bitmapIdx); + synchronized (head) { + int q = bitmapIdx >>> 6; + int r = bitmapIdx & 63; + assert (bitmap[q] >>> r & 1) != 0; + bitmap[q] ^= 1L << r; - if (numAvail ++ == 0) { - addToPool(); - return true; - } + setNextAvail(bitmapIdx); - if (numAvail != maxNumElems) { - return true; - } else { - // Subpage not in use (numAvail == maxNumElems) - if (prev == next) { - // Do not remove if this subpage is the only one left in the pool. + if (numAvail ++ == 0) { + addToPool(head); return true; } - // Remove this subpage from the pool if there are other subpages left in the pool. - doNotDestroy = false; - removeFromPool(); - return false; + if (numAvail != maxNumElems) { + return true; + } else { + // Subpage not in use (numAvail == maxNumElems) + if (prev == next) { + // Do not remove if this subpage is the only one left in the pool. + return true; + } + + // Remove this subpage from the pool if there are other subpages left in the pool. + doNotDestroy = false; + removeFromPool(); + return false; + } } } - private void addToPool() { - PoolSubpage head = chunk.arena.findSubpagePoolHead(elemSize); + private void addToPool(PoolSubpage head) { assert prev == null && next == null; prev = head; next = head.next; @@ -202,6 +221,7 @@ private long toHandle(int bitmapIdx) { return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx; } + @Override public String toString() { if (!doNotDestroy) { return "(" + memoryMapIdx + ": not in use)"; @@ -210,4 +230,24 @@ public String toString() { return String.valueOf('(') + memoryMapIdx + ": " + (maxNumElems - numAvail) + '/' + maxNumElems + ", offset: " + runOffset + ", length: " + pageSize + ", elemSize: " + elemSize + ')'; } + + @Override + public int maxNumElements() { + return maxNumElems; + } + + @Override + public int numAvailable() { + return numAvail; + } + + @Override + public int elementSize() { + return elemSize; + } + + @Override + public int pageSize() { + return pageSize; + } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolSubpageMetric.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolSubpageMetric.java new file mode 100644 index 00000000..6b3afa81 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolSubpageMetric.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.buffer; + +/** + * Metrics for a sub-page. + */ +public interface PoolSubpageMetric { + + /** + * Return the number of maximal elements that can be allocated out of the sub-page. + */ + int maxNumElements(); + + /** + * Return the number of available elements to be allocated. + */ + int numAvailable(); + + /** + * Return the size (in bytes) of the elements that will be allocated. + */ + int elementSize(); + + /** + * Return the size (in bytes) of this page. + */ + int pageSize(); +} + diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolThreadCache.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolThreadCache.java index 64591a8b..a3ce2e21 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolThreadCache.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PoolThreadCache.java @@ -17,11 +17,16 @@ package com.github.mauricio.netty.buffer; +import com.github.mauricio.netty.buffer.PoolArena.SizeClass; +import com.github.mauricio.netty.util.Recycler; +import com.github.mauricio.netty.util.Recycler.Handle; import com.github.mauricio.netty.util.ThreadDeathWatcher; +import com.github.mauricio.netty.util.internal.PlatformDependent; import com.github.mauricio.netty.util.internal.logging.InternalLogger; import com.github.mauricio.netty.util.internal.logging.InternalLoggerFactory; import java.nio.ByteBuffer; +import java.util.Queue; /** * Acts a Thread cache for allocations. This implementation is moduled after @@ -77,8 +82,10 @@ public void run() { this.heapArena = heapArena; this.directArena = directArena; if (directArena != null) { - tinySubPageDirectCaches = createSubPageCaches(tinyCacheSize, PoolArena.numTinySubpagePools); - smallSubPageDirectCaches = createSubPageCaches(smallCacheSize, directArena.numSmallSubpagePools); + tinySubPageDirectCaches = createSubPageCaches( + tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny); + smallSubPageDirectCaches = createSubPageCaches( + smallCacheSize, directArena.numSmallSubpagePools, SizeClass.Small); numShiftsNormalDirect = log2(directArena.pageSize); normalDirectCaches = createNormalCaches( @@ -92,8 +99,10 @@ public void run() { } if (heapArena != null) { // Create the caches for the heap allocations - tinySubPageHeapCaches = createSubPageCaches(tinyCacheSize, PoolArena.numTinySubpagePools); - smallSubPageHeapCaches = createSubPageCaches(smallCacheSize, heapArena.numSmallSubpagePools); + tinySubPageHeapCaches = createSubPageCaches( + tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny); + smallSubPageHeapCaches = createSubPageCaches( + smallCacheSize, heapArena.numSmallSubpagePools, SizeClass.Small); numShiftsNormalHeap = log2(heapArena.pageSize); normalHeapCaches = createNormalCaches( @@ -111,13 +120,14 @@ public void run() { ThreadDeathWatcher.watch(thread, freeTask); } - private static SubPageMemoryRegionCache[] createSubPageCaches(int cacheSize, int numCaches) { + private static MemoryRegionCache[] createSubPageCaches( + int cacheSize, int numCaches, SizeClass sizeClass) { if (cacheSize > 0) { @SuppressWarnings("unchecked") - SubPageMemoryRegionCache[] cache = new SubPageMemoryRegionCache[numCaches]; + MemoryRegionCache[] cache = new MemoryRegionCache[numCaches]; for (int i = 0; i < cache.length; i++) { // TODO: maybe use cacheSize / cache.length - cache[i] = new SubPageMemoryRegionCache(cacheSize); + cache[i] = new SubPageMemoryRegionCache(cacheSize, sizeClass); } return cache; } else { @@ -125,14 +135,14 @@ private static SubPageMemoryRegionCache[] createSubPageCaches(int cacheSi } } - private static NormalMemoryRegionCache[] createNormalCaches( + private static MemoryRegionCache[] createNormalCaches( int cacheSize, int maxCachedBufferCapacity, PoolArena area) { if (cacheSize > 0) { int max = Math.min(area.chunkSize, maxCachedBufferCapacity); - int arraySize = Math.max(1, max / area.pageSize); + int arraySize = Math.max(1, log2(max / area.pageSize) + 1); @SuppressWarnings("unchecked") - NormalMemoryRegionCache[] cache = new NormalMemoryRegionCache[arraySize]; + MemoryRegionCache[] cache = new MemoryRegionCache[arraySize]; for (int i = 0; i < cache.length; i++) { cache[i] = new NormalMemoryRegionCache(cacheSize); } @@ -191,23 +201,27 @@ private boolean allocate(MemoryRegionCache cache, PooledByteBuf buf, int reqC * Returns {@code true} if it fit into the cache {@code false} otherwise. */ @SuppressWarnings({ "unchecked", "rawtypes" }) - boolean add(PoolArena area, PoolChunk chunk, long handle, int normCapacity) { - MemoryRegionCache cache; - if (area.isTinyOrSmall(normCapacity)) { - if (PoolArena.isTiny(normCapacity)) { - cache = cacheForTiny(area, normCapacity); - } else { - cache = cacheForSmall(area, normCapacity); - } - } else { - cache = cacheForNormal(area, normCapacity); - } + boolean add(PoolArena area, PoolChunk chunk, long handle, int normCapacity, SizeClass sizeClass) { + MemoryRegionCache cache = cache(area, normCapacity, sizeClass); if (cache == null) { return false; } return cache.add(chunk, handle); } + private MemoryRegionCache cache(PoolArena area, int normCapacity, SizeClass sizeClass) { + switch (sizeClass) { + case Normal: + return cacheForNormal(area, normCapacity); + case Small: + return cacheForSmall(area, normCapacity); + case Tiny: + return cacheForTiny(area, normCapacity); + default: + throw new Error(); + } + } + /** * Should be called if the Thread that uses this cache is about to exist to release resources out of the cache */ @@ -309,8 +323,8 @@ private static MemoryRegionCache cache(MemoryRegionCache[] cache, int * Cache used for buffers which are backed by TINY or SMALL size. */ private static final class SubPageMemoryRegionCache extends MemoryRegionCache { - SubPageMemoryRegionCache(int size) { - super(size); + SubPageMemoryRegionCache(int size, SizeClass sizeClass) { + super(size, sizeClass); } @Override @@ -325,7 +339,7 @@ protected void initBuf( */ private static final class NormalMemoryRegionCache extends MemoryRegionCache { NormalMemoryRegionCache(int size) { - super(size); + super(size, SizeClass.Normal); } @Override @@ -335,24 +349,16 @@ protected void initBuf( } } - /** - * Cache of {@link PoolChunk} and handles which can be used to allocate a buffer without locking at all. - */ private abstract static class MemoryRegionCache { - private final Entry[] entries; - private final int maxUnusedCached; - private int head; - private int tail; - private int maxEntriesInUse; - private int entriesInUse; + private final int size; + private final Queue> queue; + private final SizeClass sizeClass; + private int allocations; - @SuppressWarnings("unchecked") - MemoryRegionCache(int size) { - entries = new Entry[powerOfTwo(size)]; - for (int i = 0; i < entries.length; i++) { - entries[i] = new Entry(); - } - maxUnusedCached = size / 2; + MemoryRegionCache(int size, SizeClass sizeClass) { + this.size = powerOfTwo(size); + queue = PlatformDependent.newFixedMpscQueue(this.size); + this.sizeClass = sizeClass; } private static int powerOfTwo(int res) { @@ -378,112 +384,101 @@ protected abstract void initBuf(PoolChunk chunk, long handle, /** * Add to cache if not already full. */ - public boolean add(PoolChunk chunk, long handle) { - Entry entry = entries[tail]; - if (entry.chunk != null) { - // cache is full - return false; - } - entriesInUse --; - - entry.chunk = chunk; - entry.handle = handle; - tail = nextIdx(tail); - return true; + @SuppressWarnings("unchecked") + public final boolean add(PoolChunk chunk, long handle) { + return queue.offer(newEntry(chunk, handle)); } /** * Allocate something out of the cache if possible and remove the entry from the cache. */ - public boolean allocate(PooledByteBuf buf, int reqCapacity) { - Entry entry = entries[head]; - if (entry.chunk == null) { + public final boolean allocate(PooledByteBuf buf, int reqCapacity) { + Entry entry = queue.poll(); + if (entry == null) { return false; } - - entriesInUse ++; - if (maxEntriesInUse < entriesInUse) { - maxEntriesInUse = entriesInUse; - } initBuf(entry.chunk, entry.handle, buf, reqCapacity); - // only null out the chunk as we only use the chunk to check if the buffer is full or not. - entry.chunk = null; - head = nextIdx(head); + + // allocations is not thread-safe which is fine as this is only called from the same thread all time. + ++ allocations; return true; } /** * Clear out this cache and free up all previous cached {@link PoolChunk}s and {@code handle}s. */ - public int free() { + public final int free() { + return free(Integer.MAX_VALUE); + } + + private int free(int max) { int numFreed = 0; - entriesInUse = 0; - maxEntriesInUse = 0; - for (int i = head;; i = nextIdx(i)) { - if (freeEntry(entries[i])) { - numFreed++; + for (; numFreed < max; numFreed++) { + Entry entry = queue.poll(); + if (entry != null) { + freeEntry(entry); } else { // all cleared return numFreed; } } + return numFreed; } /** * Free up cached {@link PoolChunk}s if not allocated frequently enough. */ - private void trim() { - int free = size() - maxEntriesInUse; - entriesInUse = 0; - maxEntriesInUse = 0; - - if (free <= maxUnusedCached) { - return; - } + public final void trim() { + int free = size - allocations; + allocations = 0; - int i = head; - for (; free > 0; free--) { - if (!freeEntry(entries[i])) { - // all freed - break; - } - i = nextIdx(i); + // We not even allocated all the number that are + if (free > 0) { + free(free); } - - // Update head to point to te correct entry - // See https://github.com/netty/netty/issues/2924 - head = i; } @SuppressWarnings({ "unchecked", "rawtypes" }) - private static boolean freeEntry(Entry entry) { + private void freeEntry(Entry entry) { PoolChunk chunk = entry.chunk; - if (chunk == null) { - return false; - } - // need to synchronize on the area from which it was allocated before. - synchronized (chunk.arena) { - chunk.parent.free(chunk, entry.handle); - } - entry.chunk = null; - return true; - } + long handle = entry.handle; - /** - * Return the number of cached entries. - */ - private int size() { - return tail - head & entries.length - 1; - } + // recycle now so PoolChunk can be GC'ed. + entry.recycle(); - private int nextIdx(int index) { - // use bitwise operation as this is faster as using modulo. - return index + 1 & entries.length - 1; + chunk.arena.freeChunk(chunk, handle, sizeClass); } - private static final class Entry { + static final class Entry { + final Handle recyclerHandle; PoolChunk chunk; - long handle; + long handle = -1; + + Entry(Handle recyclerHandle) { + this.recyclerHandle = recyclerHandle; + } + + void recycle() { + chunk = null; + handle = -1; + RECYCLER.recycle(this, recyclerHandle); + } } + + @SuppressWarnings("rawtypes") + private static Entry newEntry(PoolChunk chunk, long handle) { + Entry entry = RECYCLER.get(); + entry.chunk = chunk; + entry.handle = handle; + return entry; + } + + @SuppressWarnings("rawtypes") + private static final Recycler RECYCLER = new Recycler() { + @Override + protected Entry newObject(Handle handle) { + return new Entry(handle); + } + }; } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledByteBuf.java index c2d1ef38..208c8f64 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledByteBuf.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledByteBuf.java @@ -31,7 +31,7 @@ abstract class PooledByteBuf extends AbstractReferenceCountedByteBuf { protected int offset; protected int length; int maxLength; - Thread initThread; + PoolThreadCache cache; private ByteBuffer tmpNioBuf; protected PooledByteBuf(Recycler.Handle recyclerHandle, int maxCapacity) { @@ -39,7 +39,7 @@ protected PooledByteBuf(Recycler.Handle recyclerHandle, int maxCapacity) { this.recyclerHandle = recyclerHandle; } - void init(PoolChunk chunk, long handle, int offset, int length, int maxLength) { + void init(PoolChunk chunk, long handle, int offset, int length, int maxLength, PoolThreadCache cache) { assert handle >= 0; assert chunk != null; @@ -50,8 +50,9 @@ void init(PoolChunk chunk, long handle, int offset, int length, int maxLength this.length = length; this.maxLength = maxLength; setIndex(0, 0); + discardMarks(); tmpNioBuf = null; - initThread = Thread.currentThread(); + this.cache = cache; } void initUnpooled(PoolChunk chunk, int length) { @@ -64,7 +65,17 @@ void initUnpooled(PoolChunk chunk, int length) { this.length = maxLength = length; setIndex(0, 0); tmpNioBuf = null; - initThread = Thread.currentThread(); + cache = null; + } + + @Override + public ByteBuf slice(int index, int length) { + return PooledSlicedByteBuf.newInstance(this, index, length); + } + + @Override + public ByteBuf duplicate() { + return PooledDuplicatedByteBuf.newInstance(this); } @Override @@ -142,9 +153,7 @@ protected final void deallocate() { final long handle = this.handle; this.handle = -1; memory = null; - boolean sameThread = initThread == Thread.currentThread(); - initThread = null; - chunk.arena.free(chunk, handle, maxLength, sameThread); + chunk.arena.free(chunk, handle, maxLength, cache); recycle(); } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledByteBufAllocator.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledByteBufAllocator.java index 464010ef..ce45b16b 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledByteBufAllocator.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledByteBufAllocator.java @@ -23,6 +23,9 @@ import com.github.mauricio.netty.util.internal.logging.InternalLoggerFactory; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class PooledByteBufAllocator extends AbstractByteBufAllocator { @@ -43,7 +46,7 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator { private static final int MAX_CHUNK_SIZE = (int) (((long) Integer.MAX_VALUE + 1) / 2); static { - int defaultPageSize = SystemPropertyUtil.getInt("com.github.mauricio.netty.allocator.pageSize", 8192); + int defaultPageSize = SystemPropertyUtil.getInt("io.netty.allocator.pageSize", 8192); Throwable pageSizeFallbackCause = null; try { validateAndCalculatePageShifts(defaultPageSize); @@ -53,7 +56,7 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator { } DEFAULT_PAGE_SIZE = defaultPageSize; - int defaultMaxOrder = SystemPropertyUtil.getInt("com.github.mauricio.netty.allocator.maxOrder", 11); + int defaultMaxOrder = SystemPropertyUtil.getInt("io.netty.allocator.maxOrder", 11); Throwable maxOrderFallbackCause = null; try { validateAndCalculateChunkSize(DEFAULT_PAGE_SIZE, defaultMaxOrder); @@ -66,53 +69,59 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator { // Determine reasonable default for nHeapArena and nDirectArena. // Assuming each arena has 3 chunks, the pool should not consume more than 50% of max memory. final Runtime runtime = Runtime.getRuntime(); + + // Use 2 * cores by default to reduce condition as we use 2 * cores for the number of EventLoops + // in NIO and EPOLL as well. If we choose a smaller number we will run into hotspots as allocation and + // deallocation needs to be synchronized on the PoolArena. + // See https://github.com/netty/netty/issues/3888 + final int defaultMinNumArena = runtime.availableProcessors() * 2; final int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER; DEFAULT_NUM_HEAP_ARENA = Math.max(0, SystemPropertyUtil.getInt( - "com.github.mauricio.netty.allocator.numHeapArenas", + "io.netty.allocator.numHeapArenas", (int) Math.min( - runtime.availableProcessors(), - Runtime.getRuntime().maxMemory() / defaultChunkSize / 2 / 3))); + defaultMinNumArena, + runtime.maxMemory() / defaultChunkSize / 2 / 3))); DEFAULT_NUM_DIRECT_ARENA = Math.max(0, SystemPropertyUtil.getInt( - "com.github.mauricio.netty.allocator.numDirectArenas", + "io.netty.allocator.numDirectArenas", (int) Math.min( - runtime.availableProcessors(), + defaultMinNumArena, PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3))); // cache sizes - DEFAULT_TINY_CACHE_SIZE = SystemPropertyUtil.getInt("com.github.mauricio.netty.allocator.tinyCacheSize", 512); - DEFAULT_SMALL_CACHE_SIZE = SystemPropertyUtil.getInt("com.github.mauricio.netty.allocator.smallCacheSize", 256); - DEFAULT_NORMAL_CACHE_SIZE = SystemPropertyUtil.getInt("com.github.mauricio.netty.allocator.normalCacheSize", 64); + DEFAULT_TINY_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.tinyCacheSize", 512); + DEFAULT_SMALL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.smallCacheSize", 256); + DEFAULT_NORMAL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.normalCacheSize", 64); // 32 kb is the default maximum capacity of the cached buffer. Similar to what is explained in // 'Scalable memory allocation using jemalloc' DEFAULT_MAX_CACHED_BUFFER_CAPACITY = SystemPropertyUtil.getInt( - "com.github.mauricio.netty.allocator.maxCachedBufferCapacity", 32 * 1024); + "io.netty.allocator.maxCachedBufferCapacity", 32 * 1024); // the number of threshold of allocations when cached entries will be freed up if not frequently used DEFAULT_CACHE_TRIM_INTERVAL = SystemPropertyUtil.getInt( - "com.github.mauricio.netty.allocator.cacheTrimInterval", 8192); + "io.netty.allocator.cacheTrimInterval", 8192); if (logger.isDebugEnabled()) { - logger.debug("-Dcom.github.mauricio.netty.allocator.numHeapArenas: {}", DEFAULT_NUM_HEAP_ARENA); - logger.debug("-Dcom.github.mauricio.netty.allocator.numDirectArenas: {}", DEFAULT_NUM_DIRECT_ARENA); + logger.debug("-Dio.netty.allocator.numHeapArenas: {}", DEFAULT_NUM_HEAP_ARENA); + logger.debug("-Dio.netty.allocator.numDirectArenas: {}", DEFAULT_NUM_DIRECT_ARENA); if (pageSizeFallbackCause == null) { - logger.debug("-Dcom.github.mauricio.netty.allocator.pageSize: {}", DEFAULT_PAGE_SIZE); + logger.debug("-Dio.netty.allocator.pageSize: {}", DEFAULT_PAGE_SIZE); } else { - logger.debug("-Dcom.github.mauricio.netty.allocator.pageSize: {}", DEFAULT_PAGE_SIZE, pageSizeFallbackCause); + logger.debug("-Dio.netty.allocator.pageSize: {}", DEFAULT_PAGE_SIZE, pageSizeFallbackCause); } if (maxOrderFallbackCause == null) { - logger.debug("-Dcom.github.mauricio.netty.allocator.maxOrder: {}", DEFAULT_MAX_ORDER); + logger.debug("-Dio.netty.allocator.maxOrder: {}", DEFAULT_MAX_ORDER); } else { - logger.debug("-Dcom.github.mauricio.netty.allocator.maxOrder: {}", DEFAULT_MAX_ORDER, maxOrderFallbackCause); + logger.debug("-Dio.netty.allocator.maxOrder: {}", DEFAULT_MAX_ORDER, maxOrderFallbackCause); } - logger.debug("-Dcom.github.mauricio.netty.allocator.chunkSize: {}", DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER); - logger.debug("-Dcom.github.mauricio.netty.allocator.tinyCacheSize: {}", DEFAULT_TINY_CACHE_SIZE); - logger.debug("-Dcom.github.mauricio.netty.allocator.smallCacheSize: {}", DEFAULT_SMALL_CACHE_SIZE); - logger.debug("-Dcom.github.mauricio.netty.allocator.normalCacheSize: {}", DEFAULT_NORMAL_CACHE_SIZE); - logger.debug("-Dcom.github.mauricio.netty.allocator.maxCachedBufferCapacity: {}", DEFAULT_MAX_CACHED_BUFFER_CAPACITY); - logger.debug("-Dcom.github.mauricio.netty.allocator.cacheTrimInterval: {}", DEFAULT_CACHE_TRIM_INTERVAL); + logger.debug("-Dio.netty.allocator.chunkSize: {}", DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER); + logger.debug("-Dio.netty.allocator.tinyCacheSize: {}", DEFAULT_TINY_CACHE_SIZE); + logger.debug("-Dio.netty.allocator.smallCacheSize: {}", DEFAULT_SMALL_CACHE_SIZE); + logger.debug("-Dio.netty.allocator.normalCacheSize: {}", DEFAULT_NORMAL_CACHE_SIZE); + logger.debug("-Dio.netty.allocator.maxCachedBufferCapacity: {}", DEFAULT_MAX_CACHED_BUFFER_CAPACITY); + logger.debug("-Dio.netty.allocator.cacheTrimInterval: {}", DEFAULT_CACHE_TRIM_INTERVAL); } } @@ -124,8 +133,9 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator { private final int tinyCacheSize; private final int smallCacheSize; private final int normalCacheSize; - - final PoolThreadLocalCache threadCache; + private final List heapArenaMetrics; + private final List directArenaMetrics; + private final PoolThreadLocalCache threadCache; public PooledByteBufAllocator() { this(false); @@ -164,20 +174,31 @@ public PooledByteBufAllocator(boolean preferDirect, int nHeapArena, int nDirectA if (nHeapArena > 0) { heapArenas = newArenaArray(nHeapArena); + List metrics = new ArrayList(heapArenas.length); for (int i = 0; i < heapArenas.length; i ++) { - heapArenas[i] = new PoolArena.HeapArena(this, pageSize, maxOrder, pageShifts, chunkSize); + PoolArena.HeapArena arena = new PoolArena.HeapArena(this, pageSize, maxOrder, pageShifts, chunkSize); + heapArenas[i] = arena; + metrics.add(arena); } + heapArenaMetrics = Collections.unmodifiableList(metrics); } else { heapArenas = null; + heapArenaMetrics = Collections.emptyList(); } if (nDirectArena > 0) { directArenas = newArenaArray(nDirectArena); + List metrics = new ArrayList(directArenas.length); for (int i = 0; i < directArenas.length; i ++) { - directArenas[i] = new PoolArena.DirectArena(this, pageSize, maxOrder, pageShifts, chunkSize); + PoolArena.DirectArena arena = new PoolArena.DirectArena( + this, pageSize, maxOrder, pageShifts, chunkSize); + directArenas[i] = arena; + metrics.add(arena); } + directArenaMetrics = Collections.unmodifiableList(metrics); } else { directArenas = null; + directArenaMetrics = Collections.emptyList(); } } @@ -187,7 +208,7 @@ public PooledByteBufAllocator(boolean preferDirect, int nHeapArena, int nDirectA int tinyCacheSize, int smallCacheSize, int normalCacheSize, long cacheThreadAliveCheckInterval) { this(preferDirect, nHeapArena, nDirectArena, pageSize, maxOrder, - tinyCacheSize, smallCacheSize, normalCacheSize); + tinyCacheSize, smallCacheSize, normalCacheSize); } @SuppressWarnings("unchecked") @@ -283,9 +304,11 @@ public void freeThreadLocalCache() { final class PoolThreadLocalCache extends FastThreadLocal { private final AtomicInteger index = new AtomicInteger(); + final AtomicInteger caches = new AtomicInteger(); @Override protected PoolThreadCache initialValue() { + caches.incrementAndGet(); final int idx = index.getAndIncrement(); final PoolArena heapArena; final PoolArena directArena; @@ -301,7 +324,6 @@ protected PoolThreadCache initialValue() { } else { directArena = null; } - return new PoolThreadCache( heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize, DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL); @@ -310,25 +332,95 @@ protected PoolThreadCache initialValue() { @Override protected void onRemoval(PoolThreadCache value) { value.free(); + caches.decrementAndGet(); } } -// Too noisy at the moment. -// -// public String toString() { -// StringBuilder buf = new StringBuilder(); -// buf.append(heapArenas.length); -// buf.append(" heap arena(s):"); -// buf.append(StringUtil.NEWLINE); -// for (PoolArena a: heapArenas) { -// buf.append(a); -// } -// buf.append(directArenas.length); -// buf.append(" direct arena(s):"); -// buf.append(StringUtil.NEWLINE); -// for (PoolArena a: directArenas) { -// buf.append(a); -// } -// return buf.toString(); -// } + /** + * Return the number of heap arenas. + */ + public int numHeapArenas() { + return heapArenaMetrics.size(); + } + + /** + * Return the number of direct arenas. + */ + public int numDirectArenas() { + return directArenaMetrics.size(); + } + + /** + * Return a {@link List} of all heap {@link PoolArenaMetric}s that are provided by this pool. + */ + public List heapArenas() { + return heapArenaMetrics; + } + + /** + * Return a {@link List} of all direct {@link PoolArenaMetric}s that are provided by this pool. + */ + public List directArenas() { + return directArenaMetrics; + } + + /** + * Return the number of thread local caches used by this {@link PooledByteBufAllocator}. + */ + public int numThreadLocalCaches() { + return threadCache.caches.get(); + } + + /** + * Return the size of the tiny cache. + */ + public int tinyCacheSize() { + return tinyCacheSize; + } + + /** + * Return the size of the small cache. + */ + public int smallCacheSize() { + return smallCacheSize; + } + + /** + * Return the size of the normal cache. + */ + public int normalCacheSize() { + return normalCacheSize; + } + + final PoolThreadCache threadCache() { + return threadCache.get(); + } + + // Too noisy at the moment. + // + //public String toString() { + // StringBuilder buf = new StringBuilder(); + // int heapArenasLen = heapArenas == null ? 0 : heapArenas.length; + // buf.append(heapArenasLen); + // buf.append(" heap arena(s):"); + // buf.append(StringUtil.NEWLINE); + // if (heapArenasLen > 0) { + // for (PoolArena a: heapArenas) { + // buf.append(a); + // } + // } + // + // int directArenasLen = directArenas == null ? 0 : directArenas.length; + // + // buf.append(directArenasLen); + // buf.append(" direct arena(s):"); + // buf.append(StringUtil.NEWLINE); + // if (directArenasLen > 0) { + // for (PoolArena a: directArenas) { + // buf.append(a); + // } + // } + // + // return buf.toString(); + //} } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledDuplicatedByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledDuplicatedByteBuf.java new file mode 100644 index 00000000..bcd738b5 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledDuplicatedByteBuf.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.buffer; + +import com.github.mauricio.netty.util.Recycler; + +final class PooledDuplicatedByteBuf extends DuplicatedByteBuf { + + private static final Recycler RECYCLER = new Recycler() { + @Override + protected PooledDuplicatedByteBuf newObject(Handle handle) { + return new PooledDuplicatedByteBuf(handle); + } + }; + + static PooledDuplicatedByteBuf newInstance(ByteBuf buffer) { + PooledDuplicatedByteBuf buf = RECYCLER.get(); + buf.init(buffer); + return buf; + } + + private final Recycler.Handle recyclerHandle; + + private PooledDuplicatedByteBuf(Recycler.Handle recyclerHandle) { + super(0); + this.recyclerHandle = recyclerHandle; + } + + @Override + public ByteBuf slice(int index, int length) { + return PooledSlicedByteBuf.newInstance(this, index, length); + } + + @Override + public ByteBuf duplicate() { + return newInstance(this); + } + + @Override + protected void deallocate() { + RECYCLER.recycle(this, recyclerHandle); + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledSlicedByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledSlicedByteBuf.java new file mode 100644 index 00000000..8591158e --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledSlicedByteBuf.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.buffer; + +import com.github.mauricio.netty.util.Recycler; + +final class PooledSlicedByteBuf extends SlicedByteBuf { + + private static final Recycler RECYCLER = new Recycler() { + @Override + protected PooledSlicedByteBuf newObject(Handle handle) { + return new PooledSlicedByteBuf(handle); + } + }; + + static PooledSlicedByteBuf newInstance(ByteBuf buffer, int index, int length) { + PooledSlicedByteBuf buf = RECYCLER.get(); + buf.init(buffer, index, length); + return buf; + } + + private final Recycler.Handle recyclerHandle; + + private PooledSlicedByteBuf(Recycler.Handle recyclerHandle) { + super(0); + this.recyclerHandle = recyclerHandle; + } + + @Override + public ByteBuf slice(int index, int length) { + return newInstance(this, index, length); + } + + @Override + public ByteBuf duplicate() { + return PooledDuplicatedByteBuf.newInstance(this); + } + + @Override + protected void deallocate() { + RECYCLER.recycle(this, recyclerHandle); + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledUnsafeDirectByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledUnsafeDirectByteBuf.java index 74bac2f1..0fbcf010 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledUnsafeDirectByteBuf.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/PooledUnsafeDirectByteBuf.java @@ -16,6 +16,7 @@ package com.github.mauricio.netty.buffer; +import com.github.mauricio.netty.buffer.PooledByteBufAllocator.PoolThreadLocalCache; import com.github.mauricio.netty.util.Recycler; import com.github.mauricio.netty.util.internal.PlatformDependent; @@ -53,8 +54,9 @@ private PooledUnsafeDirectByteBuf(Recycler.Handle recyclerHandle, int maxCapacit } @Override - void init(PoolChunk chunk, long handle, int offset, int length, int maxLength) { - super.init(chunk, handle, offset, length, maxLength); + void init(PoolChunk chunk, long handle, int offset, int length, int maxLength, + PoolThreadCache cache) { + super.init(chunk, handle, offset, length, maxLength, cache); initMemoryAddress(); } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/SlicedByteBuf.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/SlicedByteBuf.java index 12b3089c..bb9d292e 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/SlicedByteBuf.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/SlicedByteBuf.java @@ -32,12 +32,20 @@ */ public class SlicedByteBuf extends AbstractDerivedByteBuf { - private final ByteBuf buffer; - private final int adjustment; - private final int length; + private ByteBuf buffer; + private int adjustment; + private int length; public SlicedByteBuf(ByteBuf buffer, int index, int length) { super(length); + init(buffer, index, length); + } + + SlicedByteBuf(int length) { + super(length); + } + + final void init(ByteBuf buffer, int index, int length) { if (index < 0 || index > buffer.capacity() - length) { throw new IndexOutOfBoundsException(buffer + ".slice(" + index + ", " + length + ')'); } @@ -53,8 +61,9 @@ public SlicedByteBuf(ByteBuf buffer, int index, int length) { adjustment = index; } this.length = length; - - writerIndex(length); + maxCapacity(length); + setIndex(0, length); + discardMarks(); } @Override diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/Unpooled.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/Unpooled.java index c354dbd7..22d9dd58 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/Unpooled.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/Unpooled.java @@ -67,12 +67,6 @@ * between the original data and the copied buffer. Various copy methods are * provided and their name is all {@code copiedBuffer()}. It is also convenient * to use this operation to merge multiple buffers into one buffer. - * - *

Miscellaneous utility methods

- * - * This class also provides various utility methods to help implementation - * of a new buffer type, generation of hex dump and swapping an integer's - * byte order. */ public final class Unpooled { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/package-info.java b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/package-info.java index 9413cea8..9b3a4dd7 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/buffer/package-info.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/buffer/package-info.java @@ -21,7 +21,7 @@ * Netty uses its own buffer API instead of NIO {@link java.nio.ByteBuffer} to * represent a sequence of bytes. This approach has significant advantage over * using {@link java.nio.ByteBuffer}. Netty's new buffer type, - * {@link com.github.mauricio.netty.buffer.ByteBuf}, has been designed from ground + * {@link io.netty.buffer.ByteBuf}, has been designed from ground * up to address the problems of {@link java.nio.ByteBuffer} and to meet the * daily needs of network application developers. To list a few cool features: *
    @@ -35,13 +35,13 @@ * *

    Extensibility

    * - * {@link com.github.mauricio.netty.buffer.ByteBuf} has rich set of operations + * {@link io.netty.buffer.ByteBuf} has rich set of operations * optimized for rapid protocol implementation. For example, - * {@link com.github.mauricio.netty.buffer.ByteBuf} provides various operations + * {@link io.netty.buffer.ByteBuf} provides various operations * for accessing unsigned values and strings and searching for certain byte * sequence in a buffer. You can also extend or wrap existing buffer type * to add convenient accessors. The custom buffer type still implements - * {@link com.github.mauricio.netty.buffer.ByteBuf} interface rather than + * {@link io.netty.buffer.ByteBuf} interface rather than * introducing an incompatible type. * *

    Transparent Zero Copy

    @@ -70,7 +70,7 @@ * // The composite type is incompatible with the component type. * ByteBuffer[] message = new ByteBuffer[] { header, body }; * - * By contrast, {@link com.github.mauricio.netty.buffer.ByteBuf} does not have such + * By contrast, {@link io.netty.buffer.ByteBuf} does not have such * caveats because it is fully extensible and has a built-in composite buffer * type. *
    @@ -118,7 +118,7 @@
      * 

    Better Performance

    * * Most frequently used buffer implementation of - * {@link com.github.mauricio.netty.buffer.ByteBuf} is a very thin wrapper of a + * {@link io.netty.buffer.ByteBuf} is a very thin wrapper of a * byte array (i.e. {@code byte[]}). Unlike {@link java.nio.ByteBuffer}, it has * no complicated boundary check and index compensation, and therefore it is * easier for a JVM to optimize the buffer access. More complicated buffer diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/AbstractChannel.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/AbstractChannel.java index dce7e46d..fe4061c4 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/AbstractChannel.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/AbstractChannel.java @@ -33,6 +33,7 @@ import java.net.SocketException; import java.nio.channels.ClosedChannelException; import java.nio.channels.NotYetConnectedException; +import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; /** @@ -467,10 +468,10 @@ public final void bind(final SocketAddress localAddress, final ChannelPromise pr } // See: https://github.com/netty/netty/issues/576 - if (!PlatformDependent.isWindows() && !PlatformDependent.isRoot() && - Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) && + if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) && localAddress instanceof InetSocketAddress && - !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress()) { + !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() && + !PlatformDependent.isWindows() && !PlatformDependent.isRoot()) { // Warn a user about the fact that a non-root user can't receive a // broadcast packet on *nix if the socket is bound on non-wildcard address. logger.warn( @@ -534,13 +535,17 @@ public final void close(final ChannelPromise promise) { return; } - if (inFlush0) { - invokeLater(new OneTimeTask() { - @Override - public void run() { - close(promise); - } - }); + if (outboundBuffer == null) { + // Only needed if no VoidChannelPromise. + if (!(promise instanceof VoidChannelPromise)) { + // This means close() was called before so we just register a listener and return + closeFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + promise.setSuccess(); + } + }); + } return; } @@ -550,10 +555,54 @@ public void run() { return; } - boolean wasActive = isActive(); - ChannelOutboundBuffer outboundBuffer = this.outboundBuffer; - this.outboundBuffer = null; // Disallow adding any messages and flushes to outboundBuffer. + final boolean wasActive = isActive(); + final ChannelOutboundBuffer buffer = outboundBuffer; + outboundBuffer = null; // Disallow adding any messages and flushes to outboundBuffer. + Executor closeExecutor = closeExecutor(); + if (closeExecutor != null) { + closeExecutor.execute(new OneTimeTask() { + @Override + public void run() { + try { + // Execute the close. + doClose0(promise); + } finally { + // Call invokeLater so closeAndDeregister is executed in the EventLoop again! + invokeLater(new OneTimeTask() { + @Override + public void run() { + // Fail all the queued messages + buffer.failFlushed(CLOSED_CHANNEL_EXCEPTION, false); + buffer.close(CLOSED_CHANNEL_EXCEPTION); + fireChannelInactiveAndDeregister(wasActive); + } + }); + } + } + }); + } else { + try { + // Close the channel and fail the queued messages in all cases. + doClose0(promise); + } finally { + // Fail all the queued messages. + buffer.failFlushed(CLOSED_CHANNEL_EXCEPTION, false); + buffer.close(CLOSED_CHANNEL_EXCEPTION); + } + if (inFlush0) { + invokeLater(new OneTimeTask() { + @Override + public void run() { + fireChannelInactiveAndDeregister(wasActive); + } + }); + } else { + fireChannelInactiveAndDeregister(wasActive); + } + } + } + private void doClose0(ChannelPromise promise) { try { doClose(); closeFuture.setClosed(); @@ -562,24 +611,19 @@ public void run() { closeFuture.setClosed(); safeSetFailure(promise, t); } + } - // Fail all the queued messages - try { - outboundBuffer.failFlushed(CLOSED_CHANNEL_EXCEPTION); - outboundBuffer.close(CLOSED_CHANNEL_EXCEPTION); - } finally { - - if (wasActive && !isActive()) { - invokeLater(new OneTimeTask() { - @Override - public void run() { - pipeline.fireChannelInactive(); - } - }); - } - - deregister(voidPromise()); + private void fireChannelInactiveAndDeregister(final boolean wasActive) { + if (wasActive && !isActive()) { + invokeLater(new OneTimeTask() { + @Override + public void run() { + pipeline.fireChannelInactive(); + } + }); } + + deregister(voidPromise()); } @Override @@ -702,9 +746,10 @@ protected void flush0() { if (!isActive()) { try { if (isOpen()) { - outboundBuffer.failFlushed(NOT_YET_CONNECTED_EXCEPTION); + outboundBuffer.failFlushed(NOT_YET_CONNECTED_EXCEPTION, true); } else { - outboundBuffer.failFlushed(CLOSED_CHANNEL_EXCEPTION); + // Do not trigger channelWritabilityChanged because the channel is closed already. + outboundBuffer.failFlushed(CLOSED_CHANNEL_EXCEPTION, false); } } finally { inFlush0 = false; @@ -715,8 +760,10 @@ protected void flush0() { try { doWrite(outboundBuffer); } catch (Throwable t) { - outboundBuffer.failFlushed(t); - if (t instanceof IOException && config().isAutoClose()) { + boolean close = t instanceof IOException && config().isAutoClose(); + // We do not want to trigger channelWritabilityChanged event if the channel is going to be closed. + outboundBuffer.failFlushed(t, !close); + if (close) { close(voidPromise()); } } finally { @@ -802,6 +849,15 @@ protected final Throwable annotateConnectException(Throwable cause, SocketAddres return cause; } + + /** + * @return {@link Executor} to execute {@link #doClose()} or {@code null} if it should be done in the + * {@link EventLoop}. + + + */ + protected Executor closeExecutor() { + return null; + } } /** diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/Channel.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/Channel.java index c7e27e8e..1a77dac2 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/Channel.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/Channel.java @@ -80,7 +80,7 @@ public interface Channel extends AttributeMap, Comparable { /** - * Return the {@link EventLoop} this {@link Channel} was registered too. + * Return the {@link EventLoop} this {@link Channel} was registered to. */ EventLoop eventLoop(); @@ -98,7 +98,7 @@ public interface Channel extends AttributeMap, Comparable { ChannelConfig config(); /** - * Returns {@code true} if the {@link Channel} is open an may get active later + * Returns {@code true} if the {@link Channel} is open and may get active later */ boolean isOpen(); @@ -164,7 +164,7 @@ public interface Channel extends AttributeMap, Comparable { Unsafe unsafe(); /** - * Return the assigned {@link ChannelPipeline} + * Return the assigned {@link ChannelPipeline}. */ ChannelPipeline pipeline(); @@ -179,7 +179,7 @@ public interface Channel extends AttributeMap, Comparable { ChannelPromise newPromise(); /** - * Return an new {@link ChannelProgressivePromise} + * Return an new {@link ChannelProgressivePromise}. */ ChannelProgressivePromise newProgressivePromise(); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelHandlerAdapter.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelHandlerAdapter.java index 12feec5c..e0b7138a 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelHandlerAdapter.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelHandlerAdapter.java @@ -40,7 +40,7 @@ public boolean isSharable() { * {@link WeakHashMap} instances per {@link Thread} is good enough for us and the number of * {@link Thread}s are quite limited anyway. * - * See #2289. + * See #2289. */ Class clazz = getClass(); Map, Boolean> cache = InternalThreadLocalMap.get().handlerSharableCache(); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelOption.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelOption.java index 6a488c41..09285ce4 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelOption.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelOption.java @@ -23,6 +23,8 @@ import java.net.NetworkInterface; import java.util.concurrent.ConcurrentMap; +import static com.github.mauricio.netty.util.internal.ObjectUtil.checkNotNull; + /** * A {@link ChannelOption} allows to configure a {@link ChannelConfig} in a type-safe * way. Which {@link ChannelOption} is supported depends on the actual implementation @@ -34,7 +36,8 @@ @SuppressWarnings("deprecation") public class ChannelOption extends UniqueName { - private static final ConcurrentMap names = PlatformDependent.newConcurrentHashMap(); + @SuppressWarnings("rawtypes") + private static final ConcurrentMap names = PlatformDependent.newConcurrentHashMap(); public static final ChannelOption ALLOCATOR = valueOf("ALLOCATOR"); public static final ChannelOption RCVBUF_ALLOCATOR = valueOf("RCVBUF_ALLOCATOR"); @@ -85,10 +88,44 @@ public class ChannelOption extends UniqueName { valueOf("DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION"); /** - * Creates a new {@link ChannelOption} with the specified {@code name}. + * Creates a new {@link ChannelOption} with the specified {@param name} or return the already existing + * {@link ChannelOption} for the given name. */ + @SuppressWarnings("unchecked") public static ChannelOption valueOf(String name) { - return new ChannelOption(name); + checkNotNull(name, "name"); + ChannelOption option = names.get(name); + if (option == null) { + option = new ChannelOption(name); + ChannelOption old = names.putIfAbsent(name, option); + if (old != null) { + option = old; + } + } + return option; + } + + /** + * Returns {@code true} if a {@link ChannelOption} exists for the given {@code name}. + */ + public static boolean exists(String name) { + checkNotNull(name, "name"); + return names.containsKey(name); + } + + /** + * Creates a new {@link ChannelOption} for the given {@param name} or fail with an + * {@link IllegalArgumentException} if a {@link ChannelOption} for the given {@param name} exists. + */ + @SuppressWarnings("unchecked") + public static ChannelOption newInstance(String name) { + checkNotNull(name, "name"); + ChannelOption option = new ChannelOption(name); + ChannelOption old = names.putIfAbsent(name, option); + if (old != null) { + throw new IllegalArgumentException(String.format("'%s' is already in use", name)); + } + return option; } /** @@ -96,7 +133,7 @@ public static ChannelOption valueOf(String name) { */ @Deprecated protected ChannelOption(String name) { - super(names, name); + super(name); } /** diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelOutboundBuffer.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelOutboundBuffer.java index 6bcad699..65b0ed67 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelOutboundBuffer.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelOutboundBuffer.java @@ -30,6 +30,7 @@ import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; +import java.util.Arrays; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -149,7 +150,7 @@ public void addFlush() { if (!entry.promise.setUncancellable()) { // Was cancelled so make sure we free up memory and notify about the freed bytes int pending = entry.cancel(); - decrementPendingOutboundBytes(pending, false); + decrementPendingOutboundBytes(pending, false, true); } entry = entry.next; } while (entry != null); @@ -183,16 +184,17 @@ private void incrementPendingOutboundBytes(long size, boolean invokeLater) { * This method is thread-safe! */ void decrementPendingOutboundBytes(long size) { - decrementPendingOutboundBytes(size, true); + decrementPendingOutboundBytes(size, true, true); } - private void decrementPendingOutboundBytes(long size, boolean invokeLater) { + private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) { if (size == 0) { return; } long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size); - if (newWriteBufferSize == 0 || newWriteBufferSize <= channel.config().getWriteBufferLowWaterMark()) { + if (notifyWritability && (newWriteBufferSize == 0 + || newWriteBufferSize <= channel.config().getWriteBufferLowWaterMark())) { setWritable(invokeLater); } } @@ -244,6 +246,7 @@ public void progress(long amount) { public boolean remove() { Entry e = flushedEntry; if (e == null) { + clearNioBuffers(); return false; } Object msg = e.msg; @@ -257,7 +260,7 @@ public boolean remove() { // only release message, notify and decrement if it was not canceled before. ReferenceCountUtil.safeRelease(msg); safeSuccess(promise); - decrementPendingOutboundBytes(size, false); + decrementPendingOutboundBytes(size, false, true); } // recycle the entry @@ -272,8 +275,13 @@ public boolean remove() { * {@code false} to signal that no more messages are ready to be handled. */ public boolean remove(Throwable cause) { + return remove0(cause, true); + } + + private boolean remove0(Throwable cause, boolean notifyWritability) { Entry e = flushedEntry; if (e == null) { + clearNioBuffers(); return false; } Object msg = e.msg; @@ -288,7 +296,7 @@ public boolean remove(Throwable cause) { ReferenceCountUtil.safeRelease(msg); safeFail(promise, cause); - decrementPendingOutboundBytes(size, false); + decrementPendingOutboundBytes(size, false, notifyWritability); } // recycle the entry @@ -340,6 +348,17 @@ public void removeBytes(long writtenBytes) { break; } } + clearNioBuffers(); + } + + // Clear all ByteBuffer from the array so these can be GC'ed. + // See https://github.com/netty/netty/issues/3837 + private void clearNioBuffers() { + int count = nioBufferCount; + if (count > 0) { + nioBufferCount = 0; + Arrays.fill(NIO_BUFFERS.get(), 0, count, null); + } } /** @@ -365,6 +384,19 @@ public ByteBuffer[] nioBuffers() { final int readableBytes = buf.writerIndex() - readerIndex; if (readableBytes > 0) { + if (Integer.MAX_VALUE - readableBytes < nioBufferSize) { + // If the nioBufferSize + readableBytes will overflow an Integer we stop populate the + // ByteBuffer array. This is done as bsd/osx don't allow to write more bytes then + // Integer.MAX_VALUE with one writev(...) call and so will return 'EINVAL', which will + // raise an IOException. On Linux it may work depending on the + // architecture and kernel but to be safe we also enforce the limit here. + // This said writing more the Integer.MAX_VALUE is not a good idea anyway. + // + // See also: + // - https://www.freebsd.org/cgi/man.cgi?query=write&sektion=2 + // - http://linux.die.net/man/2/writev + break; + } nioBufferSize += readableBytes; int count = entry.count; if (count == -1) { @@ -573,7 +605,7 @@ public boolean isEmpty() { return flushed == 0; } - void failFlushed(Throwable cause) { + void failFlushed(Throwable cause, boolean notify) { // Make sure that this method does not reenter. A listener added to the current promise can be notified by the // current thread in the tryFailure() call of the loop below, and the listener can trigger another fail() call // indirectly (usually by closing the channel.) @@ -586,7 +618,7 @@ void failFlushed(Throwable cause) { try { inFail = true; for (;;) { - if (!remove(cause)) { + if (!remove0(cause, notify)) { break; } } @@ -633,6 +665,7 @@ public void run() { } finally { inFail = false; } + clearNioBuffers(); } private static void safeSuccess(ChannelPromise promise) { @@ -656,6 +689,36 @@ public long totalPendingWriteBytes() { return totalPendingSize; } + /** + * Get how many bytes can be written until {@link #isWritable()} returns {@code false}. + * This quantity will always be non-negative. If {@link #isWritable()} is {@code false} then 0. + */ + public long bytesBeforeUnwritable() { + long bytes = channel.config().getWriteBufferHighWaterMark() - totalPendingSize; + // If bytes is negative we know we are not writable, but if bytes is non-negative we have to check writability. + // Note that totalPendingSize and isWritable() use different volatile variables that are not synchronized + // together. totalPendingSize will be updated before isWritable(). + if (bytes > 0) { + return isWritable() ? bytes : 0; + } + return 0; + } + + /** + * Get how many bytes must be drained from the underlying buffer until {@link #isWritable()} returns {@code true}. + * This quantity will always be non-negative. If {@link #isWritable()} is {@code true} then 0. + */ + public long bytesBeforeWritable() { + long bytes = totalPendingSize - channel.config().getWriteBufferLowWaterMark(); + // If bytes is negative we know we are writable, but if bytes is non-negative we have to check writability. + // Note that totalPendingSize and isWritable() use different volatile variables that are not synchronized + // together. totalPendingSize will be updated before isWritable(). + if (bytes > 0) { + return isWritable() ? 0 : bytes; + } + return 0; + } + /** * Call {@link MessageProcessor#processMessage(Object)} for each flushed message * in this {@link ChannelOutboundBuffer} until {@link MessageProcessor#processMessage(Object)} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelPromiseNotifier.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelPromiseNotifier.java index ea24f6b9..8d0a2062 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelPromiseNotifier.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/ChannelPromiseNotifier.java @@ -32,5 +32,4 @@ public final class ChannelPromiseNotifier public ChannelPromiseNotifier(ChannelPromise... promises) { super(promises); } - } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/DefaultChannelPipeline.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/DefaultChannelPipeline.java index 038ae9d1..9f58b08e 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/DefaultChannelPipeline.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/DefaultChannelPipeline.java @@ -1023,13 +1023,22 @@ public void handlerAdded(ChannelHandlerContext ctx) throws Exception { } public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { } @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { } + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + // This may not be a configuration error and so don't log anything. + // The event may be superfluous for the current pipeline configuration. + ReferenceCountUtil.release(evt); + } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - logger.warn( - "An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " + - "It usually means the last handler in the pipeline did not handle the exception.", cause); + try { + logger.warn( + "An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " + + "It usually means the last handler in the pipeline did not handle the exception.", + cause); + } finally { + ReferenceCountUtil.release(cause); + } } @Override diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/EventLoop.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/EventLoop.java index d8c8f8cc..a3a008d5 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/EventLoop.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/EventLoop.java @@ -18,7 +18,7 @@ import com.github.mauricio.netty.util.concurrent.EventExecutor; /** - * Will handle all the I/O-Operations for a {@link Channel} once it was registered. + * Will handle all the I/O operations for a {@link Channel} once registered. * * One {@link EventLoop} instance will usually handle more then one {@link Channel} but this may depend on * implementation details and internals. diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/EventLoopGroup.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/EventLoopGroup.java index 47a6395a..08d4be8b 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/EventLoopGroup.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/EventLoopGroup.java @@ -18,7 +18,7 @@ import com.github.mauricio.netty.util.concurrent.EventExecutorGroup; /** - * Special {@link EventExecutorGroup} which allows to register {@link Channel}'s that get + * Special {@link EventExecutorGroup} which allows registering {@link Channel}s that get * processed for later selection during the event loop. * */ diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/MultithreadEventLoopGroup.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/MultithreadEventLoopGroup.java index cceb9700..7a1cc211 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/MultithreadEventLoopGroup.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/MultithreadEventLoopGroup.java @@ -35,10 +35,10 @@ public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutor static { DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt( - "com.github.mauricio.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2)); + "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2)); if (logger.isDebugEnabled()) { - logger.debug("-Dcom.github.mauricio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS); + logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS); } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/PendingWriteQueue.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/PendingWriteQueue.java index 1a396fda..b51f4235 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/PendingWriteQueue.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/PendingWriteQueue.java @@ -99,12 +99,16 @@ public void removeAndFailAll(Throwable cause) { if (cause == null) { throw new NullPointerException("cause"); } + // Guard against re-entrance by directly reset PendingWrite write = head; + head = tail = null; + size = 0; + while (write != null) { PendingWrite next = write.next; ReferenceCountUtil.safeRelease(write.msg); ChannelPromise promise = write.promise; - recycle(write); + recycle(write, false); safeFail(promise, cause); write = next; } @@ -121,13 +125,14 @@ public void removeAndFail(Throwable cause) { throw new NullPointerException("cause"); } PendingWrite write = head; + if (write == null) { return; } ReferenceCountUtil.safeRelease(write.msg); ChannelPromise promise = write.promise; safeFail(promise, cause); - recycle(write); + recycle(write, true); } /** @@ -139,22 +144,28 @@ public void removeAndFail(Throwable cause) { */ public ChannelFuture removeAndWriteAll() { assert ctx.executor().inEventLoop(); + + if (size == 1) { + // No need to use ChannelPromiseAggregator for this case. + return removeAndWrite(); + } PendingWrite write = head; if (write == null) { // empty so just return null return null; } - if (size == 1) { - // No need to use ChannelPromiseAggregator for this case. - return removeAndWrite(); - } + + // Guard against re-entrance by directly reset + head = tail = null; + size = 0; + ChannelPromise p = ctx.newPromise(); ChannelPromiseAggregator aggregator = new ChannelPromiseAggregator(p); while (write != null) { PendingWrite next = write.next; Object msg = write.msg; ChannelPromise promise = write.promise; - recycle(write); + recycle(write, false); ctx.write(msg, promise); aggregator.add(promise); write = next; @@ -182,7 +193,7 @@ public ChannelFuture removeAndWrite() { } Object msg = write.msg; ChannelPromise promise = write.promise; - recycle(write); + recycle(write, true); return ctx.write(msg, promise); } @@ -200,7 +211,7 @@ public ChannelPromise remove() { } ChannelPromise promise = write.promise; ReferenceCountUtil.safeRelease(write.msg); - recycle(write); + recycle(write, true); return promise; } @@ -216,19 +227,21 @@ public Object current() { return write.msg; } - private void recycle(PendingWrite write) { + private void recycle(PendingWrite write, boolean update) { final PendingWrite next = write.next; final long writeSize = write.size; - size --; - - if (next == null) { - // Handled last PendingWrite so rest head and tail - head = tail = null; - assert size == 0; - } else { - head = next; - assert size > 0; + if (update) { + if (next == null) { + // Handled last PendingWrite so rest head and tail + // Guard against re-entrance by directly reset + head = tail = null; + size = 0; + } else { + head = next; + size --; + assert size > 0; + } } write.recycle(); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/SingleThreadEventLoop.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/SingleThreadEventLoop.java index de2a9903..9029c46f 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/SingleThreadEventLoop.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/SingleThreadEventLoop.java @@ -67,7 +67,7 @@ protected boolean wakesUpForTask(Runnable task) { } /** - * Marker interface for {@linkRunnable} that will not trigger an {@link #wakeup(boolean)} in all cases. + * Marker interface for {@link Runnable} that will not trigger an {@link #wakeup(boolean)} in all cases. */ interface NonWakeupRunnable extends Runnable { } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/embedded/EmbeddedChannel.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/embedded/EmbeddedChannel.java index e2c83299..27ed69c9 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/embedded/EmbeddedChannel.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/embedded/EmbeddedChannel.java @@ -217,18 +217,23 @@ public boolean writeOutbound(Object... msgs) { /** * Mark this {@link Channel} as finished. Any futher try to write data to it will fail. * - * * @return bufferReadable returns {@code true} if any of the used buffers has something left to read */ public boolean finish() { close(); runPendingTasks(); + + // Cancel all scheduled tasks that are left. + loop.cancelScheduledTasks(); + checkException(); + return !inboundMessages.isEmpty() || !outboundMessages.isEmpty(); } /** - * Run all tasks that are pending in the {@link EventLoop} for this {@link Channel} + * Run all tasks (which also includes scheduled tasks) that are pending in the {@link EventLoop} + * for this {@link Channel} */ public void runPendingTasks() { try { @@ -236,6 +241,26 @@ public void runPendingTasks() { } catch (Exception e) { recordException(e); } + + try { + loop.runScheduledTasks(); + } catch (Exception e) { + recordException(e); + } + } + + /** + * Run all pending scheduled tasks in the {@link EventLoop} for this {@link Channel} and return the + * {@code nanoseconds} when the next scheduled task is ready to run. If no other task was scheduled it will return + * {@code -1}. + */ + public long runScheduledPendingTasks() { + try { + return loop.runScheduledTasks(); + } catch (Exception e) { + recordException(e); + return loop.nextScheduledTask(); + } } private void recordException(Throwable cause) { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/embedded/EmbeddedEventLoop.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/embedded/EmbeddedEventLoop.java index 5e129877..f85c981a 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/embedded/EmbeddedEventLoop.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/embedded/EmbeddedEventLoop.java @@ -21,14 +21,14 @@ import com.github.mauricio.netty.channel.DefaultChannelPromise; import com.github.mauricio.netty.channel.EventLoop; import com.github.mauricio.netty.channel.EventLoopGroup; -import com.github.mauricio.netty.util.concurrent.AbstractEventExecutor; +import com.github.mauricio.netty.util.concurrent.AbstractScheduledEventExecutor; import com.github.mauricio.netty.util.concurrent.Future; import java.util.ArrayDeque; import java.util.Queue; import java.util.concurrent.TimeUnit; -final class EmbeddedEventLoop extends AbstractEventExecutor implements EventLoop { +final class EmbeddedEventLoop extends AbstractScheduledEventExecutor implements EventLoop { private final Queue tasks = new ArrayDeque(2); @@ -51,6 +51,27 @@ void runTasks() { } } + long runScheduledTasks() { + long time = AbstractScheduledEventExecutor.nanoTime(); + for (;;) { + Runnable task = pollScheduledTask(time); + if (task == null) { + return nextScheduledTaskNano(); + } + + task.run(); + } + } + + long nextScheduledTask() { + return nextScheduledTaskNano(); + } + + @Override + protected void cancelScheduledTasks() { + super.cancelScheduledTasks(); + } + @Override public Future shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) { throw new UnsupportedOperationException(); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/group/DefaultChannelGroup.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/group/DefaultChannelGroup.java index c966a03f..e89dd4c3 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/group/DefaultChannelGroup.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/group/DefaultChannelGroup.java @@ -332,6 +332,36 @@ public ChannelGroupFuture writeAndFlush(Object message, ChannelMatcher matcher) return new DefaultChannelGroupFuture(this, futures, executor); } + /** + * Returns the {@link ChannelGroupFuture} which will be notified when all {@link Channel}s that are part of this + * {@link ChannelGroup}, at the time of calling, are closed. + */ + public ChannelGroupFuture newCloseFuture() { + return newCloseFuture(ChannelMatchers.all()); + } + + /** + * Returns the {@link ChannelGroupFuture} which will be notified when all {@link Channel}s that are part of this + * {@link ChannelGroup}, at the time of calling, are closed. + */ + public ChannelGroupFuture newCloseFuture(ChannelMatcher matcher) { + Map futures = + new LinkedHashMap(size()); + + for (Channel c: serverChannels) { + if (matcher.matches(c)) { + futures.put(c, c.closeFuture()); + } + } + for (Channel c: nonServerChannels) { + if (matcher.matches(c)) { + futures.put(c, c.closeFuture()); + } + } + + return new DefaultChannelGroupFuture(this, futures, executor); + } + @Override public int hashCode() { return System.identityHashCode(this); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/group/package-info.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/group/package-info.java index 768fa864..5e029896 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/group/package-info.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/group/package-info.java @@ -16,6 +16,6 @@ /** * A channel registry which helps a user maintain the list of open - * {@link com.github.mauricio.netty.channel.Channel}s and perform bulk operations on them. + * {@link io.netty.channel.Channel}s and perform bulk operations on them. */ package com.github.mauricio.netty.channel.group; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/local/LocalChannel.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/local/LocalChannel.java index b4b3db92..f5e8c669 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/local/LocalChannel.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/local/LocalChannel.java @@ -29,14 +29,14 @@ import com.github.mauricio.netty.util.ReferenceCountUtil; import com.github.mauricio.netty.util.concurrent.SingleThreadEventExecutor; import com.github.mauricio.netty.util.internal.InternalThreadLocalMap; +import com.github.mauricio.netty.util.internal.OneTimeTask; +import com.github.mauricio.netty.util.internal.PlatformDependent; import java.net.SocketAddress; import java.nio.channels.AlreadyConnectedException; import java.nio.channels.ClosedChannelException; import java.nio.channels.ConnectionPendingException; import java.nio.channels.NotYetConnectedException; -import java.util.ArrayDeque; -import java.util.Collections; import java.util.Queue; /** @@ -49,7 +49,8 @@ public class LocalChannel extends AbstractChannel { private static final int MAX_READER_STACK_DEPTH = 8; private final ChannelConfig config = new DefaultChannelConfig(this); - private final Queue inboundBuffer = new ArrayDeque(); + // To futher optimize this we could write our own SPSC queue. + private final Queue inboundBuffer = PlatformDependent.newMpscQueue(); private final Runnable readTask = new Runnable() { @Override public void run() { @@ -64,7 +65,6 @@ public void run() { pipeline.fireChannelReadComplete(); } }; - private final Runnable shutdownHook = new Runnable() { @Override public void run() { @@ -286,29 +286,25 @@ protected void doWrite(ChannelOutboundBuffer in) throws Exception { final ChannelPipeline peerPipeline = peer.pipeline(); final EventLoop peerLoop = peer.eventLoop(); - if (peerLoop == eventLoop()) { - for (;;) { - Object msg = in.current(); - if (msg == null) { - break; - } - peer.inboundBuffer.add(msg); - ReferenceCountUtil.retain(msg); - in.remove(); + for (;;) { + Object msg = in.current(); + if (msg == null) { + break; } - finishPeerRead(peer, peerPipeline); - } else { - // Use a copy because the original msgs will be recycled by AbstractChannel. - final Object[] msgsCopy = new Object[in.size()]; - for (int i = 0; i < msgsCopy.length; i ++) { - msgsCopy[i] = ReferenceCountUtil.retain(in.current()); + try { + peer.inboundBuffer.add(ReferenceCountUtil.retain(msg)); in.remove(); + } catch (Throwable cause) { + in.remove(cause); } + } - peerLoop.execute(new Runnable() { + if (peerLoop == eventLoop()) { + finishPeerRead(peer, peerPipeline); + } else { + peerLoop.execute(new OneTimeTask() { @Override public void run() { - Collections.addAll(peer.inboundBuffer, msgsCopy); finishPeerRead(peer, peerPipeline); } }); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/AbstractNioByteChannel.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/AbstractNioByteChannel.java index a36ea551..ab21dfd7 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/AbstractNioByteChannel.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/AbstractNioByteChannel.java @@ -57,7 +57,7 @@ protected AbstractNioUnsafe newUnsafe() { return new NioByteUnsafe(); } - private final class NioByteUnsafe extends AbstractNioUnsafe { + protected class NioByteUnsafe extends AbstractNioUnsafe { private RecvByteBufAllocator.Handle allocHandle; private void closeOnRead(ChannelPipeline pipeline) { @@ -91,7 +91,7 @@ private void handleReadException(ChannelPipeline pipeline, } @Override - public void read() { + public final void read() { final ChannelConfig config = config(); if (!config.isAutoRead() && !isReadPending()) { // ChannelConfig.setAutoRead(false) was called in the meantime @@ -120,6 +120,7 @@ public void read() { if (localReadAmount <= 0) { // not was read release the buffer byteBuf.release(); + byteBuf = null; close = localReadAmount < 0; break; } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/AbstractNioChannel.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/AbstractNioChannel.java index 740243c9..ef94f3db 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/AbstractNioChannel.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/AbstractNioChannel.java @@ -29,6 +29,7 @@ import com.github.mauricio.netty.channel.EventLoop; import com.github.mauricio.netty.util.ReferenceCountUtil; import com.github.mauricio.netty.util.ReferenceCounted; +import com.github.mauricio.netty.util.internal.EmptyArrays; import com.github.mauricio.netty.util.internal.OneTimeTask; import com.github.mauricio.netty.util.internal.logging.InternalLogger; import com.github.mauricio.netty.util.internal.logging.InternalLoggerFactory; @@ -36,6 +37,7 @@ import java.io.IOException; import java.net.SocketAddress; import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.util.concurrent.ScheduledFuture; @@ -49,6 +51,12 @@ public abstract class AbstractNioChannel extends AbstractChannel { private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractNioChannel.class); + private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); + + static { + CLOSED_CHANNEL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + } + private final SelectableChannel ch; protected final int readInterestOp; volatile SelectionKey selectionKey; @@ -445,4 +453,20 @@ protected final ByteBuf newDirectBuffer(ReferenceCounted holder, ByteBuf buf) { return buf; } + + @Override + protected void doClose() throws Exception { + ChannelPromise promise = connectPromise; + if (promise != null) { + // Use tryFailure() instead of setFailure() to avoid the race against cancel(). + promise.tryFailure(CLOSED_CHANNEL_EXCEPTION); + connectPromise = null; + } + + ScheduledFuture future = connectTimeoutFuture; + if (future != null) { + future.cancel(false); + connectTimeoutFuture = null; + } + } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/NioEventLoop.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/NioEventLoop.java index df7a669d..c9c44e9b 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/NioEventLoop.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/nio/NioEventLoop.java @@ -55,7 +55,7 @@ public final class NioEventLoop extends SingleThreadEventLoop { private static final int CLEANUP_INTERVAL = 256; // XXX Hard-coded value, but won't need customization. private static final boolean DISABLE_KEYSET_OPTIMIZATION = - SystemPropertyUtil.getBoolean("com.github.mauricio.netty.noKeySetOptimization", false); + SystemPropertyUtil.getBoolean("io.netty.noKeySetOptimization", false); private static final int MIN_PREMATURE_SELECTOR_RETURNS = 3; private static final int SELECTOR_AUTO_REBUILD_THRESHOLD; @@ -78,7 +78,7 @@ public final class NioEventLoop extends SingleThreadEventLoop { } } - int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("com.github.mauricio.netty.selectorAutoRebuildThreshold", 512); + int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512); if (selectorAutoRebuildThreshold < MIN_PREMATURE_SELECTOR_RETURNS) { selectorAutoRebuildThreshold = 0; } @@ -86,8 +86,8 @@ public final class NioEventLoop extends SingleThreadEventLoop { SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold; if (logger.isDebugEnabled()) { - logger.debug("-Dcom.github.mauricio.netty.noKeySetOptimization: {}", DISABLE_KEYSET_OPTIMIZATION); - logger.debug("-Dcom.github.mauricio.netty.selectorAutoRebuildThreshold: {}", SELECTOR_AUTO_REBUILD_THRESHOLD); + logger.debug("-Dio.netty.noKeySetOptimization: {}", DISABLE_KEYSET_OPTIMIZATION); + logger.debug("-Dio.netty.selectorAutoRebuildThreshold: {}", SELECTOR_AUTO_REBUILD_THRESHOLD); } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/AbstractChannelPoolHandler.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/AbstractChannelPoolHandler.java new file mode 100644 index 00000000..17adfc86 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/AbstractChannelPoolHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.channel.pool; + +import com.github.mauricio.netty.channel.Channel; + +/** + * A skeletal {@link ChannelPoolHandler} implementation. + */ +public abstract class AbstractChannelPoolHandler implements ChannelPoolHandler { + + /** + * NOOP implementation, sub-classes may override this. + * + * {@inheritDoc} + */ + @Override + public void channelAcquired(@SuppressWarnings("unused") Channel ch) throws Exception { + // NOOP + } + + /** + * NOOP implementation, sub-classes may override this. + * + * {@inheritDoc} + */ + @Override + public void channelReleased(@SuppressWarnings("unused") Channel ch) throws Exception { + // NOOP + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/AbstractChannelPoolMap.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/AbstractChannelPoolMap.java new file mode 100644 index 00000000..9ab6a6c3 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/AbstractChannelPoolMap.java @@ -0,0 +1,100 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.channel.pool; + +import com.github.mauricio.netty.util.internal.PlatformDependent; +import com.github.mauricio.netty.util.internal.ReadOnlyIterator; + +import java.io.Closeable; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentMap; + +import static com.github.mauricio.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * A skeletal {@link ChannelPoolMap} implementation. To find the right {@link ChannelPool} + * the {@link Object#hashCode()} and {@link Object#equals(Object)} is used. + */ +public abstract class AbstractChannelPoolMap + implements ChannelPoolMap, Iterable>, Closeable { + private final ConcurrentMap map = PlatformDependent.newConcurrentHashMap(); + + @Override + public final P get(K key) { + P pool = map.get(checkNotNull(key, "key")); + if (pool == null) { + pool = newPool(key); + P old = map.putIfAbsent(key, pool); + if (old != null) { + // We need to destroy the newly created pool as we not use it. + pool.close(); + pool = old; + } + } + return pool; + } + /** + * Remove the {@link ChannelPool} from this {@link AbstractChannelPoolMap}. Returns {@code true} if removed, + * {@code false} otherwise. + * + * Please note that {@code null} keys are not allowed. + */ + public final boolean remove(K key) { + P pool = map.remove(checkNotNull(key, "key")); + if (pool != null) { + pool.close(); + return true; + } + return false; + } + + @Override + public final Iterator> iterator() { + return new ReadOnlyIterator>(map.entrySet().iterator()); + } + + /** + * Returns the number of {@link ChannelPool}s currently in this {@link AbstractChannelPoolMap}. + */ + public final int size() { + return map.size(); + } + + /** + * Returns {@code true} if the {@link AbstractChannelPoolMap} is empty, otherwise {@code false}. + */ + public final boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public final boolean contains(K key) { + return map.containsKey(checkNotNull(key, "key")); + } + + /** + * Called once a new {@link ChannelPool} needs to be created as non exists yet for the {@code key}. + */ + protected abstract P newPool(K key); + + @Override + public final void close() { + for (K key: map.keySet()) { + remove(key); + } + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelHealthChecker.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelHealthChecker.java new file mode 100644 index 00000000..260cb0f5 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelHealthChecker.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.channel.pool; + +import com.github.mauricio.netty.channel.Channel; +import com.github.mauricio.netty.channel.EventLoop; +import com.github.mauricio.netty.util.concurrent.Future; +import com.github.mauricio.netty.util.concurrent.Promise; + +/** + * Called before a {@link Channel} will be returned via {@link ChannelPool#acquire()} or + * {@link ChannelPool#acquire(Promise)}. + */ +public interface ChannelHealthChecker { + + /** + * {@link ChannelHealthChecker} implementation that checks if {@link Channel#isActive()} returns {@code true}. + */ + ChannelHealthChecker ACTIVE = new ChannelHealthChecker() { + @Override + public Future isHealthy(Channel channel) { + EventLoop loop = channel.eventLoop(); + return channel.isActive()? loop.newSucceededFuture(Boolean.TRUE) : loop.newSucceededFuture(Boolean.FALSE); + } + }; + + /** + * Check if the given channel is healthy which means it can be used. The returned {@link Future} is notified once + * the check is complete. If notified with {@link Boolean#TRUE} it can be used {@link Boolean#FALSE} otherwise. + * + * This method will be called by the {@link EventLoop} of the {@link Channel}. + */ + Future isHealthy(Channel channel); +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelPool.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelPool.java new file mode 100644 index 00000000..1aa49765 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelPool.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.channel.pool; + +import com.github.mauricio.netty.channel.Channel; +import com.github.mauricio.netty.util.concurrent.Future; +import com.github.mauricio.netty.util.concurrent.Promise; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Allows to acquire and release {@link Channel} and so act as a pool of these. + */ +public interface ChannelPool extends Closeable { + + /** + * Acquire a {@link Channel} from this {@link ChannelPool}. The returned {@link Future} is notified once + * the acquire is successful and failed otherwise. + */ + Future acquire(); + + /** + * Acquire a {@link Channel} from this {@link ChannelPool}. The given {@link Promise} is notified once + * the acquire is successful and failed otherwise. + */ + Future acquire(Promise promise); + + /** + * Release a {@link Channel} back to this {@link ChannelPool}. The returned {@link Future} is notified once + * the release is successful and failed otherwise. When failed the {@link Channel} will automatically closed. + */ + Future release(Channel channel); + + /** + * Release a {@link Channel} back to this {@link ChannelPool}. The given {@link Promise} is notified once + * the release is successful and failed otherwise. When failed the {@link Channel} will automatically closed. + */ + Future release(Channel channel, Promise promise); + + @Override + void close(); +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelPoolHandler.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelPoolHandler.java new file mode 100644 index 00000000..48953b79 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelPoolHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.channel.pool; + +import com.github.mauricio.netty.channel.Channel; +import com.github.mauricio.netty.channel.EventLoop; +import com.github.mauricio.netty.util.concurrent.Promise; + +/** + * Handler which is called for various actions done by the {@link ChannelPool}. + */ +public interface ChannelPoolHandler { + /** + * Called once a {@link Channel} was released by calling {@link ChannelPool#release(Channel)} or + * {@link ChannelPool#release(Channel, Promise)}. + * + * This method will be called by the {@link EventLoop} of the {@link Channel}. + */ + void channelReleased(Channel ch) throws Exception; + + /** + * Called once a {@link Channel} was acquired by calling {@link ChannelPool#acquire()} or + * {@link ChannelPool#acquire(Promise)}. + * + * This method will be called by the {@link EventLoop} of the {@link Channel}. + */ + void channelAcquired(Channel ch) throws Exception; + + /** + * Called once a new {@link Channel} is created in the {@link ChannelPool}. + * + * This method will be called by the {@link EventLoop} of the {@link Channel}. + */ + void channelCreated(Channel ch) throws Exception; +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelPoolMap.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelPoolMap.java new file mode 100644 index 00000000..bafd560c --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/ChannelPoolMap.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.channel.pool; + +/** + * Allows to map {@link ChannelPool} implementations to a specific key. + * + * @param the type of the key + * @param

    the type of the {@link ChannelPool} + */ +public interface ChannelPoolMap { + /** + * Return the {@link ChannelPool} for the {@code code}. This will never return {@code null}, + * but create a new {@link ChannelPool} if non exists for they requested {@code key}. + * + * Please note that {@code null} keys are not allowed. + */ + P get(K key); + + /** + * Returns {@code true} if a {@link ChannelPool} exists for the given {@code key}. + * + * Please note that {@code null} keys are not allowed. + */ + boolean contains(K key); +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/FixedChannelPool.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/FixedChannelPool.java new file mode 100644 index 00000000..8e8cdd1b --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/FixedChannelPool.java @@ -0,0 +1,366 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.channel.pool; + +import com.github.mauricio.netty.bootstrap.Bootstrap; +import com.github.mauricio.netty.channel.Channel; +import com.github.mauricio.netty.util.concurrent.EventExecutor; +import com.github.mauricio.netty.util.concurrent.Future; +import com.github.mauricio.netty.util.concurrent.FutureListener; +import com.github.mauricio.netty.util.concurrent.Promise; +import com.github.mauricio.netty.util.internal.EmptyArrays; +import com.github.mauricio.netty.util.internal.OneTimeTask; + +import java.nio.channels.ClosedChannelException; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * {@link ChannelPool} implementation that takes another {@link ChannelPool} implementation and enforce a maximum + * number of concurrent connections. + */ +public final class FixedChannelPool extends SimpleChannelPool { + private static final IllegalStateException FULL_EXCEPTION = + new IllegalStateException("Too many outstanding acquire operations"); + private static final TimeoutException TIMEOUT_EXCEPTION = + new TimeoutException("Acquire operation took longer then configured maximum time"); + + static { + FULL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + TIMEOUT_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + } + + public enum AcquireTimeoutAction { + /** + * Create a new connection when the timeout is detected. + */ + NEW, + + /** + * Fail the {@link Future} of the acquire call with a {@link TimeoutException}. + */ + FAIL + } + + private final EventExecutor executor; + private final long acquireTimeoutNanos; + private final Runnable timeoutTask; + + // There is no need to worry about synchronization as everything that modified the queue or counts is done + // by the above EventExecutor. + private final Queue pendingAcquireQueue = new ArrayDeque(); + private final int maxConnections; + private final int maxPendingAcquires; + private int acquiredChannelCount; + private int pendingAcquireCount; + + /** + * Creates a new instance using the {@link ChannelHealthChecker#ACTIVE}. + * + * @param bootstrap the {@link Bootstrap} that is used for connections + * @param handler the {@link ChannelPoolHandler} that will be notified for the different pool actions + * @param maxConnections the numnber of maximal active connections, once this is reached new tries to acquire + * a {@link Channel} will be delayed until a connection is returned to the pool again. + */ + public FixedChannelPool(Bootstrap bootstrap, + ChannelPoolHandler handler, int maxConnections) { + this(bootstrap, handler, maxConnections, Integer.MAX_VALUE); + } + + /** + * Creates a new instance using the {@link ChannelHealthChecker#ACTIVE}. + * + * @param bootstrap the {@link Bootstrap} that is used for connections + * @param handler the {@link ChannelPoolHandler} that will be notified for the different pool actions + * @param maxConnections the numnber of maximal active connections, once this is reached new tries to + * acquire a {@link Channel} will be delayed until a connection is returned to the + * pool again. + * @param maxPendingAcquires the maximum number of pending acquires. Once this is exceed acquire tries will + * be failed. + */ + public FixedChannelPool(Bootstrap bootstrap, + ChannelPoolHandler handler, int maxConnections, int maxPendingAcquires) { + this(bootstrap, handler, ChannelHealthChecker.ACTIVE, null, -1, maxConnections, maxPendingAcquires); + } + + /** + * Creates a new instance. + * + * @param bootstrap the {@link Bootstrap} that is used for connections + * @param handler the {@link ChannelPoolHandler} that will be notified for the different pool actions + * @param healthCheck the {@link ChannelHealthChecker} that will be used to check if a {@link Channel} is + * still healty when obtain from the {@link ChannelPool} + * @param action the {@link AcquireTimeoutAction} to use or {@code null} if non should be used. + * In this case {@param acquireTimeoutMillis} must be {@code -1}. + * @param acquireTimeoutMillis the time (in milliseconds) after which an pending acquire must complete or + * the {@link AcquireTimeoutAction} takes place. + * @param maxConnections the numnber of maximal active connections, once this is reached new tries to + * acquire a {@link Channel} will be delayed until a connection is returned to the + * pool again. + * @param maxPendingAcquires the maximum number of pending acquires. Once this is exceed acquire tries will + * be failed. + */ + public FixedChannelPool(Bootstrap bootstrap, + ChannelPoolHandler handler, + ChannelHealthChecker healthCheck, AcquireTimeoutAction action, + final long acquireTimeoutMillis, + int maxConnections, int maxPendingAcquires) { + super(bootstrap, handler, healthCheck); + if (maxConnections < 1) { + throw new IllegalArgumentException("maxConnections: " + maxConnections + " (expected: >= 1)"); + } + if (maxPendingAcquires < 1) { + throw new IllegalArgumentException("maxPendingAcquires: " + maxPendingAcquires + " (expected: >= 1)"); + } + if (action == null && acquireTimeoutMillis == -1) { + timeoutTask = null; + acquireTimeoutNanos = -1; + } else if (action == null && acquireTimeoutMillis != -1) { + throw new NullPointerException("action"); + } else if (action != null && acquireTimeoutMillis < 0) { + throw new IllegalArgumentException("acquireTimeoutMillis: " + acquireTimeoutMillis + " (expected: >= 1)"); + } else { + acquireTimeoutNanos = TimeUnit.MILLISECONDS.toNanos(acquireTimeoutMillis); + switch (action) { + case FAIL: + timeoutTask = new TimeoutTask() { + @Override + public void onTimeout(AcquireTask task) { + // Fail the promise as we timed out. + task.promise.setFailure(TIMEOUT_EXCEPTION); + } + }; + break; + case NEW: + timeoutTask = new TimeoutTask() { + @Override + public void onTimeout(AcquireTask task) { + // Increment the acquire count and delegate to super to actually acquire a Channel which will + // create a new connetion. + ++acquiredChannelCount; + + FixedChannelPool.super.acquire(task.promise); + } + }; + break; + default: + throw new Error(); + } + } + executor = bootstrap.group().next(); + this.maxConnections = maxConnections; + this.maxPendingAcquires = maxPendingAcquires; + } + + @Override + public Future acquire(final Promise promise) { + try { + if (executor.inEventLoop()) { + acquire0(promise); + } else { + executor.execute(new OneTimeTask() { + @Override + public void run() { + acquire0(promise); + } + }); + } + } catch (Throwable cause) { + promise.setFailure(cause); + } + return promise; + } + + private void acquire0(final Promise promise) { + assert executor.inEventLoop(); + + if (acquiredChannelCount < maxConnections) { + ++acquiredChannelCount; + + assert acquiredChannelCount > 0; + + // We need to create a new promise as we need to ensure the AcquireListener runs in the correct + // EventLoop + Promise p = executor.newPromise(); + p.addListener(new AcquireListener(promise)); + super.acquire(p); + } else { + if (pendingAcquireCount >= maxPendingAcquires) { + promise.setFailure(FULL_EXCEPTION); + } else { + AcquireTask task = new AcquireTask(promise); + if (pendingAcquireQueue.offer(task)) { + ++pendingAcquireCount; + + if (timeoutTask != null) { + task.timeoutFuture = executor.schedule(timeoutTask, acquireTimeoutNanos, TimeUnit.NANOSECONDS); + } + } else { + promise.setFailure(FULL_EXCEPTION); + } + } + + assert pendingAcquireCount > 0; + } + } + + @Override + public Future release(final Channel channel, final Promise promise) { + final Promise p = executor.newPromise(); + super.release(channel, p.addListener(new FutureListener() { + + @Override + public void operationComplete(Future future) throws Exception { + assert executor.inEventLoop(); + + if (future.isSuccess()) { + decrementAndRunTaskQueue(); + promise.setSuccess(null); + } else { + Throwable cause = future.cause(); + // Check if the exception was not because of we passed the Channel to the wrong pool. + if (!(cause instanceof IllegalArgumentException)) { + decrementAndRunTaskQueue(); + } + promise.setFailure(future.cause()); + } + } + })); + return p; + } + + private void decrementAndRunTaskQueue() { + --acquiredChannelCount; + + // We should never have a negative value. + assert acquiredChannelCount >= 0; + + // Run the pending acquire tasks before notify the original promise so if the user would + // try to acquire again from the ChannelFutureListener and the pendingAcquireCount is >= + // maxPendingAcquires we may be able to run some pending tasks first and so allow to add + // more. + runTaskQueue(); + } + + private void runTaskQueue() { + while (acquiredChannelCount < maxConnections) { + AcquireTask task = pendingAcquireQueue.poll(); + if (task == null) { + break; + } + + // Cancel the timeout if one was scheduled + ScheduledFuture timeoutFuture = task.timeoutFuture; + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + } + + --pendingAcquireCount; + ++acquiredChannelCount; + + super.acquire(task.promise); + } + + // We should never have a negative value. + assert pendingAcquireCount >= 0; + assert acquiredChannelCount >= 0; + } + + // AcquireTask extends AcquireListener to reduce object creations and so GC pressure + private final class AcquireTask extends AcquireListener { + final Promise promise; + final long expireNanoTime = System.nanoTime() + acquireTimeoutNanos; + ScheduledFuture timeoutFuture; + + public AcquireTask(Promise promise) { + super(promise); + // We need to create a new promise as we need to ensure the AcquireListener runs in the correct + // EventLoop. + this.promise = executor.newPromise().addListener(this); + } + } + + private abstract class TimeoutTask implements Runnable { + @Override + public final void run() { + assert executor.inEventLoop(); + long nanoTime = System.nanoTime(); + for (;;) { + AcquireTask task = pendingAcquireQueue.peek(); + // Compare nanoTime as descripted in the javadocs of System.nanoTime() + // + // See https://docs.oracle.com/javase/7/docs/api/java/lang/System.html#nanoTime() + // See https://github.com/netty/netty/issues/3705 + if (task == null || nanoTime - task.expireNanoTime < 0) { + break; + } + pendingAcquireQueue.remove(); + + --pendingAcquireCount; + onTimeout(task); + } + } + + public abstract void onTimeout(AcquireTask task); + } + + private class AcquireListener implements FutureListener { + private final Promise originalPromise; + + AcquireListener(Promise originalPromise) { + this.originalPromise = originalPromise; + } + + @Override + public void operationComplete(Future future) throws Exception { + assert executor.inEventLoop(); + + if (future.isSuccess()) { + originalPromise.setSuccess(future.getNow()); + } else { + // Something went wrong try to run pending acquire tasks. + decrementAndRunTaskQueue(); + originalPromise.setFailure(future.cause()); + } + } + } + + @Override + public void close() { + executor.execute(new OneTimeTask() { + @Override + public void run() { + for (;;) { + AcquireTask task = pendingAcquireQueue.poll(); + if (task == null) { + break; + } + ScheduledFuture f = task.timeoutFuture; + if (f != null) { + f.cancel(false); + } + task.promise.setFailure(new ClosedChannelException()); + } + acquiredChannelCount = 0; + pendingAcquireCount = 0; + FixedChannelPool.super.close(); + } + }); + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/SimpleChannelPool.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/SimpleChannelPool.java new file mode 100644 index 00000000..2b2e89a7 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/SimpleChannelPool.java @@ -0,0 +1,278 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.channel.pool; + +import com.github.mauricio.netty.bootstrap.Bootstrap; +import com.github.mauricio.netty.channel.Channel; +import com.github.mauricio.netty.channel.ChannelFuture; +import com.github.mauricio.netty.channel.ChannelFutureListener; +import com.github.mauricio.netty.channel.ChannelInitializer; +import com.github.mauricio.netty.channel.EventLoop; +import com.github.mauricio.netty.util.AttributeKey; +import com.github.mauricio.netty.util.concurrent.Future; +import com.github.mauricio.netty.util.concurrent.FutureListener; +import com.github.mauricio.netty.util.concurrent.Promise; +import com.github.mauricio.netty.util.internal.EmptyArrays; +import com.github.mauricio.netty.util.internal.OneTimeTask; +import com.github.mauricio.netty.util.internal.PlatformDependent; + +import java.util.Deque; + +import static com.github.mauricio.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Simple {@link ChannelPool} implementation which will create new {@link Channel}s if someone tries to acquire + * a {@link Channel} but none is in the pool atm. No limit on the maximal concurrent {@link Channel}s is enforced. + * + * This implementation uses LIFO order for {@link Channel}s in the {@link ChannelPool}. + * + */ +public class SimpleChannelPool implements ChannelPool { + private static final AttributeKey POOL_KEY = AttributeKey.newInstance("channelPool"); + private static final IllegalStateException FULL_EXCEPTION = new IllegalStateException("ChannelPool full"); + static { + FULL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + } + private final Deque deque = PlatformDependent.newConcurrentDeque(); + private final ChannelPoolHandler handler; + private final ChannelHealthChecker healthCheck; + private final Bootstrap bootstrap; + + /** + * Creates a new instance using the {@link ChannelHealthChecker#ACTIVE}. + * + * @param bootstrap the {@link Bootstrap} that is used for connections + * @param handler the {@link ChannelPoolHandler} that will be notified for the different pool actions + */ + public SimpleChannelPool(Bootstrap bootstrap, final ChannelPoolHandler handler) { + this(bootstrap, handler, ChannelHealthChecker.ACTIVE); + } + + /** + * Creates a new instance. + * + * @param bootstrap the {@link Bootstrap} that is used for connections + * @param handler the {@link ChannelPoolHandler} that will be notified for the different pool actions + * @param healthCheck the {@link ChannelHealthChecker} that will be used to check if a {@link Channel} is + * still healty when obtain from the {@link ChannelPool} + */ + public SimpleChannelPool(Bootstrap bootstrap, final ChannelPoolHandler handler, ChannelHealthChecker healthCheck) { + this.handler = checkNotNull(handler, "handler"); + this.healthCheck = checkNotNull(healthCheck, "healthCheck"); + // Clone the original Bootstrap as we want to set our own handler + this.bootstrap = checkNotNull(bootstrap, "bootstrap").clone(); + this.bootstrap.handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + assert ch.eventLoop().inEventLoop(); + handler.channelCreated(ch); + } + }); + } + + @Override + public final Future acquire() { + return acquire(bootstrap.group().next().newPromise()); + } + + @Override + public Future acquire(final Promise promise) { + checkNotNull(promise, "promise"); + try { + final Channel ch = pollChannel(); + if (ch == null) { + // No Channel left in the pool bootstrap a new Channel + Bootstrap bs = bootstrap.clone(); + bs.attr(POOL_KEY, this); + ChannelFuture f = connectChannel(bs); + if (f.isDone()) { + notifyConnect(f, promise); + } else { + f.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + notifyConnect(future, promise); + } + }); + } + return promise; + } + EventLoop loop = ch.eventLoop(); + if (loop.inEventLoop()) { + doHealthCheck(ch, promise); + } else { + loop.execute(new OneTimeTask() { + @Override + public void run() { + doHealthCheck(ch, promise); + } + }); + } + } catch (Throwable cause) { + promise.setFailure(cause); + } + return promise; + } + + private static void notifyConnect(ChannelFuture future, Promise promise) { + if (future.isSuccess()) { + promise.setSuccess(future.channel()); + } else { + promise.setFailure(future.cause()); + } + } + + private void doHealthCheck(final Channel ch, final Promise promise) { + assert ch.eventLoop().inEventLoop(); + + Future f = healthCheck.isHealthy(ch); + if (f.isDone()) { + notifyHealthCheck(f, ch, promise); + } else { + f.addListener(new FutureListener() { + @Override + public void operationComplete(Future future) throws Exception { + notifyHealthCheck(future, ch, promise); + } + }); + } + } + + private void notifyHealthCheck(Future future, Channel ch, Promise promise) { + assert ch.eventLoop().inEventLoop(); + + if (future.isSuccess()) { + if (future.getNow() == Boolean.TRUE) { + try { + ch.attr(POOL_KEY).set(this); + handler.channelAcquired(ch); + promise.setSuccess(ch); + } catch (Throwable cause) { + closeAndFail(ch, cause, promise); + } + } else { + closeChannel(ch); + acquire(promise); + } + } else { + closeChannel(ch); + acquire(promise); + } + } + + /** + * Bootstrap a new {@link Channel}. The default implementation uses {@link Bootstrap#connect()}, + * sub-classes may override this. + * + * The {@link Bootstrap} that is passed in here is cloned via {@link Bootstrap#clone()}, so it is safe to modify. + */ + protected ChannelFuture connectChannel(Bootstrap bs) { + return bs.connect(); + } + + @Override + public final Future release(Channel channel) { + return release(channel, channel.eventLoop().newPromise()); + } + + @Override + public Future release(final Channel channel, final Promise promise) { + checkNotNull(channel, "channel"); + checkNotNull(promise, "promise"); + try { + EventLoop loop = channel.eventLoop(); + if (loop.inEventLoop()) { + doReleaseChannel(channel, promise); + } else { + loop.execute(new OneTimeTask() { + @Override + public void run() { + doReleaseChannel(channel, promise); + } + }); + } + } catch (Throwable cause) { + closeAndFail(channel, cause, promise); + } + return promise; + } + + private void doReleaseChannel(Channel channel, Promise promise) { + assert channel.eventLoop().inEventLoop(); + // Remove the POOL_KEY attribute from the Channel and check if it was acquired from this pool, if not fail. + if (channel.attr(POOL_KEY).getAndSet(null) != this) { + closeAndFail(channel, + // Better include a stracktrace here as this is an user error. + new IllegalArgumentException( + "Channel " + channel + " was not acquired from this ChannelPool"), + promise); + } else { + try { + if (offerChannel(channel)) { + handler.channelReleased(channel); + promise.setSuccess(null); + } else { + closeAndFail(channel, FULL_EXCEPTION, promise); + } + } catch (Throwable cause) { + closeAndFail(channel, cause, promise); + } + } + } + + private static void closeChannel(Channel channel) { + channel.attr(POOL_KEY).getAndSet(null); + channel.close(); + } + + private static void closeAndFail(Channel channel, Throwable cause, Promise promise) { + closeChannel(channel); + promise.setFailure(cause); + } + + /** + * Poll a {@link Channel} out of the internal storage to reuse it. This will return {@code null} if no + * {@link Channel} is ready to be reused. + * + * Sub-classes may override {@link #pollChannel()} and {@link #offerChannel(Channel)}. Be aware that + * implementations of these methods needs to be thread-safe! + */ + protected Channel pollChannel() { + return deque.pollLast(); + } + + /** + * Offer a {@link Channel} back to the internal storage. This will return {@code true} if the {@link Channel} + * could be added, {@code false} otherwise. + * + * Sub-classes may override {@link #pollChannel()} and {@link #offerChannel(Channel)}. Be aware that + * implementations of these methods needs to be thread-safe! + */ + protected boolean offerChannel(Channel channel) { + return deque.offer(channel); + } + + @Override + public void close() { + for (;;) { + Channel channel = pollChannel(); + if (channel == null) { + break; + } + channel.close(); + } + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/package-info.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/package-info.java new file mode 100644 index 00000000..8134fc89 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/pool/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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. + */ + +/** + * Implementations and API for {@link io.netty.channel.Channel} pools. + */ +package com.github.mauricio.netty.channel.pool; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/DefaultDatagramChannelConfig.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/DefaultDatagramChannelConfig.java index d35d2081..ffe42746 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/DefaultDatagramChannelConfig.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/DefaultDatagramChannelConfig.java @@ -157,8 +157,8 @@ public DatagramChannelConfig setBroadcast(boolean broadcast) { try { // See: https://github.com/netty/netty/issues/576 if (broadcast && - !PlatformDependent.isWindows() && !PlatformDependent.isRoot() && - !javaSocket.getLocalAddress().isAnyLocalAddress()) { + !javaSocket.getLocalAddress().isAnyLocalAddress() && + !PlatformDependent.isWindows() && !PlatformDependent.isRoot()) { // Warn a user about the fact that a non-root user can't receive a // broadcast packet on *nix if the socket is bound on non-wildcard address. logger.warn( diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/nio/NioServerSocketChannel.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/nio/NioServerSocketChannel.java index 38362b7f..f32dacfa 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/nio/NioServerSocketChannel.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/nio/NioServerSocketChannel.java @@ -35,7 +35,7 @@ import java.util.List; /** - * A {@link com.github.mauricio.netty.channel.socket.ServerSocketChannel} implementation which uses + * A {@link io.netty.channel.socket.ServerSocketChannel} implementation which uses * NIO selector based implementation to accept new connections. */ public class NioServerSocketChannel extends AbstractNioMessageChannel diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/nio/NioSocketChannel.java b/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/nio/NioSocketChannel.java index ae37ced4..88a993ab 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/nio/NioSocketChannel.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/channel/socket/nio/NioSocketChannel.java @@ -28,6 +28,7 @@ import com.github.mauricio.netty.channel.socket.DefaultSocketChannelConfig; import com.github.mauricio.netty.channel.socket.ServerSocketChannel; import com.github.mauricio.netty.channel.socket.SocketChannelConfig; +import com.github.mauricio.netty.util.concurrent.GlobalEventExecutor; import com.github.mauricio.netty.util.internal.OneTimeTask; import java.io.IOException; @@ -38,9 +39,10 @@ import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; +import java.util.concurrent.Executor; /** - * {@link com.github.mauricio.netty.channel.socket.SocketChannel} which uses NIO selector based implementation. + * {@link io.netty.channel.socket.SocketChannel} which uses NIO selector based implementation. */ public class NioSocketChannel extends AbstractNioByteChannel implements com.github.mauricio.netty.channel.socket.SocketChannel { @@ -148,25 +150,39 @@ public ChannelFuture shutdownOutput() { @Override public ChannelFuture shutdownOutput(final ChannelPromise promise) { - EventLoop loop = eventLoop(); - if (loop.inEventLoop()) { - try { - javaChannel().socket().shutdownOutput(); - promise.setSuccess(); - } catch (Throwable t) { - promise.setFailure(t); - } - } else { - loop.execute(new OneTimeTask() { + Executor closeExecutor = ((NioSocketChannelUnsafe) unsafe()).closeExecutor(); + if (closeExecutor != null) { + closeExecutor.execute(new OneTimeTask() { @Override public void run() { - shutdownOutput(promise); + shutdownOutput0(promise); } }); + } else { + EventLoop loop = eventLoop(); + if (loop.inEventLoop()) { + shutdownOutput0(promise); + } else { + loop.execute(new OneTimeTask() { + @Override + public void run() { + shutdownOutput0(promise); + } + }); + } } return promise; } + private void shutdownOutput0(final ChannelPromise promise) { + try { + javaChannel().socket().shutdownOutput(); + promise.setSuccess(); + } catch (Throwable t) { + promise.setFailure(t); + } + } + @Override protected SocketAddress localAddress0() { return javaChannel().socket().getLocalSocketAddress(); @@ -217,6 +233,7 @@ protected void doDisconnect() throws Exception { @Override protected void doClose() throws Exception { + super.doClose(); javaChannel().close(); } @@ -308,6 +325,21 @@ protected void doWrite(ChannelOutboundBuffer in) throws Exception { } } + @Override + protected AbstractNioUnsafe newUnsafe() { + return new NioSocketChannelUnsafe(); + } + + private final class NioSocketChannelUnsafe extends NioByteUnsafe { + @Override + protected Executor closeExecutor() { + if (javaChannel().isOpen() && config().getSoLinger() > 0) { + return GlobalEventExecutor.INSTANCE; + } + return null; + } + } + private final class NioSocketChannelConfig extends DefaultSocketChannelConfig { private NioSocketChannelConfig(NioSocketChannel channel, Socket javaSocket) { super(channel, javaSocket); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ByteToMessageDecoder.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ByteToMessageDecoder.java index 60dc134f..58ba7e3e 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ByteToMessageDecoder.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ByteToMessageDecoder.java @@ -16,6 +16,8 @@ package com.github.mauricio.netty.handler.codec; import com.github.mauricio.netty.buffer.ByteBuf; +import com.github.mauricio.netty.buffer.ByteBufAllocator; +import com.github.mauricio.netty.buffer.CompositeByteBuf; import com.github.mauricio.netty.buffer.Unpooled; import com.github.mauricio.netty.channel.ChannelHandlerContext; import com.github.mauricio.netty.channel.ChannelInboundHandlerAdapter; @@ -50,9 +52,9 @@ * If a custom frame decoder is required, then one needs to be careful when implementing * one with {@link ByteToMessageDecoder}. Ensure there are enough bytes in the buffer for a * complete frame by checking {@link ByteBuf#readableBytes()}. If there are not enough bytes - * for a complete frame, return without modify the reader index to allow more bytes to arrive. + * for a complete frame, return without modifying the reader index to allow more bytes to arrive. *

    - * To check for complete frames without modify the reader index, use methods like {@link ByteBuf#getInt(int)}. + * To check for complete frames without modifying the reader index, use methods like {@link ByteBuf#getInt(int)}. * One MUST use the reader index when using methods like {@link ByteBuf#getInt(int)}. * For example calling in.getInt(0) is assuming the frame starts at the beginning of the buffer, which * is not always the case. Use in.getInt(in.readerIndex()) instead. @@ -61,13 +63,75 @@ * Be aware that sub-classes of {@link ByteToMessageDecoder} MUST NOT * annotated with {@link @Sharable}. *

    - * Some methods such as {@link ByteBuf.readBytes(int)} will cause a memory leak if the returned buffer - * is not released or added to the out {@link List}. Use derived buffers like {@link ByteBuf.readSlice(int)} + * Some methods such as {@link ByteBuf#readBytes(int)} will cause a memory leak if the returned buffer + * is not released or added to the out {@link List}. Use derived buffers like {@link ByteBuf#readSlice(int)} * to avoid leaking memory. */ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter { + /** + * Cumulate {@link ByteBuf}s by merge them into one {@link ByteBuf}'s, using memory copies. + */ + public static final Cumulator MERGE_CUMULATOR = new Cumulator() { + @Override + public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) { + ByteBuf buffer; + if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes() + || cumulation.refCnt() > 1) { + // Expand cumulation (by replace it) when either there is not more room in the buffer + // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or + // duplicate().retain(). + // + // See: + // - https://github.com/netty/netty/issues/2327 + // - https://github.com/netty/netty/issues/1764 + buffer = expandCumulation(alloc, cumulation, in.readableBytes()); + } else { + buffer = cumulation; + } + buffer.writeBytes(in); + in.release(); + return buffer; + } + }; + + /** + * Cumulate {@link ByteBuf}s by add them to a {@link CompositeByteBuf} and so do no memory copy whenever possible. + * Be aware that {@link CompositeByteBuf} use a more complex indexing implementation so depending on your use-case + * and the decoder implementation this may be slower then just use the {@link #MERGE_CUMULATOR}. + */ + public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() { + @Override + public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) { + ByteBuf buffer; + if (cumulation.refCnt() > 1) { + // Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the user + // use slice().retain() or duplicate().retain(). + // + // See: + // - https://github.com/netty/netty/issues/2327 + // - https://github.com/netty/netty/issues/1764 + buffer = expandCumulation(alloc, cumulation, in.readableBytes()); + buffer.writeBytes(in); + in.release(); + } else { + CompositeByteBuf composite; + if (cumulation instanceof CompositeByteBuf) { + composite = (CompositeByteBuf) cumulation; + } else { + int readable = cumulation.readableBytes(); + composite = alloc.compositeBuffer(); + composite.addComponent(cumulation).writerIndex(readable); + } + composite.addComponent(in).writerIndex(composite.writerIndex() + in.readableBytes()); + buffer = composite; + } + return buffer; + } + }; + ByteBuf cumulation; + private Cumulator cumulator = MERGE_CUMULATOR; private boolean singleDecode; private boolean decodeWasNull; private boolean first; @@ -96,6 +160,16 @@ public boolean isSingleDecode() { return singleDecode; } + /** + * Set the {@link Cumulator} to use for cumulate the received {@link ByteBuf}s. + */ + public void setCumulator(Cumulator cumulator) { + if (cumulator == null) { + throw new NullPointerException("cumulator"); + } + this.cumulator = cumulator; + } + /** * Returns the actual number of readable bytes in the internal cumulative * buffer of this decoder. You usually do not need to rely on this value @@ -123,7 +197,7 @@ protected ByteBuf internalBuffer() { public final void handlerRemoved(ChannelHandlerContext ctx) throws Exception { ByteBuf buf = internalBuffer(); int readable = buf.readableBytes(); - if (buf.isReadable()) { + if (readable > 0) { ByteBuf bytes = buf.readBytes(readable); buf.release(); ctx.fireChannelRead(bytes); @@ -151,19 +225,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception if (first) { cumulation = data; } else { - if (cumulation.writerIndex() > cumulation.maxCapacity() - data.readableBytes() - || cumulation.refCnt() > 1) { - // Expand cumulation (by replace it) when either there is not more room in the buffer - // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or - // duplicate().retain(). - // - // See: - // - https://github.com/netty/netty/issues/2327 - // - https://github.com/netty/netty/issues/1764 - expandCumulation(ctx, data.readableBytes()); - } - cumulation.writeBytes(data); - data.release(); + cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data); } callDecode(ctx, cumulation, out); } catch (DecoderException e) { @@ -188,15 +250,19 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception } } - private void expandCumulation(ChannelHandlerContext ctx, int readable) { - ByteBuf oldCumulation = cumulation; - cumulation = ctx.alloc().buffer(oldCumulation.readableBytes() + readable); - cumulation.writeBytes(oldCumulation); - oldCumulation.release(); - } - @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + discardSomeReadBytes(); + if (decodeWasNull) { + decodeWasNull = false; + if (!ctx.channel().config().isAutoRead()) { + ctx.read(); + } + } + ctx.fireChannelReadComplete(); + } + + protected final void discardSomeReadBytes() { if (cumulation != null && !first && cumulation.refCnt() == 1) { // discard some bytes if possible to make more room in the // buffer but only if the refCnt == 1 as otherwise the user may have @@ -207,13 +273,6 @@ public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { // - https://github.com/netty/netty/issues/1764 cumulation.discardSomeReadBytes(); } - if (decodeWasNull) { - decodeWasNull = false; - if (!ctx.channel().config().isAutoRead()) { - ctx.read(); - } - } - ctx.fireChannelReadComplete(); } @Override @@ -322,4 +381,24 @@ protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List ou protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { decode(ctx, in, out); } + + static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) { + ByteBuf oldCumulation = cumulation; + cumulation = alloc.buffer(oldCumulation.readableBytes() + readable); + cumulation.writeBytes(oldCumulation); + oldCumulation.release(); + return cumulation; + } + + /** + * Cumulate {@link ByteBuf}s. + */ + public interface Cumulator { + /** + * Cumulate the given {@link ByteBuf}s and return the {@link ByteBuf} that holds the cumulated bytes. + * The implementation is responsible to correctly handle the life-cycle of the given {@link ByteBuf}s and so + * call {@link ByteBuf#release()} if a {@link ByteBuf} is fully consumed. + */ + ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in); + } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LengthFieldBasedFrameDecoder.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LengthFieldBasedFrameDecoder.java index 2c47793d..7e761f7d 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LengthFieldBasedFrameDecoder.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LengthFieldBasedFrameDecoder.java @@ -491,9 +491,7 @@ private void failIfNecessary(boolean firstDetectionOfTooLongFrame) { * is overridden to avoid memory copy. */ protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) { - ByteBuf frame = ctx.alloc().buffer(length); - frame.writeBytes(buffer, index, length); - return frame; + return buffer.slice(index, length).retain(); } private void fail(long frameLength) { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LengthFieldPrepender.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LengthFieldPrepender.java index 9139def8..edee623d 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LengthFieldPrepender.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LengthFieldPrepender.java @@ -18,7 +18,9 @@ import com.github.mauricio.netty.buffer.ByteBuf; import com.github.mauricio.netty.channel.ChannelHandler.Sharable; import com.github.mauricio.netty.channel.ChannelHandlerContext; +import com.github.mauricio.netty.util.internal.ObjectUtil; +import java.nio.ByteOrder; /** * An encoder that prepends the length of the message. The length value is @@ -49,6 +51,7 @@ @Sharable public class LengthFieldPrepender extends MessageToByteEncoder { + private final ByteOrder byteOrder; private final int lengthFieldLength; private final boolean lengthIncludesLengthFieldLength; private final int lengthAdjustment; @@ -114,6 +117,28 @@ public LengthFieldPrepender(int lengthFieldLength, int lengthAdjustment) { * if {@code lengthFieldLength} is not 1, 2, 3, 4, or 8 */ public LengthFieldPrepender(int lengthFieldLength, int lengthAdjustment, boolean lengthIncludesLengthFieldLength) { + this(ByteOrder.BIG_ENDIAN, lengthFieldLength, lengthAdjustment, lengthIncludesLengthFieldLength); + } + + /** + * Creates a new instance. + * + * @param byteOrder the {@link ByteOrder} of the length field + * @param lengthFieldLength the length of the prepended length field. + * Only 1, 2, 3, 4, and 8 are allowed. + * @param lengthAdjustment the compensation value to add to the value + * of the length field + * @param lengthIncludesLengthFieldLength + * if {@code true}, the length of the prepended + * length field is added to the value of the + * prepended length field. + * + * @throws IllegalArgumentException + * if {@code lengthFieldLength} is not 1, 2, 3, 4, or 8 + */ + public LengthFieldPrepender( + ByteOrder byteOrder, int lengthFieldLength, + int lengthAdjustment, boolean lengthIncludesLengthFieldLength) { if (lengthFieldLength != 1 && lengthFieldLength != 2 && lengthFieldLength != 3 && lengthFieldLength != 4 && lengthFieldLength != 8) { @@ -121,7 +146,9 @@ public LengthFieldPrepender(int lengthFieldLength, int lengthAdjustment, boolean "lengthFieldLength must be either 1, 2, 3, 4, or 8: " + lengthFieldLength); } + ObjectUtil.checkNotNull(byteOrder, "byteOrder"); + this.byteOrder = byteOrder; this.lengthFieldLength = lengthFieldLength; this.lengthIncludesLengthFieldLength = lengthIncludesLengthFieldLength; this.lengthAdjustment = lengthAdjustment; @@ -173,4 +200,9 @@ protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throw out.writeBytes(msg, msg.readerIndex(), msg.readableBytes()); } + + @Override + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception { + return super.allocateBuffer(ctx, msg, preferDirect).order(byteOrder); + } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LineBasedFrameDecoder.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LineBasedFrameDecoder.java index 5ae3eff1..4606caec 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LineBasedFrameDecoder.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/LineBasedFrameDecoder.java @@ -130,7 +130,7 @@ protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Except fail(ctx, length); } } else { - discardedBytes = buffer.readableBytes(); + discardedBytes += buffer.readableBytes(); buffer.readerIndex(buffer.writerIndex()); } return null; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ProtocolDetectionResult.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ProtocolDetectionResult.java new file mode 100644 index 00000000..f2d5d47a --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ProtocolDetectionResult.java @@ -0,0 +1,80 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.handler.codec; + +import static com.github.mauricio.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Result of detecting a protocol. + * + * @param the type of the protocol + */ +public final class ProtocolDetectionResult { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static final ProtocolDetectionResult NEEDS_MORE_DATE = + new ProtocolDetectionResult(ProtocolDetectionState.NEEDS_MORE_DATA, null); + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static final ProtocolDetectionResult INVALID = + new ProtocolDetectionResult(ProtocolDetectionState.INVALID, null); + + private final ProtocolDetectionState state; + private final T result; + + /** + * Returns a {@link ProtocolDetectionResult} that signals that more data is needed to detect the protocol. + */ + @SuppressWarnings("unchecked") + public static ProtocolDetectionResult needsMoreData() { + return NEEDS_MORE_DATE; + } + + /** + * Returns a {@link ProtocolDetectionResult} that signals the data was invalid for the protocol. + */ + @SuppressWarnings("unchecked") + public static ProtocolDetectionResult invalid() { + return INVALID; + } + + /** + * Returns a {@link ProtocolDetectionResult} which holds the detected protocol. + */ + @SuppressWarnings("unchecked") + public static ProtocolDetectionResult detected(T protocol) { + return new ProtocolDetectionResult(ProtocolDetectionState.DETECTED, checkNotNull(protocol, "protocol")); + } + + private ProtocolDetectionResult(ProtocolDetectionState state, T result) { + this.state = state; + this.result = result; + } + + /** + * Return the {@link ProtocolDetectionState}. If the state is {@link ProtocolDetectionState#DETECTED} you + * can retrieve the protocol via {@link #detectedProtocol()}. + */ + public ProtocolDetectionState state() { + return state; + } + + /** + * Returns the protocol if {@link #state()} returns {@link ProtocolDetectionState#DETECTED}, otherwise {@code null}. + */ + public T detectedProtocol() { + return result; + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ProtocolDetectionState.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ProtocolDetectionState.java new file mode 100644 index 00000000..23ff110e --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ProtocolDetectionState.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.handler.codec; + +/** + * The state of the current detection. + */ +public enum ProtocolDetectionState { + /** + * Need more data to detect the protocol. + */ + NEEDS_MORE_DATA, + + /** + * The data was invalid. + */ + INVALID, + + /** + * Protocol was detected, + */ + DETECTED +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoder.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoder.java index d74fa693..57f3b70c 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoder.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoder.java @@ -269,7 +269,7 @@ public abstract class ReplayingDecoder extends ByteToMessageDecoder { static final Signal REPLAY = Signal.valueOf(ReplayingDecoder.class.getName() + ".REPLAY"); - private final ReplayingDecoderBuffer replayable = new ReplayingDecoderBuffer(); + private final ReplayingDecoderByteBuf replayable = new ReplayingDecoderByteBuf(); private S state; private int checkpoint = -1; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoderBuffer.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoderByteBuf.java similarity index 98% rename from db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoderBuffer.java rename to db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoderByteBuf.java index 571a593e..d8c98322 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoderBuffer.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/ReplayingDecoderByteBuf.java @@ -34,7 +34,7 @@ /** * Special {@link ByteBuf} implementation which is used by the {@link ReplayingDecoder} */ -final class ReplayingDecoderBuffer extends ByteBuf { +final class ReplayingDecoderByteBuf extends ByteBuf { private static final Signal REPLAY = ReplayingDecoder.REPLAY; @@ -42,15 +42,15 @@ final class ReplayingDecoderBuffer extends ByteBuf { private boolean terminated; private SwappedByteBuf swapped; - static final ReplayingDecoderBuffer EMPTY_BUFFER = new ReplayingDecoderBuffer(Unpooled.EMPTY_BUFFER); + static final ReplayingDecoderByteBuf EMPTY_BUFFER = new ReplayingDecoderByteBuf(Unpooled.EMPTY_BUFFER); static { EMPTY_BUFFER.terminate(); } - ReplayingDecoderBuffer() { } + ReplayingDecoderByteBuf() { } - ReplayingDecoderBuffer(ByteBuf buffer) { + ReplayingDecoderByteBuf(ByteBuf buffer) { setCumulation(buffer); } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/base64/package-info.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/base64/package-info.java index 9b92f8ed..1f4bda81 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/base64/package-info.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/base64/package-info.java @@ -17,7 +17,7 @@ /** * Encoder and decoder which transform a * Base64-encoded - * {@link java.lang.String} or {@link com.github.mauricio.netty.buffer.ByteBuf} - * into a decoded {@link com.github.mauricio.netty.buffer.ByteBuf} and vice versa. + * {@link java.lang.String} or {@link io.netty.buffer.ByteBuf} + * into a decoded {@link io.netty.buffer.ByteBuf} and vice versa. */ package com.github.mauricio.netty.handler.codec.base64; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/bytes/package-info.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/bytes/package-info.java index d7b956b8..1c132cd5 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/bytes/package-info.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/bytes/package-info.java @@ -16,6 +16,6 @@ /** * Encoder and decoder which transform an array of bytes into a - * {@link com.github.mauricio.netty.buffer.ByteBuf} and vice versa. + * {@link io.netty.buffer.ByteBuf} and vice versa. */ package com.github.mauricio.netty.handler.codec.bytes; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/JZlibDecoder.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/JZlibDecoder.java index 18a01186..73b69fb6 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/JZlibDecoder.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/JZlibDecoder.java @@ -90,13 +90,13 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t return; } - if (!in.isReadable()) { + final int inputLength = in.readableBytes(); + if (inputLength == 0) { return; } try { // Configure input. - int inputLength = in.readableBytes(); z.avail_in = inputLength; if (in.hasArray()) { z.next_in = in.array(); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/JdkZlibDecoder.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/JdkZlibDecoder.java index a156c92e..63c94ad5 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/JdkZlibDecoder.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/JdkZlibDecoder.java @@ -125,17 +125,18 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t return; } - if (!in.isReadable()) { + int readableBytes = in.readableBytes(); + if (readableBytes == 0) { return; } if (decideZlibOrNone) { // First two bytes are needed to decide if it's a ZLIB stream. - if (in.readableBytes() < 2) { + if (readableBytes < 2) { return; } - boolean nowrap = !looksLikeZlib(in.getShort(0)); + boolean nowrap = !looksLikeZlib(in.getShort(in.readerIndex())); inflater = new Inflater(nowrap); decideZlibOrNone = false; } @@ -154,13 +155,14 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t } } } + // Some bytes may have been consumed, and so we must re-set the number of readable bytes. + readableBytes = in.readableBytes(); } - int readableBytes = in.readableBytes(); if (in.hasArray()) { - inflater.setInput(in.array(), in.arrayOffset() + in.readerIndex(), in.readableBytes()); + inflater.setInput(in.array(), in.arrayOffset() + in.readerIndex(), readableBytes); } else { - byte[] array = new byte[in.readableBytes()]; + byte[] array = new byte[readableBytes]; in.getBytes(in.readerIndex(), array); inflater.setInput(array); } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/ZlibCodecFactory.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/ZlibCodecFactory.java index 23f5a1f5..b257c34d 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/ZlibCodecFactory.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/ZlibCodecFactory.java @@ -33,11 +33,11 @@ public final class ZlibCodecFactory { private static final boolean noJdkZlibEncoder; static { - noJdkZlibDecoder = SystemPropertyUtil.getBoolean("com.github.mauricio.netty.noJdkZlibDecoder", true); - logger.debug("-Dcom.github.mauricio.netty.noJdkZlibDecoder: {}", noJdkZlibDecoder); + noJdkZlibDecoder = SystemPropertyUtil.getBoolean("io.netty.noJdkZlibDecoder", true); + logger.debug("-Dio.netty.noJdkZlibDecoder: {}", noJdkZlibDecoder); - noJdkZlibEncoder = SystemPropertyUtil.getBoolean("com.github.mauricio.netty.noJdkZlibEncoder", false); - logger.debug("-Dcom.github.mauricio.netty.noJdkZlibEncoder: {}", noJdkZlibEncoder); + noJdkZlibEncoder = SystemPropertyUtil.getBoolean("io.netty.noJdkZlibEncoder", false); + logger.debug("-Dio.netty.noJdkZlibEncoder: {}", noJdkZlibEncoder); } public static ZlibEncoder newZlibEncoder(int compressionLevel) { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/package-info.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/package-info.java index 3e0a4998..2049956d 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/package-info.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/compression/package-info.java @@ -15,7 +15,7 @@ */ /** - * Encoder and decoder which compresses and decompresses {@link com.github.mauricio.netty.buffer.ByteBuf}s + * Encoder and decoder which compresses and decompresses {@link io.netty.buffer.ByteBuf}s * in a compression format such as zlib, * gzip, and * Snappy. diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufDecoder.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufDecoder.java new file mode 100644 index 00000000..69e4b179 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufDecoder.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.handler.codec.protobuf; + +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.ExtensionRegistryLite; +import com.google.protobuf.Message; +import com.google.protobuf.MessageLite; +import com.github.mauricio.netty.buffer.ByteBuf; +import com.github.mauricio.netty.channel.ChannelHandler.Sharable; +import com.github.mauricio.netty.channel.ChannelHandlerContext; +import com.github.mauricio.netty.channel.ChannelPipeline; +import com.github.mauricio.netty.handler.codec.ByteToMessageDecoder; +import com.github.mauricio.netty.handler.codec.LengthFieldBasedFrameDecoder; +import com.github.mauricio.netty.handler.codec.LengthFieldPrepender; +import com.github.mauricio.netty.handler.codec.MessageToMessageDecoder; + +import java.util.List; + +/** + * Decodes a received {@link ByteBuf} into a + * Google Protocol Buffers + * {@link Message} and {@link MessageLite}. Please note that this decoder must + * be used with a proper {@link ByteToMessageDecoder} such as {@link ProtobufVarint32FrameDecoder} + * or {@link LengthFieldBasedFrameDecoder} if you are using a stream-based + * transport such as TCP/IP. A typical setup for TCP/IP would be: + *
    + * {@link ChannelPipeline} pipeline = ...;
    + *
    + * // Decoders
    + * pipeline.addLast("frameDecoder",
    + *                  new {@link LengthFieldBasedFrameDecoder}(1048576, 0, 4, 0, 4));
    + * pipeline.addLast("protobufDecoder",
    + *                  new {@link ProtobufDecoder}(MyMessage.getDefaultInstance()));
    + *
    + * // Encoder
    + * pipeline.addLast("frameEncoder", new {@link LengthFieldPrepender}(4));
    + * pipeline.addLast("protobufEncoder", new {@link ProtobufEncoder}());
    + * 
    + * and then you can use a {@code MyMessage} instead of a {@link ByteBuf} + * as a message: + *
    + * void channelRead({@link ChannelHandlerContext} ctx, MyMessage req) {
    + *     MyMessage res = MyMessage.newBuilder().setText(
    + *                               "Did you say '" + req.getText() + "'?").build();
    + *     ch.write(res);
    + * }
    + * 
    + */ +@Sharable +public class ProtobufDecoder extends MessageToMessageDecoder { + + private static final boolean HAS_PARSER; + + static { + boolean hasParser = false; + try { + // MessageLite.getParsetForType() is not available until protobuf 2.5.0. + MessageLite.class.getDeclaredMethod("getParserForType"); + hasParser = true; + } catch (Throwable t) { + // Ignore + } + + HAS_PARSER = hasParser; + } + + private final MessageLite prototype; + private final ExtensionRegistryLite extensionRegistry; + + /** + * Creates a new instance. + */ + public ProtobufDecoder(MessageLite prototype) { + this(prototype, null); + } + + public ProtobufDecoder(MessageLite prototype, ExtensionRegistry extensionRegistry) { + this(prototype, (ExtensionRegistryLite) extensionRegistry); + } + + public ProtobufDecoder(MessageLite prototype, ExtensionRegistryLite extensionRegistry) { + if (prototype == null) { + throw new NullPointerException("prototype"); + } + this.prototype = prototype.getDefaultInstanceForType(); + this.extensionRegistry = extensionRegistry; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + final byte[] array; + final int offset; + final int length = msg.readableBytes(); + if (msg.hasArray()) { + array = msg.array(); + offset = msg.arrayOffset() + msg.readerIndex(); + } else { + array = new byte[length]; + msg.getBytes(msg.readerIndex(), array, 0, length); + offset = 0; + } + + if (extensionRegistry == null) { + if (HAS_PARSER) { + out.add(prototype.getParserForType().parseFrom(array, offset, length)); + } else { + out.add(prototype.newBuilderForType().mergeFrom(array, offset, length).build()); + } + } else { + if (HAS_PARSER) { + out.add(prototype.getParserForType().parseFrom(array, offset, length, extensionRegistry)); + } else { + out.add(prototype.newBuilderForType().mergeFrom(array, offset, length, extensionRegistry).build()); + } + } + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufEncoder.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufEncoder.java new file mode 100644 index 00000000..08bd25f2 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufEncoder.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.handler.codec.protobuf; + +import com.google.protobuf.Message; +import com.google.protobuf.MessageLite; +import com.google.protobuf.MessageLiteOrBuilder; +import com.github.mauricio.netty.buffer.ByteBuf; +import com.github.mauricio.netty.channel.ChannelHandler.Sharable; +import com.github.mauricio.netty.channel.ChannelHandlerContext; +import com.github.mauricio.netty.channel.ChannelPipeline; +import com.github.mauricio.netty.handler.codec.LengthFieldBasedFrameDecoder; +import com.github.mauricio.netty.handler.codec.LengthFieldPrepender; +import com.github.mauricio.netty.handler.codec.MessageToMessageEncoder; + +import java.util.List; + +import static com.github.mauricio.netty.buffer.Unpooled.*; + +/** + * Encodes the requested Google + * Protocol Buffers {@link Message} and {@link MessageLite} into a + * {@link ByteBuf}. A typical setup for TCP/IP would be: + *
    + * {@link ChannelPipeline} pipeline = ...;
    + *
    + * // Decoders
    + * pipeline.addLast("frameDecoder",
    + *                  new {@link LengthFieldBasedFrameDecoder}(1048576, 0, 4, 0, 4));
    + * pipeline.addLast("protobufDecoder",
    + *                  new {@link ProtobufDecoder}(MyMessage.getDefaultInstance()));
    + *
    + * // Encoder
    + * pipeline.addLast("frameEncoder", new {@link LengthFieldPrepender}(4));
    + * pipeline.addLast("protobufEncoder", new {@link ProtobufEncoder}());
    + * 
    + * and then you can use a {@code MyMessage} instead of a {@link ByteBuf} + * as a message: + *
    + * void channelRead({@link ChannelHandlerContext} ctx, MyMessage req) {
    + *     MyMessage res = MyMessage.newBuilder().setText(
    + *                               "Did you say '" + req.getText() + "'?").build();
    + *     ch.write(res);
    + * }
    + * 
    + */ +@Sharable +public class ProtobufEncoder extends MessageToMessageEncoder { + @Override + protected void encode( + ChannelHandlerContext ctx, MessageLiteOrBuilder msg, List out) throws Exception { + if (msg instanceof MessageLite) { + out.add(wrappedBuffer(((MessageLite) msg).toByteArray())); + return; + } + if (msg instanceof MessageLite.Builder) { + out.add(wrappedBuffer(((MessageLite.Builder) msg).build().toByteArray())); + } + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufVarint32FrameDecoder.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufVarint32FrameDecoder.java new file mode 100644 index 00000000..3d3941c1 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufVarint32FrameDecoder.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.handler.codec.protobuf; + +import com.github.mauricio.netty.buffer.ByteBuf; +import com.github.mauricio.netty.channel.ChannelHandlerContext; +import com.github.mauricio.netty.handler.codec.ByteToMessageDecoder; +import com.github.mauricio.netty.handler.codec.CorruptedFrameException; + +import java.util.List; + +import com.google.protobuf.CodedInputStream; + +/** + * A decoder that splits the received {@link ByteBuf}s dynamically by the + * value of the Google Protocol Buffers + * Base + * 128 Varints integer length field in the message. For example: + *
    + * BEFORE DECODE (302 bytes)       AFTER DECODE (300 bytes)
    + * +--------+---------------+      +---------------+
    + * | Length | Protobuf Data |----->| Protobuf Data |
    + * | 0xAC02 |  (300 bytes)  |      |  (300 bytes)  |
    + * +--------+---------------+      +---------------+
    + * 
    + * + * @see CodedInputStream + */ +public class ProtobufVarint32FrameDecoder extends ByteToMessageDecoder { + + // TODO maxFrameLength + safe skip + fail-fast option + // (just like LengthFieldBasedFrameDecoder) + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + in.markReaderIndex(); + final byte[] buf = new byte[5]; + for (int i = 0; i < buf.length; i ++) { + if (!in.isReadable()) { + in.resetReaderIndex(); + return; + } + + buf[i] = in.readByte(); + if (buf[i] >= 0) { + int length = CodedInputStream.newInstance(buf, 0, i + 1).readRawVarint32(); + if (length < 0) { + throw new CorruptedFrameException("negative length: " + length); + } + + if (in.readableBytes() < length) { + in.resetReaderIndex(); + return; + } else { + out.add(in.readBytes(length)); + return; + } + } + } + + // Couldn't find the byte whose MSB is off. + throw new CorruptedFrameException("length wider than 32-bit"); + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufVarint32LengthFieldPrepender.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufVarint32LengthFieldPrepender.java new file mode 100644 index 00000000..ca844d4b --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/ProtobufVarint32LengthFieldPrepender.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.handler.codec.protobuf; + +import com.google.protobuf.CodedOutputStream; +import com.github.mauricio.netty.buffer.ByteBuf; +import com.github.mauricio.netty.buffer.ByteBufOutputStream; +import com.github.mauricio.netty.channel.ChannelHandler.Sharable; +import com.github.mauricio.netty.channel.ChannelHandlerContext; +import com.github.mauricio.netty.handler.codec.MessageToByteEncoder; + +/** + * An encoder that prepends the the Google Protocol Buffers + * Base + * 128 Varints integer length field. For example: + *
    + * BEFORE DECODE (300 bytes)       AFTER DECODE (302 bytes)
    + * +---------------+               +--------+---------------+
    + * | Protobuf Data |-------------->| Length | Protobuf Data |
    + * |  (300 bytes)  |               | 0xAC02 |  (300 bytes)  |
    + * +---------------+               +--------+---------------+
    + * 
    * + * + * @see CodedOutputStream + */ +@Sharable +public class ProtobufVarint32LengthFieldPrepender extends MessageToByteEncoder { + + @Override + protected void encode( + ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { + int bodyLen = msg.readableBytes(); + int headerLen = CodedOutputStream.computeRawVarint32Size(bodyLen); + out.ensureWritable(headerLen + bodyLen); + + CodedOutputStream headerOut = + CodedOutputStream.newInstance(new ByteBufOutputStream(out), headerLen); + headerOut.writeRawVarint32(bodyLen); + headerOut.flush(); + + out.writeBytes(msg, msg.readerIndex(), bodyLen); + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/package-info.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/package-info.java new file mode 100644 index 00000000..cd12b0dc --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/protobuf/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you 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. + */ + +/** + * Encoder and decoder which transform a + * Google Protocol Buffers + * {@link com.google.protobuf.Message} into a {@link io.netty.buffer.ByteBuf} + * and vice versa. + */ +package com.github.mauricio.netty.handler.codec.protobuf; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/string/package-info.java b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/string/package-info.java index 096c1416..6106d5b1 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/string/package-info.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/handler/codec/string/package-info.java @@ -16,6 +16,6 @@ /** * Encoder and decoder which transform a {@link java.lang.String} into a - * {@link com.github.mauricio.netty.buffer.ByteBuf} and vice versa. + * {@link io.netty.buffer.ByteBuf} and vice versa. */ package com.github.mauricio.netty.handler.codec.string; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/Attribute.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/Attribute.java index 839110a5..b746bced 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/Attribute.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/Attribute.java @@ -43,14 +43,17 @@ public interface Attribute { T getAndSet(T value); /** - * Atomically sets to the given value if this {@link Attribute} does not contain a value at the moment. + * Atomically sets to the given value if this {@link Attribute}'s value is {@code null}. * If it was not possible to set the value as it contains a value it will just return the current value. */ T setIfAbsent(T value); /** - * Removes this attribute from the {@link AttributeMap} and returns the old value.. Subsequent {@link #get()} - * calls will return @{code null}. + * Removes this attribute from the {@link AttributeMap} and returns the old value. Subsequent {@link #get()} + * calls will return {@code null}. + * + * If you only want to return the old value and clear the {@link Attribute} while still keep it in + * {@link AttributeMap} use {@link #getAndSet(Object)} with a value of {@code null}. */ T getAndRemove(); @@ -61,7 +64,10 @@ public interface Attribute { boolean compareAndSet(T oldValue, T newValue); /** - * Removes this attribute from the {@link AttributeMap}. Subsequent {@link #get()} calls will return @{code null}. + * Removes this attribute from the {@link AttributeMap}. Subsequent {@link #get()} calls will return @{code null}. + * + * If you only want to remove the value and clear the {@link Attribute} while still keep it in + * {@link AttributeMap} use {@link #set(Object)} with a value of {@code null}. */ void remove(); } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/AttributeKey.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/AttributeKey.java index e8048965..a740d869 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/AttributeKey.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/AttributeKey.java @@ -19,6 +19,8 @@ import java.util.concurrent.ConcurrentMap; +import static com.github.mauricio.netty.util.internal.ObjectUtil.checkNotNull; + /** * Key which can be used to access {@link Attribute} out of the {@link AttributeMap}. Be aware that it is not be * possible to have multiple keys with the same name. @@ -29,14 +31,48 @@ @SuppressWarnings({ "UnusedDeclaration", "deprecation" }) // 'T' is used only at compile time public final class AttributeKey extends UniqueName { - private static final ConcurrentMap names = PlatformDependent.newConcurrentHashMap(); + @SuppressWarnings("rawtypes") + private static final ConcurrentMap names = PlatformDependent.newConcurrentHashMap(); /** - * Creates a new {@link AttributeKey} with the specified {@code name}. + * Creates a new {@link AttributeKey} with the specified {@param name} or return the already existing + * {@link AttributeKey} for the given name. */ - @SuppressWarnings("deprecation") + @SuppressWarnings("unchecked") public static AttributeKey valueOf(String name) { - return new AttributeKey(name); + checkNotNull(name, "name"); + AttributeKey option = names.get(name); + if (option == null) { + option = new AttributeKey(name); + AttributeKey old = names.putIfAbsent(name, option); + if (old != null) { + option = old; + } + } + return option; + } + + /** + * Returns {@code true} if a {@link AttributeKey} exists for the given {@code name}. + */ + public static boolean exists(String name) { + checkNotNull(name, "name"); + return names.containsKey(name); + } + + /** + * Creates a new {@link AttributeKey} for the given {@param name} or fail with an + * {@link IllegalArgumentException} if a {@link AttributeKey} for the given {@param name} exists. + */ + @SuppressWarnings("unchecked") + public static AttributeKey newInstance(String name) { + checkNotNull(name, "name"); + AttributeKey option = new AttributeKey(name); + AttributeKey old = names.putIfAbsent(name, option); + if (old != null) { + throw new IllegalArgumentException(String.format("'%s' is already in use", name)); + } + return option; } /** @@ -44,6 +80,6 @@ public static AttributeKey valueOf(String name) { */ @Deprecated public AttributeKey(String name) { - super(names, name); + super(name); } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/DefaultAttributeMap.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/DefaultAttributeMap.java index 06b09742..e3bb377b 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/DefaultAttributeMap.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/DefaultAttributeMap.java @@ -172,6 +172,11 @@ private void remove0() { if (next != null) { next.prev = prev; } + + // Null out prev and next - this will guard against multiple remove0() calls which may corrupt + // the linked list for the bucket. + prev = null; + next = null; } } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/NetUtil.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/NetUtil.java index 1beea0c9..0d7d7bc1 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/NetUtil.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/NetUtil.java @@ -28,6 +28,8 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; @@ -114,6 +116,11 @@ public final class NetUtil { */ private static final int IPV4_SEPARATORS = 3; + /** + * {@code true} if ipv4 should be used on a system that supports ipv4 and ipv6. + */ + private static final boolean IPV4_PREFERRED = Boolean.getBoolean("java.net.preferIPv4Stack"); + /** * The logger being used by this class */ @@ -225,38 +232,52 @@ public final class NetUtil { LOOPBACK_IF = loopbackIface; LOCALHOST = loopbackAddr; - // Determine the default somaxconn (server socket backlog) value of the platform. - // The known defaults: - // - Windows NT Server 4.0+: 200 - // - Linux and Mac OS X: 128 - int somaxconn = PlatformDependent.isWindows() ? 200 : 128; - File file = new File("/proc/sys/net/core/somaxconn"); - if (file.exists()) { - BufferedReader in = null; - try { - in = new BufferedReader(new FileReader(file)); - somaxconn = Integer.parseInt(in.readLine()); - if (logger.isDebugEnabled()) { - logger.debug("{}: {}", file, somaxconn); - } - } catch (Exception e) { - logger.debug("Failed to get SOMAXCONN from: {}", file, e); - } finally { - if (in != null) { + // As a SecurityManager may prevent reading the somaxconn file we wrap this in a privileged block. + // + // See https://github.com/netty/netty/issues/3680 + SOMAXCONN = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Integer run() { + // Determine the default somaxconn (server socket backlog) value of the platform. + // The known defaults: + // - Windows NT Server 4.0+: 200 + // - Linux and Mac OS X: 128 + int somaxconn = PlatformDependent.isWindows() ? 200 : 128; + File file = new File("/proc/sys/net/core/somaxconn"); + if (file.exists()) { + BufferedReader in = null; try { - in.close(); + in = new BufferedReader(new FileReader(file)); + somaxconn = Integer.parseInt(in.readLine()); + if (logger.isDebugEnabled()) { + logger.debug("{}: {}", file, somaxconn); + } } catch (Exception e) { - // Ignored. + logger.debug("Failed to get SOMAXCONN from: {}", file, e); + } finally { + if (in != null) { + try { + in.close(); + } catch (Exception e) { + // Ignored. + } + } + } + } else { + if (logger.isDebugEnabled()) { + logger.debug("{}: {} (non-existent)", file, somaxconn); } } + return somaxconn; } - } else { - if (logger.isDebugEnabled()) { - logger.debug("{}: {} (non-existent)", file, somaxconn); - } - } + }); + } - SOMAXCONN = somaxconn; + /** + * Returns {@code true} if ipv4 should be prefered on a system that supports ipv4 and ipv6. + */ + public static boolean isIpV4StackPreferred() { + return IPV4_PREFERRED; } /** @@ -554,6 +575,10 @@ public static boolean isValidIpV6Address(String ipAddress) { return true; } + /** + * @deprecated Do not use; published by mistake. + */ + @Deprecated public static boolean isValidIp4Word(String word) { char c; if (word.length() < 1 || word.length() > 3) { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/Recycler.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/Recycler.java index 56404e71..7354f2d5 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/Recycler.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/Recycler.java @@ -43,9 +43,9 @@ public abstract class Recycler { static { // In the future, we might have different maxCapacity for different object types. - // e.g. com.github.mauricio.netty.recycler.maxCapacity.writeTask - // com.github.mauricio.netty.recycler.maxCapacity.outboundBuffer - int maxCapacity = SystemPropertyUtil.getInt("com.github.mauricio.netty.recycler.maxCapacity.default", 0); + // e.g. io.netty.recycler.maxCapacity.writeTask + // io.netty.recycler.maxCapacity.outboundBuffer + int maxCapacity = SystemPropertyUtil.getInt("io.netty.recycler.maxCapacity.default", 0); if (maxCapacity <= 0) { // TODO: Some arbitrary large number - should adjust as we get more production experience. maxCapacity = 262144; @@ -53,7 +53,7 @@ public abstract class Recycler { DEFAULT_MAX_CAPACITY = maxCapacity; if (logger.isDebugEnabled()) { - logger.debug("-Dcom.github.mauricio.netty.recycler.maxCapacity.default: {}", DEFAULT_MAX_CAPACITY); + logger.debug("-Dio.netty.recycler.maxCapacity.default: {}", DEFAULT_MAX_CAPACITY); } INITIAL_CAPACITY = Math.min(DEFAULT_MAX_CAPACITY, 256); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/ResourceLeakDetector.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/ResourceLeakDetector.java index b96c1f8b..c510d64b 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/ResourceLeakDetector.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/ResourceLeakDetector.java @@ -33,7 +33,7 @@ public final class ResourceLeakDetector { - private static final String PROP_LEVEL = "com.github.mauricio.netty.leakDetectionLevel"; + private static final String PROP_LEVEL = "io.netty.leakDetectionLevel"; private static final Level DEFAULT_LEVEL = Level.SIMPLE; /** @@ -67,11 +67,11 @@ public enum Level { static { final boolean disabled; - if (SystemPropertyUtil.get("com.github.mauricio.netty.noResourceLeakDetection") != null) { - disabled = SystemPropertyUtil.getBoolean("com.github.mauricio.netty.noResourceLeakDetection", false); - logger.debug("-Dcom.github.mauricio.netty.noResourceLeakDetection: {}", disabled); + if (SystemPropertyUtil.get("io.netty.noResourceLeakDetection") != null) { + disabled = SystemPropertyUtil.getBoolean("io.netty.noResourceLeakDetection", false); + logger.debug("-Dio.netty.noResourceLeakDetection: {}", disabled); logger.warn( - "-Dcom.github.mauricio.netty.noResourceLeakDetection is deprecated. Use '-D{}={}' instead.", + "-Dio.netty.noResourceLeakDetection is deprecated. Use '-D{}={}' instead.", PROP_LEVEL, DEFAULT_LEVEL.name().toLowerCase()); } else { disabled = false; @@ -238,11 +238,13 @@ private void reportLeak(Level level) { logger.error("LEAK: {}.release() was not called before it's garbage-collected. " + "Enable advanced leak reporting to find out where the leak occurred. " + "To enable advanced leak reporting, " + - "specify the JVM option '-D{}={}' or call {}.setLevel()", + "specify the JVM option '-D{}={}' or call {}.setLevel() " + + "See http://netty.io/wiki/reference-counted-objects.html for more information.", resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this)); } else { logger.error( - "LEAK: {}.release() was not called before it's garbage-collected.{}", + "LEAK: {}.release() was not called before it's garbage-collected. " + + "See http://netty.io/wiki/reference-counted-objects.html for more information.{}", resourceType, records); } } @@ -317,6 +319,7 @@ public boolean close() { return false; } + @Override public String toString() { if (creationRecord == null) { return ""; @@ -353,7 +356,7 @@ public String toString() { } private static final String[] STACK_TRACE_ELEMENT_EXCLUSIONS = { - "com.github.mauricio.netty.buffer.AbstractByteBufAllocator.toLeakAwareBuffer(", + "io.netty.buffer.AbstractByteBufAllocator.toLeakAwareBuffer(", }; static String newRecord(int recordsToSkip) { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/UniqueName.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/UniqueName.java index 94833b10..42e053da 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/UniqueName.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/UniqueName.java @@ -18,6 +18,8 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; +import static com.github.mauricio.netty.util.internal.ObjectUtil.checkNotNull; + /** * @deprecated Known to have problems with class loaders. * @@ -39,12 +41,8 @@ public class UniqueName implements Comparable { * @param args the arguments to process */ public UniqueName(ConcurrentMap map, String name, Object... args) { - if (map == null) { - throw new NullPointerException("map"); - } - if (name == null) { - throw new NullPointerException("name"); - } + checkNotNull(map, "map"); + if (args != null && args.length > 0) { validateArgs(args); } @@ -52,9 +50,13 @@ public UniqueName(ConcurrentMap map, String name, Object... arg if (map.putIfAbsent(name, Boolean.TRUE) != null) { throw new IllegalArgumentException(String.format("'%s' is already in use", name)); } + this.name = checkNotNull(name, "name"); + id = nextId.incrementAndGet(); + } + protected UniqueName(String name) { + this.name = checkNotNull(name, "name"); id = nextId.incrementAndGet(); - this.name = name; } /** diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/Version.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/Version.java index 3679a5a6..53804c3f 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/Version.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/Version.java @@ -32,7 +32,7 @@ /** * Retrieves the version information of available Netty artifacts. *

    - * This class retrieves the version information from {@code META-INF/com.github.mauricio.netty.versions.properties}, which is + * This class retrieves the version information from {@code META-INF/io.netty.versions.properties}, which is * generated in build time. Note that it may not be possible to retrieve the information completely, depending on * your environment, such as the specified {@link ClassLoader}, the current {@link SecurityManager}. *

    @@ -69,7 +69,7 @@ public static Map identify(ClassLoader classLoader) { // Collect all properties. Properties props = new Properties(); try { - Enumeration resources = classLoader.getResources("META-INF/com.github.mauricio.netty.versions.properties"); + Enumeration resources = classLoader.getResources("META-INF/io.netty.versions.properties"); while (resources.hasMoreElements()) { URL url = resources.nextElement(); InputStream in = url.openStream(); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/AbstractScheduledEventExecutor.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/AbstractScheduledEventExecutor.java new file mode 100644 index 00000000..ff7ea3a1 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/AbstractScheduledEventExecutor.java @@ -0,0 +1,215 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.util.concurrent; + +import com.github.mauricio.netty.util.internal.ObjectUtil; + +import java.util.Iterator; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * Abstract base class for {@link EventExecutor}s that want to support scheduling. + */ +public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor { + + Queue> scheduledTaskQueue; + + protected static long nanoTime() { + return ScheduledFutureTask.nanoTime(); + } + + Queue> scheduledTaskQueue() { + if (scheduledTaskQueue == null) { + scheduledTaskQueue = new PriorityQueue>(); + } + return scheduledTaskQueue; + } + + private static boolean isNullOrEmpty(Queue> queue) { + return queue == null || queue.isEmpty(); + } + + /** + * Cancel all scheduled tasks. + * + * This method MUST be called only when {@link #inEventLoop()} is {@code true}. + */ + protected void cancelScheduledTasks() { + assert inEventLoop(); + Queue> scheduledTaskQueue = this.scheduledTaskQueue; + if (isNullOrEmpty(scheduledTaskQueue)) { + return; + } + + final ScheduledFutureTask[] scheduledTasks = + scheduledTaskQueue.toArray(new ScheduledFutureTask[scheduledTaskQueue.size()]); + + for (ScheduledFutureTask task: scheduledTasks) { + task.cancel(false); + } + + scheduledTaskQueue.clear(); + } + + /** + * @see {@link #pollScheduledTask(long)} + */ + protected final Runnable pollScheduledTask() { + return pollScheduledTask(nanoTime()); + } + + /** + * Return the {@link Runnable} which is ready to be executed with the given {@code nanoTime}. + * You should use {@link #nanoTime()} to retrieve the the correct {@code nanoTime}. + */ + protected final Runnable pollScheduledTask(long nanoTime) { + assert inEventLoop(); + + Queue> scheduledTaskQueue = this.scheduledTaskQueue; + ScheduledFutureTask scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek(); + if (scheduledTask == null) { + return null; + } + + if (scheduledTask.deadlineNanos() <= nanoTime) { + scheduledTaskQueue.remove(); + return scheduledTask; + } + return null; + } + + /** + * Return the nanoseconds when the next scheduled task is ready to be run or {@code -1} if no task is scheduled. + */ + protected final long nextScheduledTaskNano() { + Queue> scheduledTaskQueue = this.scheduledTaskQueue; + ScheduledFutureTask scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek(); + if (scheduledTask == null) { + return -1; + } + return Math.max(0, scheduledTask.deadlineNanos() - nanoTime()); + } + + final ScheduledFutureTask peekScheduledTask() { + Queue> scheduledTaskQueue = this.scheduledTaskQueue; + if (scheduledTaskQueue == null) { + return null; + } + return scheduledTaskQueue.peek(); + } + + /** + * Returns {@code true} if a scheduled task is ready for processing. + */ + protected final boolean hasScheduledTasks() { + Queue> scheduledTaskQueue = this.scheduledTaskQueue; + ScheduledFutureTask scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek(); + return scheduledTask != null && scheduledTask.deadlineNanos() <= nanoTime(); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + ObjectUtil.checkNotNull(command, "command"); + ObjectUtil.checkNotNull(unit, "unit"); + if (delay < 0) { + throw new IllegalArgumentException( + String.format("delay: %d (expected: >= 0)", delay)); + } + return schedule(new ScheduledFutureTask( + this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay)))); + } + + @Override + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + ObjectUtil.checkNotNull(callable, "callable"); + ObjectUtil.checkNotNull(unit, "unit"); + if (delay < 0) { + throw new IllegalArgumentException( + String.format("delay: %d (expected: >= 0)", delay)); + } + return schedule(new ScheduledFutureTask( + this, callable, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay)))); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + ObjectUtil.checkNotNull(command, "command"); + ObjectUtil.checkNotNull(unit, "unit"); + if (initialDelay < 0) { + throw new IllegalArgumentException( + String.format("initialDelay: %d (expected: >= 0)", initialDelay)); + } + if (period <= 0) { + throw new IllegalArgumentException( + String.format("period: %d (expected: > 0)", period)); + } + + return schedule(new ScheduledFutureTask( + this, Executors.callable(command, null), + ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), unit.toNanos(period))); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { + ObjectUtil.checkNotNull(command, "command"); + ObjectUtil.checkNotNull(unit, "unit"); + if (initialDelay < 0) { + throw new IllegalArgumentException( + String.format("initialDelay: %d (expected: >= 0)", initialDelay)); + } + if (delay <= 0) { + throw new IllegalArgumentException( + String.format("delay: %d (expected: > 0)", delay)); + } + + return schedule(new ScheduledFutureTask( + this, Executors.callable(command, null), + ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), -unit.toNanos(delay))); + } + + ScheduledFuture schedule(final ScheduledFutureTask task) { + if (inEventLoop()) { + scheduledTaskQueue().add(task); + } else { + execute(new Runnable() { + @Override + public void run() { + scheduledTaskQueue().add(task); + } + }); + } + + return task; + } + + void purgeCancelledScheduledTasks() { + Queue> scheduledTaskQueue = this.scheduledTaskQueue; + if (isNullOrEmpty(scheduledTaskQueue)) { + return; + } + Iterator> i = scheduledTaskQueue.iterator(); + while (i.hasNext()) { + ScheduledFutureTask task = i.next(); + if (task.isCancelled()) { + i.remove(); + } + } + } +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/EventExecutor.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/EventExecutor.java index 690c9b58..55050c23 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/EventExecutor.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/EventExecutor.java @@ -18,8 +18,8 @@ /** * The {@link EventExecutor} is a special {@link EventExecutorGroup} which comes * with some handy methods to see if a {@link Thread} is executed in a event loop. - * Beside this it also extends the {@link EventExecutorGroup} to allow a generic way to - * access methods. + * Besides this, it also extends the {@link EventExecutorGroup} to allow for a generic + * way to access methods. * */ public interface EventExecutor extends EventExecutorGroup { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/EventExecutorGroup.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/EventExecutorGroup.java index bfe38d82..d3c39508 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/EventExecutorGroup.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/EventExecutorGroup.java @@ -22,9 +22,9 @@ import java.util.concurrent.TimeUnit; /** - * The {@link EventExecutorGroup} is responsible to provide {@link EventExecutor}'s to use via its - * {@link #next()} method. Beside this it also is responsible to handle their live-cycle and allows - * to shut them down in a global fashion. + * The {@link EventExecutorGroup} is responsible for providing the {@link EventExecutor}'s to use + * via its {@link #next()} method. Besides this, it is also responsible for handling their + * life-cycle and allows shutting them down in a global fashion. * */ public interface EventExecutorGroup extends ScheduledExecutorService, Iterable { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/GlobalEventExecutor.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/GlobalEventExecutor.java index 5eedb62d..c66388cb 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/GlobalEventExecutor.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/GlobalEventExecutor.java @@ -18,11 +18,8 @@ import com.github.mauricio.netty.util.internal.logging.InternalLogger; import com.github.mauricio.netty.util.internal.logging.InternalLoggerFactory; -import java.util.Iterator; -import java.util.PriorityQueue; import java.util.Queue; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; @@ -35,7 +32,7 @@ * task pending in the task queue for 1 second. Please note it is not scalable to schedule large number of tasks to * this executor; use a dedicated executor. */ -public final class GlobalEventExecutor extends AbstractEventExecutor { +public final class GlobalEventExecutor extends AbstractScheduledEventExecutor { private static final InternalLogger logger = InternalLoggerFactory.getInstance(GlobalEventExecutor.class); @@ -44,9 +41,8 @@ public final class GlobalEventExecutor extends AbstractEventExecutor { public static final GlobalEventExecutor INSTANCE = new GlobalEventExecutor(); final BlockingQueue taskQueue = new LinkedBlockingQueue(); - final Queue> delayedTaskQueue = new PriorityQueue>(); final ScheduledFutureTask purgeTask = new ScheduledFutureTask( - this, delayedTaskQueue, Executors.callable(new PurgeTask(), null), + this, Executors.callable(new PurgeTask(), null), ScheduledFutureTask.deadlineNanos(SCHEDULE_PURGE_INTERVAL), -SCHEDULE_PURGE_INTERVAL); private final ThreadFactory threadFactory = new DefaultThreadFactory(getClass()); @@ -57,7 +53,7 @@ public final class GlobalEventExecutor extends AbstractEventExecutor { private final Future terminationFuture = new FailedFuture(this, new UnsupportedOperationException()); private GlobalEventExecutor() { - delayedTaskQueue.add(purgeTask); + scheduledTaskQueue().add(purgeTask); } @Override @@ -73,8 +69,8 @@ public EventExecutorGroup parent() { Runnable takeTask() { BlockingQueue taskQueue = this.taskQueue; for (;;) { - ScheduledFutureTask delayedTask = delayedTaskQueue.peek(); - if (delayedTask == null) { + ScheduledFutureTask scheduledTask = peekScheduledTask(); + if (scheduledTask == null) { Runnable task = null; try { task = taskQueue.take(); @@ -83,7 +79,7 @@ Runnable takeTask() { } return task; } else { - long delayNanos = delayedTask.delayNanos(); + long delayNanos = scheduledTask.delayNanos(); Runnable task; if (delayNanos > 0) { try { @@ -96,7 +92,7 @@ Runnable takeTask() { } if (task == null) { - fetchFromDelayedQueue(); + fetchFromScheduledTaskQueue(); task = taskQueue.poll(); } @@ -107,23 +103,15 @@ Runnable takeTask() { } } - private void fetchFromDelayedQueue() { - long nanoTime = 0L; - for (;;) { - ScheduledFutureTask delayedTask = delayedTaskQueue.peek(); - if (delayedTask == null) { - break; - } - - if (nanoTime == 0L) { - nanoTime = ScheduledFutureTask.nanoTime(); - } - - if (delayedTask.deadlineNanos() <= nanoTime) { - delayedTaskQueue.remove(); - taskQueue.add(delayedTask); - } else { - break; + private void fetchFromScheduledTaskQueue() { + if (hasScheduledTasks()) { + long nanoTime = AbstractScheduledEventExecutor.nanoTime(); + for (;;) { + Runnable scheduledTask = pollScheduledTask(nanoTime); + if (scheduledTask == null) { + break; + } + taskQueue.add(scheduledTask); } } } @@ -223,103 +211,6 @@ public void execute(Runnable task) { } } - // ScheduledExecutorService implementation - - @Override - public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { - if (command == null) { - throw new NullPointerException("command"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } - if (delay < 0) { - throw new IllegalArgumentException( - String.format("delay: %d (expected: >= 0)", delay)); - } - return schedule(new ScheduledFutureTask( - this, delayedTaskQueue, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay)))); - } - - @Override - public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { - if (callable == null) { - throw new NullPointerException("callable"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } - if (delay < 0) { - throw new IllegalArgumentException( - String.format("delay: %d (expected: >= 0)", delay)); - } - return schedule(new ScheduledFutureTask( - this, delayedTaskQueue, callable, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay)))); - } - - @Override - public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { - if (command == null) { - throw new NullPointerException("command"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } - if (initialDelay < 0) { - throw new IllegalArgumentException( - String.format("initialDelay: %d (expected: >= 0)", initialDelay)); - } - if (period <= 0) { - throw new IllegalArgumentException( - String.format("period: %d (expected: > 0)", period)); - } - - return schedule(new ScheduledFutureTask( - this, delayedTaskQueue, Executors.callable(command, null), - ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), unit.toNanos(period))); - } - - @Override - public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { - if (command == null) { - throw new NullPointerException("command"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } - if (initialDelay < 0) { - throw new IllegalArgumentException( - String.format("initialDelay: %d (expected: >= 0)", initialDelay)); - } - if (delay <= 0) { - throw new IllegalArgumentException( - String.format("delay: %d (expected: > 0)", delay)); - } - - return schedule(new ScheduledFutureTask( - this, delayedTaskQueue, Executors.callable(command, null), - ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), -unit.toNanos(delay))); - } - - private ScheduledFuture schedule(final ScheduledFutureTask task) { - if (task == null) { - throw new NullPointerException("task"); - } - - if (inEventLoop()) { - delayedTaskQueue.add(task); - } else { - execute(new Runnable() { - @Override - public void run() { - delayedTaskQueue.add(task); - } - }); - } - - return task; - } - private void startThread() { if (started.compareAndSet(false, true)) { Thread t = threadFactory.newThread(taskRunner); @@ -345,8 +236,9 @@ public void run() { } } + Queue> scheduledTaskQueue = GlobalEventExecutor.this.scheduledTaskQueue; // Terminate if there is no task in the queue (except the purge task). - if (taskQueue.isEmpty() && delayedTaskQueue.size() == 1) { + if (taskQueue.isEmpty() && (scheduledTaskQueue == null || scheduledTaskQueue.size() == 1)) { // Mark the current thread as stopped. // The following CAS must always success and must be uncontended, // because only one thread should be running at the same time. @@ -354,7 +246,7 @@ public void run() { assert stopped; // Check if there are pending entries added by execute() or schedule*() while we do CAS above. - if (taskQueue.isEmpty() && delayedTaskQueue.size() == 1) { + if (taskQueue.isEmpty() && (scheduledTaskQueue == null || scheduledTaskQueue.size() == 1)) { // A) No new task was added and thus there's nothing to handle // -> safe to terminate because there's nothing left to do // B) A new thread started and handled all the new tasks. @@ -380,13 +272,7 @@ public void run() { private final class PurgeTask implements Runnable { @Override public void run() { - Iterator> i = delayedTaskQueue.iterator(); - while (i.hasNext()) { - ScheduledFutureTask task = i.next(); - if (task.isCancelled()) { - i.remove(); - } - } + purgeCancelledScheduledTasks(); } } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/PromiseNotifier.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/PromiseNotifier.java index 67515ba8..7ab04ad3 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/PromiseNotifier.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/PromiseNotifier.java @@ -15,15 +15,21 @@ */ package com.github.mauricio.netty.util.concurrent; +import com.github.mauricio.netty.util.internal.logging.InternalLogger; +import com.github.mauricio.netty.util.internal.logging.InternalLoggerFactory; + +import static com.github.mauricio.netty.util.internal.ObjectUtil.checkNotNull; + /** * {@link GenericFutureListener} implementation which takes other {@link Future}s * and notifies them on completion. * - * @param V the type of value returned by the future - * @param F the type of future + * @param the type of value returned by the future + * @param the type of future */ public class PromiseNotifier> implements GenericFutureListener { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(PromiseNotifier.class); private final Promise[] promises; /** @@ -33,9 +39,7 @@ public class PromiseNotifier> implements GenericFutureLis */ @SafeVarargs public PromiseNotifier(Promise... promises) { - if (promises == null) { - throw new NullPointerException("promises"); - } + checkNotNull(promises, "promises"); for (Promise promise: promises) { if (promise == null) { throw new IllegalArgumentException("promises contains null Promise"); @@ -49,15 +53,18 @@ public void operationComplete(F future) throws Exception { if (future.isSuccess()) { V result = future.get(); for (Promise p: promises) { - p.setSuccess(result); + if (!p.trySuccess(result)) { + logger.warn("Failed to mark a promise as success because it is done already: {}", p); + } } return; } Throwable cause = future.cause(); for (Promise p: promises) { - p.setFailure(cause); + if (!p.tryFailure(cause)) { + logger.warn("Failed to mark a promise as failure because it's done already: {}", p, cause); + } } } - } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/ScheduledFutureTask.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/ScheduledFutureTask.java index 4215a981..73b57642 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/ScheduledFutureTask.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/ScheduledFutureTask.java @@ -36,37 +36,34 @@ static long deadlineNanos(long delay) { } private final long id = nextTaskId.getAndIncrement(); - private final Queue> delayedTaskQueue; private long deadlineNanos; /* 0 - no repeat, >0 - repeat at fixed rate, <0 - repeat with fixed delay */ private final long periodNanos; ScheduledFutureTask( - EventExecutor executor, Queue> delayedTaskQueue, + AbstractScheduledEventExecutor executor, Runnable runnable, V result, long nanoTime) { - this(executor, delayedTaskQueue, toCallable(runnable, result), nanoTime); + this(executor, toCallable(runnable, result), nanoTime); } ScheduledFutureTask( - EventExecutor executor, Queue> delayedTaskQueue, + AbstractScheduledEventExecutor executor, Callable callable, long nanoTime, long period) { super(executor, callable); if (period == 0) { throw new IllegalArgumentException("period: 0 (expected: != 0)"); } - this.delayedTaskQueue = delayedTaskQueue; deadlineNanos = nanoTime; periodNanos = period; } ScheduledFutureTask( - EventExecutor executor, Queue> delayedTaskQueue, + AbstractScheduledEventExecutor executor, Callable callable, long nanoTime) { super(executor, callable); - this.delayedTaskQueue = delayedTaskQueue; deadlineNanos = nanoTime; periodNanos = 0; } @@ -135,7 +132,11 @@ public void run() { deadlineNanos = nanoTime() - p; } if (!isCancelled()) { - delayedTaskQueue.add(this); + // scheduledTaskQueue can never be null as we lazy init it before submit the task! + Queue> scheduledTaskQueue = + ((AbstractScheduledEventExecutor) executor()).scheduledTaskQueue; + assert scheduledTaskQueue != null; + scheduledTaskQueue.add(this); } } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/SingleThreadEventExecutor.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/SingleThreadEventExecutor.java index ae965264..39012aca 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/SingleThreadEventExecutor.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/concurrent/SingleThreadEventExecutor.java @@ -20,14 +20,11 @@ import com.github.mauricio.netty.util.internal.logging.InternalLoggerFactory; import java.util.ArrayList; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; -import java.util.PriorityQueue; import java.util.Queue; import java.util.Set; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; @@ -40,7 +37,7 @@ * Abstract base class for {@link EventExecutor}'s that execute all its submitted tasks in a single thread. * */ -public abstract class SingleThreadEventExecutor extends AbstractEventExecutor { +public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor { private static final InternalLogger logger = InternalLoggerFactory.getInstance(SingleThreadEventExecutor.class); @@ -71,8 +68,6 @@ public void run() { private final EventExecutorGroup parent; private final Queue taskQueue; - final Queue> delayedTaskQueue = new PriorityQueue>(); - private final Thread thread; private final Semaphore threadLock = new Semaphore(0); private final Set shutdownHooks = new LinkedHashSet(); @@ -215,8 +210,8 @@ protected Runnable takeTask() { BlockingQueue taskQueue = (BlockingQueue) this.taskQueue; for (;;) { - ScheduledFutureTask delayedTask = delayedTaskQueue.peek(); - if (delayedTask == null) { + ScheduledFutureTask scheduledTask = peekScheduledTask(); + if (scheduledTask == null) { Runnable task = null; try { task = taskQueue.take(); @@ -228,7 +223,7 @@ protected Runnable takeTask() { } return task; } else { - long delayNanos = delayedTask.delayNanos(); + long delayNanos = scheduledTask.delayNanos(); Runnable task = null; if (delayNanos > 0) { try { @@ -238,11 +233,11 @@ protected Runnable takeTask() { } } if (task == null) { - // We need to fetch the delayed tasks now as otherwise there may be a chance that - // delayed tasks are never executed if there is always one task in the taskQueue. + // We need to fetch the scheduled tasks now as otherwise there may be a chance that + // scheduled tasks are never executed if there is always one task in the taskQueue. // This is for example true for the read task of OIO Transport // See https://github.com/netty/netty/issues/1614 - fetchFromDelayedQueue(); + fetchFromScheduledTaskQueue(); task = taskQueue.poll(); } @@ -253,23 +248,15 @@ protected Runnable takeTask() { } } - private void fetchFromDelayedQueue() { - long nanoTime = 0L; - for (;;) { - ScheduledFutureTask delayedTask = delayedTaskQueue.peek(); - if (delayedTask == null) { - break; - } - - if (nanoTime == 0L) { - nanoTime = ScheduledFutureTask.nanoTime(); - } - - if (delayedTask.deadlineNanos() <= nanoTime) { - delayedTaskQueue.remove(); - taskQueue.add(delayedTask); - } else { - break; + private void fetchFromScheduledTaskQueue() { + if (hasScheduledTasks()) { + long nanoTime = AbstractScheduledEventExecutor.nanoTime(); + for (;;) { + Runnable scheduledTask = pollScheduledTask(nanoTime); + if (scheduledTask == null) { + break; + } + taskQueue.add(scheduledTask); } } } @@ -290,16 +277,6 @@ protected boolean hasTasks() { return !taskQueue.isEmpty(); } - /** - * Returns {@code true} if a scheduled task is ready for processing by {@link #runAllTasks()} or - * {@link #runAllTasks(long)}. - */ - protected boolean hasScheduledTasks() { - assert inEventLoop(); - ScheduledFutureTask delayedTask = delayedTaskQueue.peek(); - return delayedTask != null && delayedTask.deadlineNanos() <= ScheduledFutureTask.nanoTime(); - } - /** * Return the number of tasks that are pending for processing. * @@ -340,7 +317,7 @@ protected boolean removeTask(Runnable task) { * @return {@code true} if and only if at least one task was run */ protected boolean runAllTasks() { - fetchFromDelayedQueue(); + fetchFromScheduledTaskQueue(); Runnable task = pollTask(); if (task == null) { return false; @@ -366,7 +343,7 @@ protected boolean runAllTasks() { * the tasks in the task queue and returns if it ran longer than {@code timeoutNanos}. */ protected boolean runAllTasks(long timeoutNanos) { - fetchFromDelayedQueue(); + fetchFromScheduledTaskQueue(); Runnable task = pollTask(); if (task == null) { return false; @@ -408,12 +385,12 @@ protected boolean runAllTasks(long timeoutNanos) { * Returns the amount of time left until the scheduled task with the closest dead line is executed. */ protected long delayNanos(long currentTimeNanos) { - ScheduledFutureTask delayedTask = delayedTaskQueue.peek(); - if (delayedTask == null) { + ScheduledFutureTask scheduledTask = peekScheduledTask(); + if (scheduledTask == null) { return SCHEDULE_PURGE_INTERVAL; } - return delayedTask.delayNanos(currentTimeNanos); + return scheduledTask.delayNanos(currentTimeNanos); } /** @@ -641,7 +618,7 @@ protected boolean confirmShutdown() { throw new IllegalStateException("must be invoked from an event loop"); } - cancelDelayedTasks(); + cancelScheduledTasks(); if (gracefulShutdownStartTime == 0) { gracefulShutdownStartTime = ScheduledFutureTask.nanoTime(); @@ -682,21 +659,6 @@ protected boolean confirmShutdown() { return true; } - private void cancelDelayedTasks() { - if (delayedTaskQueue.isEmpty()) { - return; - } - - final ScheduledFutureTask[] delayedTasks = - delayedTaskQueue.toArray(new ScheduledFutureTask[delayedTaskQueue.size()]); - - for (ScheduledFutureTask task: delayedTasks) { - task.cancel(false); - } - - delayedTaskQueue.clear(); - } - @Override public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { if (unit == null) { @@ -749,106 +711,11 @@ protected static void reject() { private static final long SCHEDULE_PURGE_INTERVAL = TimeUnit.SECONDS.toNanos(1); - @Override - public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { - if (command == null) { - throw new NullPointerException("command"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } - if (delay < 0) { - throw new IllegalArgumentException( - String.format("delay: %d (expected: >= 0)", delay)); - } - return schedule(new ScheduledFutureTask( - this, delayedTaskQueue, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay)))); - } - - @Override - public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { - if (callable == null) { - throw new NullPointerException("callable"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } - if (delay < 0) { - throw new IllegalArgumentException( - String.format("delay: %d (expected: >= 0)", delay)); - } - return schedule(new ScheduledFutureTask( - this, delayedTaskQueue, callable, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay)))); - } - - @Override - public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { - if (command == null) { - throw new NullPointerException("command"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } - if (initialDelay < 0) { - throw new IllegalArgumentException( - String.format("initialDelay: %d (expected: >= 0)", initialDelay)); - } - if (period <= 0) { - throw new IllegalArgumentException( - String.format("period: %d (expected: > 0)", period)); - } - - return schedule(new ScheduledFutureTask( - this, delayedTaskQueue, Executors.callable(command, null), - ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), unit.toNanos(period))); - } - - @Override - public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { - if (command == null) { - throw new NullPointerException("command"); - } - if (unit == null) { - throw new NullPointerException("unit"); - } - if (initialDelay < 0) { - throw new IllegalArgumentException( - String.format("initialDelay: %d (expected: >= 0)", initialDelay)); - } - if (delay <= 0) { - throw new IllegalArgumentException( - String.format("delay: %d (expected: > 0)", delay)); - } - - return schedule(new ScheduledFutureTask( - this, delayedTaskQueue, Executors.callable(command, null), - ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), -unit.toNanos(delay))); - } - - private ScheduledFuture schedule(final ScheduledFutureTask task) { - if (task == null) { - throw new NullPointerException("task"); - } - - if (inEventLoop()) { - delayedTaskQueue.add(task); - } else { - execute(new Runnable() { - @Override - public void run() { - delayedTaskQueue.add(task); - } - }); - } - - return task; - } - private void startThread() { if (STATE_UPDATER.get(this) == ST_NOT_STARTED) { if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { - delayedTaskQueue.add(new ScheduledFutureTask( - this, delayedTaskQueue, Executors.callable(new PurgeTask(), null), + schedule(new ScheduledFutureTask( + this, Executors.callable(new PurgeTask(), null), ScheduledFutureTask.deadlineNanos(SCHEDULE_PURGE_INTERVAL), -SCHEDULE_PURGE_INTERVAL)); thread.start(); } @@ -858,13 +725,7 @@ private void startThread() { private final class PurgeTask implements Runnable { @Override public void run() { - Iterator> i = delayedTaskQueue.iterator(); - while (i.hasNext()) { - ScheduledFutureTask task = i.next(); - if (task.isCancelled()) { - i.remove(); - } - } + purgeCancelledScheduledTasks(); } } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/ConcurrentCircularArrayQueue.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/ConcurrentCircularArrayQueue.java new file mode 100644 index 00000000..52acd27f --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/ConcurrentCircularArrayQueue.java @@ -0,0 +1,207 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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. + */ +/* + * 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.github.mauricio.netty.util.internal; + + +import java.util.AbstractQueue; +import java.util.Iterator; + +/** + * Forked from JCTools. + * + * A concurrent access enabling class used by circular array based queues this class exposes an offset computation + * method along with differently memory fenced load/store methods into the underlying array. The class is pre-padded and + * the array is padded on either side to help with False sharing prvention. It is expected theat subclasses handle post + * padding. + *

    + * Offset calculation is separate from access to enable the reuse of a give compute offset. + *

    + * Load/Store methods using a buffer parameter are provided to allow the prevention of final field reload after a + * LoadLoad barrier. + *

    + * + * @param + */ +abstract class ConcurrentCircularArrayQueue extends ConcurrentCircularArrayQueueL0Pad { + protected static final int REF_BUFFER_PAD; + private static final long REF_ARRAY_BASE; + private static final int REF_ELEMENT_SHIFT; + static { + final int scale = PlatformDependent0.UNSAFE.arrayIndexScale(Object[].class); + if (4 == scale) { + REF_ELEMENT_SHIFT = 2; + } else if (8 == scale) { + REF_ELEMENT_SHIFT = 3; + } else { + throw new IllegalStateException("Unknown pointer size"); + } + // 2 cache lines pad + // TODO: replace 64 with the value we can detect + REF_BUFFER_PAD = (64 * 2) / scale; + // Including the buffer pad in the array base offset + REF_ARRAY_BASE = PlatformDependent0.UNSAFE.arrayBaseOffset(Object[].class) + (REF_BUFFER_PAD * scale); + } + protected final long mask; + // @Stable :( + protected final E[] buffer; + + @SuppressWarnings("unchecked") + public ConcurrentCircularArrayQueue(int capacity) { + int actualCapacity = roundToPowerOfTwo(capacity); + mask = actualCapacity - 1; + // pad data on either end with some empty slots. + buffer = (E[]) new Object[actualCapacity + REF_BUFFER_PAD * 2]; + } + + private static int roundToPowerOfTwo(final int value) { + return 1 << (32 - Integer.numberOfLeadingZeros(value - 1)); + } + /** + * @param index desirable element index + * @return the offset in bytes within the array for a given index. + */ + protected final long calcElementOffset(long index) { + return calcElementOffset(index, mask); + } + /** + * @param index desirable element index + * @param mask + * @return the offset in bytes within the array for a given index. + */ + protected static final long calcElementOffset(long index, long mask) { + return REF_ARRAY_BASE + ((index & mask) << REF_ELEMENT_SHIFT); + } + /** + * A plain store (no ordering/fences) of an element to a given offset + * + * @param offset computed via {@link ConcurrentCircularArrayQueue#calcElementOffset(long)} + * @param e a kitty + */ + protected final void spElement(long offset, E e) { + spElement(buffer, offset, e); + } + + /** + * A plain store (no ordering/fences) of an element to a given offset + * + * @param buffer this.buffer + * @param offset computed via {@link ConcurrentCircularArrayQueue#calcElementOffset(long)} + * @param e an orderly kitty + */ + protected static final void spElement(E[] buffer, long offset, E e) { + PlatformDependent0.UNSAFE.putObject(buffer, offset, e); + } + + /** + * An ordered store(store + StoreStore barrier) of an element to a given offset + * + * @param offset computed via {@link ConcurrentCircularArrayQueue#calcElementOffset(long)} + * @param e an orderly kitty + */ + protected final void soElement(long offset, E e) { + soElement(buffer, offset, e); + } + + /** + * An ordered store(store + StoreStore barrier) of an element to a given offset + * + * @param buffer this.buffer + * @param offset computed via {@link ConcurrentCircularArrayQueue#calcElementOffset(long)} + * @param e an orderly kitty + */ + protected static final void soElement(E[] buffer, long offset, E e) { + PlatformDependent0.UNSAFE.putOrderedObject(buffer, offset, e); + } + + /** + * A plain load (no ordering/fences) of an element from a given offset. + * + * @param offset computed via {@link ConcurrentCircularArrayQueue#calcElementOffset(long)} + * @return the element at the offset + */ + protected final E lpElement(long offset) { + return lpElement(buffer, offset); + } + + /** + * A plain load (no ordering/fences) of an element from a given offset. + * + * @param buffer this.buffer + * @param offset computed via {@link ConcurrentCircularArrayQueue#calcElementOffset(long)} + * @return the element at the offset + */ + @SuppressWarnings("unchecked") + protected static final E lpElement(E[] buffer, long offset) { + return (E) PlatformDependent0.UNSAFE.getObject(buffer, offset); + } + + /** + * A volatile load (load + LoadLoad barrier) of an element from a given offset. + * + * @param offset computed via {@link ConcurrentCircularArrayQueue#calcElementOffset(long)} + * @return the element at the offset + */ + protected final E lvElement(long offset) { + return lvElement(buffer, offset); + } + + /** + * A volatile load (load + LoadLoad barrier) of an element from a given offset. + * + * @param buffer this.buffer + * @param offset computed via {@link ConcurrentCircularArrayQueue#calcElementOffset(long)} + * @return the element at the offset + */ + @SuppressWarnings("unchecked") + protected static final E lvElement(E[] buffer, long offset) { + return (E) PlatformDependent0.UNSAFE.getObjectVolatile(buffer, offset); + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + while (poll() != null || !isEmpty()) { + // looping + } + } + + public int capacity() { + return (int) (mask + 1); + } +} + +abstract class ConcurrentCircularArrayQueueL0Pad extends AbstractQueue { + long p00, p01, p02, p03, p04, p05, p06, p07; + long p30, p31, p32, p33, p34, p35, p36, p37; +} + diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/EmptyArrays.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/EmptyArrays.java index 8bab4336..af1b92e0 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/EmptyArrays.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/EmptyArrays.java @@ -17,6 +17,7 @@ package com.github.mauricio.netty.util.internal; import java.nio.ByteBuffer; +import java.security.cert.Certificate; import java.security.cert.X509Certificate; public final class EmptyArrays { @@ -34,7 +35,10 @@ public final class EmptyArrays { public static final String[] EMPTY_STRINGS = new String[0]; public static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0]; public static final ByteBuffer[] EMPTY_BYTE_BUFFERS = new ByteBuffer[0]; + public static final Certificate[] EMPTY_CERTIFICATES = new Certificate[0]; public static final X509Certificate[] EMPTY_X509_CERTIFICATES = new X509Certificate[0]; + public static final javax.security.cert.X509Certificate[] EMPTY_JAVAX_X509_CERTIFICATES = + new javax.security.cert.X509Certificate[0]; private EmptyArrays() { } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/JavassistTypeParameterMatcherGenerator.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/JavassistTypeParameterMatcherGenerator.java index 6db67f34..32bd4e4b 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/JavassistTypeParameterMatcherGenerator.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/JavassistTypeParameterMatcherGenerator.java @@ -56,7 +56,7 @@ public static TypeParameterMatcher generate(Class type) { public static TypeParameterMatcher generate(Class type, ClassLoader classLoader) { final String typeName = typeName(type); - final String className = "com.github.mauricio.netty.util.internal.__matchers__." + typeName + "Matcher"; + final String className = "io.netty.util.internal.__matchers__." + typeName + "Matcher"; try { try { return (TypeParameterMatcher) Class.forName(className, true, classLoader).newInstance(); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/LongCounter.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/LongCounter.java new file mode 100644 index 00000000..8e628cb6 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/LongCounter.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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.github.mauricio.netty.util.internal; + +/** + * Counter for long. + */ +public interface LongCounter { + void add(long delta); + void increment(); + void decrement(); + long value(); +} diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/MpscArrayQueue.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/MpscArrayQueue.java new file mode 100644 index 00000000..c328f2a8 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/MpscArrayQueue.java @@ -0,0 +1,331 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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. + */ +/* + * 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.github.mauricio.netty.util.internal; + +/** + * Forked from JCTools. + * + * A Multi-Producer-Single-Consumer queue based on a {@link ConcurrentCircularArrayQueue}. This implies that + * any thread may call the offer method, but only a single thread may call poll/peek for correctness to + * maintained.
    + * This implementation follows patterns documented on the package level for False Sharing protection.
    + * This implementation is using the Fast Flow + * method for polling from the queue (with minor change to correctly publish the index) and an extension of + * the Leslie Lamport concurrent queue algorithm (originated by Martin Thompson) on the producer side.
    + * + * @param + */ +final class MpscArrayQueue extends MpscArrayQueueConsumerField { + long p40, p41, p42, p43, p44, p45, p46; + long p30, p31, p32, p33, p34, p35, p36, p37; + + public MpscArrayQueue(final int capacity) { + super(capacity); + } + + /** + * {@inheritDoc}
    + * + * IMPLEMENTATION NOTES:
    + * Lock free offer using a single CAS. As class name suggests access is permitted to many threads + * concurrently. + * + * @see java.util.Queue#offer(java.lang.Object) + */ + @Override + public boolean offer(final E e) { + if (null == e) { + throw new NullPointerException("Null is not a valid element"); + } + + // use a cached view on consumer index (potentially updated in loop) + final long mask = this.mask; + final long capacity = mask + 1; + long consumerIndexCache = lvConsumerIndexCache(); // LoadLoad + long currentProducerIndex; + do { + currentProducerIndex = lvProducerIndex(); // LoadLoad + final long wrapPoint = currentProducerIndex - capacity; + if (consumerIndexCache <= wrapPoint) { + final long currHead = lvConsumerIndex(); // LoadLoad + if (currHead <= wrapPoint) { + return false; // FULL :( + } else { + // update shared cached value of the consumerIndex + svConsumerIndexCache(currHead); // StoreLoad + // update on stack copy, we might need this value again if we lose the CAS. + consumerIndexCache = currHead; + } + } + } while (!casProducerIndex(currentProducerIndex, currentProducerIndex + 1)); + /* + * NOTE: the new producer index value is made visible BEFORE the element in the array. If we relied on + * the index visibility to poll() we would need to handle the case where the element is not visible. + */ + + // Won CAS, move on to storing + final long offset = calcElementOffset(currentProducerIndex, mask); + soElement(offset, e); // StoreStore + return true; // AWESOME :) + } + + /** + * A wait free alternative to offer which fails on CAS failure. + * + * @param e new element, not null + * @return 1 if next element cannot be filled, -1 if CAS failed, 0 if successful + */ + public int weakOffer(final E e) { + if (null == e) { + throw new NullPointerException("Null is not a valid element"); + } + final long mask = this.mask; + final long capacity = mask + 1; + final long currentTail = lvProducerIndex(); // LoadLoad + final long consumerIndexCache = lvConsumerIndexCache(); // LoadLoad + final long wrapPoint = currentTail - capacity; + if (consumerIndexCache <= wrapPoint) { + long currHead = lvConsumerIndex(); // LoadLoad + if (currHead <= wrapPoint) { + return 1; // FULL :( + } else { + svConsumerIndexCache(currHead); // StoreLoad + } + } + + // look Ma, no loop! + if (!casProducerIndex(currentTail, currentTail + 1)) { + return -1; // CAS FAIL :( + } + + // Won CAS, move on to storing + final long offset = calcElementOffset(currentTail, mask); + soElement(offset, e); + return 0; // AWESOME :) + } + + /** + * {@inheritDoc} + *

    + * IMPLEMENTATION NOTES:
    + * Lock free poll using ordered loads/stores. As class name suggests access is limited to a single thread. + * + * @see java.util.Queue#poll() + */ + @Override + public E poll() { + final long consumerIndex = lvConsumerIndex(); // LoadLoad + final long offset = calcElementOffset(consumerIndex); + // Copy field to avoid re-reading after volatile load + final E[] buffer = this.buffer; + + // If we can't see the next available element we can't poll + E e = lvElement(buffer, offset); // LoadLoad + if (null == e) { + /* + * NOTE: Queue may not actually be empty in the case of a producer (P1) being interrupted after + * winning the CAS on offer but before storing the element in the queue. Other producers may go on + * to fill up the queue after this element. + */ + if (consumerIndex != lvProducerIndex()) { + do { + e = lvElement(buffer, offset); + } while (e == null); + } else { + return null; + } + } + + spElement(buffer, offset, null); + soConsumerIndex(consumerIndex + 1); // StoreStore + return e; + } + + /** + * {@inheritDoc} + *

    + * IMPLEMENTATION NOTES:
    + * Lock free peek using ordered loads. As class name suggests access is limited to a single thread. + * + * @see java.util.Queue#poll() + */ + @Override + public E peek() { + // Copy field to avoid re-reading after volatile load + final E[] buffer = this.buffer; + + final long consumerIndex = lvConsumerIndex(); // LoadLoad + final long offset = calcElementOffset(consumerIndex); + E e = lvElement(buffer, offset); + if (null == e) { + /* + * NOTE: Queue may not actually be empty in the case of a producer (P1) being interrupted after + * winning the CAS on offer but before storing the element in the queue. Other producers may go on + * to fill up the queue after this element. + */ + if (consumerIndex != lvProducerIndex()) { + do { + e = lvElement(buffer, offset); + } while (e == null); + } else { + return null; + } + } + return e; + } + + /** + * {@inheritDoc} + *

    + * + */ + @Override + public int size() { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + return (int) (currentProducerIndex - after); + } + } + } + + @Override + public boolean isEmpty() { + // Order matters! + // Loading consumer before producer allows for producer increments after consumer index is read. + // This ensures the correctness of this method at least for the consumer thread. Other threads POV is + // not really + // something we can fix here. + return lvConsumerIndex() == lvProducerIndex(); + } +} + +abstract class MpscArrayQueueL1Pad extends ConcurrentCircularArrayQueue { + long p10, p11, p12, p13, p14, p15, p16; + long p30, p31, p32, p33, p34, p35, p36, p37; + + public MpscArrayQueueL1Pad(int capacity) { + super(capacity); + } +} + +abstract class MpscArrayQueueTailField extends MpscArrayQueueL1Pad { + private static final long P_INDEX_OFFSET; + + static { + try { + P_INDEX_OFFSET = PlatformDependent0.UNSAFE.objectFieldOffset(MpscArrayQueueTailField.class + .getDeclaredField("producerIndex")); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + private volatile long producerIndex; + + public MpscArrayQueueTailField(int capacity) { + super(capacity); + } + + protected final long lvProducerIndex() { + return producerIndex; + } + + protected final boolean casProducerIndex(long expect, long newValue) { + return PlatformDependent0.UNSAFE.compareAndSwapLong(this, P_INDEX_OFFSET, expect, newValue); + } +} + +abstract class MpscArrayQueueMidPad extends MpscArrayQueueTailField { + long p20, p21, p22, p23, p24, p25, p26; + long p30, p31, p32, p33, p34, p35, p36, p37; + + public MpscArrayQueueMidPad(int capacity) { + super(capacity); + } +} + +abstract class MpscArrayQueueHeadCacheField extends MpscArrayQueueMidPad { + private volatile long headCache; + + public MpscArrayQueueHeadCacheField(int capacity) { + super(capacity); + } + + protected final long lvConsumerIndexCache() { + return headCache; + } + + protected final void svConsumerIndexCache(long v) { + headCache = v; + } +} + +abstract class MpscArrayQueueL2Pad extends MpscArrayQueueHeadCacheField { + long p20, p21, p22, p23, p24, p25, p26; + long p30, p31, p32, p33, p34, p35, p36, p37; + + public MpscArrayQueueL2Pad(int capacity) { + super(capacity); + } +} + +abstract class MpscArrayQueueConsumerField extends MpscArrayQueueL2Pad { + private static final long C_INDEX_OFFSET; + static { + try { + C_INDEX_OFFSET = PlatformDependent0.UNSAFE.objectFieldOffset(MpscArrayQueueConsumerField.class + .getDeclaredField("consumerIndex")); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + private volatile long consumerIndex; + + public MpscArrayQueueConsumerField(int capacity) { + super(capacity); + } + + protected final long lvConsumerIndex() { + return consumerIndex; + } + + protected void soConsumerIndex(long l) { + PlatformDependent0.UNSAFE.putOrderedLong(this, C_INDEX_OFFSET, l); + } +} + diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/MpscLinkedQueue.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/MpscLinkedQueue.java index 3f3a69ad..a8ea8b81 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/MpscLinkedQueue.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/MpscLinkedQueue.java @@ -21,10 +21,10 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.lang.reflect.Array; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; import java.util.Queue; @@ -164,12 +164,20 @@ public E peek() { public int size() { int count = 0; MpscLinkedQueueNode n = peekNode(); - for (;;) { - if (n == null) { + for (;;) { + // If value == null it means that clearMaybe() was called on the MpscLinkedQueueNode. + if (n == null || n.value() == null) { + break; + } + MpscLinkedQueueNode next = n.next(); + if (n == next) { + break; + } + n = next; + if (++ count == Integer.MAX_VALUE) { + // Guard against overflow of integer. break; } - count ++; - n = n.next(); } return count; } @@ -186,40 +194,26 @@ public boolean contains(Object o) { if (n == null) { break; } - if (n.value() == o) { + E value = n.value(); + // If value == null it means that clearMaybe() was called on the MpscLinkedQueueNode. + if (value == null) { + return false; + } + if (value == o) { return true; } - n = n.next(); + MpscLinkedQueueNode next = n.next(); + if (n == next) { + break; + } + n = next; } return false; } @Override public Iterator iterator() { - return new Iterator() { - private MpscLinkedQueueNode node = peekNode(); - - @Override - public boolean hasNext() { - return node != null; - } - - @Override - public E next() { - MpscLinkedQueueNode node = this.node; - if (node == null) { - throw new NoSuchElementException(); - } - E value = node.value(); - this.node = node.next(); - return value; - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; + return new ReadOnlyIterator(toList().iterator()); } @Override @@ -248,53 +242,46 @@ public E element() { throw new NoSuchElementException(); } - @Override - public Object[] toArray() { - final Object[] array = new Object[size()]; - final Iterator it = iterator(); - for (int i = 0; i < array.length; i ++) { - if (it.hasNext()) { - array[i] = it.next(); - } else { - return Arrays.copyOf(array, i); + private List toList(int initialCapacity) { + return toList(new ArrayList(initialCapacity)); + } + + private List toList() { + return toList(new ArrayList()); + } + + private List toList(List elements) { + MpscLinkedQueueNode n = peekNode(); + for (;;) { + if (n == null) { + break; + } + E value = n.value(); + if (value == null) { + break; + } + if (!elements.add(value)) { + // Seems like there is no space left, break here. + break; + } + MpscLinkedQueueNode next = n.next(); + if (n == next) { + break; } + n = next; } - return array; + return elements; + } + + @Override + public Object[] toArray() { + return toList().toArray(); } @Override @SuppressWarnings("unchecked") public T[] toArray(T[] a) { - final int size = size(); - final T[] array; - if (a.length >= size) { - array = a; - } else { - array = (T[]) Array.newInstance(a.getClass().getComponentType(), size); - } - - final Iterator it = iterator(); - for (int i = 0; i < array.length; i++) { - if (it.hasNext()) { - array[i] = (T) it.next(); - } else { - if (a == array) { - array[i] = null; - return array; - } - - if (a.length < i) { - return Arrays.copyOf(array, i); - } - - System.arraycopy(array, 0, a, 0, i); - if (a.length > i) { - a[i] = null; - } - return a; - } - } - return array; + return toList(a.length).toArray(a); } @Override diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/NativeLibraryLoader.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/NativeLibraryLoader.java index 4825ae31..050e9ca1 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/NativeLibraryLoader.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/NativeLibraryLoader.java @@ -41,7 +41,7 @@ public final class NativeLibraryLoader { static { OSNAME = SystemPropertyUtil.get("os.name", "").toLowerCase(Locale.US).replaceAll("[^a-z0-9]+", ""); - String workdir = SystemPropertyUtil.get("com.github.mauricio.netty.native.workdir"); + String workdir = SystemPropertyUtil.get("io.netty.native.workdir"); if (workdir != null) { File f = new File(workdir); f.mkdirs(); @@ -53,25 +53,25 @@ public final class NativeLibraryLoader { } WORKDIR = f; - logger.debug("-Dcom.github.mauricio.netty.netty.workdir: " + WORKDIR); + logger.debug("-Dio.netty.netty.workdir: " + WORKDIR); } else { WORKDIR = tmpdir(); - logger.debug("-Dcom.github.mauricio.netty.netty.workdir: " + WORKDIR + " (com.github.mauricio.netty.tmpdir)"); + logger.debug("-Dio.netty.netty.workdir: " + WORKDIR + " (io.netty.tmpdir)"); } } private static File tmpdir() { File f; try { - f = toDirectory(SystemPropertyUtil.get("com.github.mauricio.netty.tmpdir")); + f = toDirectory(SystemPropertyUtil.get("io.netty.tmpdir")); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: " + f); + logger.debug("-Dio.netty.tmpdir: " + f); return f; } f = toDirectory(SystemPropertyUtil.get("java.io.tmpdir")); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: " + f + " (java.io.tmpdir)"); + logger.debug("-Dio.netty.tmpdir: " + f + " (java.io.tmpdir)"); return f; } @@ -79,7 +79,7 @@ private static File tmpdir() { if (isWindows()) { f = toDirectory(System.getenv("TEMP")); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: " + f + " (%TEMP%)"); + logger.debug("-Dio.netty.tmpdir: " + f + " (%TEMP%)"); return f; } @@ -87,20 +87,20 @@ private static File tmpdir() { if (userprofile != null) { f = toDirectory(userprofile + "\\AppData\\Local\\Temp"); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: " + f + " (%USERPROFILE%\\AppData\\Local\\Temp)"); + logger.debug("-Dio.netty.tmpdir: " + f + " (%USERPROFILE%\\AppData\\Local\\Temp)"); return f; } f = toDirectory(userprofile + "\\Local Settings\\Temp"); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: " + f + " (%USERPROFILE%\\Local Settings\\Temp)"); + logger.debug("-Dio.netty.tmpdir: " + f + " (%USERPROFILE%\\Local Settings\\Temp)"); return f; } } } else { f = toDirectory(System.getenv("TMPDIR")); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: " + f + " ($TMPDIR)"); + logger.debug("-Dio.netty.tmpdir: " + f + " ($TMPDIR)"); return f; } } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/PlatformDependent.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/PlatformDependent.java index 27f1e8ef..ab4f90fc 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/PlatformDependent.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/PlatformDependent.java @@ -17,6 +17,7 @@ import com.github.mauricio.netty.util.CharsetUtil; import com.github.mauricio.netty.util.internal.chmv8.ConcurrentHashMapV8; +import com.github.mauricio.netty.util.internal.chmv8.LongAdderV8; import com.github.mauricio.netty.util.internal.logging.InternalLogger; import com.github.mauricio.netty.util.internal.logging.InternalLoggerFactory; @@ -29,14 +30,19 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; +import java.util.Deque; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.regex.Matcher; @@ -49,7 +55,7 @@ * {@code sun.misc.Unsafe} object. *

    * You can disable the use of {@code sun.misc.Unsafe} if you specify - * the system property com.github.mauricio.netty.noUnsafe. + * the system property io.netty.noUnsafe. */ public final class PlatformDependent { @@ -69,7 +75,7 @@ public final class PlatformDependent { private static final boolean HAS_UNSAFE = hasUnsafe0(); private static final boolean CAN_USE_CHM_V8 = HAS_UNSAFE && JAVA_VERSION < 8; private static final boolean DIRECT_BUFFER_PREFERRED = - HAS_UNSAFE && !SystemPropertyUtil.getBoolean("com.github.mauricio.netty.noPreferDirect", false); + HAS_UNSAFE && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false); private static final long MAX_DIRECT_MEMORY = maxDirectMemory0(); private static final long ARRAY_BASE_OFFSET = arrayBaseOffset0(); @@ -84,7 +90,7 @@ public final class PlatformDependent { static { if (logger.isDebugEnabled()) { - logger.debug("-Dcom.github.mauricio.netty.noPreferDirect: {}", !DIRECT_BUFFER_PREFERRED); + logger.debug("-Dio.netty.noPreferDirect: {}", !DIRECT_BUFFER_PREFERRED); } if (!hasUnsafe() && !isAndroid()) { @@ -147,8 +153,8 @@ public static boolean hasUnsafe() { } /** - * Returns {@code true} if the platform has reliable low-level direct buffer access API and a user specified - * {@code -Dcom.github.mauricio.netty.preferDirect} option. + * Returns {@code true} if the platform has reliable low-level direct buffer access API and a user has not specified + * {@code -Dio.netty.noPreferDirect} option. */ public static boolean directBufferPreferred() { return DIRECT_BUFFER_PREFERRED; @@ -225,6 +231,17 @@ public static ConcurrentMap newConcurrentHashMap() { } } + /** + * Creates a new fastest {@link LongCounter} implementaion for the current platform. + */ + public static LongCounter newLongCounter() { + if (HAS_UNSAFE) { + return new LongAdderV8(); + } else { + return new AtomicLongCounter(); + } + } + /** * Creates a new fastest {@link ConcurrentMap} implementaion for the current platform. */ @@ -409,6 +426,18 @@ public static Queue newMpscQueue() { return new MpscLinkedQueue(); } + /** + * Create a new {@link Queue} which is safe to use for multiple producers (different threads) and a single + * consumer (one thread!) with the given fixes {@code capacity}. + */ + public static Queue newFixedMpscQueue(int capacity) { + if (hasUnsafe()) { + return new MpscArrayQueue(capacity); + } else { + return new LinkedBlockingQueue(capacity); + } + } + /** * Return the {@link ClassLoader} for the given {@link Class}. */ @@ -430,6 +459,17 @@ public static ClassLoader getSystemClassLoader() { return PlatformDependent0.getSystemClassLoader(); } + /** + * Returns a new concurrent {@link Deque}. + */ + public static Deque newConcurrentDeque() { + if (javaVersion() < 7) { + return new LinkedBlockingDeque(); + } else { + return new ConcurrentLinkedDeque(); + } + } + private static boolean isAndroid0() { boolean android; try { @@ -459,7 +499,7 @@ private static boolean isRoot0() { return false; } - String[] ID_COMMANDS = { "/usr/bin/id", "/bin/id", "id", "/usr/xpg4/bin/id"}; + String[] ID_COMMANDS = { "/usr/bin/id", "/bin/id", "/usr/xpg4/bin/id", "id"}; Pattern UID_PATTERN = Pattern.compile("^(?:0|[1-9][0-9]*)$"); for (String idCmd: ID_COMMANDS) { Process p = null; @@ -586,8 +626,8 @@ private static int javaVersion0() { } private static boolean hasUnsafe0() { - boolean noUnsafe = SystemPropertyUtil.getBoolean("com.github.mauricio.netty.noUnsafe", false); - logger.debug("-Dcom.github.mauricio.netty.noUnsafe: {}", noUnsafe); + boolean noUnsafe = SystemPropertyUtil.getBoolean("io.netty.noUnsafe", false); + logger.debug("-Dio.netty.noUnsafe: {}", noUnsafe); if (isAndroid()) { logger.debug("sun.misc.Unsafe: unavailable (Android)"); @@ -595,20 +635,20 @@ private static boolean hasUnsafe0() { } if (noUnsafe) { - logger.debug("sun.misc.Unsafe: unavailable (com.github.mauricio.netty.noUnsafe)"); + logger.debug("sun.misc.Unsafe: unavailable (io.netty.noUnsafe)"); return false; } // Legacy properties boolean tryUnsafe; - if (SystemPropertyUtil.contains("com.github.mauricio.netty.tryUnsafe")) { - tryUnsafe = SystemPropertyUtil.getBoolean("com.github.mauricio.netty.tryUnsafe", true); + if (SystemPropertyUtil.contains("io.netty.tryUnsafe")) { + tryUnsafe = SystemPropertyUtil.getBoolean("io.netty.tryUnsafe", true); } else { tryUnsafe = SystemPropertyUtil.getBoolean("org.jboss.netty.tryUnsafe", true); } if (!tryUnsafe) { - logger.debug("sun.misc.Unsafe: unavailable (com.github.mauricio.netty.tryUnsafe/org.jboss.netty.tryUnsafe)"); + logger.debug("sun.misc.Unsafe: unavailable (io.netty.tryUnsafe/org.jboss.netty.tryUnsafe)"); return false; } @@ -696,11 +736,11 @@ private static boolean hasJavassist0() { return false; } - boolean noJavassist = SystemPropertyUtil.getBoolean("com.github.mauricio.netty.noJavassist", false); - logger.debug("-Dcom.github.mauricio.netty.noJavassist: {}", noJavassist); + boolean noJavassist = SystemPropertyUtil.getBoolean("io.netty.noJavassist", false); + logger.debug("-Dio.netty.noJavassist: {}", noJavassist); if (noJavassist) { - logger.debug("Javassist: unavailable (com.github.mauricio.netty.noJavassist)"); + logger.debug("Javassist: unavailable (io.netty.noJavassist)"); return false; } @@ -721,15 +761,15 @@ private static boolean hasJavassist0() { private static File tmpdir0() { File f; try { - f = toDirectory(SystemPropertyUtil.get("com.github.mauricio.netty.tmpdir")); + f = toDirectory(SystemPropertyUtil.get("io.netty.tmpdir")); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: {}", f); + logger.debug("-Dio.netty.tmpdir: {}", f); return f; } f = toDirectory(SystemPropertyUtil.get("java.io.tmpdir")); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: {} (java.io.tmpdir)", f); + logger.debug("-Dio.netty.tmpdir: {} (java.io.tmpdir)", f); return f; } @@ -737,7 +777,7 @@ private static File tmpdir0() { if (isWindows()) { f = toDirectory(System.getenv("TEMP")); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: {} (%TEMP%)", f); + logger.debug("-Dio.netty.tmpdir: {} (%TEMP%)", f); return f; } @@ -745,20 +785,20 @@ private static File tmpdir0() { if (userprofile != null) { f = toDirectory(userprofile + "\\AppData\\Local\\Temp"); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: {} (%USERPROFILE%\\AppData\\Local\\Temp)", f); + logger.debug("-Dio.netty.tmpdir: {} (%USERPROFILE%\\AppData\\Local\\Temp)", f); return f; } f = toDirectory(userprofile + "\\Local Settings\\Temp"); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: {} (%USERPROFILE%\\Local Settings\\Temp)", f); + logger.debug("-Dio.netty.tmpdir: {} (%USERPROFILE%\\Local Settings\\Temp)", f); return f; } } } else { f = toDirectory(System.getenv("TMPDIR")); if (f != null) { - logger.debug("-Dcom.github.mauricio.netty.tmpdir: {} ($TMPDIR)", f); + logger.debug("-Dio.netty.tmpdir: {} ($TMPDIR)", f); return f; } } @@ -799,21 +839,21 @@ private static File toDirectory(String path) { private static int bitMode0() { // Check user-specified bit mode first. - int bitMode = SystemPropertyUtil.getInt("com.github.mauricio.netty.bitMode", 0); + int bitMode = SystemPropertyUtil.getInt("io.netty.bitMode", 0); if (bitMode > 0) { - logger.debug("-Dcom.github.mauricio.netty.bitMode: {}", bitMode); + logger.debug("-Dio.netty.bitMode: {}", bitMode); return bitMode; } // And then the vendor specific ones which is probably most reliable. bitMode = SystemPropertyUtil.getInt("sun.arch.data.model", 0); if (bitMode > 0) { - logger.debug("-Dcom.github.mauricio.netty.bitMode: {} (sun.arch.data.model)", bitMode); + logger.debug("-Dio.netty.bitMode: {} (sun.arch.data.model)", bitMode); return bitMode; } bitMode = SystemPropertyUtil.getInt("com.ibm.vm.bitmode", 0); if (bitMode > 0) { - logger.debug("-Dcom.github.mauricio.netty.bitMode: {} (com.ibm.vm.bitmode)", bitMode); + logger.debug("-Dio.netty.bitMode: {} (com.ibm.vm.bitmode)", bitMode); return bitMode; } @@ -826,7 +866,7 @@ private static int bitMode0() { } if (bitMode > 0) { - logger.debug("-Dcom.github.mauricio.netty.bitMode: {} (os.arch: {})", bitMode, arch); + logger.debug("-Dio.netty.bitMode: {} (os.arch: {})", bitMode, arch); } // Last resort: guess from VM name and then fall back to most common 64-bit mode. @@ -847,6 +887,28 @@ private static int addressSize0() { return PlatformDependent0.addressSize(); } + private static final class AtomicLongCounter extends AtomicLong implements LongCounter { + @Override + public void add(long delta) { + addAndGet(delta); + } + + @Override + public void increment() { + incrementAndGet(); + } + + @Override + public void decrement() { + decrementAndGet(); + } + + @Override + public long value() { + return get(); + } + } + private PlatformDependent() { // only static method supported } diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/PlatformDependent0.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/PlatformDependent0.java index 2606c6d3..91253351 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/PlatformDependent0.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/PlatformDependent0.java @@ -36,7 +36,7 @@ final class PlatformDependent0 { private static final InternalLogger logger = InternalLoggerFactory.getInstance(PlatformDependent0.class); - private static final Unsafe UNSAFE; + static final Unsafe UNSAFE; private static final boolean BIG_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN; private static final long ADDRESS_FIELD_OFFSET; diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/RecyclableArrayList.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/RecyclableArrayList.java index b7af4c3a..842c89d1 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/RecyclableArrayList.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/RecyclableArrayList.java @@ -25,7 +25,7 @@ import java.util.RandomAccess; /** - * A simple list which is reyclable. This implementation does not allow {@code null} elements to be added. + * A simple list which is recyclable. This implementation does not allow {@code null} elements to be added. */ public final class RecyclableArrayList extends ArrayList { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/StringUtil.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/StringUtil.java index 9b68dfce..7de23a33 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/StringUtil.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/StringUtil.java @@ -26,10 +26,14 @@ public final class StringUtil { public static final String NEWLINE; - + public static final char DOUBLE_QUOTE = '\"'; + public static final char COMMA = ','; + public static final char LINE_FEED = '\n'; + public static final char CARRIAGE_RETURN = '\r'; + public static final String EMPTY_STRING = ""; private static final String[] BYTE2HEX_PAD = new String[256]; private static final String[] BYTE2HEX_NOPAD = new String[256]; - private static final String EMPTY_STRING = ""; + private static final char PACKAGE_SEPARATOR_CHAR = '.'; static { // Determine the newline character of the current platform. @@ -302,16 +306,12 @@ public static String simpleClassName(Object o) { * with anonymous classes. */ public static String simpleClassName(Class clazz) { - if (clazz == null) { - return "null_class"; - } - - Package pkg = clazz.getPackage(); - if (pkg != null) { - return clazz.getName().substring(pkg.getName().length() + 1); - } else { - return clazz.getName(); + String className = ObjectUtil.checkNotNull(clazz, "clazz").getName(); + final int lastDotIdx = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR); + if (lastDotIdx > -1) { + return className.substring(lastDotIdx + 1); } + return className; } private StringUtil() { diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/ThreadLocalRandom.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/ThreadLocalRandom.java index e777f2b9..78b22a89 100644 --- a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/ThreadLocalRandom.java +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/ThreadLocalRandom.java @@ -42,7 +42,7 @@ * than shared {@code Random} objects in concurrent programs will * typically encounter much less overhead and contention. Use of * {@code ThreadLocalRandom} is particularly appropriate when multiple - * tasks (for example, each a {@link com.github.mauricio.netty.util.internal.chmv8.ForkJoinTask}) use random numbers + * tasks (for example, each a {@link io.netty.util.internal.chmv8.ForkJoinTask}) use random numbers * in parallel in thread pools. * *

    Usages of this class should typically be of the form: @@ -76,7 +76,7 @@ public static synchronized long getInitialSeedUniquifier() { if (initialSeedUniquifier == 0) { // Use the system property value. ThreadLocalRandom.initialSeedUniquifier = initialSeedUniquifier = - SystemPropertyUtil.getLong("com.github.mauricio.netty.initialSeedUniquifier", 0); + SystemPropertyUtil.getLong("io.netty.initialSeedUniquifier", 0); } // Otherwise, generate one. @@ -167,7 +167,7 @@ private static long newSeed() { if (seedUniquifier.compareAndSet(current, next)) { if (current == 0 && logger.isDebugEnabled()) { logger.debug(String.format( - "-Dcom.github.mauricio.netty.initialSeedUniquifier: 0x%016x (took %d ms)", + "-Dio.netty.initialSeedUniquifier: 0x%016x (took %d ms)", actualCurrent, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime))); } return next ^ System.nanoTime(); diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/chmv8/LongAdderV8.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/chmv8/LongAdderV8.java new file mode 100644 index 00000000..177ba031 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/chmv8/LongAdderV8.java @@ -0,0 +1,225 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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. + */ + +/* + * Written by Doug Lea with assistance from members of JCP JSR-166 + * Expert Group and released to the public domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package com.github.mauricio.netty.util.internal.chmv8; + + +import com.github.mauricio.netty.util.internal.LongCounter; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicLong; + +/** + * One or more variables that together maintain an initially zero + * {@code long} sum. When updates (method {@link #add}) are contended + * across threads, the set of variables may grow dynamically to reduce + * contention. Method {@link #sum} (or, equivalently, {@link + * #longValue}) returns the current total combined across the + * variables maintaining the sum. + * + *

    This class is usually preferable to {@link AtomicLong} when + * multiple threads update a common sum that is used for purposes such + * as collecting statistics, not for fine-grained synchronization + * control. Under low update contention, the two classes have similar + * characteristics. But under high contention, expected throughput of + * this class is significantly higher, at the expense of higher space + * consumption. + * + *

    This class extends {@link Number}, but does not define + * methods such as {@code equals}, {@code hashCode} and {@code + * compareTo} because instances are expected to be mutated, and so are + * not useful as collection keys. + * + *

    jsr166e note: This class is targeted to be placed in + * java.util.concurrent.atomic. + * + * @since 1.8 + * @author Doug Lea + */ +@SuppressWarnings("all") +public class LongAdderV8 extends Striped64 implements Serializable, LongCounter { + private static final long serialVersionUID = 7249069246863182397L; + + /** + * Version of plus for use in retryUpdate + */ + final long fn(long v, long x) { return v + x; } + + /** + * Creates a new adder with initial sum of zero. + */ + public LongAdderV8() { + } + + /** + * Adds the given value. + * + * @param x the value to add + */ + public void add(long x) { + Cell[] as; long b, v; int[] hc; Cell a; int n; + if ((as = cells) != null || !casBase(b = base, b + x)) { + boolean uncontended = true; + if ((hc = threadHashCode.get()) == null || + as == null || (n = as.length) < 1 || + (a = as[(n - 1) & hc[0]]) == null || + !(uncontended = a.cas(v = a.value, v + x))) + retryUpdate(x, hc, uncontended); + } + } + + /** + * Equivalent to {@code add(1)}. + */ + public void increment() { + add(1L); + } + + /** + * Equivalent to {@code add(-1)}. + */ + public void decrement() { + add(-1L); + } + + /** + * Returns the current sum. The returned value is NOT an + * atomic snapshot; invocation in the absence of concurrent + * updates returns an accurate result, but concurrent updates that + * occur while the sum is being calculated might not be + * incorporated. + * + * @return the sum + */ + public long sum() { + long sum = base; + Cell[] as = cells; + if (as != null) { + int n = as.length; + for (int i = 0; i < n; ++i) { + Cell a = as[i]; + if (a != null) + sum += a.value; + } + } + return sum; + } + + /** + * Resets variables maintaining the sum to zero. This method may + * be a useful alternative to creating a new adder, but is only + * effective if there are no concurrent updates. Because this + * method is intrinsically racy, it should only be used when it is + * known that no threads are concurrently updating. + */ + public void reset() { + internalReset(0L); + } + + /** + * Equivalent in effect to {@link #sum} followed by {@link + * #reset}. This method may apply for example during quiescent + * points between multithreaded computations. If there are + * updates concurrent with this method, the returned value is + * not guaranteed to be the final value occurring before + * the reset. + * + * @return the sum + */ + public long sumThenReset() { + long sum = base; + Cell[] as = cells; + base = 0L; + if (as != null) { + int n = as.length; + for (int i = 0; i < n; ++i) { + Cell a = as[i]; + if (a != null) { + sum += a.value; + a.value = 0L; + } + } + } + return sum; + } + + /** + * Returns the String representation of the {@link #sum}. + * @return the String representation of the {@link #sum} + */ + public String toString() { + return Long.toString(sum()); + } + + /** + * Equivalent to {@link #sum}. + * + * @return the sum + */ + public long longValue() { + return sum(); + } + + /** + * Returns the {@link #sum} as an {@code int} after a narrowing + * primitive conversion. + */ + public int intValue() { + return (int)sum(); + } + + /** + * Returns the {@link #sum} as a {@code float} + * after a widening primitive conversion. + */ + public float floatValue() { + return (float)sum(); + } + + /** + * Returns the {@link #sum} as a {@code double} after a widening + * primitive conversion. + */ + public double doubleValue() { + return (double)sum(); + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + s.writeLong(sum()); + } + + private void readObject(ObjectInputStream s) + throws IOException, ClassNotFoundException { + s.defaultReadObject(); + busy = 0; + cells = null; + base = s.readLong(); + } + + @Override + public long value() { + return sum(); + } +} \ No newline at end of file diff --git a/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/chmv8/Striped64.java b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/chmv8/Striped64.java new file mode 100644 index 00000000..86b412f8 --- /dev/null +++ b/db-async-common/src/main/java/com/github/mauricio/netty/util/internal/chmv8/Striped64.java @@ -0,0 +1,351 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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. + */ + +/* + * Written by Doug Lea with assistance from members of JCP JSR-166 + * Expert Group and released to the public domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package com.github.mauricio.netty.util.internal.chmv8; + + +import java.util.Random; + +/** + * A package-local class holding common representation and mechanics + * for classes supporting dynamic striping on 64bit values. The class + * extends Number so that concrete subclasses must publicly do so. + */ +@SuppressWarnings("all") +abstract class Striped64 extends Number { + /* + * This class maintains a lazily-initialized table of atomically + * updated variables, plus an extra "base" field. The table size + * is a power of two. Indexing uses masked per-thread hash codes. + * Nearly all declarations in this class are package-private, + * accessed directly by subclasses. + * + * Table entries are of class Cell; a variant of AtomicLong padded + * to reduce cache contention on most processors. Padding is + * overkill for most Atomics because they are usually irregularly + * scattered in memory and thus don't interfere much with each + * other. But Atomic objects residing in arrays will tend to be + * placed adjacent to each other, and so will most often share + * cache lines (with a huge negative performance impact) without + * this precaution. + * + * In part because Cells are relatively large, we avoid creating + * them until they are needed. When there is no contention, all + * updates are made to the base field. Upon first contention (a + * failed CAS on base update), the table is initialized to size 2. + * The table size is doubled upon further contention until + * reaching the nearest power of two greater than or equal to the + * number of CPUS. Table slots remain empty (null) until they are + * needed. + * + * A single spinlock ("busy") is used for initializing and + * resizing the table, as well as populating slots with new Cells. + * There is no need for a blocking lock; when the lock is not + * available, threads try other slots (or the base). During these + * retries, there is increased contention and reduced locality, + * which is still better than alternatives. + * + * Per-thread hash codes are initialized to random values. + * Contention and/or table collisions are indicated by failed + * CASes when performing an update operation (see method + * retryUpdate). Upon a collision, if the table size is less than + * the capacity, it is doubled in size unless some other thread + * holds the lock. If a hashed slot is empty, and lock is + * available, a new Cell is created. Otherwise, if the slot + * exists, a CAS is tried. Retries proceed by "double hashing", + * using a secondary hash (Marsaglia XorShift) to try to find a + * free slot. + * + * The table size is capped because, when there are more threads + * than CPUs, supposing that each thread were bound to a CPU, + * there would exist a perfect hash function mapping threads to + * slots that eliminates collisions. When we reach capacity, we + * search for this mapping by randomly varying the hash codes of + * colliding threads. Because search is random, and collisions + * only become known via CAS failures, convergence can be slow, + * and because threads are typically not bound to CPUS forever, + * may not occur at all. However, despite these limitations, + * observed contention rates are typically low in these cases. + * + * It is possible for a Cell to become unused when threads that + * once hashed to it terminate, as well as in the case where + * doubling the table causes no thread to hash to it under + * expanded mask. We do not try to detect or remove such cells, + * under the assumption that for long-running instances, observed + * contention levels will recur, so the cells will eventually be + * needed again; and for short-lived ones, it does not matter. + */ + + /** + * Padded variant of AtomicLong supporting only raw accesses plus CAS. + * The value field is placed between pads, hoping that the JVM doesn't + * reorder them. + * + * JVM intrinsics note: It would be possible to use a release-only + * form of CAS here, if it were provided. + */ + static final class Cell { + volatile long p0, p1, p2, p3, p4, p5, p6; + volatile long value; + volatile long q0, q1, q2, q3, q4, q5, q6; + Cell(long x) { value = x; } + + final boolean cas(long cmp, long val) { + return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); + } + + // Unsafe mechanics + private static final sun.misc.Unsafe UNSAFE; + private static final long valueOffset; + static { + try { + UNSAFE = getUnsafe(); + Class ak = Cell.class; + valueOffset = UNSAFE.objectFieldOffset + (ak.getDeclaredField("value")); + } catch (Exception e) { + throw new Error(e); + } + } + + } + + /** + * ThreadLocal holding a single-slot int array holding hash code. + * Unlike the JDK8 version of this class, we use a suboptimal + * int[] representation to avoid introducing a new type that can + * impede class-unloading when ThreadLocals are not removed. + */ + static final ThreadLocal threadHashCode = new ThreadLocal(); + + /** + * Generator of new random hash codes + */ + static final Random rng = new Random(); + + /** Number of CPUS, to place bound on table size */ + static final int NCPU = Runtime.getRuntime().availableProcessors(); + + /** + * Table of cells. When non-null, size is a power of 2. + */ + transient volatile Cell[] cells; + + /** + * Base value, used mainly when there is no contention, but also as + * a fallback during table initialization races. Updated via CAS. + */ + transient volatile long base; + + /** + * Spinlock (locked via CAS) used when resizing and/or creating Cells. + */ + transient volatile int busy; + + /** + * Package-private default constructor + */ + Striped64() { + } + + /** + * CASes the base field. + */ + final boolean casBase(long cmp, long val) { + return UNSAFE.compareAndSwapLong(this, baseOffset, cmp, val); + } + + /** + * CASes the busy field from 0 to 1 to acquire lock. + */ + final boolean casBusy() { + return UNSAFE.compareAndSwapInt(this, busyOffset, 0, 1); + } + + /** + * Computes the function of current and new value. Subclasses + * should open-code this update function for most uses, but the + * virtualized form is needed within retryUpdate. + * + * @param currentValue the current value (of either base or a cell) + * @param newValue the argument from a user update call + * @return result of the update function + */ + abstract long fn(long currentValue, long newValue); + + /** + * Handles cases of updates involving initialization, resizing, + * creating new Cells, and/or contention. See above for + * explanation. This method suffers the usual non-modularity + * problems of optimistic retry code, relying on rechecked sets of + * reads. + * + * @param x the value + * @param hc the hash code holder + * @param wasUncontended false if CAS failed before call + */ + final void retryUpdate(long x, int[] hc, boolean wasUncontended) { + int h; + if (hc == null) { + threadHashCode.set(hc = new int[1]); // Initialize randomly + int r = rng.nextInt(); // Avoid zero to allow xorShift rehash + h = hc[0] = (r == 0) ? 1 : r; + } + else + h = hc[0]; + boolean collide = false; // True if last slot nonempty + for (;;) { + Cell[] as; Cell a; int n; long v; + if ((as = cells) != null && (n = as.length) > 0) { + if ((a = as[(n - 1) & h]) == null) { + if (busy == 0) { // Try to attach new Cell + Cell r = new Cell(x); // Optimistically create + if (busy == 0 && casBusy()) { + boolean created = false; + try { // Recheck under lock + Cell[] rs; int m, j; + if ((rs = cells) != null && + (m = rs.length) > 0 && + rs[j = (m - 1) & h] == null) { + rs[j] = r; + created = true; + } + } finally { + busy = 0; + } + if (created) + break; + continue; // Slot is now non-empty + } + } + collide = false; + } + else if (!wasUncontended) // CAS already known to fail + wasUncontended = true; // Continue after rehash + else if (a.cas(v = a.value, fn(v, x))) + break; + else if (n >= NCPU || cells != as) + collide = false; // At max size or stale + else if (!collide) + collide = true; + else if (busy == 0 && casBusy()) { + try { + if (cells == as) { // Expand table unless stale + Cell[] rs = new Cell[n << 1]; + for (int i = 0; i < n; ++i) + rs[i] = as[i]; + cells = rs; + } + } finally { + busy = 0; + } + collide = false; + continue; // Retry with expanded table + } + h ^= h << 13; // Rehash + h ^= h >>> 17; + h ^= h << 5; + hc[0] = h; // Record index for next time + } + else if (busy == 0 && cells == as && casBusy()) { + boolean init = false; + try { // Initialize table + if (cells == as) { + Cell[] rs = new Cell[2]; + rs[h & 1] = new Cell(x); + cells = rs; + init = true; + } + } finally { + busy = 0; + } + if (init) + break; + } + else if (casBase(v = base, fn(v, x))) + break; // Fall back on using base + } + } + + + /** + * Sets base and all cells to the given value. + */ + final void internalReset(long initialValue) { + Cell[] as = cells; + base = initialValue; + if (as != null) { + int n = as.length; + for (int i = 0; i < n; ++i) { + Cell a = as[i]; + if (a != null) + a.value = initialValue; + } + } + } + + // Unsafe mechanics + private static final sun.misc.Unsafe UNSAFE; + private static final long baseOffset; + private static final long busyOffset; + static { + try { + UNSAFE = getUnsafe(); + Class sk = Striped64.class; + baseOffset = UNSAFE.objectFieldOffset + (sk.getDeclaredField("base")); + busyOffset = UNSAFE.objectFieldOffset + (sk.getDeclaredField("busy")); + } catch (Exception e) { + throw new Error(e); + } + } + + /** + * Returns a sun.misc.Unsafe. Suitable for use in a 3rd party package. + * Replace with a simple call to Unsafe.getUnsafe when integrating + * into a jdk. + * + * @return a sun.misc.Unsafe + */ + private static sun.misc.Unsafe getUnsafe() { + try { + return sun.misc.Unsafe.getUnsafe(); + } catch (SecurityException tryReflectionInstead) {} + try { + return java.security.AccessController.doPrivileged + (new java.security.PrivilegedExceptionAction() { + public sun.misc.Unsafe run() throws Exception { + Class k = sun.misc.Unsafe.class; + for (java.lang.reflect.Field f : k.getDeclaredFields()) { + f.setAccessible(true); + Object x = f.get(null); + if (k.isInstance(x)) + return k.cast(x); + } + throw new NoSuchFieldError("the Unsafe"); + }}); + } catch (java.security.PrivilegedActionException e) { + throw new RuntimeException("Could not initialize intrinsics", + e.getCause()); + } + } +} \ No newline at end of file diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala index 2a5d32d6..aebc01eb 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala @@ -17,11 +17,11 @@ package com.github.mauricio.async.db import java.nio.charset.Charset -import scala.Predef._ -import scala.{None, Option, Int} + import com.github.mauricio.netty.util.CharsetUtil import com.github.mauricio.netty.buffer.AbstractByteBufAllocator import com.github.mauricio.netty.buffer.PooledByteBufAllocator +import com.github.mauricio.netty.buffer.ByteBufAllocator import scala.concurrent.duration._ object Configuration { @@ -44,6 +44,11 @@ object Configuration { * OOM or eternal loop attacks the client could have, defaults to 16 MB. You can set this * to any value you would like but again, make sure you know what you are doing if you do * change it. + * @param allocator the netty buffer allocator to be used + * @param connectTimeout the timeout for connecting to servers + * @param testTimeout the timeout for connection tests performed by pools + * @param queryTimeout the optional query timeout + * */ case class Configuration(username: String, @@ -53,7 +58,7 @@ case class Configuration(username: String, database: Option[String] = None, charset: Charset = Configuration.DefaultCharset, maximumMessageSize: Int = 16777216, - allocator: AbstractByteBufAllocator = PooledByteBufAllocator.DEFAULT, + allocator: ByteBufAllocator = PooledByteBufAllocator.DEFAULT, connectTimeout: Duration = 5.seconds, - testTimeout: Duration = 5.seconds - ) + testTimeout: Duration = 5.seconds, + queryTimeout: Option[Duration] = None) diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeEncoderDecoder.scala index 9a801775..a7d0c879 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeEncoderDecoder.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeEncoderDecoder.scala @@ -33,14 +33,16 @@ class TimeEncoderDecoder extends ColumnEncoderDecoder { .appendOptional(optional) .toFormatter + final private val printer = new DateTimeFormatterBuilder() + .appendPattern("HH:mm:ss.SSSSSS") + .toFormatter + def formatter = format - override def decode(value: String): LocalTime = { + override def decode(value: String): LocalTime = format.parseLocalTime(value) - } - override def encode(value: Any): String = { - this.format.print(value.asInstanceOf[LocalTime]) - } + override def encode(value: Any): String = + this.printer.print(value.asInstanceOf[LocalTime]) } diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/UUIDEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/UUIDEncoderDecoder.scala new file mode 100644 index 00000000..11987835 --- /dev/null +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/UUIDEncoderDecoder.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2013 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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.github.mauricio.async.db.column + +import java.util.UUID + +object UUIDEncoderDecoder extends ColumnEncoderDecoder { + + override def decode(value: String): UUID = UUID.fromString(value) + +} diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionTimeoutedException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionTimeoutedException.scala new file mode 100644 index 00000000..7e02c17c --- /dev/null +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionTimeoutedException.scala @@ -0,0 +1,6 @@ +package com.github.mauricio.async.db.exceptions + +import com.github.mauricio.async.db.Connection + +class ConnectionTimeoutedException( val connection : Connection ) + extends DatabaseException( "The connection %s has a timeouted query and is being closed".format(connection) ) \ No newline at end of file diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ArrayRowData.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ArrayRowData.scala index c232a12a..fe582481 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ArrayRowData.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ArrayRowData.scala @@ -17,14 +17,10 @@ package com.github.mauricio.async.db.general import com.github.mauricio.async.db.RowData -import scala.collection.mutable -class ArrayRowData( columnCount : Int, row : Int, val mapping : Map[String, Int] ) - extends RowData +class ArrayRowData(row : Int, val mapping : Map[String, Int], val columns : Array[Any]) extends RowData { - private val columns = new Array[Any](columnCount) - /** * * Returns a column value by it's position in the originating query. @@ -51,16 +47,5 @@ class ArrayRowData( columnCount : Int, row : Int, val mapping : Map[String, Int] */ def rowNumber: Int = row - /** - * - * Sets a value to a column in this collection. - * - * @param i - * @param x - */ - - def update(i: Int, x: Any) = columns(i) = x - def length: Int = columns.length - } diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/general/MutableResultSet.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/MutableResultSet.scala index 0422a4cf..00cc712b 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/general/MutableResultSet.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/MutableResultSet.scala @@ -31,22 +31,18 @@ class MutableResultSet[T <: ColumnData]( private val columnMapping: Map[String, Int] = this.columnTypes.indices.map( index => ( this.columnTypes(index).name, index ) ).toMap - + val columnNames : IndexedSeq[String] = this.columnTypes.map(c => c.name) + val types : IndexedSeq[Int] = this.columnTypes.map(c => c.dataType) + override def length: Int = this.rows.length override def apply(idx: Int): RowData = this.rows(idx) - def addRow( row : Seq[Any] ) { - val realRow = new ArrayRowData( columnTypes.size, this.rows.size, this.columnMapping ) - var x = 0 - while ( x < row.size ) { - realRow(x) = row(x) - x += 1 - } - this.rows += realRow + def addRow(row : Array[Any] ) { + this.rows += new ArrayRowData(this.rows.size, this.columnMapping, row) } -} \ No newline at end of file +} diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/AsyncObjectPool.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/AsyncObjectPool.scala index a288eea5..3e4345a8 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/AsyncObjectPool.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/AsyncObjectPool.scala @@ -16,7 +16,7 @@ package com.github.mauricio.async.db.pool -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ExecutionContext, Future, Promise} /** * @@ -72,11 +72,24 @@ trait AsyncObjectPool[T] { def use[A](f: (T) => Future[A])(implicit executionContext: ExecutionContext): Future[A] = take.flatMap { item => - f(item) recoverWith { - case error => giveBack(item).flatMap(_ => Future.failed(error)) - } flatMap { res => - giveBack(item).map { _ => res } + val p = Promise[A]() + try { + f(item).onComplete { r => + giveBack(item).onComplete { _ => + p.complete(r) + } + } + } catch { + // calling f might throw exception. + // in that case the item will be removed from the pool if identified as invalid by the factory. + // the error returned to the user is the original error thrown by f. + case error: Throwable => + giveBack(item).onComplete { _ => + p.failure(error) + } } + + p.future } } diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/TimeoutScheduler.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/TimeoutScheduler.scala new file mode 100644 index 00000000..db30c102 --- /dev/null +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/TimeoutScheduler.scala @@ -0,0 +1,63 @@ +package com.github.mauricio.async.db.pool + +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.{TimeUnit, TimeoutException, ScheduledFuture} +import com.github.mauricio.netty.channel.EventLoopGroup +import scala.concurrent.{ExecutionContext, Promise} +import scala.concurrent.duration.Duration + +trait TimeoutScheduler { + + private var isTimeoutedBool = new AtomicBoolean(false) + + /** + * + * The event loop group to be used for scheduling. + * + * @return + */ + + def eventLoopGroup : EventLoopGroup + + /** + * Implementors should decide here what they want to do when a timeout occur + */ + + def onTimeout // implementors should decide here what they want to do when a timeout occur + + /** + * + * We need this property as isClosed takes time to complete and + * we don't want the connection to be used again. + * + * @return + */ + + def isTimeouted : Boolean = + isTimeoutedBool.get + + def addTimeout[A]( + promise: Promise[A], + durationOption: Option[Duration]) + (implicit executionContext : ExecutionContext) : Option[ScheduledFuture[_]] = { + durationOption.map { + duration => + val scheduledFuture = schedule( + { + if (promise.tryFailure(new TimeoutException(s"Operation is timeouted after it took too long to return (${duration})"))) { + isTimeoutedBool.set(true) + onTimeout + } + }, + duration) + promise.future.onComplete(x => scheduledFuture.cancel(false)) + + scheduledFuture + } + } + + def schedule(block: => Unit, duration: Duration) : ScheduledFuture[_] = + eventLoopGroup.schedule(new Runnable { + override def run(): Unit = block + }, duration.toMillis, TimeUnit.MILLISECONDS) +} diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/DummyTimeoutScheduler.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/DummyTimeoutScheduler.scala new file mode 100644 index 00000000..783ec163 --- /dev/null +++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/DummyTimeoutScheduler.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2013 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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.github.mauricio.async.db.pool + +import java.util.concurrent.atomic.AtomicInteger +import com.github.mauricio.async.db.util.{NettyUtils, ExecutorServiceUtils} +import com.github.mauricio.netty.channel.EventLoopGroup + +/** + * Implementation of TimeoutScheduler used for testing + */ +class DummyTimeoutScheduler extends TimeoutScheduler { + implicit val internalPool = ExecutorServiceUtils.CachedExecutionContext + private val timeOuts = new AtomicInteger + override def onTimeout = timeOuts.incrementAndGet + def timeoutCount = timeOuts.get() + def eventLoopGroup : EventLoopGroup = NettyUtils.DefaultEventLoopGroup +} diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPoolSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPoolSpec.scala index 3b84755d..51d58fb0 100644 --- a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPoolSpec.scala +++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPoolSpec.scala @@ -1,5 +1,7 @@ package com.github.mauricio.async.db.pool +import java.util.concurrent.atomic.AtomicInteger + import org.specs2.mutable.Specification import scala.util.Try import scala.concurrent.Await @@ -17,17 +19,16 @@ class PartitionedAsyncObjectPoolSpec extends SpecificationWithJUnit { val config = PoolConfiguration(100, Long.MaxValue, 100, Int.MaxValue) - + private var current = new AtomicInteger val factory = new ObjectFactory[Int] { var reject = Set[Int]() var failCreate = false - private var current = 0 + def create = if (failCreate) throw new IllegalStateException else { - current += 1 - current + current.incrementAndGet() } def destroy(item: Int) = {} def validate(item: Int) = diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala new file mode 100644 index 00000000..acc952e7 --- /dev/null +++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2013 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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.github.mauricio.async.db.pool + +import java.util.concurrent.{ScheduledFuture, TimeoutException} +import com.github.mauricio.async.db.util.{ByteBufferUtils, ExecutorServiceUtils} +import org.specs2.mutable.SpecificationWithJUnit +import scala.concurrent.duration._ +import scala.concurrent.{Future, Promise} + +/** + * Tests for TimeoutScheduler + */ +class TimeoutSchedulerSpec extends SpecificationWithJUnit { + + val TIMEOUT_DID_NOT_PASS = "timeout did not pass" + + "test timeout did not pass" in { + val timeoutScheduler = new DummyTimeoutScheduler() + val promise = Promise[String]() + val scheduledFuture = timeoutScheduler.addTimeout(promise,Some(Duration(1000, MILLISECONDS))) + Thread.sleep(100); + promise.isCompleted === false + promise.success(TIMEOUT_DID_NOT_PASS) + Thread.sleep(1500) + promise.future.value.get.get === TIMEOUT_DID_NOT_PASS + scheduledFuture.get.isCancelled === true + timeoutScheduler.timeoutCount === 0 + } + + "test timeout passed" in { + val timeoutMillis = 100 + val promise = Promise[String]() + val timeoutScheduler = new DummyTimeoutScheduler() + val scheduledFuture = timeoutScheduler.addTimeout(promise,Some(Duration(timeoutMillis, MILLISECONDS))) + Thread.sleep(1000) + promise.isCompleted === true + scheduledFuture.get.isCancelled === false + promise.trySuccess(TIMEOUT_DID_NOT_PASS) + timeoutScheduler.timeoutCount === 1 + promise.future.value.get.get must throwA[TimeoutException](message = s"Operation is timeouted after it took too long to return \\(${timeoutMillis} milliseconds\\)") + } + + "test no timeout" in { + val timeoutScheduler = new DummyTimeoutScheduler() + val promise = Promise[String]() + val scheduledFuture = timeoutScheduler.addTimeout(promise,None) + Thread.sleep(1000) + scheduledFuture === None + promise.isCompleted === false + promise.success(TIMEOUT_DID_NOT_PASS) + promise.future.value.get.get === TIMEOUT_DID_NOT_PASS + timeoutScheduler.timeoutCount === 0 + } +} + diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLConnection.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLConnection.scala index 936420ad..41584880 100644 --- a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLConnection.scala +++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLConnection.scala @@ -16,13 +16,16 @@ package com.github.mauricio.async.db.mysql +import java.util.concurrent.atomic.{AtomicLong, AtomicReference} + import com.github.mauricio.async.db._ import com.github.mauricio.async.db.exceptions._ -import com.github.mauricio.async.db.mysql.codec.{MySQLHandlerDelegate, MySQLConnectionHandler} +import com.github.mauricio.async.db.mysql.codec.{MySQLConnectionHandler, MySQLHandlerDelegate} import com.github.mauricio.async.db.mysql.exceptions.MySQLException import com.github.mauricio.async.db.mysql.message.client._ import com.github.mauricio.async.db.mysql.message.server._ import com.github.mauricio.async.db.mysql.util.CharsetMapper +import com.github.mauricio.async.db.pool.TimeoutScheduler import com.github.mauricio.async.db.util.ChannelFutureTransformer.toFuture import com.github.mauricio.async.db.util._ import java.util.concurrent.atomic.{AtomicLong,AtomicReference} @@ -42,10 +45,11 @@ class MySQLConnection( configuration: Configuration, charsetMapper: CharsetMapper = CharsetMapper.Instance, group : EventLoopGroup = NettyUtils.DefaultEventLoopGroup, - executionContext : ExecutionContext = ExecutorServiceUtils.CachedExecutionContext + implicit val executionContext : ExecutionContext = ExecutorServiceUtils.CachedExecutionContext ) extends MySQLHandlerDelegate with Connection + with TimeoutScheduler { import MySQLConnection.log @@ -53,10 +57,8 @@ class MySQLConnection( // validate that this charset is supported charsetMapper.toInt(configuration.charset) - private final val connectionCount = MySQLConnection.Counter.incrementAndGet() private final val connectionId = s"[mysql-connection-$connectionCount]" - private implicit val internalPool = executionContext private final val connectionHandler = new MySQLConnectionHandler( configuration, @@ -78,6 +80,8 @@ class MySQLConnection( def lastException : Throwable = this._lastException def count : Long = this.connectionCount + override def eventLoopGroup : EventLoopGroup = group + def connect: Future[Connection] = { this.connectionHandler.connect.onFailure { case e => this.connectionPromise.tryFailure(e) @@ -185,18 +189,17 @@ class MySQLConnection( def sendQuery(query: String): Future[QueryResult] = { this.validateIsReadyForQuery() - val promise = Promise[QueryResult] + val promise = Promise[QueryResult]() this.setQueryPromise(promise) this.connectionHandler.write(new QueryMessage(query)) + addTimeout(promise, configuration.queryTimeout) promise.future } private def failQueryPromise(t: Throwable) { - this.clearQueryPromise.foreach { _.tryFailure(t) } - } private def succeedQueryPromise(queryResult: QueryResult) { @@ -225,6 +228,7 @@ class MySQLConnection( } def disconnect: Future[Connection] = this.close + override def onTimeout = disconnect def isConnected: Boolean = this.connectionHandler.isConnected @@ -234,9 +238,10 @@ class MySQLConnection( if ( values.length != totalParameters ) { throw new InsufficientParametersException(totalParameters, values) } - val promise = Promise[QueryResult] + val promise = Promise[QueryResult]() this.setQueryPromise(promise) this.connectionHandler.sendPreparedStatement(query, values) + addTimeout(promise,configuration.queryTimeout) promise.future } diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoder.scala index 5684c118..d579a0ec 100644 --- a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoder.scala +++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoder.scala @@ -31,7 +31,7 @@ class BinaryRowDecoder { //import BinaryRowDecoder._ - def decode(buffer: ByteBuf, columns: Seq[ColumnDefinitionMessage]): IndexedSeq[Any] = { + def decode(buffer: ByteBuf, columns: Seq[ColumnDefinitionMessage]): Array[Any] = { //log.debug("columns are {} - {}", buffer.readableBytes(), columns) //log.debug( "decoding row\n{}", MySQLHelper.dumpAsHex(buffer)) @@ -79,7 +79,7 @@ class BinaryRowDecoder { throw new BufferNotFullyConsumedException(buffer) } - row + row.toArray } } \ No newline at end of file diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLConnectionHandler.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLConnectionHandler.scala index eb2794a0..00daf706 100644 --- a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLConnectionHandler.scala +++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLConnectionHandler.scala @@ -18,6 +18,7 @@ package com.github.mauricio.async.db.mysql.codec import java.net.InetSocketAddress import java.nio.ByteBuffer +import java.util.concurrent.TimeUnit import com.github.mauricio.async.db.Configuration import com.github.mauricio.async.db.exceptions.DatabaseException @@ -38,6 +39,7 @@ import scala.Some import scala.annotation.switch import scala.collection.mutable.{ArrayBuffer, HashMap} import scala.concurrent._ +import scala.concurrent.duration.Duration class MySQLConnectionHandler( configuration: Configuration, @@ -320,17 +322,18 @@ class MySQLConnectionHandler( } private def writeAndHandleError( message : Any ) : ChannelFuture = { - if ( this.currentContext.channel().isActive ) { - val future = this.currentContext.writeAndFlush(message) + val res = this.currentContext.writeAndFlush(message) - future.onFailure { + res.onFailure { case e : Throwable => handleException(e) } - future + res } else { - throw new DatabaseException("This channel is not active and can't take messages") + val error = new DatabaseException("This channel is not active and can't take messages") + handleException(error) + this.currentContext.channel().newFailedFuture(error) } } @@ -352,4 +355,10 @@ class MySQLConnectionHandler( } } + def schedule(block: => Unit, duration: Duration): Unit = { + this.currentContext.channel().eventLoop().schedule(new Runnable { + override def run(): Unit = block + }, duration.toMillis, TimeUnit.MILLISECONDS) + } + } diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactory.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactory.scala index 83791366..273e76af 100644 --- a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactory.scala +++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactory.scala @@ -21,9 +21,8 @@ import com.github.mauricio.async.db.pool.ObjectFactory import com.github.mauricio.async.db.mysql.MySQLConnection import scala.util.Try import scala.concurrent.Await -import scala.concurrent.duration._ import com.github.mauricio.async.db.util.Log -import com.github.mauricio.async.db.exceptions.{ConnectionStillRunningQueryException, ConnectionNotConnectedException} +import com.github.mauricio.async.db.exceptions.{ConnectionTimeoutedException, ConnectionStillRunningQueryException, ConnectionNotConnectedException} object MySQLConnectionFactory { final val log = Log.get[MySQLConnectionFactory] @@ -90,7 +89,9 @@ class MySQLConnectionFactory( configuration : Configuration ) extends ObjectFact */ def validate(item: MySQLConnection): Try[MySQLConnection] = { Try{ - + if ( item.isTimeouted ) { + throw new ConnectionTimeoutedException(item) + } if ( !item.isConnected ) { throw new ConnectionNotConnectedException(item) } diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConnectionHelper.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConnectionHelper.scala index 771fe1e3..8ace95e7 100644 --- a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConnectionHelper.scala +++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConnectionHelper.scala @@ -115,6 +115,19 @@ trait ConnectionHelper { } + def withConfigurablePool[T]( configuration : Configuration )( fn : (ConnectionPool[MySQLConnection]) => T ) : T = { + + val factory = new MySQLConnectionFactory(configuration) + val pool = new ConnectionPool[MySQLConnection](factory, PoolConfiguration.Default) + + try { + fn(pool) + } finally { + awaitFuture( pool.close ) + } + + } + def withConnection[T]( fn : (MySQLConnection) => T ) : T = withConfigurableConnection(this.defaultConfiguration)(fn) diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/QueryTimeoutSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/QueryTimeoutSpec.scala new file mode 100644 index 00000000..65827432 --- /dev/null +++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/QueryTimeoutSpec.scala @@ -0,0 +1,80 @@ +/* + * Copyright 2013 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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.github.mauricio.async.db.mysql + +import java.util.concurrent.TimeoutException +import com.github.mauricio.async.db.Configuration +import org.specs2.execute.{AsResult, Success, ResultExecution} +import org.specs2.mutable.Specification +import scala.concurrent.Await +import scala.concurrent.duration._ + +class QueryTimeoutSpec extends Specification with ConnectionHelper { + implicit def unitAsResult: AsResult[Unit] = new AsResult[Unit] { + def asResult(r: =>Unit) = + ResultExecution.execute(r)(_ => Success()) + } + "Simple query with 1 nanosec timeout" in { + withConfigurablePool(shortTimeoutConfiguration) { + pool => { + val connection = Await.result(pool.take, Duration(10,SECONDS)) + connection.isTimeouted === false + connection.isConnected === true + val queryResultFuture = connection.sendQuery("select sleep(1)") + Await.result(queryResultFuture, Duration(10,SECONDS)) must throwA[TimeoutException]() + connection.isTimeouted === true + Await.ready(pool.giveBack(connection), Duration(10,SECONDS)) + pool.availables.count(_ == connection) === 0 // connection removed from pool + // we do not know when the connection will be closed. + } + } + } + + "Simple query with 5 sec timeout" in { + withConfigurablePool(longTimeoutConfiguration) { + pool => { + val connection = Await.result(pool.take, Duration(10,SECONDS)) + connection.isTimeouted === false + connection.isConnected === true + val queryResultFuture = connection.sendQuery("select sleep(1)") + Await.result(queryResultFuture, Duration(10,SECONDS)).rows.get.size === 1 + connection.isTimeouted === false + connection.isConnected === true + Await.ready(pool.giveBack(connection), Duration(10,SECONDS)) + pool.availables.count(_ == connection) === 1 // connection returned to pool + } + } + } + + def shortTimeoutConfiguration = new Configuration( + "mysql_async", + "localhost", + port = 3306, + password = Some("root"), + database = Some("mysql_async_tests"), + queryTimeout = Some(Duration(1,NANOSECONDS)) + ) + + def longTimeoutConfiguration = new Configuration( + "mysql_async", + "localhost", + port = 3306, + password = Some("root"), + database = Some("mysql_async_tests"), + queryTimeout = Some(Duration(5,SECONDS)) + ) +} diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala new file mode 100644 index 00000000..3d68563b --- /dev/null +++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala @@ -0,0 +1,132 @@ +/* + * Copyright 2013 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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.github.mauricio.async.db.mysql + +import com.github.mauricio.async.db.ResultSet +import com.github.mauricio.async.db.util.FutureUtils._ +import org.specs2.mutable.Specification + +class StoredProceduresSpec extends Specification with ConnectionHelper { + + "connection" should { + + "be able to execute create stored procedure" in { + withConnection { + connection => + val future = for( + drop <- connection.sendQuery("DROP PROCEDURE IF exists helloWorld;"); + create <- connection.sendQuery( + """ + CREATE PROCEDURE helloWorld(OUT param1 VARCHAR(20)) + BEGIN + SELECT 'hello' INTO param1; + END + """ + ) + ) yield create + awaitFuture(future).statusMessage === "" + } + } + + "be able to call stored procedure" in { + withConnection { + connection => + val future = for( + drop <- connection.sendQuery("DROP PROCEDURE IF exists constTest;"); + create <- connection.sendQuery( + """ + CREATE PROCEDURE constTest(OUT param INT) + BEGIN + SELECT 125 INTO param; + END + """ + ); + call <- connection.sendQuery("CALL constTest(@arg)"); + arg <- connection.sendQuery("SELECT @arg") + ) yield arg + val result: Option[ResultSet] = awaitFuture(future).rows + result.isDefined === true + val rows = result.get + rows.size === 1 + rows(0)(rows.columnNames.head) === 125 + } + } + + "be able to call stored procedure with input parameter" in { + withConnection { + connection => + val future = for( + drop <- connection.sendQuery("DROP PROCEDURE IF exists addTest;"); + create <- connection.sendQuery( + """ + CREATE PROCEDURE addTest(IN a INT, IN b INT, OUT sum INT) + BEGIN + SELECT a+b INTO sum; + END + """ + ); + call <- connection.sendQuery("CALL addTest(132, 245, @sm)"); + res <- connection.sendQuery("SELECT @sm") + ) yield res + val result: Option[ResultSet] = awaitFuture(future).rows + result.isDefined === true + val rows = result.get + rows.size === 1 + rows(0)(rows.columnNames.head) === 377 + } + } + + "be able to remove stored procedure" in { + withConnection { + connection => + val createResult: Option[ResultSet] = awaitFuture( + for( + drop <- connection.sendQuery("DROP PROCEDURE IF exists remTest;"); + create <- connection.sendQuery( + """ + CREATE PROCEDURE remTest(OUT cnst INT) + BEGIN + SELECT 987 INTO cnst; + END + """ + ); + routine <- connection.sendQuery( + """ + SELECT routine_name FROM INFORMATION_SCHEMA.ROUTINES WHERE routine_name="remTest" + """ + ) + ) yield routine + ).rows + createResult.isDefined === true + createResult.get.size === 1 + createResult.get(0)("routine_name") === "remTest" + val removeResult: Option[ResultSet] = awaitFuture( + for( + drop <- connection.sendQuery("DROP PROCEDURE remTest;"); + routine <- connection.sendQuery( + """ + SELECT routine_name FROM INFORMATION_SCHEMA.ROUTINES WHERE routine_name="remTest" + """ + ) + ) yield routine + ).rows + removeResult.isDefined === true + removeResult.get.isEmpty === true + } + } + } +} \ No newline at end of file diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala index 0312f0d5..0ef2f86b 100644 --- a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala +++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala @@ -1,14 +1,28 @@ package com.github.mauricio.async.db.mysql +import java.util.UUID +import java.util.concurrent.TimeUnit + import org.specs2.mutable.Specification import com.github.mauricio.async.db.util.FutureUtils.awaitFuture import com.github.mauricio.async.db.mysql.exceptions.MySQLException import com.github.mauricio.async.db.Connection +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future} +import scala.util.{Success, Failure} + +object TransactionSpec { + + val BrokenInsert = """INSERT INTO users (id, name) VALUES (1, 'Maurício Aragão')""" + val InsertUser = """INSERT INTO users (name) VALUES (?)""" + val TransactionInsert = "insert into transaction_test (id) values (?)" + +} + class TransactionSpec extends Specification with ConnectionHelper { - val brokenInsert = """INSERT INTO users (id, name) VALUES (1, 'Maurício Aragão')""" - val insertUser = """INSERT INTO users (name) VALUES (?)""" + import TransactionSpec._ "connection in transaction" should { @@ -42,7 +56,7 @@ class TransactionSpec extends Specification with ConnectionHelper { val future = connection.inTransaction { c => - c.sendQuery(this.insert).flatMap(r => c.sendQuery(brokenInsert)) + c.sendQuery(this.insert).flatMap(r => c.sendQuery(BrokenInsert)) } try { @@ -77,7 +91,7 @@ class TransactionSpec extends Specification with ConnectionHelper { val future = pool.inTransaction { c => connection = c - c.sendQuery(this.brokenInsert) + c.sendQuery(BrokenInsert) } try { @@ -97,6 +111,38 @@ class TransactionSpec extends Specification with ConnectionHelper { } + "runs commands for a transaction in a single connection" in { + + val id = UUID.randomUUID().toString + + withPool { + pool => + val operations = pool.inTransaction { + connection => + connection.sendPreparedStatement(TransactionInsert, List(id)).flatMap { + result => + connection.sendPreparedStatement(TransactionInsert, List(id)).map { + failure => + List(result, failure) + } + } + } + + Await.ready(operations, Duration(5, TimeUnit.SECONDS)) + + operations.value.get match { + case Success(e) => failure("should not have executed") + case Failure(e) => { + e.asInstanceOf[MySQLException].errorMessage.errorCode === 1062 + executePreparedStatement(pool, "select * from transaction_test where id = ?", id).rows.get.size === 0 + success("ok") + } + } + + } + + } + } } diff --git a/netty b/netty index 087db82e..4c482c12 160000 --- a/netty +++ b/netty @@ -1 +1 @@ -Subproject commit 087db82e787883c467938aadd9c4cf9aae798c67 +Subproject commit 4c482c1215ef4c0c1d2d25f9f7bd24cb4587a474 diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala index bdbe8827..d073178b 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala @@ -20,6 +20,7 @@ import com.github.mauricio.async.db.QueryResult import com.github.mauricio.async.db.column.{ColumnEncoderRegistry, ColumnDecoderRegistry} import com.github.mauricio.async.db.exceptions.{InsufficientParametersException, ConnectionStillRunningQueryException} import com.github.mauricio.async.db.general.MutableResultSet +import com.github.mauricio.async.db.pool.TimeoutScheduler import com.github.mauricio.async.db.postgresql.codec.{PostgreSQLConnectionDelegate, PostgreSQLConnectionHandler} import com.github.mauricio.async.db.postgresql.column.{PostgreSQLColumnDecoderRegistry, PostgreSQLColumnEncoderRegistry} import com.github.mauricio.async.db.postgresql.exceptions._ @@ -45,10 +46,11 @@ class PostgreSQLConnection encoderRegistry: ColumnEncoderRegistry = PostgreSQLColumnEncoderRegistry.Instance, decoderRegistry: ColumnDecoderRegistry = PostgreSQLColumnDecoderRegistry.Instance, group : EventLoopGroup = NettyUtils.DefaultEventLoopGroup, - executionContext : ExecutionContext = ExecutorServiceUtils.CachedExecutionContext + implicit val executionContext : ExecutionContext = ExecutorServiceUtils.CachedExecutionContext ) extends PostgreSQLConnectionDelegate - with Connection { + with Connection + with TimeoutScheduler { import PostgreSQLConnection._ @@ -63,7 +65,6 @@ class PostgreSQLConnection private final val currentCount = Counter.incrementAndGet() private final val preparedStatementsCounter = new AtomicInteger() - private final implicit val internalExecutionContext = executionContext private val parameterStatus = new scala.collection.mutable.HashMap[String, String]() private val parsedStatements = new scala.collection.mutable.HashMap[String, PreparedStatementHolder]() @@ -80,6 +81,7 @@ class PostgreSQLConnection private var queryResult: Option[QueryResult] = None + override def eventLoopGroup : EventLoopGroup = group def isReadyForQuery: Boolean = this.queryPromise.isEmpty def connect: Future[Connection] = { @@ -91,6 +93,7 @@ class PostgreSQLConnection } override def disconnect: Future[Connection] = this.connectionHandler.disconnect.map( c => this ) + override def onTimeout = disconnect override def isConnected: Boolean = this.connectionHandler.isConnected @@ -103,7 +106,7 @@ class PostgreSQLConnection this.setQueryPromise(promise) write(new QueryMessage(query)) - + addTimeout(promise,configuration.queryTimeout) promise.future } @@ -130,7 +133,7 @@ class PostgreSQLConnection holder.prepared = true new PreparedStatementOpeningMessage(holder.statementId, holder.realQuery, values, this.encoderRegistry) }) - + addTimeout(promise,configuration.queryTimeout) promise.future } diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala index 7f15b0f6..29c6b736 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala @@ -63,6 +63,7 @@ object ColumnTypes { final val MoneyArray = 791 final val NameArray = 1003 + final val UUID = 2950 final val UUIDArray = 2951 final val XMLArray = 143 diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala index 285f74ba..ddf7afdc 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala @@ -45,6 +45,7 @@ class PostgreSQLColumnDecoderRegistry( charset : Charset = CharsetUtil.UTF_8 ) e private final val timeArrayDecoder = new ArrayDecoder(TimeEncoderDecoder.Instance) private final val timeWithTimestampArrayDecoder = new ArrayDecoder(TimeWithTimezoneEncoderDecoder) private final val intervalArrayDecoder = new ArrayDecoder(PostgreSQLIntervalEncoderDecoder) + private final val uuidArrayDecoder = new ArrayDecoder(UUIDEncoderDecoder) override def decode(kind: ColumnData, value: ByteBuf, charset: Charset): Any = { decoderFor(kind.dataType).decode(kind, value, charset) @@ -108,7 +109,8 @@ class PostgreSQLColumnDecoderRegistry( charset : Charset = CharsetUtil.UTF_8 ) e case MoneyArray => this.stringArrayDecoder case NameArray => this.stringArrayDecoder - case UUIDArray => this.stringArrayDecoder + case UUID => UUIDEncoderDecoder + case UUIDArray => this.uuidArrayDecoder case XMLArray => this.stringArrayDecoder case ByteA => ByteArrayEncoderDecoder diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala index 83e7e1ac..bb4bb603 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala @@ -52,6 +52,8 @@ class PostgreSQLColumnEncoderRegistry extends ColumnEncoderRegistry { classOf[BigDecimal] -> (BigDecimalEncoderDecoder -> ColumnTypes.Numeric), classOf[java.math.BigDecimal] -> (BigDecimalEncoderDecoder -> ColumnTypes.Numeric), + classOf[java.util.UUID] -> (UUIDEncoderDecoder -> ColumnTypes.UUID), + classOf[LocalDate] -> ( DateEncoderDecoder -> ColumnTypes.Date ), classOf[LocalDateTime] -> (TimestampEncoderDecoder.Instance -> ColumnTypes.Timestamp), classOf[DateTime] -> (TimestampWithTimezoneEncoderDecoder -> ColumnTypes.TimestampWithTimezone), diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/pool/PostgreSQLConnectionFactory.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/pool/PostgreSQLConnectionFactory.scala index e8fa9180..6be74203 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/pool/PostgreSQLConnectionFactory.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/pool/PostgreSQLConnectionFactory.scala @@ -17,6 +17,7 @@ package com.github.mauricio.async.db.postgresql.pool import com.github.mauricio.async.db.Configuration +import com.github.mauricio.async.db.exceptions.ConnectionTimeoutedException import com.github.mauricio.async.db.pool.ObjectFactory import com.github.mauricio.async.db.postgresql.PostgreSQLConnection import com.github.mauricio.async.db.util.Log @@ -69,6 +70,9 @@ class PostgreSQLConnectionFactory( def validate( item : PostgreSQLConnection ) : Try[PostgreSQLConnection] = { Try { + if ( item.isTimeouted ) { + throw new ConnectionTimeoutedException(item) + } if ( !item.isConnected || item.hasRecentError ) { throw new ClosedChannelException() } diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala index 7e0713cc..a7998a0c 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala @@ -14,23 +14,22 @@ * under the License. */ -package com.github.mauricio.postgresql +package com.github.mauricio.async.db.postgresql import java.nio.ByteBuffer -import com.github.mauricio.async.db.column.{TimestampEncoderDecoder, TimeEncoderDecoder, DateEncoderDecoder} +import com.github.mauricio.async.db.column.{DateEncoderDecoder, TimeEncoderDecoder, TimestampEncoderDecoder} import com.github.mauricio.async.db.exceptions.UnsupportedAuthenticationMethodException -import com.github.mauricio.async.db.postgresql.exceptions.{QueryMustNotBeNullOrEmptyException, GenericDatabaseException} +import com.github.mauricio.async.db.postgresql.exceptions.{GenericDatabaseException, QueryMustNotBeNullOrEmptyException} import com.github.mauricio.async.db.postgresql.messages.backend.InformationMessage -import com.github.mauricio.async.db.postgresql.{PostgreSQLConnection, DatabaseTestHelper} import com.github.mauricio.async.db.util.Log import com.github.mauricio.async.db.{Configuration, QueryResult, Connection} import com.github.mauricio.netty.buffer.Unpooled import concurrent.{Future, Await} import org.specs2.mutable.Specification -import scala.concurrent.ExecutionContext.Implicits.global + import scala.concurrent.duration._ -import org.joda.time.LocalDateTime +import scala.concurrent.{Await, Future} object PostgreSQLConnectionSpec { val log = Log.get[PostgreSQLConnectionSpec] @@ -285,16 +284,12 @@ class PostgreSQLConnectionSpec extends Specification with DatabaseTestHelper { try { withHandler(configuration, { handler => - executeQuery(handler, "SELECT 0") - throw new IllegalStateException("should not have come here") + val result = executeQuery(handler, "SELECT 0") + throw new IllegalStateException("should not have arrived") }) } catch { - case e: GenericDatabaseException => { + case e: GenericDatabaseException => e.errorMessage.fields(InformationMessage.Routine) === "auth_failed" - } - case e: Exception => { - throw new IllegalStateException("should not have come here") - } } } diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PreparedStatementSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PreparedStatementSpec.scala index 20c645cc..6fd7d9a6 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PreparedStatementSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PreparedStatementSpec.scala @@ -20,7 +20,7 @@ import org.specs2.mutable.Specification import org.joda.time.LocalDate import com.github.mauricio.async.db.util.Log import com.github.mauricio.async.db.exceptions.InsufficientParametersException -import java.util.Date +import java.util.UUID import com.github.mauricio.async.db.postgresql.exceptions.GenericDatabaseException class PreparedStatementSpec extends Specification with DatabaseTestHelper { @@ -282,6 +282,61 @@ class PreparedStatementSpec extends Specification with DatabaseTestHelper { } } + "support UUID" in { + if ( System.getenv("TRAVIS") == null ) { + withHandler { + handler => + val create = """create temp table uuids + |( + |id bigserial primary key, + |my_id uuid + |);""".stripMargin + + val insert = "INSERT INTO uuids (my_id) VALUES (?) RETURNING id" + val select = "SELECT * FROM uuids" + + val uuid = UUID.randomUUID() + + executeDdl(handler, create) + executePreparedStatement(handler, insert, Array(uuid) ) + val result = executePreparedStatement(handler, select).rows.get + + result(0)("my_id").asInstanceOf[UUID] === uuid + } + success + } else { + pending + } + } + + "support UUID array" in { + if ( System.getenv("TRAVIS") == null ) { + withHandler { + handler => + val create = """create temp table uuids + |( + |id bigserial primary key, + |my_id uuid[] + |);""".stripMargin + + val insert = "INSERT INTO uuids (my_id) VALUES (?) RETURNING id" + val select = "SELECT * FROM uuids" + + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + + executeDdl(handler, create) + executePreparedStatement(handler, insert, Array(Array(uuid1, uuid2)) ) + val result = executePreparedStatement(handler, select).rows.get + + result(0)("my_id").asInstanceOf[Seq[UUID]] === Seq(uuid1, uuid2) + } + success + } else { + pending + } + } + } } diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TimeAndDateSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TimeAndDateSpec.scala index e671a5b4..67e7b877 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TimeAndDateSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TimeAndDateSpec.scala @@ -35,7 +35,7 @@ class TimeAndDateSpec extends Specification with DatabaseTestHelper { )""" executeDdl(handler, create) - executeQuery(handler, "INSERT INTO messages (moment) VALUES ('04:05:06')") + executePreparedStatement(handler, "INSERT INTO messages (moment) VALUES (?)", Array[Any](new LocalTime(4, 5, 6))) val rows = executePreparedStatement(handler, "select * from messages").rows.get @@ -60,7 +60,7 @@ class TimeAndDateSpec extends Specification with DatabaseTestHelper { )""" executeDdl(handler, create) - executeQuery(handler, "INSERT INTO messages (moment) VALUES ('04:05:06.134')") + executePreparedStatement(handler, "INSERT INTO messages (moment) VALUES (?)", Array[Any](new LocalTime(4, 5, 6, 134))) val rows = executePreparedStatement(handler, "select * from messages").rows.get @@ -200,6 +200,22 @@ class TimeAndDateSpec extends Specification with DatabaseTestHelper { } + "handle sending a LocalDateTime and return a LocalDateTime for a timestamp without timezone column" in { + + withTimeHandler { + conn => + val date1 = new LocalDateTime(2190319) + + await(conn.sendPreparedStatement("CREATE TEMP TABLE TEST(T TIMESTAMP)")) + await(conn.sendPreparedStatement("INSERT INTO TEST(T) VALUES(?)", Seq(date1))) + val result = await(conn.sendPreparedStatement("SELECT T FROM TEST")) + val date2 = result.rows.get.head(0) + + date2 === date1 + } + + } + "handle sending a date with timezone and retrieving the date with the same time zone" in { withTimeHandler { diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala index 02295b16..b71ebe65 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala @@ -16,12 +16,21 @@ package com.github.mauricio.async.db.postgresql.pool +import java.util.UUID + import com.github.mauricio.async.db.pool.{ConnectionPool, PoolConfiguration} +import com.github.mauricio.async.db.postgresql.exceptions.GenericDatabaseException import com.github.mauricio.async.db.postgresql.{PostgreSQLConnection, DatabaseTestHelper} import org.specs2.mutable.Specification +object ConnectionPoolSpec { + val Insert = "insert into transaction_test (id) values (?)" +} + class ConnectionPoolSpec extends Specification with DatabaseTestHelper { + import ConnectionPoolSpec.Insert + "pool" should { "give you a connection when sending statements" in { @@ -51,6 +60,29 @@ class ConnectionPoolSpec extends Specification with DatabaseTestHelper { } } + "runs commands for a transaction in a single connection" in { + + val id = UUID.randomUUID().toString + + withPool { + pool => + val operations = pool.inTransaction { + connection => + connection.sendPreparedStatement(Insert, List(id)).flatMap { + result => + connection.sendPreparedStatement(Insert, List(id)).map { + failure => + List(result, failure) + } + } + } + + await(operations) must throwA[GenericDatabaseException] + + } + + } + } def withPool[R]( fn : (ConnectionPool[PostgreSQLConnection]) => R ) : R = { diff --git a/project/Build.scala b/project/Build.scala index f4e656c5..dc422294 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1,5 +1,5 @@ +import sbt.Keys._ import sbt._ -import Keys._ object ProjectBuild extends Build { @@ -45,18 +45,18 @@ object ProjectBuild extends Build { object Configuration { - val commonVersion = "0.2.17-SNAPSHOT" - val projectScalaVersion = "2.10.4" + val commonVersion = "0.2.18-EmbeddedNetty" + val projectScalaVersion = "2.11.7" val specs2Dependency = "org.specs2" %% "specs2" % "2.3.11" % "test" - val logbackDependency = "ch.qos.logback" % "logback-classic" % "1.0.13" % "test" + val logbackDependency = "ch.qos.logback" % "logback-classic" % "1.1.3" % "test" val commonDependencies = Seq( - "org.slf4j" % "slf4j-api" % "1.7.5", - "joda-time" % "joda-time" % "2.3", + "org.slf4j" % "slf4j-api" % "1.7.12", + "joda-time" % "joda-time" % "2.8.2", "org.joda" % "joda-convert" % "1.5", -// "io.netty" % "netty-all" % "4.0.25.Final", - "org.javassist" % "javassist" % "3.18.1-GA", +// "io.netty" % "netty-all" % "4.0.29.Final", + "org.javassist" % "javassist" % "3.20.0-GA", "commons-logging" % "commons-logging" % "1.1.3" % "optional", "org.jboss.marshalling" % "jboss-marshalling" % "1.3.18.GA" % "optional", "com.jcraft" % "jzlib" % "1.1.2" % "optional", diff --git a/project/build.properties b/project/build.properties index 8ac605a3..d638b4f3 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.2 +sbt.version = 0.13.8 \ No newline at end of file diff --git a/script/prepare_build.sh b/script/prepare_build.sh index a3a4e183..96aa8345 100755 --- a/script/prepare_build.sh +++ b/script/prepare_build.sh @@ -2,6 +2,7 @@ echo "Preparing MySQL configs" mysql -u root -e 'create database mysql_async_tests;' +mysql -u root -e "create table mysql_async_tests.transaction_test (id varchar(255) not null, primary key (id))" mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'mysql_async'@'localhost' IDENTIFIED BY 'root' WITH GRANT OPTION"; mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'mysql_async_old'@'localhost' WITH GRANT OPTION"; mysql -u root -e "UPDATE mysql.user SET Password = OLD_PASSWORD('do_not_use_this'), plugin = 'mysql_old_password' where User = 'mysql_async_old'; flush privileges;"; @@ -12,10 +13,11 @@ echo "preparing postgresql configs" psql -c 'create database netty_driver_test;' -U postgres psql -c 'create database netty_driver_time_test;' -U postgres psql -c "alter database netty_driver_time_test set timezone to 'GMT'" -U postgres +psql -c "create table transaction_test ( id varchar(255) not null, constraint id_unique primary key (id))" -U postgres netty_driver_test psql -c "CREATE USER postgres_md5 WITH PASSWORD 'postgres_md5'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_md5;" -U postgres psql -c "CREATE USER postgres_cleartext WITH PASSWORD 'postgres_cleartext'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_cleartext;" -U postgres psql -c "CREATE USER postgres_kerberos WITH PASSWORD 'postgres_kerberos'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_kerberos;" -U postgres -psql -d "netty_driver_test" -c "CREATE TYPE example_mood AS ENUM ('sad', 'ok', 'happy');" +psql -d "netty_driver_test" -c "CREATE TYPE example_mood AS ENUM ('sad', 'ok', 'happy');" -U postgres sudo chmod 777 /etc/postgresql/9.1/main/pg_hba.conf