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

org.neo4j.driver.internal.pool.ThreadCachingPool Maven / Gradle / Ivy

There is a newer version: 5.28.5
Show newest version
/**
 * Copyright (c) 2002-2016 "Neo Technology,"
 * Network Engine for Objects in Lund AB [http://neotechnology.com]
 *
 * This file is part of Neo4j.
 *
 * Licensed 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.neo4j.driver.internal.pool;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.neo4j.driver.internal.util.Clock;
import org.neo4j.driver.internal.util.Consumer;
import org.neo4j.driver.v1.exceptions.Neo4jException;

import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * A general pool implementation, heavily inspired by Chris Vests "stormpot" pool, but without a background thread
 * managing allocation.
 * 

* Some quick info to understand this pool: *

  • * The pool caches a reference for each thread who uses this pool to the resource that has ever been assigned to the * thread by the pool. * Next time when the same thread wants to value a resource from the pool again, if the cached resource happens to be * free, the same resource will be assigned directly to the thread to avoid searching from the global pool. *
  • *
  • * The pool will fail all incoming resource requests once all the resources in the pool has been consumed. But the * resource requesting thread could choose to wait for a while for a possible available resource. *
  • * * @param A pool of T */ public class ThreadCachingPool implements AutoCloseable { /** * Keeps a reference to a locally cached pool slot, to avoid global lookups. * Other threads may still access the slot in here, if they cannot acquire an object from the global pool. */ private final ThreadLocal> local = new ThreadLocal<>(); /** Keeps references to slots that are likely (but not necessarily) live */ private final BlockingQueue> live = new LinkedBlockingQueue<>(); /** Keeps references to slots that have been disposed of. Used when re-allocating. */ private final BlockingQueue> disposed = new LinkedBlockingQueue<>(); /** * All slots in the pool, used when we shut down to dispose of all instances, as well as when there are no known * live pool objects, when we use this array to find slots cached by other threads */ private final Slot[] all; /** Max number of slots in the pool */ private final int maxSize; /** While the pool is initially populating, this tracks indexes into the {@link #all} array */ private final AtomicInteger nextSlotIndex = new AtomicInteger( 0 ); /** Shutdown flag */ private final AtomicBoolean stopped = new AtomicBoolean( false ); private final Allocator allocator; private final ValidationStrategy validationStrategy; private final Clock clock; public ThreadCachingPool( int targetSize, Allocator allocator, ValidationStrategy validationStrategy, Clock clock ) { this.maxSize = targetSize; this.allocator = allocator; this.validationStrategy = validationStrategy; this.clock = clock; this.all = new Slot[targetSize]; } public T acquire( long timeout, TimeUnit unit ) throws InterruptedException { long deadline = clock.millis() + unit.toMillis( timeout ); // 1. Try and value an object from our local slot Slot slot = local.get(); if ( slot != null && slot.availableToClaimed() ) { if ( slot.isValid( validationStrategy ) ) { allocator.onAcquire( slot.value ); return slot.value; } else { // We've acquired the slot, but the validation strategy says it's time for it to die. Dispose of it, // and go to the global pool. dispose( slot ); } } // 2. If that fails, acquire from big pool return acquireFromGlobal( deadline ); } private T acquireFromGlobal( long deadline ) throws InterruptedException { Slot slot = live.poll(); for (; ; ) { if ( stopped.get() ) { throw new IllegalStateException( "Pool has been closed, cannot acquire new values." ); } // 1. Check if the slot we pulled from the live queue is viable if ( slot != null ) { // Yay, got a slot - can we keep it? if ( slot.availableToClaimed() ) { if ( slot.isValid( validationStrategy ) ) { break; } else { // We've acquired the slot, but the validation strategy says it's time for it to die. dispose( slot ); } } } else { // 2. Exhausted the likely-to-be-live list, are there any disposed-of slots we can recycle? slot = disposed.poll(); if ( slot != null ) { // Got a hold of a previously disposed slot! slot = allocate( slot.index ); break; } // 3. Can we expand the pool? int index = nextSlotIndex.get(); if ( maxSize > index && nextSlotIndex.compareAndSet( index, index + 1 ) ) { slot = allocate( index ); break; } } // Enforce max wait time long timeLeft = deadline - clock.millis(); if ( timeLeft <= 0 ) { return null; } // Wait for a bit to see if someone releases something to the live queue slot = live.poll( Math.min( timeLeft, 10 ), MILLISECONDS ); } // Keep this slot cached with our thread, so that we can grab this value quickly next time, // assuming threads generally availableToClaimed one instance at a time local.set( slot ); allocator.onAcquire( slot.value ); return slot.value; } private void dispose( Slot slot ) { if ( !slot.claimedToDisposed() ) { throw new IllegalStateException( "Cannot dispose unclaimed pool object: " + slot ); } // Done before below, in case dispose call fails. This is safe since objects on the // pool are used for read-only operations disposed.add( slot ); allocator.onDispose( slot.value ); } /** * This method will create allocate a new value, returning the slot in the {@code CLAIMED} state. If allocation * fails, a slot for the same index will be added to the {@link #disposed} list, and an exception will be thrown. * @param slotIndex the slot index to use for the new item * @return a slot in the {@code CLAIMED} state */ private Slot allocate( int slotIndex ) { final Slot slot = new Slot<>( slotIndex, clock ); try { // Allocate the new item - this may fail with an exception slot.set( allocator.allocate( createDisposeCallback( slot ) ) ); // Store the slot in the global list of slots all[slotIndex] = slot; // Return it :) return slot; } catch( Neo4jException e ) { // Failed to allocate slot, return it to the list of disposed slots, rethrow exception. slot.claimedToDisposed(); disposed.add( slot ); throw e; } } private Consumer createDisposeCallback( final Slot slot ) { return new Consumer() { @Override public void accept( T t ) { slot.updateUsageTimestamp(); if ( !slot.isValid( validationStrategy ) ) { // The value has for some reason become invalid, dispose of it dispose( slot ); return; } if ( !slot.claimedToAvailable() ) { throw new IllegalStateException( "Failed to release pooled object: " + slot ); } // Make sure the pool isn't being stopped in the middle of all these shenanigans if ( !stopped.get() ) { // All good, as you were. live.add( slot ); } else { // Another thread concurrently closing the pool may have started closing before we // set our slot to "available". In that case, the slot will not be disposed of by the closing thread // We mitigate this by trying to claim the slot back - if we are able to, we dispose the slot. // If we can't claim the slot back, that means another thread is dealing with it. if ( slot.availableToClaimed() ) { dispose( slot ); } } } }; } @Override public void close() { if ( !stopped.compareAndSet( false, true ) ) { return; } for ( Slot slot : all ) { if ( slot != null && slot.availableToClaimed() ) { dispose( slot ); } } } } /** * Stores one pooled resource, along with pooling metadata about it. Every instance the pool manages * has one of these objects, independent of if it's currently in use or if it is idle in the pool. */ class Slot { enum State { AVAILABLE, CLAIMED, DISPOSED } final AtomicReference state = new AtomicReference<>( State.CLAIMED ); final int index; final Clock clock; long lastUsed; T value; public static Slot disposed( int index, Clock clock ) { Slot slot = new Slot<>( index, clock ); slot.claimedToDisposed(); return slot; } /** * @param index the index into the {@link ThreadCachingPool#all all} array, used to re-use that slot when this is * disposed */ Slot( int index, Clock clock ) { this.index = index; this.clock = clock; this.lastUsed = 0; } public void set( T value ) { this.value = value; } public boolean availableToClaimed() { return state.compareAndSet( State.AVAILABLE, State.CLAIMED ); } public boolean claimedToAvailable() { updateUsageTimestamp(); return state.compareAndSet( State.CLAIMED, State.AVAILABLE ); } public boolean claimedToDisposed() { return state.compareAndSet( State.CLAIMED, State.DISPOSED ); } public void updateUsageTimestamp() { lastUsed = clock.millis(); } boolean isValid( ValidationStrategy strategy ) { return strategy.isValid( value, clock.millis() - lastUsed ); } @Override public String toString() { return "Slot{" + "value=" + value + ", lastUsed=" + lastUsed + ", index=" + index + ", state=" + state.get() + '}'; } }




    © 2015 - 2025 Weber Informatics LLC | Privacy Policy