-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add database healthcheck callback #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2e3dff6
4b6d94b
9bfd935
d232fe6
7e244d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,14 @@ import {URL} from 'url'; | |
| import knexBuilder from 'knex'; | ||
| import type {Knex} from 'knex'; | ||
|
|
||
| import type {Dict, ExLogger, PDOptions} from './types'; | ||
| import type { | ||
| Dict, | ||
| ExLogger, | ||
| PDOptions, | ||
| PGConnectionRole, | ||
| PGHealthcheckHandler, | ||
| PGHealthcheckStatus, | ||
| } from './types'; | ||
|
|
||
| import Timeout = NodeJS.Timer; | ||
|
|
||
|
|
@@ -26,6 +33,7 @@ export interface PDConstructorArgs { | |
| logger: ExLogger; | ||
|
|
||
| onKnexCreated?: (knex: Knex) => void; | ||
| onHealthcheck?: PGHealthcheckHandler; | ||
| } | ||
|
|
||
| interface PDConnection { | ||
|
|
@@ -51,15 +59,18 @@ export class PGDispatcher { | |
| private connections: PDConnection[]; | ||
| private options: PDOptions; | ||
| private logger: {info: InfoLogger; error: ErrorLogger}; | ||
| private onHealthcheck?: PGHealthcheckHandler; | ||
| private hcTimer?: Timeout | null; | ||
| private isInit = false; | ||
| private isTerminating = false; | ||
|
|
||
| constructor({ | ||
| connections = [], | ||
| options, | ||
| knexOptions = {}, | ||
| logger, | ||
| onKnexCreated, | ||
| onHealthcheck, | ||
| }: PDConstructorArgs) { | ||
| if (!connections.length) { | ||
| throw new Error('Empty connections list is not allowed'); | ||
|
|
@@ -83,6 +94,7 @@ export class PGDispatcher { | |
| }); | ||
| this.options = options; | ||
| this.knexOptions = knexOptions; | ||
| this.onHealthcheck = onHealthcheck; | ||
|
|
||
| this.logger = { | ||
| info: ({message, data}) => { | ||
|
|
@@ -120,6 +132,8 @@ export class PGDispatcher { | |
| } | ||
|
|
||
| terminate() { | ||
| this.isTerminating = true; | ||
|
|
||
| if (this.hcTimer) { | ||
| clearInterval(this.hcTimer); | ||
| } | ||
|
|
@@ -200,6 +214,10 @@ export class PGDispatcher { | |
| private async initHealthcheck() { | ||
| await this.knexReady(); | ||
|
|
||
| if (this.isTerminating) { | ||
| return; | ||
| } | ||
|
|
||
| const performHealthcheck = () => { | ||
| const checkups = this.connections.map((connection) => | ||
| this.checkDatabase(connection).catch((error) => { | ||
|
|
@@ -209,18 +227,21 @@ export class PGDispatcher { | |
| }), | ||
| ); | ||
| Promise.all(checkups).then(() => { | ||
| // Connections hold shared current state; this is not an isolated per-cycle result. | ||
| const status = this.getHealthcheckStatus(); | ||
| this.logger.info({ | ||
| message: 'Database current status', | ||
| data: { | ||
| ...(this.isProxyMode ? {topologyMode: this.options.topologyMode} : {}), | ||
| connections: this.connections.map((c) => ({ | ||
| host: c.host, | ||
| ...(this.isProxyMode ? {} : {primary: c.primary}), | ||
| healthy: c.healthy, | ||
| latency: c.latency, | ||
| connections: this.connections.map((connection) => ({ | ||
| host: connection.host, | ||
| ...(this.isProxyMode ? {} : {primary: connection.primary}), | ||
| healthy: connection.healthy, | ||
| latency: connection.latency, | ||
| })), | ||
| }, | ||
| }); | ||
| this.notifyHealthcheck(status); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Do not notify after
AI generated
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 7e244d1. Termination now marks the dispatcher before clearing its timer, prevents initialization from starting a healthcheck after termination, and suppresses callback notification from an in-flight check. |
||
| }); | ||
| }; | ||
|
|
||
|
|
@@ -299,6 +320,56 @@ export class PGDispatcher { | |
| await Promise.all(this.connections.map((c) => c.knex)); | ||
| } | ||
|
|
||
| private getHealthcheckStatus(): PGHealthcheckStatus { | ||
| if (this.isProxyMode) { | ||
| return { | ||
| topologyMode: 'proxy', | ||
| connections: this.connections.map((connection) => ({ | ||
| host: connection.host, | ||
| healthy: connection.healthy, | ||
| latency: connection.latency, | ||
| })), | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| topologyMode: 'primary-replica', | ||
| connections: this.connections.map((connection) => ({ | ||
| host: connection.host, | ||
| role: this.getConnectionRole(connection), | ||
| healthy: connection.healthy, | ||
| latency: connection.latency, | ||
| })), | ||
| }; | ||
| } | ||
|
|
||
| private getConnectionRole(connection: PDConnection): PGConnectionRole { | ||
| if (!connection.healthy) { | ||
| return 'unknown'; | ||
| } | ||
|
|
||
| return connection.primary ? 'primary' : 'replica'; | ||
| } | ||
|
|
||
| private notifyHealthcheck(status: PGHealthcheckStatus) { | ||
| if (!this.onHealthcheck || this.isTerminating) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| this.onHealthcheck(status); | ||
| } catch (error) { | ||
| this.reportHealthcheckCallbackError(error); | ||
| } | ||
| } | ||
|
|
||
| private reportHealthcheckCallbackError(error: unknown) { | ||
| this.logger.error({ | ||
| message: 'Database healthcheck callback failed', | ||
| error: error as Error, | ||
| }); | ||
| } | ||
|
|
||
| private get healthyConnections() { | ||
| return this.connections.filter((c) => c.healthy); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Build the snapshot from one health-check cycle
Health-check cycles can overlap when
healthcheckIntervalis shorter than a check. Each cycle mutates the sharedthis.connections, whilegetHealthcheckStatus()reads that shared state only after its ownPromise.allcompletes. A slower older cycle can therefore publish a mixture of its own results and values written by a newer cycle. Please serialize cycles or build the snapshot from per-cycle local results.AI generated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The callback intentionally mirrors the existing Database current status semantics and snapshots shared current connection state rather than cycle-local results. I added a short comment to make this explicit. Serializing healthcheck cycles would change existing dispatcher behavior and is better handled separately.