stream_client.c (13190B)
1 /* 2 This file is part of paivana tests. 3 Copyright (C) 2026 Taler Systems SA 4 5 Paivana is free software; you can redistribute it and/or 6 modify it under the terms of the GNU Affero General Public License 7 as published by the Free Software Foundation; either version 8 3, or (at your option) any later version. 9 10 Paivana is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty 12 of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 13 the GNU Affero General Public License for more details. 14 15 You should have received a copy of the GNU Affero General Public 16 License along with Paivana; see the file COPYING. If not, 17 write to the Free Software Foundation, Inc., 51 Franklin 18 Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 */ 20 21 /** 22 * @file stream_client.c 23 * @brief HTTP client for the streaming tests: verifies a body against 24 * the generated pattern as it arrives, and reports what it saw 25 * about the *framing* rather than only about the bytes. 26 * 27 * Incremental verification is the point. A 200 MiB body 28 * written to a file and compared afterwards costs the disk and 29 * the wall clock of two extra copies, and — worse for these 30 * tests — says nothing about whether paivana streamed it or 31 * assembled it first. Here nothing larger than one libcurl 32 * delivery is ever held, so the client cannot be the thing that 33 * blows up the memory bound the tests are checking. 34 * 35 * What it prints on stdout, one `key=value' per line: 36 * 37 * status=N HTTP status 38 * bytes=N body bytes received 39 * pattern=ok|CORRUPT|... incremental pattern check 40 * chunked=yes|no Transfer-Encoding: chunked on the wire 41 * content_length=N|none the client-visible Content-Length 42 * curl=N CURLcode (0 = success, 18 = partial) 43 * ttfb_ms=N time to first body byte 44 * total_ms=N time to last byte 45 * body=... the response body, with --print-body 46 * 47 * The rate options are what make the congestion cases 48 * reproducible. Without a slow peer at one end, loopback and 49 * the kernel socket buffers absorb everything and no 50 * backpressure is ever exercised: paivana would look 51 * well-behaved whether or not it had any. 52 * 53 * The exit status is 0 whenever the exchange was carried out, 54 * *including* when the transfer failed: which failure occurred 55 * is data the driver asserts on, not a reason to abort. Exit 1 56 * is reserved for the client itself being unable to run. 57 * 58 * Usage: 59 * stream_client URL [--upload N] [--chunked-upload] 60 * [--expect-bytes N] [--head] [--print-body] 61 * [--abort-after N] [--read-rate N] 62 * [--upload-rate N] [--recv-buffer N] 63 */ 64 #include "platform.h" 65 #include <curl/curl.h> 66 #include <inttypes.h> 67 #include <stdbool.h> 68 #include <stdint.h> 69 #include <stdio.h> 70 #include <stdlib.h> 71 #include <string.h> 72 #include <strings.h> 73 74 75 /** 76 * The body byte at offset @a off. Must agree with the same function 77 * in `stream_upstream.c'. 78 */ 79 static uint8_t 80 pattern_at (uint64_t off) 81 { 82 return (uint8_t) ((off * 7) + (off >> 13)); 83 } 84 85 86 /** 87 * Body bytes received so far. 88 */ 89 static uint64_t received; 90 91 /** 92 * Offset of the first byte that did not match the pattern, or 93 * UINT64_MAX if they all have. 94 */ 95 static uint64_t bad_at = UINT64_MAX; 96 97 /** 98 * Did the response carry `Transfer-Encoding: chunked'? 99 */ 100 static bool saw_chunked; 101 102 /** 103 * `Content-Length' of the response, or UINT64_MAX if there was none. 104 */ 105 static uint64_t saw_length = UINT64_MAX; 106 107 /** 108 * Bytes of request body still to send. 109 */ 110 static uint64_t upload_left; 111 112 /** 113 * Offset of the next request-body byte to send. 114 */ 115 static uint64_t upload_off; 116 117 /** 118 * CURLOPT_BUFFERSIZE, or 0 to leave libcurl's default. A small value 119 * makes libcurl read the socket in small units, so paivana's content 120 * reader is called far more often and for far less each time -- 121 * hundreds of pause/resume transitions where the default buffer would 122 * produce a handful. 123 */ 124 static unsigned long recv_buffer; 125 126 /** 127 * Read the response body at no more than this many bytes per second, 128 * or 0 for as fast as it arrives. 129 */ 130 static uint64_t read_rate; 131 132 /** 133 * Send the request body at no more than this many bytes per second, 134 * or 0 for as fast as libcurl will take it. 135 */ 136 static uint64_t upload_rate; 137 138 /** 139 * Abort the transfer once this many body bytes have arrived, or 0 140 * never to. For the cases about what paivana does when its client 141 * walks away mid-response. 142 */ 143 static uint64_t abort_after; 144 145 /** 146 * Print the response body rather than checking it against the 147 * pattern? For the targets whose answer is a short report about the 148 * request we sent, which is the interesting half of an upload case. 149 */ 150 static bool print_body; 151 152 /** 153 * First #BODY_KEEP bytes of the response, when @e print_body is set. 154 */ 155 static char body_buf[512]; 156 157 /** 158 * Bytes held in @e body_buf. 159 */ 160 static size_t body_len; 161 162 /** 163 * Time of the first body byte, or 0 if none has arrived. 164 */ 165 static struct timespec first_byte; 166 167 /** 168 * When the request started. 169 */ 170 static struct timespec started; 171 172 173 static long 174 elapsed_ms (const struct timespec *from, 175 const struct timespec *to) 176 { 177 return (long) ((to->tv_sec - from->tv_sec) * 1000 178 + (to->tv_nsec - from->tv_nsec) / 1000000); 179 } 180 181 182 /** 183 * Sleep for @a ms milliseconds, resuming across signals. 184 */ 185 static void 186 sleep_ms (long ms) 187 { 188 struct timespec ts; 189 190 if (0 >= ms) 191 return; 192 ts.tv_sec = ms / 1000; 193 ts.tv_nsec = (ms % 1000) * 1000000L; 194 while ( (0 != nanosleep (&ts, &ts)) && 195 (EINTR == errno) ) 196 ; /* again */ 197 } 198 199 200 /** 201 * Hold @a moved bytes down to @a rate bytes per second, measuring from 202 * @a since. 203 * 204 * Paced against the clock rather than by a fixed nap per callback: the 205 * callback is handed whatever libcurl happened to read, so a fixed nap 206 * would make the actual rate depend on the delivery size and the cases 207 * that assert on elapsed time would be measuring the wrong thing. 208 * 209 * @param since when the transfer started 210 * @param moved bytes moved so far 211 * @param rate bytes per second to hold to 212 */ 213 static void 214 pace (const struct timespec *since, 215 uint64_t moved, 216 uint64_t rate) 217 { 218 struct timespec now; 219 long due_ms; 220 long spent_ms; 221 222 if (0 == rate) 223 return; 224 clock_gettime (CLOCK_MONOTONIC, 225 &now); 226 due_ms = (long) ((moved * 1000ULL) / rate); 227 spent_ms = elapsed_ms (since, 228 &now); 229 sleep_ms (due_ms - spent_ms); 230 } 231 232 233 static size_t 234 write_cb (char *ptr, 235 size_t size, 236 size_t nmemb, 237 void *cls) 238 { 239 size_t n = size * nmemb; 240 241 (void) cls; 242 if (0 == received) 243 clock_gettime (CLOCK_MONOTONIC, 244 &first_byte); 245 if (print_body) 246 { 247 size_t room = sizeof (body_buf) - body_len; 248 size_t take = (n < room) ? n : room; 249 250 memcpy (&body_buf[body_len], 251 ptr, 252 take); 253 body_len += take; 254 } 255 else 256 { 257 for (size_t i = 0; i < n; i++) 258 if ( (UINT64_MAX == bad_at) && 259 ((uint8_t) ptr[i] != pattern_at (received + i)) ) 260 bad_at = received + i; 261 } 262 received += n; 263 if ( (0 != abort_after) && 264 (received >= abort_after) ) 265 return 0; /* fail the transfer: this is us hanging up */ 266 /* Blocking here is the whole point: libcurl stops reading the socket 267 while we sleep, its receive window closes, and paivana's own 268 download ring backs up. That is the congestion the cases are 269 about, and there is no way to produce it on loopback without it. */ 270 pace (&first_byte, 271 received, 272 read_rate); 273 return n; 274 } 275 276 277 static size_t 278 header_cb (char *ptr, 279 size_t size, 280 size_t nmemb, 281 void *cls) 282 { 283 size_t n = size * nmemb; 284 static const char te[] = "transfer-encoding:"; 285 static const char cl[] = "content-length:"; 286 287 (void) cls; 288 if ( (n >= sizeof (te) - 1) && 289 (0 == strncasecmp (ptr, te, sizeof (te) - 1)) ) 290 { 291 /* The whole point of looking is to see the framing paivana chose, 292 so match the value rather than assume it. */ 293 for (size_t i = sizeof (te) - 1; i + 6 < n; i++) 294 if (0 == strncasecmp (&ptr[i], "chunked", 7)) 295 { 296 saw_chunked = true; 297 break; 298 } 299 } 300 if ( (n >= sizeof (cl) - 1) && 301 (0 == strncasecmp (ptr, cl, sizeof (cl) - 1)) ) 302 saw_length = strtoull (&ptr[sizeof (cl) - 1], 303 NULL, 304 10); 305 return n; 306 } 307 308 309 static size_t 310 read_cb (char *ptr, 311 size_t size, 312 size_t nmemb, 313 void *cls) 314 { 315 size_t room = size * nmemb; 316 size_t n; 317 318 (void) cls; 319 n = (size_t) ((upload_left < (uint64_t) room) 320 ? upload_left 321 : (uint64_t) room); 322 for (size_t i = 0; i < n; i++) 323 ptr[i] = (char) pattern_at (upload_off + i); 324 upload_off += n; 325 upload_left -= n; 326 pace (&started, 327 upload_off, 328 upload_rate); 329 return n; 330 } 331 332 333 int 334 main (int argc, 335 char **argv) 336 { 337 CURL *eh; 338 CURLcode rc; 339 long status = 0; 340 const char *url = NULL; 341 uint64_t upload = 0; 342 bool chunked_upload = false; 343 bool head = false; 344 uint64_t expect = UINT64_MAX; 345 struct timespec done; 346 struct curl_slist *hdrs = NULL; 347 348 for (int i = 1; i < argc; i++) 349 { 350 if (0 == strcmp (argv[i], "--upload")) 351 upload = strtoull (argv[++i], NULL, 10); 352 else if (0 == strcmp (argv[i], "--chunked-upload")) 353 chunked_upload = true; 354 else if (0 == strcmp (argv[i], "--head")) 355 head = true; 356 else if (0 == strcmp (argv[i], "--print-body")) 357 print_body = true; 358 else if (0 == strcmp (argv[i], "--abort-after")) 359 abort_after = strtoull (argv[++i], NULL, 10); 360 else if (0 == strcmp (argv[i], "--read-rate")) 361 read_rate = strtoull (argv[++i], NULL, 10); 362 else if (0 == strcmp (argv[i], "--upload-rate")) 363 upload_rate = strtoull (argv[++i], NULL, 10); 364 else if (0 == strcmp (argv[i], "--recv-buffer")) 365 recv_buffer = strtoul (argv[++i], NULL, 10); 366 else if (0 == strcmp (argv[i], "--expect-bytes")) 367 expect = strtoull (argv[++i], NULL, 10); 368 else if (NULL == url) 369 url = argv[i]; 370 else 371 { 372 fprintf (stderr, 373 "unexpected argument `%s'\n", 374 argv[i]); 375 return 1; 376 } 377 } 378 if (NULL == url) 379 { 380 fprintf (stderr, 381 "usage: %s URL [--upload N] [--chunked-upload]" 382 " [--head] [--print-body] [--expect-bytes N]" 383 " [--abort-after N] [--read-rate N] [--upload-rate N]" 384 " [--recv-buffer N]\n", 385 argv[0]); 386 return 1; 387 } 388 eh = curl_easy_init (); 389 if (NULL == eh) 390 { 391 fprintf (stderr, 392 "curl_easy_init failed\n"); 393 return 1; 394 } 395 upload_left = upload; 396 curl_easy_setopt (eh, CURLOPT_URL, url); 397 curl_easy_setopt (eh, CURLOPT_WRITEFUNCTION, &write_cb); 398 curl_easy_setopt (eh, CURLOPT_HEADERFUNCTION, &header_cb); 399 curl_easy_setopt (eh, CURLOPT_NOSIGNAL, 1L); 400 if (0 != recv_buffer) 401 curl_easy_setopt (eh, 402 CURLOPT_BUFFERSIZE, 403 (long) recv_buffer); 404 if (head) 405 { 406 curl_easy_setopt (eh, CURLOPT_NOBODY, 1L); 407 } 408 else if (0 != upload) 409 { 410 curl_easy_setopt (eh, CURLOPT_POST, 1L); 411 curl_easy_setopt (eh, CURLOPT_READFUNCTION, &read_cb); 412 if (chunked_upload) 413 { 414 /* Say nothing about the length and libcurl chunks; this is the 415 framing paivana has to reproduce upstream. */ 416 hdrs = curl_slist_append (hdrs, 417 "Transfer-Encoding: chunked"); 418 curl_easy_setopt (eh, CURLOPT_HTTPHEADER, hdrs); 419 } 420 else 421 { 422 curl_easy_setopt (eh, 423 CURLOPT_POSTFIELDSIZE_LARGE, 424 (curl_off_t) upload); 425 } 426 } 427 clock_gettime (CLOCK_MONOTONIC, &started); 428 rc = curl_easy_perform (eh); 429 clock_gettime (CLOCK_MONOTONIC, &done); 430 curl_easy_getinfo (eh, 431 CURLINFO_RESPONSE_CODE, 432 &status); 433 printf ("status=%ld\n", status); 434 printf ("bytes=%" PRIu64 "\n", received); 435 if (print_body) 436 printf ("pattern=n/a\n"); 437 else if (UINT64_MAX != bad_at) 438 printf ("pattern=CORRUPT at %" PRIu64 "\n", bad_at); 439 else if ( (UINT64_MAX != expect) && 440 (received != expect) ) 441 printf ("pattern=SHORT want %" PRIu64 "\n", expect); 442 else 443 printf ("pattern=ok\n"); 444 printf ("chunked=%s\n", saw_chunked ? "yes" : "no"); 445 if (UINT64_MAX == saw_length) 446 printf ("content_length=none\n"); 447 else 448 printf ("content_length=%" PRIu64 "\n", saw_length); 449 printf ("curl=%d\n", (int) rc); 450 printf ("ttfb_ms=%ld\n", 451 (0 == received) ? -1 : elapsed_ms (&started, &first_byte)); 452 printf ("total_ms=%ld\n", elapsed_ms (&started, &done)); 453 if (print_body) 454 printf ("body=%.*s\n", 455 (int) body_len, 456 body_buf); 457 curl_slist_free_all (hdrs); 458 curl_easy_cleanup (eh); 459 return 0; 460 } 461 462 463 /* end of stream_client.c */