diff --git a/src/main/java/com/fasterxml/jackson/core/JsonFactory.java b/src/main/java/com/fasterxml/jackson/core/JsonFactory.java index 07317f1fc6..7eaae033f3 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonFactory.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonFactory.java @@ -19,12 +19,7 @@ import com.fasterxml.jackson.core.json.async.NonBlockingJsonParser; import com.fasterxml.jackson.core.sym.ByteQuadsCanonicalizer; import com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer; -import com.fasterxml.jackson.core.util.BufferRecycler; -import com.fasterxml.jackson.core.util.BufferRecyclerPool; -import com.fasterxml.jackson.core.util.BufferRecyclers; -import com.fasterxml.jackson.core.util.JacksonFeature; -import com.fasterxml.jackson.core.util.JsonGeneratorDecorator; -import com.fasterxml.jackson.core.util.Separators; +import com.fasterxml.jackson.core.util.*; /** * The main factory class of Jackson package, used to configure and @@ -115,8 +110,10 @@ public enum Feature FAIL_ON_SYMBOL_HASH_OVERFLOW(true), /** - * Feature that determines whether we will use {@link BufferRecycler} with - * {@link ThreadLocal} and {@link SoftReference}, for efficient reuse of + * Feature that determines whether we will use a {@link BufferRecyclerPool} + * for allocating and possibly recycling {@link BufferRecycler} or not. + * The default {@link BufferRecyclerPool} implementation uses + * {@link ThreadLocal} and {@link SoftReference} for efficient reuse of * underlying input/output buffers. * This usually makes sense on normal J2SE/J2EE server-side processing; * but may not make sense on platforms where {@link SoftReference} handling @@ -125,6 +122,10 @@ public enum Feature * jackson-core#189 * for a possible case) *

+ * Note that since 2.16 naming here is somewhat misleading as this is used + * to now enable or disable pooling; but the actual pooling implementation + * is configurable and may not be based on {@link ThreadLocal}. + *

* This setting is enabled by default. * * @since 2.6 @@ -370,7 +371,7 @@ public static int collectDefaults() { public JsonFactory() { this((ObjectCodec) null); } public JsonFactory(ObjectCodec oc) { - _bufferRecyclerPool = BufferRecyclers.defaultRecyclerPool(); + _bufferRecyclerPool = BufferRecyclerPool.defaultPool(); _objectCodec = oc; _quoteChar = DEFAULT_QUOTE_CHAR; _streamReadConstraints = StreamReadConstraints.defaults(); @@ -1873,6 +1874,7 @@ protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOExce e.addSuppressed(e2); } } + ctxt.close(); throw e; } } @@ -2151,7 +2153,7 @@ protected JsonGenerator _decorate(JsonGenerator g) { */ public BufferRecycler _getBufferRecycler() { - return _getBufferRecyclerPool().acquireBufferRecycler(this); + return _getBufferRecyclerPool().acquireBufferRecycler(); } /** @@ -2165,7 +2167,7 @@ public BufferRecyclerPool _getBufferRecyclerPool() { // scheme, for cases where it is considered harmful (possibly // on Android, for example) if (!Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING.enabledIn(_factoryFeatures)) { - return BufferRecyclers.nopRecyclerPool(); + return BufferRecyclerPool.nonRecyclingPool(); } return _bufferRecyclerPool; } diff --git a/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java b/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java index f34c655c31..8ee09de195 100644 --- a/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java +++ b/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java @@ -9,7 +9,6 @@ import com.fasterxml.jackson.core.json.JsonReadFeature; import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.core.util.BufferRecyclerPool; -import com.fasterxml.jackson.core.util.BufferRecyclers; import com.fasterxml.jackson.core.util.JsonGeneratorDecorator; /** @@ -142,7 +141,7 @@ protected TSFBuilder(JsonFactory base) protected TSFBuilder(int factoryFeatures, int parserFeatures, int generatorFeatures) { - _bufferRecyclerPool = BufferRecyclers.defaultRecyclerPool(); + _bufferRecyclerPool = BufferRecyclerPool.defaultPool(); _factoryFeatures = factoryFeatures; _streamReadFeatures = parserFeatures; diff --git a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java index 5395e880db..320c8438bd 100644 --- a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java +++ b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java @@ -395,6 +395,7 @@ protected void _checkStdFeatureChanges(int newFeatureFlags, int changedFeatures) // as per [JACKSON-324], do in finally block // Also, internal buffer(s) can now be released as well _releaseBuffers(); + _ioContext.close(); } } } diff --git a/src/main/java/com/fasterxml/jackson/core/io/IOContext.java b/src/main/java/com/fasterxml/jackson/core/io/IOContext.java index 060446c9db..56d0ec1eaf 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/IOContext.java +++ b/src/main/java/com/fasterxml/jackson/core/io/IOContext.java @@ -16,7 +16,7 @@ *

* NOTE: non-final since 2.4, to allow sub-classing. */ -public class IOContext +public class IOContext implements AutoCloseable { /* /********************************************************************** @@ -119,6 +119,8 @@ public class IOContext */ protected char[] _nameCopyBuffer; + private boolean _closed = false; + /* /********************************************************************** /* Life-cycle @@ -458,4 +460,12 @@ private IllegalArgumentException wrongBuf() { // sanity check failed; trying to return different, smaller buffer. return new IllegalArgumentException("Trying to release buffer smaller than original"); } + + @Override + public void close() { + if (!_closed) { + _bufferRecycler.release(); + _closed = true; + } + } } diff --git a/src/main/java/com/fasterxml/jackson/core/io/UTF8Writer.java b/src/main/java/com/fasterxml/jackson/core/io/UTF8Writer.java index b7a47fe7fd..b7561839d1 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/UTF8Writer.java +++ b/src/main/java/com/fasterxml/jackson/core/io/UTF8Writer.java @@ -76,6 +76,7 @@ public void close() illegalSurrogate(code); } } + _context.close(); } @Override diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java b/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java index 2bc37a116e..ad2eaa3fa0 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java @@ -120,6 +120,14 @@ public abstract class JsonGeneratorImpl extends GeneratorBase /********************************************************** */ + @Override + public void close() throws IOException { + if (!isClosed()) { + super.close(); + _ioContext.close(); + } + } + @SuppressWarnings("deprecation") public JsonGeneratorImpl(IOContext ctxt, int features, ObjectCodec codec) { @@ -237,6 +245,23 @@ public JacksonFeatureSet getWriteCapabilities() { return JSON_WRITE_CAPABILITIES; } + /* + /********************************************************** + /* Misc other accessors + /********************************************************** + */ + + /** + * Accessor for use by {@code jackson-core} itself (tests in particular). + * + * @return {@link IOContext} in use by this generator + * + * @since 2.16 + */ + public IOContext ioContext() { + return _ioContext; + } + /* /********************************************************** /* Shared helper methods diff --git a/src/main/java/com/fasterxml/jackson/core/util/BufferRecycler.java b/src/main/java/com/fasterxml/jackson/core/util/BufferRecycler.java index a17532a683..136d9498df 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/BufferRecycler.java +++ b/src/main/java/com/fasterxml/jackson/core/util/BufferRecycler.java @@ -1,17 +1,21 @@ package com.fasterxml.jackson.core.util; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReferenceArray; /** * This is a small utility class, whose main functionality is to allow - * simple reuse of raw byte/char buffers. It is usually used through - * ThreadLocal member of the owning class pointing to - * instance of this class through a SoftReference. The - * end result is a low-overhead GC-cleanable recycling: hopefully + * simple reuse of raw byte/char buffers. It is usually allocated through + * {@link BufferRecyclerPool} (starting with 2.16): multiple pool + * implementations exists. + * The default pool implementation uses + * {@code ThreadLocal} combined with {@code SoftReference}. + * The end result is a low-overhead GC-cleanable recycling: hopefully * ideal for use by stream readers. *

* Rewritten in 2.10 to be thread-safe (see [jackson-core#479] for details), - * to not rely on {@code ThreadLocal} access. + * to not rely on {@code ThreadLocal} access.
+ * Rewritten in 2.16 to work with {@link BufferRecyclerPool} abstraction. */ public class BufferRecycler { @@ -82,6 +86,8 @@ public class BufferRecycler // Note: changed from simple array in 2.10: protected final AtomicReferenceArray _charBuffers; + private BufferRecyclerPool _pool; + /* /********************************************************** /* Construction @@ -189,4 +195,36 @@ protected int charBufferLength(int ix) { protected byte[] balloc(int size) { return new byte[size]; } protected char[] calloc(int size) { return new char[size]; } + + /** + * Method called by owner of this recycler instance, to provide reference to + * {@link BufferRecyclerPool} into which instance is to be released (if any) + * + * @since 2.16 + */ + BufferRecycler withPool(BufferRecyclerPool pool) { + if (this._pool != null) { + throw new IllegalStateException("BufferRecycler already linked to pool: "+pool); + } + // assign to pool to which this BufferRecycler belongs in order to release it + // to the same pool when the work will be completed + _pool = Objects.requireNonNull(pool); + return this; + } + + /** + * Method called when owner of this recycler no longer wishes use it; this should + * return it to pool passed via {@code withPool()} (if any). + * + * @since 2.16 + */ + public void release() { + if (_pool != null) { + BufferRecyclerPool tmpPool = _pool; + // nullify the reference to the pool in order to avoid the risk of releasing + // the same BufferRecycler more than once, thus compromising the pool integrity + _pool = null; + tmpPool.releaseBufferRecycler(this); + } + } } diff --git a/src/main/java/com/fasterxml/jackson/core/util/BufferRecyclerPool.java b/src/main/java/com/fasterxml/jackson/core/util/BufferRecyclerPool.java index cd2276b0c2..672f5f352b 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/BufferRecyclerPool.java +++ b/src/main/java/com/fasterxml/jackson/core/util/BufferRecyclerPool.java @@ -1,17 +1,465 @@ package com.fasterxml.jackson.core.util; -import com.fasterxml.jackson.core.TokenStreamFactory; +import java.io.Serializable; +import java.util.Deque; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicReference; /** * Interface for entity that controls creation and possible reuse of {@link BufferRecycler} * instances used for recycling of underlying input/output buffers. + *

+ * Different pool implementations use different strategies on retaining + * recyclers for reuse. For example we have: + *

+ * + *

+ * Default implementations are also included as nested classes. * * @since 2.16 */ -public interface BufferRecyclerPool - extends java.io.Serializable +public interface BufferRecyclerPool extends Serializable { - public BufferRecycler acquireBufferRecycler(TokenStreamFactory forFactory); + /** + * Method called to acquire {@link BufferRecycler}; possibly + * (but necessarily) a pooled recycler instance (depends on implementation + * and pool state). + * + * @return {@link BufferRecycler} for caller to use; caller expected + * to call {@link #releaseBufferRecycler} after it is done using recycler. + */ + BufferRecycler acquireBufferRecycler(); + + /** + * Method that should be called when previously acquired (see {@link #acquireBufferRecycler}) + * recycler instances is no longer needed; this lets pool to take ownership + * for possible reuse. + * + * @param recycler + */ + void releaseBufferRecycler(BufferRecycler recycler); + + /** + * @return the default {@link BufferRecyclerPool} implementation + * which is the thread local based one: + * basically alias to {@link #threadLocalPool()}). + */ + static BufferRecyclerPool defaultPool() { + return threadLocalPool(); + } + + /** + * @return Globally shared instance of {@link ThreadLocalPool}; same as calling + * {@link ThreadLocalPool#shared()}. + */ + static BufferRecyclerPool threadLocalPool() { + return ThreadLocalPool.shared(); + } + + /** + * @return Globally shared instance of {@link NonRecyclingPool}; same as calling + * {@link NonRecyclingPool#shared()}. + */ + static BufferRecyclerPool nonRecyclingPool() { + return NonRecyclingPool.shared(); + } + + /* + /********************************************************************** + /* Default BufferRecyclerPool implementations + /********************************************************************** + */ + + /** + * Default {@link BufferRecyclerPool} implementation that uses + * {@link ThreadLocal} for recycling instances. {@link BufferRecycler} + * instances are stored using {@link java.lang.ref.SoftReference}s so that + * they may be Garbage Collected as needed by JVM. + *

+ * Note that this implementation may not work well on platforms where + * {@link java.lang.ref.SoftReference}s are not well supported (like + * Android), or on platforms where {@link java.lang.Thread}s are not + * long-living or reused (like Project Loom). + */ + class ThreadLocalPool implements BufferRecyclerPool + { + private static final long serialVersionUID = 1L; + + private static final BufferRecyclerPool GLOBAL = new ThreadLocalPool(); + + /** + * Accessor for the global, shared instance of {@link ThreadLocal}-based + * pool: due to its nature it is essentially Singleton as there can only + * be a single recycled {@link BufferRecycler} per thread. + * + * @return Shared pool instance + */ + public static BufferRecyclerPool shared() { + return GLOBAL; + } + + // No instances beyond shared one should be constructed + private ThreadLocalPool() { } + + // // // JDK serialization support + + protected Object readResolve() { return GLOBAL; } + + // // // Actual API implementation + + @SuppressWarnings("deprecation") + @Override + public BufferRecycler acquireBufferRecycler() { + return BufferRecyclers.getBufferRecycler(); + } + + @Override + public void releaseBufferRecycler(BufferRecycler recycler) { + ; // nothing to do, relies on ThreadLocal + } + } + + /** + * {@link BufferRecyclerPool} implementation that does not use + * any pool but simply creates new instances when necessary. + */ + class NonRecyclingPool implements BufferRecyclerPool + { + private static final long serialVersionUID = 1L; + + private static final BufferRecyclerPool GLOBAL = new NonRecyclingPool(); + + // No instances beyond shared one should be constructed + private NonRecyclingPool() { } + + /** + * Accessor for the shared singleton instance; due to implementation having no state + * this is preferred over creating instances. + * + * @return Shared pool instance + */ + public static BufferRecyclerPool shared() { + return GLOBAL; + } + + // // // JDK serialization support + + protected Object readResolve() { return GLOBAL; } + + // // // Actual API implementation + + @Override + public BufferRecycler acquireBufferRecycler() { + // Could link back to this pool as marker? For now just leave back-ref empty + return new BufferRecycler(); + } + + @Override + public void releaseBufferRecycler(BufferRecycler recycler) { + ; // nothing to do, there is no underlying pool + } + } + + /** + * Intermediate base class for instances that are stateful and require + * special handling with respect to JDK serialization, to retain + * "global" reference distinct from non-shared ones. + */ + abstract class StatefulImplBase implements BufferRecyclerPool { + private static final long serialVersionUID = 1L; + + protected final static int SERIALIZATION_SHARED = -1; + + protected final static int SERIALIZATION_NON_SHARED = 1; + + /** + * Value that indicates basic aspects of pool for JDK serialization; + * either marker for shared/non-shared, or possibly bounded size; + * depends on sub-class. + */ + protected final int _serialization; + + protected StatefulImplBase(int serialization) { + _serialization = serialization; + } + + protected Optional _resolveToShared(BufferRecyclerPool shared) { + if (_serialization == SERIALIZATION_SHARED) { + return Optional.of(shared); + } + return Optional.empty(); + } + } + + /** + * {@link BufferRecyclerPool} implementation that uses + * {@link ConcurrentLinkedDeque} for recycling instances. + *

+ * Pool is unbounded: see {@link BufferRecyclerPool} what this means. + */ + class ConcurrentDequePool extends StatefulImplBase + { + private static final long serialVersionUID = 1L; + + private static final ConcurrentDequePool GLOBAL = new ConcurrentDequePool(SERIALIZATION_SHARED); + + private final transient Deque pool; + + // // // Life-cycle (constructors, factory methods) + + protected ConcurrentDequePool(int serialization) { + super(serialization); + pool = new ConcurrentLinkedDeque<>(); + } + + /** + * Accessor for getting the globally shared singleton instance. + * Note that if you choose to use this instance, + * pool may be shared by many other {@code JsonFactory} instances. + * + * @return Shared pool instance + */ + public static ConcurrentDequePool shared() { + return GLOBAL; + } + + /** + * Accessor for creating and returning a new, non-shared pool instance. + * + * @return Newly constructed, non-shared pool instance + */ + public static ConcurrentDequePool nonShared() { + return new ConcurrentDequePool(SERIALIZATION_NON_SHARED); + } + + // // // JDK serialization support + + /** + * Make sure to re-link to global/shared or non-shared. + */ + protected Object readResolve() { + return _resolveToShared(GLOBAL).orElseGet(() -> nonShared()); + } + + // // // Actual API implementation + + @Override + public BufferRecycler acquireBufferRecycler() { + BufferRecycler bufferRecycler = pool.pollFirst(); + if (bufferRecycler == null) { + bufferRecycler = new BufferRecycler(); + } + return bufferRecycler.withPool(this); + } + + @Override + public void releaseBufferRecycler(BufferRecycler bufferRecycler) { + pool.offerLast(bufferRecycler); + } + } + + /** + * {@link BufferRecyclerPool} implementation that uses + * a lock free linked list for recycling instances. + * Pool is unbounded: see {@link BufferRecyclerPool} for + * details on what this means. + */ + class LockFreePool extends StatefulImplBase + { + private static final long serialVersionUID = 1L; + + /** + * Globally shared pool instance. + */ + private static final LockFreePool GLOBAL = new LockFreePool(SERIALIZATION_SHARED); + + // Needs to be transient to avoid JDK serialization from writing it out + private final transient AtomicReference head; + + // // // Life-cycle (constructors, factory methods) + + private LockFreePool(int serialization) { + super(serialization); + head = new AtomicReference<>(); + } + + /** + * Accessor for getting the globally shared singleton instance. + * Note that if you choose to use this instance, + * pool may be shared by many other {@code JsonFactory} instances. + * + * @return Shared pool instance + */ + public static LockFreePool shared() { + return GLOBAL; + } + + /** + * Accessor for creating and returning a new, non-shared pool instance. + * + * @return Newly constructed, non-shared pool instance + */ + public static LockFreePool nonShared() { + return new LockFreePool(SERIALIZATION_NON_SHARED); + } + + // // // JDK serialization support + + /** + * Make sure to re-link to global/shared or non-shared. + */ + protected Object readResolve() { + return _resolveToShared(GLOBAL).orElseGet(() -> nonShared()); + } + + // // // Actual API implementation + + @Override + public BufferRecycler acquireBufferRecycler() { + return _getRecycler().withPool(this); + } + + private BufferRecycler _getRecycler() { + // This simple lock free algorithm uses an optimistic compareAndSet strategy to + // populate the underlying linked list in a thread-safe way. However, under very + // heavy contention, the compareAndSet could fail multiple times, so it seems a + // reasonable heuristic to limit the number of retries in this situation. + for (int i = 0; i < 3; i++) { + Node currentHead = head.get(); + if (currentHead == null) { + return new BufferRecycler(); + } + if (head.compareAndSet(currentHead, currentHead.next)) { + currentHead.next = null; + return currentHead.value; + } + } + return new BufferRecycler(); + } + + @Override + public void releaseBufferRecycler(BufferRecycler bufferRecycler) { + LockFreePool.Node newHead = new LockFreePool.Node(bufferRecycler); + for (int i = 0; i < 3; i++) { + newHead.next = head.get(); + if (head.compareAndSet(newHead.next, newHead)) { + return; + } + } + } + + private static class Node { + final BufferRecycler value; + LockFreePool.Node next; + + Node(BufferRecycler value) { + this.value = value; + } + } + } + + /** + * {@link BufferRecyclerPool} implementation that uses + * a bounded queue ({@link ArrayBlockingQueue} for recycling instances. + * This is "bounded" pool since it will never hold on to more + * {@link BufferRecycler} instances than its size configuration: + * the default size is {@link BoundedPool#DEFAULT_CAPACITY}. + */ + class BoundedPool extends StatefulImplBase + { + private static final long serialVersionUID = 1L; + + /** + * Default capacity which limits number of recyclers that are ever + * retained for reuse. + */ + public final static int DEFAULT_CAPACITY = 100; + + private static final BoundedPool GLOBAL = new BoundedPool(SERIALIZATION_SHARED); + + private final transient ArrayBlockingQueue pool; + + private final transient int capacity; + + // // // Life-cycle (constructors, factory methods) + + protected BoundedPool(int capacityAsId) { + super(capacityAsId); + capacity = (capacityAsId <= 0) ? DEFAULT_CAPACITY : capacityAsId; + pool = new ArrayBlockingQueue<>(capacity); + } + + /** + * Accessor for getting the globally shared singleton instance. + * Note that if you choose to use this instance, + * pool may be shared by many other {@code JsonFactory} instances. + * + * @return Shared pool instance + */ + public static BoundedPool shared() { + return GLOBAL; + } + + /** + * Accessor for creating and returning a new, non-shared pool instance. + * + * @param capacity Maximum capacity of the pool: must be positive number above zero. + * + * @return Newly constructed, non-shared pool instance + */ + public static BoundedPool nonShared(int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("capacity must be > 0, was: "+capacity); + } + return new BoundedPool(capacity); + } + + // // // JDK serialization support + + /** + * Make sure to re-link to global/shared or non-shared. + */ + protected Object readResolve() { + return _resolveToShared(GLOBAL).orElseGet(() -> nonShared(_serialization)); + } + + // // // Actual API implementation + + @Override + public BufferRecycler acquireBufferRecycler() { + BufferRecycler bufferRecycler = pool.poll(); + if (bufferRecycler == null) { + bufferRecycler = new BufferRecycler(); + } + return bufferRecycler.withPool(this); + } + + @Override + public void releaseBufferRecycler(BufferRecycler bufferRecycler) { + pool.offer(bufferRecycler); + } + + // // // Other methods - public void releaseBufferRecycler(BufferRecycler recycler); + public int capacity() { + return capacity; + } + } } diff --git a/src/main/java/com/fasterxml/jackson/core/util/BufferRecyclers.java b/src/main/java/com/fasterxml/jackson/core/util/BufferRecyclers.java index c7cbfd025a..0e8fffc14b 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/BufferRecyclers.java +++ b/src/main/java/com/fasterxml/jackson/core/util/BufferRecyclers.java @@ -2,7 +2,6 @@ import java.lang.ref.SoftReference; -import com.fasterxml.jackson.core.TokenStreamFactory; import com.fasterxml.jackson.core.io.JsonStringEncoder; /** @@ -18,7 +17,7 @@ public class BufferRecyclers /** * System property that is checked to see if recycled buffers (see {@link BufferRecycler}) * should be tracked, for purpose of forcing release of all such buffers, typically - * during major classloading. + * during major garbage-collection. * * @since 2.9.6 */ @@ -185,66 +184,4 @@ public static void quoteAsJsonText(CharSequence input, StringBuilder output) { public static byte[] quoteAsJsonUTF8(String rawText) { return JsonStringEncoder.getInstance().quoteAsUTF8(rawText); } - - /* - /********************************************************************** - /* Default BufferRecyclerPool implementations - /********************************************************************** - */ - - public static BufferRecyclerPool defaultRecyclerPool() { - return ThreadLocalRecyclerPool.INSTANCE; - } - - public static BufferRecyclerPool nopRecyclerPool() { - return NonRecyclingRecyclerPool.INSTANCE; - } - - /** - * Default {@link BufferRecyclerPool} implementation that uses - * {@link ThreadLocal} for recycling instances. - * - * @since 2.16 - */ - public static class ThreadLocalRecyclerPool - implements BufferRecyclerPool - { - private static final long serialVersionUID = 1L; - - public final static ThreadLocalRecyclerPool INSTANCE = new ThreadLocalRecyclerPool(); - - @Override - public BufferRecycler acquireBufferRecycler(TokenStreamFactory forFactory) { - return getBufferRecycler(); - } - - @Override - public void releaseBufferRecycler(BufferRecycler recycler) { - ; // nothing to do, relies on ThreadLocal - } - } - - /** - * {@link BufferRecyclerPool} implementation that does not use - * any pool but simply creates new instances when necessary. - * - * @since 2.16 - */ - public static class NonRecyclingRecyclerPool - implements BufferRecyclerPool - { - private static final long serialVersionUID = 1L; - - public final static ThreadLocalRecyclerPool INSTANCE = new ThreadLocalRecyclerPool(); - - @Override - public BufferRecycler acquireBufferRecycler(TokenStreamFactory forFactory) { - return new BufferRecycler(); - } - - @Override - public void releaseBufferRecycler(BufferRecycler recycler) { - ; // nothing to do, relies on ThreadLocal - } - } } diff --git a/src/test/java/com/fasterxml/jackson/core/TestJDKSerializability.java b/src/test/java/com/fasterxml/jackson/core/TestJDKSerializability.java index 951748ca62..d03740d152 100644 --- a/src/test/java/com/fasterxml/jackson/core/TestJDKSerializability.java +++ b/src/test/java/com/fasterxml/jackson/core/TestJDKSerializability.java @@ -3,6 +3,7 @@ import java.io.*; import com.fasterxml.jackson.core.io.ContentReference; +import com.fasterxml.jackson.core.util.BufferRecyclerPool; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; /** @@ -105,6 +106,48 @@ public void testSourceReference() throws Exception assertSame(ref2, ContentReference.unknown()); } + /* + /********************************************************************** + /* Other entities + /********************************************************************** + */ + + public void testRecyclerPools() throws Exception + { + // First: shared/global pools that will always remain/become globally + // shared instances + _testRecyclerPoolGlobal(BufferRecyclerPool.nonRecyclingPool()); + _testRecyclerPoolGlobal(BufferRecyclerPool.threadLocalPool()); + + _testRecyclerPoolGlobal(BufferRecyclerPool.ConcurrentDequePool.shared()); + _testRecyclerPoolGlobal(BufferRecyclerPool.LockFreePool.shared()); + BufferRecyclerPool.BoundedPool bounded = + _testRecyclerPoolGlobal(BufferRecyclerPool.BoundedPool.shared()); + assertEquals(BufferRecyclerPool.BoundedPool.DEFAULT_CAPACITY, bounded.capacity()); + + _testRecyclerPoolNonShared(BufferRecyclerPool.ConcurrentDequePool.nonShared()); + _testRecyclerPoolNonShared(BufferRecyclerPool.LockFreePool.nonShared()); + bounded = _testRecyclerPoolNonShared(BufferRecyclerPool.BoundedPool.nonShared(250)); + assertEquals(250, bounded.capacity()); + } + + private T _testRecyclerPoolGlobal(T pool) throws Exception { + byte[] stuff = jdkSerialize(pool); + T result = jdkDeserialize(stuff); + assertNotNull(result); + assertSame(pool.getClass(), result.getClass()); + return result; + } + + private T _testRecyclerPoolNonShared(T pool) throws Exception { + byte[] stuff = jdkSerialize(pool); + T result = jdkDeserialize(stuff); + assertNotNull(result); + assertEquals(pool.getClass(), result.getClass()); + assertNotSame(pool, result); + return result; + } + /* /********************************************************************** /* Exception types diff --git a/src/test/java/com/fasterxml/jackson/core/io/BufferRecyclerPoolTest.java b/src/test/java/com/fasterxml/jackson/core/io/BufferRecyclerPoolTest.java new file mode 100644 index 0000000000..2381f520b1 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/BufferRecyclerPoolTest.java @@ -0,0 +1,86 @@ +package com.fasterxml.jackson.core.io; + +import com.fasterxml.jackson.core.BaseTest; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.json.JsonGeneratorImpl; +import com.fasterxml.jackson.core.util.BufferRecycler; +import com.fasterxml.jackson.core.util.BufferRecyclerPool; + +import java.io.IOException; +import java.io.OutputStream; + +public class BufferRecyclerPoolTest extends BaseTest +{ + public void testNoOp() { + // no-op pool doesn't actually pool anything, so avoid checking it + checkBufferRecyclerPoolImpl(BufferRecyclerPool.NonRecyclingPool.shared(), false); + } + + public void testThreadLocal() { + checkBufferRecyclerPoolImpl(BufferRecyclerPool.ThreadLocalPool.shared(), true); + } + + public void testLockFree() { + checkBufferRecyclerPoolImpl(BufferRecyclerPool.LockFreePool.nonShared(), true); + } + + public void testConcurrentDequeue() { + checkBufferRecyclerPoolImpl(BufferRecyclerPool.ConcurrentDequePool.nonShared(), true); + } + + public void testBounded() { + checkBufferRecyclerPoolImpl(BufferRecyclerPool.BoundedPool.nonShared(1), true); + } + + private void checkBufferRecyclerPoolImpl(BufferRecyclerPool pool, boolean checkPooledResource) { + JsonFactory jsonFactory = JsonFactory.builder() + .bufferRecyclerPool(pool) + .build(); + BufferRecycler usedBufferRecycler = write("test", jsonFactory, 6); + + if (checkPooledResource) { + // acquire the pooled BufferRecycler again and check if it is the same instance used before + BufferRecycler pooledBufferRecycler = pool.acquireBufferRecycler(); + try { + assertSame(usedBufferRecycler, pooledBufferRecycler); + } finally { + pooledBufferRecycler.release(); + } + } + } + + protected final BufferRecycler write(Object value, JsonFactory jsonFactory, int expectedSize) { + BufferRecycler bufferRecycler; + NopOutputStream out = new NopOutputStream(); + try (JsonGenerator gen = jsonFactory.createGenerator(out)) { + bufferRecycler = ((JsonGeneratorImpl) gen).ioContext()._bufferRecycler; + gen.writeObject(value); + } catch (IOException e) { + throw new RuntimeException(e); + } + assertEquals(expectedSize, out.size); + return bufferRecycler; + } + + public class NopOutputStream extends OutputStream { + protected int size = 0; + + public NopOutputStream() { } + + @Override + public void write(int b) throws IOException { ++size; } + + @Override + public void write(byte[] b) throws IOException { size += b.length; } + + @Override + public void write(byte[] b, int offset, int len) throws IOException { size += len; } + + public NopOutputStream reset() { + size = 0; + return this; + } + public int size() { return size; } + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/io/TestIOContext.java b/src/test/java/com/fasterxml/jackson/core/io/TestIOContext.java index e6ca0ce0bb..ebd0b1d272 100644 --- a/src/test/java/com/fasterxml/jackson/core/io/TestIOContext.java +++ b/src/test/java/com/fasterxml/jackson/core/io/TestIOContext.java @@ -94,6 +94,7 @@ public void testAllocations() throws Exception verifyException(e, "smaller than original"); } ctxt.releaseNameCopyBuffer(null); + ctxt.close(); } } diff --git a/src/test/java/com/fasterxml/jackson/core/io/TestMergedStream.java b/src/test/java/com/fasterxml/jackson/core/io/TestMergedStream.java index 7707ac3d34..81a75ba1e7 100644 --- a/src/test/java/com/fasterxml/jackson/core/io/TestMergedStream.java +++ b/src/test/java/com/fasterxml/jackson/core/io/TestMergedStream.java @@ -20,6 +20,8 @@ public void testSimple() throws Exception ctxt.setEncoding(JsonEncoding.UTF8); MergedStream ms = new MergedStream(ctxt, new ByteArrayInputStream(second), first, 99, 99+5); + ctxt.close(); + // Ok, first, should have 5 bytes from first buffer: assertEquals(5, ms.available()); // not supported when there's buffered stuff... diff --git a/src/test/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBufferTest.java b/src/test/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBufferTest.java index 6c4211ab77..cfb163cff1 100644 --- a/src/test/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBufferTest.java +++ b/src/test/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBufferTest.java @@ -16,41 +16,55 @@ class ReadConstrainedTextBufferTest { @Test public void appendCharArray() throws Exception { - TextBuffer constrained = makeConstrainedBuffer(SEGMENT_SIZE); - char[] chars = new char[SEGMENT_SIZE]; - Arrays.fill(chars, 'A'); - constrained.append(chars, 0, SEGMENT_SIZE); - Assertions.assertThrows(IOException.class, () -> constrained.append(chars, 0, SEGMENT_SIZE)); + try (IOContext ioContext = makeConstrainedContext(SEGMENT_SIZE)) { + TextBuffer constrained = ioContext.constructReadConstrainedTextBuffer(); + char[] chars = new char[SEGMENT_SIZE]; + Arrays.fill(chars, 'A'); + constrained.append(chars, 0, SEGMENT_SIZE); + Assertions.assertThrows(IOException.class, () -> { + constrained.append(chars, 0, SEGMENT_SIZE); + constrained.contentsAsString(); + }); + } } @Test public void appendString() throws Exception { - TextBuffer constrained = makeConstrainedBuffer(SEGMENT_SIZE); - char[] chars = new char[SEGMENT_SIZE]; - Arrays.fill(chars, 'A'); - constrained.append(new String(chars), 0, SEGMENT_SIZE); - Assertions.assertThrows(IOException.class, () -> constrained.append(new String(chars), 0, SEGMENT_SIZE)); + try (IOContext ioContext = makeConstrainedContext(SEGMENT_SIZE)) { + TextBuffer constrained = ioContext.constructReadConstrainedTextBuffer(); + char[] chars = new char[SEGMENT_SIZE]; + Arrays.fill(chars, 'A'); + constrained.append(new String(chars), 0, SEGMENT_SIZE); + Assertions.assertThrows(IOException.class, () -> { + constrained.append(new String(chars), 0, SEGMENT_SIZE); + constrained.contentsAsString(); + }); + } } @Test public void appendSingle() throws Exception { - TextBuffer constrained = makeConstrainedBuffer(SEGMENT_SIZE); - char[] chars = new char[SEGMENT_SIZE]; - Arrays.fill(chars, 'A'); - constrained.append(chars, 0, SEGMENT_SIZE); - Assertions.assertThrows(IOException.class, () -> constrained.append('x')); + try (IOContext ioContext = makeConstrainedContext(SEGMENT_SIZE)) { + TextBuffer constrained = ioContext.constructReadConstrainedTextBuffer(); + char[] chars = new char[SEGMENT_SIZE]; + Arrays.fill(chars, 'A'); + constrained.append(chars, 0, SEGMENT_SIZE); + Assertions.assertThrows(IOException.class, () -> { + constrained.append('x'); + constrained.contentsAsString(); + }); + } } - private static TextBuffer makeConstrainedBuffer(int maxStringLen) { + private static IOContext makeConstrainedContext(int maxStringLen) { StreamReadConstraints constraints = StreamReadConstraints.builder() .maxStringLength(maxStringLen) .build(); - IOContext ioContext = new IOContext( + return new IOContext( constraints, StreamWriteConstraints.defaults(), ErrorReportConfiguration.defaults(), new BufferRecycler(), ContentReference.rawReference("N/A"), true); - return ioContext.constructReadConstrainedTextBuffer(); } } \ No newline at end of file