This is an automated email from the ASF dual-hosted git repository.

moonchen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 46be2f5008 Destroy replaced configs on ET_TASK (#13491)
46be2f5008 is described below

commit 46be2f5008dd9026d2bdd6b3f02c5066031605e9
Author: Mo Chen <[email protected]>
AuthorDate: Fri Aug 7 09:55:56 2026 -0500

    Destroy replaced configs on ET_TASK (#13491)
    
    * Destroy replaced configs on ET_TASK
    
    ConfigProcessor::set() scheduled the deferred destruction of the replaced
    config with schedule_in(), which defaults to ET_CALL, so a network thread
    ran the destructor 60 seconds later inside the drain phase of its event
    loop.  The destructor blocks that thread for as long as the config takes
    to release, which is bounded only by the size of the config.
    
    ConfigProcessor::release() is the only place a config is destroyed, and
    two callers reach it: the releaser at 60 seconds, which destroys the
    config whenever nothing else still holds a reference, and a transaction
    that outlived the releaser and drops the last reference itself.  Schedule
    the releaser on ET_TASK, and hand the destructor from the transaction path
    to ET_TASK as well, so neither can block a network thread.
    
    The 60 second wait is unchanged.  Shortening it would narrow the window
    that makes the load-then-increment in get() safe.
    
    The config debug tag now reports the duration of each destruction and the
    thread that ran it.
---
 src/iocore/eventsystem/ConfigProcessor.cc          | 76 ++++++++++++++++++++-
 .../config_processor/config_destroy_thread.test.py | 77 ++++++++++++++++++++++
 2 files changed, 151 insertions(+), 2 deletions(-)

diff --git a/src/iocore/eventsystem/ConfigProcessor.cc 
b/src/iocore/eventsystem/ConfigProcessor.cc
index a49335154a..adf3cbc69f 100644
--- a/src/iocore/eventsystem/ConfigProcessor.cc
+++ b/src/iocore/eventsystem/ConfigProcessor.cc
@@ -22,7 +22,10 @@
  */
 
 #include "iocore/eventsystem/ConfigProcessor.h"
+#include "iocore/eventsystem/EThread.h"
+#include "iocore/eventsystem/Tasks.h"
 #include "tscore/ink_atomic.h"
+#include "tscore/ink_thread.h"
 #if TS_HAS_TESTS
 #include "tscore/TestBox.h"
 #endif
@@ -34,8 +37,69 @@ namespace
 
 DbgCtl dbg_ctl_config{"config"};
 
+void
+destroy_config(unsigned int id, ConfigInfo *info)
+{
+  ink_hrtime start = ink_get_hrtime();
+
+  delete info;
+
+  if (dbg_ctl_config.on()) {
+    char thread_name[MAX_THREAD_NAME_LENGTH] = {};
+
+    ink_get_thread_name(thread_name, sizeof(thread_name));
+    DbgPrint(dbg_ctl_config, "Destroyed config %u in %" PRId64 " ns on thread 
%s", id, ink_get_hrtime() - start, thread_name);
+  }
+}
+
+/// Runs the destructor of a detached ConfigInfo on ET_TASK.
+class ConfigInfoDestroyer : public Continuation
+{
+public:
+  ConfigInfoDestroyer(unsigned int id, ConfigInfo *info) : 
Continuation(nullptr), m_id(id), m_info(info)
+  {
+    SET_HANDLER(&ConfigInfoDestroyer::handle_event);
+  }
+
+  int
+  handle_event(int /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */)
+  {
+    destroy_config(m_id, m_info);
+    delete this;
+    return EVENT_DONE;
+  }
+
+private:
+  unsigned int m_id;
+  ConfigInfo  *m_info;
+};
+
+/// Hand a detached ConfigInfo to ET_TASK for destruction. Returns false when 
the caller has to
+/// destroy it itself.
+bool
+destroy_config_on_task_thread(unsigned int id, ConfigInfo *info)
+{
+  EThread *ethread = this_ethread();
+
+  // ET_TASK is ET_CALL until the task threads are registered, so before that 
point an ET_NET caller
+  // destroys the config on its own thread.
+  if (ethread == nullptr || ethread->is_event_type(ET_TASK)) {
+    return false;
+  }
+
+  ConfigInfoDestroyer *destroyer = new ConfigInfoDestroyer(id, info);
+
+  if (eventProcessor.schedule_imm(destroyer, ET_TASK) == nullptr) {
+    // The event system is shutting down and will never run the destroyer.
+    delete destroyer;
+    return false;
+  }
+
+  return true;
 }
 
+} // namespace
+
 class ConfigInfoReleaser : public Continuation
 {
 public:
@@ -94,7 +158,10 @@ ConfigProcessor::set(unsigned int id, ConfigInfo *info, 
unsigned timeout_secs)
     // The ConfigInfoReleaser now takes our refcount, but
     // some other thread might also have one ...
     ink_assert(old_info->refcount() > 0);
-    eventProcessor.schedule_in(new ConfigInfoReleaser(id, old_info), 
HRTIME_SECONDS(timeout_secs));
+    // Destroying a config releases everything it owns - a replaced 
certificate table takes its whole
+    // certificate set with the chains, keys and staples. Run it on ET_TASK, 
which already carries the
+    // config load, so the cost cannot land on a network event loop.
+    eventProcessor.schedule_in(new ConfigInfoReleaser(id, old_info), 
HRTIME_SECONDS(timeout_secs), ET_TASK);
   }
 
   return id;
@@ -140,7 +207,12 @@ ConfigProcessor::release(unsigned int id, ConfigInfo *info)
     // When we release, we should already have replaced this object in the 
index.
     Dbg(dbg_ctl_config, "Release config %d %p", id, info);
     ink_release_assert(info != this->infos[idx]);
-    delete info;
+
+    // The releaser runs on ET_TASK, but a transaction that outlived it drops 
the last reference on
+    // its own thread, which serves network connections.
+    if (!destroy_config_on_task_thread(id, info)) {
+      destroy_config(id, info);
+    }
   }
 }
 
diff --git a/tests/gold_tests/config_processor/config_destroy_thread.test.py 
b/tests/gold_tests/config_processor/config_destroy_thread.test.py
new file mode 100644
index 0000000000..0909c0f3bd
--- /dev/null
+++ b/tests/gold_tests/config_processor/config_destroy_thread.test.py
@@ -0,0 +1,77 @@
+'''
+Verify that a replaced config is destroyed on an ET_TASK thread.
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import os
+
+Test.Summary = '''
+Verify that a replaced config is destroyed on an ET_TASK thread.
+'''
+
+# ConfigProcessor::set() waits CONFIG_PROCESSOR_RELEASE_SECS before it 
releases the config that it
+# replaced.  That timeout is a compile time constant of 60 seconds, so this 
test needs more than a
+# minute of wall clock and does not run in CI.  Comment out the next line to 
run it.
+Test.SkipIf(Condition.true("Test takes over 60 seconds to run."))
+
+Test.ContinueOnFail = True
+
+ts = Test.MakeATSProcess("ts")
+
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'config',
+})
+
+ts.Disk.remap_config.AddLine('map / http://127.0.0.1:8080')
+
+config_dir = ts.Variables.CONFIGDIR
+
+# Two replacements, reached two different ways.  Touching parent.config 
replaces ParentConfigParams
+# through the reload framework, which runs on ET_TASK.  Changing an HTTP 
record replaces
+# HttpConfigParams from a network thread, which is the case this test is 
really about.  Neither old
+# config is referenced once the test stops sending traffic, so both reach a 
zero reference count and
+# are destroyed when the release timeout expires.
+tr = Test.AddTestRun("Mark parent.config for reload")
+tr.Processes.Default.StartBefore(ts)
+tr.Processes.Default.Command = f"sleep 3 && touch {os.path.join(config_dir, 
'parent.config')} && sleep 1"
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = ts
+
+Test.AddConfigReload(ts, expect="any", token="config_destroy_thread")
+
+tr = Test.AddTestRun("Replace the HTTP config from a network thread")
+tr.DelayStart = 3
+tr.Processes.Default.Env = ts.Env
+tr.Processes.Default.Command = "traffic_ctl config set 
proxy.config.http.response_server_str probe && sleep 3"
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = ts
+
+tr = Test.AddTestRun("Wait for the release timeout to expire")
+tr.DelayStart = 3
+tr.Processes.Default.Command = "sleep 80"
+tr.Processes.Default.ReturnCode = 0
+tr.TimeOut = 150
+tr.StillRunningAfter = ts
+
+# The releaser runs on ET_TASK, so it destroys the replaced config there.
+ts.Disk.traffic_out.Content = Testers.ContainsExpression(
+    r"Destroyed config \d+ in \d+ ns on thread \[ET_TASK", "a replaced config 
should be destroyed on a task thread")
+
+# Destroying a config on a network thread is the regression this test guards 
against.
+ts.Disk.traffic_out.Content += Testers.ExcludesExpression(
+    r"Destroyed config \d+ in \d+ ns on thread \[ET_NET", "no config should be 
destroyed on a network thread")

Reply via email to