trovesuite 1.0.28__tar.gz → 1.0.30__tar.gz

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.
Files changed (43) hide show
  1. {trovesuite-1.0.28/src/trovesuite.egg-info → trovesuite-1.0.30}/PKG-INFO +1 -1
  2. {trovesuite-1.0.28 → trovesuite-1.0.30}/pyproject.toml +2 -2
  3. {trovesuite-1.0.28 → trovesuite-1.0.30}/setup.py +1 -1
  4. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/__init__.py +1 -1
  5. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/configs/database.py +165 -17
  6. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/configs/settings.py +0 -5
  7. {trovesuite-1.0.28 → trovesuite-1.0.30/src/trovesuite.egg-info}/PKG-INFO +1 -1
  8. {trovesuite-1.0.28 → trovesuite-1.0.30}/LICENSE +0 -0
  9. {trovesuite-1.0.28 → trovesuite-1.0.30}/MANIFEST.in +0 -0
  10. {trovesuite-1.0.28 → trovesuite-1.0.30}/README.md +0 -0
  11. {trovesuite-1.0.28 → trovesuite-1.0.30}/requirements.txt +0 -0
  12. {trovesuite-1.0.28 → trovesuite-1.0.30}/setup.cfg +0 -0
  13. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/auth/__init__.py +0 -0
  14. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/auth/auth_base.py +0 -0
  15. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/auth/auth_controller.py +0 -0
  16. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/auth/auth_read_dto.py +0 -0
  17. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/auth/auth_service.py +0 -0
  18. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/auth/auth_write_dto.py +0 -0
  19. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/configs/__init__.py +0 -0
  20. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/configs/logging.py +0 -0
  21. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/entities/__init__.py +0 -0
  22. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/entities/health.py +0 -0
  23. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/entities/sh_response.py +0 -0
  24. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/notification/__init__.py +0 -0
  25. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/notification/notification_base.py +0 -0
  26. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/notification/notification_controller.py +0 -0
  27. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/notification/notification_read_dto.py +0 -0
  28. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/notification/notification_service.py +0 -0
  29. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/notification/notification_write_dto.py +0 -0
  30. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/storage/__init__.py +0 -0
  31. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/storage/storage_base.py +0 -0
  32. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/storage/storage_controller.py +0 -0
  33. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/storage/storage_read_dto.py +0 -0
  34. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/storage/storage_service.py +0 -0
  35. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/storage/storage_write_dto.py +0 -0
  36. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/utils/__init__.py +0 -0
  37. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/utils/helper.py +0 -0
  38. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite/utils/templates.py +0 -0
  39. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite.egg-info/SOURCES.txt +0 -0
  40. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite.egg-info/dependency_links.txt +0 -0
  41. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite.egg-info/not-zip-safe +0 -0
  42. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite.egg-info/requires.txt +0 -0
  43. {trovesuite-1.0.28 → trovesuite-1.0.30}/src/trovesuite.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trovesuite
3
- Version: 1.0.28
3
+ Version: 1.0.30
4
4
  Summary: TroveSuite services package providing authentication, authorization, notifications, Azure Storage, and other enterprise services for TroveSuite applications
5
5
  Home-page: https://dev.azure.com/brightgclt/trovesuite/_git/packages
6
6
  Author: Bright Debrah Owusu
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "trovesuite"
7
- version = "1.0.28"
7
+ version = "1.0.30"
8
8
  description = "TroveSuite services package providing authentication, authorization, notifications, Azure Storage, and other enterprise services for TroveSuite applications"
9
9
  authors = ["brightgclt <brightgclt@gmail.com>"]
10
10
  license = "MIT"
@@ -58,7 +58,7 @@ Documentation = "https://dev.azure.com/brightgclt/trovesuite/_git/packages"
58
58
 
59
59
  [project]
60
60
  name = "trovesuite"
61
- version = "1.0.28"
61
+ version = "1.0.30"
62
62
  description = "TroveSuite services package providing authentication, authorization, notifications, Azure Storage, and other enterprise services for TroveSuite applications"
63
63
  readme = "README.md"
64
64
  license = {text = "MIT"}
@@ -15,7 +15,7 @@ with open("pyproject.toml", "r", encoding="utf-8") as fh:
15
15
 
16
16
  setup(
17
17
  name="trovesuite",
18
- version="1.0.28",
18
+ version="1.0.30",
19
19
  author="Bright Debrah Owusu",
20
20
  author_email="owusu.debrah@deladetech.com",
21
21
  description="TroveSuite services package providing authentication, authorization, notifications, and other enterprise services for TroveSuite applications",
@@ -11,7 +11,7 @@ from .notification import NotificationService
11
11
  from .storage import StorageService
12
12
  from .utils import Helper
13
13
 
14
- __version__ = "1.0.20"
14
+ __version__ = "1.0.29"
15
15
  __author__ = "Bright Debrah Owusu"
16
16
  __email__ = "owusu.debrah@deladetech.com"
17
17
 
@@ -6,6 +6,7 @@ from typing import Generator, Optional
6
6
  import psycopg2
7
7
  import psycopg2.pool
8
8
  from psycopg2.extras import RealDictCursor
9
+ import threading
9
10
  from .settings import db_settings
10
11
  from .logging import get_logger
11
12
 
@@ -13,16 +14,23 @@ logger = get_logger("database")
13
14
 
14
15
  # Database connection pool
15
16
  _connection_pool: Optional[psycopg2.pool.ThreadedConnectionPool] = None
17
+ _initialization_lock = threading.Lock()
16
18
 
17
19
 
18
20
  class DatabaseConfig:
19
21
  """Database configuration and connection management"""
20
22
 
21
23
  def __init__(self):
24
+ import os
22
25
  self.settings = db_settings
23
26
  self.database_url = self.settings.database_url
24
- self.pool_size = 5
25
- self.max_overflow = 10
27
+ # CRITICAL: Azure PostgreSQL B_Standard_B1ms has very limited connections (~50 max, practical limit ~30-40)
28
+ # Default pool size is set to 1 to avoid connection exhaustion with multiple workers/replicas
29
+ # Formula: connections = pool_size × workers × replicas
30
+ # Example: 1 pool × 4 workers × 2 replicas = 8 connections (safe)
31
+ # With old default (5): 5 × 4 × 2 = 40 connections (exceeds limit!)
32
+ # Can be overridden with DB_POOL_SIZE environment variable
33
+ self.pool_size = int(os.getenv("DB_POOL_SIZE", "1"))
26
34
 
27
35
  def get_connection_params(self) -> dict:
28
36
  """Get database connection parameters"""
@@ -35,7 +43,7 @@ class DatabaseConfig:
35
43
  "keepalives_idle": 30,
36
44
  "keepalives_interval": 10,
37
45
  "keepalives_count": 5,
38
- "connect_timeout": 10
46
+ "connect_timeout": 30 # Increased timeout for Azure PostgreSQL
39
47
  }
40
48
 
41
49
  # fallback to individual DB_* variables
@@ -55,15 +63,54 @@ class DatabaseConfig:
55
63
  }
56
64
 
57
65
  def create_connection_pool(self) -> psycopg2.pool.ThreadedConnectionPool:
58
- """Create a connection pool for psycopg2"""
66
+ """Create a connection pool for psycopg2
67
+
68
+ Note: For Azure PostgreSQL B_Standard_B1ms, keep pool_size ≤ 1 per worker
69
+ to avoid connection exhaustion. Consider using PgBouncer for higher concurrency.
70
+ """
59
71
  try:
72
+ # For Azure PostgreSQL, use minimum pool size to avoid connection exhaustion
73
+ # Multiple replicas/workers can quickly exhaust database connections on Basic tier
74
+ import os
75
+ dsn = os.getenv("DATABASE_URL", "") or str(self.database_url or "")
76
+ is_azure = "database.azure.com" in dsn.lower()
77
+
78
+ # Ensure pool size is appropriate for Azure Basic tier
79
+ pool_size = self.pool_size
80
+ if is_azure and pool_size > 2:
81
+ logger.warning(
82
+ f"⚠️ Pool size {pool_size} may be too high for Azure Basic tier. "
83
+ f"Recommended: 1-2 connections per worker. "
84
+ f"Set DB_POOL_SIZE=1 to avoid connection exhaustion."
85
+ )
86
+
60
87
  pool = psycopg2.pool.ThreadedConnectionPool(
61
88
  minconn=1,
62
- maxconn=self.pool_size,
89
+ maxconn=pool_size,
63
90
  **self.get_connection_params()
64
91
  )
65
- logger.info(f"Database connection pool created with {self.pool_size} connections")
92
+ logger.info(
93
+ f"Database connection pool created with {pool_size} connections "
94
+ f"(Azure: {is_azure}, DB_POOL_SIZE: {self.pool_size})"
95
+ )
66
96
  return pool
97
+ except psycopg2.OperationalError as e:
98
+ # Check if it's a connection limit error
99
+ error_str = str(e).lower()
100
+ if any(keyword in error_str for keyword in ["connection", "slot", "limit", "exhausted", "too many"]):
101
+ logger.error("⚠️ Database connection limit reached!")
102
+ logger.error(" Possible causes:")
103
+ logger.error(" 1. Too many connections from multiple replicas/workers")
104
+ logger.error(" 2. Pool size too high (DB_POOL_SIZE environment variable)")
105
+ logger.error(" 3. Too many Gunicorn workers (GUNICORN_WORKERS environment variable)")
106
+ logger.error(" 4. Connections not being properly returned to pool")
107
+ logger.error(" Solutions:")
108
+ logger.error(" - Set DB_POOL_SIZE=1 (recommended for Azure Basic tier)")
109
+ logger.error(" - Reduce GUNICORN_WORKERS (default: 4)")
110
+ logger.error(" - Consider using PgBouncer for connection pooling")
111
+ logger.error(" - Upgrade to a higher PostgreSQL tier if needed")
112
+ logger.error(f"Failed to create database connection pool: {str(e)}")
113
+ raise
67
114
  except Exception as e:
68
115
  logger.error(f"Failed to create database connection pool: {str(e)}")
69
116
  raise
@@ -91,6 +138,15 @@ db_config = DatabaseConfig()
91
138
  def initialize_database():
92
139
  """Initialize database connections and pool"""
93
140
  global _connection_pool
141
+
142
+ # Close existing pool if it exists (cleanup before reinitializing)
143
+ if _connection_pool is not None:
144
+ try:
145
+ _connection_pool.closeall()
146
+ logger.info("Closed existing connection pool before reinitialization")
147
+ except Exception as e:
148
+ logger.warning(f"Error closing existing pool: {str(e)}")
149
+ _connection_pool = None
94
150
 
95
151
  try:
96
152
  # Test connection first
@@ -99,39 +155,131 @@ def initialize_database():
99
155
 
100
156
  # Create connection pool
101
157
  _connection_pool = db_config.create_connection_pool()
158
+
159
+ # Verify pool was created successfully
160
+ if _connection_pool is None:
161
+ raise Exception("Connection pool creation returned None")
102
162
 
103
- logger.info("Database initialization completed successfully")
163
+ logger.info("Database initialization completed successfully")
104
164
 
105
165
  except Exception as e:
106
- logger.error(f"Database initialization failed: {str(e)}")
166
+ logger.error(f"Database initialization failed: {str(e)}")
167
+ _connection_pool = None # Ensure pool is None on failure
107
168
  raise
108
169
 
109
170
 
171
+ def _is_pool_valid(pool) -> bool:
172
+ """Check if the connection pool is valid and usable"""
173
+ if pool is None:
174
+ return False
175
+ try:
176
+ # ThreadedConnectionPool doesn't expose a direct "closed" attribute
177
+ # Check if pool has the necessary internal structures
178
+ if not hasattr(pool, '_pool'):
179
+ return False
180
+ if pool._pool is None:
181
+ return False
182
+ # Additional check: verify pool has connection parameters
183
+ if not hasattr(pool, '_kwargs'):
184
+ return False
185
+ return True
186
+ except (AttributeError, Exception) as e:
187
+ logger.debug(f"Pool validation check: {str(e)}")
188
+ return False
189
+
190
+
191
+ def _recover_connection_pool() -> bool:
192
+ """Attempt to recover the connection pool with retry logic"""
193
+ global _connection_pool, _initialization_lock
194
+ import time
195
+
196
+ with _initialization_lock:
197
+ # Double-check after acquiring lock
198
+ if _connection_pool is not None and _is_pool_valid(_connection_pool):
199
+ return True
200
+
201
+ # Close invalid pool if it exists
202
+ if _connection_pool is not None:
203
+ try:
204
+ _connection_pool.closeall()
205
+ logger.info("Closed invalid connection pool")
206
+ except Exception as e:
207
+ logger.warning(f"Error closing invalid pool: {str(e)}")
208
+ _connection_pool = None
209
+
210
+ # Retry with exponential backoff
211
+ max_retries = 3
212
+ base_delay = 1 # Start with 1 second
213
+
214
+ for attempt in range(1, max_retries + 1):
215
+ try:
216
+ logger.warning(f"Attempting to reinitialize connection pool (attempt {attempt}/{max_retries})...")
217
+ initialize_database()
218
+
219
+ if _connection_pool is not None and _is_pool_valid(_connection_pool):
220
+ logger.info(f"✅ Connection pool reinitialized successfully (attempt {attempt})")
221
+ return True
222
+ else:
223
+ logger.warning(f"Pool initialized but validation failed (attempt {attempt})")
224
+
225
+ except Exception as e:
226
+ logger.error(f"Pool reinitialization attempt {attempt} failed: {str(e)}")
227
+ if attempt < max_retries:
228
+ delay = base_delay * (2 ** (attempt - 1)) # Exponential backoff: 1s, 2s, 4s
229
+ logger.info(f"Retrying in {delay} seconds...")
230
+ time.sleep(delay)
231
+
232
+ logger.error("❌ Failed to reinitialize connection pool after all retries")
233
+ return False
234
+
235
+
110
236
  def get_connection_pool() -> psycopg2.pool.ThreadedConnectionPool:
111
- """Get the database connection pool"""
237
+ """Get the database connection pool, with automatic reinitialization if needed"""
112
238
  global _connection_pool
113
- if _connection_pool is None:
239
+
240
+ # Fast path: pool exists and is valid
241
+ if _connection_pool is not None and _is_pool_valid(_connection_pool):
242
+ return _connection_pool
243
+
244
+ # Pool is None or invalid, attempt recovery
245
+ if not _recover_connection_pool():
114
246
  error_msg = (
115
- "Database not initialized. This usually means:\n"
116
- "1. Missing or incorrect .env file in app/ directory\n"
117
- "2. Database credentials are wrong\n"
118
- "3. Database container is not running\n"
119
- "4. Database initialization failed during startup\n"
120
- "Please check the startup logs for more details."
247
+ "Database connection pool is unavailable. This usually means:\n"
248
+ "1. Database server is unreachable or down\n"
249
+ "2. Network connectivity issues\n"
250
+ "3. Database credentials are incorrect\n"
251
+ "4. Connection pool exhausted or closed\n"
252
+ "5. Database initialization failed\n"
253
+ "Please check the startup logs and database status."
121
254
  )
122
255
  logger.error(error_msg)
123
256
  raise Exception(error_msg)
257
+
258
+ if _connection_pool is None:
259
+ error_msg = "Connection pool recovery completed but pool is still None"
260
+ logger.error(error_msg)
261
+ raise Exception(error_msg)
262
+
124
263
  return _connection_pool
125
264
 
126
265
 
127
266
  def _validate_connection(conn) -> bool:
128
267
  """Validate if a connection is still alive"""
129
268
  try:
269
+ # Check if connection is closed first
270
+ if conn.closed:
271
+ return False
272
+
130
273
  # Test if connection is alive with a simple query
131
274
  with conn.cursor() as cursor:
132
275
  cursor.execute("SELECT 1")
276
+ cursor.fetchone()
133
277
  return True
134
- except (psycopg2.OperationalError, psycopg2.InterfaceError):
278
+ except (psycopg2.OperationalError, psycopg2.InterfaceError, psycopg2.DatabaseError) as e:
279
+ logger.warning(f"Connection validation failed: {str(e)}")
280
+ return False
281
+ except Exception as e:
282
+ logger.warning(f"Unexpected error during connection validation: {str(e)}")
135
283
  return False
136
284
 
137
285
 
@@ -10,11 +10,6 @@ class Settings:
10
10
  DB_PORT: str = os.getenv("DB_PORT")
11
11
  DB_PASSWORD: str = os.getenv("DB_PASSWORD")
12
12
 
13
- # CORS Endpoint
14
- # LOCAL_HOST_URL = os.getenv("LOCAL_HOST_URL")
15
- # FRONTEND_SERVER_URL_1 = os.getenv("FRONTEND_SERVER_URL")
16
- # FRONTEND_SERVER_URL_2 = os.getenv("FRONTEND_SERVER_URL")
17
-
18
13
  # Application settings
19
14
  DEBUG: bool = os.getenv("DEBUG", "True").lower() in ("true",1)
20
15
  APP_NAME: str = os.getenv("APP_NAME", "Python Template API")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trovesuite
3
- Version: 1.0.28
3
+ Version: 1.0.30
4
4
  Summary: TroveSuite services package providing authentication, authorization, notifications, Azure Storage, and other enterprise services for TroveSuite applications
5
5
  Home-page: https://dev.azure.com/brightgclt/trovesuite/_git/packages
6
6
  Author: Bright Debrah Owusu
File without changes
File without changes
File without changes
File without changes