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

com.bigdata.io.DirectBufferPoolAllocator Maven / Gradle / Ivy

/**

Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016.  All rights reserved.

Contact:
     SYSTAP, LLC DBA Blazegraph
     2501 Calvert ST NW #106
     Washington, DC 20008
     [email protected]

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
/*
 * Created on Sep 8, 2010
 */

package com.bigdata.io;

import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;

import com.bigdata.service.ResourceService;
import com.bigdata.util.concurrent.Haltable;

/**
 * An allocator for {@link ByteBuffer} slices backed by direct
 * {@link ByteBuffer}s allocated against a {@link DirectBufferPool}.
 * 
 * @author Bryan Thompson
 * @version $Id$
 * 
 * @todo Make the type of the identifier for the {@link IAllocation} generic
 *       using a factory pattern (we need {@link UUID} for scale-out, but the
 *       class could be reused for other purposes as well). [The allocation
 *       context identifier should continue to be an application specified
 *       object.]
 */
public class DirectBufferPoolAllocator {

    /**
     * The pool from which the direct {@link ByteBuffer}s are allocated.
     */
    private final DirectBufferPool directBufferPool;

    /**
     * The set of allocation contexts.
     */
    private final ConcurrentHashMap allocationContexts = new ConcurrentHashMap();

    /**
     * The set of {@link IAllocation} outstanding against the
     * {@link #directBufferPool}.
     */
    private final ConcurrentHashMap allocations = new ConcurrentHashMap();

    /**
     * @todo Maybe replace this with a private {@link Haltable} (or extend
     *       {@link Haltable}) so we can test {@link Haltable#halted()} in
     *       critical methods? If we expose the {@link Haltable} then the
     *       {@link ResourceService} can also check it to see whether all
     *       allocations have been invalidated. However, that will not help us
     *       to invalidate a specific {@link IAllocationContext}. For that
     *       purpose we would need to do pretty much the same thing recursively.
     */
    private final AtomicBoolean open = new AtomicBoolean(true);
    
    /**
     * 
     * @param pool
     *            The pool from which the direct {@link ByteBuffer}s are
     *            allocated.
     */
    public DirectBufferPoolAllocator(final DirectBufferPool pool) {
        
        this.directBufferPool = pool;
        
    }

    /**
     * Extended to {@link #close()} the allocator.
     */
    @Override
    protected void finalize() throws Throwable {
        
        close();
        
        super.finalize();
        
    }
    
    /**
     * Releases all {@link AllocationContext}s and all direct {@link ByteBuffer}
     * s which they are using.
     */
    public void close() {

        if (open.compareAndSet(true/* expect */, false/* update */)) {

            for (AllocationContext c : allocationContexts.values()) {

                c.release();

            }

        }

    }

    /**
     * The maximum #of bytes in a single {@link IAllocation}.
     */
    public int getMaxSlotSize() {

        return directBufferPool.getBufferCapacity();

    }
    
    /**
     * Return an allocation context for the key. If none exists for that key,
     * then one is atomically created and returned.
     * 
     * @param key
     *            A key which uniquely identifies that context. The key will be
     *            inserted into a hash table and therefore must have appropriate
     *            hashCode() and equals() methods.
     * 
     * @return The allocation context.
     */
    public IAllocationContext getAllocationContext(final Object key) {

        AllocationContext c = allocationContexts.get(key);

        if (c == null) {

            final AllocationContext t = allocationContexts.putIfAbsent(key,
                    c = new AllocationContext(key));

            if (t != null) {

                // lost the race to another thread.
                c = t;

            }

        }

        return c;
        
    }
    
    /**
     * Return the allocation associated with that id.
     * 
     * @param id
     *            The allocation identifier.
     * 
     * @return The allocation -or- null if there is no such
     *         allocation.
     */
    public IAllocation getAllocation(final UUID id) {

        return allocations.get(id);

    }
    
//    /**
//     * A direct {@link ByteBuffer} allocated from the {@link #directBufferPool}
//     * together with the identifier assigned to that {@link ByteBuffer} (we can
//     * not directly insert {@link ByteBuffer}s into the keys of a hash map since
//     * their hash code is a function of their content).
//     */
//    private class DirectBufferAllocation {
//
//        private final Long id;
//
//        private final ByteBuffer directBuffer;
//
//        public DirectBufferAllocation(final Long id,
//                final ByteBuffer directBuffer) {
//
//            if (id == null)
//                throw new IllegalArgumentException();
//            
//            if (directBuffer == null)
//                throw new IllegalArgumentException();
//            
//            this.id = id;
//
//            this.directBuffer = directBuffer;
//            
//        }
//
//    }
    
    /**
     * An allocation context links some application specified key with a list
     * of direct {@link ByteBuffer}s on which allocations have been made by
     * the application.
     */
    public interface IAllocationContext {

        /**
         * Allocate a series of {@link ByteBuffer} slices on which the
         * application may write data. The application is encouraged to maintain
         * the order of the allocations in the array in order to preserve the
         * ordering of data written onto those allocation.
         * 
         * @param nbytes
         *            The #of bytes required.
         * 
         * @return The {@link UUID}s of those allocations.
         * 
         * @throws InterruptedException
         */
        IAllocation[] alloc(int nbytes) throws InterruptedException;

        /**
         * Release all allocations made against this allocation context.
         */
        void release();

    }

    /**
     * An allocation against a direct {@link ByteBuffer}.
     */
    public interface IAllocation {
       
        /** The allocation identifier. */
        public UUID getId();

        /**
         * The allocated {@link ByteBuffer#slice()}.
         */
        public ByteBuffer getSlice();

        /**
         * Release this allocation.
         * 

* Note: The implementation is encouraged to release the associated * direct {@link ByteBuffer} if there are no remaining allocations * against it and MAY made the slice of the buffer available for * reallocation. *

* Note: An {@link InterruptedException} MAY be thrown. This allows us * to handle cases where a concurrent process (such as a query) was * halted and its component threads were interrupted. By looking for the * interrupt, we can avoid attempts to release an allocation in some * thread where the entire {@link IAllocationContext} has already been * released by another thread. * * @throws InterruptedException */ public void release() throws InterruptedException; } /** * An allocation against a direct {@link ByteBuffer}. */ // Note: package private for the unit tests. /*private*/ class Allocation implements IAllocation { private final AllocationContext allocationContext; /** The allocation identifier. */ final private UUID id; /** * The direct {@link ByteBuffer} against which the allocation was made. * * @todo Allow incremental recycling of allocations. To do this we would * keep an allocation map * * @todo Allow incremental release of direct buffers as allocations are * released. */ // Note: package private for the unit tests. final /*private*/ IBufferAccess nativeBuffer; /** * A {@link ByteBuffer#slice()} onto the allocated region of the * {@link #nativeBuffer}. The slice is cleared when the allocation is * released. */ private volatile ByteBuffer allocatedSlice; public UUID getId() { return id; } public ByteBuffer getSlice() { if (!open.get()) { // allocator was closed. throw new IllegalStateException(); } final ByteBuffer t = allocatedSlice; if(t == null) { // slice was released. throw new IllegalStateException(); } return t; } private Allocation(final AllocationContext allocationContext, final IBufferAccess nativeBuffer, final ByteBuffer allocatedSlice) { if (allocationContext == null) throw new IllegalArgumentException(); if (nativeBuffer == null) throw new IllegalArgumentException(); if (allocatedSlice == null) throw new IllegalArgumentException(); this.allocationContext = allocationContext; this.id = UUID.randomUUID(); this.nativeBuffer = nativeBuffer; this.allocatedSlice = allocatedSlice; } public void release() throws InterruptedException { allocationContext.release(this); allocatedSlice = null; } public String toString() { return getClass().getName() + "{id=" + id + ",slice=" + allocatedSlice + ",context=" + allocationContext + "}"; } } // class Allocation /** * An allocation context links some application specified key with a list of * direct {@link ByteBuffer}s on which allocations have been made by the * application. */ // Note: package private for the unit tests. /*private*/ class AllocationContext implements IAllocationContext { /** * The application key for this allocation context. */ private final Object key; /** * Lock guarding allocations made by this allocation context. */ private final ReentrantLock lock = new ReentrantLock(); /** * The set of native {@link ByteBuffer}s in use by this allocation * context. Last element in the list is the {@link ByteBuffer} against * which allocations are currently being made. */ private final LinkedList directBuffers = new LinkedList(); /** * The set of native {@link ByteBuffer}s in use by this allocation * context. The positive of in that buffer is the next byte available in * that buffer. The limit is the buffer capacity. Mutable views of this * buffer object are published by {@link #alloc(int)}. Those views have * independent position, offset, and limit from the original * {@link ByteBuffer}. */ private final ConcurrentHashMap allocations = new ConcurrentHashMap(); /** * Human readable summary (non-blocking). *

* Note: The #of native buffers is approximate since this method does * not acquire the lock guarding that field and thus can not ensure * timely visibility. */ public String toString() { return getClass().getName() + "{key=" + key + ",#allocations=" + allocations.size() + ",#nativeBuffers=" + directBuffers.size() + "}"; } public AllocationContext(final Object key) { if (key == null) throw new IllegalArgumentException(); this.key = key; } public IAllocation[] alloc(int nbytes) throws InterruptedException { if (nbytes <= 0) throw new IllegalArgumentException(); lock.lock(); try { final LinkedList allocations = new LinkedList(); while (nbytes > 0) { IBufferAccess directBuffer = directBuffers.peekLast(); if (directBuffer == null) { if (!open.get()) { /* * The allocation pool is closed. * * Note: The lock serializes these decisions since * otherwise allocations could be made concurrent * with a shutdown and release of the buffers in the * pool which would be a memory leak. */ throw new IllegalStateException(); } directBuffers.add(directBuffer = directBufferPool .acquire()); } final ByteBuffer nativeBuffer = directBuffer.buffer(); final int remaining = nativeBuffer.remaining(); final int allocSize = Math.min(remaining, nbytes); final int start = nativeBuffer.position(); final int limit = start + allocSize; nativeBuffer.limit(limit); // create the slice. final Allocation a = new Allocation(this, directBuffer, nativeBuffer.slice()); // restore limit to the remaining capacity. nativeBuffer.limit(nativeBuffer.capacity()); // advance position over the allocated slice. nativeBuffer.position(limit); // remember allocation in the global scope. DirectBufferPoolAllocator.this.allocations.put(a.id, a); // remember allocation in the allocation context scope. this.allocations.put(a.id, a); // add to list of allocations made for this request. allocations.add(a); // subtract out the #of bytes allocated. nbytes -= allocSize; } return allocations.toArray(new IAllocation[allocations.size()]); } finally { lock.unlock(); } } /** * Release all direct {@link ByteBuffer}s associated with this * allocation context back to the {@link #directBufferPool}. If the * current thread is interrupted, the interrupt will be trapped and the * thread will be interrupted on exit (after all allocations have been * released). */ public void release() { boolean interrupted = false; // Note: lock is NOT interruptable. lock.lock(); try { for(Allocation a : allocations.values()) { // clear the slice reference. a.allocatedSlice = null; } // clear collection. allocations.clear(); // release backing buffers. final Iterator bitr = directBuffers.iterator(); while(bitr.hasNext()) { final IBufferAccess b = bitr.next(); while (true) { try { b.release(); break; } catch (InterruptedException e) { interrupted = true; } finally { bitr.remove(); } } } } finally { lock.unlock(); } if (interrupted) Thread.currentThread().interrupt(); } /** * Release the allocation. * * @throws InterruptedException * if the thread was interrupted */ private void release(final Allocation a) throws InterruptedException { if (a == null) throw new IllegalArgumentException(); lock.lockInterruptibly(); try { if (!allocations.remove(a.id, a)) { /* * The allocation was not found in the map. */ throw new RuntimeException(); } } finally { lock.unlock(); } } } /** * Copy the caller's data onto the ordered array of allocations. The * position of each {@link IAllocation#getSlice() allocation} will be * advanced by the #of bytes copied into that allocation. * * @param src * The source data. * @param a * The allocations. * * @throws BufferOverflowException * if there is not enough room in the allocations for the data * to be copied. */ static public void put(final byte[] src, final IAllocation[] a) { int offset = 0; int remaining = src.length; for (int i = 0; i < a.length && remaining > 0; i++) { final ByteBuffer slice = a[i].getSlice(); final int length = Math.min(remaining, slice.remaining()); slice.put(src, offset, length); offset += length; remaining -= length; } if (remaining > 0) throw new BufferOverflowException(); } /** * Copy the caller's data onto the ordered array of allocations. The * position of each {@link IAllocation#getSlice() allocation} will be * advanced by the #of bytes copied into that allocation. * * @param src * The source data. * @param a * The allocations. * * @throws BufferOverflowException * if there is not enough room in the allocations for the data * to be copied. */ static public void put(final ByteBuffer src, final IAllocation[] a) { int remaining = src.remaining(); for (int i = 0; i < a.length && remaining > 0; i++) { final ByteBuffer slice = a[i].getSlice(); final int length = Math.min(remaining, slice.remaining()); slice.put(src); remaining -= length; } if (remaining > 0) throw new BufferOverflowException(); } }