Skip to content

Commit

Permalink
Add ExasolPool (allow more than 1 connection) (#37)
Browse files Browse the repository at this point in the history
* add package 'generic pool' and run 'npm audit fix' to fix reported vulnerabilities

* add signature to call query method from pool (ts specific)

* add ExasolPool class

* add exasolpool class to exports in index.ts

* sql-client: use this.acquire method instead of this.pool.acquire (like in all other query methods)

* add pool integration test classes

* version bump + ran project-keeper

* Add documentation on using ExasolPool

* Add release notes on ExasolPool
  • Loading branch information
pj-spoelders authored Nov 13, 2024
1 parent 500cb55 commit d1aa19e
Show file tree
Hide file tree
Showing 14 changed files with 527 additions and 18 deletions.
13 changes: 8 additions & 5 deletions dependencies.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions doc/changes/changelog.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions doc/changes/changes_0.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Exasol Driver ts 0.2.0, released 2024-??-??

Code name: TBD

## Summary

Adds `ExasolPool`, which is a connection pool using `ExasolDriver` underneath. See the user guide for a new section on how to configure and use the `ExasolPool` class.

## Features

- #28: Add a connection pool.

## Dependency Updates

### Compile Dependency Updates

* Added `generic-pool:^3.9.0`
94 changes: 94 additions & 0 deletions doc/user_guide/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,97 @@ console.log(queryResult.getRows()[0]['X']);
| `fetchSize` | number, >0 | `128*1024` | Amount of data in kB which should be obtained by Exasol during a fetch. The application can run out of memory if the value is too high. |
| `resultSetMaxRows` | number | | Set the max amount of rows in the result set. |
| `schema` | string | | Exasol schema name. |

### Pool

As of version 0.2.0 we now also provide a connection pool called `ExasolPool`.

#### NPM packages

Install the following dependencies from the [npm](https://www.npmjs.com/) package registry:

NodeJS:

```bash
npm install --save @exasol/exasol-driver-ts ws @types/ws
```

Browser:

```bash
npm install --save @exasol/exasol-driver-ts
```

#### Creating a connection pool:

NodeJs:

```js
import { ExaWebsocket, ExasolPool } from "@exasol/exasol-driver-ts";
import { WebSocket } from 'ws';

const pool = new ExasolPool((url) => {
return new WebSocket(url) as ExaWebsocket;
}, {
host: 'localhost',
port: 8563,
user: 'sys',
password: 'exasol',
encryption: false,
minimumPoolSize: 1,
maximumPoolSize: 10,
});
```

Browser:

```js
import { ExasolDriver,ExaWebsocket } from '@exasol/exasol-driver-ts';

const pool = new ExasolPool((url) => {
return new WebSocket(url) as ExaWebsocket;
}, {
host: 'localhost',
port: 8563,
user: 'sys',
password: 'exasol',
encryption: false,
minimumPoolSize: 1,
maximumPoolSize: 10,
});
```

The configuration is very similar to the `ExasolDriver` (client). With the added `minimumPoolSize` and `maximumPoolSize` options you can specify the minimum and maximum number of active connections in the pool. Defaults are 0 (minimumPoolSize) and 5 (maximumPoolSize).

#### Runninq a query

```js
const queryResult = await pool.query('SELECT x FROM SCHEMANAME.TABLENAME');
```

#### Clearing the pool

Draining and clearing the pool (do this when you don't need the pool anymore or before exiting the application):

```js
await pool.drain();
await pool.clear();
```

### Supported Driver Properties

| Property | Value | Default | Description |
| :----------------- | :----------------: | :-----------------: | :-------------------------------------------------------------------------------------------------------------------------------------- |
| `host` | string | 'localhost' | Host name or ip address. |
| `port` | number | 8563 | Port number. |
| `user` | string | | Exasol username. |
| `password` | string | | Exasol password. |
| `autocommit` | false=off, true=on | true | Switch autocommit on or off. |
| `clientName` | string | 'Javascript client' | Tell the server the application name. |
| `clientVersion` | string | 1 | Tell the server the version of the application. |
| `encryption` | false=off, true=on | true | Switch automatic encryption on or off. |
| `fetchSize` | number, >0 | `128*1024` | Amount of data in kB which should be obtained by Exasol during a fetch. The application can run out of memory if the value is too high. |
| `resultSetMaxRows` | number | | Set the max amount of rows in the result set. |
| `schema` | string | | Exasol schema name. |
| `minimumPoolSize` | number | 0 | Minimum amount of active connections. |
| `maximumPoolSize` | number | 5 | Maximum amount of active connections. |
5 changes: 5 additions & 0 deletions integration-test/browser/pool.runner.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ExaWebsocket } from '../../src';
import { basicPoolTests } from '../testcases/pool.basic.spec';
basicPoolTests('Browser', (url) => {
return new WebSocket(url) as ExaWebsocket;
});
6 changes: 6 additions & 0 deletions integration-test/node/pool.runner.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { WebSocket } from 'ws';
import { ExaWebsocket } from '../../src/lib/connection';
import { basicPoolTests } from '../testcases/pool.basic.spec';
basicPoolTests('Node', (url) => {
return new WebSocket(url) as ExaWebsocket;
});
1 change: 1 addition & 0 deletions integration-test/runner.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DOCKER_CONTAINER_VERSION: string = 'exasol/docker-db:7.1.22';
3 changes: 2 additions & 1 deletion integration-test/testcases/basic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GenericContainer, StartedTestContainer, Wait } from 'testcontainers';
import { ExasolDriver, websocketFactory } from '../../src/lib/sql-client';
import { RandomUuid } from 'testcontainers/dist/uuid';
import { DOCKER_CONTAINER_VERSION } from '../runner.config';

export const basicTests = (name: string, factory: websocketFactory) =>
describe(name, () => {
Expand All @@ -11,7 +12,7 @@ export const basicTests = (name: string, factory: websocketFactory) =>
let schemaName = '';

beforeAll(async () => {
container = await new GenericContainer('exasol/docker-db:7.1.22')
container = await new GenericContainer(DOCKER_CONTAINER_VERSION)
.withExposedPorts(8563, 2580)
.withPrivilegedMode()
.withDefaultLogDriver()
Expand Down
204 changes: 204 additions & 0 deletions integration-test/testcases/pool.basic.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { GenericContainer, StartedTestContainer, Wait } from 'testcontainers';
import { ExasolDriver, websocketFactory } from '../../src/lib/sql-client';
import { ExasolPool } from '../../src/lib/sql-pool';
import { RandomUuid } from 'testcontainers/dist/uuid';
import { QueryResult } from '../../src/lib/query-result';
import { DOCKER_CONTAINER_VERSION } from '../runner.config';

export const basicPoolTests = (name: string, factory: websocketFactory) =>
describe(name, () => {
const randomId = new RandomUuid();
let container: StartedTestContainer;
jest.setTimeout(7000000);
let schemaName = '';

beforeAll(async () => {
container = await new GenericContainer(DOCKER_CONTAINER_VERSION)
.withExposedPorts(8563, 2580)
.withPrivilegedMode()
.withDefaultLogDriver()
.withReuse()
.withWaitStrategy(Wait.forLogMessage('All stages finished'))
.start();
});

beforeEach(() => {
schemaName = 'TEST_SCHEMA' + randomId.nextUuid();
});

it('Connect to DB', async () => {
const poolToQuery = createPool(factory, container, 1, 10);
await poolToQuery.drain();
await poolToQuery.clear();
});

it('Exec and fetch (default min / max connection settings)', async () => {
const setupClient = createSetupClient(factory, container);

const poolToQuery = createPoolWithDefaultSize(factory, container);

await setupClient.connect();

await createSimpleTestTable(setupClient, schemaName);

const data = await poolToQuery.query('SELECT x FROM ' + schemaName + '.TEST_TABLE');

expect(data.getColumns()[0].name).toBe('X');
expect(data.getRows()[0]['X']).toBe(15);

await poolToQuery.drain();
await poolToQuery.clear();

await setupClient.close();
});

it('Exec and fetch', async () => {
const setupClient = createSetupClient(factory, container);

const poolToQuery = createPool(factory, container, 1, 10);

await setupClient.connect();

await createSimpleTestTable(setupClient, schemaName);

const data = await poolToQuery.query('SELECT x FROM ' + schemaName + '.TEST_TABLE');

expect(data.getColumns()[0].name).toBe('X');
expect(data.getRows()[0]['X']).toBe(15);

await poolToQuery.drain();
await poolToQuery.clear();

await setupClient.close();
});

it('Fetch multiple queries simultaneously/asynchronously', async () => {
const setupClient = createSetupClient(factory, container);

const poolToQuery = createPool(factory, container, 1, 10);

await setupClient.connect();

await createSimpleTestTable(setupClient, schemaName);

const dataPromise1 = poolToQuery.query('SELECT x FROM ' + schemaName + '.TEST_TABLE');
const dataPromise2 = poolToQuery.query('SELECT x FROM ' + schemaName + '.TEST_TABLE');
const dataPromise3 = poolToQuery.query('SELECT x FROM ' + schemaName + '.TEST_TABLE');
const dataPromise4 = poolToQuery.query('SELECT x FROM ' + schemaName + '.TEST_TABLE');

const data1 = await dataPromise1;
expect(data1.getColumns()[0].name).toBe('X');
expect(data1.getRows()[0]['X']).toBe(15);

const data2 = await dataPromise2;
expect(data2.getColumns()[0].name).toBe('X');
expect(data2.getRows()[0]['X']).toBe(15);

const data3 = await dataPromise3;
expect(data3.getColumns()[0].name).toBe('X');
expect(data3.getRows()[0]['X']).toBe(15);

const data4 = await dataPromise4;
expect(data4.getColumns()[0].name).toBe('X');
expect(data4.getRows()[0]['X']).toBe(15);

await poolToQuery.drain();
await poolToQuery.clear();

await setupClient.close();
});

it('Fetch multiple queries asynchronously (20)', async () => {
const setupClient = createSetupClient(factory, container);

const poolToQuery = createPool(factory, container, 1, 10);

await setupClient.connect();

await createSimpleTestTable(setupClient, schemaName);

const amountOfRequests = 20;

await runQueryXNumberOfTimesAndCheckResult(amountOfRequests, poolToQuery, schemaName);

await poolToQuery.drain();
await poolToQuery.clear();

await setupClient.close();
});
it('Fetch multiple queries asynchronously (100)', async () => {
const setupClient = createSetupClient(factory, container);

const poolToQuery = createPool(factory, container, 1, 10);

await setupClient.connect();

await createSimpleTestTable(setupClient, schemaName);

const amountOfRequests = 100;

await runQueryXNumberOfTimesAndCheckResult(amountOfRequests, poolToQuery, schemaName);

await poolToQuery.drain();
await poolToQuery.clear();

await setupClient.close();
});

afterAll(async () => {});
});

async function createSimpleTestTable(setupClient: ExasolDriver, schemaName: string) {
await setupClient.execute('CREATE SCHEMA ' + schemaName);
await setupClient.execute('CREATE TABLE ' + schemaName + '.TEST_TABLE(x INT)');
await setupClient.execute('INSERT INTO ' + schemaName + '.TEST_TABLE VALUES (15)');
}

function createSetupClient(factory: websocketFactory, container: StartedTestContainer) {
return new ExasolDriver(factory, {
host: container.getHost(),
port: container.getMappedPort(8563),
user: 'sys',
password: 'exasol',
encryption: false,
});
}

function createPoolWithDefaultSize(factory: websocketFactory, container: StartedTestContainer) {
return new ExasolPool(factory, {
host: container.getHost(),
port: container.getMappedPort(8563),
user: 'sys',
password: 'exasol',
encryption: false,
});
}

function createPool(factory: websocketFactory, container: StartedTestContainer, minimumPoolSize: number, maximumPoolSize: number) {
return new ExasolPool(factory, {
host: container.getHost(),
port: container.getMappedPort(8563),
user: 'sys',
password: 'exasol',
encryption: false,
minimumPoolSize: minimumPoolSize,
maximumPoolSize: maximumPoolSize,
});
}

async function runQueryXNumberOfTimesAndCheckResult(amountOfRequests: number, poolToQuery: ExasolPool, schemaName: string) {
const promiseArr: Promise<QueryResult>[] = [];

for (let i = 0; i < amountOfRequests; i++) {
const dataPromise = poolToQuery.query('SELECT x FROM ' + schemaName + '.TEST_TABLE');
promiseArr.push(dataPromise);
}

await Promise.all(promiseArr);

for (let i = 0; i < amountOfRequests; i++) {
const data = await promiseArr[i];
expect(data.getColumns()[0].name).toBe('X');
expect(data.getRows()[0]['X']).toBe(15);
}
}
Loading

0 comments on commit d1aa19e

Please sign in to comment.