Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

node-pool

A robust, configurable object pooling library for Node.js, modeled after Apache Commons Pool 2.

This library helps you manage expensive-to-create or limited resources (database connections, HTTP clients, worker instances, parsers, renderers, etc.) by maintaining a pool of reusable objects with well-defined lifecycle and validation rules.

Key Concepts

  • ObjectFactory<T>: Responsible for creating, destroying, and resetting objects (create, destroy, activate, passivate).
  • PooledObjectValidator<T>: A separate, pluggable component that decides whether an object is still valid. This is the modern, recommended way to handle validation.
  • GenericObjectPool<T>: The main pool implementation. It controls when validation occurs (testOnBorrow, testOnCreate, etc.) and how many validation failures to tolerate via maxValidationRetries.
  • PooledObject<T>: An internal wrapper that tracks metadata (create time, borrow count, idle time, etc.). Validators receive this wrapper plus context.

Validation is no longer part of ObjectFactory. This separation makes the design cleaner and more testable.

Installation

npm install node-pool

Quick Start

import { GenericObjectPool } from 'node-pool';

// 1. Define the factory (creation + lifecycle)
const resourceFactory = {
  async create() {
    return await createExpensiveResource();
  },
  async destroy(resource) {
    await resource.cleanup();
  },
  async activate(resource) {
    // Reset state before handing to a borrower
  },
  async passivate(resource) {
    // Prepare the resource for return to the pool
  }
};

// 2. Define a validator (recommended approach)
const resourceValidator = {
  validate(pooledObject, context) {
    const resource = pooledObject.getObject();
    // You can use pooledObject metadata + context
    if (context.reason === 'borrow' && context.attemptNumber > 1) {
      console.log(`Retry attempt #${context.attemptNumber}`);
    }
    return resource.isHealthy();
  }
};

// 3. Create the pool
const pool = new GenericObjectPool(resourceFactory, {
  maxTotal: 10,
  maxIdle: 5,
  minIdle: 2,
  validator: resourceValidator,
  testOnBorrow: true,
  testOnCreate: true,
  maxValidationRetries: 3,           // default is 3
  timeBetweenEvictionRuns: 30000,
  minEvictableIdleTime: 5 * 60 * 1000
});

// 4. Borrow and return
const resource = await pool.borrowObject();
try {
  await resource.doWork();
} finally {
  await pool.returnObject(resource);
}

// Pre-populate idle objects
await pool.addObjects(3);

// Inspect
console.log(pool.getStats());

// Shutdown (destroys idle objects)
await pool.close();

ObjectFactory Contract

interface ObjectFactory<T> {
  create(): Promise<T> | T;
  destroy(obj: T): Promise<void> | void;
  activate?(obj: T): Promise<void> | void;   // called before borrow
  passivate?(obj: T): Promise<void> | void;  // called on return
}

The factory should focus purely on creation, destruction, and state reset. Do not put health checks here anymore.

PooledObjectValidator (Recommended)

Validation is now handled by a dedicated PooledObjectValidator. This is more powerful because validators receive the PooledObject wrapper and a context object.

interface PooledObjectValidator<T> {
  validate(
    pooledObject: PooledObject<T>,
    context: { reason: 'create' | 'borrow' | 'return' | 'idle'; attemptNumber: number }
  ): boolean | Promise<boolean>;
}

Why a separate validator?

  • Clear separation of concerns.
  • Access to rich metadata (getBorrowedCount(), idle time, create time, etc.).
  • attemptNumber enables sophisticated retry-aware validation logic.
  • Easy to swap validation strategies without touching creation code.

Using the validator + retry configuration

const pool = new GenericObjectPool(factory, {
  validator: {
    validate(pooledObject, context) {
      const obj = pooledObject.getObject();
      // Example: be more lenient on later attempts
      if (context.attemptNumber > 1) {
        return obj.isUsableWithWarnings();
      }
      return obj.isHealthy();
    }
  },
  maxValidationRetries: 3,   // how many bad objects we will discard before giving up
  testOnBorrow: true,
  testOnCreate: true
});

Behavior:

  • When testOnBorrow or testOnCreate is enabled, the pool validates newly acquired objects.
  • If validation fails, the object is destroyed and the pool tries again (up to maxValidationRetries times).
  • If the pool is below maxTotal, it will prefer creating a fresh object rather than immediately failing.
  • After exhausting the retry budget with no valid object, borrowObject() throws NoSuchElementException with a descriptive message.

You can also use the helper:

import { createPooledObjectValidator } from 'node-pool';

const validator = createPooledObjectValidator((obj, context) => {
  return obj.isHealthy();
});

Configuration Reference

Option Default Description
maxTotal 8 Hard limit on total objects (active + idle)
maxIdle 8 Maximum idle objects to keep
minIdle 0 Minimum idle objects the pool tries to maintain
maxValidationRetries 3 Max number of validation failures tolerated during a single borrow/create before giving up
validator null PooledObjectValidator instance (recommended)
testOnCreate false Validate objects immediately after creation
testOnBorrow false Validate objects before returning them to callers
testOnReturn false Validate objects when they are returned to the pool
testWhileIdle false Validate idle objects during background eviction
maxWait -1 Max time (ms) to wait for an object when exhausted (-1 = wait forever)
blockWhenExhausted true Whether to block when no objects are available
lifo true true = return most recently used first
fairness false Fair (FIFO) waiting for exhausted pools
timeBetweenEvictionRuns -1 How often (ms) the background evictor runs
minEvictableIdleTime 30min Minimum time an object must be idle before it can be evicted
softMinEvictableIdleTime -1 Like above, but will not drop below minIdle

Common Usage Patterns

Safe Borrow Pattern

Always use try/finally (or equivalent) to guarantee return:

const obj = await pool.borrowObject();
try {
  // use obj
} finally {
  await pool.returnObject(obj);
}

Invalidating Bad Objects

If an object becomes unusable while you have it, invalidate it instead of returning:

const obj = await pool.borrowObject();
try {
  await doWork(obj);
} catch (err) {
  if (isFatal(err)) {
    await pool.invalidateObject(obj);
    return;
  }
  throw err;
} finally {
  // Only return if we didn't invalidate
  if (/* still valid */) {
    await pool.returnObject(obj);
  }
}

Pre-warming the Pool

await pool.preparePool();           // ensure minIdle objects exist
await pool.addObjects(10);          // add more idle objects proactively

Monitoring & Statistics

const stats = pool.getStats();
console.log(stats);
// { createdCount, destroyedCount, borrowedCount, returnedCount, numActive, numIdle, ... }

Best Practices & Guidelines

  1. Prefer PooledObjectValidator over embedding validation in the factory.
  2. Keep maxValidationRetries small (the default of 3 is usually sufficient). High values can hide factory bugs.
  3. Use testOnBorrow for correctness-critical resources. Use testWhileIdle for expensive health checks that you don't want on the hot path.
  4. Implement activate / passivate to make objects safe for reuse without leaking state between borrowers.
  5. Always return or invalidate — never drop references to borrowed objects.
  6. Set reasonable minEvictableIdleTime and timeBetweenEvictionRuns to avoid thrashing the evictor.
  7. Use getStats() in production for observability (especially destroyedByBorrowValidationCount).
  8. Close the pool on shutdown so idle resources are properly cleaned up.

Error Types

  • NoSuchElementException — thrown by borrowObject() when the pool cannot provide a valid object (exhausted, closed, or too many validation failures).
  • IllegalStateException / PoolClosedException — returned objects must have been obtained from this pool and not already returned/invalidated.

Advanced Topics

Custom Eviction Policy

import { DefaultEvictionPolicy } from 'node-pool';

const pool = new GenericObjectPool(factory, {
  validator,
  evictionPolicy: new MyCustomEvictionPolicy(),
  timeBetweenEvictionRuns: 10000
});

LIFO vs FIFO

  • lifo: true (default) — most recently returned objects are handed out first (good for cache locality).
  • lifo: false — oldest objects are handed out first.

Fairness

Set fairness: true if you want threads waiting for objects to be served in FIFO order when the pool is exhausted.

Comparison to Apache Commons Pool 2

This library follows the same core ideas:

  • Separate factory for lifecycle vs validator for health.
  • PooledObject for metadata.
  • Configurable validation points + bounded retry on acquisition.
  • Idle object eviction with pluggable policy.

It is deliberately general purpose — not tied to any specific resource type.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages