Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -25,87 +25,72 @@ logging.basicConfig(
|
|
| 25 |
logger = logging.getLogger(__name__)
|
| 26 |
|
| 27 |
# --- Enhanced Session Management ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
class SessionManager:
|
| 29 |
-
"""
|
|
|
|
|
|
|
| 30 |
|
| 31 |
def __init__(self):
|
| 32 |
-
self._session = None
|
| 33 |
-
self._session_lock =
|
| 34 |
self._creation_time = None
|
| 35 |
self._request_count = 0
|
| 36 |
-
self._loop_id = None # Track which event loop owns the session
|
| 37 |
self.max_session_age = 300 # 5 minutes
|
| 38 |
self.max_requests_per_session = 1000
|
| 39 |
|
| 40 |
-
def
|
| 41 |
-
"""
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
self._creation_time = None
|
| 52 |
-
self._request_count = 0
|
| 53 |
-
|
| 54 |
-
self._loop_id = current_loop_id
|
| 55 |
-
|
| 56 |
-
if self._session_lock is None:
|
| 57 |
-
self._session_lock = asyncio.Lock()
|
| 58 |
-
|
| 59 |
-
return self._session_lock
|
| 60 |
-
except RuntimeError:
|
| 61 |
-
# No event loop running
|
| 62 |
-
return None
|
| 63 |
-
|
| 64 |
async def get_session(self) -> aiohttp.ClientSession:
|
| 65 |
-
"""
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
async with lock:
|
| 71 |
now = time.time()
|
| 72 |
|
| 73 |
-
# Check if session needs renewal
|
| 74 |
needs_renewal = (
|
| 75 |
self._session is None or
|
| 76 |
self._session.closed or
|
| 77 |
(self._creation_time and now - self._creation_time > self.max_session_age) or
|
| 78 |
-
self._request_count > self.max_requests_per_session
|
| 79 |
)
|
| 80 |
|
| 81 |
if needs_renewal:
|
| 82 |
-
#
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
| 90 |
|
| 91 |
-
# Create new
|
| 92 |
try:
|
| 93 |
connector = aiohttp.TCPConnector(
|
| 94 |
-
limit=100,
|
| 95 |
-
|
| 96 |
-
ttl_dns_cache=300,
|
| 97 |
-
use_dns_cache=True,
|
| 98 |
-
keepalive_timeout=30,
|
| 99 |
enable_cleanup_closed=True,
|
| 100 |
-
force_close=False,
|
| 101 |
)
|
| 102 |
-
|
| 103 |
-
timeout = aiohttp.ClientTimeout(
|
| 104 |
-
total=60,
|
| 105 |
-
connect=10,
|
| 106 |
-
sock_read=30
|
| 107 |
-
)
|
| 108 |
-
|
| 109 |
headers = {
|
| 110 |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
| 111 |
'Accept': 'application/json',
|
|
@@ -115,38 +100,35 @@ class SessionManager:
|
|
| 115 |
}
|
| 116 |
|
| 117 |
self._session = aiohttp.ClientSession(
|
| 118 |
-
connector=connector,
|
| 119 |
-
timeout=timeout,
|
| 120 |
-
headers=headers,
|
| 121 |
-
raise_for_status=False
|
| 122 |
)
|
| 123 |
|
| 124 |
self._creation_time = now
|
| 125 |
self._request_count = 0
|
| 126 |
-
logger.info(f"🚀 Created new session at {datetime.fromtimestamp(now)}")
|
| 127 |
|
| 128 |
except Exception as e:
|
| 129 |
-
logger.error(f"Failed to create session: {e}")
|
|
|
|
|
|
|
|
|
|
| 130 |
raise
|
| 131 |
|
| 132 |
return self._session
|
| 133 |
|
| 134 |
def increment_request_count(self):
|
| 135 |
-
"""
|
| 136 |
self._request_count += 1
|
| 137 |
|
| 138 |
async def close(self):
|
| 139 |
-
"""Clean shutdown of session."""
|
| 140 |
if self._session and not self._session.closed:
|
| 141 |
try:
|
| 142 |
-
logger.info(f"🔒 Closing session after {self._request_count} requests")
|
| 143 |
await self._session.close()
|
| 144 |
-
await asyncio.sleep(0.1)
|
| 145 |
except Exception as e:
|
| 146 |
-
logger.warning(f"Error during session cleanup: {e}")
|
| 147 |
self._session = None
|
| 148 |
-
self._session_lock = None
|
| 149 |
-
|
| 150 |
# --- Enhanced Data Models ---
|
| 151 |
|
| 152 |
class StockDataRequest(BaseModel):
|
|
@@ -252,21 +234,6 @@ def get_time_range_in_unix_ms(start_date_str: str, end_date_str: str, timezone:
|
|
| 252 |
return {"start_timestamp_ms": start_ts, "end_timestamp_ms": end_ts}
|
| 253 |
|
| 254 |
|
| 255 |
-
# Global session manager per event loop to handle multiple threads/loops
|
| 256 |
-
_session_managers = weakref.WeakKeyDictionary()
|
| 257 |
-
|
| 258 |
-
def get_session_manager():
|
| 259 |
-
"""Get or create session manager for current event loop."""
|
| 260 |
-
try:
|
| 261 |
-
loop = asyncio.get_running_loop()
|
| 262 |
-
if loop not in _session_managers:
|
| 263 |
-
_session_managers[loop] = SessionManager()
|
| 264 |
-
logger.debug(f"📝 Created new SessionManager for event loop {id(loop)}")
|
| 265 |
-
return _session_managers[loop]
|
| 266 |
-
except RuntimeError:
|
| 267 |
-
# No event loop running
|
| 268 |
-
logger.warning("⚠️ No event loop running")
|
| 269 |
-
return None
|
| 270 |
|
| 271 |
# --- Optimized API Functions ---
|
| 272 |
|
|
|
|
| 25 |
logger = logging.getLogger(__name__)
|
| 26 |
|
| 27 |
# --- Enhanced Session Management ---
|
| 28 |
+
import asyncio
|
| 29 |
+
import time
|
| 30 |
+
from datetime import datetime
|
| 31 |
+
import aiohttp
|
| 32 |
+
import logging
|
| 33 |
+
|
| 34 |
+
logger = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
class SessionManager:
|
| 37 |
+
"""
|
| 38 |
+
Thread-safe session manager with graceful rotation to prevent race conditions.
|
| 39 |
+
"""
|
| 40 |
|
| 41 |
def __init__(self):
|
| 42 |
+
self._session: aiohttp.ClientSession = None
|
| 43 |
+
self._session_lock = asyncio.Lock() # This lock is tied to the event loop it's created in.
|
| 44 |
self._creation_time = None
|
| 45 |
self._request_count = 0
|
|
|
|
| 46 |
self.max_session_age = 300 # 5 minutes
|
| 47 |
self.max_requests_per_session = 1000
|
| 48 |
|
| 49 |
+
async def _graceful_close(self, session_to_close: aiohttp.ClientSession, delay: int = 5):
|
| 50 |
+
"""Waits for a delay before closing a stale session to allow in-flight requests to complete."""
|
| 51 |
+
if session_to_close and not session_to_close.closed:
|
| 52 |
+
logger.info(f"⏳ Waiting {delay}s before closing stale session (ID: {id(session_to_close)})...")
|
| 53 |
+
await asyncio.sleep(delay)
|
| 54 |
+
try
|
| 55 |
+
logger.info(f"🧹 Gracefully closing stale session (ID: {id(session_to_close)}).")
|
| 56 |
+
await session_to_close.close()
|
| 57 |
+
except Exception as e:
|
| 58 |
+
logger.warning(f"⚠️ Error during graceful close of stale session: {e}")
|
| 59 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
async def get_session(self) -> aiohttp.ClientSession:
|
| 61 |
+
"""
|
| 62 |
+
Get or create a session with graceful rotation to prevent race conditions.
|
| 63 |
+
"""
|
| 64 |
+
async with self._session_lock:
|
|
|
|
|
|
|
| 65 |
now = time.time()
|
| 66 |
|
| 67 |
+
# Check if session needs renewal. Use >= for precision on max requests.
|
| 68 |
needs_renewal = (
|
| 69 |
self._session is None or
|
| 70 |
self._session.closed or
|
| 71 |
(self._creation_time and now - self._creation_time > self.max_session_age) or
|
| 72 |
+
self._request_count >= self.max_requests_per_session
|
| 73 |
)
|
| 74 |
|
| 75 |
if needs_renewal:
|
| 76 |
+
# Keep a reference to the old session
|
| 77 |
+
old_session = self._session
|
| 78 |
+
|
| 79 |
+
# *** THE FIX: Schedule the old session's closure instead of awaiting it. ***
|
| 80 |
+
if old_session and not old_session.closed:
|
| 81 |
+
logger.info(f"🔄 Scheduling closure of old session after {self._request_count} requests.")
|
| 82 |
+
loop = asyncio.get_running_loop()
|
| 83 |
+
# This creates a background task that will run independently.
|
| 84 |
+
loop.create_task(self._graceful_close(old_session))
|
| 85 |
|
| 86 |
+
# --- Create the new session immediately ---
|
| 87 |
try:
|
| 88 |
connector = aiohttp.TCPConnector(
|
| 89 |
+
limit=100, limit_per_host=50, ttl_dns_cache=300,
|
| 90 |
+
use_dns_cache=True, keepalive_timeout=30,
|
|
|
|
|
|
|
|
|
|
| 91 |
enable_cleanup_closed=True,
|
|
|
|
| 92 |
)
|
| 93 |
+
timeout = aiohttp.ClientTimeout(total=60, connect=10, sock_read=30)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
headers = {
|
| 95 |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
| 96 |
'Accept': 'application/json',
|
|
|
|
| 100 |
}
|
| 101 |
|
| 102 |
self._session = aiohttp.ClientSession(
|
| 103 |
+
connector=connector, timeout=timeout, headers=headers
|
|
|
|
|
|
|
|
|
|
| 104 |
)
|
| 105 |
|
| 106 |
self._creation_time = now
|
| 107 |
self._request_count = 0
|
| 108 |
+
logger.info(f"🚀 Created new session (ID: {id(self._session)}) at {datetime.fromtimestamp(now)}")
|
| 109 |
|
| 110 |
except Exception as e:
|
| 111 |
+
logger.error(f"Failed to create new session: {e}")
|
| 112 |
+
# If creation fails, restore the old session if it's still valid
|
| 113 |
+
if old_session and not old_session.closed:
|
| 114 |
+
self._session = old_session
|
| 115 |
raise
|
| 116 |
|
| 117 |
return self._session
|
| 118 |
|
| 119 |
def increment_request_count(self):
|
| 120 |
+
"""Increment the request count. No lock needed as it's called after getting the session."""
|
| 121 |
self._request_count += 1
|
| 122 |
|
| 123 |
async def close(self):
|
| 124 |
+
"""Clean shutdown of the current active session."""
|
| 125 |
if self._session and not self._session.closed:
|
| 126 |
try:
|
| 127 |
+
logger.info(f"🔒 Closing active session after {self._request_count} requests.")
|
| 128 |
await self._session.close()
|
|
|
|
| 129 |
except Exception as e:
|
| 130 |
+
logger.warning(f"Error during final session cleanup: {e}")
|
| 131 |
self._session = None
|
|
|
|
|
|
|
| 132 |
# --- Enhanced Data Models ---
|
| 133 |
|
| 134 |
class StockDataRequest(BaseModel):
|
|
|
|
| 234 |
return {"start_timestamp_ms": start_ts, "end_timestamp_ms": end_ts}
|
| 235 |
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
# --- Optimized API Functions ---
|
| 239 |
|