@@ -11,6 +11,7 @@ export interface ElasticsearchRetryConfig {
1111 sniffOnStart ?: boolean
1212 sniffInterval ?: number | false
1313 sniffOnConnectionFault ?: boolean
14+ healthCheckInterval ?: number
1415}
1516
1617export 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
3135class 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
135328export 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