Soumik Bose commited on
Commit
95d1612
·
1 Parent(s): 8bb62e2
Files changed (1) hide show
  1. controller.py +30 -19
controller.py CHANGED
@@ -10,6 +10,7 @@ import multiprocessing
10
  import hashlib
11
  import json
12
  import psutil
 
13
  from typing import List, Optional, Dict, Any, TypeVar, Generic
14
  from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
15
  from threading import Lock
@@ -29,7 +30,7 @@ import asyncpg
29
  import asyncmy
30
  from asyncmy.cursors import DictCursor
31
 
32
- # --- Database Drivers (Kept for compatibility/types if needed, but unused in SQL routes) ---
33
  from bson import ObjectId
34
  import mysql.connector
35
  import psycopg2
@@ -68,7 +69,7 @@ class AsyncPoolManager:
68
  async def get_pg_pool(self, db_url: str) -> asyncpg.Pool:
69
  async with self._lock:
70
  if db_url not in self._pg_pools:
71
- logger.info(f"Creating new AsyncPG pool for: {db_url[:20]}...")
72
  try:
73
  # asyncpg handles Keepalives and SSL automatically better than psycopg2
74
  pool = await asyncpg.create_pool(
@@ -85,13 +86,27 @@ class AsyncPoolManager:
85
  return self._pg_pools[db_url]
86
 
87
  async def get_mysql_pool(self, db_url: str) -> asyncmy.Pool:
 
 
 
 
88
  async with self._lock:
 
89
  if db_url not in self._mysql_pools:
90
- logger.info(f"Creating new AsyncMy pool for: {db_url[:20]}...")
91
  parsed = urlparse(db_url)
92
- # Check for SSL requirement in query params
93
  qs = parse_qs(parsed.query)
94
- # AsyncMy often auto-negotiates SSL, but we can enforce it if needed based on qs
 
 
 
 
 
 
 
 
 
 
95
 
96
  try:
97
  pool = await asyncmy.create_pool(
@@ -103,12 +118,14 @@ class AsyncPoolManager:
103
  minsize=1,
104
  maxsize=20,
105
  autocommit=True,
106
- pool_recycle=280, # Recycle before default 8hr timeout to prevent "Gone Away"
 
107
  )
108
  self._mysql_pools[db_url] = pool
109
  except Exception as e:
110
  logger.error(f"Failed to create MySQL pool: {e}")
111
  raise e
 
112
  return self._mysql_pools[db_url]
113
 
114
  async def close_all(self):
@@ -119,11 +136,9 @@ class AsyncPoolManager:
119
  pool.close()
120
  await pool.wait_closed()
121
 
122
- # Initialize Global Async Manager
123
  async_pool_manager = AsyncPoolManager()
124
 
125
  def get_dynamic_thread_limit():
126
- """Calculates a safe thread limit based on available RAM."""
127
  try:
128
  total_ram_bytes = psutil.virtual_memory().total
129
  total_cores = multiprocessing.cpu_count()
@@ -141,12 +156,10 @@ def get_dynamic_thread_limit():
141
 
142
  @asynccontextmanager
143
  async def lifespan(app: FastAPI):
144
- # Startup
145
  safe_limit = get_dynamic_thread_limit()
146
  to_thread.current_default_thread_limiter().total_tokens = safe_limit
147
  logger.info(f"Worker Process Started: Thread pool capacity set to {safe_limit}. Async Drivers Ready.")
148
  yield
149
- # Shutdown
150
  await async_pool_manager.close_all()
151
 
152
  app = FastAPI(title="Unified Data Executor API", lifespan=lifespan)
@@ -203,7 +216,6 @@ class RequestCoalescer:
203
  self._active_requests[key] = future
204
 
205
  try:
206
- # Works for both async functions and run_in_threadpool
207
  result = await func(*args, **kwargs)
208
  if not future.done(): future.set_result(result)
209
  return result
@@ -230,10 +242,15 @@ def is_aggregate_query(query: str) -> bool:
230
  return False
231
 
232
  def normalize_mysql_uri(uri: str) -> str:
 
 
 
 
233
  try:
234
  parsed_uri = urlparse(uri)
235
  query_params = parse_qs(parsed_uri.query)
236
- query_params.pop('ssl-mode', None)
 
237
  new_query = urlencode(query_params, doseq=True)
238
  parsed_uri = parsed_uri._replace(query=new_query)
239
  return urlunparse(parsed_uri)
@@ -253,7 +270,6 @@ def normalize_postgres_uri(uri: str) -> str:
253
  async def _execute_async_mysql(db_url: str, sql_query: str, max_rows: int = 20, limited: bool = False) -> dict:
254
  start_time = time.time()
255
  try:
256
- # Get pool
257
  pool = await async_pool_manager.get_mysql_pool(db_url)
258
 
259
  async with pool.acquire() as conn:
@@ -285,7 +301,6 @@ async def _execute_async_mysql(db_url: str, sql_query: str, max_rows: int = 20,
285
  await cursor.execute(final_query)
286
  results = await cursor.fetchall()
287
 
288
- # Check actual length
289
  if is_limited_result and len(results) < max_rows:
290
  is_limited_result = False
291
  message = f"Returned {len(results)} rows."
@@ -333,7 +348,6 @@ async def _execute_async_postgres(db_url: str, sql_query: str, max_rows: int = 2
333
  is_limited_result = True
334
  message = f"Showing first {max_rows} rows."
335
 
336
- # Fetch results
337
  records = await conn.fetch(final_query)
338
  results = [dict(r) for r in records]
339
 
@@ -405,7 +419,6 @@ async def execute_mysql_endpoint(query: SqlQueryRequest, token: str = Depends(va
405
  unique_params = {
406
  "db": normalized_url, "q": query.sql_query, "l": limit_val, "lim": query.limited
407
  }
408
- # Direct Async Call (No Threadpool)
409
  result_dict = await coalescer.execute(
410
  "mysql", unique_params, _execute_async_mysql,
411
  db_url=normalized_url, sql_query=query.sql_query, max_rows=limit_val, limited=query.limited
@@ -425,7 +438,6 @@ async def execute_postgres_endpoint(query: PgQueryRequest, token: str = Depends(
425
  unique_params = {
426
  "db": clean_url, "q": query.sql_query, "l": limit_val, "lim": query.limited
427
  }
428
- # Direct Async Call (No Threadpool)
429
  result_dict = await coalescer.execute(
430
  "postgres", unique_params, _execute_async_postgres,
431
  db_url=clean_url, sql_query=query.sql_query, max_rows=limit_val, limited=query.limited
@@ -446,7 +458,6 @@ async def execute_mongo_endpoint(payload: ExecutorPayload, token: str = Depends(
446
  "uri": payload.mongo_uri, "db": payload.db_name, "col": payload.collection_name,
447
  "q": parsed_query, "lim": payload.limited, "lrows": payload.limit_rows
448
  }
449
- # Mongo is Sync via PyMongo -> Uses run_in_threadpool
450
  result_data = await coalescer.execute(
451
  "mongo", unique_params, run_in_threadpool, execute_mongo_operation,
452
  mongo_uri=payload.mongo_uri, db_name=payload.db_name, collection_name=payload.collection_name,
@@ -558,7 +569,7 @@ async def test_parallel_calc_endpoint(payload: TestCalcRequest):
558
 
559
  @app.get("/")
560
  async def root():
561
- return {"message": "Code Execution Server is running"}
562
 
563
  @app.get("/ping")
564
  async def ping():
 
10
  import hashlib
11
  import json
12
  import psutil
13
+ import ssl
14
  from typing import List, Optional, Dict, Any, TypeVar, Generic
15
  from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
16
  from threading import Lock
 
30
  import asyncmy
31
  from asyncmy.cursors import DictCursor
32
 
33
+ # --- Database Drivers (Sync - unused but kept for imports) ---
34
  from bson import ObjectId
35
  import mysql.connector
36
  import psycopg2
 
69
  async def get_pg_pool(self, db_url: str) -> asyncpg.Pool:
70
  async with self._lock:
71
  if db_url not in self._pg_pools:
72
+ logger.info(f"Creating new AsyncPG pool for: {db_url[:25]}...")
73
  try:
74
  # asyncpg handles Keepalives and SSL automatically better than psycopg2
75
  pool = await asyncpg.create_pool(
 
86
  return self._pg_pools[db_url]
87
 
88
  async def get_mysql_pool(self, db_url: str) -> asyncmy.Pool:
89
+ # Check first without lock for speed
90
+ if db_url in self._mysql_pools:
91
+ return self._mysql_pools[db_url]
92
+
93
  async with self._lock:
94
+ # Check again inside lock
95
  if db_url not in self._mysql_pools:
96
+ logger.info(f"Creating new AsyncMy pool for: {db_url[:25]}...")
97
  parsed = urlparse(db_url)
 
98
  qs = parse_qs(parsed.query)
99
+
100
+ # Determine SSL settings
101
+ # asyncmy needs an SSL context or ssl=True/False
102
+ ssl_ctx = None
103
+ if 'ssl-mode' in qs:
104
+ mode = qs['ssl-mode'][0].upper()
105
+ if mode != 'DISABLED':
106
+ # Create a default SSL context that allows self-signed certs (common in cloud DBs)
107
+ ssl_ctx = ssl.create_default_context()
108
+ ssl_ctx.check_hostname = False
109
+ ssl_ctx.verify_mode = ssl.CERT_NONE
110
 
111
  try:
112
  pool = await asyncmy.create_pool(
 
118
  minsize=1,
119
  maxsize=20,
120
  autocommit=True,
121
+ pool_recycle=280,
122
+ ssl=ssl_ctx # Pass the SSL context explicitly
123
  )
124
  self._mysql_pools[db_url] = pool
125
  except Exception as e:
126
  logger.error(f"Failed to create MySQL pool: {e}")
127
  raise e
128
+
129
  return self._mysql_pools[db_url]
130
 
131
  async def close_all(self):
 
136
  pool.close()
137
  await pool.wait_closed()
138
 
 
139
  async_pool_manager = AsyncPoolManager()
140
 
141
  def get_dynamic_thread_limit():
 
142
  try:
143
  total_ram_bytes = psutil.virtual_memory().total
144
  total_cores = multiprocessing.cpu_count()
 
156
 
157
  @asynccontextmanager
158
  async def lifespan(app: FastAPI):
 
159
  safe_limit = get_dynamic_thread_limit()
160
  to_thread.current_default_thread_limiter().total_tokens = safe_limit
161
  logger.info(f"Worker Process Started: Thread pool capacity set to {safe_limit}. Async Drivers Ready.")
162
  yield
 
163
  await async_pool_manager.close_all()
164
 
165
  app = FastAPI(title="Unified Data Executor API", lifespan=lifespan)
 
216
  self._active_requests[key] = future
217
 
218
  try:
 
219
  result = await func(*args, **kwargs)
220
  if not future.done(): future.set_result(result)
221
  return result
 
242
  return False
243
 
244
  def normalize_mysql_uri(uri: str) -> str:
245
+ """
246
+ Normalizes the URI to ensure consistent cache keys.
247
+ DOES NOT REMOVE parameters (like ssl-mode) blindly, but sorts them.
248
+ """
249
  try:
250
  parsed_uri = urlparse(uri)
251
  query_params = parse_qs(parsed_uri.query)
252
+ # Re-encode with sorting to ensure ?a=1&b=2 is same as ?b=2&a=1
253
+ # We do NOT remove ssl-mode here anymore, so the pool manager knows about it
254
  new_query = urlencode(query_params, doseq=True)
255
  parsed_uri = parsed_uri._replace(query=new_query)
256
  return urlunparse(parsed_uri)
 
270
  async def _execute_async_mysql(db_url: str, sql_query: str, max_rows: int = 20, limited: bool = False) -> dict:
271
  start_time = time.time()
272
  try:
 
273
  pool = await async_pool_manager.get_mysql_pool(db_url)
274
 
275
  async with pool.acquire() as conn:
 
301
  await cursor.execute(final_query)
302
  results = await cursor.fetchall()
303
 
 
304
  if is_limited_result and len(results) < max_rows:
305
  is_limited_result = False
306
  message = f"Returned {len(results)} rows."
 
348
  is_limited_result = True
349
  message = f"Showing first {max_rows} rows."
350
 
 
351
  records = await conn.fetch(final_query)
352
  results = [dict(r) for r in records]
353
 
 
419
  unique_params = {
420
  "db": normalized_url, "q": query.sql_query, "l": limit_val, "lim": query.limited
421
  }
 
422
  result_dict = await coalescer.execute(
423
  "mysql", unique_params, _execute_async_mysql,
424
  db_url=normalized_url, sql_query=query.sql_query, max_rows=limit_val, limited=query.limited
 
438
  unique_params = {
439
  "db": clean_url, "q": query.sql_query, "l": limit_val, "lim": query.limited
440
  }
 
441
  result_dict = await coalescer.execute(
442
  "postgres", unique_params, _execute_async_postgres,
443
  db_url=clean_url, sql_query=query.sql_query, max_rows=limit_val, limited=query.limited
 
458
  "uri": payload.mongo_uri, "db": payload.db_name, "col": payload.collection_name,
459
  "q": parsed_query, "lim": payload.limited, "lrows": payload.limit_rows
460
  }
 
461
  result_data = await coalescer.execute(
462
  "mongo", unique_params, run_in_threadpool, execute_mongo_operation,
463
  mongo_uri=payload.mongo_uri, db_name=payload.db_name, collection_name=payload.collection_name,
 
569
 
570
  @app.get("/")
571
  async def root():
572
+ return {"message": "Python Code Execution Server is running"}
573
 
574
  @app.get("/ping")
575
  async def ping():