Skip to content

Commit eff9718

Browse files
committed
added more checks
1 parent eb9d9e1 commit eff9718

3 files changed

Lines changed: 241 additions & 19 deletions

File tree

docs/env.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Environmental variables are also tracked in `ENVIRONMENT_VARIABLES` within `src/
4747
- `ELASTICSEARCH_SNIFF_ON_START`: Enable cluster node discovery on Elasticsearch client startup. Default is `true`. Example: `true`
4848
- `ELASTICSEARCH_SNIFF_INTERVAL`: Interval in milliseconds for periodic cluster health monitoring and node discovery. Set to 'false' to disable. Default is `30000`. Example: `30000`
4949
- `ELASTICSEARCH_SNIFF_ON_CONNECTION_FAULT`: Enable automatic cluster node discovery when connection faults occur. Default is `true`. Example: `true`
50+
- `ELASTICSEARCH_HEALTH_CHECK_INTERVAL`: Interval in milliseconds for proactive connection health monitoring. Default is `60000`. Example: `60000`
5051

5152
## Payments
5253

src/components/database/ElasticSearchDatabase.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ export class ElasticsearchIndexerDatabase extends AbstractIndexerDatabase {
2121

2222
constructor(config: OceanNodeDBConfig) {
2323
super(config)
24-
this.client = createElasticsearchClientWithRetry(config)
2524
this.index = 'indexer'
2625

27-
this.initializeIndex()
26+
return (async (): Promise<ElasticsearchIndexerDatabase> => {
27+
this.client = await createElasticsearchClientWithRetry(config)
28+
await this.initializeIndex()
29+
return this
30+
})() as unknown as ElasticsearchIndexerDatabase
2831
}
2932

3033
private async initializeIndex() {
@@ -148,10 +151,13 @@ export class ElasticsearchDdoStateDatabase extends AbstractDdoStateDatabase {
148151

149152
constructor(config: OceanNodeDBConfig) {
150153
super(config)
151-
this.client = createElasticsearchClientWithRetry(config)
152154
this.index = 'ddo_state'
153155

154-
this.initializeIndex()
156+
return (async (): Promise<ElasticsearchDdoStateDatabase> => {
157+
this.client = await createElasticsearchClientWithRetry(config)
158+
await this.initializeIndex()
159+
return this
160+
})() as unknown as ElasticsearchDdoStateDatabase
155161
}
156162

157163
private async initializeIndex() {
@@ -320,7 +326,11 @@ export class ElasticsearchOrderDatabase extends AbstractOrderDatabase {
320326

321327
constructor(config: OceanNodeDBConfig, schema: ElasticsearchSchema) {
322328
super(config, schema)
323-
this.provider = createElasticsearchClientWithRetry(config)
329+
330+
return (async (): Promise<ElasticsearchOrderDatabase> => {
331+
this.provider = await createElasticsearchClientWithRetry(config)
332+
return this
333+
})() as unknown as ElasticsearchOrderDatabase
324334
}
325335

326336
getSchema(): ElasticsearchSchema {
@@ -466,7 +476,11 @@ export class ElasticsearchDdoDatabase extends AbstractDdoDatabase {
466476

467477
constructor(config: OceanNodeDBConfig, schemas: ElasticsearchSchema[]) {
468478
super(config, schemas)
469-
this.client = createElasticsearchClientWithRetry(config)
479+
480+
return (async (): Promise<ElasticsearchDdoDatabase> => {
481+
this.client = await createElasticsearchClientWithRetry(config)
482+
return this
483+
})() as unknown as ElasticsearchDdoDatabase
470484
}
471485

472486
getSchemas(): ElasticsearchSchema[] {
@@ -754,10 +768,13 @@ export class ElasticsearchLogDatabase extends AbstractLogDatabase {
754768

755769
constructor(config: OceanNodeDBConfig) {
756770
super(config)
757-
this.client = createElasticsearchClientWithRetry(config)
758771
this.index = 'log'
759772

760-
this.initializeIndex()
773+
return (async (): Promise<ElasticsearchLogDatabase> => {
774+
this.client = await createElasticsearchClientWithRetry(config)
775+
await this.initializeIndex()
776+
return this
777+
})() as unknown as ElasticsearchLogDatabase
761778
}
762779

763780
private async initializeIndex() {

src/components/database/ElasticsearchConfigHelper.ts

Lines changed: 215 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface ElasticsearchRetryConfig {
1111
sniffOnStart?: boolean
1212
sniffInterval?: number | false
1313
sniffOnConnectionFault?: boolean
14+
healthCheckInterval?: number
1415
}
1516

1617
export const DEFAULT_ELASTICSEARCH_CONFIG: Required<ElasticsearchRetryConfig> = {
@@ -25,7 +26,10 @@ export const DEFAULT_ELASTICSEARCH_CONFIG: Required<ElasticsearchRetryConfig> =
2526
process.env.ELASTICSEARCH_SNIFF_INTERVAL === 'false'
2627
? false
2728
: parseInt(process.env.ELASTICSEARCH_SNIFF_INTERVAL || '30000'),
28-
sniffOnConnectionFault: process.env.ELASTICSEARCH_SNIFF_ON_CONNECTION_FAULT !== 'false'
29+
sniffOnConnectionFault: process.env.ELASTICSEARCH_SNIFF_ON_CONNECTION_FAULT !== 'false',
30+
healthCheckInterval: parseInt(
31+
process.env.ELASTICSEARCH_HEALTH_CHECK_INTERVAL || '60000'
32+
)
2933
}
3034

3135
class ElasticsearchClientSingleton {
@@ -34,6 +38,9 @@ class ElasticsearchClientSingleton {
3438
private config: OceanNodeDBConfig | null = null
3539
private connectionAttempts: number = 0
3640
private lastConnectionTime: number = 0
41+
private isRetrying: boolean = false
42+
private healthCheckTimer: NodeJS.Timeout | null = null
43+
private isMonitoring: boolean = false
3744

3845
private constructor() {}
3946

@@ -44,21 +51,175 @@ class ElasticsearchClientSingleton {
4451
return ElasticsearchClientSingleton.instance
4552
}
4653

47-
public getClient(
54+
public async getClient(
4855
config: OceanNodeDBConfig,
4956
customConfig: Partial<ElasticsearchRetryConfig> = {}
50-
): Client {
57+
): Promise<Client> {
5158
if (this.client && this.config) {
52-
return this.client
59+
const isHealthy = await this.checkConnectionHealth()
60+
if (isHealthy) {
61+
this.startHealthMonitoring(config, customConfig)
62+
return this.client
63+
} else {
64+
DATABASE_LOGGER.logMessageWithEmoji(
65+
`Elasticsearch connection interrupted or failed to ${this.maskUrl(
66+
this.config.url
67+
)} - starting retry phase`,
68+
true,
69+
GENERIC_EMOJIS.EMOJI_CROSS_MARK,
70+
LOG_LEVELS_STR.LEVEL_WARN
71+
)
72+
this.closeConnectionSync()
73+
return this.startRetryConnection(config, customConfig)
74+
}
75+
}
76+
77+
const client = await this.createNewConnection(config, customConfig)
78+
this.startHealthMonitoring(config, customConfig)
79+
return client
80+
}
81+
82+
private startHealthMonitoring(
83+
config: OceanNodeDBConfig,
84+
customConfig: Partial<ElasticsearchRetryConfig> = {}
85+
): void {
86+
if (this.isMonitoring || !this.client) return
87+
88+
const finalConfig = {
89+
...DEFAULT_ELASTICSEARCH_CONFIG,
90+
...customConfig
91+
}
92+
93+
this.isMonitoring = true
94+
DATABASE_LOGGER.logMessageWithEmoji(
95+
`Starting Elasticsearch connection monitoring (health check every ${finalConfig.healthCheckInterval}ms)`,
96+
true,
97+
GENERIC_EMOJIS.EMOJI_OCEAN_WAVE,
98+
LOG_LEVELS_STR.LEVEL_DEBUG
99+
)
100+
101+
this.healthCheckTimer = setInterval(async () => {
102+
if (this.client && !this.isRetrying) {
103+
const isHealthy = await this.checkConnectionHealth()
104+
if (!isHealthy) {
105+
DATABASE_LOGGER.logMessageWithEmoji(
106+
`Elasticsearch connection lost during monitoring - triggering automatic reconnection`,
107+
true,
108+
GENERIC_EMOJIS.EMOJI_CROSS_MARK,
109+
LOG_LEVELS_STR.LEVEL_WARN
110+
)
111+
this.closeConnectionSync()
112+
try {
113+
await this.startRetryConnection(config, customConfig)
114+
} catch (error) {
115+
DATABASE_LOGGER.logMessageWithEmoji(
116+
`Automatic reconnection failed: ${error.message}`,
117+
true,
118+
GENERIC_EMOJIS.EMOJI_CROSS_MARK,
119+
LOG_LEVELS_STR.LEVEL_ERROR
120+
)
121+
}
122+
}
123+
}
124+
}, finalConfig.healthCheckInterval)
125+
}
126+
127+
private stopHealthMonitoring(): void {
128+
if (this.healthCheckTimer) {
129+
clearInterval(this.healthCheckTimer)
130+
this.healthCheckTimer = null
131+
this.isMonitoring = false
132+
DATABASE_LOGGER.logMessageWithEmoji(
133+
`Stopped Elasticsearch connection monitoring`,
134+
true,
135+
GENERIC_EMOJIS.EMOJI_OCEAN_WAVE,
136+
LOG_LEVELS_STR.LEVEL_DEBUG
137+
)
138+
}
139+
}
140+
141+
private async startRetryConnection(
142+
config: OceanNodeDBConfig,
143+
customConfig: Partial<ElasticsearchRetryConfig> = {}
144+
): Promise<Client> {
145+
this.isRetrying = true
146+
const finalConfig = {
147+
...DEFAULT_ELASTICSEARCH_CONFIG,
148+
...customConfig
53149
}
54150

55-
return this.createNewConnection(config, customConfig)
151+
DATABASE_LOGGER.logMessageWithEmoji(
152+
`Starting Elasticsearch retry connection phase to ${this.maskUrl(
153+
config.url
154+
)} (max retries: ${finalConfig.maxRetries})`,
155+
true,
156+
GENERIC_EMOJIS.EMOJI_OCEAN_WAVE,
157+
LOG_LEVELS_STR.LEVEL_INFO
158+
)
159+
160+
for (let attempt = 1; attempt <= finalConfig.maxRetries; attempt++) {
161+
try {
162+
DATABASE_LOGGER.logMessageWithEmoji(
163+
`Elasticsearch reconnection attempt ${attempt}/${
164+
finalConfig.maxRetries
165+
} to ${this.maskUrl(config.url)}`,
166+
true,
167+
GENERIC_EMOJIS.EMOJI_OCEAN_WAVE,
168+
LOG_LEVELS_STR.LEVEL_INFO
169+
)
170+
171+
const client = await this.createNewConnection(config, customConfig)
172+
this.isRetrying = false
173+
return client
174+
} catch (error) {
175+
if (attempt === finalConfig.maxRetries) {
176+
this.isRetrying = false
177+
DATABASE_LOGGER.logMessageWithEmoji(
178+
`Elasticsearch retry connection failed after ${
179+
finalConfig.maxRetries
180+
} attempts to ${this.maskUrl(config.url)}: ${error.message}`,
181+
true,
182+
GENERIC_EMOJIS.EMOJI_CROSS_MARK,
183+
LOG_LEVELS_STR.LEVEL_ERROR
184+
)
185+
throw error
186+
}
187+
188+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000)
189+
DATABASE_LOGGER.logMessageWithEmoji(
190+
`Elasticsearch retry attempt ${attempt}/${finalConfig.maxRetries} failed, waiting ${delay}ms before next attempt: ${error.message}`,
191+
true,
192+
GENERIC_EMOJIS.EMOJI_CROSS_MARK,
193+
LOG_LEVELS_STR.LEVEL_WARN
194+
)
195+
await new Promise((resolve) => setTimeout(resolve, delay))
196+
}
197+
}
198+
199+
throw new Error('Maximum retry attempts reached')
200+
}
201+
202+
private async checkConnectionHealth(): Promise<boolean> {
203+
if (!this.client) return false
204+
205+
try {
206+
await this.client.ping()
207+
return true
208+
} catch (error) {
209+
DATABASE_LOGGER.logMessageWithEmoji(
210+
`Elasticsearch connection health check failed: ${error.message}`,
211+
true,
212+
GENERIC_EMOJIS.EMOJI_CROSS_MARK,
213+
LOG_LEVELS_STR.LEVEL_DEBUG
214+
)
215+
return false
216+
}
56217
}
57218

58-
private createNewConnection(
219+
private async createNewConnection(
59220
config: OceanNodeDBConfig,
60221
customConfig: Partial<ElasticsearchRetryConfig> = {}
61-
): Client {
222+
): Promise<Client> {
62223
this.connectionAttempts++
63224
this.lastConnectionTime = Date.now()
64225

@@ -68,7 +229,7 @@ class ElasticsearchClientSingleton {
68229
}
69230

70231
try {
71-
this.client = new Client({
232+
const client = new Client({
72233
node: config.url,
73234
auth:
74235
config.username && config.password
@@ -83,6 +244,9 @@ class ElasticsearchClientSingleton {
83244
sniffOnConnectionFault: finalConfig.sniffOnConnectionFault
84245
})
85246

247+
await client.ping()
248+
249+
this.client = client
86250
this.config = { ...config }
87251

88252
DATABASE_LOGGER.logMessageWithEmoji(
@@ -122,14 +286,43 @@ class ElasticsearchClientSingleton {
122286
return url.replace(/\/\/[^@]+@/, '//***:***@')
123287
}
124288
}
289+
290+
private closeConnectionSync(): void {
291+
this.stopHealthMonitoring()
292+
if (this.client) {
293+
try {
294+
this.client.close()
295+
} catch (error) {
296+
// silent close, no logging needed
297+
}
298+
this.client = null
299+
this.config = null
300+
}
301+
}
302+
303+
public getConnectionStats(): {
304+
attempts: number
305+
lastConnection: number
306+
connected: boolean
307+
isRetrying: boolean
308+
isMonitoring: boolean
309+
} {
310+
return {
311+
attempts: this.connectionAttempts,
312+
lastConnection: this.lastConnectionTime,
313+
connected: this.client !== null,
314+
isRetrying: this.isRetrying,
315+
isMonitoring: this.isMonitoring
316+
}
317+
}
125318
}
126319

127-
export function createElasticsearchClientWithRetry(
320+
export async function createElasticsearchClientWithRetry(
128321
config: OceanNodeDBConfig,
129322
customConfig: Partial<ElasticsearchRetryConfig> = {}
130-
): Client {
323+
): Promise<Client> {
131324
const singleton = ElasticsearchClientSingleton.getInstance()
132-
return singleton.getClient(config, customConfig)
325+
return await singleton.getClient(config, customConfig)
133326
}
134327

135328
export function getElasticsearchConfig(
@@ -140,3 +333,14 @@ export function getElasticsearchConfig(
140333
...retryConfig
141334
}
142335
}
336+
337+
export function getElasticsearchConnectionStats(): {
338+
attempts: number
339+
lastConnection: number
340+
connected: boolean
341+
isRetrying: boolean
342+
isMonitoring: boolean
343+
} {
344+
const singleton = ElasticsearchClientSingleton.getInstance()
345+
return singleton.getConnectionStats()
346+
}

0 commit comments

Comments
 (0)