Skip to main content

Generic pool

The eridu-tech/generic-pool component reuses expensive resources instead of creating a new one for every call. The examples below pool SFTP connections created with ssh2-sftp-client, which are slow to open and cannot be shared by concurrent operations.

Contracts​

The contracts are exported from eridu-tech/generic-pool/contracts:

  • IResourceFactory creates a resource, destroys it and optionally validates it before it is reused.
  • IGenericPoolBase borrows a client with acquireOrFail(), returns it with release() and runs a callback with a client through run().
  • IGenericPool extends IGenericPoolBase with close().

QueuePool​

QueuePool creates resources on demand and queues callers in first-in-first-out order when no resource is available. An SftpClient is connected when the pool creates it and ended when the pool destroys it:

./samples/queue-pool.ts
import { QueuePool } from "eridu-tech/generic-pool";
import SftpClient from "ssh2-sftp-client";

const connectionFactory = {
create: async (): Promise<SftpClient> => {
const client = new SftpClient("sftp-connection");
await client.connect({
host: "sftp.example.com",
port: 22,
username: "user",
password: "password",
});
return client;
},
destroy: async (client: SftpClient): Promise<void> => {
await client.end();
},
};

const sftpPool = new QueuePool(connectionFactory);

const fileNames = await sftpPool.run(async (client) => {
const files = await client.list("/uploads");
return files.map((file) => file.name);
});

console.log(fileNames);

The pool is configured with the following settings:

  • maxResources - Maximum number of resources the pool can create. Default 10.
  • maxWaitSize - Maximum number of callers that can wait for a resource, null means no limit. Default null.
./samples/queue-pool-settings.ts
import { QueuePool } from "eridu-tech/generic-pool";
import SftpClient from "ssh2-sftp-client";

const sftpPool = new QueuePool<SftpClient>(
{
create: async () => {
const client = new SftpClient("sftp-connection");
await client.connect({
host: "sftp.example.com",
port: 22,
username: "user",
password: "password",
});
return client;
},
destroy: async (client) => {
await client.end();
},
},
{
maxResources: 5,
maxWaitSize: 100,
},
);

Instead of run(), you can borrow a client manually. Always release it when you are done, preferably in a finally block:

./samples/queue-pool-acquire-release.ts
import { QueuePool } from "eridu-tech/generic-pool";
import SftpClient from "ssh2-sftp-client";

const sftpPool = new QueuePool<SftpClient>({
create: async () => {
const client = new SftpClient("sftp-connection");
await client.connect({
host: "sftp.example.com",
port: 22,
username: "user",
password: "password",
});
return client;
},
destroy: async (client) => {
await client.end();
},
});

const client = await sftpPool.acquireOrFail();

try {
await client.put("./report.pdf", "/uploads/report.pdf");
} finally {
await sftpPool.release(client);
}

A recycled connection is checked with the optional IResourceFactory.validate(). When it returns false or throws, the connection is ended and replaced, and the caller goes back to the front of the queue. Here cwd() reports whether the connection still responds:

./samples/queue-pool-validate.ts
import { QueuePool } from "eridu-tech/generic-pool";
import SftpClient from "ssh2-sftp-client";

const sftpPool = new QueuePool<SftpClient>({
create: async () => {
const client = new SftpClient("sftp-connection");
await client.connect({
host: "sftp.example.com",
port: 22,
username: "user",
password: "password",
});
return client;
},
destroy: async (client) => {
await client.end();
},
// A connection that no longer responds is ended and replaced.
validate: async (client) => {
try {
await client.cwd();
return true;
} catch {
return false;
}
},
});

Closing the pool ends all idle connections and rejects every waiting caller. Connections that are still borrowed are ended when they are released:

./samples/queue-pool-close.ts
import { QueuePool } from "eridu-tech/generic-pool";
import { UnableToAcquireError } from "eridu-tech/generic-pool/contracts";
import SftpClient from "ssh2-sftp-client";

const sftpPool = new QueuePool<SftpClient>({
create: async () => {
const client = new SftpClient("sftp-connection");
await client.connect({
host: "sftp.example.com",
port: 22,
username: "user",
password: "password",
});
return client;
},
destroy: async (client) => {
await client.end();
},
});

const client = await sftpPool.acquireOrFail();

// Ends the idle connections, waiting callers are rejected.
await sftpPool.close();

// Later acquisitions are rejected too.
try {
await sftpPool.acquireOrFail();
} catch (error) {
console.log(error instanceof UnableToAcquireError);
}

// The borrowed connection is ended when it is released.
await sftpPool.release(client);

NoOpPool​

NoOpPool gives the same client to every caller and ignores releases. Use it when the connection is already exclusive, for example when your application opens one connection itself:

./samples/no-op-pool.ts
import { NoOpPool } from "eridu-tech/generic-pool";
import SftpClient from "ssh2-sftp-client";

const client = new SftpClient("shared-sftp-connection");
await client.connect({
host: "sftp.example.com",
port: 22,
username: "user",
password: "password",
});

const sftpPool = new NoOpPool({
client,
destroy: async (sftpClient) => {
await sftpClient.end();
},
});

const first = await sftpPool.acquireOrFail();
const second = await sftpPool.acquireOrFail();

// logs true, every caller receives the same connection
console.log(first === second);

await sftpPool.close();

Errors​

acquireOrFail() rejects with UnableToAcquireError when a client cannot be acquired, for example when the wait queue is full or the pool has been closed:

./samples/unable-to-acquire-error.ts
import { QueuePool } from "eridu-tech/generic-pool";
import { UnableToAcquireError } from "eridu-tech/generic-pool/contracts";
import SftpClient from "ssh2-sftp-client";

const sftpPool = new QueuePool<SftpClient>(
{
create: async () => {
const client = new SftpClient("sftp-connection");
await client.connect({
host: "sftp.example.com",
port: 22,
username: "user",
password: "password",
});
return client;
},
destroy: async (client) => {
await client.end();
},
},
{
maxResources: 1,
maxWaitSize: 0,
},
);

const client = await sftpPool.acquireOrFail();

try {
// The only connection is borrowed and no caller is allowed to wait.
await sftpPool.acquireOrFail();
} catch (error) {
// logs true
console.log(error instanceof UnableToAcquireError);
}

await sftpPool.release(client);

Usage in the library​

The FTP and SFTP file storage adapters use this component to pool their clients, so concurrent operations do not share one connection. For SFTP, SftpFactory is an IResourceFactory<SftpClient> and sftpPool() combines it with a pool, both available from eridu-tech/file-storage/sftp-file-storage-adapter.

Further information​

For further information refer to eridu-tech/generic-pool API docs.