devdox-ai-locust 0.1.3.post1__py3-none-any.whl → 0.1.4__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.
Potentially problematic release.
This version of devdox-ai-locust might be problematic. Click here for more details.
- devdox_ai_locust/cli.py +19 -3
- devdox_ai_locust/config.py +1 -1
- devdox_ai_locust/hybrid_loctus_generator.py +37 -8
- devdox_ai_locust/locust_generator.py +84 -9
- devdox_ai_locust/prompt/test_data.j2 +152 -28
- devdox_ai_locust/prompt/workflow.j2 +349 -11
- devdox_ai_locust/templates/mongo/data_provider.py.j2 +303 -0
- devdox_ai_locust/templates/mongo/db_config.py.j2 +271 -0
- devdox_ai_locust/templates/mongo/db_integration.j2 +352 -0
- devdox_ai_locust/templates/readme.md.j2 +3 -1
- devdox_ai_locust/templates/requirement.txt.j2 +5 -2
- devdox_ai_locust/templates/test_data.py.j2 +5 -1
- {devdox_ai_locust-0.1.3.post1.dist-info → devdox_ai_locust-0.1.4.dist-info}/METADATA +26 -11
- {devdox_ai_locust-0.1.3.post1.dist-info → devdox_ai_locust-0.1.4.dist-info}/RECORD +18 -15
- {devdox_ai_locust-0.1.3.post1.dist-info → devdox_ai_locust-0.1.4.dist-info}/WHEEL +0 -0
- {devdox_ai_locust-0.1.3.post1.dist-info → devdox_ai_locust-0.1.4.dist-info}/entry_points.txt +0 -0
- {devdox_ai_locust-0.1.3.post1.dist-info → devdox_ai_locust-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {devdox_ai_locust-0.1.3.post1.dist-info → devdox_ai_locust-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB Data Provider for Locust Load Testing
|
|
3
|
+
|
|
4
|
+
Provides efficient data retrieval from MongoDB with:
|
|
5
|
+
- Random sampling for realistic load distribution
|
|
6
|
+
- Caching to reduce database queries
|
|
7
|
+
- Fallback to generated data if MongoDB unavailable
|
|
8
|
+
- Smart data rotation to prevent hotspots
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import random
|
|
13
|
+
from typing import Dict, List, Any, Optional
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from collections import deque
|
|
16
|
+
import threading
|
|
17
|
+
import uuid
|
|
18
|
+
from db_config import mongo_config
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MongoDataProvider:
|
|
24
|
+
"""
|
|
25
|
+
Provides test data from MongoDB with intelligent caching and fallback.
|
|
26
|
+
|
|
27
|
+
This class is designed for high-concurrency load testing:
|
|
28
|
+
- Thread-safe caching mechanism
|
|
29
|
+
- Rotating cache to prevent data hotspots
|
|
30
|
+
- Automatic fallback to generated data
|
|
31
|
+
- Periodic cache refresh
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, cache_size: int = 1000, cache_ttl_seconds: int = 300):
|
|
35
|
+
"""
|
|
36
|
+
Initialize MongoDB data provider
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
cache_size: Maximum items to cache per collection
|
|
40
|
+
cache_ttl_seconds: Cache time-to-live in seconds
|
|
41
|
+
"""
|
|
42
|
+
self.cache_size = cache_size
|
|
43
|
+
self.cache_ttl_seconds = cache_ttl_seconds
|
|
44
|
+
|
|
45
|
+
# Thread-safe caches with rotation
|
|
46
|
+
self._cache = {}
|
|
47
|
+
self._cache_timestamps = {}
|
|
48
|
+
self._cache_lock = threading.Lock()
|
|
49
|
+
|
|
50
|
+
# Usage tracking for intelligent preloading
|
|
51
|
+
self._access_counts = {}
|
|
52
|
+
|
|
53
|
+
# Statistics
|
|
54
|
+
self.stats = {
|
|
55
|
+
"cache_hits": 0,
|
|
56
|
+
"cache_misses": 0,
|
|
57
|
+
"db_queries": 0,
|
|
58
|
+
"fallback_generations": 0,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def _get_cache_key(self, collection: str, query: Dict) -> str:
|
|
62
|
+
"""Generate cache key from collection and query"""
|
|
63
|
+
query_str = str(sorted(query.items()))
|
|
64
|
+
return f"{collection}:{query_str}"
|
|
65
|
+
|
|
66
|
+
def _is_cache_valid(self, cache_key: str) -> bool:
|
|
67
|
+
"""Check if cache is still valid"""
|
|
68
|
+
if cache_key not in self._cache_timestamps:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
age = (datetime.now() - self._cache_timestamps[cache_key]).total_seconds()
|
|
72
|
+
return age < self.cache_ttl_seconds
|
|
73
|
+
|
|
74
|
+
def _update_cache(self, cache_key: str, data: List[Dict]):
|
|
75
|
+
"""Update cache with new data"""
|
|
76
|
+
with self._cache_lock:
|
|
77
|
+
# Limit cache size using deque for efficient rotation
|
|
78
|
+
if len(data) > self.cache_size:
|
|
79
|
+
self._cache[cache_key] = deque(
|
|
80
|
+
random.sample(data, self.cache_size),
|
|
81
|
+
maxlen=self.cache_size,
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
self._cache[cache_key] = deque(data, maxlen=self.cache_size)
|
|
85
|
+
|
|
86
|
+
self._cache_timestamps[cache_key] = datetime.now()
|
|
87
|
+
|
|
88
|
+
def get_random_document(
|
|
89
|
+
self,
|
|
90
|
+
collection_name: str,
|
|
91
|
+
query: Dict = None,
|
|
92
|
+
projection: Dict = None,
|
|
93
|
+
) -> Optional[Dict]:
|
|
94
|
+
"""
|
|
95
|
+
Get a random document from MongoDB collection with caching.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
collection_name: Name of the collection (e.g., 'pets', 'users')
|
|
99
|
+
query: MongoDB query filter
|
|
100
|
+
projection: Fields to include/exclude
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Random document from collection, or None if not found
|
|
104
|
+
"""
|
|
105
|
+
if not mongo_config.enable_mongodb:
|
|
106
|
+
logger.debug("MongoDB disabled, using generated data")
|
|
107
|
+
return self._generate_fallback_data(collection_name)
|
|
108
|
+
|
|
109
|
+
query = query or {}
|
|
110
|
+
cache_key = self._get_cache_key(collection_name, query)
|
|
111
|
+
|
|
112
|
+
# Try cache first
|
|
113
|
+
if cache_key in self._cache and self._is_cache_valid(cache_key):
|
|
114
|
+
self.stats["cache_hits"] += 1
|
|
115
|
+
with self._cache_lock:
|
|
116
|
+
cached_data = list(self._cache[cache_key])
|
|
117
|
+
if cached_data:
|
|
118
|
+
# Rotate the deque for better distribution
|
|
119
|
+
item = cached_data[0]
|
|
120
|
+
self._cache[cache_key].rotate(-1)
|
|
121
|
+
return item
|
|
122
|
+
|
|
123
|
+
# Cache miss - query database
|
|
124
|
+
self.stats["cache_misses"] += 1
|
|
125
|
+
try:
|
|
126
|
+
collection = mongo_config.get_collection(collection_name)
|
|
127
|
+
if not collection:
|
|
128
|
+
logger.warning(f"Collection {collection_name} not available")
|
|
129
|
+
return self._generate_fallback_data(collection_name)
|
|
130
|
+
|
|
131
|
+
self.stats["db_queries"] += 1
|
|
132
|
+
|
|
133
|
+
# Fetch multiple documents for caching
|
|
134
|
+
documents = list(collection.find(query, projection).limit(self.cache_size))
|
|
135
|
+
|
|
136
|
+
if documents:
|
|
137
|
+
# Update cache
|
|
138
|
+
self._update_cache(cache_key, documents)
|
|
139
|
+
|
|
140
|
+
# Return random document
|
|
141
|
+
return random.choice(documents)
|
|
142
|
+
else:
|
|
143
|
+
logger.warning(f"No documents found in {collection_name} with query {query}")
|
|
144
|
+
return self._generate_fallback_data(collection_name)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Error fetching from MongoDB {collection_name}: {e}")
|
|
148
|
+
self.stats["fallback_generations"] += 1
|
|
149
|
+
return self._generate_fallback_data(collection_name)
|
|
150
|
+
|
|
151
|
+
def get_document(
|
|
152
|
+
self,
|
|
153
|
+
collection_name: str,
|
|
154
|
+
query: Dict = None,
|
|
155
|
+
projection: Dict = None,
|
|
156
|
+
sort: List[tuple] = None,
|
|
157
|
+
) -> Optional[Dict]:
|
|
158
|
+
"""
|
|
159
|
+
Get a single specific document from MongoDB (not random).
|
|
160
|
+
Returns the first matching document based on query and sort order.
|
|
161
|
+
"""
|
|
162
|
+
if not mongo_config.enable_mongodb:
|
|
163
|
+
logger.debug("MongoDB disabled, using generated data")
|
|
164
|
+
return self._generate_fallback_data(collection_name)
|
|
165
|
+
|
|
166
|
+
query = query or {}
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
collection = mongo_config.get_collection(collection_name)
|
|
170
|
+
if not collection:
|
|
171
|
+
logger.warning(f"Collection {collection_name} not available")
|
|
172
|
+
return self._generate_fallback_data(collection_name)
|
|
173
|
+
|
|
174
|
+
self.stats["db_queries"] += 1
|
|
175
|
+
|
|
176
|
+
# Build the query
|
|
177
|
+
cursor = collection.find(query, projection)
|
|
178
|
+
|
|
179
|
+
# Apply sort if specified
|
|
180
|
+
if sort:
|
|
181
|
+
cursor = cursor.sort(sort)
|
|
182
|
+
|
|
183
|
+
# Get first document
|
|
184
|
+
document = cursor.limit(1).next() if cursor else None
|
|
185
|
+
|
|
186
|
+
if document:
|
|
187
|
+
return document
|
|
188
|
+
else:
|
|
189
|
+
logger.warning(f"No document found in {collection_name} with query {query}")
|
|
190
|
+
return self._generate_fallback_data(collection_name)
|
|
191
|
+
|
|
192
|
+
except StopIteration:
|
|
193
|
+
logger.warning(f"No document found in {collection_name} with query {query}")
|
|
194
|
+
return self._generate_fallback_data(collection_name)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f"Error fetching from MongoDB {collection_name}: {e}")
|
|
197
|
+
self.stats["fallback_generations"] += 1
|
|
198
|
+
return self._generate_fallback_data(collection_name)
|
|
199
|
+
|
|
200
|
+
def get_multiple_documents(
|
|
201
|
+
self,
|
|
202
|
+
collection_name: str,
|
|
203
|
+
count: int = 10,
|
|
204
|
+
query: Dict = None,
|
|
205
|
+
projection: Dict = None,
|
|
206
|
+
random: bool = True,
|
|
207
|
+
) -> List[Dict]:
|
|
208
|
+
"""
|
|
209
|
+
Get multiple random documents from collection.
|
|
210
|
+
"""
|
|
211
|
+
collection = mongo_config.get_collection(collection_name)
|
|
212
|
+
if random:
|
|
213
|
+
# ✅ SINGLE EFFICIENT QUERY using MongoDB's $sample
|
|
214
|
+
pipeline = []
|
|
215
|
+
if query:
|
|
216
|
+
pipeline.append({"$match": query})
|
|
217
|
+
pipeline.append({"$sample": {"size": count}})
|
|
218
|
+
if projection:
|
|
219
|
+
pipeline.append({"$project": projection})
|
|
220
|
+
|
|
221
|
+
documents = list(collection.aggregate(pipeline))
|
|
222
|
+
return documents
|
|
223
|
+
else:
|
|
224
|
+
try:
|
|
225
|
+
collection = mongo_config.get_collection(collection_name)
|
|
226
|
+
if not collection:
|
|
227
|
+
logger.warning(f"Collection {collection_name} not available")
|
|
228
|
+
return [self._generate_fallback_data(collection_name) for _ in range(count)]
|
|
229
|
+
|
|
230
|
+
query = query or {}
|
|
231
|
+
self.stats["db_queries"] += 1
|
|
232
|
+
|
|
233
|
+
documents = list(collection.find(query, projection).limit(count))
|
|
234
|
+
|
|
235
|
+
if documents:
|
|
236
|
+
return documents
|
|
237
|
+
else:
|
|
238
|
+
logger.warning(f"No documents found in {collection_name} with query {query}")
|
|
239
|
+
return [self._generate_fallback_data(collection_name) for _ in range(count)]
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Error fetching from MongoDB {collection_name}: {e}")
|
|
243
|
+
return [self._generate_fallback_data(collection_name) for _ in range(count)]
|
|
244
|
+
|
|
245
|
+
def _generate_fallback_data(self, collection_name: str) -> Dict:
|
|
246
|
+
"""
|
|
247
|
+
Generate fallback data when MongoDB is unavailable.
|
|
248
|
+
"""
|
|
249
|
+
self.stats["fallback_generations"] += 1
|
|
250
|
+
|
|
251
|
+
# Generic fallback
|
|
252
|
+
return {
|
|
253
|
+
"_id": str(uuid.uuid4()),
|
|
254
|
+
"data": f"fallback_{collection_name}",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
def clear_cache(self):
|
|
258
|
+
"""Clear all cached data"""
|
|
259
|
+
with self._cache_lock:
|
|
260
|
+
self._cache.clear()
|
|
261
|
+
self._cache_timestamps.clear()
|
|
262
|
+
logger.info("Cache cleared")
|
|
263
|
+
|
|
264
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
265
|
+
"""Get usage statistics"""
|
|
266
|
+
total_requests = self.stats["cache_hits"] + self.stats["cache_misses"]
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
**self.stats,
|
|
270
|
+
"cache_hit_rate": (
|
|
271
|
+
self.stats["cache_hits"] / total_requests * 100 if total_requests > 0 else 0
|
|
272
|
+
),
|
|
273
|
+
"cache_size": sum(len(cache) for cache in self._cache.values()),
|
|
274
|
+
"cached_collections": len(self._cache),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
def preload_cache(self, collection_name: str, query: Dict = None):
|
|
278
|
+
"""
|
|
279
|
+
Preload cache for a collection to improve initial performance.
|
|
280
|
+
"""
|
|
281
|
+
logger.info(f"Preloading cache for {collection_name}...")
|
|
282
|
+
try:
|
|
283
|
+
collection = mongo_config.get_collection(collection_name)
|
|
284
|
+
if not collection:
|
|
285
|
+
logger.warning(f"Collection {collection_name} not available for preload")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
query = query or {}
|
|
289
|
+
documents = list(collection.find(query).limit(self.cache_size))
|
|
290
|
+
|
|
291
|
+
if documents:
|
|
292
|
+
cache_key = self._get_cache_key(collection_name, query)
|
|
293
|
+
self._update_cache(cache_key, documents)
|
|
294
|
+
logger.info(f"✅ Preloaded {len(documents)} documents for {collection_name}")
|
|
295
|
+
else:
|
|
296
|
+
logger.warning(f"No documents found to preload for {collection_name}")
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.error(f"Failed to preload cache for {collection_name}: {e}")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# Global instance
|
|
303
|
+
mongo_data_provider = MongoDataProvider()
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB Configuration for Load Testing
|
|
3
|
+
|
|
4
|
+
This module provides MongoDB connection management with:
|
|
5
|
+
- Connection pooling optimized for concurrent load testing
|
|
6
|
+
- Singleton pattern to prevent connection exhaustion
|
|
7
|
+
- Proper error handling and retry logic
|
|
8
|
+
- Environment-based configuration
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Optional, Dict, Any, List
|
|
14
|
+
from pymongo import MongoClient
|
|
15
|
+
from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
|
|
16
|
+
from dotenv import load_dotenv
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
load_dotenv()
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MongoDBConfig:
|
|
24
|
+
"""MongoDB configuration with connection pooling for load testing"""
|
|
25
|
+
|
|
26
|
+
_instance = None
|
|
27
|
+
_client = None
|
|
28
|
+
_database = None
|
|
29
|
+
|
|
30
|
+
def __new__(cls):
|
|
31
|
+
"""Singleton pattern to ensure single connection pool"""
|
|
32
|
+
if cls._instance is None:
|
|
33
|
+
cls._instance = super(MongoDBConfig, cls).__new__(cls)
|
|
34
|
+
cls._instance._initialize()
|
|
35
|
+
return cls._instance
|
|
36
|
+
|
|
37
|
+
def _initialize(self):
|
|
38
|
+
"""Initialize MongoDB connection with proper pooling"""
|
|
39
|
+
# MongoDB Configuration
|
|
40
|
+
self.mongo_uri = os.getenv("MONGODB_URI", "mongodb://localhost:27017/")
|
|
41
|
+
self.database_name = os.getenv("MONGODB_DATABASE", "locust_test_data")
|
|
42
|
+
|
|
43
|
+
# Connection Pool Settings - CRITICAL FOR LOAD TESTING
|
|
44
|
+
# These values are optimized for concurrent Locust users
|
|
45
|
+
self.max_pool_size = int(os.getenv("MONGODB_MAX_POOL_SIZE", "100"))
|
|
46
|
+
self.min_pool_size = int(os.getenv("MONGODB_MIN_POOL_SIZE", "10"))
|
|
47
|
+
|
|
48
|
+
# Timeout Settings
|
|
49
|
+
self.connect_timeout_ms = int(os.getenv("MONGODB_CONNECT_TIMEOUT_MS", "5000"))
|
|
50
|
+
self.server_selection_timeout_ms = int(
|
|
51
|
+
os.getenv("MONGODB_SERVER_SELECTION_TIMEOUT_MS", "5000")
|
|
52
|
+
)
|
|
53
|
+
self.socket_timeout_ms = int(os.getenv("MONGODB_SOCKET_TIMEOUT_MS", "10000"))
|
|
54
|
+
|
|
55
|
+
# Wait Queue Settings - Prevents connection exhaustion
|
|
56
|
+
self.max_idle_time_ms = int(os.getenv("MONGODB_MAX_IDLE_TIME_MS", "60000"))
|
|
57
|
+
self.wait_queue_timeout_ms = int(
|
|
58
|
+
os.getenv("MONGODB_WAIT_QUEUE_TIMEOUT_MS", "10000")
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Feature Flags
|
|
62
|
+
self.enable_mongodb = os.getenv("ENABLE_MONGODB", "false").lower() == "true"
|
|
63
|
+
self.use_mongodb_for_test_data = (
|
|
64
|
+
os.getenv("USE_MONGODB_FOR_TEST_DATA", "false").lower() == "true"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Collections
|
|
68
|
+
self.collections = {
|
|
69
|
+
"pets": os.getenv("MONGODB_COLLECTION_PETS", "pets"),
|
|
70
|
+
"users": os.getenv("MONGODB_COLLECTION_USERS", "users"),
|
|
71
|
+
"orders": os.getenv("MONGODB_COLLECTION_ORDERS", "orders"),
|
|
72
|
+
"test_sessions": os.getenv("MONGODB_COLLECTION_SESSIONS", "test_sessions"),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def get_client(self) -> Optional[MongoClient]:
|
|
76
|
+
"""
|
|
77
|
+
Get MongoDB client with connection pooling.
|
|
78
|
+
|
|
79
|
+
CRITICAL: Returns same client instance for all Locust workers
|
|
80
|
+
to share connection pool efficiently.
|
|
81
|
+
"""
|
|
82
|
+
if not self.enable_mongodb:
|
|
83
|
+
logger.warning("MongoDB is disabled. Enable with ENABLE_MONGODB=true")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
if self._client is None:
|
|
87
|
+
try:
|
|
88
|
+
logger.info("Initializing MongoDB connection pool...")
|
|
89
|
+
logger.info(f"Pool size: {self.min_pool_size} to {self.max_pool_size}")
|
|
90
|
+
|
|
91
|
+
self._client = MongoClient(
|
|
92
|
+
self.mongo_uri,
|
|
93
|
+
# Connection Pool Configuration
|
|
94
|
+
maxPoolSize=self.max_pool_size,
|
|
95
|
+
minPoolSize=self.min_pool_size,
|
|
96
|
+
maxIdleTimeMS=self.max_idle_time_ms,
|
|
97
|
+
waitQueueTimeoutMS=self.wait_queue_timeout_ms,
|
|
98
|
+
|
|
99
|
+
# Timeout Configuration
|
|
100
|
+
connectTimeoutMS=self.connect_timeout_ms,
|
|
101
|
+
serverSelectionTimeoutMS=self.server_selection_timeout_ms,
|
|
102
|
+
socketTimeoutMS=self.socket_timeout_ms,
|
|
103
|
+
|
|
104
|
+
# Resilience Settings
|
|
105
|
+
retryWrites=True,
|
|
106
|
+
retryReads=True,
|
|
107
|
+
|
|
108
|
+
# Application Name for monitoring
|
|
109
|
+
appname="locust-load-test",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Verify connection
|
|
113
|
+
self._client.admin.command("ping")
|
|
114
|
+
logger.info(f"✅ MongoDB connected successfully to {self.database_name}")
|
|
115
|
+
|
|
116
|
+
except (ConnectionFailure, ServerSelectionTimeoutError) as e:
|
|
117
|
+
logger.error(f"❌ Failed to connect to MongoDB: {e}")
|
|
118
|
+
logger.error("Please verify MongoDB is running and connection string is correct")
|
|
119
|
+
self._client = None
|
|
120
|
+
raise
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(f"❌ Unexpected MongoDB connection error: {e}")
|
|
123
|
+
self._client = None
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
return self._client
|
|
127
|
+
|
|
128
|
+
def get_database(self):
|
|
129
|
+
"""Get database instance"""
|
|
130
|
+
if self._database is None:
|
|
131
|
+
client = self.get_client()
|
|
132
|
+
if client:
|
|
133
|
+
self._database = client[self.database_name]
|
|
134
|
+
return self._database
|
|
135
|
+
|
|
136
|
+
def get_collection(self, collection_name: str):
|
|
137
|
+
"""Get collection instance"""
|
|
138
|
+
db = self.get_database()
|
|
139
|
+
if db:
|
|
140
|
+
# Use the mapped collection name if it exists
|
|
141
|
+
actual_name = self.collections.get(collection_name, collection_name)
|
|
142
|
+
return db[actual_name]
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def close(self):
|
|
146
|
+
"""Close MongoDB connection"""
|
|
147
|
+
if self._client:
|
|
148
|
+
logger.info("Closing MongoDB connection...")
|
|
149
|
+
self._client.close()
|
|
150
|
+
self._client = None
|
|
151
|
+
self._database = None
|
|
152
|
+
logger.info("✅ MongoDB connection closed")
|
|
153
|
+
|
|
154
|
+
def validate_config(self) -> List[str]:
|
|
155
|
+
"""
|
|
156
|
+
Validate MongoDB configuration and return warnings/errors
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of configuration issues
|
|
160
|
+
"""
|
|
161
|
+
issues = []
|
|
162
|
+
|
|
163
|
+
# Pool size validation
|
|
164
|
+
if self.max_pool_size < self.min_pool_size:
|
|
165
|
+
issues.append(
|
|
166
|
+
f"⚠️ max_pool_size ({self.max_pool_size}) is less than "
|
|
167
|
+
f"min_pool_size ({self.min_pool_size})"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Check if pool size is adequate for Locust users
|
|
171
|
+
# Rule of thumb: max_pool_size should be >= number of Locust users
|
|
172
|
+
locust_users = int(os.getenv("LOCUST_USERS", "50"))
|
|
173
|
+
if self.max_pool_size < locust_users:
|
|
174
|
+
issues.append(
|
|
175
|
+
f"⚠️ CRITICAL: max_pool_size ({self.max_pool_size}) is less than "
|
|
176
|
+
f"LOCUST_USERS ({locust_users}). This WILL cause connection "
|
|
177
|
+
f"exhaustion and test failures. Recommended: {locust_users * 2}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Timeout validation
|
|
181
|
+
if self.connect_timeout_ms < 1000:
|
|
182
|
+
issues.append(
|
|
183
|
+
f"⚠️ connect_timeout_ms ({self.connect_timeout_ms}) is very low. "
|
|
184
|
+
"May cause connection failures under load."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if self.wait_queue_timeout_ms < 5000:
|
|
188
|
+
issues.append(
|
|
189
|
+
f"⚠️ wait_queue_timeout_ms ({self.wait_queue_timeout_ms}) is low. "
|
|
190
|
+
"Users may get connection timeouts under heavy load."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return issues
|
|
194
|
+
|
|
195
|
+
def get_connection_stats(self) -> Dict[str, Any]:
|
|
196
|
+
"""Get current connection pool statistics"""
|
|
197
|
+
client = self.get_client()
|
|
198
|
+
if not client:
|
|
199
|
+
return {"status": "disabled"}
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
# Get server status for connection pool info
|
|
203
|
+
server_status = client.admin.command("serverStatus")
|
|
204
|
+
connections = server_status.get("connections", {})
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"status": "connected",
|
|
208
|
+
"current_connections": connections.get("current", 0),
|
|
209
|
+
"available_connections": connections.get("available", 0),
|
|
210
|
+
"total_created": connections.get("totalCreated", 0),
|
|
211
|
+
"active": connections.get("active", 0),
|
|
212
|
+
"configured_max": self.max_pool_size,
|
|
213
|
+
"configured_min": self.min_pool_size,
|
|
214
|
+
}
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.error(f"Failed to get connection stats: {e}")
|
|
217
|
+
return {"status": "error", "error": str(e)}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Global singleton instance
|
|
221
|
+
mongo_config = MongoDBConfig()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_connection():
|
|
225
|
+
"""Test MongoDB connection and display configuration"""
|
|
226
|
+
print("\n" + "=" * 60)
|
|
227
|
+
print("MongoDB Connection Test")
|
|
228
|
+
print("=" * 60)
|
|
229
|
+
|
|
230
|
+
# Display configuration
|
|
231
|
+
print(f"\nConnection URI: {mongo_config.mongo_uri}")
|
|
232
|
+
print(f"Database: {mongo_config.database_name}")
|
|
233
|
+
print(f"Pool Size: {mongo_config.min_pool_size} - {mongo_config.max_pool_size}")
|
|
234
|
+
print(f"Connect Timeout: {mongo_config.connect_timeout_ms}ms")
|
|
235
|
+
print(f"Socket Timeout: {mongo_config.socket_timeout_ms}ms")
|
|
236
|
+
|
|
237
|
+
# Check for configuration issues
|
|
238
|
+
issues = mongo_config.validate_config()
|
|
239
|
+
if issues:
|
|
240
|
+
print("\n⚠️ Configuration Issues:")
|
|
241
|
+
for issue in issues:
|
|
242
|
+
print(f" {issue}")
|
|
243
|
+
else:
|
|
244
|
+
print("\n✅ Configuration looks good!")
|
|
245
|
+
|
|
246
|
+
# Try to connect
|
|
247
|
+
print("\nAttempting to connect...")
|
|
248
|
+
try:
|
|
249
|
+
client = mongo_config.get_client()
|
|
250
|
+
if client:
|
|
251
|
+
stats = mongo_config.get_connection_stats()
|
|
252
|
+
print("\n✅ Connection successful!")
|
|
253
|
+
print(f"\nConnection Stats:")
|
|
254
|
+
for key, value in stats.items():
|
|
255
|
+
print(f" {key}: {value}")
|
|
256
|
+
|
|
257
|
+
# List collections
|
|
258
|
+
db = mongo_config.get_database()
|
|
259
|
+
collections = db.list_collection_names()
|
|
260
|
+
print(f"\nAvailable collections: {', '.join(collections) if collections else 'None'}")
|
|
261
|
+
else:
|
|
262
|
+
print("\n❌ Connection failed - MongoDB is disabled")
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
print(f"\n❌ Connection failed: {e}")
|
|
266
|
+
|
|
267
|
+
print("\n" + "=" * 60)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if __name__ == "__main__":
|
|
271
|
+
test_connection()
|