You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by ic...@apache.org on 2023/01/09 07:35:19 UTC

svn commit: r1906475 [10/11] - in /httpd/httpd/branches/2.4.x: ./ changes-entries/ modules/http2/ test/ test/modules/http2/ test/modules/http2/htdocs/cgi/ test/modules/http2/mod_h2test/ test/pyhttpd/ test/pyhttpd/mod_aptest/

Modified: httpd/httpd/branches/2.4.x/modules/http2/h2_workers.c
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/modules/http2/h2_workers.c?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/modules/http2/h2_workers.c (original)
+++ httpd/httpd/branches/2.4.x/modules/http2/h2_workers.c Mon Jan  9 07:35:18 2023
@@ -15,304 +15,352 @@
  */
 
 #include <assert.h>
-#include <apr_atomic.h>
+#include <apr_ring.h>
 #include <apr_thread_mutex.h>
 #include <apr_thread_cond.h>
 
 #include <mpm_common.h>
 #include <httpd.h>
+#include <http_connection.h>
 #include <http_core.h>
 #include <http_log.h>
+#include <http_protocol.h>
 
 #include "h2.h"
 #include "h2_private.h"
 #include "h2_mplx.h"
-#include "h2_task.h"
+#include "h2_c2.h"
 #include "h2_workers.h"
 #include "h2_util.h"
 
+typedef enum {
+    PROD_IDLE,
+    PROD_ACTIVE,
+    PROD_JOINED,
+} prod_state_t;
+
+struct ap_conn_producer_t {
+    APR_RING_ENTRY(ap_conn_producer_t) link;
+    const char *name;
+    void *baton;
+    ap_conn_producer_next *fn_next;
+    ap_conn_producer_done *fn_done;
+    ap_conn_producer_shutdown *fn_shutdown;
+    volatile prod_state_t state;
+    volatile int conns_active;
+};
+
+
+typedef enum {
+    H2_SLOT_FREE,
+    H2_SLOT_RUN,
+    H2_SLOT_ZOMBIE,
+} h2_slot_state_t;
+
 typedef struct h2_slot h2_slot;
 struct h2_slot {
-    int id;
-    int sticks;
-    h2_slot *next;
+    APR_RING_ENTRY(h2_slot) link;
+    apr_uint32_t id;
+    apr_pool_t *pool;
+    h2_slot_state_t state;
+    volatile int should_shutdown;
+    volatile int is_idle;
     h2_workers *workers;
-    h2_task *task;
+    ap_conn_producer_t *prod;
     apr_thread_t *thread;
-    apr_thread_mutex_t *lock;
-    apr_thread_cond_t *not_idle;
-    volatile apr_uint32_t timed_out;
+    struct apr_thread_cond_t *more_work;
+    int activations;
 };
 
-static h2_slot *pop_slot(h2_slot *volatile *phead) 
-{
-    /* Atomically pop a slot from the list */
-    for (;;) {
-        h2_slot *first = *phead;
-        if (first == NULL) {
-            return NULL;
-        }
-        if (apr_atomic_casptr((void*)phead, first->next, first) == first) {
-            first->next = NULL;
-            return first;
-        }
-    }
-}
+struct h2_workers {
+    server_rec *s;
+    apr_pool_t *pool;
+
+    apr_uint32_t max_slots;
+    apr_uint32_t min_active;
+    volatile apr_time_t idle_limit;
+    volatile int aborted;
+    volatile int shutdown;
+    int dynamic;
+
+    volatile apr_uint32_t active_slots;
+    volatile apr_uint32_t idle_slots;
+
+    apr_threadattr_t *thread_attr;
+    h2_slot *slots;
+
+    APR_RING_HEAD(h2_slots_free, h2_slot) free;
+    APR_RING_HEAD(h2_slots_idle, h2_slot) idle;
+    APR_RING_HEAD(h2_slots_busy, h2_slot) busy;
+    APR_RING_HEAD(h2_slots_zombie, h2_slot) zombie;
+
+    APR_RING_HEAD(ap_conn_producer_active, ap_conn_producer_t) prod_active;
+    APR_RING_HEAD(ap_conn_producer_idle, ap_conn_producer_t) prod_idle;
+
+    struct apr_thread_mutex_t *lock;
+    struct apr_thread_cond_t *prod_done;
+    struct apr_thread_cond_t *all_done;
+};
 
-static void push_slot(h2_slot *volatile *phead, h2_slot *slot)
-{
-    /* Atomically push a slot to the list */
-    ap_assert(!slot->next);
-    for (;;) {
-        h2_slot *next = slot->next = *phead;
-        if (apr_atomic_casptr((void*)phead, slot, next) == next) {
-            return;
-        }
-    }
-}
 
 static void* APR_THREAD_FUNC slot_run(apr_thread_t *thread, void *wctx);
-static void slot_done(h2_slot *slot);
 
-static apr_status_t activate_slot(h2_workers *workers, h2_slot *slot) 
+static apr_status_t activate_slot(h2_workers *workers)
 {
+    h2_slot *slot;
+    apr_pool_t *pool;
     apr_status_t rv;
-    
-    slot->workers = workers;
-    slot->task = NULL;
 
-    apr_thread_mutex_lock(workers->lock);
-    if (!slot->lock) {
-        rv = apr_thread_mutex_create(&slot->lock,
-                                         APR_THREAD_MUTEX_DEFAULT,
-                                         workers->pool);
-        if (rv != APR_SUCCESS) goto cleanup;
+    if (APR_RING_EMPTY(&workers->free, h2_slot, link)) {
+        return APR_EAGAIN;
     }
+    slot = APR_RING_FIRST(&workers->free);
+    ap_assert(slot->state == H2_SLOT_FREE);
+    APR_RING_REMOVE(slot, link);
 
-    if (!slot->not_idle) {
-        rv = apr_thread_cond_create(&slot->not_idle, workers->pool);
-        if (rv != APR_SUCCESS) goto cleanup;
-    }
-    
     ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, workers->s,
-                 "h2_workers: new thread for slot %d", slot->id); 
+                 "h2_workers: activate slot %d", slot->id);
+
+    slot->state = H2_SLOT_RUN;
+    slot->should_shutdown = 0;
+    slot->is_idle = 0;
+    slot->pool = NULL;
+    ++workers->active_slots;
+    rv = apr_pool_create(&pool, workers->pool);
+    if (APR_SUCCESS != rv) goto cleanup;
+    apr_pool_tag(pool, "h2_worker_slot");
+    slot->pool = pool;
 
-    /* thread will either immediately start work or add itself
-     * to the idle queue */
-    apr_atomic_inc32(&workers->worker_count);
-    slot->timed_out = 0;
     rv = ap_thread_create(&slot->thread, workers->thread_attr,
-                          slot_run, slot, workers->pool);
-    if (rv != APR_SUCCESS) {
-        apr_atomic_dec32(&workers->worker_count);
-    }
+                          slot_run, slot, slot->pool);
 
 cleanup:
-    apr_thread_mutex_unlock(workers->lock);
     if (rv != APR_SUCCESS) {
-        push_slot(&workers->free, slot);
-    }
-    return rv;
-}
-
-static apr_status_t add_worker(h2_workers *workers)
-{
-    h2_slot *slot = pop_slot(&workers->free);
-    if (slot) {
-        return activate_slot(workers, slot);
-    }
-    return APR_EAGAIN;
-}
-
-static void wake_idle_worker(h2_workers *workers) 
-{
-    h2_slot *slot = pop_slot(&workers->idle);
-    if (slot) {
-        int timed_out = 0;
-        apr_thread_mutex_lock(slot->lock);
-        timed_out = slot->timed_out;
-        if (!timed_out) {
-            apr_thread_cond_signal(slot->not_idle);
+        AP_DEBUG_ASSERT(0);
+        slot->state = H2_SLOT_FREE;
+        if (slot->pool) {
+            apr_pool_destroy(slot->pool);
+            slot->pool = NULL;
         }
-        apr_thread_mutex_unlock(slot->lock);
-        if (timed_out) {
-            slot_done(slot);
-            wake_idle_worker(workers);
-        }
-    }
-    else if (workers->dynamic && !workers->shutdown) {
-        add_worker(workers);
+        APR_RING_INSERT_TAIL(&workers->free, slot, h2_slot, link);
+        --workers->active_slots;
     }
+    return rv;
 }
 
 static void join_zombies(h2_workers *workers)
 {
     h2_slot *slot;
-    while ((slot = pop_slot(&workers->zombies))) {
-        apr_status_t status;
+    apr_status_t status;
+
+    while (!APR_RING_EMPTY(&workers->zombie, h2_slot, link)) {
+        slot = APR_RING_FIRST(&workers->zombie);
+        APR_RING_REMOVE(slot, link);
+        ap_assert(slot->state == H2_SLOT_ZOMBIE);
         ap_assert(slot->thread != NULL);
+
+        apr_thread_mutex_unlock(workers->lock);
         apr_thread_join(&status, slot->thread);
-        slot->thread = NULL;
+        apr_thread_mutex_lock(workers->lock);
 
-        push_slot(&workers->free, slot);
+        slot->thread = NULL;
+        slot->state = H2_SLOT_FREE;
+        if (slot->pool) {
+            apr_pool_destroy(slot->pool);
+            slot->pool = NULL;
+        }
+        APR_RING_INSERT_TAIL(&workers->free, slot, h2_slot, link);
     }
 }
 
-static apr_status_t slot_pull_task(h2_slot *slot, h2_mplx *m)
+static void wake_idle_worker(h2_workers *workers, ap_conn_producer_t *prod)
 {
-    apr_status_t rv;
-    
-    rv = h2_mplx_s_pop_task(m, &slot->task);
-    if (slot->task) {
-        /* Ok, we got something to give back to the worker for execution. 
-         * If we still have idle workers, we let the worker be sticky, 
-         * e.g. making it poll the task's h2_mplx instance for more work 
-         * before asking back here. */
-        slot->sticks = slot->workers->max_workers;
-        return rv;            
+    if (!APR_RING_EMPTY(&workers->idle, h2_slot, link)) {
+        h2_slot *slot;
+        for (slot = APR_RING_FIRST(&workers->idle);
+             slot != APR_RING_SENTINEL(&workers->idle, h2_slot, link);
+             slot = APR_RING_NEXT(slot, link)) {
+             if (slot->is_idle && !slot->should_shutdown) {
+                apr_thread_cond_signal(slot->more_work);
+                slot->is_idle = 0;
+                return;
+             }
+        }
+    }
+    if (workers->dynamic && !workers->shutdown
+        && (workers->active_slots < workers->max_slots)) {
+        activate_slot(workers);
     }
-    slot->sticks = 0;
-    return APR_EOF;
-}
-
-static h2_fifo_op_t mplx_peek(void *head, void *ctx)
-{
-    h2_mplx *m = head;
-    h2_slot *slot = ctx;
-    
-    if (slot_pull_task(slot, m) == APR_EAGAIN) {
-        wake_idle_worker(slot->workers);
-        return H2_FIFO_OP_REPUSH;
-    } 
-    return H2_FIFO_OP_PULL;
 }
 
 /**
- * Get the next task for the given worker. Will block until a task arrives
- * or the max_wait timer expires and more than min workers exist.
+ * Get the next connection to work on.
  */
-static int get_next(h2_slot *slot)
+static conn_rec *get_next(h2_slot *slot)
 {
     h2_workers *workers = slot->workers;
-    int non_essential = slot->id >= workers->min_workers;
-    apr_status_t rv;
-
-    while (!workers->aborted && !slot->timed_out) {
-        ap_assert(slot->task == NULL);
-        if (non_essential && workers->shutdown) {
-            /* Terminate non-essential worker on shutdown */
-            break;
+    conn_rec *c = NULL;
+    ap_conn_producer_t *prod;
+    int has_more;
+
+    slot->prod = NULL;
+    if (!APR_RING_EMPTY(&workers->prod_active, ap_conn_producer_t, link)) {
+        slot->prod = prod = APR_RING_FIRST(&workers->prod_active);
+        APR_RING_REMOVE(prod, link);
+        AP_DEBUG_ASSERT(PROD_ACTIVE == prod->state);
+
+        c = prod->fn_next(prod->baton, &has_more);
+        if (c && has_more) {
+            APR_RING_INSERT_TAIL(&workers->prod_active, prod, ap_conn_producer_t, link);
+            wake_idle_worker(workers, slot->prod);
         }
-        if (h2_fifo_try_peek(workers->mplxs, mplx_peek, slot) == APR_EOF) {
-            /* The queue is terminated with the MPM child being cleaned up,
-             * just leave. */
-            break;
-        }
-        if (slot->task) {
-            return 1;
+        else {
+            prod->state = PROD_IDLE;
+            APR_RING_INSERT_TAIL(&workers->prod_idle, prod, ap_conn_producer_t, link);
         }
-        
-        join_zombies(workers);
-
-        apr_thread_mutex_lock(slot->lock);
-        if (!workers->aborted) {
-
-            push_slot(&workers->idle, slot);
-            if (non_essential && workers->max_idle_duration) {
-                rv = apr_thread_cond_timedwait(slot->not_idle, slot->lock,
-                                               workers->max_idle_duration);
-                if (APR_TIMEUP == rv) {
-                    slot->timed_out = 1;
-                }
-            }
-            else {
-                apr_thread_cond_wait(slot->not_idle, slot->lock);
-            }
+        if (c) {
+            ++prod->conns_active;
         }
-        apr_thread_mutex_unlock(slot->lock);
     }
 
-    return 0;
+    return c;
 }
 
-static void slot_done(h2_slot *slot)
+static void* APR_THREAD_FUNC slot_run(apr_thread_t *thread, void *wctx)
 {
+    h2_slot *slot = wctx;
     h2_workers *workers = slot->workers;
+    conn_rec *c;
+    apr_status_t rv;
 
-    push_slot(&workers->zombies, slot);
+    apr_thread_mutex_lock(workers->lock);
+    slot->state = H2_SLOT_RUN;
+    ++slot->activations;
+    APR_RING_ELEM_INIT(slot, link);
+    for(;;) {
+        if (APR_RING_NEXT(slot, link) != slot) {
+            /* slot is part of the idle ring from the last loop */
+            APR_RING_REMOVE(slot, link);
+            --workers->idle_slots;
+        }
+        slot->is_idle = 0;
+
+        if (!workers->aborted && !slot->should_shutdown) {
+            APR_RING_INSERT_TAIL(&workers->busy, slot, h2_slot, link);
+            do {
+                c = get_next(slot);
+                if (!c) {
+                    break;
+                }
+                apr_thread_mutex_unlock(workers->lock);
+                /* See the discussion at <https://github.com/icing/mod_h2/issues/195>
+                 *
+                 * Each conn_rec->id is supposed to be unique at a point in time. Since
+                 * some modules (and maybe external code) uses this id as an identifier
+                 * for the request_rec they handle, it needs to be unique for secondary
+                 * connections also.
+                 *
+                 * The MPM module assigns the connection ids and mod_unique_id is using
+                 * that one to generate identifier for requests. While the implementation
+                 * works for HTTP/1.x, the parallel execution of several requests per
+                 * connection will generate duplicate identifiers on load.
+                 *
+                 * The original implementation for secondary connection identifiers used
+                 * to shift the master connection id up and assign the stream id to the
+                 * lower bits. This was cramped on 32 bit systems, but on 64bit there was
+                 * enough space.
+                 *
+                 * As issue 195 showed, mod_unique_id only uses the lower 32 bit of the
+                 * connection id, even on 64bit systems. Therefore collisions in request ids.
+                 *
+                 * The way master connection ids are generated, there is some space "at the
+                 * top" of the lower 32 bits on allmost all systems. If you have a setup
+                 * with 64k threads per child and 255 child processes, you live on the edge.
+                 *
+                 * The new implementation shifts 8 bits and XORs in the worker
+                 * id. This will experience collisions with > 256 h2 workers and heavy
+                 * load still. There seems to be no way to solve this in all possible
+                 * configurations by mod_h2 alone.
+                 */
+                if (c->master) {
+                    c->id = (c->master->id << 8)^slot->id;
+                }
+                c->current_thread = thread;
+                AP_DEBUG_ASSERT(slot->prod);
 
-    /* If this worker is the last one exiting and the MPM child is stopping,
-     * unblock workers_pool_cleanup().
-     */
-    if (!apr_atomic_dec32(&workers->worker_count) && workers->aborted) {
-        apr_thread_mutex_lock(workers->lock);
-        apr_thread_cond_signal(workers->all_done);
-        apr_thread_mutex_unlock(workers->lock);
-    }
-}
+#if AP_HAS_RESPONSE_BUCKETS
+                ap_process_connection(c, ap_get_conn_socket(c));
+#else
+                h2_c2_process(c, thread, slot->id);
+#endif
+                slot->prod->fn_done(slot->prod->baton, c);
+
+                apr_thread_mutex_lock(workers->lock);
+                if (--slot->prod->conns_active <= 0) {
+                    apr_thread_cond_broadcast(workers->prod_done);
+                }
+                if (slot->prod->state == PROD_IDLE) {
+                    APR_RING_REMOVE(slot->prod, link);
+                    slot->prod->state = PROD_ACTIVE;
+                    APR_RING_INSERT_TAIL(&workers->prod_active, slot->prod, ap_conn_producer_t, link);
+                }
 
+            } while (!workers->aborted && !slot->should_shutdown);
+            APR_RING_REMOVE(slot, link); /* no longer busy */
+        }
 
-static void* APR_THREAD_FUNC slot_run(apr_thread_t *thread, void *wctx)
-{
-    h2_slot *slot = wctx;
-    
-    /* Get the h2_task(s) from the ->mplxs queue. */
-    while (get_next(slot)) {
-        ap_assert(slot->task != NULL);
-        do {
-            h2_task_do(slot->task, thread, slot->id);
-            
-            /* Report the task as done. If stickyness is left, offer the
-             * mplx the opportunity to give us back a new task right away.
-             */
-            if (!slot->workers->aborted && --slot->sticks > 0) {
-                h2_mplx_s_task_done(slot->task->mplx, slot->task, &slot->task);
-            }
-            else {
-                h2_mplx_s_task_done(slot->task->mplx, slot->task, NULL);
-                slot->task = NULL;
+        if (workers->aborted || slot->should_shutdown) {
+            break;
+        }
+
+        join_zombies(workers);
+
+        /* we are idle */
+        APR_RING_INSERT_TAIL(&workers->idle, slot, h2_slot, link);
+        ++workers->idle_slots;
+        slot->is_idle = 1;
+        if (slot->id >= workers->min_active && workers->idle_limit > 0) {
+            rv = apr_thread_cond_timedwait(slot->more_work, workers->lock,
+                                           workers->idle_limit);
+            if (APR_TIMEUP == rv) {
+                APR_RING_REMOVE(slot, link);
+                --workers->idle_slots;
+                ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, workers->s,
+                             "h2_workers: idle timeout slot %d in state %d (%d activations)",
+                             slot->id, slot->state, slot->activations);
+                break;
             }
-        } while (slot->task);
+        }
+        else {
+            apr_thread_cond_wait(slot->more_work, workers->lock);
+        }
     }
 
-    if (!slot->timed_out) {
-        slot_done(slot);
+    ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, workers->s,
+                 "h2_workers: terminate slot %d in state %d (%d activations)",
+                 slot->id, slot->state, slot->activations);
+    slot->is_idle = 0;
+    slot->state = H2_SLOT_ZOMBIE;
+    slot->should_shutdown = 0;
+    APR_RING_INSERT_TAIL(&workers->zombie, slot, h2_slot, link);
+    --workers->active_slots;
+    if (workers->active_slots <= 0) {
+        apr_thread_cond_broadcast(workers->all_done);
     }
+    apr_thread_mutex_unlock(workers->lock);
 
     apr_thread_exit(thread, APR_SUCCESS);
     return NULL;
 }
 
-static void wake_non_essential_workers(h2_workers *workers)
-{
-    h2_slot *slot;
-    /* pop all idle, signal the non essentials and add the others again */
-    if ((slot = pop_slot(&workers->idle))) {
-        wake_non_essential_workers(workers);
-        if (slot->id > workers->min_workers) {
-            apr_thread_mutex_lock(slot->lock);
-            apr_thread_cond_signal(slot->not_idle);
-            apr_thread_mutex_unlock(slot->lock);
-        }
-        else {
-            push_slot(&workers->idle, slot);
-        }
-    }
-}
-
-static void workers_abort_idle(h2_workers *workers)
+static void wake_all_idles(h2_workers *workers)
 {
     h2_slot *slot;
-
-    workers->shutdown = 1;
-    workers->aborted = 1;
-    h2_fifo_term(workers->mplxs);
-
-    /* abort all idle slots */
-    while ((slot = pop_slot(&workers->idle))) {
-        apr_thread_mutex_lock(slot->lock);
-        apr_thread_cond_signal(slot->not_idle);
-        apr_thread_mutex_unlock(slot->lock);
+    for (slot = APR_RING_FIRST(&workers->idle);
+         slot != APR_RING_SENTINEL(&workers->idle, h2_slot, link);
+         slot = APR_RING_NEXT(slot, link))
+    {
+        apr_thread_cond_signal(slot->more_work);
     }
 }
 
@@ -321,40 +369,50 @@ static apr_status_t workers_pool_cleanup
     h2_workers *workers = data;
     apr_time_t end, timeout = apr_time_from_sec(1);
     apr_status_t rv;
-    int n, wait_sec = 5;
+    int n = 0, wait_sec = 5;
 
     ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s,
-                 "h2_workers: cleanup %d workers idling",
-                 (int)apr_atomic_read32(&workers->worker_count));
-    workers_abort_idle(workers);
+                 "h2_workers: cleanup %d workers (%d idle)",
+                 workers->active_slots, workers->idle_slots);
+    apr_thread_mutex_lock(workers->lock);
+    workers->shutdown = 1;
+    workers->aborted = 1;
+    wake_all_idles(workers);
+    apr_thread_mutex_unlock(workers->lock);
 
     /* wait for all the workers to become zombies and join them.
      * this gets called after the mpm shuts down and all connections
      * have either been handled (graceful) or we are forced exiting
      * (ungrateful). Either way, we show limited patience. */
-    apr_thread_mutex_lock(workers->lock);
     end = apr_time_now() + apr_time_from_sec(wait_sec);
-    while ((n = apr_atomic_read32(&workers->worker_count)) > 0
-           && apr_time_now() < end) {
+    while (apr_time_now() < end) {
+        apr_thread_mutex_lock(workers->lock);
+        if (!(n = workers->active_slots)) {
+            apr_thread_mutex_unlock(workers->lock);
+            break;
+        }
+        wake_all_idles(workers);
         rv = apr_thread_cond_timedwait(workers->all_done, workers->lock, timeout);
+        apr_thread_mutex_unlock(workers->lock);
+
         if (APR_TIMEUP == rv) {
             ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s,
-                         APLOGNO(10290) "h2_workers: waiting for idle workers to close, "
-                         "still seeing %d workers living",
-                         apr_atomic_read32(&workers->worker_count));
-            continue;
+                         APLOGNO(10290) "h2_workers: waiting for workers to close, "
+                         "still seeing %d workers (%d idle) living",
+                         workers->active_slots, workers->idle_slots);
         }
     }
     if (n) {
         ap_log_error(APLOG_MARK, APLOG_WARNING, 0, workers->s,
-                     APLOGNO(10291) "h2_workers: cleanup, %d idle workers "
+                     APLOGNO(10291) "h2_workers: cleanup, %d workers (%d idle) "
                      "did not exit after %d seconds.",
-                     n, wait_sec);
+                     n, workers->idle_slots, wait_sec);
     }
-    apr_thread_mutex_unlock(workers->lock);
     ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s,
                  "h2_workers: cleanup all workers terminated");
+    apr_thread_mutex_lock(workers->lock);
     join_zombies(workers);
+    apr_thread_mutex_unlock(workers->lock);
     ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s,
                  "h2_workers: cleanup zombie workers joined");
 
@@ -362,23 +420,35 @@ static apr_status_t workers_pool_cleanup
 }
 
 h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pchild,
-                              int min_workers, int max_workers,
-                              int idle_secs)
+                              int max_slots, int min_active,
+                              apr_time_t idle_limit)
 {
     apr_status_t rv;
     h2_workers *workers;
     apr_pool_t *pool;
-    int i, n;
+    apr_allocator_t *allocator;
+    int locked = 0;
+    apr_uint32_t i;
 
     ap_assert(s);
     ap_assert(pchild);
+    ap_assert(idle_limit > 0);
 
     /* let's have our own pool that will be parent to all h2_worker
      * instances we create. This happens in various threads, but always
      * guarded by our lock. Without this pool, all subpool creations would
      * happen on the pool handed to us, which we do not guard.
      */
-    apr_pool_create(&pool, pchild);
+    rv = apr_allocator_create(&allocator);
+    if (rv != APR_SUCCESS) {
+        goto cleanup;
+    }
+    rv = apr_pool_create_ex(&pool, pchild, NULL, allocator);
+    if (rv != APR_SUCCESS) {
+        apr_allocator_destroy(allocator);
+        goto cleanup;
+    }
+    apr_allocator_owner_set(allocator, pool);
     apr_pool_tag(pool, "h2_workers");
     workers = apr_pcalloc(pool, sizeof(h2_workers));
     if (!workers) {
@@ -387,27 +457,23 @@ h2_workers *h2_workers_create(server_rec
     
     workers->s = s;
     workers->pool = pool;
-    workers->min_workers = min_workers;
-    workers->max_workers = max_workers;
-    workers->max_idle_duration = apr_time_from_sec((idle_secs > 0)? idle_secs : 10);
+    workers->min_active = min_active;
+    workers->max_slots = max_slots;
+    workers->idle_limit = idle_limit;
+    workers->dynamic = (workers->min_active < workers->max_slots);
+
+    ap_log_error(APLOG_MARK, APLOG_INFO, 0, s,
+                 "h2_workers: created with min=%d max=%d idle_ms=%d",
+                 workers->min_active, workers->max_slots,
+                 (int)apr_time_as_msec(idle_limit));
+
+    APR_RING_INIT(&workers->idle, h2_slot, link);
+    APR_RING_INIT(&workers->busy, h2_slot, link);
+    APR_RING_INIT(&workers->free, h2_slot, link);
+    APR_RING_INIT(&workers->zombie, h2_slot, link);
 
-    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s,
-                 "h2_workers: created with min=%d max=%d idle_timeout=%d sec",
-                 workers->min_workers, workers->max_workers,
-                 (int)apr_time_sec(workers->max_idle_duration));
-    /* FIXME: the fifo set we use here has limited capacity. Once the
-     * set is full, connections with new requests do a wait. Unfortunately,
-     * we have optimizations in place there that makes such waiting "unfair"
-     * in the sense that it may take connections a looong time to get scheduled.
-     *
-     * Need to rewrite this to use one of our double-linked lists and a mutex
-     * to have unlimited capacity and fair scheduling.
-     *
-     * For now, we just make enough room to have many connections inside one
-     * process.
-     */
-    rv = h2_fifo_set_create(&workers->mplxs, pool, 8 * 1024);
-    if (rv != APR_SUCCESS) goto cleanup;
+    APR_RING_INIT(&workers->prod_active, ap_conn_producer_t, link);
+    APR_RING_INIT(&workers->prod_idle, ap_conn_producer_t, link);
 
     rv = apr_threadattr_create(&workers->thread_attr, workers->pool);
     if (rv != APR_SUCCESS) goto cleanup;
@@ -426,32 +492,35 @@ h2_workers *h2_workers_create(server_rec
     if (rv != APR_SUCCESS) goto cleanup;
     rv = apr_thread_cond_create(&workers->all_done, workers->pool);
     if (rv != APR_SUCCESS) goto cleanup;
+    rv = apr_thread_cond_create(&workers->prod_done, workers->pool);
+    if (rv != APR_SUCCESS) goto cleanup;
 
-    n = workers->nslots = workers->max_workers;
-    workers->slots = apr_pcalloc(workers->pool, n * sizeof(h2_slot));
-    if (workers->slots == NULL) {
-        n = workers->nslots = 0;
-        rv = APR_ENOMEM;
-        goto cleanup;
-    }
-    for (i = 0; i < n; ++i) {
+    apr_thread_mutex_lock(workers->lock);
+    locked = 1;
+
+    /* create the slots and put them on the free list */
+    workers->slots = apr_pcalloc(workers->pool, workers->max_slots * sizeof(h2_slot));
+
+    for (i = 0; i < workers->max_slots; ++i) {
         workers->slots[i].id = i;
-    }
-    /* we activate all for now, TODO: support min_workers again.
-     * do this in reverse for vanity reasons so slot 0 will most
-     * likely be at head of idle queue. */
-    n = workers->min_workers;
-    for (i = n-1; i >= 0; --i) {
-        rv = activate_slot(workers, &workers->slots[i]);
+        workers->slots[i].state = H2_SLOT_FREE;
+        workers->slots[i].workers = workers;
+        APR_RING_ELEM_INIT(&workers->slots[i], link);
+        APR_RING_INSERT_TAIL(&workers->free, &workers->slots[i], h2_slot, link);
+        rv = apr_thread_cond_create(&workers->slots[i].more_work, workers->pool);
         if (rv != APR_SUCCESS) goto cleanup;
     }
-    /* the rest of the slots go on the free list */
-    for(i = n; i < workers->nslots; ++i) {
-        push_slot(&workers->free, &workers->slots[i]);
+
+    /* activate the min amount of workers */
+    for (i = 0; i < workers->min_active; ++i) {
+        rv = activate_slot(workers);
+        if (rv != APR_SUCCESS) goto cleanup;
     }
-    workers->dynamic = (workers->worker_count < workers->max_workers);
 
 cleanup:
+    if (locked) {
+        apr_thread_mutex_unlock(workers->lock);
+    }
     if (rv == APR_SUCCESS) {
         /* Stop/join the workers threads when the MPM child exits (pchild is
          * destroyed), and as a pre_cleanup of pchild thus before the threads
@@ -461,24 +530,97 @@ cleanup:
         apr_pool_pre_cleanup_register(pchild, workers, workers_pool_cleanup);    
         return workers;
     }
+    ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s,
+                 "h2_workers: errors initializing");
     return NULL;
 }
 
-apr_status_t h2_workers_register(h2_workers *workers, struct h2_mplx *m)
+apr_uint32_t h2_workers_get_max_workers(h2_workers *workers)
+{
+    return workers->max_slots;
+}
+
+void h2_workers_shutdown(h2_workers *workers, int graceful)
 {
-    apr_status_t status = h2_fifo_push(workers->mplxs, m);
-    wake_idle_worker(workers);
-    return status;
+    ap_conn_producer_t *prod;
+
+    apr_thread_mutex_lock(workers->lock);
+    ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, workers->s,
+                 "h2_workers: shutdown graceful=%d", graceful);
+    workers->shutdown = 1;
+    workers->idle_limit = apr_time_from_sec(1);
+    wake_all_idles(workers);
+    for (prod = APR_RING_FIRST(&workers->prod_idle);
+        prod != APR_RING_SENTINEL(&workers->prod_idle, ap_conn_producer_t, link);
+        prod = APR_RING_NEXT(prod, link)) {
+        if (prod->fn_shutdown) {
+            prod->fn_shutdown(prod->baton, graceful);
+        }
+    }
+    apr_thread_mutex_unlock(workers->lock);
 }
 
-apr_status_t h2_workers_unregister(h2_workers *workers, struct h2_mplx *m)
+ap_conn_producer_t *h2_workers_register(h2_workers *workers,
+                                        apr_pool_t *producer_pool,
+                                        const char *name,
+                                        ap_conn_producer_next *fn_next,
+                                        ap_conn_producer_done *fn_done,
+                                        ap_conn_producer_shutdown *fn_shutdown,
+                                        void *baton)
+{
+    ap_conn_producer_t *prod;
+
+    prod = apr_pcalloc(producer_pool, sizeof(*prod));
+    APR_RING_ELEM_INIT(prod, link);
+    prod->name = name;
+    prod->fn_next = fn_next;
+    prod->fn_done = fn_done;
+    prod->fn_shutdown = fn_shutdown;
+    prod->baton = baton;
+
+    apr_thread_mutex_lock(workers->lock);
+    prod->state = PROD_IDLE;
+    APR_RING_INSERT_TAIL(&workers->prod_idle, prod, ap_conn_producer_t, link);
+    apr_thread_mutex_unlock(workers->lock);
+
+    return prod;
+}
+
+apr_status_t h2_workers_join(h2_workers *workers, ap_conn_producer_t *prod)
 {
-    return h2_fifo_remove(workers->mplxs, m);
+    apr_status_t rv = APR_SUCCESS;
+
+    apr_thread_mutex_lock(workers->lock);
+    if (PROD_JOINED == prod->state) {
+        AP_DEBUG_ASSERT(APR_RING_NEXT(prod, link) == prod); /* should be in no ring */
+        rv = APR_EINVAL;
+    }
+    else {
+        AP_DEBUG_ASSERT(PROD_ACTIVE == prod->state || PROD_IDLE == prod->state);
+        APR_RING_REMOVE(prod, link);
+        prod->state = PROD_JOINED; /* prevent further activations */
+        while (prod->conns_active > 0) {
+            apr_thread_cond_wait(workers->prod_done, workers->lock);
+        }
+        APR_RING_ELEM_INIT(prod, link); /* make it link to itself */
+    }
+    apr_thread_mutex_unlock(workers->lock);
+    return rv;
 }
 
-void h2_workers_graceful_shutdown(h2_workers *workers)
+apr_status_t h2_workers_activate(h2_workers *workers, ap_conn_producer_t *prod)
 {
-    workers->shutdown = 1;
-    workers->max_idle_duration = apr_time_from_sec(1);
-    wake_non_essential_workers(workers);
+    apr_status_t rv = APR_SUCCESS;
+    apr_thread_mutex_lock(workers->lock);
+    if (PROD_IDLE == prod->state) {
+        APR_RING_REMOVE(prod, link);
+        prod->state = PROD_ACTIVE;
+        APR_RING_INSERT_TAIL(&workers->prod_active, prod, ap_conn_producer_t, link);
+        wake_idle_worker(workers, prod);
+    }
+    else if (PROD_JOINED == prod->state) {
+        rv = APR_EINVAL;
+    }
+    apr_thread_mutex_unlock(workers->lock);
+    return rv;
 }

Modified: httpd/httpd/branches/2.4.x/modules/http2/h2_workers.h
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/modules/http2/h2_workers.h?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/modules/http2/h2_workers.h (original)
+++ httpd/httpd/branches/2.4.x/modules/http2/h2_workers.h Mon Jan  9 07:35:18 2023
@@ -17,73 +17,113 @@
 #ifndef __mod_h2__h2_workers__
 #define __mod_h2__h2_workers__
 
-/* Thread pool specific to executing h2_tasks. Has a minimum and maximum 
- * number of workers it creates. Starts with minimum workers and adds
- * some on load, reduces the number again when idle.
- *
+/* Thread pool specific to executing secondary connections.
+ * Has a minimum and maximum number of workers it creates.
+ * Starts with minimum workers and adds some on load,
+ * reduces the number again when idle.
  */
 struct apr_thread_mutex_t;
 struct apr_thread_cond_t;
 struct h2_mplx;
 struct h2_request;
-struct h2_task;
 struct h2_fifo;
 
-struct h2_slot;
-
 typedef struct h2_workers h2_workers;
 
-struct h2_workers {
-    server_rec *s;
-    apr_pool_t *pool;
-    
-    int next_worker_id;
-    apr_uint32_t max_workers;
-    volatile apr_uint32_t min_workers; /* is changed during graceful shutdown */
-    volatile apr_interval_time_t max_idle_duration; /* is changed during graceful shutdown */
-    
-    volatile int aborted;
-    volatile int shutdown;
-    int dynamic;
-
-    apr_threadattr_t *thread_attr;
-    int nslots;
-    struct h2_slot *slots;
-    
-    volatile apr_uint32_t worker_count;
-    
-    struct h2_slot *free;
-    struct h2_slot *idle;
-    struct h2_slot *zombies;
-    
-    struct h2_fifo *mplxs;
-    
-    struct apr_thread_mutex_t *lock;
-    struct apr_thread_cond_t *all_done;
-};
 
-
-/* Create a worker pool with the given minimum and maximum number of
- * threads.
+/**
+ * Create a worker set with a maximum number of 'slots', e.g. worker
+ * threads to run. Always keep `min_active` workers running. Shutdown
+ * any additional workers after `idle_secs` seconds of doing nothing.
+ *
+ * @oaram s the base server
+ * @param pool for allocations
+ * @param min_active minimum number of workers to run
+ * @param max_slots maximum number of worker slots
+ * @param idle_limit upper duration of idle after a non-minimal slots shuts down
  */
 h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pool,
-                              int min_size, int max_size, int idle_secs);
+                              int max_slots, int min_active, apr_time_t idle_limit);
+
+/**
+ *  Shut down processing.
+ */
+void h2_workers_shutdown(h2_workers *workers, int graceful);
+
+/**
+ * Get the maximum number of workers.
+ */
+apr_uint32_t h2_workers_get_max_workers(h2_workers *workers);
+
+/**
+ * ap_conn_producer_t is the source of connections (conn_rec*) to run.
+ *
+ * Active producers are queried by idle workers for connections.
+ * If they do not hand one back, they become inactive and are not
+ * queried further. `h2_workers_activate()` places them on the active
+ * list again.
+ *
+ * A producer finishing MUST call `h2_workers_join()` which removes
+ * it completely from workers processing and waits for all ongoing
+ * work for this producer to be done.
+ */
+typedef struct ap_conn_producer_t ap_conn_producer_t;
+
+/**
+ * Ask a producer for the next connection to process.
+ * @param baton value from producer registration
+ * @param pconn holds the connection to process on return
+ * @param pmore if the producer has more connections that may be retrieved
+ * @return APR_SUCCESS for a connection to process, APR_EAGAIN for no
+ *         connection being available at the time.
+ */
+typedef conn_rec *ap_conn_producer_next(void *baton, int *pmore);
+
+/**
+ * Tell the producer that processing the connection is done.
+ * @param baton value from producer registration
+ * @param conn the connection that has been processed.
+ */
+typedef void ap_conn_producer_done(void *baton, conn_rec *conn);
+
+/**
+ * Tell the producer that the workers are shutting down.
+ * @param baton value from producer registration
+ * @param graceful != 0 iff shutdown is graceful
+ */
+typedef void ap_conn_producer_shutdown(void *baton, int graceful);
 
 /**
- * Registers a h2_mplx for task scheduling. If this h2_mplx runs
- * out of tasks, it will be automatically be unregistered. Should
- * new tasks arrive, it needs to be registered again.
+ * Register a new producer with the given `baton` and callback functions.
+ * Will allocate internal structures from the given pool (but make no use
+ * of the pool after registration).
+ * Producers are inactive on registration. See `h2_workers_activate()`.
+ * @param producer_pool to allocate the producer from
+ * @param name descriptive name of the producer, must not be unique
+ * @param fn_next callback for retrieving connections to process
+ * @param fn_done callback for processed connections
+ * @param baton provided value passed on in callbacks
+ * @return the producer instance created
  */
-apr_status_t h2_workers_register(h2_workers *workers, struct h2_mplx *m);
+ap_conn_producer_t *h2_workers_register(h2_workers *workers,
+                                        apr_pool_t *producer_pool,
+                                        const char *name,
+                                        ap_conn_producer_next *fn_next,
+                                        ap_conn_producer_done *fn_done,
+                                        ap_conn_producer_shutdown *fn_shutdown,
+                                        void *baton);
 
 /**
- * Remove a h2_mplx from the worker registry.
+ * Stop retrieving more connection from the producer and wait
+ * for all ongoing for from that producer to be done.
  */
-apr_status_t h2_workers_unregister(h2_workers *workers, struct h2_mplx *m);
+apr_status_t h2_workers_join(h2_workers *workers, ap_conn_producer_t *producer);
 
 /**
- *  Shut down processing gracefully by terminating all idle workers.
+ * Activate a producer. A worker will query the producer for a connection
+ * to process, once a worker is available.
+ * This may be called, irregardless of the producers active/inactive.
  */
-void h2_workers_graceful_shutdown(h2_workers *workers);
+apr_status_t h2_workers_activate(h2_workers *workers, ap_conn_producer_t *producer);
 
 #endif /* defined(__mod_h2__h2_workers__) */

Modified: httpd/httpd/branches/2.4.x/modules/http2/mod_http2.c
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/modules/http2/mod_http2.c?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/modules/http2/mod_http2.c (original)
+++ httpd/httpd/branches/2.4.x/modules/http2/mod_http2.c Mon Jan  9 07:35:18 2023
@@ -30,19 +30,18 @@
 
 #include <nghttp2/nghttp2.h>
 #include "h2_stream.h"
-#include "h2_alt_svc.h"
-#include "h2_conn.h"
-#include "h2_filter.h"
-#include "h2_task.h"
+#include "h2_c1.h"
+#include "h2_c2.h"
 #include "h2_session.h"
 #include "h2_config.h"
-#include "h2_ctx.h"
-#include "h2_h2.h"
+#include "h2_conn_ctx.h"
+#include "h2_protocol.h"
 #include "h2_mplx.h"
 #include "h2_push.h"
 #include "h2_request.h"
 #include "h2_switch.h"
 #include "h2_version.h"
+#include "h2_bucket_beam.h"
 
 
 static void h2_hooks(apr_pool_t *pool);
@@ -126,27 +125,6 @@ static int h2_post_config(apr_pool_t *p,
                  myfeats.dyn_windows? "+DWINS"  : "",
                  ngh2?                ngh2->version_str : "unknown");
     
-    switch (h2_conn_mpm_type()) {
-        case H2_MPM_SIMPLE:
-        case H2_MPM_MOTORZ:
-        case H2_MPM_NETWARE:
-        case H2_MPM_WINNT:
-            /* not sure we need something extra for those. */
-            break;
-        case H2_MPM_EVENT:
-        case H2_MPM_WORKER:
-            /* all fine, we know these ones */
-            break;
-        case H2_MPM_PREFORK:
-            /* ok, we now know how to handle that one */
-            break;
-        case H2_MPM_UNKNOWN:
-            /* ??? */
-            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(03091)
-                         "post_config: mpm type unknown");
-            break;
-    }
-    
     if (!h2_mpm_supported() && !mpm_warned) {
         mpm_warned = 1;
         ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, APLOGNO(10034)
@@ -158,14 +136,11 @@ static int h2_post_config(apr_pool_t *p,
                      h2_conn_mpm_name());
     }
     
-    status = h2_h2_init(p, s);
+    status = h2_protocol_init(p, s);
     if (status == APR_SUCCESS) {
         status = h2_switch_init(p, s);
     }
-    if (status == APR_SUCCESS) {
-        status = h2_task_init(p, s);
-    }
-    
+
     return status;
 }
 
@@ -175,7 +150,9 @@ static int http2_is_h2(conn_rec *);
 
 static void http2_get_num_workers(server_rec *s, int *minw, int *maxw)
 {
-    h2_get_num_workers(s, minw, maxw);
+    apr_time_t tdummy;
+
+    h2_get_workers_config(s, minw, maxw, &tdummy);
 }
 
 /* Runs once per created child process. Perform any process 
@@ -183,29 +160,15 @@ static void http2_get_num_workers(server
  */
 static void h2_child_init(apr_pool_t *pchild, server_rec *s)
 {
-    apr_allocator_t *allocator;
-    apr_thread_mutex_t *mutex;
-    apr_status_t status;
-
-    /* The allocator of pchild has no mutex with MPM prefork, but we need one
-     * for h2 workers threads synchronization. Even though mod_http2 shouldn't
-     * be used with prefork, better be safe than sorry, so forcibly set the
-     * mutex here. For MPM event/worker, pchild has no allocator so pconf's
-     * is used, with its mutex.
-     */
-    allocator = apr_pool_allocator_get(pchild);
-    if (allocator) {
-        mutex = apr_allocator_mutex_get(allocator);
-        if (!mutex) {
-            apr_thread_mutex_create(&mutex, APR_THREAD_MUTEX_DEFAULT, pchild);
-            apr_allocator_mutex_set(allocator, mutex);
-        }
-    }
+    apr_status_t rv;
 
     /* Set up our connection processing */
-    status = h2_conn_child_init(pchild, s);
-    if (status != APR_SUCCESS) {
-        ap_log_error(APLOG_MARK, APLOG_ERR, status, s,
+    rv = h2_c1_child_init(pchild, s);
+    if (APR_SUCCESS == rv) {
+        rv = h2_c2_child_init(pchild, s);
+    }
+    if (APR_SUCCESS != rv) {
+        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s,
                      APLOGNO(02949) "initializing connection handling");
     }
 }
@@ -230,41 +193,38 @@ static void h2_hooks(apr_pool_t *pool)
      */
     ap_hook_child_init(h2_child_init, NULL, NULL, APR_HOOK_MIDDLE);
 #if AP_MODULE_MAGIC_AT_LEAST(20120211, 110)
-    ap_hook_child_stopping(h2_conn_child_stopping, NULL, NULL, APR_HOOK_MIDDLE);
+    ap_hook_child_stopping(h2_c1_child_stopping, NULL, NULL, APR_HOOK_MIDDLE);
 #endif
-    h2_h2_register_hooks();
+
+    h2_c1_register_hooks();
     h2_switch_register_hooks();
-    h2_task_register_hooks();
+    h2_c2_register_hooks();
 
-    h2_alt_svc_register_hooks();
-    
-    /* Setup subprocess env for certain variables 
+    /* Setup subprocess env for certain variables
      */
     ap_hook_fixups(h2_h2_fixups, NULL,NULL, APR_HOOK_MIDDLE);
-    
-    /* test http2 connection status handler */
-    ap_hook_handler(h2_filter_h2_status_handler, NULL, NULL, APR_HOOK_MIDDLE);
 }
 
 static const char *val_HTTP2(apr_pool_t *p, server_rec *s,
-                             conn_rec *c, request_rec *r, h2_ctx *ctx)
+                             conn_rec *c, request_rec *r, h2_conn_ctx_t *ctx)
 {
     return ctx? "on" : "off";
 }
 
 static const char *val_H2_PUSH(apr_pool_t *p, server_rec *s,
-                               conn_rec *c, request_rec *r, h2_ctx *ctx)
+                               conn_rec *c, request_rec *r,
+                               h2_conn_ctx_t *conn_ctx)
 {
-    if (ctx) {
+    if (conn_ctx) {
         if (r) {
-            if (ctx->task) {
-                h2_stream *stream = h2_mplx_t_stream_get(ctx->task->mplx, ctx->task);
+            if (conn_ctx->stream_id) {
+                const h2_stream *stream = h2_mplx_c2_stream_get(conn_ctx->mplx, conn_ctx->stream_id);
                 if (stream && stream->push_policy != H2_PUSH_NONE) {
                     return "on";
                 }
             }
         }
-        else if (c && h2_session_push_enabled(ctx->session)) {
+        else if (c && h2_session_push_enabled(conn_ctx->session)) {
             return "on";
         }
     }
@@ -277,10 +237,11 @@ static const char *val_H2_PUSH(apr_pool_
 }
 
 static const char *val_H2_PUSHED(apr_pool_t *p, server_rec *s,
-                                 conn_rec *c, request_rec *r, h2_ctx *ctx)
+                                 conn_rec *c, request_rec *r,
+                                 h2_conn_ctx_t *conn_ctx)
 {
-    if (ctx) {
-        if (ctx->task && !H2_STREAM_CLIENT_INITIATED(ctx->task->stream_id)) {
+    if (conn_ctx) {
+        if (conn_ctx->stream_id && !H2_STREAM_CLIENT_INITIATED(conn_ctx->stream_id)) {
             return "PUSHED";
         }
     }
@@ -288,11 +249,12 @@ static const char *val_H2_PUSHED(apr_poo
 }
 
 static const char *val_H2_PUSHED_ON(apr_pool_t *p, server_rec *s,
-                                    conn_rec *c, request_rec *r, h2_ctx *ctx)
+                                    conn_rec *c, request_rec *r,
+                                    h2_conn_ctx_t *conn_ctx)
 {
-    if (ctx) {
-        if (ctx->task && !H2_STREAM_CLIENT_INITIATED(ctx->task->stream_id)) {
-            h2_stream *stream = h2_mplx_t_stream_get(ctx->task->mplx, ctx->task);
+    if (conn_ctx) {
+        if (conn_ctx->stream_id && !H2_STREAM_CLIENT_INITIATED(conn_ctx->stream_id)) {
+            const h2_stream *stream = h2_mplx_c2_stream_get(conn_ctx->mplx, conn_ctx->stream_id);
             if (stream) {
                 return apr_itoa(p, stream->initiated_on);
             }
@@ -302,28 +264,30 @@ static const char *val_H2_PUSHED_ON(apr_
 }
 
 static const char *val_H2_STREAM_TAG(apr_pool_t *p, server_rec *s,
-                                     conn_rec *c, request_rec *r, h2_ctx *ctx)
+                                     conn_rec *c, request_rec *r, h2_conn_ctx_t *ctx)
 {
-    if (ctx) {
-        if (ctx->task) {
-            return ctx->task->id;
+    if (c) {
+        h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c);
+        if (conn_ctx) {
+            return conn_ctx->stream_id == 0? conn_ctx->id
+               : apr_psprintf(p, "%s-%d", conn_ctx->id, conn_ctx->stream_id);
         }
     }
     return "";
 }
 
 static const char *val_H2_STREAM_ID(apr_pool_t *p, server_rec *s,
-                                    conn_rec *c, request_rec *r, h2_ctx *ctx)
+                                    conn_rec *c, request_rec *r, h2_conn_ctx_t *ctx)
 {
     const char *cp = val_H2_STREAM_TAG(p, s, c, r, ctx);
-    if (cp && (cp = ap_strchr_c(cp, '-'))) {
+    if (cp && (cp = ap_strrchr_c(cp, '-'))) {
         return ++cp;
     }
     return NULL;
 }
 
 typedef const char *h2_var_lookup(apr_pool_t *p, server_rec *s,
-                                  conn_rec *c, request_rec *r, h2_ctx *ctx);
+                                  conn_rec *c, request_rec *r, h2_conn_ctx_t *ctx);
 typedef struct h2_var_def {
     const char *name;
     h2_var_lookup *lookup;
@@ -347,19 +311,19 @@ static h2_var_def H2_VARS[] = {
 
 static int http2_is_h2(conn_rec *c)
 {
-    return h2_ctx_get(c->master? c->master : c, 0) != NULL;
+    return h2_conn_ctx_get(c->master? c->master : c) != NULL;
 }
 
 static char *http2_var_lookup(apr_pool_t *p, server_rec *s,
                               conn_rec *c, request_rec *r, char *name)
 {
-    int i;
+    unsigned int i;
     /* If the # of vars grow, we need to put definitions in a hash */
     for (i = 0; i < H2_ALEN(H2_VARS); ++i) {
         h2_var_def *vdef = &H2_VARS[i];
         if (!strcmp(vdef->name, name)) {
-            h2_ctx *ctx = (r? h2_ctx_get(c, 0) : 
-                           h2_ctx_get(c->master? c->master : c, 0));
+            h2_conn_ctx_t *ctx = (r? h2_conn_ctx_get(c) :
+                           h2_conn_ctx_get(c->master? c->master : c));
             return (char *)vdef->lookup(p, s, c, r, ctx);
         }
     }
@@ -369,9 +333,9 @@ static char *http2_var_lookup(apr_pool_t
 static int h2_h2_fixups(request_rec *r)
 {
     if (r->connection->master) {
-        h2_ctx *ctx = h2_ctx_get(r->connection, 0);
-        int i;
-        
+        h2_conn_ctx_t *ctx = h2_conn_ctx_get(r->connection);
+        unsigned int i;
+
         for (i = 0; ctx && i < H2_ALEN(H2_VARS); ++i) {
             h2_var_def *vdef = &H2_VARS[i];
             if (vdef->subprocess) {

Modified: httpd/httpd/branches/2.4.x/modules/http2/mod_http2.dsp
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/modules/http2/mod_http2.dsp?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/modules/http2/mod_http2.dsp (original)
+++ httpd/httpd/branches/2.4.x/modules/http2/mod_http2.dsp Mon Jan  9 07:35:18 2023
@@ -101,10 +101,6 @@ PostBuild_Cmds=if exist $(TargetPath).ma
 # Name "mod_http2 - Win32 Debug"
 # Begin Source File
 
-SOURCE=./h2_alt_svc.c
-# End Source File
-# Begin Source File
-
 SOURCE=./h2_bucket_beam.c
 # End Source File
 # Begin Source File
@@ -113,31 +109,31 @@ SOURCE=./h2_bucket_eos.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_config.c
+SOURCE=./h2_c1.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_conn.c
+SOURCE=./h2_c1_io.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_conn_io.c
+SOURCE=./h2_c2.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_ctx.c
+SOURCE=./h2_c2_filter.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_filter.c
+SOURCE=./h2_config.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_from_h1.c
+SOURCE=./h2_conn_ctx.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_h2.c
+SOURCE=./h2_headers.c
 # End Source File
 # Begin Source File
 
@@ -145,15 +141,15 @@ SOURCE=./h2_mplx.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_push.c
+SOURCE=./h2_protocol.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_request.c
+SOURCE=./h2_push.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_headers.c
+SOURCE=./h2_request.c
 # End Source File
 # Begin Source File
 
@@ -169,15 +165,15 @@ SOURCE=./h2_switch.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_task.c
+SOURCE=./h2_util.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_util.c
+SOURCE=./h2_workers.c
 # End Source File
 # Begin Source File
 
-SOURCE=./h2_workers.c
+SOURCE=./mod_http2.c
 # End Source File
 # Begin Source File
 

Modified: httpd/httpd/branches/2.4.x/modules/http2/mod_http2.h
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/modules/http2/mod_http2.h?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/modules/http2/mod_http2.h (original)
+++ httpd/httpd/branches/2.4.x/modules/http2/mod_http2.h Mon Jan  9 07:35:18 2023
@@ -28,6 +28,9 @@ APR_DECLARE_OPTIONAL_FN(char *,
 APR_DECLARE_OPTIONAL_FN(int, 
                         http2_is_h2, (conn_rec *));
 
+APR_DECLARE_OPTIONAL_FN(void,
+                        http2_get_num_workers, (server_rec *s,
+                                                int *minw, int *max));
 
 /*******************************************************************************
  * START HTTP/2 request engines (DEPRECATED)
@@ -68,9 +71,6 @@ APR_DECLARE_OPTIONAL_FN(void,
                                                 conn_rec *rconn,
                                                 apr_status_t status));
 
-APR_DECLARE_OPTIONAL_FN(void,
-                        http2_get_num_workers, (server_rec *s,
-                                                int *minw, int *max));
 
 /*******************************************************************************
  * END HTTP/2 request engines (DEPRECATED)

Modified: httpd/httpd/branches/2.4.x/modules/http2/mod_proxy_http2.c
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/modules/http2/mod_proxy_http2.c?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/modules/http2/mod_proxy_http2.c (original)
+++ httpd/httpd/branches/2.4.x/modules/http2/mod_proxy_http2.c Mon Jan  9 07:35:18 2023
@@ -249,7 +249,7 @@ static apr_status_t ctx_run(h2_proxy_ctx
     ctx->r_done = 0;
     add_request(ctx->session, ctx->r);
     
-    while (!ctx->master->aborted && !ctx->r_done) {
+    while (!ctx->owner->aborted && !ctx->r_done) {
     
         status = h2_proxy_session_process(ctx->session);
         if (status != APR_SUCCESS) {
@@ -267,16 +267,13 @@ static apr_status_t ctx_run(h2_proxy_ctx
     }
     
 out:
-    if (ctx->master->aborted) {
+    if (ctx->owner->aborted) {
         /* master connection gone */
         ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->owner, 
                       APLOGNO(03374) "eng(%s): master connection gone", ctx->id);
         /* cancel all ongoing requests */
         h2_proxy_session_cancel_all(ctx->session);
         h2_proxy_session_process(ctx->session);
-        if (!ctx->master->aborted) {
-            status = ctx->r_status = APR_SUCCESS;
-        }
     }
     
     ctx->session->user_data = NULL;
@@ -291,7 +288,7 @@ static int proxy_http2_handler(request_r
                                const char *proxyname,
                                apr_port_t proxyport)
 {
-    const char *proxy_func, *task_id;
+    const char *proxy_func;
     char *locurl = url, *u;
     apr_size_t slen;
     int is_ssl = 0;
@@ -324,11 +321,9 @@ static int proxy_http2_handler(request_r
             return DECLINED;
     }
 
-    task_id = apr_table_get(r->connection->notes, H2_TASK_ID_NOTE);
-    
     ctx = apr_pcalloc(r->pool, sizeof(*ctx));
     ctx->master = r->connection->master? r->connection->master : r->connection;
-    ctx->id = task_id? task_id : apr_psprintf(r->pool, "%ld", (long)ctx->master->id);
+    ctx->id = apr_psprintf(r->pool, "%ld", (long)ctx->master->id);
     ctx->owner = r->connection;
     ctx->pool = r->pool;
     ctx->server = r->server;
@@ -350,7 +345,7 @@ static int proxy_http2_handler(request_r
                   "H2: serving URL %s", url);
     
 run_connect:    
-    if (ctx->master->aborted) goto cleanup;
+    if (ctx->owner->aborted) goto cleanup;
 
     /* Get a proxy_conn_rec from the worker, might be a new one, might
      * be one still open from another request, or it might fail if the
@@ -402,10 +397,10 @@ run_connect:
                        "proxy-request-alpn-protos", "h2");
     }
 
-    if (ctx->master->aborted) goto cleanup;
+    if (ctx->owner->aborted) goto cleanup;
     status = ctx_run(ctx);
 
-    if (ctx->r_status != APR_SUCCESS && ctx->r_may_retry && !ctx->master->aborted) {
+    if (ctx->r_status != APR_SUCCESS && ctx->r_may_retry && !ctx->owner->aborted) {
         /* Not successfully processed, but may retry, tear down old conn and start over */
         if (ctx->p_conn) {
             ctx->p_conn->close = 1;

Modified: httpd/httpd/branches/2.4.x/test/conftest.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/conftest.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/conftest.py (original)
+++ httpd/httpd/branches/2.4.x/test/conftest.py Mon Jan  9 07:35:18 2023
@@ -1,6 +1,8 @@
 import sys
 import os
 
+import pytest
+
 sys.path.append(os.path.join(os.path.dirname(__file__), '.'))
 
 from pyhttpd.env import HttpdTestEnv
@@ -9,4 +11,21 @@ def pytest_report_header(config, startdi
     env = HttpdTestEnv()
     return f"[apache httpd: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]"
 
+def pytest_addoption(parser):
+    parser.addoption("--repeat", action="store", type=int, default=1,
+                     help='Number of times to repeat each test')
+    parser.addoption("--all", action="store_true")
+
+
+def pytest_generate_tests(metafunc):
+    if "repeat" in metafunc.fixturenames:
+        count = int(metafunc.config.getoption("repeat"))
+        metafunc.fixturenames.append('tmp_ct')
+        metafunc.parametrize('repeat', range(count))
+
+@pytest.fixture(autouse=True, scope="function")
+def _function_scope(env, request):
+    env.set_current_test_name(request.node.name)
+    yield
+    env.set_current_test_name(None)
 

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/conftest.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/conftest.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/conftest.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/conftest.py Mon Jan  9 07:35:18 2023
@@ -14,19 +14,6 @@ def pytest_report_header(config, startdi
     return f"mod_h2 [apache: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]"
 
 
-def pytest_addoption(parser):
-    parser.addoption("--repeat", action="store", type=int, default=1,
-                     help='Number of times to repeat each test')
-    parser.addoption("--all", action="store_true")
-
-
-def pytest_generate_tests(metafunc):
-    if "repeat" in metafunc.fixturenames:
-        count = int(metafunc.config.getoption("repeat"))
-        metafunc.fixturenames.append('tmp_ct')
-        metafunc.parametrize('repeat', range(count))
-
-
 @pytest.fixture(scope="package")
 def env(pytestconfig) -> H2TestEnv:
     level = logging.INFO

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/env.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/env.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/env.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/env.py Mon Jan  9 07:35:18 2023
@@ -57,14 +57,20 @@ class H2TestSetup(HttpdTestSetup):
 
 class H2TestEnv(HttpdTestEnv):
 
+    @classmethod
+    @property
+    def is_unsupported(cls):
+        mpm_module = f"mpm_{os.environ['MPM']}" if 'MPM' in os.environ else 'mpm_event'
+        return mpm_module == 'mpm_prefork'
+
     def __init__(self, pytestconfig=None):
         super().__init__(pytestconfig=pytestconfig)
         self.add_httpd_conf([
-                             "H2MinWorkers 1",
-                             "H2MaxWorkers 64",
-                             "Protocols h2 http/1.1 h2c",
-                         ])
-        self.add_httpd_log_modules(["http2", "proxy_http2", "h2test"])
+            "H2MinWorkers 1",
+            "H2MaxWorkers 64",
+            "Protocols h2 http/1.1 h2c",
+        ])
+        self.add_httpd_log_modules(["http2", "proxy_http2", "h2test", "proxy", "proxy_http"])
         self.add_cert_specs([
             CertificateSpec(domains=[
                 f"push.{self._http_tld}",
@@ -86,7 +92,12 @@ class H2TestEnv(HttpdTestEnv):
             'AH00135',
             'AH02261',  # Re-negotiation handshake failed (our test_101)
             'AH03490',  # scoreboard full, happens on limit tests
-            'AH01247',  # cgi reports sometimes error on accept on cgid socket
+            'AH02429',  # invalid chars in response header names, see test_h2_200
+            'AH02430',  # invalid chars in response header values, see test_h2_200
+            'AH10373',  # SSL errors on uncompleted handshakes, see test_h2_105
+            'AH01247',  # mod_cgid sometimes freaks out on load tests
+            'AH01110',  # error by proxy reading response
+            'AH10400',  # warning that 'enablereuse' has not effect in certain configs test_h2_600
         ])
         self.httpd_error_log.add_ignored_patterns([
             re.compile(r'.*malformed header from script \'hecho.py\': Bad header: x.*'),
@@ -112,6 +123,15 @@ class H2Conf(HttpdConf):
             f"cgi.{env.http_tld}": [
                 "SSLOptions +StdEnvVars",
                 "AddHandler cgi-script .py",
+                "<Location \"/h2test/echo\">",
+                "    SetHandler h2test-echo",
+                "</Location>",
+                "<Location \"/h2test/delay\">",
+                "    SetHandler h2test-delay",
+                "</Location>",
+                "<Location \"/h2test/error\">",
+                "    SetHandler h2test-error",
+                "</Location>",
             ]
         }))
 

Added: httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/alive.json
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/alive.json?rev=1906475&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/alive.json (added)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/alive.json Mon Jan  9 07:35:18 2023
@@ -0,0 +1,4 @@
+{
+    "host" : "cgi",
+    "alive" : true
+}

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/hello.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/hello.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/hello.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/hello.py Mon Jan  9 07:35:18 2023
@@ -6,11 +6,15 @@ print("Content-Type: application/json")
 print()
 print("{")
 print("  \"https\" : \"%s\"," % (os.getenv('HTTPS', '')))
-print("  \"host\" : \"%s\"," % (os.getenv('SERVER_NAME', '')))
+print("  \"host\" : \"%s\"," % (os.getenv('X_HOST', '') \
+    if 'X_HOST' in os.environ else os.getenv('SERVER_NAME', '')))
+print("  \"server\" : \"%s\"," % (os.getenv('SERVER_NAME', '')))
+print("  \"h2_original_host\" : \"%s\"," % (os.getenv('H2_ORIGINAL_HOST', '')))
 print("  \"port\" : \"%s\"," % (os.getenv('SERVER_PORT', '')))
 print("  \"protocol\" : \"%s\"," % (os.getenv('SERVER_PROTOCOL', '')))
 print("  \"ssl_protocol\" : \"%s\"," % (os.getenv('SSL_PROTOCOL', '')))
 print("  \"h2\" : \"%s\"," % (os.getenv('HTTP2', '')))
-print("  \"h2push\" : \"%s\"" % (os.getenv('H2PUSH', '')))
+print("  \"h2push\" : \"%s\"," % (os.getenv('H2PUSH', '')))
+print("  \"h2_stream_id\" : \"%s\"" % (os.getenv('H2_STREAM_ID', '')))
 print("}")
 

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/mnot164.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/mnot164.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/mnot164.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/htdocs/cgi/mnot164.py Mon Jan  9 07:35:18 2023
@@ -12,10 +12,13 @@ try:
 except KeyError:
     text="a"
     count=77784
-    
-    
+
+count = int(count)
+
 print("Status: 200 OK")
 print("Content-Type: text/html")
 print()
-sys.stdout.write(text*int(count))
+sys.stdout.flush()
+for _ in range(count):
+    sys.stdout.write(text)
 

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/mod_h2test/mod_h2test.c
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/mod_h2test/mod_h2test.c?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/mod_h2test/mod_h2test.c (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/mod_h2test/mod_h2test.c Mon Jan  9 07:35:18 2023
@@ -280,7 +280,7 @@ static int h2test_delay_handler(request_
 
 cleanup:
     ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r,
-                  "delay_handler: request cleanup, r->status=%d, aborted=%d",
+                  "delay_handler: request cleanup, r->status=%d, aborte=%d",
                   r->status, c->aborted);
     if (rv == APR_SUCCESS
         || r->status != HTTP_OK
@@ -297,9 +297,8 @@ static int h2test_trailer_handler(reques
     apr_bucket *b;
     apr_status_t rv;
     char buffer[8192];
-    int i, chunks = 3;
     long l;
-    int body_len = 0, tclen;
+    int body_len = 0;
 
     if (strcmp(r->handler, "h2test-trailer")) {
         return DECLINED;
@@ -309,7 +308,7 @@ static int h2test_trailer_handler(reques
     }
 
     if (r->args) {
-        tclen = body_len = (int)apr_atoi64(r->args);
+        body_len = (int)apr_atoi64(r->args);
         if (body_len < 0) body_len = 0;
     }
 
@@ -320,6 +319,10 @@ static int h2test_trailer_handler(reques
     ap_set_content_length(r, body_len);
 
     ap_set_content_type(r, "application/octet-stream");
+    apr_table_mergen(r->headers_out, "Trailer", "trailer-content-length");
+    apr_table_set(r->trailers_out, "trailer-content-length",
+                  apr_psprintf(r->pool, "%d", body_len));
+
     bb = apr_brigade_create(r->pool, c->bucket_alloc);
     memset(buffer, 0, sizeof(buffer));
     while (body_len > 0) {
@@ -332,16 +335,6 @@ static int h2test_trailer_handler(reques
         ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
                       "trailer_handler: passed %ld bytes as response body", l);
     }
-    /* flush it */
-    b = apr_bucket_flush_create(c->bucket_alloc);
-    APR_BRIGADE_INSERT_TAIL(bb, b);
-    rv = ap_pass_brigade(r->output_filters, bb);
-    apr_brigade_cleanup(bb);
-    apr_sleep(apr_time_from_msec(500));
-    /* set trailers */
-    apr_table_mergen(r->headers_out, "Trailer", "trailer-content-length");
-    apr_table_set(r->trailers_out, "trailer-content-length",
-                  apr_psprintf(r->pool, "%d", tclen));
     /* we are done */
     b = apr_bucket_eos_create(c->bucket_alloc);
     APR_BRIGADE_INSERT_TAIL(bb, b);
@@ -351,7 +344,7 @@ static int h2test_trailer_handler(reques
 
 cleanup:
     ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r,
-                  "trailer_handler: request cleanup, r->status=%d, aborted=%d",
+                  "trailer_handler: request cleanup, r->status=%d, aborte=%d",
                   r->status, c->aborted);
     if (rv == APR_SUCCESS
         || r->status != HTTP_OK
@@ -361,6 +354,154 @@ cleanup:
     return AP_FILTER_ERROR;
 }
 
+static int status_from_str(const char *s, apr_status_t *pstatus)
+{
+    if (!strcmp("timeout", s)) {
+        *pstatus = APR_TIMEUP;
+        return 1;
+    }
+    else if (!strcmp("reset", s)) {
+        *pstatus = APR_ECONNRESET;
+        return 1;
+    }
+    return 0;
+}
+
+static int h2test_error_handler(request_rec *r)
+{
+    conn_rec *c = r->connection;
+    apr_bucket_brigade *bb;
+    apr_bucket *b;
+    apr_status_t rv;
+    char buffer[8192];
+    int i, chunks = 3, error_bucket = 1;
+    long l;
+    apr_time_t delay = 0, body_delay = 0;
+    apr_array_header_t *args = NULL;
+    int http_status = 200;
+    apr_status_t error = APR_SUCCESS, body_error = APR_SUCCESS;
+
+    if (strcmp(r->handler, "h2test-error")) {
+        return DECLINED;
+    }
+    if (r->method_number != M_GET && r->method_number != M_POST) {
+        return DECLINED;
+    }
+
+    if (r->args) {
+        args = apr_cstr_split(r->args, "&", 1, r->pool);
+        for (i = 0; i < args->nelts; ++i) {
+            char *s, *val, *arg = APR_ARRAY_IDX(args, i, char*);
+            s = strchr(arg, '=');
+            if (s) {
+                *s = '\0';
+                val = s + 1;
+                if (!strcmp("status", arg)) {
+                    http_status = (int)apr_atoi64(val);
+                    if (val > 0) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("error", arg)) {
+                    if (status_from_str(val, &error)) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("error_bucket", arg)) {
+                    error_bucket = (int)apr_atoi64(val);
+                    if (val >= 0) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("body_error", arg)) {
+                    if (status_from_str(val, &body_error)) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("delay", arg)) {
+                    rv = duration_parse(&delay, r->args, "s");
+                    if (APR_SUCCESS == rv) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("body_delay", arg)) {
+                    rv = duration_parse(&body_delay, r->args, "s");
+                    if (APR_SUCCESS == rv) {
+                        continue;
+                    }
+                }
+            }
+            ap_die(HTTP_BAD_REQUEST, r);
+            return OK;
+        }
+    }
+
+    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "error_handler: processing request, %s",
+                  r->args? r->args : "(no args)");
+    r->status = http_status;
+    r->clength = -1;
+    r->chunked = 1;
+    apr_table_unset(r->headers_out, "Content-Length");
+    /* Discourage content-encodings */
+    apr_table_unset(r->headers_out, "Content-Encoding");
+    apr_table_setn(r->subprocess_env, "no-brotli", "1");
+    apr_table_setn(r->subprocess_env, "no-gzip", "1");
+
+    ap_set_content_type(r, "application/octet-stream");
+    bb = apr_brigade_create(r->pool, c->bucket_alloc);
+
+    if (delay) {
+        apr_sleep(delay);
+    }
+    if (error != APR_SUCCESS) {
+        return ap_map_http_request_error(error, HTTP_BAD_REQUEST);
+    }
+    /* flush response */
+    b = apr_bucket_flush_create(c->bucket_alloc);
+    APR_BRIGADE_INSERT_TAIL(bb, b);
+    rv = ap_pass_brigade(r->output_filters, bb);
+    if (APR_SUCCESS != rv) goto cleanup;
+
+    memset(buffer, 'X', sizeof(buffer));
+    l = sizeof(buffer);
+    for (i = 0; i < chunks; ++i) {
+        if (body_delay) {
+            apr_sleep(body_delay);
+        }
+        rv = apr_brigade_write(bb, NULL, NULL, buffer, l);
+        if (APR_SUCCESS != rv) goto cleanup;
+        rv = ap_pass_brigade(r->output_filters, bb);
+        if (APR_SUCCESS != rv) goto cleanup;
+        ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
+                      "error_handler: passed %ld bytes as response body", l);
+        if (body_error != APR_SUCCESS) {
+            rv = body_error;
+            goto cleanup;
+        }
+    }
+    /* we are done */
+    b = apr_bucket_eos_create(c->bucket_alloc);
+    APR_BRIGADE_INSERT_TAIL(bb, b);
+    rv = ap_pass_brigade(r->output_filters, bb);
+    apr_brigade_cleanup(bb);
+    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "error_handler: response passed");
+
+cleanup:
+    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r,
+                  "error_handler: request cleanup, r->status=%d, aborted=%d",
+                  r->status, c->aborted);
+    if (rv == APR_SUCCESS) {
+        return OK;
+    }
+    if (error_bucket) {
+        http_status = ap_map_http_request_error(rv, HTTP_BAD_REQUEST);
+        b = ap_bucket_error_create(http_status, NULL, r->pool, c->bucket_alloc);
+        APR_BRIGADE_INSERT_TAIL(bb, b);
+        ap_pass_brigade(r->output_filters, bb);
+    }
+    return AP_FILTER_ERROR;
+}
+
 /* Install this module into the apache2 infrastructure.
  */
 static void h2test_hooks(apr_pool_t *pool)
@@ -381,5 +522,6 @@ static void h2test_hooks(apr_pool_t *poo
     ap_hook_handler(h2test_echo_handler, NULL, NULL, APR_HOOK_MIDDLE);
     ap_hook_handler(h2test_delay_handler, NULL, NULL, APR_HOOK_MIDDLE);
     ap_hook_handler(h2test_trailer_handler, NULL, NULL, APR_HOOK_MIDDLE);
+    ap_hook_handler(h2test_error_handler, NULL, NULL, APR_HOOK_MIDDLE);
 }
 

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_001_httpd_alive.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_001_httpd_alive.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_001_httpd_alive.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_001_httpd_alive.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestBasicAlive:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_002_curl_basics.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_002_curl_basics.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_002_curl_basics.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_002_curl_basics.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestCurlBasics:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_003_get.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_003_get.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_003_get.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_003_get.py Mon Jan  9 07:35:18 2023
@@ -1,9 +1,10 @@
 import re
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestGet:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -211,3 +212,54 @@ content-type: text/html
         assert 1024 == len(r.response["body"])
         assert "content-length" in h
         assert clen == h["content-length"]
+
+    # use an invalid scheme
+    def test_h2_003_51(self, env):
+        url = env.mkurl("https", "cgi", "/")
+        opt = ["-H:scheme: invalid"]
+        r = env.nghttp().get(url, options=opt)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 400
+
+    # use an differing scheme, but one that is acceptable
+    def test_h2_003_52(self, env):
+        url = env.mkurl("https", "cgi", "/")
+        opt = ["-H:scheme: http"]
+        r = env.nghttp().get(url, options=opt)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 200
+
+    # Test that we get a proper `Date` and `Server` headers on responses
+    def test_h2_003_60(self, env):
+        url = env.mkurl("https", "test1", "/index.html")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 200
+        assert 'date' in r.response['header']
+        assert 'server' in r.response['header']
+
+    # lets do some error tests
+    def test_h2_003_70(self, env):
+        url = env.mkurl("https", "cgi", "/h2test/error?status=500")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 500
+        url = env.mkurl("https", "cgi", "/h2test/error?error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 408
+
+    # produce an error during response body
+    def test_h2_003_71(self, env, repeat):
+        url = env.mkurl("https", "cgi", "/h2test/error?body_error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, f"{r}"
+        url = env.mkurl("https", "cgi", "/h2test/error?body_error=reset")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, f"{r}"
+
+    # produce an error, fail to generate an error bucket
+    def test_h2_003_72(self, env, repeat):
+        url = env.mkurl("https", "cgi", "/h2test/error?body_error=timeout&error_bucket=0")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, f"{r}"

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_004_post.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_004_post.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_004_post.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_004_post.py Mon Jan  9 07:35:18 2023
@@ -8,9 +8,10 @@ import sys
 
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestPost:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -68,7 +69,7 @@ class TestPost:
         ("H2_PUSHED", ""),
         ("H2_PUSHED_ON", ""),
         ("H2_STREAM_ID", "1"),
-        ("H2_STREAM_TAG", r'\d+-1'),
+        ("H2_STREAM_TAG", r'\d+-\d+-1'),
     ])
     def test_h2_004_07(self, env, name, value):
         url = env.mkurl("https", "cgi", "/env.py")

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_005_files.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_005_files.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_005_files.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_005_files.py Mon Jan  9 07:35:18 2023
@@ -1,7 +1,7 @@
 import os
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
 def mk_text_file(fpath: str, lines: int):
@@ -15,6 +15,7 @@ def mk_text_file(fpath: str, lines: int)
             fd.write("\n")
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestFiles:
 
     URI_PATHS = []

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_006_assets.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_006_assets.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_006_assets.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_006_assets.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestAssets:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_100_conn_reuse.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_100_conn_reuse.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_100_conn_reuse.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_100_conn_reuse.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestConnReuse:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -26,7 +27,7 @@ class TestConnReuse:
     def test_h2_100_02(self, env):
         url = env.mkurl("https", "cgi", "/hello.py")
         hostname = ("cgi-alias.%s" % env.http_tld)
-        r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ])
+        r = env.curl_get(url, 5, options=["-H", f"Host: {hostname}"])
         assert r.response["status"] == 200
         assert "HTTP/2" == r.response["protocol"]
         assert hostname == r.response["json"]["host"]

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_101_ssl_reneg.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_101_ssl_reneg.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_101_ssl_reneg.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_101_ssl_reneg.py Mon Jan  9 07:35:18 2023
@@ -4,6 +4,7 @@ import pytest
 from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 @pytest.mark.skipif(H2TestEnv.get_ssl_module() != "mod_ssl", reason="only for mod_ssl")
 class TestSslRenegotiation:
 

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_102_require.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_102_require.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_102_require.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_102_require.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestRequire:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_103_upgrade.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_103_upgrade.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_103_upgrade.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_103_upgrade.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestUpgrade:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_104_padding.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_104_padding.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_104_padding.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_104_padding.py Mon Jan  9 07:35:18 2023
@@ -1,6 +1,6 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
 def frame_padding(payload, padbits):
@@ -8,6 +8,7 @@ def frame_padding(payload, padbits):
     return ((payload + 9 + mask) & ~mask) - (payload + 9)
         
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestPadding:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_105_timeout.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_105_timeout.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_105_timeout.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_105_timeout.py Mon Jan  9 07:35:18 2023
@@ -111,40 +111,39 @@ class TestTimeout:
         assert piper.exitcode == 0
         assert len("".join(stdout)) == 3 * 8192
 
-    @pytest.mark.skip(reason="only in 2.5.x")
     def test_h2_105_11(self, env):
         # short connection timeout, longer stream delay
-        # receiving the first response chunk, then timeout
+        # connection timeout must not abort ongoing streams
         conf = H2Conf(env)
         conf.add_vhost_cgi()
         conf.add("Timeout 1")
         conf.install()
         assert env.apache_restart() == 0
-        url = env.mkurl("https", "cgi", "/h2test/delay?5")
+        url = env.mkurl("https", "cgi", "/h2test/delay?1200ms")
         piper = CurlPiper(env=env, url=url)
         piper.start()
         stdout, stderr = piper.close()
-        assert len("".join(stdout)) == 8192
+        assert len("".join(stdout)) == 3 * 8192
 
-    @pytest.mark.skip(reason="only in 2.5.x")
     def test_h2_105_12(self, env):
         # long connection timeout, short stream timeout
         # sending a slow POST
-        conf = H2Conf(env)
-        conf.add_vhost_cgi()
-        conf.add("Timeout 10")
-        conf.add("H2StreamTimeout 1")
-        conf.install()
-        assert env.apache_restart() == 0
-        url = env.mkurl("https", "cgi", "/h2test/delay?5")
-        piper = CurlPiper(env=env, url=url)
-        piper.start()
-        for _ in range(3):
-            time.sleep(2)
-            try:
-                piper.send("0123456789\n")
-            except BrokenPipeError:
-                break
-        piper.close()
-        assert piper.response
-        assert piper.response['status'] == 408
+        if env.httpd_is_at_least("2.5.0"):
+            conf = H2Conf(env)
+            conf.add_vhost_cgi()
+            conf.add("Timeout 10")
+            conf.add("H2StreamTimeout 1")
+            conf.install()
+            assert env.apache_restart() == 0
+            url = env.mkurl("https", "cgi", "/h2test/delay?5")
+            piper = CurlPiper(env=env, url=url)
+            piper.start()
+            for _ in range(3):
+                time.sleep(2)
+                try:
+                    piper.send("0123456789\n")
+                except BrokenPipeError:
+                    break
+            piper.close()
+            assert piper.response
+            assert piper.response['status'] == 408, f"{piper.response}"

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_106_shutdown.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_106_shutdown.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_106_shutdown.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_106_shutdown.py Mon Jan  9 07:35:18 2023
@@ -7,10 +7,11 @@ from threading import Thread
 
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 from pyhttpd.result import ExecResult
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestShutdown:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_200_header_invalid.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_200_header_invalid.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_200_header_invalid.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_200_header_invalid.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestInvalidHeaders:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -86,7 +87,8 @@ class TestInvalidHeaders:
     def test_h2_200_12(self, env):
         url = env.mkurl("https", "cgi", "/")
         opt = []
-        for i in range(98):  # curl sends 2 headers itself (user-agent and accept)
+        # curl sends 3 headers itself (user-agent, accept, and our AP-Test-Name)
+        for i in range(97):
             opt += ["-H", "x: 1"]
         r = env.curl_get(url, options=opt)
         assert r.response["status"] == 200
@@ -98,8 +100,9 @@ class TestInvalidHeaders:
     def test_h2_200_13(self, env):
         url = env.mkurl("https", "cgi", "/")
         opt = []
-        for i in range(98):  # curl sends 2 headers itself (user-agent and accept)
-            opt += ["-H", "x{0}: 1".format(i)]
+        # curl sends 3 headers itself (user-agent, accept, and our AP-Test-Name)
+        for i in range(97):
+            opt += ["-H", f"x{i}: 1"]
         r = env.curl_get(url, options=opt)
         assert r.response["status"] == 200
         r = env.curl_get(url, options=(opt + ["-H", "y: 2"]))