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

org.apache.openejb.util.Pool Maven / Gradle / Ivy

There is a newer version: 4.7.5
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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 org.apache.openejb.util;

import org.apache.openejb.monitoring.Managed;

import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.LinkedBlockingQueue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Any successful pop() call requires a corresponding push() or discard() call.
 * 

* A pop() call that returns null is considered successful. A null indicates * that the calling code has a permit to create a poolable object and call * {@link Pool#push(Object)}. This is the only situation in which that method * may be called. *

* To simply fill the pool without a corresponding pop(), the add() method * must be used. This method will attempt to aquire a permit to add to the pool. * * @version $Rev$ $Date$ */ public class Pool { private final LinkedList pool = new LinkedList(); private final Semaphore instances; private final Semaphore available; private final Semaphore minimum; private final Executor executor; @Managed private final long maxAge; @Managed private final AtomicInteger poolVersion = new AtomicInteger(); private final Supplier supplier; private final AtomicReference timer = new AtomicReference(); private final Sweeper sweeper; private final CountingLatch out = new CountingLatch(); @Managed private final long sweepInterval; @Managed private final boolean replaceAged; @Managed private final boolean replaceFlushed; @Managed private final double maxAgeOffset; @Managed private final Stats stats; @Managed private final boolean garbageCollection; public Pool(int max, int min, boolean strict) { this(max, min, strict, 0, 0, 0, null, null, false, -1, false, false); } public Pool(int max, int min, boolean strict, long maxAge, long idleTimeout, long sweepInterval, Executor executor, Supplier supplier, boolean replaceAged, double maxAgeOffset, boolean garbageCollection, boolean replaceFlushed) { if (min > max) greater("max", max, "min", min); if (maxAge != 0 && idleTimeout > maxAge) greater("MaxAge", maxAge, "IdleTimeout", idleTimeout); this.executor = executor != null ? executor : createExecutor(); this.supplier = supplier != null ? supplier : new NoSupplier(); this.available = (strict) ? new Semaphore(max) : new Overdraft(max); this.minimum = new Semaphore(min); this.instances = new Semaphore(max); this.maxAge = maxAge; this.maxAgeOffset = maxAgeOffset; this.replaceAged = replaceAged; this.replaceFlushed = replaceFlushed; if (sweepInterval == 0) sweepInterval = 5 * 60 * 1000; // five minutes this.sweepInterval = sweepInterval; this.sweeper = new Sweeper(idleTimeout, max); this.stats = new Stats(min, max, idleTimeout); this.garbageCollection = garbageCollection; } public Pool start() { if (timer.compareAndSet(null, new Timer("PoolEviction@" + hashCode(), true))) { timer.get().scheduleAtFixedRate(sweeper, 0, this.sweepInterval); } return this; } public void stop() { Timer timer = this.timer.get(); if (timer != null && this.timer.compareAndSet(timer, null)) { timer.cancel(); } } public boolean running() { return timer.get() != null; } private Executor createExecutor() { return new ThreadPoolExecutor(5, 10, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue(), new DaemonThreadFactory("o.a.openejb.util.Pool", hashCode())); } private void greater(String maxName, long max, String minName, long min) { throw new IllegalArgumentException(minName + " cannot be greater than " + maxName + ": " + minName + "=" + min + ", " + maxName + "=" + max); } public void flush() { stats.flushes.record(); poolVersion.incrementAndGet(); } /** * Any successful pop() call requires a corresponding push() or discard() call *

* A pop() call that returns null is considered successful. * * @param timeout time to block while waiting for an instance * @param unit unit of time dicated by the timeout * @return an entry from the pool or null indicating permission to create and push() an instance into the pool * @throws InterruptedException vm level thread interruption * @throws IllegalStateException if a permit could not be acquired * @throws java.util.concurrent.TimeoutException if no instance could be obtained within the timeout */ public Entry pop(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { return pop(timeout, unit, true); } /** * Any successful pop() call requires a corresponding push() or discard() call *

* A pop() call that returns null is considered successful. * * @param timeout time to block while waiting for an instance * @param unit unit of time dicated by the timeout * @param record should this be reflected in the stats * @return an entry from the pool or null indicating permission to create and push() an instance into the pool * @throws InterruptedException vm level thread interruption * @throws IllegalStateException if a permit could not be acquired * @throws TimeoutException if no instance could be obtained within the timeout */ private Entry pop(long timeout, TimeUnit unit, boolean record) throws InterruptedException, TimeoutException { if (timeout == -1) { available.tryAcquire(); } else if (!available.tryAcquire(timeout, unit)) { if (record) stats.accessTimeouts.record(); throw new TimeoutException("Waited " + timeout + " " + unit); } Entry entry = null; while (entry == null) { synchronized (pool) { try { entry = pool.removeFirst(); } catch (NoSuchElementException e) { return null; } } final Pool.Entry.Instance instance = entry.soft.get(); if (instance != null) { final boolean notBusy = entry.active.compareAndSet(null, instance); if (notBusy) return entry; } else { // the SoftReference was garbage collected instances.release(); } entry = null; } return null; } /** * Attempt to aquire a permit to add the object to the pool. * * @param obj object to add to the pool * @return true of the item as added */ public boolean add(T obj) { return add(obj, 0); } /** * Attempt to aquire a permit to add the object to the pool. * * @param obj object to add to the pool * @param offset creation time offset, used for maxAge * @return true of the item as added */ public boolean add(T obj, long offset) { try { if (available.tryAcquire(100, MILLISECONDS)) { try { if (push(obj, offset)) return true; available.release(); } catch (RuntimeException e) { available.release(); throw e; } } return false; } catch (InterruptedException e) { Thread.interrupted(); e.printStackTrace(); } return false; } /** * Never call this method without having successfully called * {@link #pop(long, java.util.concurrent.TimeUnit)} beforehand. *

* Failure to do so will increase the max pool size by one. * * @param obj object to push onto the pool * @return false if the pool max size was exceeded */ public boolean push(T obj) { return push(obj, 0); } /** * Never call this method without having successfully called * {@link #pop(long, java.util.concurrent.TimeUnit)} beforehand. *

* Failure to do so will increase the max pool size by one. * * @param obj object to push onto the pool * @param offset * @return false if the pool max size was exceeded */ private boolean push(T obj, long offset) { if (instances.tryAcquire()){ return push(new Entry(obj, offset)); } if (obj != null) new Discard(obj, Event.FULL).run(); if (available instanceof Overdraft) available.release(); return false; } public boolean push(Entry entry) { return push(entry, false); } private boolean push(Entry entry, boolean sweeper) { boolean added = false; boolean release = true; Event event = Event.FULL; final Entry.Instance obj = (entry == null) ? null : entry.active.getAndSet(null); try { if (entry == null) return added; if (!sweeper) entry.markLastUsed(); final long age = now() - entry.created; final boolean aged = maxAge > 0 && age > maxAge; final boolean flushed = entry.version != this.poolVersion.get(); if (aged || flushed) { if (aged) event = Event.AGED; if (flushed) event = Event.FLUSHED; if (entry.hasHardReference() || (aged && replaceAged) || (flushed && replaceFlushed)) { // Don't release the lock, this // entry will be directly replaced release = false; entry.active.set(obj); executor.execute(new Replace(entry)); } } else { // make this a "min" instance if we can if (!entry.hasHardReference() && minimum.tryAcquire()) entry.hard.set(obj); synchronized (pool) { pool.addFirst(entry); } added = true; } } finally { if (release) { available.release(); if (entry != null && !added) { instances.release(); } } } if (!added && obj != null) { if (sweeper) { // if the caller is the PoolEviction thread, we do not // want to be calling discard() directly and should just // queue it up instead. executor.execute(obj.discard(event)); } else { obj.discard(event).run(); } } return added; } // private void println(String s) { // Thread thread = Thread.currentThread(); // PrintStream out = System.out; // synchronized (out) { // String s1 = thread.getName(); // out.format("%1$tH:%1$tM:%1$tS.%1$tL - %2$s - %3$s\n", System.currentTimeMillis(), s1, s); // out.flush(); // } // } /** * Used when a call to pop() was made that returned null * indicating that the caller has a permit to create an * object for this pool, but the caller will not be exercising * that permit and wishes intstead to return "null" to the pool. */ public void discard() { discard(null); } public void discard(Entry entry) { if (entry != null) { if (entry.hasHardReference()) { minimum.release(); } instances.release(); } available.release(); } public boolean close(long timeout, TimeUnit unit) throws InterruptedException { // drain all keys so no new instances will be accepted into the pool while (instances.tryAcquire()); while (minimum.tryAcquire()); // Stop the sweeper thread stop(); // flush and sweep flush(); sweeper.run(); // Drain all leases if (!(available instanceof Overdraft)) while (available.tryAcquire()); // Wait for any pending discards return out.await(timeout, unit); } /** * This internal method allows us to "swap" the status * of two entries before returning them to the pool. *

* This allows us to elect a replacement in the min pool * without ever loosing loosing pool consistency. *

* Neither argument is allowed to be null. * * @param hard the "min" pool item that will be discarded * @param soft the "min" pool item to replace the discarded instance */ private void discardAndReplace(Entry hard, Entry soft) { // The replacement becomes a hard reference -- a "min" pool item soft.hard.set(soft.active()); push(soft); // The discarded item becomes a soft reference hard.hard.set(null); discard(hard); } private static long now() { return TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS); } public class Entry { private final long created; private long used; private final int version; private final SoftReference soft; private final AtomicReference hard = new AtomicReference(); // Added this so the soft reference isn't collected // after the Entry instance is returned from a "pop" method // Also acts as an "inUse" boolean private final AtomicReference active = new AtomicReference(); /** * Constructor is private so that it is impossible for an Entry object * to exist without there being a corresponding permit issued for the * object wrapped by this Entry. *

* This helps ensure that when an Entry is returned to the pool it is * always safe to call {@link Semaphore#release()} which increases the * permit size by one. * * @param obj object that this Entry will wrap * @param offset creation time offset, used for maxAge */ private Entry(T obj, long offset) { if (obj == null) throw new NullPointerException("entry is null"); final Instance instance = new Instance(obj); this.soft = garbageCollection? new SoftReference(instance): new HardReference(instance); this.version = poolVersion.get(); this.active.set(instance); this.created = now() + offset; this.used = created; } public T get() { return active().instance; } private Instance active() { return active.get(); } public void harden() { hard.set(active()); } public void markLastUsed() { used = now(); } public long getUsed() { return used; } /** * Largely for testing purposes * * @return true if this entry is in the "min" pool */ public boolean hasHardReference() { return hard.get() != null; } @Override public String toString() { long now = now(); return "Entry{" + "min=" + (hard.get() != null) + ", age=" + (now - created) + ", idle=" + (now - used) + ", bean=" + soft.get() + '}'; } private class Discarded implements Runnable { public void run() { } } /** * Exists for the garbage collection related callbacks */ public class Instance { private final AtomicBoolean callback = new AtomicBoolean(); private final T instance; public Instance(T instance) { this.instance = instance; } @Override protected void finalize() throws Throwable { discard(Event.GC).run(); } public Runnable discard(Event event) { if (callback.compareAndSet(false, true)) { return new Discard(instance, event); } return new Discarded(); } } } private final class Sweeper extends TimerTask { private final AtomicInteger previousVersion = new AtomicInteger(poolVersion.get()); private final long idleTimeout; private final boolean timeouts; private final int max; private Sweeper(long idleTimeout, int max) { this.idleTimeout = idleTimeout; timeouts = maxAge > 0 || idleTimeout > 0; this.max = max; } public void run() { stats.sweeps.record(); final int currentVersion = poolVersion.get(); final boolean isCurrent = previousVersion.getAndSet(currentVersion) == currentVersion; // No timeouts to enforce? // Pool version not changed? // Just return if (!timeouts && isCurrent) return; final long now = now(); final List entries = new ArrayList(max); // Pull all the entries from the pool try { while (true) { final Entry entry = pop(0, MILLISECONDS, false); if (entry == null) { push(entry, true); break; } entries.add(entry); } } catch (InterruptedException e) { Thread.interrupted(); } catch (TimeoutException e) { // pool has been drained } final List expiredList = new ArrayList(max); { // Expire aged instances, enforce pool "versioning" // Any non-aged "min" refs are immediately returned final Iterator iter = entries.iterator(); while (iter.hasNext()) { final Entry entry = iter.next(); // is too old || is old version? final boolean aged = maxAge > 0 && now - entry.created > maxAge; final boolean flushed = entry.version != currentVersion; if (aged || flushed) { // Entry is too old, expire it iter.remove(); final Expired expired = new Expired(entry, aged ? Event.AGED : Event.FLUSHED); expiredList.add(expired); if (!expired.entry.hasHardReference() && !(aged && replaceAged)) { expired.tryDiscard(); } } else if (entry.hasHardReference()) { // This is an item from the "minimum" pool // and therefore cannot timeout in the next // algorithm. Return it immediately. push(entry, true); iter.remove(); } } } // At this point all Entries not eligible for idle timeout // have been returned to the pool and can now be in use. // There are no "null" and no min-pool ("hard") entries beyond // this point. Everything is a weak reference, possibly timed out. // If items from the "min" pool have expired, we will need // to return that number to the pool regardless of their // timeout setting so that they may take the place of the // expired instances Iterator discardables = expiredList.iterator(); while (discardables.hasNext() && entries.size() > 0) { if (discardables.next().replaceMinEntry(entries.get(0))) { entries.remove(0); } } // At this point all the expired "min" pool refs will have // been replaced with entries from our initial list. // // Unless, of course, we didn't have enough entries left over // to fill the "min" pool deficit. In that case, the entries // list will be empty and this loop will do nothing. final Iterator iter = entries.iterator(); while (iter.hasNext()) { final Entry entry = iter.next(); iter.remove(); // we know we're going to use it final long idle = now - entry.used; if (idleTimeout > 0 && idle > idleTimeout) { // too lazy -- timed out final Expired expired = new Expired(entry, Event.IDLE); expiredList.add(expired); expired.tryDiscard(); } else { push(entry, true); } } // Ok, now we have the task of invoking callbacks // on all the expired instances. // // If there are any "min" pool instances left over // we need to queue up creation of a replacement List replace = new ArrayList(); for (Expired expired : expiredList) { executor.execute(expired.entry.active().discard(expired.event)); if (expired.entry.hasHardReference() || expired.aged() && replaceAged) { replace.add(expired); } } for (int i = 0; i < replace.size(); i++) { long offset = maxAge > 0 ? ((long) (maxAge / replace.size() * i * maxAgeOffset)) % maxAge : 0l; executor.execute(new Replace(replace.get(i).entry, offset)); } } } public static enum Event { FULL, IDLE, AGED, FLUSHED, GC } private class Expired { private final Entry entry; private final AtomicBoolean discarded = new AtomicBoolean(); private final Event event; private Expired(Entry entry, Event event) { this.entry = entry; this.event = event; } public boolean aged() { return event == Event.AGED; } public boolean tryDiscard() { if (discarded.getAndSet(true)) return false; discard(entry); return true; } public boolean replaceMinEntry(Entry replacement) { if (!entry.hasHardReference()) return false; if (replacement.hasHardReference()) return false; if (discarded.getAndSet(true)) return false; discardAndReplace(entry, replacement); return true; } } private class Replace implements Runnable { private final Entry expired; private final long offset; private Replace(Entry expired) { this(expired, 0); } private Replace(Entry expired, long offset) { this.expired = expired; this.offset = offset; } public void run() { if (!running()) { discard(expired); return; } try { final T t = supplier.create(); if (t == null) { discard(expired); } else { final Entry entry = new Entry(t, offset); if (expired.hasHardReference()) entry.harden(); push(entry); } } catch (Throwable e) { // Retry and logging should be done in // the Supplier implementation discard(expired); } finally { stats.replaced.record(); } } } private class Discard implements Runnable { private final T expired; private final Event event; private Discard(T expired, Event event) { out.countUp(); if (expired == null) throw new NullPointerException("expired object cannot be null"); this.expired = expired; this.event = event; } public void run() { switch (event) { case AGED: stats.aged.record(); break; case FLUSHED: stats.flushed.record(); break; case FULL: stats.overdrafts.record(); break; case IDLE: stats.idleTimeouts.record(); break; case GC: stats.garbageCollected.record(); break; } try { supplier.discard(expired, event); } finally { out.countDown(); } } } public static interface Supplier { void discard(T t, Event reason); T create(); } private static class NoSupplier implements Supplier { public void discard(Object o, Event reason) { } public Object create() { return null; } } private static final class Overdraft extends Semaphore { private final AtomicInteger permits = new AtomicInteger(); public Overdraft(int permits) { super(0); this.permits.set(permits); } @Override public void acquire() throws InterruptedException { permits.decrementAndGet(); } @Override public void acquireUninterruptibly() { permits.decrementAndGet(); } @Override public boolean tryAcquire() { permits.decrementAndGet(); return true; } @Override public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { permits.decrementAndGet(); return true; } @Override public void acquire(int permits) throws InterruptedException { this.permits.addAndGet(-permits); } @Override public void acquireUninterruptibly(int permits) { this.permits.addAndGet(-permits); } @Override public boolean tryAcquire(int permits) { this.permits.addAndGet(-permits); return true; } @Override public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { this.permits.addAndGet(-permits); return true; } @Override public void release() { this.permits.incrementAndGet(); } @Override public void release(int permits) { this.permits.addAndGet(permits); } @Override public int availablePermits() { return permits.get(); } @Override public int drainPermits() { return 0; } @Override protected void reducePermits(int reduction) { } } @Managed private class Stats { @Managed private final org.apache.openejb.monitoring.Event sweeps = new org.apache.openejb.monitoring.Event(); @Managed private final org.apache.openejb.monitoring.Event flushes = new org.apache.openejb.monitoring.Event(); @Managed private final org.apache.openejb.monitoring.Event accessTimeouts = new org.apache.openejb.monitoring.Event(); @Managed private final org.apache.openejb.monitoring.Event garbageCollected = new org.apache.openejb.monitoring.Event(); @Managed private final org.apache.openejb.monitoring.Event idleTimeouts = new org.apache.openejb.monitoring.Event(); @Managed private final org.apache.openejb.monitoring.Event aged = new org.apache.openejb.monitoring.Event(); @Managed private final org.apache.openejb.monitoring.Event flushed = new org.apache.openejb.monitoring.Event(); @Managed private final org.apache.openejb.monitoring.Event overdrafts = new org.apache.openejb.monitoring.Event(); @Managed private final org.apache.openejb.monitoring.Event replaced = new org.apache.openejb.monitoring.Event(); @Managed private final int minSize; @Managed private final int maxSize; @Managed private long idleTimeout; private Stats(int minSize, int maxSize, long idleTimeout) { this.minSize = minSize; this.maxSize = maxSize; this.idleTimeout = idleTimeout; } @Managed private boolean getStrictPooling() { return !(available instanceof Overdraft); } @Managed private int getAvailablePermits() { return available.availablePermits(); } @Managed private int getInstancesPooled() { return maxSize - Pool.this.instances.availablePermits(); } @Managed private int getInstancesIdle() { return Math.max(0, getInstancesPooled() - getInstancesActive()); } @Managed private int getInstancesInitializing() { return Math.max(0, getInstancesActive() - getInstancesPooled()); } @Managed private int getInstancesActive() { return maxSize - getAvailablePermits(); } @Managed private int getMinimumInstances() { return minSize - minimum.availablePermits(); } } public static class Builder { private int max = 10; private int min = 0; private boolean strict = true; private Duration maxAge = new Duration(0, MILLISECONDS); private double maxAgeOffset = -1; private Duration idleTimeout = new Duration(0, MILLISECONDS); private Duration interval = new Duration(5 * 60, TimeUnit.SECONDS); private Supplier supplier; private Executor executor; private boolean replaceAged; private boolean replaceFlushed; private boolean garbageCollection = true; public Builder(Builder that) { this.max = that.max; this.min = that.min; this.strict = that.strict; this.maxAge = that.maxAge; this.idleTimeout = that.idleTimeout; this.interval = that.interval; this.executor = that.executor; this.supplier = that.supplier; this.maxAgeOffset = that.maxAgeOffset; this.replaceAged = that.replaceAged; this.replaceFlushed = that.replaceFlushed; this.garbageCollection = that.garbageCollection; } public Builder() { } public int getMin() { return min; } public boolean isGarbageCollection() { return garbageCollection; } public void setGarbageCollection(boolean garbageCollection) { this.garbageCollection = garbageCollection; } public void setReplaceAged(boolean replaceAged) { this.replaceAged = replaceAged; } public void setReplaceFlushed(boolean replaceFlushed) { this.replaceFlushed = replaceFlushed; } public void setMaxSize(int max) { this.max = max; } /** * Alias for pool size * @param max * @return */ public void setPoolSize(int max) { setMaxSize(max); } public void setMinSize(int min) { this.min = min; } public void setStrictPooling(boolean strict) { this.strict = strict; } public void setMaxAge(Duration maxAge) { this.maxAge = maxAge; } public Duration getMaxAge() { return maxAge; } public boolean isStrict() { return strict; } public Duration getIdleTimeout() { return idleTimeout; } public Duration getInterval() { return interval; } public boolean isReplaceAged() { return replaceAged; } public void setMaxAgeOffset(double maxAgeOffset) { this.maxAgeOffset = maxAgeOffset; } public double getMaxAgeOffset() { return maxAgeOffset; } public void setIdleTimeout(Duration idleTimeout) { this.idleTimeout = idleTimeout; } public void setSweepInterval(Duration interval) { this.interval = interval; } public void setSupplier(Supplier supplier) { this.supplier = supplier; } public void setExecutor(Executor executor) { this.executor = executor; } public Pool build() { return new Pool(max, min, strict, maxAge.getTime(MILLISECONDS), idleTimeout.getTime(MILLISECONDS), interval.getTime(MILLISECONDS), executor, supplier, replaceAged, maxAgeOffset, this.garbageCollection, replaceFlushed); } } public static class HardReference extends SoftReference { /** * Effectively disables the soft reference */ private final T hard; public HardReference(T referent) { super(referent); this.hard = referent; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy