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

org.eclipse.jetty.util.ConcurrentPool 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.IOException;
import java.lang.ref.WeakReference;
import java.util.Collection;
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.concurrent.atomic.LongAdder;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import java.util.stream.Stream;

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 concurrent implementation of {@link Pool}.

*

This implementation offers a number of {@link StrategyType strategies} * used to select the entry returned from {@link #acquire()}, and its * capacity is bounded to a {@link #getMaxSize() max size}.

*

When a pooled item is {@link #acquire() acquired} from this pool, it is only held * by a {@link WeakReference}, so that if it is collected before being {@link #release(Entry) released}, * then that leak is detected and the entry is {@link #remove(Entry) removed} (see {@link #getLeaked()}.

* * @param

the type of the pooled objects */ @ManagedObject public class ConcurrentPool

implements Pool

, Dumpable { /** * {@link ConcurrentPool} internally needs to linearly scan a list to perform an acquisition. * This list needs to be reasonably short otherwise there is a risk that scanning the list * becomes a bottleneck. Instances created with a size at most this value should be immune * to this problem. */ public static final int OPTIMAL_MAX_SIZE = 256; private static final Logger LOG = LoggerFactory.getLogger(ConcurrentPool.class); private final List> entries = new CopyOnWriteArrayList<>(); private final int maxSize; private final StrategyType strategyType; private final AutoLock lock = new AutoLock(); private final AtomicInteger nextIndex; private final ToIntFunction

maxMultiplex; private final LongAdder leaked = new LongAdder(); private volatile boolean terminated; /** *

Creates an instance with the specified strategy.

* * @param strategyType the strategy to used to lookup entries * @param maxSize the maximum number of pooled entries */ public ConcurrentPool(StrategyType strategyType, int maxSize) { this(strategyType, maxSize, pooled -> 1); } /** *

Creates an instance with the specified strategy.

* * @param strategyType the strategy to used to lookup entries * @param maxSize the maximum number of pooled entries * @param cache whether a {@link ThreadLocal} cache should be used for the most recently released entry * @deprecated cache is no longer supported. Use {@link StrategyType#THREAD_ID} */ @Deprecated public ConcurrentPool(StrategyType strategyType, int maxSize, boolean cache) { this(strategyType, maxSize, pooled -> 1); } /** *

Creates an instance with the specified strategy. * and a function that returns the max multiplex count for a given pooled object.

* * @param strategyType the strategy to used to lookup entries * @param maxSize the maximum number of pooled entries * @param cache whether a {@link ThreadLocal} cache should be used for the most recently released entry * @param maxMultiplex a function that given the pooled object returns the max multiplex count * @deprecated cache is no longer supported. Use {@link StrategyType#THREAD_ID} */ @Deprecated public ConcurrentPool(StrategyType strategyType, int maxSize, boolean cache, ToIntFunction

maxMultiplex) { this(strategyType, maxSize, maxMultiplex); } /** *

Creates an instance with the specified strategy. * and a function that returns the max multiplex count for a given pooled object.

* * @param strategyType the strategy to used to lookup entries * @param maxSize the maximum number of pooled entries * @param maxMultiplex a function that given the pooled object returns the max multiplex count */ public ConcurrentPool(StrategyType strategyType, int maxSize, ToIntFunction

maxMultiplex) { if (maxSize > OPTIMAL_MAX_SIZE && LOG.isDebugEnabled()) LOG.debug("{} configured with max size {} which is above the recommended value {}", getClass().getSimpleName(), maxSize, OPTIMAL_MAX_SIZE); this.maxSize = maxSize; this.strategyType = Objects.requireNonNull(strategyType); this.nextIndex = strategyType == StrategyType.ROUND_ROBIN ? new AtomicInteger() : null; this.maxMultiplex = Objects.requireNonNull(maxMultiplex); } @ManagedAttribute("number of entries leaked (not released nor referenced)") public long getLeaked() { return leaked.longValue(); } /** *

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 */ private int getMaxMultiplex(P pooled) { return maxMultiplex.applyAsInt(pooled); } private void leaked(Holder

holder) { leaked.increment(); if (LOG.isDebugEnabled()) LOG.debug("Leaked " + holder); leaked(); } protected void leaked() { } @Override public Entry

reserve() { try (AutoLock ignored = lock.lock()) { // Because the list of entries is modified, this.terminated // must be read with the lock held, see method terminate(). if (terminated) { if (LOG.isDebugEnabled()) LOG.debug("terminated, cannot reserve entry for {}", this); return null; } // If we have no space int entriesSize = entries.size(); if (maxSize > 0 && entriesSize >= maxSize) { // Sweep for collected entries sweep(); entriesSize = entries.size(); if (entriesSize >= maxSize) { if (LOG.isDebugEnabled()) LOG.debug("no space: {} >= {}, cannot reserve entry for {}", entriesSize, maxSize, this); return null; } } ConcurrentEntry

entry = new ConcurrentEntry<>(this); entries.add(entry.getHolder()); if (LOG.isDebugEnabled()) LOG.debug("returning reserved entry {} for {}", entry, this); return entry; } } void sweep() { for (int i = 0; i < entries.size(); i++) { Holder

holder = entries.get(i); if (holder.getEntry() == null) { entries.remove(i--); leaked(holder); } } } @Override public Entry

acquire() { if (terminated) return null; int size = entries.size(); if (size == 0) return null; int index = startIndex(size); for (int tries = size; tries-- > 0; ) { try { Holder

holder = entries.get(index); if (holder != null) { ConcurrentEntry

entry = (ConcurrentEntry

)holder.getEntry(); if (entry == null) { entries.remove(index); leaked(holder); continue; } if (entry.tryAcquire()) { if (LOG.isDebugEnabled()) LOG.debug("returning entry {} for {}", entry, this); return entry; } } } catch (IndexOutOfBoundsException e) { LOG.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; } if (++index >= size) index = 0; } return null; } private int startIndex(int size) { return switch (strategyType) { case FIRST -> 0; case RANDOM -> ThreadLocalRandom.current().nextInt(size); case ROUND_ROBIN -> nextIndex.getAndUpdate(c -> Math.max(0, c + 1)) % size; case THREAD_ID -> (int)(Thread.currentThread().getId() % size); }; } private boolean release(Entry

entry) { boolean released = ((ConcurrentEntry

)entry).tryRelease(); if (LOG.isDebugEnabled()) LOG.debug("released {} {} for {}", released, entry, this); return released; } private boolean remove(Entry

entry) { boolean removed = ((ConcurrentEntry

)entry).tryRemove(); if (LOG.isDebugEnabled()) LOG.debug("removed {} {} for {}", removed, entry, this); if (!removed) return false; // No need to lock, no race with reserve() // and the race with terminate() is harmless. Holder

holder = ((ConcurrentEntry

)entry).getHolder(); boolean evicted = entries.remove(holder); if (LOG.isDebugEnabled()) LOG.debug("evicted {} {} for {}", evicted, entry, this); return true; } @Override public boolean isTerminated() { return terminated; } @Override public Collection> terminate() { if (LOG.isDebugEnabled()) LOG.debug("terminating {}", this); List> copy; try (AutoLock ignored = lock.lock()) { // Field this.terminated must be modified with the lock held // because the list of entries is modified, see reserve(). terminated = true; copy = entries.stream() .map(Holder::getEntry) .filter(Objects::nonNull) .toList(); entries.clear(); } // Iterate over the copy and terminate its entries. copy.forEach(entry -> ((ConcurrentEntry

)entry).terminate()); return copy; } private boolean terminate(Entry

entry) { boolean terminated = ((ConcurrentEntry

)entry).tryTerminate(); if (!terminated) { if (LOG.isDebugEnabled()) LOG.debug("entry still in use or already terminated {} for {}", entry, this); } return terminated; } @Override public int size() { return entries.size(); } @Override public int getMaxSize() { return maxSize; } @Override public Stream> stream() { return entries.stream().map(Holder::getEntry).filter(Objects::nonNull); } @Override public int getReservedCount() { return getCount(Entry::isReserved); } @Override public int getIdleCount() { return getCount(Entry::isIdle); } @Override public int getInUseCount() { return getCount(Entry::isInUse); } @Override public int getTerminatedCount() { return getCount(Entry::isTerminated); } private int getCount(Predicate> predicate) { int count = 0; for (Holder

holder : entries) { Entry

entry = holder.getEntry(); if (entry != null && predicate.test(entry)) count++; } return count; } @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[strategy=%s,inUse=%d,size=%d,max=%d,leaked=%d,terminated=%b]", getClass().getSimpleName(), hashCode(), strategyType, getInUseCount(), size(), getMaxSize(), getLeaked(), isTerminated()); } /** * 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. * 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 } /** *

A Pool entry that holds metadata and a pooled object.

*/ public static class ConcurrentEntry implements Entry { // HI: // -2 -> already removed // -1 -> terminated | removed // 0 -> available // LO: // -1 -> reserved // 0 -> idle // 1+ -> multiplex count private final AtomicBiInteger state = new AtomicBiInteger(0, -1); private final ConcurrentPool pool; private final Holder holder; // 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 E pooled; public ConcurrentEntry(ConcurrentPool pool) { this.pool = pool; this.holder = new Holder<>(this); } private Holder getHolder() { return holder; } @Override public boolean enable(E pooled, boolean acquire) { Objects.requireNonNull(pooled); if (!isReserved()) { if (isTerminated()) return false; throw new IllegalStateException("Entry already enabled " + this + " for " + pool); } this.pooled = pooled; if (tryEnable(acquire)) { if (LOG.isDebugEnabled()) LOG.debug("enabled {} for {}", this, pool); return true; } this.pooled = null; if (isTerminated()) return false; throw new IllegalStateException("Entry already enabled " + this + " for " + pool); } @Override public E getPooled() { return pooled; } @Override public boolean release() { return pool.release(this); } @Override public boolean remove() { return pool.remove(this); } private boolean terminate() { return pool.terminate(this); } /** *

Tries to enable, and possible also acquire, this {@code Entry}.

* * @param acquire whether to also acquire this {@code Entry} * @return whether this {@code Entry} was enabled */ private boolean tryEnable(boolean acquire) { boolean enabled = state.compareAndSet(0, 0, -1, acquire ? 1 : 0); if (enabled && !acquire) getHolder().released(); return enabled; } /** *

Tries to acquire this {@code Entry}.

* * @return whether this {@code Entry} was acquired */ private boolean tryAcquire() { while (true) { long encoded = state.get(); // Already terminated? if (AtomicBiInteger.getHi(encoded) < 0) return false; int multiplexCount = AtomicBiInteger.getLo(encoded); // Not yet enabled? if (multiplexCount < 0) return false; int maxMultiplexed = pool.getMaxMultiplex(pooled); if (maxMultiplexed > 0 && multiplexCount >= maxMultiplexed) return false; int newMultiplexCount = multiplexCount + 1; // Handles integer overflow. if (newMultiplexCount < 0) return false; if (state.compareAndSet(encoded, 0, newMultiplexCount)) { if (newMultiplexCount == 1) getHolder().acquired(); return true; } } } /** *

Tries to release this {@code Entry}.

* * @return true if this {@code Entry} was released, * false if {@link #tryRemove()} should be called. */ private boolean tryRelease() { while (true) { long encoded = state.get(); if (AtomicBiInteger.getHi(encoded) < 0) return false; int multiplexCount = AtomicBiInteger.getLo(encoded); if (multiplexCount <= 0) return false; int newMultiplexCount = multiplexCount - 1; if (state.compareAndSet(encoded, 0, newMultiplexCount)) { if (newMultiplexCount == 0) getHolder().released(); return true; } } } /** *

Tries to remove this {@code Entry} by marking it as terminated.

* * @return whether this {@code Entry} can be removed */ private boolean tryRemove() { while (true) { long encoded = state.get(); int removed = AtomicBiInteger.getHi(encoded); int multiplexCount = AtomicBiInteger.getLo(encoded); // The entry was already removed once before. if (removed == -2) return false; int newMultiplexCount; if (multiplexCount <= 0) { // Removing a reserved or idle entry; // keep the same multiplex count. newMultiplexCount = multiplexCount; } else { // Removing an in-use entry; // decrement the multiplex count. newMultiplexCount = multiplexCount - 1; } // Always mark the entry as removed, // and update the multiplex count. boolean result = newMultiplexCount <= 0; removed = result ? -2 : -1; if (state.compareAndSet(encoded, removed, newMultiplexCount)) return result; } } private boolean tryTerminate() { while (true) { long encoded = state.get(); if (AtomicBiInteger.getHi(encoded) < 0) return false; int multiplexCount = AtomicBiInteger.getLo(encoded); if (state.compareAndSet(encoded, -1, multiplexCount)) return multiplexCount <= 0; } } @Override public boolean isTerminated() { return state.getHi() < 0; } @Override public boolean isReserved() { return state.getLo() < 0; } @Override public boolean isIdle() { return state.getLo() == 0; } @Override public boolean isInUse() { return state.getLo() > 0; } @Override public String toString() { long encoded = state.get(); return String.format("%s@%x{terminated=%b,multiplex=%d,pooled=%s}", getClass().getSimpleName(), hashCode(), AtomicBiInteger.getHi(encoded) < 0, AtomicBiInteger.getLo(encoded), getPooled()); } } /** *

Holds a strong and a weak reference to an {@link Entry} to avoid holding * on to entries that are not released, so that they can be garbage collected.

*

Methods {@link #released()} and {@link #acquired()} work together to clear the * strong reference when the entry is acquired, and assign it when the entry * is released.

*

This class handles a race condition happening when an entry is being * released with multiplex count going {@code 1 -> 0} by one thread that * has not yet called {@link #released()}, and immediately acquired by another * thread that is calling {@link #acquired()}. * The call to {@link #acquired()} spin loops until {@link #released()} returns.

* * @param

*/ private static class Holder

{ private final WeakReference> _weak; private volatile ConcurrentEntry

_strong; protected Holder(ConcurrentEntry

entry) { _weak = new WeakReference<>(entry); } public Entry

getEntry() { return _weak.get(); } /** *

Called when an entry is released to the pool with multiplex count going from {@code 1} to {@code 0}.

*/ public void released() { _strong = _weak.get(); } /** *

Called when an entry is acquired from the pool with multiplex count going from {@code 0} to {@code 1}.

*/ public void acquired() { ConcurrentEntry

entry = _weak.get(); if (entry == null) return; // Free must only be called when we know the holder will be held. while (_strong == null && !entry.isTerminated()) Thread.onSpinWait(); _strong = null; } @Override public String toString() { return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), _strong == null ? "acquired" : "released", _weak.get()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy