Subham9126 commited on
Commit
1d2b0ce
·
verified ·
1 Parent(s): 1d66a78

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +53 -86
app.py CHANGED
@@ -25,87 +25,72 @@ logging.basicConfig(
25
  logger = logging.getLogger(__name__)
26
 
27
  # --- Enhanced Session Management ---
 
 
 
 
 
 
 
 
28
  class SessionManager:
29
- """Thread-safe session manager with proper event loop handling."""
 
 
30
 
31
  def __init__(self):
32
- self._session = None
33
- self._session_lock = None # Will be created per event loop
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 _get_or_create_lock(self):
41
- """Get or create lock for current event loop."""
42
- try:
43
- current_loop = asyncio.get_running_loop()
44
- current_loop_id = id(current_loop)
45
-
46
- # If we're in a different event loop, reset session
47
- if self._loop_id and self._loop_id != current_loop_id:
48
- logger.info(f"🔄 Event loop changed, resetting session")
49
- self._session = None
50
- self._session_lock = None
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
- """Get or create a session with proper event loop handling."""
66
- lock = self._get_or_create_lock()
67
- if lock is None:
68
- raise RuntimeError("No event loop is running")
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
- # Close old session if exists
83
- if self._session and not self._session.closed:
84
- logger.info(f"🔄 Closing old session after {self._request_count} requests")
85
- try:
86
- await self._session.close()
87
- await asyncio.sleep(0.1) # Allow cleanup
88
- except Exception as e:
89
- logger.warning(f"Error closing old session: {e}")
 
90
 
91
- # Create new optimized session
92
  try:
93
  connector = aiohttp.TCPConnector(
94
- limit=100,
95
- limit_per_host=50,
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
- """Thread-safe request count increment."""
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