Principle and practice of apache common pool2

Keywords: Database Apache Redis

brief introduction

As the name suggests, object pool is the pool where objects are stored. Like thread pool, database connection pool and http connection pool, it is a typical pool design idea.

The advantage of object pool is that it can centrally manage the objects in the pool and reduce the frequent creation and destruction of long-term objects, so as to improve reusability and save resource consumption. It can effectively avoid frequent allocation of memory for objects and release memory in the heap, so as to reduce the burden of jvm garbage collector and avoid memory jitter.

Apache Common Pool2 is a general object pool technology implementation provided by Apache, which can easily customize the object pool you need. The famous Redis client Jedis internal connection pool is implemented based on it.

Core interface

The core internal classes of Apache Common Pool2 are as follows:

ObjectPool: object pool interface, object pool entity, and the place where objects are accessed

Object provision and return (factory operation): borowobject returnobject
Create object (using factory to create): addObject
Destroy object (use factory to destroy): invalideobject
Number of free objects and used objects in the pool: getNumActive getNumIdle
PooledObject: the wrapped object is an object in the pool. In addition to the object itself, it contains many information such as creation time, last called time, etc

PooledObjectFactory: object factory, which manages the life cycle of objects and provides a series of functions such as object creation, destruction, verification, passivation and activation

BaseObjectPoolConfig: provides some necessary configurations, such as whether the idle queue is first in first out, whether the factory needs to test before creating objects, and whether the objects are tested when they are taken out of the object pool. GenericObjectPoolConfig inherits this class and makes the default configuration. We can inherit it in actual use. We can expand the object pool configuration in combination with business conditions, For example, database connection pool thread prefix, string pool length or name rule, etc

Keyedobjectpool < K, V >: an object pool interface in the form of key value pairs, which is rarely used

Keyedpooledobjectfactory < K, V >: the same as above. It is the factory for managing objects in the key value pair object pool

The state of the pool object

Check the source code. All possible states of pool objects are listed under the PooledObjectState enumeration.

public enum PooledObjectState {
    //In the idle queue, it has not been used
    IDLE,
    //in use
    ALLOCATED,
    //In the idle queue, it is currently testing whether the eviction condition is met
    EVICTION,
	  //It is not in the idle queue and is currently being tested for possible eviction. Because during the test, an attempt was made to borrow an object and remove it from the queue.
    //After the recycle test is completed, it should be returned to the head of the queue.
    EVICTION_RETURN_TO_HEAD,
	  //In the queue, is being verified
    VALIDATION,
	  //Not in queue, currently validating. This object is borrowed during verification. Because testonmirror is configured,
    //So delete it from the queue and pre allocate it. Once validation is complete, it should be assigned.
    VALIDATION_PREALLOCATED,
	  //Not in queue, currently validating. An attempt was made to borrow the object before testing whether to remove it from the queue.
    //Once validation is complete, it should be returned to the head of the queue.
    VALIDATION_RETURN_TO_HEAD,
	  //Invalid status (e.g. expulsion test or verification) and will / have been destroyed
    INVALID,
	  //If it is determined as invalid, it will be set as abandoned
    ABANDONED,
	  //After using, return to the pool
    RETURNING
}

State understanding

Disabled: it is marked in this status if it has not been used for a long time after being lent. As shown in the code, when the object is in the ALLOCATED state, that is, it is being lent out for use, and the time since it was last used exceeds the set getRemoveAbandonedTimeout, it is marked as abandoned.

private void removeAbandoned(final AbandonedConfig abandonedConfig) {
    // Generate a list of abandoned objects to remove
    final long now = System.currentTimeMillis();
    final long timeout =
            now - (abandonedConfig.getRemoveAbandonedTimeout() * 1000L);
    final ArrayList<PooledObject<T>> remove = new ArrayList<>();
    final Iterator<PooledObject<T>> it = allObjects.values().iterator();
    while (it.hasNext()) {
        final PooledObject<T> pooledObject = it.next();
        synchronized (pooledObject) {
            if (pooledObject.getState() == PooledObjectState.ALLOCATED &&
                    pooledObject.getLastUsedTime() <= timeout) {
                pooledObject.markAbandoned();
                remove.add(pooledObject);
            }
        }
    }

Process understanding

1. Where is the real object stored?

 private PooledObject<T> create() throws Exception {
  		.....
 		final PooledObject<T> p;
        try {
            p = factory.makeObject();
        .....
        allObjects.put(new IdentityWrapper<>(p.getObject()), p);
        return p;
 }

When we look at all objects, all objects are stored in the ConcurrentHashMap, except for the killed objects.

/*
 * All of the objects currently associated with this pool in any state. It
 * excludes objects that have been destroyed. The size of
 * {@link #allObjects} will always be less than or equal to {@link
 * #_maxActive}. Map keys are pooled objects, values are the PooledObject
 * wrappers used internally by the pool.
 */
private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects =
    new ConcurrentHashMap<>();

2. The logic of access object is summarized as follows

First, according to the configuration of AbandonedConfig, judge whether to perform the cleanup operation before fetching the object
Then try to get the object from idleObject. If you can't get it, create a new object
Judge whether blockWhenExhausted is set to true (this configuration means whether to block until there are idle objects when the number of objects in the active state of the object pool has reached the maximum value of maximum)
If yes, wait for available objects according to the borrowMaxWaitMillis property set
After the object is available, the factory.activateObject method of the factory is invoked to activate the object.
When gettestonmirror is set to true, call factory.validateObject § to verify the object. After passing the verification, execute the next step
Call the updateStatsBorrow method to update some statistical items after the object is successfully lent, such as the number of objects returned to the object pool

//....
private final LinkedBlockingDeque<PooledObject<T>> idleObjects;
//....
public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
        assertOpen();
        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
                (getNumIdle() < 2) &&
                (getNumActive() > getMaxTotal() - 3) ) {
            removeAbandoned(ac);
        }
        PooledObject<T> p = null;
        // Get local copy of current config so it is consistent for entire
        // method execution
        final boolean blockWhenExhausted = getBlockWhenExhausted();
        boolean create;
        final long waitTime = System.currentTimeMillis();
        while (p == null) {
            create = false;
            p = idleObjects.pollFirst();
            if (p == null) {
                p = create();
                if (p != null) {
                    create = true;
                }
            }
            if (blockWhenExhausted) {
                if (p == null) {
                    if (borrowMaxWaitMillis < 0) {
                        p = idleObjects.takeFirst();
                    } else {
                        p = idleObjects.pollFirst(borrowMaxWaitMillis,
                                TimeUnit.MILLISECONDS);
                    }
                }
                if (p == null) {
                    throw new NoSuchElementException(
                            "Timeout waiting for idle object");
                }
            } else {
                if (p == null) {
                    throw new NoSuchElementException("Pool exhausted");
                }
            }
            if (!p.allocate()) {
                p = null;
            }
            if (p != null) {
                try {
                    factory.activateObject(p);
                } catch (final Exception e) {
                    try {
                        destroy(p, DestroyMode.NORMAL);
                    } catch (final Exception e1) {
                        // Ignore - activation failure is more important
                    }
                    p = null;
                    if (create) {
                        final NoSuchElementException nsee = new NoSuchElementException(
                                "Unable to activate object");
                        nsee.initCause(e);
                        throw nsee;
                    }
                }
                if (p != null && getTestOnBorrow()) {
                    boolean validate = false;
                    Throwable validationThrowable = null;
                    try {
                        validate = factory.validateObject(p);
                    } catch (final Throwable t) {
                        PoolUtils.checkRethrow(t);
                        validationThrowable = t;
                    }
                    if (!validate) {
                        try {
                            destroy(p, DestroyMode.NORMAL);
                            destroyedByBorrowValidationCount.incrementAndGet();
                        } catch (final Exception e) {
                            // Ignore - validation failure is more important
                        }
                        p = null;
                        if (create) {
                            final NoSuchElementException nsee = new NoSuchElementException(
                                    "Unable to validate object");
                            nsee.initCause(validationThrowable);
                            throw nsee;
                        }
                    }
                }
            }
        }
        updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
        return p.getObject();
    }

3. What is the use of the factory's passiveobject (pooledobject P) and passiveobject (pooledobject P), i.e. object activation and passivation methods?

As shown in the figure, when the object is returned to the object pool after use, if the verification fails, it is directly destroyed. If the verification passes, the object needs to be passivated first and then stored in the idle queue. As for the method of activating the object, it will also be activated first and then taken out when accessing the object above. Therefore, we can find that the idle and in use objects have different states. We can also add new differences between them by activating and passivating. For example, we want to make an Elasticsearch connection pool. Each object is a connection instance with ip and port. Obviously, the access es cluster is multiple different IPs, Therefore, the ip of each access is not necessarily the same. We can assign ip and port to the object in the activation operation, and set the ip and port to the default value or empty in the passivation operation, so that the process is more standard.

 public void returnObject(final T obj) {
        final PooledObject<T> p = allObjects.get(new IdentityWrapper<>(obj));
   			//....
       	//Verification failed. Destroy directly return
				//...
        try {
            factory.passivateObject(p);
        } catch (final Exception e1) {
            swallowException(e1);
            try {
                destroy(p, DestroyMode.NORMAL);
            } catch (final Exception e) {
                swallowException(e);
            }
            try {
                ensureIdle(1, false);
            } catch (final Exception e) {
                swallowException(e);
            }
            updateStatsReturn(activeTime);
            return;
        }
   			//......
   			//Return to idle queue
    }

Object pool related configuration items

The object pool provides many configuration items. In the default basic object pool of GenericObjectPool, we can pass parameters to GenericObjectPoolConfig through the construction method. Of course, we can also see the basic class BaseObjectPoolConfig implemented at the bottom of GenericObjectPoolConfig, which specifically includes the following configurations:

maxTotal: the maximum number of objects used in the object pool. The default value is 8

maxIdle: the maximum number of idle objects in an object. The default value is 8

minIdle: the minimum number of free objects in the object pool. The default is 8

lifo: whether to follow the principle of last in first out when obtaining idle instances in the object pool. The default value is true

blockWhenExhausted: whether to block the thread to get the instance when the object pool is in the exhausted state, that is, when the available instance is empty. The default is true

fairness: when the object pool is in the exhausted state, that is, when the available instances are empty, a large number of threads are blocking and waiting to obtain the available instances at the same time. fairness is configured to control whether to enable the fair lock algorithm, that is, first come, first served. The default is false. The premise of this item is that blockWhenExhausted is configured as true

maxWaitMillis: maximum blocking time. When the object pool is in the exhausted state, that is, the available instances are empty, a large number of threads are blocking at the same time waiting to obtain the available instances. If the blocking time exceeds maxWaitMillis, an exception will be thrown. When this value is negative, it means blocking indefinitely until it is available. The default is - 1

testOnCreate: whether to verify before creating an object (that is, call the validateObject() method of the factory). If the verification fails, the return of borobject () will fail. The default is false

Testonmirror: whether to check before fetching an object. The default value is false

testOnReturn: check before returning the object pool, that is, call the factory's returnObject(). If the check fails, the object will be destroyed instead of returned to the pool. The default value is false

Timebetween evictionrunsmillis: eviction cycle. The default value is - 1, which means no eviction test is performed

Testwhiteidle: whether the idle object in the idle queue is evicted by the evictor for verification. When the last running time of the object exceeds the value set by settimebetween evictionrunsmilis (long)), it will be evicted for verification. Call the validateObject() method. If the verification is successful, the object will be destroyed. The default is false

Use steps

Create factory class: inherit BaseGenericObjectPool or implement the basic interface PooledObjectFactory, and rewrite the creation, destruction, verification, activation and passivation methods of objects according to business requirements. Most of the destruction methods are connection closing, emptying, etc.
Create pool: inherit GenericObjectPool or implement the basic interface ObjectPool. It is recommended to use the former. It provides us with an idle object expulsion detection mechanism (that is, destroy the objects that have not been used for a long time in the idle queue to reduce memory consumption) and provides basic information about many objects, such as the last time the object was used Whether the object is inspected before use, etc.
Create pool related configuration (optional): increase the configuration control of thread pool by inheriting GenericObjectPoolConfig or BaseObjectPoolConfig. The former is recommended. It implements the basic method for us and only needs to add the required attributes.
Create wrapper class (optional): for the objects to exist in the object pool, add many basic attributes outside the actual objects to understand the real-time status of the objects in the object pool.

matters needing attention

Although we use the default implementation, we should also optimize it in combination with the actual production situation. We can't use thread pool, but the performance is lower. In use, we should pay attention to the following matters:

To set the maximum and minimum value of idle queue for the object pool, the default maximum and minimum value is 8, which is often not enough

private volatile int maxIdle = GenericObjectPoolConfig.DEFAULT_MAX_IDLE;
private volatile int minIdle = GenericObjectPoolConfig.DEFAULT_MIN_IDLE;
public static final int DEFAULT_MAX_IDLE = 8;
public static final int DEFAULT_MIN_IDLE = 0;

Set the maxWaitMillis property of the object pool, that is, the maximum waiting time of the object

After using the object, release the object in time and return the object to the pool. In particular, if an exception occurs, ensure the release through try... Chat... finally to avoid occupying resources

Let's talk about the precautions. First, why do we set maxWaitMillis? We use the following methods to get objects

public T borrowObject() throws Exception {
    return borrowObject(getMaxWaitMillis());
}

You can see that the default maximum wait time is - 1L

private volatile long maxWaitMillis =
        BaseObjectPoolConfig.DEFAULT_MAX_WAIT_MILLIS;
	//....
public static final long DEFAULT_MAX_WAIT_MILLIS = -1L;

Let's look at the object fetching logic again. blockWhenExhausted defaults to true, which means that when there are no idle objects in the pool, the thread will be blocked until there are new available objects. From the above, we know that - 1L will execute idleObjects.takeFirst()

public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
        //.......
        final boolean blockWhenExhausted = getBlockWhenExhausted();
        boolean create;
        final long waitTime = System.currentTimeMillis();
        while (p == null) {
          //.......
            if (blockWhenExhausted) {
                if (p == null) {
                    if (borrowMaxWaitMillis < 0) {
                        p = idleObjects.takeFirst();
                    } else {
                        p = idleObjects.pollFirst(borrowMaxWaitMillis,
                                TimeUnit.MILLISECONDS);
                    }
                }
            }
        }
}

As follows, the blocking queue will block until there are free objects. This setting will cause a large area of blocking when the throughput increases

   public E takeFirst() throws InterruptedException {
        lock.lock();
        try {
            E x;
            while ( (x = unlinkFirst()) == null) {
                notEmpty.await();
            }
            return x;
        } finally {
            lock.unlock();
        }
    }

Another note is to remember to recycle resources, that is, call the public void returnObject(final T obj) method. The reason is obvious. The object pool is insensitive to whether we have used up the object. We need to call this method to recycle the object. In particular, we should ensure recycling in case of abnormalities. Therefore, the best practice is as follows:

 try{
   item = pool.borrowObject();
 } catch(Exception e) {
   log.error("....");
 } finally {
   pool.returnObject(item);
 }

Instance use

Example 1: implement a simple string pool

Create string factory

package com.anqi.demo.demopool2.pool.fac;

import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;

/**
 * String pool factory
 */
public class StringPoolFac extends BasePooledObjectFactory<String> {
    public StringPoolFac() {
        super();
    }

    @Override
    public String create() throws Exception {
        return "str-val-";
    }

    @Override
    public PooledObject<String> wrap(String s) {
        return new DefaultPooledObject<>(s);
    }

    @Override
    public void destroyObject(PooledObject<String> p) throws Exception {
    }

    @Override
    public boolean validateObject(PooledObject<String> p) {
        return super.validateObject(p);
    }

    @Override
    public void activateObject(PooledObject<String> p) throws Exception {
        super.activateObject(p);
    }

    @Override
    public void passivateObject(PooledObject<String> p) throws Exception {
        super.passivateObject(p);
    }
}

Create string pool

package com.anqi.demo.demopool2.pool;

import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

/**
 * String pool
 */
public class StringPool extends GenericObjectPool<String> {
    public StringPool(PooledObjectFactory<String> factory) {
        super(factory);
    }

    public StringPool(PooledObjectFactory<String> factory, GenericObjectPoolConfig<String> config) {
        super(factory, config);
    }
}

Test main class

First, we set setMaxTotal to 2, that is, at most two objects are taken out for use, and set setMaxWaitMillis to 3S, that is, it is blocked for 3S at most. We cycle for 3 times without releasing resources

import com.anqi.demo.demopool2.pool.fac.StringPoolFac;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StringPoolTest {
    private static final Logger LOG = LoggerFactory.getLogger(StringPoolTest.class);

    public static void main(String[] args) {
        StringPoolFac fac = new StringPoolFac();
        GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(2);
        config.setMinIdle(1);
        config.setMaxWaitMillis(3000);
        StringPool pool = new StringPool(fac, config);
        for (int i = 0; i < 3; i++) {
            String s = "";
            try {
                s = pool.borrowObject();
                LOG.info("str:{}", s);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
//                if (!s.equals("")) {
//                    pool.returnObject(s);
//                }
            }
        }
    }
}

The results are as follows. After two successful calls, block 3S, and then the program stops with an error. This is because the maximum available resources are 2. If they are not released, no resources will be available. The new caller will be blocked for 3S, and then a false access failure will be reported.

16:18:42.499 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-
16:18:42.505 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-
java.util.NoSuchElementException: Timeout waiting for idle object

We release the annotation and get the normal execution result after releasing the resources

16:20:52.384 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-
16:20:52.388 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-
16:20:52.388 [main] INFO com.anqi.demo.demopool2.pool.StringPoolTest - str:str-val-

Posted by s-mack on Fri, 08 Oct 2021 02:04:26 -0700