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.

@@ -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()