package org.rrd4j.core; import java.io.IOException; import java.net.URI; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * This class should be used to synchronize access to RRD files * in a multithreaded environment. This class should be also used to prevent opening of * too many RRD files at the same time (thus avoiding operating system limits). * <p> * It's much more scalable than the previous pool */ public class RrdDbPool { private static class RrdDbPoolSingletonHolder { static final RrdDbPool instance = new RrdDbPool(); private RrdDbPoolSingletonHolder() {} } /** * Initial capacity of the pool i.e. maximum number of simultaneously open RRD files. The pool will * never open too many RRD files at the same time. */ public static final int INITIAL_CAPACITY = 200; private static class RrdEntry { RrdDb rrdDb = null; int count = 0; final CountDownLatch waitempty; final CountDownLatch inuse; final boolean placeholder; final URI uri; RrdEntry(boolean placeholder, URI canonicalPath) { this.placeholder = placeholder; this.uri = canonicalPath; if( placeholder) { inuse = new CountDownLatch(1); waitempty = null; } else { inuse = null; waitempty = new CountDownLatch(1); } } } /** * Creates a single instance of the class on the first call, * or returns already existing one. Uses Initialization On Demand Holder idiom. * * @return Single instance of this class * @throws java.lang.RuntimeException Thrown if the default RRD backend is not derived from the {@link org.rrd4j.core.RrdFileBackendFactory} */ public static RrdDbPool getInstance() { return RrdDbPoolSingletonHolder.instance; } private final AtomicInteger usage = new AtomicInteger(0); private final ReentrantLock countLock = new ReentrantLock(); private final Condition full = countLock.newCondition(); private int maxCapacity = INITIAL_CAPACITY; private final ConcurrentMap<URI, RrdEntry> pool = new ConcurrentHashMap<URI, RrdEntry>(INITIAL_CAPACITY); private final RrdBackendFactory defaultFactory; /** * Constructor for RrdDbPool. * * Not private, used by junit tests */ RrdDbPool() { if (!(RrdBackendFactory.getDefaultFactory() instanceof RrdFileBackendFactory)) { throw new RuntimeException("Cannot create instance of " + getClass().getName() + " with " + "a default backend factory not derived from RrdFileBackendFactory"); } defaultFactory = RrdBackendFactory.getDefaultFactory(); } /** * Returns the number of open RRD files. * * @return Number of currently open RRD files held in the pool. */ public int getOpenFileCount() { return usage.get(); } /** * Returns an array of open file URI. * * @return Array with {@link URI} to open RRD files held in the pool. */ public URI[] getOpenUri() { //Direct toarray from keySet can fail Set<URI> files = new HashSet<>(); files.addAll(pool.keySet()); return files.toArray(new URI[0]); } /** * Returns an array of open file path. * * @return Array with canonical path to open RRD files held in the pool. */ public String[] getOpenFiles() { //Direct toarray from keySet can fail Set<String> files = new HashSet<>(); for (RrdEntry i: pool.values()) { files.add(i.rrdDb.getPath()); } return files.toArray(new String[0]); } private RrdEntry getEntry(URI uri, boolean cancreate) throws IOException, InterruptedException { RrdEntry ref; do { ref = pool.get(uri); if(ref == null) { //Slot empty //If still absent put a place holder, and create the entry to return try { countLock.lockInterruptibly(); while(ref == null && usage.get() >= maxCapacity && cancreate) { full.await(); ref = pool.get(uri); } if(ref == null && cancreate) { ref = pool.putIfAbsent(uri, new RrdEntry(true, uri)); if(ref == null) { ref = new RrdEntry(false, uri); usage.incrementAndGet(); } } } finally { if(countLock.isHeldByCurrentThread()) { countLock.unlock(); } } } else if(! ref.placeholder) { // Real entry, try to put a place holder if some one didn't get it meanwhile if( ! pool.replace(uri, ref, new RrdEntry(true, uri))) { //Dummy ref, a new iteration is needed ref = new RrdEntry(true, uri); } } else { // a place holder, wait for the using task to finish ref.inuse.await(); } } while(ref != null && ref.placeholder); return ref; } private enum ACTION { SWAP, DROP; }; private void passNext(ACTION a, RrdEntry e) { RrdEntry o = null; switch (a) { case SWAP: o = pool.put(e.uri, e); break; case DROP: o = pool.remove(e.uri); if(usage.decrementAndGet() < maxCapacity) { try { countLock.lockInterruptibly(); full.signalAll(); countLock.unlock(); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); } } break; } //task finished, waiting on a place holder can go on if(o != null) { o.inuse.countDown(); } } /** * Releases RrdDb reference previously obtained from the pool. When a reference is released, its usage * count is decremented by one. If usage count drops to zero, the underlying RRD file will be closed. * * @param rrdDb RrdDb reference to be returned to the pool * @throws java.io.IOException Thrown in case of I/O error */ public void release(RrdDb rrdDb) throws IOException { // null pointer should not kill the thread, just ignore it if (rrdDb == null) { return; } URI dburi = rrdDb.getUri(); RrdEntry ref; try { ref = getEntry(dburi, false); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("release interrupted for " + rrdDb, e); } if(ref == null) { return; } if (ref.count <= 0) { passNext(ACTION.DROP, ref); throw new IllegalStateException("Could not release [" + rrdDb.getPath() + "], the file was never requested"); } if (--ref.count == 0) { if(ref.rrdDb == null) { passNext(ACTION.DROP, ref); throw new IllegalStateException("Could not release [" + rrdDb.getPath() + "], pool corruption"); } ref.rrdDb.close(); passNext(ACTION.DROP, ref); //If someone is waiting for an empty entry, signal it ref.waitempty.countDown(); } else { passNext(ACTION.SWAP, ref); } } /** * <p>Requests a RrdDb reference for the given RRD file path.</p> * <ul> * <li>If the file is already open, previously returned RrdDb reference will be returned. Its usage count * will be incremented by one. * <li>If the file is not already open and the number of already open RRD files is less than * {@link #INITIAL_CAPACITY}, the file will be open and a new RrdDb reference will be returned. * If the file is not already open and the number of already open RRD files is equal to * {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed. * </ul> * <p>The path is transformed internally to URI using the default factory, that is the reference that will * be used elsewhere.</p> * * @param path Path to existing RRD file * @return reference for the give RRD file * @throws java.io.IOException Thrown in case of I/O error */ public RrdDb requestRrdDb(String path) throws IOException { return requestRrdDb(defaultFactory.getUri(path), defaultFactory); } /** * <p>Requests a RrdDb reference for the given RRD file path.</p> * <ul> * <li>If the file is already open, previously returned RrdDb reference will be returned. Its usage count * will be incremented by one. * <li>If the file is not already open and the number of already open RRD files is less than * {@link #INITIAL_CAPACITY}, the file will be open and a new RrdDb reference will be returned. * If the file is not already open and the number of already open RRD files is equal to * {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed. * </ul> * * @param uri {@link URI} to existing RRD file * @return reference for the give RRD file * @throws java.io.IOException Thrown in case of I/O error */ public RrdDb requestRrdDb(URI uri) throws IOException { RrdBackendFactory factory = RrdBackendFactory.findFactory(uri); return requestRrdDb(uri, factory); } private RrdDb requestRrdDb(URI uri, RrdBackendFactory factory) throws IOException { RrdEntry ref = null; try { ref = getEntry(uri, true); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("request interrupted for " + uri, e); } //Someone might have already open it, rechecks if(ref.count == 0) { try { ref.rrdDb = new RrdDb(factory.getPath(uri), factory); } catch (IOException e) { passNext(ACTION.DROP, ref); throw e; } } ref.count++; passNext(ACTION.SWAP, ref); return ref.rrdDb; } /** * Wait for a empty reference with no usage * @param uri * @return an reference with no usage * @throws IOException * @throws InterruptedException */ private RrdEntry waitEmpty(URI uri) throws IOException, InterruptedException { RrdEntry ref = getEntry(uri, true); try { while(ref.count != 0) { //Not empty, give it back, but wait for signal passNext(ACTION.SWAP, ref); ref.waitempty.await(); ref = getEntry(uri, true); } return ref; } catch (InterruptedException e) { passNext(ACTION.DROP, ref); throw e; } } /** * Got an empty reference, use it only if slots are available * But don't hold any lock waiting for it * @param uri * @return an reference with no usage * @throws InterruptedException * @throws IOException */ private RrdEntry requestEmpty(URI uri) throws InterruptedException, IOException { RrdEntry ref = waitEmpty(uri); ref.count = 1; return ref; } /** * <p>Requests a RrdDb reference for the given RRD file definition object.</p> * <ul> * <li>If the file with the path specified in the RrdDef object is already open, * the method blocks until the file is closed. * <li>If the file is not already open and the number of already open RRD files is less than * {@link #INITIAL_CAPACITY}, a new RRD file will be created and a its RrdDb reference will be returned. * If the file is not already open and the number of already open RRD files is equal to * {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed. * </ul> * * @param rrdDef Definition of the RRD file to be created * @return Reference to the newly created RRD file * @throws java.io.IOException Thrown in case of I/O error */ public RrdDb requestRrdDb(RrdDef rrdDef) throws IOException { RrdEntry ref = null; try { URI uri = RrdBackendFactory.findFactory(rrdDef.getUri()).getCanonicalUri(rrdDef.getUri()); ref = requestEmpty(uri); ref.rrdDb = new RrdDb(rrdDef); return ref.rrdDb; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("request interrupted for new rrdDef " + rrdDef.getPath(), e); } finally { if(ref != null) { passNext(ACTION.SWAP, ref); } } } /** * <p>Requests a RrdDb reference for the given path. The file will be created from * external data (from XML dump or RRDTool's binary RRD file).</p> * <ul> * <li>If the file with the path specified is already open, * the method blocks until the file is closed. * <li>If the file is not already open and the number of already open RRD files is less than * {@link #INITIAL_CAPACITY}, a new RRD file will be created and a its RrdDb reference will be returned. * If the file is not already open and the number of already open RRD files is equal to * {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed. * </ul> * <p>The path is transformed internally to URI using the default factory, that is the reference that will * be used elsewhere.</p> * * @param path Path to RRD file which should be created * @param sourcePath Path to external data which is to be converted to Rrd4j's native RRD file format * @return Reference to the newly created RRD file * @throws java.io.IOException Thrown in case of I/O error */ public RrdDb requestRrdDb(String path, String sourcePath) throws IOException { RrdEntry ref = null; try { ref = requestEmpty(defaultFactory.getUri(path)); ref.rrdDb = new RrdDb(path, sourcePath); return ref.rrdDb; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("request interrupted for new rrd " + path, e); } finally { if(ref != null) { passNext(ACTION.SWAP, ref); } } } /** * <p>Requests a RrdDb reference for the given path. The file will be created from * external data (from XML dump or RRDTool's binary RRD file).</p> * <ul> * <li>If the file with the path specified is already open, * the method blocks until the file is closed. * <li>If the file is not already open and the number of already open RRD files is less than * {@link #INITIAL_CAPACITY}, a new RRD file will be created and a its RrdDb reference will be returned. * If the file is not already open and the number of already open RRD files is equal to * {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed. * </ul> * <p>The path is transformed internally to URI using the default factory, that is the reference that will * be used elsewhere.</p> * * @param uri Path to RRD file which should be created * @param sourcePath Path to external data which is to be converted to Rrd4j's native RRD file format * @return Reference to the newly created RRD file * @throws java.io.IOException Thrown in case of I/O error */ public RrdDb requestRrdDb(URI uri, String sourcePath) throws IOException { RrdEntry ref = null; try { ref = requestEmpty(uri); ref.rrdDb = new RrdDb(uri, sourcePath); return ref.rrdDb; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("request interrupted for new rrd " + uri, e); } finally { if(ref != null) { passNext(ACTION.SWAP, ref); } } } /** * Sets the maximum number of simultaneously open RRD files. * * @param newCapacity Maximum number of simultaneously open RRD files. */ public void setCapacity(int newCapacity) { int oldUsage = usage.getAndSet(maxCapacity); try { if (oldUsage != 0) { throw new RuntimeException("Can only be done on a empty pool"); } } finally { usage.set(oldUsage); } maxCapacity = newCapacity; } /** * Returns the maximum number of simultaneously open RRD files. * * @return maximum number of simultaneously open RRD files */ public int getCapacity() { return maxCapacity; } /** * Returns the number of usage for a RRD. * * @param rrdDb RrdDb reference for which informations is needed. * @return the number of request for this rrd * @throws java.io.IOException if any. */ public int getOpenCount(RrdDb rrdDb) throws IOException { return getOpenCount(rrdDb.getUri()); } /** * Returns the number of usage for a RRD. * * @param path RRD's path for which informations is needed. * @return the number of request for this file * @throws java.io.IOException if any. */ public int getOpenCount(String path) throws IOException { return getOpenCount(defaultFactory.getUri(path)); } /** * Returns the number of usage for a RRD. * * @param uri RRD's uri for which informations is needed. * @return the number of request for this file * @throws java.io.IOException if any. */ public int getOpenCount(URI uri) throws IOException { RrdEntry ref = null; try { ref = getEntry(uri, false); if(ref == null) return 0; else { return ref.count; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("getOpenCount interrupted", e); } finally { if(ref != null) { passNext(ACTION.SWAP, ref); } } } }