Skip to content

Commit fc4d1ed

Browse files
authored
Merge pull request #636 from devforth/feature/AdminForth/1673/create-db_recover-option
fix: add option to recover Postgres connection pool after DB drops wi…
2 parents 28a7957 + 6f30517 commit fc4d1ed

5 files changed

Lines changed: 65 additions & 13 deletions

File tree

adminforth/dataConnectors/baseConnector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
4444
return this.client;
4545
}
4646

47-
setupClient(url: string): Promise<void> {
47+
setupClient(url: string, options?: { recovery?: boolean }): Promise<void> {
4848
throw new Error('Method not implemented.');
4949
}
5050

adminforth/dataConnectors/postgres.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,35 @@ types.setTypeParser(1082, (val) => val); // DATE
1616

1717
class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector {
1818

19-
async setupClient(url: string): Promise<void> {
19+
async setupClient(url: string, options?: { recovery?: boolean }): Promise<void> {
2020
this.client = new Pool({
2121
connectionString: url
2222
});
23-
try {
24-
await this.client.connect();
25-
this.client.on('error', async (err) => {
26-
afLogger.error(`Postgres error: ${err.message} ${err.stack}`);
27-
this.client.end();
28-
await new Promise((resolve) => { setTimeout(resolve, 1000) });
29-
this.setupClient(url);
23+
24+
const selfHeal = options?.recovery !== false;
25+
26+
if (selfHeal) {
27+
this.client.on('error', (err) => {
28+
afLogger.error(`Postgres pool idle client error (pool self-heals on next query): ${err.message} ${err.stack}`);
3029
});
31-
} catch (e) {
32-
afLogger.error(`Failed to connect to Postgres ${e}`);
30+
try {
31+
const client = await this.client.connect();
32+
client.release();
33+
} catch (e) {
34+
afLogger.error(`Failed to connect to Postgres ${e}`);
35+
}
36+
} else {
37+
try {
38+
await this.client.connect();
39+
this.client.on('error', async (err) => {
40+
afLogger.error(`Postgres error: ${err.message} ${err.stack}`);
41+
this.client.end();
42+
await new Promise((resolve) => { setTimeout(resolve, 1000) });
43+
this.setupClient(url, options);
44+
});
45+
} catch (e) {
46+
afLogger.error(`Failed to connect to Postgres ${e}`);
47+
}
3348
}
3449
}
3550

adminforth/documentation/docs/tutorial/02-glossary.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ It used to:
1515

1616
There might be several datasources in the system for various databases e.g. One datasource to Mongo DBs and one to Postgres DB.
1717

18+
### connectionRecovery
19+
20+
For PostgreSQL datasources AdminForth keeps a connection pool. The optional `connectionRecovery` flag controls how the connector reacts when that connection drops (DB restart, failover, network blip, etc.):
21+
22+
```ts
23+
dataSources: [
24+
{
25+
id: 'maindb',
26+
url: `${process.env.DATABASE_URL}`,
27+
connectionRecovery: true, // default
28+
},
29+
],
30+
```
31+
32+
- `true` (default, recommended) — **self-heal mode.** The pool recovers automatically: a dead idle connection is dropped and a fresh one is transparently opened on the next query, so the app keeps working without a manual restart. Queries that were in-flight at the moment of the outage will fail, but subsequent queries succeed once the database is back.
33+
- `false`**legacy mode.** On a connection error the pool is destroyed and recreated after 1 second. If the outage outlasts that retry, the app can be left with a permanently dead pool and require a manual restart. Kept only for backward compatibility.
34+
35+
This flag is currently honored by the PostgreSQL connector; other connectors rely on their driver's built-in recovery.
36+
1837
## resource
1938

2039
A [Resource](/docs/api/Back/interfaces/AdminForthResource.md) is a AdminForth representation of a table or collection in database. One resource is one table in the database. Resource has `table` property which should be equal to the name of the table in the database.

adminforth/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,10 @@ class AdminForth implements IAdminForth {
513513

514514
await Promise.all(Object.keys(this.connectors).map(async (dataSourceId) => {
515515
try {
516-
await this.connectors[dataSourceId].setupClient(this.config.dataSources.find((ds) => ds.id === dataSourceId).url);
516+
await this.connectors[dataSourceId].setupClient(
517+
this.config.dataSources.find((ds) => ds.id === dataSourceId).url,
518+
{ recovery: this.config.dataSources.find((ds) => ds.id === dataSourceId).connectionRecovery !== false }
519+
);
517520
} catch (e) {
518521
afLogger.error(`Error while connecting to datasource '${dataSourceId}': ${e}`);
519522
}

adminforth/types/Back.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,10 @@ export interface IAdminForthDataSourceConnector {
273273
/**
274274
* Function to setup client connection to database.
275275
* @param url URL to database. Examples: clickhouse://demo:demo@localhost:8125/demo
276+
* @param options Optional connection options. `recovery` mirrors the dataSource
277+
* `connectionRecovery` flag (defaults to true when omitted).
276278
*/
277-
setupClient(url: string): Promise<void>;
279+
setupClient(url: string, options?: { recovery?: boolean }): Promise<void>;
278280

279281
/**
280282
* Function to get all tables from database.
@@ -1245,6 +1247,19 @@ export type AdminForthDataSource = {
12451247
* - SQLite: `sqlite://<path>`
12461248
*/
12471249
url: string,
1250+
1251+
/**
1252+
* Controls how the connector reacts to a dropped database connection.
1253+
* Currently honored by the PostgreSQL connector.
1254+
*
1255+
* - `true` (default): self-heal mode. The connection pool recovers automatically — when an
1256+
* idle connection dies (DB restart, failover, network blip, etc.) it is dropped and a fresh
1257+
* one is opened on the next query, so the app keeps working without a manual restart.
1258+
* - `false`: legacy mode. On a connection error the pool is destroyed and recreated after 1s.
1259+
* If the outage outlasts that retry the app can be left with a permanently dead pool and
1260+
* require a manual restart. Kept only for backward compatibility.
1261+
*/
1262+
connectionRecovery?: boolean,
12481263
}
12491264

12501265
type AdminForthPageDeclaration = {

0 commit comments

Comments
 (0)