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

org.eclipse.jetty.util.Pool Maven / Gradle / Ivy

There is a newer version: 12.1.0.alpha0
Show newest version
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.util;

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

A pool of objects, with optional support for multiplexing, * max usage count and several optimized strategies plus * an optional {@link ThreadLocal} cache of the last release entry.

*

When the method {@link #close()} is called, all {@link Closeable}s * object pooled by the pool are also closed.

* * @param the type of the pooled objects */ @ManagedObject public class Pool implements AutoCloseable, Dumpable { private static final Logger LOGGER = LoggerFactory.getLogger(Pool.class); private final List entries = new CopyOnWriteArrayList<>(); private final int maxEntries; private final StrategyType strategyType; /* * The cache is used to avoid hammering on the first index of the entry list. * Caches can become poisoned (i.e.: containing entries that are in use) when * the release isn't done by the acquiring thread or when the entry pool is * undersized compared to the load applied on it. * When an entry can't be found in the cache, the global list is iterated * with the configured strategy so the cache has no visible effect besides performance. */ private final AutoLock lock = new AutoLock(); private final ThreadLocal cache; private final AtomicInteger nextIndex; private volatile boolean closed; @Deprecated private volatile int maxUsage = -1; @Deprecated private volatile int maxMultiplex = -1; /** * The type of the strategy to use for the pool. * The strategy primarily determines where iteration over the pool entries begins. */ public enum StrategyType { /** * A strategy that looks for an entry always starting from the first entry. * It will favour the early entries in the pool, but may contend on them more. */ FIRST, /** * A strategy that looks for an entry by iterating from a random starting * index. No entries are favoured and contention is reduced. */ RANDOM, /** * A strategy that uses the {@link Thread#getId()} of the current thread * to select a starting point for an entry search. Whilst not as performant as * using the {@link ThreadLocal} cache, it may be suitable when the pool is substantially smaller * than the number of available threads. * No entries are favoured and contention is reduced. */ THREAD_ID, /** * A strategy that looks for an entry by iterating from a starting point * that is incremented on every search. This gives similar results to the * random strategy but with more predictable behaviour. * No entries are favoured and contention is reduced. */ ROUND_ROBIN } /** * Construct a Pool with a specified lookup strategy and no * {@link ThreadLocal} cache. * * @param strategyType The strategy to used for looking up entries. * @param maxEntries the maximum amount of entries that the pool will accept. */ public Pool(StrategyType strategyType, int maxEntries) { this(strategyType, maxEntries, false); } /** * Construct a Pool with the specified thread-local cache size and * an optional {@link ThreadLocal} cache. * * @param strategyType The strategy to used for looking up entries. * @param maxEntries the maximum amount of entries that the pool will accept. * @param cache True if a {@link ThreadLocal} cache should be used to try the most recently released entry. */ public Pool(StrategyType strategyType, int maxEntries, boolean cache) { this.maxEntries = maxEntries; this.strategyType = Objects.requireNonNull(strategyType); this.cache = cache ? new ThreadLocal<>() : null; this.nextIndex = strategyType == StrategyType.ROUND_ROBIN ? new AtomicInteger() : null; } /** * @return the number of reserved entries */ @ManagedAttribute("The number of reserved entries") public int getReservedCount() { return (int)entries.stream().filter(Entry::isReserved).count(); } /** * @return the number of idle entries */ @ManagedAttribute("The number of idle entries") public int getIdleCount() { return (int)entries.stream().filter(Entry::isIdle).count(); } /** * @return the number of in-use entries */ @ManagedAttribute("The number of in-use entries") public int getInUseCount() { return (int)entries.stream().filter(Entry::isInUse).count(); } /** * @return the number of closed entries */ @ManagedAttribute("The number of closed entries") public int getClosedCount() { return (int)entries.stream().filter(Entry::isClosed).count(); } /** * @return the maximum number of entries */ @ManagedAttribute("The maximum number of entries") public int getMaxEntries() { return maxEntries; } /** * @return the default maximum multiplex count of entries * @deprecated Multiplex functionalities will be removed */ @ManagedAttribute("The default maximum multiplex count of entries") @Deprecated public int getMaxMultiplex() { return maxMultiplex == -1 ? 1 : maxMultiplex; } /** *

Retrieves the max multiplex count for the given pooled object.

* * @param pooled the pooled object * @return the max multiplex count for the given pooled object * @deprecated Multiplex functionalities will be removed */ @Deprecated protected int getMaxMultiplex(T pooled) { return getMaxMultiplex(); } /** *

Sets the default maximum multiplex count for the Pool's entries.

* * @param maxMultiplex the default maximum multiplex count of entries * @deprecated Multiplex functionalities will be removed */ @Deprecated public final void setMaxMultiplex(int maxMultiplex) { if (maxMultiplex < 1) throw new IllegalArgumentException("Max multiplex must be >= 1"); try (AutoLock l = lock.lock()) { if (closed) return; if (entries.stream().anyMatch(MonoEntry.class::isInstance)) throw new IllegalStateException("Pool entries do not support multiplexing"); this.maxMultiplex = maxMultiplex; } } /** *

Returns the maximum number of times the entries of the pool * can be acquired.

* * @return the default maximum usage count of entries * @deprecated MaxUsage functionalities will be removed */ @ManagedAttribute("The default maximum usage count of entries") @Deprecated public int getMaxUsageCount() { return maxUsage; } /** *

Retrieves the max usage count for the given pooled object.

* * @param pooled the pooled object * @return the max usage count for the given pooled object * @deprecated MaxUsage functionalities will be removed */ @Deprecated protected int getMaxUsageCount(T pooled) { return getMaxUsageCount(); } /** *

Sets the maximum usage count for the Pool's entries.

*

All existing idle entries that have a usage count larger * than this new value are removed from the Pool and closed.

* * @param maxUsageCount the default maximum usage count of entries * @deprecated MaxUsage functionalities will be removed */ @Deprecated public final void setMaxUsageCount(int maxUsageCount) { if (maxUsageCount == 0) throw new IllegalArgumentException("Max usage count must be != 0"); // Iterate the entries, remove overused ones and collect a list of the closeable removed ones. List copy; try (AutoLock l = lock.lock()) { if (closed) return; if (entries.stream().anyMatch(MonoEntry.class::isInstance)) throw new IllegalStateException("Pool entries do not support max usage"); this.maxUsage = maxUsageCount; copy = entries.stream() .filter(entry -> entry.isIdleAndOverUsed() && remove(entry) && entry.pooled instanceof Closeable) .map(entry -> (Closeable)entry.pooled) .collect(Collectors.toList()); } // Iterate the copy and close the collected entries. copy.forEach(IO::close); } /** *

Creates a new disabled slot into the pool.

*

The returned entry must ultimately have the {@link Entry#enable(Object, boolean)} * method called or be removed via {@link Pool.Entry#remove()} or * {@link Pool#remove(Pool.Entry)}.

* * @param allotment the desired allotment, where each entry handles an allotment of maxMultiplex, * or a negative number to always trigger the reservation of a new entry. * @return a disabled entry that is contained in the pool, * or null if the pool is closed or if the pool already contains * {@link #getMaxEntries()} entries, or the allotment has already been reserved * @deprecated Use {@link #reserve()} instead */ @Deprecated public Entry reserve(int allotment) { try (AutoLock l = lock.lock()) { if (closed) return null; int space = maxEntries - entries.size(); if (space <= 0) return null; if (allotment >= 0 && (getReservedCount() * getMaxMultiplex()) >= allotment) return null; Entry entry = newEntry(); entries.add(entry); return entry; } } /** *

Creates a new disabled slot into the pool.

*

The returned entry must ultimately have the {@link Entry#enable(Object, boolean)} * method called or be removed via {@link Pool.Entry#remove()} or * {@link Pool#remove(Pool.Entry)}.

* * @return a disabled entry that is contained in the pool, * or null if the pool is closed or if the pool already contains * {@link #getMaxEntries()} entries */ public Entry reserve() { try (AutoLock l = lock.lock()) { if (closed) { if (LOGGER.isDebugEnabled()) LOGGER.debug("{} is closed, returning null reserved entry", this); return null; } // If we have no space int entriesSize = entries.size(); if (maxEntries > 0 && entriesSize >= maxEntries) { if (LOGGER.isDebugEnabled()) LOGGER.debug("{} has no space: {} >= {}, returning null reserved entry", this, entriesSize, maxEntries); return null; } Entry entry = newEntry(); entries.add(entry); if (LOGGER.isDebugEnabled()) LOGGER.debug("{} returning new reserved entry {}", this, entry); return entry; } } private Entry newEntry() { // Do not allow more than 2 implementations of Entry, otherwise call sites in Pool // referencing Entry methods will become mega-morphic and kill the performance. if (maxMultiplex >= 0 || maxUsage >= 0) return new MultiEntry(); return new MonoEntry(); } /** *

Acquires an entry from the pool.

*

Only enabled entries will be returned from this method * and their {@link Entry#enable(Object, boolean)} * method must not be called.

* * @return an entry from the pool or null if none is available. */ public Entry acquire() { if (closed) return null; int size = entries.size(); if (size == 0) return null; if (cache != null) { Pool.Entry entry = cache.get(); if (entry != null && entry.tryAcquire()) return entry; } int index = startIndex(size); for (int tries = size; tries-- > 0;) { try { Pool.Entry entry = entries.get(index); if (entry != null && entry.tryAcquire()) return entry; } catch (IndexOutOfBoundsException e) { LOGGER.trace("IGNORED", e); size = entries.size(); // Size can be 0 when the pool is in the middle of // acquiring a connection while another thread // removes the last one from the pool. if (size == 0) break; } index = (index + 1) % size; } return null; } private int startIndex(int size) { switch (strategyType) { case FIRST: return 0; case RANDOM: return ThreadLocalRandom.current().nextInt(size); case ROUND_ROBIN: return nextIndex.getAndUpdate(c -> Math.max(0, c + 1)) % size; case THREAD_ID: return (int)(Thread.currentThread().getId() % size); default: throw new IllegalArgumentException("Unknown strategy type: " + strategyType); } } /** *

Acquires an entry from the pool, * reserving and creating a new entry if necessary.

* * @param creator a function to create the pooled value for a reserved entry. * @return an entry from the pool or null if none is available. */ public Entry acquire(Function.Entry, T> creator) { Entry entry = acquire(); if (entry != null) return entry; entry = reserve(); if (entry == null) return null; T value; try { value = creator.apply(entry); } catch (Throwable th) { remove(entry); throw th; } if (value == null) { remove(entry); return null; } return entry.enable(value, true) ? entry : null; } /** *

Releases an {@link #acquire() acquired} entry to the pool.

*

Entries that are acquired from the pool but never released * will result in a memory leak.

* * @param entry the value to return to the pool * @return true if the entry was released and could be acquired again, * false if the entry should be removed by calling {@link #remove(Pool.Entry)} * and the object contained by the entry should be disposed. */ public boolean release(Entry entry) { if (closed) return false; boolean released = entry.tryRelease(); if (released && cache != null) cache.set(entry); return released; } /** *

Removes an entry from the pool.

* * @param entry the value to remove * @return true if the entry was removed, false otherwise */ public boolean remove(Entry entry) { if (closed) return false; if (!entry.tryRemove()) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Attempt to remove an object from the pool that is still in use: {}", entry); return false; } boolean removed = entries.remove(entry); if (!removed && LOGGER.isDebugEnabled()) LOGGER.debug("Attempt to remove an object from the pool that does not exist: {}", entry); return removed; } public boolean isClosed() { return closed; } @Override public void close() { if (LOGGER.isDebugEnabled()) LOGGER.debug("Closing {}", this); List copy; try (AutoLock l = lock.lock()) { closed = true; copy = new ArrayList<>(entries); entries.clear(); } // iterate the copy and close its entries for (Entry entry : copy) { boolean removed = entry.tryRemove(); if (removed) { if (entry.pooled instanceof Closeable) IO.close((Closeable)entry.pooled); } else { if (LOGGER.isDebugEnabled()) LOGGER.debug("Pooled object still in use: {}", entry); } } } public int size() { return entries.size(); } public Collection values() { return Collections.unmodifiableCollection(entries); } @Override public void dump(Appendable out, String indent) throws IOException { Dumpable.dumpObjects(out, indent, this, new DumpableCollection("entries", entries)); } @Override public String toString() { return String.format("%s@%x[inUse=%d,size=%d,max=%d,closed=%b]", getClass().getSimpleName(), hashCode(), getInUseCount(), size(), getMaxEntries(), isClosed()); } /** *

A Pool entry that holds metadata and a pooled object.

*/ public abstract class Entry { // The pooled object. This is not volatile as it is set once and then never changed. // Other threads accessing must check the state field above first, so a good before/after // relationship exists to make a memory barrier. private T pooled; /** *

Enables this, previously {@link #reserve() reserved}, Entry.

*

An entry returned from the {@link #reserve()} method must be enabled with this method, * once and only once, before it is usable by the pool.

*

The entry may be enabled and not acquired, in which case it is immediately available to be * acquired, potentially by another thread; or it can be enabled and acquired atomically so that * no other thread can acquire it, although the acquire may still fail if the pool has been closed.

* * @param pooled the pooled object for this Entry * @param acquire whether this Entry should be atomically enabled and acquired * @return whether this Entry was enabled * @throws IllegalStateException if this Entry was already enabled */ public boolean enable(T pooled, boolean acquire) { Objects.requireNonNull(pooled); if (!isReserved()) { if (isClosed()) return false; // Pool has been closed throw new IllegalStateException("Entry already enabled: " + this); } this.pooled = pooled; if (tryEnable(acquire)) return true; this.pooled = null; if (isClosed()) return false; // Pool has been closed throw new IllegalStateException("Entry already enabled: " + this); } /** * @return the pooled object */ public T getPooled() { return pooled; } /** *

Releases this Entry.

*

This is equivalent to calling {@link Pool#release(Pool.Entry)} passing this entry.

* * @return whether this Entry was released */ public boolean release() { return Pool.this.release(this); } /** *

Removes this Entry from the Pool.

*

This is equivalent to calling {@link Pool#remove(Pool.Entry)} passing this entry.

* * @return whether this Entry was removed */ public boolean remove() { return Pool.this.remove(this); } /** *

Tries to enable, and possible also acquire, this Entry.

* * @param acquire whether to also acquire this Entry * @return whether this Entry was enabled */ abstract boolean tryEnable(boolean acquire); /** *

Tries to acquire this Entry.

* * @return whether this Entry was acquired */ abstract boolean tryAcquire(); /** *

Tries to release this Entry.

* * @return true if this Entry was released, * false if {@link #tryRemove()} should be called. */ abstract boolean tryRelease(); /** *

Tries to remove the entry by marking it as closed.

* * @return whether the entry can be removed from the containing pool */ abstract boolean tryRemove(); /** * @return whether this Entry is closed */ public abstract boolean isClosed(); /** * @return whether this Entry is reserved */ public abstract boolean isReserved(); /** * @return whether this Entry is idle */ public abstract boolean isIdle(); /** * @return whether this entry is in use. */ public abstract boolean isInUse(); /** * @return whether this entry has been used beyond {@link #getMaxUsageCount()} * @deprecated MaxUsage functionalities will be removed */ @Deprecated public boolean isOverUsed() { return false; } boolean isIdleAndOverUsed() { return false; } // Only for testing. int getUsageCount() { return 0; } // Only for testing. void setUsageCount(int usageCount) { } } /** *

A Pool entry that holds metadata and a pooled object, * that can only be acquired concurrently at most once, and * can be acquired/released multiple times.

*/ private class MonoEntry extends Entry { // MIN_VALUE => pending; -1 => closed; 0 => idle; 1 => active; private final AtomicInteger state = new AtomicInteger(Integer.MIN_VALUE); @Override protected boolean tryEnable(boolean acquire) { return state.compareAndSet(Integer.MIN_VALUE, acquire ? 1 : 0); } @Override boolean tryAcquire() { while (true) { int s = state.get(); if (s != 0) return false; if (state.compareAndSet(s, 1)) return true; } } @Override boolean tryRelease() { while (true) { int s = state.get(); if (s < 0) return false; if (s == 0) throw new IllegalStateException("Cannot release an already released entry"); if (state.compareAndSet(s, 0)) return true; } } @Override boolean tryRemove() { state.set(-1); return true; } @Override public boolean isClosed() { return state.get() < 0; } @Override public boolean isReserved() { return state.get() == Integer.MIN_VALUE; } @Override public boolean isIdle() { return state.get() == 0; } @Override public boolean isInUse() { return state.get() == 1; } @Override public String toString() { String s; switch (state.get()) { case Integer.MIN_VALUE: s = "PENDING"; break; case -1: s = "CLOSED"; break; case 0: s = "IDLE"; break; default: s = "ACTIVE"; } return String.format("%s@%x{%s,pooled=%s}", getClass().getSimpleName(), hashCode(), s, getPooled()); } } /** *

A Pool entry that holds metadata and a pooled object, * that can be acquired concurrently multiple times, and * can be acquired/released multiple times.

*/ class MultiEntry extends Entry { // hi: MIN_VALUE => pending; -1 => closed; 0+ => usage counter; // lo: 0 => idle; positive => multiplex counter private final AtomicBiInteger state; MultiEntry() { this.state = new AtomicBiInteger(Integer.MIN_VALUE, 0); } @Override void setUsageCount(int usageCount) { this.state.getAndSetHi(usageCount); } @Override protected boolean tryEnable(boolean acquire) { int usage = acquire ? 1 : 0; return state.compareAndSet(Integer.MIN_VALUE, usage, 0, usage); } /** *

Tries to acquire the entry if possible by incrementing both the usage * count and the multiplex count.

* * @return true if the usage count is less than {@link #getMaxUsageCount()} and * the multiplex count is less than {@link #getMaxMultiplex(Object)} and * the entry is not closed, false otherwise. */ @Override boolean tryAcquire() { while (true) { long encoded = state.get(); int usageCount = AtomicBiInteger.getHi(encoded); int multiplexCount = AtomicBiInteger.getLo(encoded); boolean closed = usageCount < 0; if (closed) return false; T pooled = getPooled(); int maxUsageCount = getMaxUsageCount(pooled); if (maxUsageCount > 0 && usageCount >= maxUsageCount) return false; int maxMultiplexed = getMaxMultiplex(pooled); if (maxMultiplexed > 0 && multiplexCount >= maxMultiplexed) return false; // Prevent overflowing the usage counter by capping it at Integer.MAX_VALUE. int newUsageCount = usageCount == Integer.MAX_VALUE ? Integer.MAX_VALUE : usageCount + 1; if (state.compareAndSet(encoded, newUsageCount, multiplexCount + 1)) return true; } } /** *

Tries to release the entry if possible by decrementing the multiplex * count unless the entity is closed.

* * @return true if the entry was released, * false if {@link #tryRemove()} should be called. */ @Override boolean tryRelease() { int newMultiplexCount; int usageCount; while (true) { long encoded = state.get(); usageCount = AtomicBiInteger.getHi(encoded); boolean closed = usageCount < 0; if (closed) return false; newMultiplexCount = AtomicBiInteger.getLo(encoded) - 1; if (newMultiplexCount < 0) throw new IllegalStateException("Cannot release an already released entry"); if (state.compareAndSet(encoded, usageCount, newMultiplexCount)) break; } int currentMaxUsageCount = getMaxUsageCount(getPooled()); boolean overUsed = currentMaxUsageCount > 0 && usageCount >= currentMaxUsageCount; return !(overUsed && newMultiplexCount == 0); } /** *

Tries to remove the entry by marking it as closed and decrementing the multiplex counter.

*

The multiplex counter will never go below zero and if it reaches zero, the entry is considered removed.

* * @return true if the entry can be removed from the containing pool, false otherwise. */ @Override boolean tryRemove() { while (true) { long encoded = state.get(); int usageCount = AtomicBiInteger.getHi(encoded); int multiplexCount = AtomicBiInteger.getLo(encoded); int newMultiplexCount = Math.max(multiplexCount - 1, 0); boolean removed = state.compareAndSet(usageCount, -1, multiplexCount, newMultiplexCount); if (removed) return newMultiplexCount == 0; } } @Override public boolean isClosed() { return state.getHi() < 0; } @Override public boolean isReserved() { return state.getHi() == Integer.MIN_VALUE; } @Override public boolean isIdle() { long encoded = state.get(); return AtomicBiInteger.getHi(encoded) >= 0 && AtomicBiInteger.getLo(encoded) == 0; } @Override public boolean isInUse() { long encoded = state.get(); return AtomicBiInteger.getHi(encoded) >= 0 && AtomicBiInteger.getLo(encoded) > 0; } @Override public boolean isOverUsed() { int maxUsageCount = getMaxUsageCount(); int usageCount = state.getHi(); return maxUsageCount > 0 && usageCount >= maxUsageCount; } @Override boolean isIdleAndOverUsed() { int maxUsageCount = getMaxUsageCount(); long encoded = state.get(); int usageCount = AtomicBiInteger.getHi(encoded); int multiplexCount = AtomicBiInteger.getLo(encoded); return maxUsageCount > 0 && usageCount >= maxUsageCount && multiplexCount == 0; } @Override int getUsageCount() { return Math.max(state.getHi(), 0); } @Override public String toString() { long encoded = state.get(); int usageCount = AtomicBiInteger.getHi(encoded); int multiplexCount = AtomicBiInteger.getLo(encoded); String state = usageCount < 0 ? (usageCount == Integer.MIN_VALUE ? "PENDING" : "CLOSED") : (multiplexCount == 0 ? "IDLE" : "ACTIVE"); return String.format("%s@%x{%s,usage=%d,multiplex=%d,pooled=%s}", getClass().getSimpleName(), hashCode(), state, Math.max(usageCount, 0), Math.max(multiplexCount, 0), getPooled()); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy