trovesuite 1.0.28__py3-none-any.whl → 1.0.30__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.
- trovesuite/__init__.py +1 -1
- trovesuite/configs/database.py +165 -17
- trovesuite/configs/settings.py +0 -5
- {trovesuite-1.0.28.dist-info → trovesuite-1.0.30.dist-info}/METADATA +1 -1
- {trovesuite-1.0.28.dist-info → trovesuite-1.0.30.dist-info}/RECORD +8 -8
- {trovesuite-1.0.28.dist-info → trovesuite-1.0.30.dist-info}/WHEEL +0 -0
- {trovesuite-1.0.28.dist-info → trovesuite-1.0.30.dist-info}/licenses/LICENSE +0 -0
- {trovesuite-1.0.28.dist-info → trovesuite-1.0.30.dist-info}/top_level.txt +0 -0
trovesuite/__init__.py
CHANGED
trovesuite/configs/database.py
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
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":
|
|
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=
|
|
89
|
+
maxconn=pool_size,
|
|
63
90
|
**self.get_connection_params()
|
|
64
91
|
)
|
|
65
|
-
logger.info(
|
|
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
|
-
|
|
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
|
|
116
|
-
"1.
|
|
117
|
-
"2.
|
|
118
|
-
"3. Database
|
|
119
|
-
"4.
|
|
120
|
-
"
|
|
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
|
|
trovesuite/configs/settings.py
CHANGED
|
@@ -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.
|
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
trovesuite/__init__.py,sha256=
|
|
1
|
+
trovesuite/__init__.py,sha256=tcNxxwRpfYZ23yCEn4b5EHqGL5zRe_BqQHLcNAP23PQ,646
|
|
2
2
|
trovesuite/auth/__init__.py,sha256=OjZllVvjul1glDazJ-d5TrNjgHFigFlQQi1G99DYshk,239
|
|
3
3
|
trovesuite/auth/auth_base.py,sha256=rZHQVLeJRBQ8GClgF5UwG-er4_HXVX5-nt8o6_Z29uY,75
|
|
4
4
|
trovesuite/auth/auth_controller.py,sha256=PAgaVlf5TYEfkSfK4vGGsvO84i8zEmeVVXyUF2YBppI,420
|
|
@@ -6,9 +6,9 @@ trovesuite/auth/auth_read_dto.py,sha256=e27JqKVPVUM83A_mYF452QCflsvGNo7aKje7q_ur
|
|
|
6
6
|
trovesuite/auth/auth_service.py,sha256=TQOJFG0AzhPGwZBAXVxMkHxyG2wyct4Zcoq4z0cVBO4,22201
|
|
7
7
|
trovesuite/auth/auth_write_dto.py,sha256=rdwI7w6-9QZGv1H0PAGrjkLBCzaMHjgPIXeLb9RmNec,234
|
|
8
8
|
trovesuite/configs/__init__.py,sha256=h1mSZOaZ3kUy1ZMO_m9O9KklsxywM0RfMVZLh9h9WvQ,328
|
|
9
|
-
trovesuite/configs/database.py,sha256=
|
|
9
|
+
trovesuite/configs/database.py,sha256=x6ucbF7lNLSNqeCqFgJ4NUbkkAvYifdcQbOd9kxZUPQ,18414
|
|
10
10
|
trovesuite/configs/logging.py,sha256=mGjR2d4urVNry9l5_aXycMMtcY2RAFIpEL35hw33KZg,9308
|
|
11
|
-
trovesuite/configs/settings.py,sha256=
|
|
11
|
+
trovesuite/configs/settings.py,sha256=s-RvzFI7IR8-Q_A0ZRxlTr-iDoY6MvbxM0RkbhmJOL4,6915
|
|
12
12
|
trovesuite/entities/__init__.py,sha256=Dbl_03Bueyh2vOP2hykd40MmNMrl5nNHSRGP-kqwwNo,160
|
|
13
13
|
trovesuite/entities/health.py,sha256=KaW7yxTQdymIPlnkJJkDqEebBXkD0a7A66i5GgNZLoE,2700
|
|
14
14
|
trovesuite/entities/sh_response.py,sha256=1_sw3PpVaDxWsNiBU0W9YLHZgTFxEj4JJBLBfSY63Ho,1579
|
|
@@ -27,8 +27,8 @@ trovesuite/storage/storage_write_dto.py,sha256=vl1iCZ93bpFmpvkCrn587QtMtOA_TPDse
|
|
|
27
27
|
trovesuite/utils/__init__.py,sha256=mDZuY77BphvQFYLmcWxjP5Tcq9ZZ3WXJWBKB1v6wzHU,185
|
|
28
28
|
trovesuite/utils/helper.py,sha256=qpd-EWPaX3-QJA5xvxb4s9rEb9W2RKPCDXcdAKSUwSM,30858
|
|
29
29
|
trovesuite/utils/templates.py,sha256=_92k4-EkqWs-h0LNJxPgorbspmp24kDngS7O3qWIFyQ,20388
|
|
30
|
-
trovesuite-1.0.
|
|
31
|
-
trovesuite-1.0.
|
|
32
|
-
trovesuite-1.0.
|
|
33
|
-
trovesuite-1.0.
|
|
34
|
-
trovesuite-1.0.
|
|
30
|
+
trovesuite-1.0.30.dist-info/licenses/LICENSE,sha256=EJT35ct-Q794JYPdAQy3XNczQGKkU1HzToLeK1YVw2s,1070
|
|
31
|
+
trovesuite-1.0.30.dist-info/METADATA,sha256=jlg46ILbbTBMpJ7SMUVcH9lBjA_HmnFm6oejaTshjX4,21737
|
|
32
|
+
trovesuite-1.0.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
trovesuite-1.0.30.dist-info/top_level.txt,sha256=GzKhG_-MTaxeHrIgkGkBH_nof2vroGFBrjeHKWUIwNc,11
|
|
34
|
+
trovesuite-1.0.30.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|