diff options
-rw-r--r-- | changes-entries/h2_workers_dynamic.txt | 13 | ||||
-rw-r--r-- | modules/http2/h2_mplx.c | 32 | ||||
-rw-r--r-- | modules/http2/h2_request.c | 105 | ||||
-rw-r--r-- | modules/http2/h2_session.c | 1 | ||||
-rw-r--r-- | modules/http2/h2_task.c | 2 | ||||
-rw-r--r-- | modules/http2/h2_version.h | 4 | ||||
-rw-r--r-- | modules/http2/h2_workers.c | 190 | ||||
-rw-r--r-- | modules/http2/h2_workers.h | 8 | ||||
-rw-r--r-- | modules/http2/mod_proxy_http2.c | 11 |
9 files changed, 282 insertions, 84 deletions
diff --git a/changes-entries/h2_workers_dynamic.txt b/changes-entries/h2_workers_dynamic.txt new file mode 100644 index 0000000000..1d3115c724 --- /dev/null +++ b/changes-entries/h2_workers_dynamic.txt @@ -0,0 +1,13 @@ + *) mod_http2: + - Aborting requests via RST_STREAM no longer affect the available + resources of a connection when the first chunk of the response + body has been sent. + - H2Min/MaxWorkers behave as intended again. The module will initially + create H2MinWorkers threads and add up to H2MaxWorkers when needed. These + additional workers time out when idle after H2MaxWorkerIdleSeconds and + disappear again. + - When the shutdown of a child is detected (e.g. graceful shutdown), the + module will terminate all idle workers above H2MinWorkers right away. + This detection currently only happens when a HTTP/2 connection is active. + [Stefan Eissing] + diff --git a/modules/http2/h2_mplx.c b/modules/http2/h2_mplx.c index d787e0d09d..7ab5ec9300 100644 --- a/modules/http2/h2_mplx.c +++ b/modules/http2/h2_mplx.c @@ -739,6 +739,12 @@ static h2_task *s_next_stream_task(h2_mplx *m) return stream->task; } } + if (m->tasks_active >= m->limit_active && !h2_iq_empty(m->q)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c, + "h2_session(%ld): delaying request processing. " + "Current limit is %d and %d workers are in use.", + m->id, m->limit_active, m->tasks_active); + } return NULL; } @@ -1132,14 +1138,36 @@ int h2_mplx_m_awaits_data(h2_mplx *m) return waiting; } +static int reset_is_acceptable(h2_stream *stream) +{ + /* client may terminate a stream via H2 RST_STREAM message at any time. + * This is annyoing when we have committed resources (e.g. worker threads) + * to it, so our mood (e.g. willingness to commit resources on this + * connection in the future) goes down. + * + * This is a DoS protection. We do not want to make it too easy for + * a client to eat up server resources. + * + * However: there are cases where a RST_STREAM is the only way to end + * a request. This includes websockets and server-side-event streams (SSEs). + * The responses to such requests continue forever otherwise. + * + */ + if (!stream->task) return 1; /* have not started or already ended for us. acceptable. */ + if (!(stream->id & 0x01)) return 1; /* stream initiated by us. acceptable. */ + if (!stream->has_response) return 0; /* no response headers produced yet. bad. */ + if (!stream->out_data_frames) return 0; /* no response body data sent yet. bad. */ + return 1; /* otherwise, be forgiving */ +} + apr_status_t h2_mplx_m_client_rst(h2_mplx *m, int stream_id) { h2_stream *stream; apr_status_t status = APR_SUCCESS; - + H2_MPLX_ENTER_ALWAYS(m); stream = h2_ihash_get(m->streams, stream_id); - if (stream && stream->task) { + if (stream && !reset_is_acceptable(stream)) { status = m_be_annoyed(m); } H2_MPLX_LEAVE(m); diff --git a/modules/http2/h2_request.c b/modules/http2/h2_request.c index 5adf84151e..7c4fb95ea4 100644 --- a/modules/http2/h2_request.c +++ b/modules/http2/h2_request.c @@ -210,12 +210,74 @@ h2_request *h2_request_clone(apr_pool_t *p, const h2_request *src) return dst; } +#if !AP_MODULE_MAGIC_AT_LEAST(20120211, 106) +static request_rec *my_ap_create_request(conn_rec *c) +{ + apr_pool_t *p; + request_rec *r; + + apr_pool_create(&p, c->pool); + apr_pool_tag(p, "request"); + r = apr_pcalloc(p, sizeof(request_rec)); + AP_READ_REQUEST_ENTRY((intptr_t)r, (uintptr_t)c); + r->pool = p; + r->connection = c; + r->server = c->base_server; + + r->user = NULL; + r->ap_auth_type = NULL; + + r->allowed_methods = ap_make_method_list(p, 2); + + r->headers_in = apr_table_make(r->pool, 5); + r->trailers_in = apr_table_make(r->pool, 5); + r->subprocess_env = apr_table_make(r->pool, 25); + r->headers_out = apr_table_make(r->pool, 12); + r->err_headers_out = apr_table_make(r->pool, 5); + r->trailers_out = apr_table_make(r->pool, 5); + r->notes = apr_table_make(r->pool, 5); + + r->request_config = ap_create_request_config(r->pool); + /* Must be set before we run create request hook */ + + r->proto_output_filters = c->output_filters; + r->output_filters = r->proto_output_filters; + r->proto_input_filters = c->input_filters; + r->input_filters = r->proto_input_filters; + ap_run_create_request(r); + r->per_dir_config = r->server->lookup_defaults; + + r->sent_bodyct = 0; /* bytect isn't for body */ + + r->read_length = 0; + r->read_body = REQUEST_NO_BODY; + + r->status = HTTP_OK; /* Until further notice */ + r->header_only = 0; + r->the_request = NULL; + + /* Begin by presuming any module can make its own path_info assumptions, + * until some module interjects and changes the value. + */ + r->used_path_info = AP_REQ_DEFAULT_PATH_INFO; + + r->useragent_addr = c->client_addr; + r->useragent_ip = c->client_ip; + return r; +} +#endif + request_rec *h2_request_create_rec(const h2_request *req, conn_rec *c) { int access_status = HTTP_OK; +#if AP_MODULE_MAGIC_AT_LEAST(20120211, 106) request_rec *r = ap_create_request(c); +#else + request_rec *r = my_ap_create_request(c); +#endif +#if AP_MODULE_MAGIC_AT_LEAST(20120211, 107) ap_run_pre_read_request(r, c); /* Time to populate r with the data we have. */ @@ -244,6 +306,49 @@ request_rec *h2_request_create_rec(const h2_request *req, conn_rec *c) r->status = HTTP_OK; goto die; } +#else + { + const char *s; + + r->headers_in = apr_table_clone(r->pool, req->headers); + ap_run_pre_read_request(r, c); + + /* Time to populate r with the data we have. */ + r->request_time = req->request_time; + r->method = apr_pstrdup(r->pool, req->method); + /* Provide quick information about the request method as soon as known */ + r->method_number = ap_method_number_of(r->method); + if (r->method_number == M_GET && r->method[0] == 'H') { + r->header_only = 1; + } + ap_parse_uri(r, req->path ? req->path : ""); + r->protocol = (char*)"HTTP/2.0"; + r->proto_num = HTTP_VERSION(2, 0); + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + r->method, req->path ? req->path : ""); + + /* Start with r->hostname = NULL, ap_check_request_header() will get it + * form Host: header, otherwise we get complains about port numbers. + */ + r->hostname = NULL; + ap_update_vhost_from_headers(r); + + /* we may have switched to another server */ + r->per_dir_config = r->server->lookup_defaults; + + s = apr_table_get(r->headers_in, "Expect"); + if (s && s[0]) { + if (ap_cstr_casecmp(s, "100-continue") == 0) { + r->expecting_100 = 1; + } + else { + r->status = HTTP_EXPECTATION_FAILED; + access_status = r->status; + goto die; + } + } + } +#endif /* we may have switched to another server */ r->per_dir_config = r->server->lookup_defaults; diff --git a/modules/http2/h2_session.c b/modules/http2/h2_session.c index 1915855792..4cc2c96be3 100644 --- a/modules/http2/h2_session.c +++ b/modules/http2/h2_session.c @@ -1908,6 +1908,7 @@ static void h2_session_ev_mpm_stopping(h2_session *session, int arg, const char break; default: h2_session_shutdown_notice(session); + h2_workers_graceful_shutdown(session->workers); break; } } diff --git a/modules/http2/h2_task.c b/modules/http2/h2_task.c index 399c40b60f..5b32656a91 100644 --- a/modules/http2/h2_task.c +++ b/modules/http2/h2_task.c @@ -593,7 +593,7 @@ apr_status_t h2_task_do(h2_task *task, apr_thread_t *thread, int worker_id) * configurations by mod_h2 alone. */ task->c->id = (c->master->id << 8)^worker_id; - task->id = apr_psprintf(task->pool, "%ld-%d", c->master->id, + task->id = apr_psprintf(task->pool, "%ld-%d", task->mplx->id, task->stream_id); } diff --git a/modules/http2/h2_version.h b/modules/http2/h2_version.h index c8b1106439..38a2190444 100644 --- a/modules/http2/h2_version.h +++ b/modules/http2/h2_version.h @@ -27,7 +27,7 @@ * @macro * Version number of the http2 module as c string */ -#define MOD_HTTP2_VERSION "1.15.18" +#define MOD_HTTP2_VERSION "1.15.21" /** * @macro @@ -35,7 +35,7 @@ * release. This is a 24 bit number with 8 bits for major number, 8 bits * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. */ -#define MOD_HTTP2_VERSION_NUM 0x010f12 +#define MOD_HTTP2_VERSION_NUM 0x010f15 #endif /* mod_h2_h2_version_h */ diff --git a/modules/http2/h2_workers.c b/modules/http2/h2_workers.c index bb64a1a506..7832ac0e38 100644 --- a/modules/http2/h2_workers.c +++ b/modules/http2/h2_workers.c @@ -41,6 +41,7 @@ struct h2_slot { apr_thread_t *thread; apr_thread_mutex_t *lock; apr_thread_cond_t *not_idle; + volatile apr_uint32_t timed_out; }; static h2_slot *pop_slot(h2_slot *volatile *phead) @@ -71,47 +72,47 @@ static void push_slot(h2_slot *volatile *phead, h2_slot *slot) } 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) { - apr_status_t status; + apr_status_t rv; slot->workers = workers; slot->task = NULL; + apr_thread_mutex_lock(workers->lock); if (!slot->lock) { - status = apr_thread_mutex_create(&slot->lock, + rv = apr_thread_mutex_create(&slot->lock, APR_THREAD_MUTEX_DEFAULT, workers->pool); - if (status != APR_SUCCESS) { - push_slot(&workers->free, slot); - return status; - } + if (rv != APR_SUCCESS) goto cleanup; } if (!slot->not_idle) { - status = apr_thread_cond_create(&slot->not_idle, workers->pool); - if (status != APR_SUCCESS) { - push_slot(&workers->free, slot); - return status; - } + rv = apr_thread_cond_create(&slot->not_idle, workers->pool); + if (rv != APR_SUCCESS) goto cleanup; } - ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, workers->s, + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, workers->s, "h2_workers: new thread for slot %d", slot->id); /* thread will either immediately start work or add itself * to the idle queue */ apr_atomic_inc32(&workers->worker_count); - status = apr_thread_create(&slot->thread, workers->thread_attr, + slot->timed_out = 0; + rv = apr_thread_create(&slot->thread, workers->thread_attr, slot_run, slot, workers->pool); - if (status != APR_SUCCESS) { + if (rv != APR_SUCCESS) { apr_atomic_dec32(&workers->worker_count); + } + +cleanup: + apr_thread_mutex_unlock(workers->lock); + if (rv != APR_SUCCESS) { push_slot(&workers->free, slot); - return status; } - - return APR_SUCCESS; + return rv; } static apr_status_t add_worker(h2_workers *workers) @@ -127,11 +128,19 @@ 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); - apr_thread_cond_signal(slot->not_idle); + timed_out = slot->timed_out; + if (!timed_out) { + apr_thread_cond_signal(slot->not_idle); + } apr_thread_mutex_unlock(slot->lock); + if (timed_out) { + slot_done(slot); + wake_idle_worker(workers); + } } - else if (workers->dynamic) { + else if (workers->dynamic && !workers->shutdown) { add_worker(workers); } } @@ -185,13 +194,18 @@ static h2_fifo_op_t mplx_peek(void *head, void *ctx) static int 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) { + while (!workers->aborted && !slot->timed_out) { ap_assert(slot->task == NULL); + if (non_essential && workers->shutdown) { + /* Terminate non-essential worker on shutdown */ + break; + } 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. - */ + * just leave. */ break; } if (slot->task) { @@ -202,8 +216,18 @@ static int get_next(h2_slot *slot) apr_thread_mutex_lock(slot->lock); if (!workers->aborted) { + push_slot(&workers->idle, slot); - apr_thread_cond_wait(slot->not_idle, slot->lock); + 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); + } } apr_thread_mutex_unlock(slot->lock); } @@ -251,17 +275,36 @@ static void* APR_THREAD_FUNC slot_run(apr_thread_t *thread, void *wctx) } while (slot->task); } - slot_done(slot); + if (!slot->timed_out) { + slot_done(slot); + } apr_thread_exit(thread, APR_SUCCESS); return NULL; } -static apr_status_t workers_pool_cleanup(void *data) +static void wake_non_essential_workers(h2_workers *workers) { - h2_workers *workers = data; 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) +{ + h2_slot *slot; + + workers->shutdown = 1; workers->aborted = 1; h2_fifo_term(workers->mplxs); @@ -271,6 +314,13 @@ static apr_status_t workers_pool_cleanup(void *data) apr_thread_cond_signal(slot->not_idle); apr_thread_mutex_unlock(slot->lock); } +} + +static apr_status_t workers_pool_cleanup(void *data) +{ + h2_workers *workers = data; + + workers_abort_idle(workers); /* wait for all the workers to become zombies and join them */ apr_thread_mutex_lock(workers->lock); @@ -287,7 +337,7 @@ h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pchild, int min_workers, int max_workers, int idle_secs) { - apr_status_t status; + apr_status_t rv; h2_workers *workers; apr_pool_t *pool; int i, n; @@ -311,7 +361,7 @@ h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pchild, workers->pool = pool; workers->min_workers = min_workers; workers->max_workers = max_workers; - workers->max_idle_secs = (idle_secs > 0)? idle_secs : 10; + workers->max_idle_duration = apr_time_from_sec((idle_secs > 0)? idle_secs : 10); /* FIXME: the fifo set we use here has limited capacity. Once the * set is full, connections with new requests do a wait. Unfortunately, @@ -324,16 +374,12 @@ h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pchild, * For now, we just make enough room to have many connections inside one * process. */ - status = h2_fifo_set_create(&workers->mplxs, pool, 8 * 1024); - if (status != APR_SUCCESS) { - return NULL; - } - - status = apr_threadattr_create(&workers->thread_attr, workers->pool); - if (status != APR_SUCCESS) { - return NULL; - } - + rv = h2_fifo_set_create(&workers->mplxs, pool, 8 * 1024); + if (rv != APR_SUCCESS) goto cleanup; + + rv = apr_threadattr_create(&workers->thread_attr, workers->pool); + if (rv != APR_SUCCESS) goto cleanup; + if (ap_thread_stacksize != 0) { apr_threadattr_stacksize_set(workers->thread_attr, ap_thread_stacksize); @@ -342,38 +388,39 @@ h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pchild, (long)ap_thread_stacksize); } - status = apr_thread_mutex_create(&workers->lock, - APR_THREAD_MUTEX_DEFAULT, - workers->pool); - if (status == APR_SUCCESS) { - status = apr_thread_cond_create(&workers->all_done, workers->pool); + rv = apr_thread_mutex_create(&workers->lock, + APR_THREAD_MUTEX_DEFAULT, + workers->pool); + if (rv != APR_SUCCESS) goto cleanup; + rv = apr_thread_cond_create(&workers->all_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; } - if (status == APR_SUCCESS) { - n = workers->nslots = workers->max_workers; - workers->slots = apr_pcalloc(workers->pool, n * sizeof(h2_slot)); - if (workers->slots == NULL) { - n = workers->nslots = 0; - status = APR_ENOMEM; - } - for (i = 0; i < n; ++i) { - workers->slots[i].id = i; - } + for (i = 0; i < n; ++i) { + workers->slots[i].id = i; } - if (status == APR_SUCCESS) { - /* 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->max_workers; - for (i = n-1; i >= 0; --i) { - status = activate_slot(workers, &workers->slots[i]); - } - /* the rest of the slots go on the free list */ - for(i = n; i < workers->nslots; ++i) { - push_slot(&workers->free, &workers->slots[i]); - } - workers->dynamic = (workers->worker_count < workers->max_workers); + /* 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]); + 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]); } - if (status == APR_SUCCESS) { + workers->dynamic = (workers->worker_count < workers->max_workers); + +cleanup: + 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 * pools (children of workers->pool) so that they are not destroyed @@ -396,3 +443,10 @@ apr_status_t h2_workers_unregister(h2_workers *workers, struct h2_mplx *m) { return h2_fifo_remove(workers->mplxs, m); } + +void h2_workers_graceful_shutdown(h2_workers *workers) +{ + workers->shutdown = 1; + h2_fifo_term(workers->mplxs); + wake_non_essential_workers(workers); +} diff --git a/modules/http2/h2_workers.h b/modules/http2/h2_workers.h index 6298c81397..cc310c9b75 100644 --- a/modules/http2/h2_workers.h +++ b/modules/http2/h2_workers.h @@ -40,9 +40,10 @@ struct h2_workers { int next_worker_id; apr_uint32_t min_workers; apr_uint32_t max_workers; - int max_idle_secs; + apr_interval_time_t max_idle_duration; volatile int aborted; + volatile int shutdown; int dynamic; apr_threadattr_t *thread_attr; @@ -80,4 +81,9 @@ apr_status_t h2_workers_register(h2_workers *workers, struct h2_mplx *m); */ apr_status_t h2_workers_unregister(h2_workers *workers, struct h2_mplx *m); +/** + * Shut down processing gracefully by terminating all idle workers. + */ +void h2_workers_graceful_shutdown(h2_workers *workers); + #endif /* defined(__mod_h2__h2_workers__) */ diff --git a/modules/http2/mod_proxy_http2.c b/modules/http2/mod_proxy_http2.c index 893aa8fd31..4ea4fb9741 100644 --- a/modules/http2/mod_proxy_http2.c +++ b/modules/http2/mod_proxy_http2.c @@ -397,18 +397,9 @@ run_connect: if (!ctx->p_conn->data && ctx->is_ssl) { /* New SSL connection: set a note on the connection about what - * protocol we want. - */ + * protocol we need. */ apr_table_setn(ctx->p_conn->connection->notes, "proxy-request-alpn-protos", "h2"); - if (ctx->p_conn->ssl_hostname) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, ctx->owner, - "set SNI to %s for (%s)", - ctx->p_conn->ssl_hostname, - ctx->p_conn->hostname); - apr_table_setn(ctx->p_conn->connection->notes, - "proxy-request-hostname", ctx->p_conn->ssl_hostname); - } } if (ctx->master->aborted) goto cleanup; |