From 15d0fbb6671182ef48e597c2841edac8482ef580 Mon Sep 17 00:00:00 2001 From: Brian Neradt Date: Tue, 21 Apr 2026 23:04:14 +0000 Subject: [PATCH] Fix bg fill teardown crash in update_size_and_time_stats Production crash logs show ink_assert(0) firing in HttpTransact::update_size_and_time_stats when an HTTP/2 stream close re-enters HttpSM after the user-agent side was detached for background fill. The server-to-cache tunnel should continue, but the stale user-agent event misses the VC table and falls through to the default tunnel handler, which tears down the SM with background_fill still STARTED. This ignores stale user-agent VIO and close events while a background fill tunnel is active, so tunnel_handler_server remains responsible for driving the fill to COMPLETED or ABORTED. This also keeps an unconditional kill_this cleanup as a last-resort guard to balance background_fill_current_count if any unexpected path still tears down mid-fill. --- src/proxy/http/HttpSM.cc | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index de97e3a2002..827e5ac1a54 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -2759,11 +2759,47 @@ HttpSM::main_handler(int event, void *data) } } + // Is there an active background-fill tunnel? + // Background fill detaches the user-agent VC and lets the server/cache side + // finish the response body. An Http2Stream can still deliver a scheduled UA + // close/error callback after that detach, but the UA VIO no longer has a VC + // table entry. This predicate recognizes only those detached UA-side events + // so they do not fall through to the default tunnel handler and tear down the + // HttpSM before the active background-fill tunnel completes. + auto is_stale_bg_fill_ua_event = [&]() -> bool { + if (background_fill != BackgroundFill_t::STARTED || !tunnel.is_tunnel_alive() || _ua.get_txn() == nullptr) { + return false; + } + + switch (event) { + case VC_EVENT_EOS: + case VC_EVENT_ERROR: + case VC_EVENT_INACTIVITY_TIMEOUT: + case VC_EVENT_ACTIVE_TIMEOUT: + case VC_EVENT_WRITE_READY: + case VC_EVENT_WRITE_COMPLETE: + case VC_EVENT_READ_READY: + case VC_EVENT_READ_COMPLETE: + break; + default: + return false; + } + + if (data == nullptr) { + return true; + } + + return static_cast(data)->vc_server == _ua.get_txn(); + }; + if (vc_entry) { jump_point = (static_cast(data) == vc_entry->read_vio) ? vc_entry->vc_read_handler : vc_entry->vc_write_handler; ink_assert(jump_point != (HttpSMHandler) nullptr); ink_assert(vc_entry->vc != (VConnection *)nullptr); (this->*jump_point)(event, data); + } else if (is_stale_bg_fill_ua_event()) { + SMDbg(dbg_ctl_http, "ignoring stale %s event for closed user agent while background fill is active", + HttpDebugNames::get_event_name(event)); } else { ink_assert(default_handler != (HttpSMHandler) nullptr); (this->*default_handler)(event, data); @@ -7706,6 +7742,16 @@ HttpSM::kill_this() // we must check it again if (kill_this_async_done == true) { pending_action = nullptr; + + // This should only be a last-resort cleanup path. A background fill is + // normally driven to COMPLETED or ABORTED by tunnel_handler_server, but + // any unexpected teardown must still balance the active fill gauge before + // optional stats/logging run. + if (background_fill == BackgroundFill_t::STARTED) { + background_fill = BackgroundFill_t::ABORTED; + Metrics::Gauge::decrement(http_rsb.background_fill_current_count); + } + if (t_state.http_config_param->enable_http_stats) { update_stats(); }