iris-vector-rag 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adapters/rag_templates_bridge.py +578 -0
- common/__init__.py +1 -0
- common/batch_utils.py +294 -0
- common/config.py +58 -0
- common/connection_manager.py +242 -0
- common/connection_pool.py +445 -0
- common/database.py +141 -0
- common/database_schema_manager.py +289 -0
- common/db_init_with_indexes.py +181 -0
- common/db_vector_utils.py +181 -0
- common/environment_manager.py +220 -0
- common/huggingface_utils.py +206 -0
- common/iris_client.py +92 -0
- common/iris_connection_manager.py +411 -0
- common/iris_connection_pool.py +360 -0
- common/iris_connector.py +71 -0
- common/iris_dbapi_connector.py +323 -0
- common/iris_index_utils.py +195 -0
- common/iris_sql_utils.py +704 -0
- common/iris_stream_reader.py +166 -0
- common/llm_cache_config.py +308 -0
- common/llm_cache_iris.py +671 -0
- common/llm_cache_manager.py +911 -0
- common/utils.py +559 -0
- common/vector_sql_utils.py +584 -0
- evaluation_framework/__init__.py +66 -0
- evaluation_framework/biomedical_question_generator.py +826 -0
- evaluation_framework/comparative_analysis_system.py +1173 -0
- evaluation_framework/empirical_reporting.py +1298 -0
- evaluation_framework/evaluation_orchestrator.py +1123 -0
- evaluation_framework/execute_production_evaluation.py +382 -0
- evaluation_framework/pmc_data_pipeline.py +1332 -0
- evaluation_framework/production_evaluation_config.py +353 -0
- evaluation_framework/ragas_metrics_framework.py +1013 -0
- evaluation_framework/real_production_evaluation.py +544 -0
- evaluation_framework/run_10k_evaluation.py +175 -0
- evaluation_framework/simple_test.py +364 -0
- evaluation_framework/statistical_evaluation_methodology.py +1016 -0
- evaluation_framework/test_framework.py +281 -0
- evaluation_framework/test_iris_connectivity.py +244 -0
- evaluation_framework/test_small_scale_evaluation.py +432 -0
- evaluation_framework/true_e2e_evaluation.py +639 -0
- evaluation_framework/visualization_engine.py +585 -0
- iris_rag/__init__.py +284 -0
- iris_rag/config/default_config.yaml +295 -0
- iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
- iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
- iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
- iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
- iris_vector_rag-0.2.1.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IRIS Database Connection Pool Manager.
|
|
3
|
+
|
|
4
|
+
Provides connection pooling for concurrent REST API requests to prevent
|
|
5
|
+
connection pool exhaustion and ensure efficient resource management.
|
|
6
|
+
|
|
7
|
+
Based on research.md findings:
|
|
8
|
+
- 20 base connections, 10 overflow (adaptive under load)
|
|
9
|
+
- 1-hour connection recycle to prevent stale connections
|
|
10
|
+
- Pre-ping validation before each use
|
|
11
|
+
- Thread-safe pool management
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from typing import Any, Dict, Generator, Optional
|
|
18
|
+
|
|
19
|
+
import intersystems_iris.dbapi._DBAPI as iris_dbapi
|
|
20
|
+
from intersystems_iris.dbapi._DBAPI import Connection
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IRISConnectionPool:
|
|
26
|
+
"""
|
|
27
|
+
Thread-safe connection pool for IRIS database.
|
|
28
|
+
|
|
29
|
+
This class implements connection pooling to support concurrent RAG API requests
|
|
30
|
+
without exhausting the IRIS connection limit.
|
|
31
|
+
|
|
32
|
+
Features:
|
|
33
|
+
- Configurable pool size with overflow capacity
|
|
34
|
+
- Connection recycling to prevent stale connections
|
|
35
|
+
- Pre-ping validation for connection health
|
|
36
|
+
- Thread-safe acquire/release operations
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
host: str,
|
|
42
|
+
port: int,
|
|
43
|
+
namespace: str,
|
|
44
|
+
username: str,
|
|
45
|
+
password: str,
|
|
46
|
+
pool_size: int = 20,
|
|
47
|
+
max_overflow: int = 10,
|
|
48
|
+
pool_recycle: int = 3600,
|
|
49
|
+
pool_pre_ping: bool = True,
|
|
50
|
+
pool_timeout: int = 30,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Initialize IRIS connection pool.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
host: IRIS server hostname
|
|
57
|
+
port: IRIS server port
|
|
58
|
+
namespace: IRIS namespace
|
|
59
|
+
username: Database username
|
|
60
|
+
password: Database password
|
|
61
|
+
pool_size: Number of base connections (default: 20)
|
|
62
|
+
max_overflow: Additional connections under load (default: 10)
|
|
63
|
+
pool_recycle: Seconds before recycling connection (default: 3600)
|
|
64
|
+
pool_pre_ping: Validate connection before use (default: True)
|
|
65
|
+
pool_timeout: Seconds to wait for connection (default: 30)
|
|
66
|
+
"""
|
|
67
|
+
self.host = host
|
|
68
|
+
self.port = port
|
|
69
|
+
self.namespace = namespace
|
|
70
|
+
self.username = username
|
|
71
|
+
self.password = password
|
|
72
|
+
self.pool_size = pool_size
|
|
73
|
+
self.max_overflow = max_overflow
|
|
74
|
+
self.pool_recycle = pool_recycle
|
|
75
|
+
self.pool_pre_ping = pool_pre_ping
|
|
76
|
+
self.pool_timeout = pool_timeout
|
|
77
|
+
|
|
78
|
+
# Connection tracking
|
|
79
|
+
self._available_connections: list[Connection] = []
|
|
80
|
+
self._in_use_connections: set[Connection] = set()
|
|
81
|
+
self._connection_timestamps: Dict[Connection, float] = {}
|
|
82
|
+
|
|
83
|
+
# Thread safety
|
|
84
|
+
import threading
|
|
85
|
+
|
|
86
|
+
self._lock = threading.Lock()
|
|
87
|
+
|
|
88
|
+
# Metrics
|
|
89
|
+
self._total_connections_created = 0
|
|
90
|
+
self._total_connections_recycled = 0
|
|
91
|
+
self._total_acquisitions = 0
|
|
92
|
+
|
|
93
|
+
logger.info(
|
|
94
|
+
f"Initialized IRIS connection pool: "
|
|
95
|
+
f"pool_size={pool_size}, max_overflow={max_overflow}, "
|
|
96
|
+
f"recycle={pool_recycle}s, pre_ping={pool_pre_ping}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _create_connection(self) -> Connection:
|
|
100
|
+
"""
|
|
101
|
+
Create a new IRIS database connection.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
IRIS connection object
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
Exception: If connection creation fails
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
connection = iris_dbapi.connect(
|
|
111
|
+
hostname=self.host,
|
|
112
|
+
port=self.port,
|
|
113
|
+
namespace=self.namespace,
|
|
114
|
+
username=self.username,
|
|
115
|
+
password=self.password,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
import time
|
|
119
|
+
|
|
120
|
+
self._connection_timestamps[connection] = time.time()
|
|
121
|
+
self._total_connections_created += 1
|
|
122
|
+
|
|
123
|
+
logger.debug(
|
|
124
|
+
f"Created new IRIS connection "
|
|
125
|
+
f"(total created: {self._total_connections_created})"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return connection
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Failed to create IRIS connection: {e}")
|
|
132
|
+
raise
|
|
133
|
+
|
|
134
|
+
def _validate_connection(self, connection: Connection) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Validate that a connection is still alive.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
connection: Connection to validate
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if connection is valid, False otherwise
|
|
143
|
+
"""
|
|
144
|
+
if not self.pool_pre_ping:
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
cursor = connection.cursor()
|
|
149
|
+
cursor.execute("SELECT 1")
|
|
150
|
+
cursor.close()
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.warning(f"Connection validation failed: {e}")
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
def _should_recycle_connection(self, connection: Connection) -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Check if connection should be recycled based on age.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
connection: Connection to check
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
True if connection should be recycled, False otherwise
|
|
166
|
+
"""
|
|
167
|
+
if connection not in self._connection_timestamps:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
import time
|
|
171
|
+
|
|
172
|
+
age = time.time() - self._connection_timestamps[connection]
|
|
173
|
+
return age >= self.pool_recycle
|
|
174
|
+
|
|
175
|
+
def _recycle_connection(self, connection: Connection) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Close and remove a connection from the pool.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
connection: Connection to recycle
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
connection.close()
|
|
184
|
+
self._total_connections_recycled += 1
|
|
185
|
+
|
|
186
|
+
if connection in self._connection_timestamps:
|
|
187
|
+
del self._connection_timestamps[connection]
|
|
188
|
+
|
|
189
|
+
logger.debug(
|
|
190
|
+
f"Recycled IRIS connection "
|
|
191
|
+
f"(total recycled: {self._total_connections_recycled})"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.warning(f"Error recycling connection: {e}")
|
|
196
|
+
|
|
197
|
+
def acquire(self) -> Connection:
|
|
198
|
+
"""
|
|
199
|
+
Acquire a connection from the pool.
|
|
200
|
+
|
|
201
|
+
This method will:
|
|
202
|
+
1. Try to get an available connection from the pool
|
|
203
|
+
2. Validate the connection if pre_ping is enabled
|
|
204
|
+
3. Recycle connections that are too old
|
|
205
|
+
4. Create new connections if pool is not full
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Active IRIS connection
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
Exception: If unable to acquire connection within timeout
|
|
212
|
+
"""
|
|
213
|
+
import time
|
|
214
|
+
|
|
215
|
+
start_time = time.time()
|
|
216
|
+
|
|
217
|
+
while (time.time() - start_time) < self.pool_timeout:
|
|
218
|
+
with self._lock:
|
|
219
|
+
# Try to get existing connection
|
|
220
|
+
if self._available_connections:
|
|
221
|
+
connection = self._available_connections.pop()
|
|
222
|
+
|
|
223
|
+
# Check if connection should be recycled
|
|
224
|
+
if self._should_recycle_connection(connection):
|
|
225
|
+
self._recycle_connection(connection)
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
# Validate connection health
|
|
229
|
+
if not self._validate_connection(connection):
|
|
230
|
+
self._recycle_connection(connection)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Connection is good - mark as in use
|
|
234
|
+
self._in_use_connections.add(connection)
|
|
235
|
+
self._total_acquisitions += 1
|
|
236
|
+
|
|
237
|
+
logger.debug(
|
|
238
|
+
f"Acquired connection from pool "
|
|
239
|
+
f"(available: {len(self._available_connections)}, "
|
|
240
|
+
f"in_use: {len(self._in_use_connections)})"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return connection
|
|
244
|
+
|
|
245
|
+
# No available connections - check if we can create new one
|
|
246
|
+
total_connections = (
|
|
247
|
+
len(self._available_connections) + len(self._in_use_connections)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if total_connections < (self.pool_size + self.max_overflow):
|
|
251
|
+
# Create new connection
|
|
252
|
+
connection = self._create_connection()
|
|
253
|
+
self._in_use_connections.add(connection)
|
|
254
|
+
self._total_acquisitions += 1
|
|
255
|
+
|
|
256
|
+
logger.debug(
|
|
257
|
+
f"Created new connection "
|
|
258
|
+
f"(total: {total_connections + 1}/{self.pool_size + self.max_overflow})"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return connection
|
|
262
|
+
|
|
263
|
+
# Pool is full - wait briefly and retry
|
|
264
|
+
time.sleep(0.1)
|
|
265
|
+
|
|
266
|
+
# Timeout exceeded
|
|
267
|
+
raise Exception(
|
|
268
|
+
f"Timeout acquiring connection from pool after {self.pool_timeout}s "
|
|
269
|
+
f"(pool_size: {self.pool_size}, max_overflow: {self.max_overflow}, "
|
|
270
|
+
f"in_use: {len(self._in_use_connections)})"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def release(self, connection: Connection) -> None:
|
|
274
|
+
"""
|
|
275
|
+
Release a connection back to the pool.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
connection: Connection to release
|
|
279
|
+
"""
|
|
280
|
+
with self._lock:
|
|
281
|
+
if connection in self._in_use_connections:
|
|
282
|
+
self._in_use_connections.remove(connection)
|
|
283
|
+
|
|
284
|
+
# Check if we should recycle instead of returning to pool
|
|
285
|
+
if self._should_recycle_connection(connection):
|
|
286
|
+
self._recycle_connection(connection)
|
|
287
|
+
else:
|
|
288
|
+
self._available_connections.append(connection)
|
|
289
|
+
|
|
290
|
+
logger.debug(
|
|
291
|
+
f"Released connection to pool "
|
|
292
|
+
f"(available: {len(self._available_connections)}, "
|
|
293
|
+
f"in_use: {len(self._in_use_connections)})"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
@contextmanager
|
|
297
|
+
def get_connection(self) -> Generator[Connection, None, None]:
|
|
298
|
+
"""
|
|
299
|
+
Context manager for acquiring and releasing connections.
|
|
300
|
+
|
|
301
|
+
Usage:
|
|
302
|
+
>>> pool = IRISConnectionPool(...)
|
|
303
|
+
>>> with pool.get_connection() as conn:
|
|
304
|
+
... cursor = conn.cursor()
|
|
305
|
+
... cursor.execute("SELECT * FROM table")
|
|
306
|
+
|
|
307
|
+
Yields:
|
|
308
|
+
IRIS connection
|
|
309
|
+
"""
|
|
310
|
+
connection = self.acquire()
|
|
311
|
+
try:
|
|
312
|
+
yield connection
|
|
313
|
+
finally:
|
|
314
|
+
self.release(connection)
|
|
315
|
+
|
|
316
|
+
def close_all(self) -> None:
|
|
317
|
+
"""
|
|
318
|
+
Close all connections in the pool.
|
|
319
|
+
|
|
320
|
+
This should be called on application shutdown.
|
|
321
|
+
"""
|
|
322
|
+
with self._lock:
|
|
323
|
+
# Close available connections
|
|
324
|
+
for connection in self._available_connections:
|
|
325
|
+
self._recycle_connection(connection)
|
|
326
|
+
self._available_connections.clear()
|
|
327
|
+
|
|
328
|
+
# Close in-use connections (should be empty at shutdown)
|
|
329
|
+
for connection in self._in_use_connections:
|
|
330
|
+
self._recycle_connection(connection)
|
|
331
|
+
self._in_use_connections.clear()
|
|
332
|
+
|
|
333
|
+
logger.info(
|
|
334
|
+
f"Closed all connections in pool "
|
|
335
|
+
f"(total created: {self._total_connections_created}, "
|
|
336
|
+
f"recycled: {self._total_connections_recycled}, "
|
|
337
|
+
f"acquisitions: {self._total_acquisitions})"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
341
|
+
"""
|
|
342
|
+
Get connection pool statistics.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Dictionary with pool metrics
|
|
346
|
+
"""
|
|
347
|
+
with self._lock:
|
|
348
|
+
return {
|
|
349
|
+
"pool_size": self.pool_size,
|
|
350
|
+
"max_overflow": self.max_overflow,
|
|
351
|
+
"available_connections": len(self._available_connections),
|
|
352
|
+
"in_use_connections": len(self._in_use_connections),
|
|
353
|
+
"total_connections": len(self._available_connections)
|
|
354
|
+
+ len(self._in_use_connections),
|
|
355
|
+
"total_created": self._total_connections_created,
|
|
356
|
+
"total_recycled": self._total_connections_recycled,
|
|
357
|
+
"total_acquisitions": self._total_acquisitions,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# Global connection pool instance (initialized by FastAPI app)
|
|
362
|
+
_global_pool: Optional[IRISConnectionPool] = None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def get_pool() -> IRISConnectionPool:
|
|
366
|
+
"""
|
|
367
|
+
Get the global connection pool instance.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Global IRISConnectionPool instance
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
RuntimeError: If pool has not been initialized
|
|
374
|
+
"""
|
|
375
|
+
if _global_pool is None:
|
|
376
|
+
raise RuntimeError(
|
|
377
|
+
"Connection pool not initialized. "
|
|
378
|
+
"Call initialize_pool() during application startup."
|
|
379
|
+
)
|
|
380
|
+
return _global_pool
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def initialize_pool(
|
|
384
|
+
host: Optional[str] = None,
|
|
385
|
+
port: Optional[int] = None,
|
|
386
|
+
namespace: Optional[str] = None,
|
|
387
|
+
username: Optional[str] = None,
|
|
388
|
+
password: Optional[str] = None,
|
|
389
|
+
**kwargs: Any,
|
|
390
|
+
) -> IRISConnectionPool:
|
|
391
|
+
"""
|
|
392
|
+
Initialize the global connection pool.
|
|
393
|
+
|
|
394
|
+
This should be called during FastAPI application startup.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
host: IRIS server hostname (default: from IRIS_HOST env)
|
|
398
|
+
port: IRIS server port (default: from IRIS_PORT env)
|
|
399
|
+
namespace: IRIS namespace (default: from IRIS_NAMESPACE env)
|
|
400
|
+
username: Database username (default: from IRIS_USERNAME env)
|
|
401
|
+
password: Database password (default: from IRIS_PASSWORD env)
|
|
402
|
+
**kwargs: Additional IRISConnectionPool arguments
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Initialized global connection pool
|
|
406
|
+
"""
|
|
407
|
+
global _global_pool
|
|
408
|
+
|
|
409
|
+
# Get connection parameters from environment if not provided
|
|
410
|
+
host = host or os.getenv("IRIS_HOST", "localhost")
|
|
411
|
+
port = port or int(os.getenv("IRIS_PORT", "1972"))
|
|
412
|
+
namespace = namespace or os.getenv("IRIS_NAMESPACE", "USER")
|
|
413
|
+
username = username or os.getenv("IRIS_USERNAME", "_SYSTEM")
|
|
414
|
+
password = password or os.getenv("IRIS_PASSWORD", "SYS")
|
|
415
|
+
|
|
416
|
+
_global_pool = IRISConnectionPool(
|
|
417
|
+
host=host,
|
|
418
|
+
port=port,
|
|
419
|
+
namespace=namespace,
|
|
420
|
+
username=username,
|
|
421
|
+
password=password,
|
|
422
|
+
**kwargs,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
logger.info(
|
|
426
|
+
f"Initialized global connection pool: {host}:{port}/{namespace} "
|
|
427
|
+
f"(pool_size={_global_pool.pool_size}, "
|
|
428
|
+
f"max_overflow={_global_pool.max_overflow})"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return _global_pool
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def close_pool() -> None:
|
|
435
|
+
"""
|
|
436
|
+
Close the global connection pool.
|
|
437
|
+
|
|
438
|
+
This should be called during FastAPI application shutdown.
|
|
439
|
+
"""
|
|
440
|
+
global _global_pool
|
|
441
|
+
|
|
442
|
+
if _global_pool is not None:
|
|
443
|
+
_global_pool.close_all()
|
|
444
|
+
_global_pool = None
|
|
445
|
+
logger.info("Closed global connection pool")
|
common/database.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database connectivity module for RAG Templates.
|
|
3
|
+
Provides simple database connection checking functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def wait_for_database(
|
|
15
|
+
max_attempts: int = 30, wait_seconds: int = 2, database_url: Optional[str] = None
|
|
16
|
+
) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Wait for database to become available.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
max_attempts: Maximum number of connection attempts
|
|
22
|
+
wait_seconds: Seconds to wait between attempts
|
|
23
|
+
database_url: Database URL to connect to (optional)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
True if database is available, False otherwise
|
|
27
|
+
"""
|
|
28
|
+
# Get database URL from environment if not provided
|
|
29
|
+
if not database_url:
|
|
30
|
+
database_url = os.getenv("DATABASE_URL", "iris://localhost:1974/USER")
|
|
31
|
+
|
|
32
|
+
logger.info(f"Waiting for database at {database_url}")
|
|
33
|
+
|
|
34
|
+
for attempt in range(max_attempts):
|
|
35
|
+
try:
|
|
36
|
+
# Try to import and use IRIS connection
|
|
37
|
+
# Parse the database URL to get connection parameters
|
|
38
|
+
# Format: iris://username:password@host:port/namespace
|
|
39
|
+
import urllib.parse
|
|
40
|
+
|
|
41
|
+
from .iris_client import IRISClient
|
|
42
|
+
|
|
43
|
+
parsed = urllib.parse.urlparse(database_url)
|
|
44
|
+
|
|
45
|
+
host = parsed.hostname or "localhost"
|
|
46
|
+
port = parsed.port or 1974
|
|
47
|
+
username = parsed.username or "demo"
|
|
48
|
+
password = parsed.password or "demo"
|
|
49
|
+
namespace = parsed.path.lstrip("/") or "USER"
|
|
50
|
+
|
|
51
|
+
# Create IRIS client and test connection
|
|
52
|
+
client = IRISClient(
|
|
53
|
+
host=host,
|
|
54
|
+
port=port,
|
|
55
|
+
username=username,
|
|
56
|
+
password=password,
|
|
57
|
+
namespace=namespace,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Test the connection with a simple query
|
|
61
|
+
result = client.execute_query("SELECT 1")
|
|
62
|
+
if result:
|
|
63
|
+
logger.info("Database connection successful")
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
except ImportError:
|
|
67
|
+
# Fallback to basic socket check if IRIS client is not available
|
|
68
|
+
try:
|
|
69
|
+
import socket
|
|
70
|
+
import urllib.parse
|
|
71
|
+
|
|
72
|
+
parsed = urllib.parse.urlparse(database_url)
|
|
73
|
+
host = parsed.hostname or "localhost"
|
|
74
|
+
port = parsed.port or 1974
|
|
75
|
+
|
|
76
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
77
|
+
sock.settimeout(5)
|
|
78
|
+
result = sock.connect_ex((host, port))
|
|
79
|
+
sock.close()
|
|
80
|
+
|
|
81
|
+
if result == 0:
|
|
82
|
+
logger.info(f"Database port {port} is accessible on {host}")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.warning(f"Socket connection check failed: {e}")
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.warning(f"Database connection attempt {attempt + 1} failed: {e}")
|
|
90
|
+
|
|
91
|
+
if attempt < max_attempts - 1:
|
|
92
|
+
logger.info(f"Retrying in {wait_seconds} seconds...")
|
|
93
|
+
time.sleep(wait_seconds)
|
|
94
|
+
|
|
95
|
+
logger.error(f"Failed to connect to database after {max_attempts} attempts")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check_database_health() -> dict:
|
|
100
|
+
"""
|
|
101
|
+
Check database health and return status information.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dictionary with health status information
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
from .iris_client import IRISClient
|
|
108
|
+
|
|
109
|
+
# Get connection parameters from environment
|
|
110
|
+
database_url = os.getenv("DATABASE_URL", "iris://localhost:1974/USER")
|
|
111
|
+
|
|
112
|
+
import urllib.parse
|
|
113
|
+
|
|
114
|
+
parsed = urllib.parse.urlparse(database_url)
|
|
115
|
+
|
|
116
|
+
host = parsed.hostname or "localhost"
|
|
117
|
+
port = parsed.port or 1972
|
|
118
|
+
username = parsed.username or "demo"
|
|
119
|
+
password = parsed.password or "demo"
|
|
120
|
+
namespace = parsed.path.lstrip("/") or "USER"
|
|
121
|
+
|
|
122
|
+
# Create IRIS client and test connection
|
|
123
|
+
client = IRISClient(
|
|
124
|
+
host=host,
|
|
125
|
+
port=port,
|
|
126
|
+
username=username,
|
|
127
|
+
password=password,
|
|
128
|
+
namespace=namespace,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Test with a simple query
|
|
132
|
+
result = client.execute_query("SELECT 1")
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"status": "healthy" if result else "unhealthy",
|
|
136
|
+
"database_url": f"iris://{host}:{port}/{namespace}",
|
|
137
|
+
"connection_test": "passed" if result else "failed",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return {"status": "unhealthy", "error": str(e), "connection_test": "failed"}
|