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!(
89            "for (let i = 0; i < {}; i++) {{\n",
90            config.count
91        ));
92        code.push_str("  batchRequests.push({\n");
93        code.push_str("    method: 'POST',\n");
94        code.push_str(&format!("    url: `${{BASE_URL}}{}`", path));
95        code.push_str(",\n");
96        code.push_str(&format!("    body: JSON.stringify({}),\n", body_template));
97        code.push_str("    params: { headers }\n");
98        code.push_str("  });\n");
99        code.push_str("}\n\n");
100
101        // Execute batch with size limits
102        if config.count > config.max_batch_size {
103            code.push_str("// Execute in batches to avoid overwhelming the server\n");
104            code.push_str("const createdIds = [];\n");
105            code.push_str(&format!(
106                "const batchSize = {};\n",
107                config.max_batch_size
108            ));
109            code.push_str("for (let batchStart = 0; batchStart < batchRequests.length; batchStart += batchSize) {\n");
110            code.push_str("  const batchEnd = Math.min(batchStart + batchSize, batchRequests.length);\n");
111            code.push_str("  const batch = batchRequests.slice(batchStart, batchEnd);\n");
112            code.push_str("  const responses = http.batch(batch);\n\n");
113
114            if config.collect_ids {
115                code.push_str("  // Collect IDs from responses\n");
116                code.push_str("  for (const res of responses) {\n");
117                code.push_str("    if (res.status >= 200 && res.status < 300) {\n");
118                code.push_str("      try {\n");
119                code.push_str(&format!(
120                    "        const id = res.json('{}');\n",
121                    id_field
122                ));
123                code.push_str("        if (id) createdIds.push(id);\n");
124                code.push_str("      } catch (e) {\n");
125                code.push_str("        console.error('Failed to extract ID:', e);\n");
126                code.push_str("      }\n");
127                code.push_str("    }\n");
128                code.push_str("  }\n\n");
129            }
130
131            code.push_str("  // Check batch results\n");
132            code.push_str("  const batchSuccess = responses.every(r => r.status >= 200 && r.status < 300);\n");
133            code.push_str("  check(responses, {\n");
134            code.push_str("    'batch creation successful': () => batchSuccess\n");
135            code.push_str("  });\n\n");
136
137            if config.batch_delay_ms > 0 {
138                code.push_str(&format!(
139                    "  sleep({});\n",
140                    config.batch_delay_ms as f64 / 1000.0
141                ));
142            }
143            code.push_str("}\n");
144        } else {
145            code.push_str("// Execute all requests in parallel\n");
146            code.push_str("const responses = http.batch(batchRequests);\n\n");
147
148            if config.collect_ids {
149                code.push_str("// Collect IDs from responses\n");
150                code.push_str("const createdIds = [];\n");
151                code.push_str("for (const res of responses) {\n");
152                code.push_str("  if (res.status >= 200 && res.status < 300) {\n");
153                code.push_str("    try {\n");
154                code.push_str(&format!(
155                    "      const id = res.json('{}');\n",
156                    id_field
157                ));
158                code.push_str("      if (id) createdIds.push(id);\n");
159                code.push_str("    } catch (e) {\n");
160                code.push_str("      console.error('Failed to extract ID:', e);\n");
161                code.push_str("    }\n");
162                code.push_str("  }\n");
163                code.push_str("}\n\n");
164            }
165
166            code.push_str("// Check all responses\n");
167            code.push_str("const allSuccess = responses.every(r => r.status >= 200 && r.status < 300);\n");
168            code.push_str("check(responses, {\n");
169            code.push_str("  'parallel creation successful': () => allSuccess\n");
170            code.push_str("});\n");
171        }
172
173        code
174    }
175
176    /// Generate k6 code for parallel GET requests using collected IDs
177    pub fn generate_parallel_get(path_template: &str, id_param: &str) -> String {
178        let mut code = String::new();
179
180        code.push_str("// Parallel resource retrieval\n");
181        code.push_str("if (createdIds.length > 0) {\n");
182        code.push_str("  const getRequests = createdIds.map(id => ({\n");
183        code.push_str("    method: 'GET',\n");
184        code.push_str(&format!(
185            "    url: `${{BASE_URL}}{}`.replace('{{{{{}}}}}', id),\n",
186            path_template, id_param
187        ));
188        code.push_str("    params: { headers }\n");
189        code.push_str("  }));\n\n");
190        code.push_str("  const getResponses = http.batch(getRequests);\n");
191        code.push_str("  const getSuccess = getResponses.every(r => r.status >= 200 && r.status < 300);\n");
192        code.push_str("  check(getResponses, {\n");
193        code.push_str("    'parallel retrieval successful': () => getSuccess\n");
194        code.push_str("  });\n");
195        code.push_str("}\n");
196
197        code
198    }
199
200    /// Generate k6 code for parallel DELETE requests using collected IDs
201    pub fn generate_parallel_delete(path_template: &str, id_param: &str) -> String {
202        let mut code = String::new();
203
204        code.push_str("// Parallel resource cleanup\n");
205        code.push_str("if (createdIds.length > 0) {\n");
206        code.push_str("  const deleteRequests = createdIds.map(id => ({\n");
207        code.push_str("    method: 'DELETE',\n");
208        code.push_str(&format!(
209            "    url: `${{BASE_URL}}{}`.replace('{{{{{}}}}}', id),\n",
210            path_template, id_param
211        ));
212        code.push_str("    params: { headers }\n");
213        code.push_str("  }));\n\n");
214        code.push_str("  const deleteResponses = http.batch(deleteRequests);\n");
215        code.push_str("  const deleteSuccess = deleteResponses.every(r => r.status >= 200 && r.status < 300);\n");
216        code.push_str("  check(deleteResponses, {\n");
217        code.push_str("    'parallel cleanup successful': () => deleteSuccess\n");
218        code.push_str("  });\n");
219        code.push_str("}\n");
220
221        code
222    }
223
224    /// Generate k6 helper functions for batch operations
225    pub fn generate_batch_helper(config: &ParallelConfig) -> String {
226        let mut code = String::new();
227
228        code.push_str("// Parallel batch execution helpers\n");
229        code.push_str(&format!(
230            "const PARALLEL_BATCH_SIZE = {};\n",
231            config.max_batch_size
232        ));
233        code.push_str(&format!(
234            "const PARALLEL_COUNT = {};\n\n",
235            config.count
236        ));
237
238        code.push_str(r#"function executeBatch(requests) {
239  const results = [];
240  const batchSize = PARALLEL_BATCH_SIZE;
241
242  for (let i = 0; i < requests.length; i += batchSize) {
243    const batch = requests.slice(i, i + batchSize);
244    const responses = http.batch(batch);
245    results.push(...responses);
246  }
247
248  return results;
249}
250
251function collectIds(responses, idField = 'id') {
252  const ids = [];
253  for (const res of responses) {
254    if (res.status >= 200 && res.status < 300) {
255      try {
256        const id = res.json(idField);
257        if (id) ids.push(id);
258      } catch (e) {
259        // Ignore parse errors
260      }
261    }
262  }
263  return ids;
264}
265"#);
266
267        code
268    }
269
270    /// Generate complete parallel test scenario
271    pub fn generate_complete_scenario(
272        config: &ParallelConfig,
273        base_path: &str,
274        detail_path: &str,
275        id_param: &str,
276        body_template: &str,
277        id_field: &str,
278        include_cleanup: bool,
279    ) -> String {
280        let mut code = String::new();
281
282        // Create resources
283        code.push_str(&Self::generate_parallel_post(config, base_path, body_template, id_field));
284        code.push_str("\n");
285
286        // Read all created resources
287        code.push_str(&Self::generate_parallel_get(detail_path, id_param));
288        code.push_str("\n");
289
290        // Cleanup if requested
291        if include_cleanup {
292            code.push_str(&Self::generate_parallel_delete(detail_path, id_param));
293        }
294
295        code
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_parallel_config_default() {
305        let config = ParallelConfig::default();
306        assert_eq!(config.count, 10);
307        assert!(config.collect_ids);
308        assert_eq!(config.max_batch_size, 100);
309    }
310
311    #[test]
312    fn test_parallel_config_new() {
313        let config = ParallelConfig::new(50);
314        assert_eq!(config.count, 50);
315        assert!(config.collect_ids);
316    }
317
318    #[test]
319    fn test_parallel_config_builders() {
320        let config = ParallelConfig::new(100)
321            .with_collect_ids(false)
322            .with_max_batch_size(25);
323
324        assert_eq!(config.count, 100);
325        assert!(!config.collect_ids);
326        assert_eq!(config.max_batch_size, 25);
327    }
328
329    #[test]
330    fn test_num_batches() {
331        let config = ParallelConfig::new(100).with_max_batch_size(30);
332        assert_eq!(config.num_batches(), 4); // 30 + 30 + 30 + 10
333
334        let config2 = ParallelConfig::new(50).with_max_batch_size(100);
335        assert_eq!(config2.num_batches(), 1);
336
337        let config3 = ParallelConfig::new(100).with_max_batch_size(100);
338        assert_eq!(config3.num_batches(), 1);
339    }
340
341    #[test]
342    fn test_batch_size() {
343        let config = ParallelConfig::new(100).with_max_batch_size(30);
344
345        assert_eq!(config.batch_size(0), 30);
346        assert_eq!(config.batch_size(1), 30);
347        assert_eq!(config.batch_size(2), 30);
348        assert_eq!(config.batch_size(3), 10); // Last batch is smaller
349    }
350
351    #[test]
352    fn test_generate_parallel_post_small_batch() {
353        let config = ParallelConfig::new(5);
354        let code = ParallelRequestGenerator::generate_parallel_post(
355            &config,
356            "/resources",
357            "{ name: `resource-${__VU}-${i}` }",
358            "id",
359        );
360
361        assert!(code.contains("batchRequests.push"));
362        assert!(code.contains("for (let i = 0; i < 5; i++)"));
363        assert!(code.contains("http.batch(batchRequests)"));
364        assert!(code.contains("res.json('id')"));
365    }
366
367    #[test]
368    fn test_generate_parallel_post_large_batch() {
369        let config = ParallelConfig::new(150).with_max_batch_size(50);
370        let code = ParallelRequestGenerator::generate_parallel_post(
371            &config,
372            "/resources",
373            "{ name: `resource-${i}` }",
374            "uuid",
375        );
376
377        assert!(code.contains("Execute in batches"));
378        assert!(code.contains("batchSize = 50"));
379        assert!(code.contains("batchStart + batchSize"));
380        assert!(code.contains("res.json('uuid')"));
381    }
382
383    #[test]
384    fn test_generate_parallel_post_no_collect() {
385        let config = ParallelConfig::new(10).with_collect_ids(false);
386        let code = ParallelRequestGenerator::generate_parallel_post(
387            &config,
388            "/resources",
389            "{ name: 'test' }",
390            "id",
391        );
392
393        assert!(!code.contains("createdIds.push"));
394        assert!(!code.contains("res.json"));
395    }
396
397    #[test]
398    fn test_generate_parallel_get() {
399        let code = ParallelRequestGenerator::generate_parallel_get("/resources/{id}", "id");
400
401        assert!(code.contains("Parallel resource retrieval"));
402        assert!(code.contains("createdIds.map"));
403        assert!(code.contains("method: 'GET'"));
404        // The generated code uses double braces for escaping in format strings
405        assert!(code.contains("{{id}}") || code.contains("{id}"));
406    }
407
408    #[test]
409    fn test_generate_parallel_delete() {
410        let code = ParallelRequestGenerator::generate_parallel_delete("/resources/{resourceId}", "resourceId");
411
412        assert!(code.contains("Parallel resource cleanup"));
413        assert!(code.contains("method: 'DELETE'"));
414        // The generated code uses double braces for escaping in format strings
415        assert!(code.contains("{{resourceId}}") || code.contains("{resourceId}"));
416    }
417
418    #[test]
419    fn test_generate_complete_scenario() {
420        let config = ParallelConfig::new(20);
421        let code = ParallelRequestGenerator::generate_complete_scenario(
422            &config,
423            "/users",
424            "/users/{id}",
425            "id",
426            "{ name: `user-${i}` }",
427            "id",
428            true,
429        );
430
431        assert!(code.contains("Parallel resource creation"));
432        assert!(code.contains("Parallel resource retrieval"));
433        assert!(code.contains("Parallel resource cleanup"));
434    }
435
436    #[test]
437    fn test_generate_complete_scenario_no_cleanup() {
438        let config = ParallelConfig::new(20);
439        let code = ParallelRequestGenerator::generate_complete_scenario(
440            &config,
441            "/users",
442            "/users/{id}",
443            "id",
444            "{ name: `user-${i}` }",
445            "id",
446            false,
447        );
448
449        assert!(code.contains("Parallel resource creation"));
450        assert!(code.contains("Parallel resource retrieval"));
451        assert!(!code.contains("Parallel resource cleanup"));
452    }
453}