Skip to main content

mockforge_bench/
parallel_requests.rs

1//! Parallel request execution support for load testing
2//!
3//! This module provides functionality to generate k6 scripts that execute
4//! multiple requests in parallel using http.batch(), enabling high-throughput
5//! testing scenarios like creating 300 resources simultaneously.
6
7use serde::{Deserialize, Serialize};
8
9/// Configuration for parallel request execution
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ParallelConfig {
12    /// Number of resources to create in parallel per VU
13    pub count: u32,
14    /// Whether to collect response IDs for subsequent operations
15    pub collect_ids: bool,
16    /// Maximum batch size (k6 limits)
17    pub max_batch_size: u32,
18    /// Delay between batches in milliseconds
19    pub batch_delay_ms: u32,
20}
21
22impl Default for ParallelConfig {
23    fn default() -> Self {
24        Self {
25            count: 10,
26            collect_ids: true,
27            max_batch_size: 100, // k6 recommends keeping batches reasonable
28            batch_delay_ms: 100,
29        }
30    }
31}
32
33impl ParallelConfig {
34    /// Create a new parallel config with the specified count
35    pub fn new(count: u32) -> Self {
36        Self {
37            count,
38            ..Default::default()
39        }
40    }
41
42    /// Set whether to collect IDs from responses
43    pub fn with_collect_ids(mut self, collect: bool) -> Self {
44        self.collect_ids = collect;
45        self
46    }
47
48    /// Set the maximum batch size
49    pub fn with_max_batch_size(mut self, size: u32) -> Self {
50        self.max_batch_size = size;
51        self
52    }
53
54    /// Calculate the number of batches needed
55    pub fn num_batches(&self) -> u32 {
56        if self.max_batch_size == 0 {
57            return 1;
58        }
59        (self.count + self.max_batch_size - 1) / self.max_batch_size
60    }
61
62    /// Get the size of a specific batch (last batch may be smaller)
63    pub fn batch_size(&self, batch_index: u32) -> u32 {
64        let remaining = self.count - (batch_index * self.max_batch_size);
65        remaining.min(self.max_batch_size)
66    }
67}
68
69/// Generates k6 JavaScript code for parallel request execution
70pub struct ParallelRequestGenerator;
71
72impl ParallelRequestGenerator {
73    /// Generate k6 code for parallel POST requests
74    ///
75    /// Generates code that uses http.batch() to execute multiple requests in parallel,
76    /// optionally collecting the response IDs for subsequent operations.
77    pub fn generate_parallel_post(
78        config: &ParallelConfig,
79        path: &str,
80        body_template: &str,
81        id_field: &str,
82    ) -> String {
83        let mut code = String::new();
84
85        // Generate batch creation loop
86        code.push_str("// Parallel resource creation\n");
87        code.push_str("const batchRequests = [];\n");
88        code.push_str(&format!("for (let i = 0; i < {}; i++) {{\n", config.count));
89        code.push_str("  batchRequests.push({\n");
90        code.push_str("    method: 'POST',\n");
91        code.push_str(&format!("    url: `${{BASE_URL}}{}`", path));
92        code.push_str(",\n");
93        code.push_str(&format!("    body: JSON.stringify({}),\n", body_template));
94        code.push_str("    params: { headers }\n");
95        code.push_str("  });\n");
96        code.push_str("}\n\n");
97
98        // Execute batch with size limits
99        if config.count > config.max_batch_size {
100            code.push_str("// Execute in batches to avoid overwhelming the server\n");
101            code.push_str("const createdIds = [];\n");
102            code.push_str(&format!("const batchSize = {};\n", config.max_batch_size));
103            code.push_str("for (let batchStart = 0; batchStart < batchRequests.length; batchStart += batchSize) {\n");
104            code.push_str(
105                "  const batchEnd = Math.min(batchStart + batchSize, batchRequests.length);\n",
106            );
107            code.push_str("  const batch = batchRequests.slice(batchStart, batchEnd);\n");
108            code.push_str("  const responses = http.batch(batch);\n\n");
109
110            if config.collect_ids {
111                code.push_str("  // Collect IDs from responses\n");
112                code.push_str("  for (const res of responses) {\n");
113                code.push_str("    if (res.status >= 200 && res.status < 300) {\n");
114                code.push_str("      try {\n");
115                code.push_str(&format!("        const id = res.json('{}');\n", id_field));
116                code.push_str("        if (id) createdIds.push(id);\n");
117                code.push_str("      } catch (e) {\n");
118                code.push_str("        console.error('Failed to extract ID:', e);\n");
119                code.push_str("      }\n");
120                code.push_str("    }\n");
121                code.push_str("  }\n\n");
122            }
123
124            code.push_str("  // Check batch results\n");
125            code.push_str(
126                "  const batchSuccess = responses.every(r => r.status >= 200 && r.status < 300);\n",
127            );
128            code.push_str("  check(responses, {\n");
129            code.push_str("    'batch creation successful': () => batchSuccess\n");
130            code.push_str("  });\n\n");
131
132            if config.batch_delay_ms > 0 {
133                code.push_str(&format!("  sleep({});\n", config.batch_delay_ms as f64 / 1000.0));
134            }
135            code.push_str("}\n");
136        } else {
137            code.push_str("// Execute all requests in parallel\n");
138            code.push_str("const responses = http.batch(batchRequests);\n\n");
139
140            if config.collect_ids {
141                code.push_str("// Collect IDs from responses\n");
142                code.push_str("const createdIds = [];\n");
143                code.push_str("for (const res of responses) {\n");
144                code.push_str("  if (res.status >= 200 && res.status < 300) {\n");
145                code.push_str("    try {\n");
146                code.push_str(&format!("      const id = res.json('{}');\n", id_field));
147                code.push_str("      if (id) createdIds.push(id);\n");
148                code.push_str("    } catch (e) {\n");
149                code.push_str("      console.error('Failed to extract ID:', e);\n");
150                code.push_str("    }\n");
151                code.push_str("  }\n");
152                code.push_str("}\n\n");
153            }
154
155            code.push_str("// Check all responses\n");
156            code.push_str(
157                "const allSuccess = responses.every(r => r.status >= 200 && r.status < 300);\n",
158            );
159            code.push_str("check(responses, {\n");
160            code.push_str("  'parallel creation successful': () => allSuccess\n");
161            code.push_str("});\n");
162        }
163
164        code
165    }
166
167    /// Generate k6 code for parallel GET requests using collected IDs
168    pub fn generate_parallel_get(path_template: &str, id_param: &str) -> String {
169        let mut code = String::new();
170
171        code.push_str("// Parallel resource retrieval\n");
172        code.push_str("if (createdIds.length > 0) {\n");
173        code.push_str("  const getRequests = createdIds.map(id => ({\n");
174        code.push_str("    method: 'GET',\n");
175        code.push_str(&format!(
176            "    url: `${{BASE_URL}}{}`.replace('{{{{{}}}}}', id),\n",
177            path_template, id_param
178        ));
179        code.push_str("    params: { headers }\n");
180        code.push_str("  }));\n\n");
181        code.push_str("  const getResponses = http.batch(getRequests);\n");
182        code.push_str(
183            "  const getSuccess = getResponses.every(r => r.status >= 200 && r.status < 300);\n",
184        );
185        code.push_str("  check(getResponses, {\n");
186        code.push_str("    'parallel retrieval successful': () => getSuccess\n");
187        code.push_str("  });\n");
188        code.push_str("}\n");
189
190        code
191    }
192
193    /// Generate k6 code for parallel DELETE requests using collected IDs
194    pub fn generate_parallel_delete(path_template: &str, id_param: &str) -> String {
195        let mut code = String::new();
196
197        code.push_str("// Parallel resource cleanup\n");
198        code.push_str("if (createdIds.length > 0) {\n");
199        code.push_str("  const deleteRequests = createdIds.map(id => ({\n");
200        code.push_str("    method: 'DELETE',\n");
201        code.push_str(&format!(
202            "    url: `${{BASE_URL}}{}`.replace('{{{{{}}}}}', id),\n",
203            path_template, id_param
204        ));
205        code.push_str("    params: { headers }\n");
206        code.push_str("  }));\n\n");
207        code.push_str("  const deleteResponses = http.batch(deleteRequests);\n");
208        code.push_str("  const deleteSuccess = deleteResponses.every(r => r.status >= 200 && r.status < 300);\n");
209        code.push_str("  check(deleteResponses, {\n");
210        code.push_str("    'parallel cleanup successful': () => deleteSuccess\n");
211        code.push_str("  });\n");
212        code.push_str("}\n");
213
214        code
215    }
216
217    /// Generate k6 helper functions for batch operations
218    pub fn generate_batch_helper(config: &ParallelConfig) -> String {
219        let mut code = String::new();
220
221        code.push_str("// Parallel batch execution helpers\n");
222        code.push_str(&format!("const PARALLEL_BATCH_SIZE = {};\n", config.max_batch_size));
223        code.push_str(&format!("const PARALLEL_COUNT = {};\n\n", config.count));
224
225        code.push_str(
226            r#"function executeBatch(requests) {
227  const results = [];
228  const batchSize = PARALLEL_BATCH_SIZE;
229
230  for (let i = 0; i < requests.length; i += batchSize) {
231    const batch = requests.slice(i, i + batchSize);
232    const responses = http.batch(batch);
233    results.push(...responses);
234  }
235
236  return results;
237}
238
239function collectIds(responses, idField = 'id') {
240  const ids = [];
241  for (const res of responses) {
242    if (res.status >= 200 && res.status < 300) {
243      try {
244        const id = res.json(idField);
245        if (id) ids.push(id);
246      } catch (e) {
247        // Ignore parse errors
248      }
249    }
250  }
251  return ids;
252}
253"#,
254        );
255
256        code
257    }
258
259    /// Generate complete parallel test scenario
260    pub fn generate_complete_scenario(
261        config: &ParallelConfig,
262        base_path: &str,
263        detail_path: &str,
264        id_param: &str,
265        body_template: &str,
266        id_field: &str,
267        include_cleanup: bool,
268    ) -> String {
269        let mut code = String::new();
270
271        // Create resources
272        code.push_str(&Self::generate_parallel_post(config, base_path, body_template, id_field));
273        code.push_str("\n");
274
275        // Read all created resources
276        code.push_str(&Self::generate_parallel_get(detail_path, id_param));
277        code.push_str("\n");
278
279        // Cleanup if requested
280        if include_cleanup {
281            code.push_str(&Self::generate_parallel_delete(detail_path, id_param));
282        }
283
284        code
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_parallel_config_default() {
294        let config = ParallelConfig::default();
295        assert_eq!(config.count, 10);
296        assert!(config.collect_ids);
297        assert_eq!(config.max_batch_size, 100);
298    }
299
300    #[test]
301    fn test_parallel_config_new() {
302        let config = ParallelConfig::new(50);
303        assert_eq!(config.count, 50);
304        assert!(config.collect_ids);
305    }
306
307    #[test]
308    fn test_parallel_config_builders() {
309        let config = ParallelConfig::new(100).with_collect_ids(false).with_max_batch_size(25);
310
311        assert_eq!(config.count, 100);
312        assert!(!config.collect_ids);
313        assert_eq!(config.max_batch_size, 25);
314    }
315
316    #[test]
317    fn test_num_batches() {
318        let config = ParallelConfig::new(100).with_max_batch_size(30);
319        assert_eq!(config.num_batches(), 4); // 30 + 30 + 30 + 10
320
321        let config2 = ParallelConfig::new(50).with_max_batch_size(100);
322        assert_eq!(config2.num_batches(), 1);
323
324        let config3 = ParallelConfig::new(100).with_max_batch_size(100);
325        assert_eq!(config3.num_batches(), 1);
326    }
327
328    #[test]
329    fn test_batch_size() {
330        let config = ParallelConfig::new(100).with_max_batch_size(30);
331
332        assert_eq!(config.batch_size(0), 30);
333        assert_eq!(config.batch_size(1), 30);
334        assert_eq!(config.batch_size(2), 30);
335        assert_eq!(config.batch_size(3), 10); // Last batch is smaller
336    }
337
338    #[test]
339    fn test_generate_parallel_post_small_batch() {
340        let config = ParallelConfig::new(5);
341        let code = ParallelRequestGenerator::generate_parallel_post(
342            &config,
343            "/resources",
344            "{ name: `resource-${__VU}-${i}` }",
345            "id",
346        );
347
348        assert!(code.contains("batchRequests.push"));
349        assert!(code.contains("for (let i = 0; i < 5; i++)"));
350        assert!(code.contains("http.batch(batchRequests)"));
351        assert!(code.contains("res.json('id')"));
352    }
353
354    #[test]
355    fn test_generate_parallel_post_large_batch() {
356        let config = ParallelConfig::new(150).with_max_batch_size(50);
357        let code = ParallelRequestGenerator::generate_parallel_post(
358            &config,
359            "/resources",
360            "{ name: `resource-${i}` }",
361            "uuid",
362        );
363
364        assert!(code.contains("Execute in batches"));
365        assert!(code.contains("batchSize = 50"));
366        assert!(code.contains("batchStart + batchSize"));
367        assert!(code.contains("res.json('uuid')"));
368    }
369
370    #[test]
371    fn test_generate_parallel_post_no_collect() {
372        let config = ParallelConfig::new(10).with_collect_ids(false);
373        let code = ParallelRequestGenerator::generate_parallel_post(
374            &config,
375            "/resources",
376            "{ name: 'test' }",
377            "id",
378        );
379
380        assert!(!code.contains("createdIds.push"));
381        assert!(!code.contains("res.json"));
382    }
383
384    #[test]
385    fn test_generate_parallel_get() {
386        let code = ParallelRequestGenerator::generate_parallel_get("/resources/{id}", "id");
387
388        assert!(code.contains("Parallel resource retrieval"));
389        assert!(code.contains("createdIds.map"));
390        assert!(code.contains("method: 'GET'"));
391        // The generated code uses double braces for escaping in format strings
392        assert!(code.contains("{{id}}") || code.contains("{id}"));
393    }
394
395    #[test]
396    fn test_generate_parallel_delete() {
397        let code = ParallelRequestGenerator::generate_parallel_delete(
398            "/resources/{resourceId}",
399            "resourceId",
400        );
401
402        assert!(code.contains("Parallel resource cleanup"));
403        assert!(code.contains("method: 'DELETE'"));
404        // The generated code uses double braces for escaping in format strings
405        assert!(code.contains("{{resourceId}}") || code.contains("{resourceId}"));
406    }
407
408    #[test]
409    fn test_generate_complete_scenario() {
410        let config = ParallelConfig::new(20);
411        let code = ParallelRequestGenerator::generate_complete_scenario(
412            &config,
413            "/users",
414            "/users/{id}",
415            "id",
416            "{ name: `user-${i}` }",
417            "id",
418            true,
419        );
420
421        assert!(code.contains("Parallel resource creation"));
422        assert!(code.contains("Parallel resource retrieval"));
423        assert!(code.contains("Parallel resource cleanup"));
424    }
425
426    #[test]
427    fn test_generate_complete_scenario_no_cleanup() {
428        let config = ParallelConfig::new(20);
429        let code = ParallelRequestGenerator::generate_complete_scenario(
430            &config,
431            "/users",
432            "/users/{id}",
433            "id",
434            "{ name: `user-${i}` }",
435            "id",
436            false,
437        );
438
439        assert!(code.contains("Parallel resource creation"));
440        assert!(code.contains("Parallel resource retrieval"));
441        assert!(!code.contains("Parallel resource cleanup"));
442    }
443}