Author: brane Date: Fri Jul 18 12:47:56 2025 New Revision: 1927308 URL: http://svn.apache.org/viewvc?rev=1927308&view=rev Log: Implement a prototype libunbound-based asynchronous resolver. EXPERIMENTAL, does not include proper discovery of libunbound.
* CMakeLists.txt: Add some manually configurable bits to find libunbound. Show the result in the summary. * build/SerfGenClangd.cmake: Add the unbound include directory. * serf_private.h (serf_context_t): - resolve_init_status renamed from resolve_guard_status; - added resolve_context, which is implementation-specific. (serf__create_resolve_context): New prototype. * src/context.c (serf_context_create_ex): Initialize the resolve_context and track the status in resolve_init_status, along with the mutex status. * src/resolve.c: - include <unbound.h> when enabled; - include APR_WANT_BYTEFUNC through <apr_want.h>, used for logging resolve results. Seems to work fine on Windows/Fedora/Debian, needs testing on various *BSDs etc. - Add implementaiton for libunbound. (SERF_HAVE_ASYNC_RESOLVER): Renamed from SERF_USE_ASYNC_RESOLVER. Again. (serf__create_resolve_context): Implement here. * test/test_context.c (test_async_resolve): Check the status before the connection pointer, otherwise we don't see the status in the results if the pointer is NULL. Modified: serf/trunk/CMakeLists.txt serf/trunk/build/SerfGenClangd.cmake serf/trunk/serf_private.h serf/trunk/src/context.c serf/trunk/src/resolve.c serf/trunk/test/test_context.c Modified: serf/trunk/CMakeLists.txt URL: http://svn.apache.org/viewvc/serf/trunk/CMakeLists.txt?rev=1927308&r1=1927307&r2=1927308&view=diff ============================================================================== --- serf/trunk/CMakeLists.txt (original) +++ serf/trunk/CMakeLists.txt Fri Jul 18 12:47:56 2025 @@ -28,6 +28,7 @@ # ZLIB_ROOT - Path to zlib's install area # Brotli_ROOT - Path to Brotli's install area # GSSAPI_ROOT - Path to GSSAPI's install area +# Unbound_ROOT - Path to libunbound's install area # =================================================================== cmake_minimum_required(VERSION 3.12) @@ -261,6 +262,21 @@ else() endif() endif() + +# FIXME: VERYTEMPORARY, figure out FindUnbound.cmake first. +set(Unbound_FOUND FALSE) +if (Unbound_FOUND) + set(UNBOUND_INCLUDE_DIR "/opt/homebrew/opt/unbound/include") + set(UNBOUND_LIBRARIES "/opt/homebrew/opt/unbound/lib/libunbound.dylib") + # set(UNBOUND_LIBRARIES "/usr/lib/aarch64-linux-gnu/libunbound.so") + # set(UNBOUND_LIBRARIES "/usr/lib64/libunbound.so") + add_library(Unbound::Unbound UNKNOWN IMPORTED) + set_target_properties(Unbound::Unbound PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${UNBOUND_INCLUDE_DIR}") + set_target_properties(Unbound::Unbound PROPERTIES + IMPORTED_LOCATION "${UNBOUND_LIBRARIES}") +endif(Unbound_FOUND) + # Calculate the set of private and public targets set(SERF_PRIVATE_TARGETS OpenSSL::Crypto OpenSSL::SSL ZLIB::ZLIB) if(Brotli_FOUND) @@ -270,6 +286,11 @@ if(GSSAPI_FOUND) list(APPEND SERF_C_DEFINES "SERF_HAVE_GSSAPI") list(APPEND SERF_PRIVATE_TARGETS KRB5::GSSAPI) endif() +if(Unbound_FOUND) + list(APPEND SERF_C_DEFINES "SERF_HAVE_ASYNC_RESOLVER=1") + list(APPEND SERF_C_DEFINES "SERF_HAVE_UNBOUND") + list(APPEND SERF_PRIVATE_TARGETS Unbound::Unbound) +endif() if(APR_STATIC) if(SERF_WINDOWS) @@ -532,6 +553,7 @@ set(_gen_dot_clangd OFF) set(_have_brotli OFF) set(_have_gssapi OFF) set(_have_sspi OFF) +set(_have_unbound OFF) if(NOT SKIP_SHARED) set(_build_shared ON) @@ -558,6 +580,9 @@ endif() if("SERF_HAVE_SSPI" IN_LIST SERF_C_DEFINES) set(_have_sspi ON) endif() +if ("SERF_HAVE_UNBOUND" IN_LIST SERF_C_DEFINES) + set(_have_unbound "EXPERIMENTAL") +endif() message(STATUS "Summary:") message(STATUS " Version ................... : ${SERF_VERSION}") @@ -574,6 +599,7 @@ message(STATUS " Options:") message(STATUS " Brotli .................. : ${_have_brotli}") message(STATUS " GSSAPI .................. : ${_have_gssapi}") message(STATUS " SSPI .................... : ${_have_sspi}") +message(STATUS " Unbound ................. : ${_have_unbound}") message(STATUS " Install:") message(STATUS " prefix: ................. : ${CMAKE_INSTALL_PREFIX}") message(STATUS " headers: ................ : ${SERF_INSTALL_HEADERS}") Modified: serf/trunk/build/SerfGenClangd.cmake URL: http://svn.apache.org/viewvc/serf/trunk/build/SerfGenClangd.cmake?rev=1927308&r1=1927307&r2=1927308&view=diff ============================================================================== --- serf/trunk/build/SerfGenClangd.cmake (original) +++ serf/trunk/build/SerfGenClangd.cmake Fri Jul 18 12:47:56 2025 @@ -67,6 +67,9 @@ function(SerfGenClangd) if(GSSAPI_FOUND) list(APPEND includes ${GSSAPI_INCLUDES}) endif() + if(Unbound_FOUND) + list(APPEND includes ${UNBOUND_INCLUDE_DIR}) + endif() list(REMOVE_DUPLICATES includes) write_includes(${includes}) Modified: serf/trunk/serf_private.h URL: http://svn.apache.org/viewvc/serf/trunk/serf_private.h?rev=1927308&r1=1927307&r2=1927308&view=diff ============================================================================== --- serf/trunk/serf_private.h (original) +++ serf/trunk/serf_private.h Fri Jul 18 12:47:56 2025 @@ -498,12 +498,13 @@ struct serf_context_t { serf_config_t *config; - /* The results of asynchronous address resolution. */ + /* Support for asynchronous address resolution. */ + apr_status_t resolve_init_status; + serf__resolve_result_t *resolve_head; #if APR_HAS_THREADS apr_thread_mutex_t *resolve_guard; - apr_status_t resolve_guard_status; #endif - serf__resolve_result_t *resolve_head; + void *resolve_context; }; struct serf_listener_t { @@ -685,6 +686,10 @@ struct serf_connection_t { up buckets that may still reference buckets of this request */ void serf__connection_pre_cleanup(serf_connection_t *); +/* Called from serf_context_create_ex() to set up the context-specific + asynchronous address resolver context. */ +apr_status_t serf__create_resolve_context(serf_context_t *ctx); + /* Called from serf_context_prerun() before handling the connections. Processes the results of any asynchronously resolved addresses that were initiated for CTX. */ Modified: serf/trunk/src/context.c URL: http://svn.apache.org/viewvc/serf/trunk/src/context.c?rev=1927308&r1=1927307&r2=1927308&view=diff ============================================================================== --- serf/trunk/src/context.c (original) +++ serf/trunk/src/context.c Fri Jul 18 12:47:56 2025 @@ -195,14 +195,21 @@ serf_context_t *serf_context_create_ex( ctx->server_authn_info = apr_hash_make(pool); /* Initialize async resolver result queue. */ + ctx->resolve_init_status = APR_SUCCESS; + ctx->resolve_head = NULL; #if APR_HAS_THREADS - ctx->resolve_guard_status = apr_thread_mutex_create( + ctx->resolve_init_status = apr_thread_mutex_create( &ctx->resolve_guard, APR_THREAD_MUTEX_DEFAULT, ctx->pool); - if (ctx->resolve_guard_status != APR_SUCCESS) { + if (ctx->resolve_init_status != APR_SUCCESS) { ctx->resolve_guard = NULL; } #endif - ctx->resolve_head = NULL; + if (ctx->resolve_init_status == APR_SUCCESS) { + ctx->resolve_init_status = serf__create_resolve_context(ctx); + } + if (ctx->resolve_init_status != APR_SUCCESS) { + ctx->resolve_context = NULL; + } /* Assume returned status is APR_SUCCESS */ serf__config_store_init(ctx); Modified: serf/trunk/src/resolve.c URL: http://svn.apache.org/viewvc/serf/trunk/src/resolve.c?rev=1927308&r1=1927307&r2=1927308&view=diff ============================================================================== --- serf/trunk/src/resolve.c (original) +++ serf/trunk/src/resolve.c Fri Jul 18 12:47:56 2025 @@ -20,16 +20,32 @@ #include <apr.h> #include <apr_errno.h> -#include <apr_network_io.h> #include <apr_pools.h> +#include <apr_network_io.h> #include <apr_thread_mutex.h> #include <apr_thread_pool.h> +/* This will include <netinet/in.h> and/or <arpa/inet.h>, which we'll + use for logging the resolver results. On Windows, we'll always get + <Winsock2.h> from <apr.h>. */ +#define APR_WANT_BYTEFUNC +#include <apr_want.h> + #include "serf.h" #include "serf_private.h" -#define HAVE_ASYNC_RESOLVER (SERF_USE_ASYNC_RESOLVER || APR_HAS_THREADS) +#define HAVE_ASYNC_RESOLVER (SERF_HAVE_ASYNC_RESOLVER || APR_HAS_THREADS) + +#if SERF_HAVE_ASYNC_RESOLVER +#if SERF_HAVE_UNBOUND +#include <unbound.h> +#else +/* Really shouldn't happen, but just in case it does, fall back + to the apr_thread_pool-based resolver. */ +#undef SERF_HAVE_ASYNC_RESOLVER +#endif /* SERF_HAVE_UNBOUND */ +#endif /* * FIXME: EXPERIMENTAL @@ -41,13 +57,23 @@ * - Wake the poll/select in serf_context_run() when new resolve * results are available. * - * - Add a way to cancel a resolve task. + * - Add a way to cancel a resolve task? * * - Figure out what to do if the lock/unlock calls return an error. * This should not be possible unless we messed up the implementation, * but there should be a way for clients to back out of this situation. * Failed lock/unlock could potentially leave the context in an * inconsistent state. + * + * TODO for Unbound: + * - Convert unbound results to apr_sockaddr_t. + * + * - Resolve both IPv4 and IPv6 addresses. This will require creating two + * asynchronous resolve tasks and combining the results so that our + * callback gets invoked only once both tasks are completed. + * + * - Figure out how to use libunbound's event-based API, because it uses + * true asynchronous I/O instead of background threads. */ @@ -57,7 +83,7 @@ onto the context's result queue. */ static void push_resolve_result(serf_context_t *ctx, apr_sockaddr_t *host_address, - apr_status_t status, + apr_status_t resolve_status, serf_address_resolved_t resolved, void *resolved_baton, apr_pool_t *resolve_pool); @@ -80,8 +106,8 @@ apr_status_t serf_address_resolve_async( apr_pool_t *resolve_pool; #if APR_HAS_THREADS - if (ctx->resolve_guard_status != APR_SUCCESS) { - return ctx->resolve_guard_status; + if (ctx->resolve_init_status != APR_SUCCESS) { + return ctx->resolve_init_status; } #endif @@ -117,10 +143,16 @@ apr_status_t serf_address_resolve_async( #endif /* !HAVE_ASYNC_RESOLVER */ -#if SERF_USE_ASYNC_RESOLVER +#if SERF_HAVE_ASYNC_RESOLVER /* TODO: Add implementation for one or more async resolver libraries. */ #if 0 +/* Called during context creation. Must initialize ctx->resolver_context. */ +static apr_status_t create_resolve_context(serf_context_t *ctx) +{ + ... +} + static apr_status_t resolve_address_async(serf_context_t *ctx, apr_uri_t host_info, serf_address_resolved_t resolved, @@ -134,13 +166,257 @@ static apr_status_t resolve_address_asyn /* Some asynchronous resolved libraries use event loop to harvest results. This function will be called from serf__process_async_resolve_results() so, in effect, from serf_context_prerun(). */ -static void run_async_resolver_loop(void) +static apr_status_t run_async_resolver_loop(serf_context_t *ctx) { ... } -#endif +#endif /* 0 */ + +#if SERF_HAVE_UNBOUND + +static apr_status_t err_to_status(enum ub_ctx_err err) +{ + switch (err) + { + case UB_NOERROR: + /* no error */ + return APR_SUCCESS; + + case UB_SOCKET: + /* socket operation. Set to -1, so that if an error from _fd() is + passed (-1) it gives a socket error. */ + if (errno) + return APR_FROM_OS_ERROR(errno); + return APR_ENOTSOCK; + + case UB_NOMEM: + /* alloc failure */ + return APR_ENOMEM; + + case UB_SYNTAX: + /* syntax error */ + return APR_EINIT; + + case UB_SERVFAIL: + /* DNS service failed */ + return APR_EAGAIN; + + case UB_FORKFAIL: + /* fork() failed */ + return APR_ENOMEM; + + case UB_AFTERFINAL: + /* cfg change after finalize() */ + return APR_EINIT; + + case UB_INITFAIL: + /* initialization failed (bad settings) */ + return APR_EINIT; + + case UB_PIPE: + /* error in pipe communication with async bg worker */ + return APR_EPIPE; + + case UB_READFILE: + /* error reading from file (resolv.conf) */ + if (errno) + return APR_FROM_OS_ERROR(errno); + return APR_ENOENT; + + case UB_NOID: + /* error async_id does not exist or result already been delivered */ + return APR_EINVAL; + + default: + return APR_EGENERAL; + } +} + + +static apr_status_t cleanup_resolve_context(void *baton) +{ + struct ub_ctx *const resolve_context = baton; + ub_ctx_delete(resolve_context); + return APR_SUCCESS; +} + +static apr_status_t create_resolve_context(serf_context_t *ctx) +{ + int err; + struct ub_ctx *const resolve_context = ub_ctx_create(); + if (!resolve_context) + return APR_ENOMEM; + + err = ub_ctx_resolvconf(resolve_context, NULL); + if (!err) + err = ub_ctx_hosts(resolve_context, NULL); + if (!err) + err = ub_ctx_async(resolve_context, true); + + if (err) { + const apr_status_t status = err_to_status(err); + /* TODO: Error callback */ + serf__log(LOGLVL_ERROR, LOGCOMP_CONN, __FILE__, ctx->config, + "unbound ctx init: %s\n", ub_strerror(err)); + return status; + } + + ctx->resolve_context = resolve_context; + /* pre-cleanup because the live resolve tasks contain subpools of the + context pool and must be canceled before their pools go away. */ + apr_pool_pre_cleanup_register(ctx->pool, resolve_context, + cleanup_resolve_context); + return APR_SUCCESS; +} + + +/* Task data for the Unbound resolver. */ +typedef struct unbound_resolve_task resolve_task_t; +struct unbound_resolve_task +{ + serf_context_t *ctx; + apr_port_t host_port; + serf_address_resolved_t resolved; + void *resolved_baton; + apr_pool_t *resolve_pool; +}; + +static void resolve_callback(void* baton, int err, + struct ub_result* result) +{ + resolve_task_t *const task = baton; + apr_status_t status = err_to_status(err); + + if (err) { + /* TODO: Error callback */ + serf__log(LOGLVL_ERROR, LOGCOMP_CONN, __FILE__, task->ctx->config, + "unbound resolve: error %s\n", ub_strerror(err)); + } + if (!result->havedata) { + if (result->nxdomain) { + /* TODO: Error callback */ + serf__log(LOGLVL_ERROR, LOGCOMP_CONN, __FILE__, task->ctx->config, + "unbound resolve: NXDOMAIN [%d]\n", result->rcode); + if (status == APR_SUCCESS) + status = APR_ENOENT; + } + if (result->bogus) { + /* TODO: Error callback */ + serf__log(LOGLVL_ERROR, LOGCOMP_CONN, __FILE__, task->ctx->config, + "unbound resolve: BOGUS [%d]%s%s\n", result->rcode, + result->why_bogus ? " " : "", + result->why_bogus ? result->why_bogus : ""); + if (status == APR_SUCCESS) + status = APR_EINVAL; + } + if (result->was_ratelimited) { + /* TODO: Error callback */ + serf__log(LOGLVL_ERROR, LOGCOMP_CONN, __FILE__, task->ctx->config, + "unbound resolve: SERVFAIL [%d]\n", result->rcode); + if (status == APR_SUCCESS) + status = APR_EAGAIN; + } + + /* This shouldn't happen, one of the previous checks should + have caught an error. */ + if (status == APR_SUCCESS) { + /* TODO: Error callback */ + serf__log(LOGLVL_ERROR, LOGCOMP_CONN, __FILE__, task->ctx->config, + "unbound resolve: no data [%d]\n", result->rcode); + status = APR_ENOENT; + } + } + + if (status) + { + push_resolve_result(task->ctx, NULL, status, + task->resolved, task->resolved_baton, + task->resolve_pool); + } + else + { + if (serf__log_enabled(LOGLVL_DEBUG, LOGCOMP_CONN,task->ctx->config)) + { + int i; + + for (i = 0; result->data && result->data[i]; ++i) { + char buf[INET6_ADDRSTRLEN]; + const socklen_t len = sizeof(buf); + const char *address = "(AF-unknown)"; + + if (result->len[i] == sizeof(struct in_addr)) + address = inet_ntop(AF_INET, result->data[i], buf, len); + else if (result->len[i] == sizeof(struct in6_addr)) + address = inet_ntop(AF_INET6, result->data[i], buf, len); + serf__log(LOGLVL_DEBUG, LOGCOMP_CONN, + __FILE__, task->ctx->config, + "unbound resolve: %s: %s\n", result->qname, address); + } + } + + /* TODO: Convert ub_result to apr_sockaddr_t */ + push_resolve_result(task->ctx, NULL, APR_EAFNOSUPPORT, + task->resolved, task->resolved_baton, + task->resolve_pool); + } -#else /* !SERF_USE_ASYNC_RESOLVER */ + ub_resolve_free(result); +} + +static apr_status_t resolve_address_async(serf_context_t *ctx, + apr_uri_t host_info, + serf_address_resolved_t resolved, + void *resolved_baton, + apr_pool_t *resolve_pool, + apr_pool_t *scratch_pool) +{ + struct ub_ctx *const resolve_context = ctx->resolve_context; + resolve_task_t *const task = apr_palloc(resolve_pool, sizeof(*task)); + apr_status_t status = APR_SUCCESS; + int err; + + task->ctx = ctx; + task->host_port = host_info.port; + task->resolved = resolved; + task->resolved_baton = resolved_baton; + task->resolve_pool = resolve_pool; + + /* FIXME: We should resolve both RRType 1 (A) and RRType 28 (AAAA). */ + if ((err = ub_resolve_async(resolve_context, host_info.hostname, + 1, /* rrtype: IPv4 host address (A) */ + 1, /* rrclass: IN(ternet) */ + task, resolve_callback, NULL))) + { + /* TODO: Error callback */ + status = err_to_status(err); + serf__log(LOGLVL_ERROR, LOGCOMP_CONN, __FILE__, ctx->config, + "unbound resolve start: %s\n", ub_strerror(err)); + } + + return status; +} + +static apr_status_t run_async_resolver_loop(serf_context_t *ctx) +{ + struct ub_ctx *const resolve_context = ctx->resolve_context; + + if (ub_poll(resolve_context)) { + const int err = ub_process(resolve_context); + if (err) { + const apr_status_t status = err_to_status(err); + /* TODO: Error callback */ + serf__log(LOGLVL_ERROR, LOGCOMP_CONN, __FILE__, ctx->config, + "unbound process: %s\n", ub_strerror(err)); + return status; + } + } + + return APR_SUCCESS; +} + +#endif /* SERF_HAVE_UNBOUND */ + +#else /* !SERF_HAVE_ASYNC_RESOLVER */ #if APR_HAS_THREADS /* This could be made configurable, but given that this is a fallback @@ -165,9 +441,16 @@ static apr_status_t init_work_queue(void } +static apr_status_t create_resolve_context(serf_context_t *ctx) +{ + ctx->resolve_context = NULL; + return APR_SUCCESS; +} + + /* Task data for the thred pool resolver. */ -typedef struct resolve_task_t resolve_task_t; -struct resolve_task_t +typedef struct threadpool_resolve_task resolve_task_t; +struct threadpool_resolve_task { serf_context_t *ctx; apr_uri_t host_info; @@ -188,6 +471,28 @@ static void *APR_THREAD_FUNC resolve(apr APR_UNSPEC, task->host_info.port, 0, task->resolve_pool); + + if (status) { + host_address = NULL; + } + else if (serf__log_enabled(LOGLVL_DEBUG, LOGCOMP_CONN, task->ctx->config)) + { + apr_sockaddr_t *addr = host_address; + while (addr) + { + char buf[INET6_ADDRSTRLEN]; + const socklen_t len = sizeof(buf); + const char *address = "(AF-unknown)"; + + if (addr->family == APR_INET || addr->family == APR_INET6) + address = inet_ntop(addr->family, addr->ipaddr_ptr, buf, len); + serf__log(LOGLVL_DEBUG, LOGCOMP_CONN, + __FILE__, task->ctx->config, + "apr async resolve: %s: %s\n", addr->hostname, address); + addr = addr->next; + } + } + push_resolve_result(task->ctx, host_address, status, task->resolved, task->resolved_baton, task->resolve_pool); @@ -222,10 +527,13 @@ static apr_status_t resolve_address_asyn /* This is a no-op since we're using a thread pool that does its own task queue management. */ -static void run_async_resolver_loop(void) {} +static apr_status_t run_async_resolver_loop(serf_context_t *ctx) +{ + return APR_SUCCESS; +} #endif /* !APR_HAS_THREADS */ -#endif /* !SERF_USE_ASYNC_RESOLVER */ +#endif /* !SERF_HAVE_ASYNC_RESOLVER */ /*******************************************************************/ @@ -269,30 +577,37 @@ static apr_status_t unlock_results(serf_ static void push_resolve_result(serf_context_t *ctx, apr_sockaddr_t *host_address, - apr_status_t status, + apr_status_t resolve_status, serf_address_resolved_t resolved, void *resolved_baton, apr_pool_t *resolve_pool) { serf__resolve_result_t *result; - apr_status_t lock_status; + apr_status_t status; result = apr_palloc(resolve_pool, sizeof(*result)); result->host_address = host_address; - result->status = status; + result->status = resolve_status; result->resolved = resolved; result->resolved_baton = resolved_baton; result->result_pool = resolve_pool; - lock_status = lock_results(ctx); - if (!lock_status) + status = lock_results(ctx); + if (!status) { result->next = ctx->resolve_head; ctx->resolve_head = result; - lock_status = unlock_results(ctx); + status = unlock_results(ctx); } - /* TODO: if (lock_status) ... then what? */ + /* TODO: if (status) ... then what? */ +} + + +/* Internal API */ +apr_status_t serf__create_resolve_context(serf_context_t *ctx) +{ + return create_resolve_context(ctx); } @@ -300,21 +615,23 @@ static void push_resolve_result(serf_con apr_status_t serf__process_async_resolve_results(serf_context_t *ctx) { serf__resolve_result_t *result = NULL; - apr_status_t lock_status; + apr_status_t status; - run_async_resolver_loop(); + status = run_async_resolver_loop(ctx); + if (status) + return status; - lock_status = lock_results(ctx); - if (lock_status) - return lock_status; + status = lock_results(ctx); + if (status) + return status; result = ctx->resolve_head; ctx->resolve_head = NULL; - lock_status = unlock_results(ctx); + status = unlock_results(ctx); - /* TODO: if (lock_status) ... then what? Shouldn't be possible. */ - /* if (lock_status) */ - /* return lock_status; */ + /* TODO: if (status) ... then what? Shouldn't be possible. */ + /* if (status) */ + /* return status; */ while (result) { Modified: serf/trunk/test/test_context.c URL: http://svn.apache.org/viewvc/serf/trunk/test/test_context.c?rev=1927308&r1=1927307&r2=1927308&view=diff ============================================================================== --- serf/trunk/test/test_context.c (original) +++ serf/trunk/test/test_context.c Fri Jul 18 12:47:56 2025 @@ -1046,8 +1046,8 @@ static void test_async_resolve(CuTest *t if (!APR_STATUS_IS_TIMEUP(status)) CuAssertIntEquals(tc, APR_SUCCESS, status); } - CuAssertPtrNotNull(tc, tb->connection); CuAssertIntEquals(tc, APR_SUCCESS, tb->user_status); + CuAssertPtrNotNull(tc, tb->connection); /* Send some requests on the connections */ for (i = 0 ; i < num_requests ; i++) {