Skip to content

Destroy replaced configs on ET_TASK - #13491

Merged
moonchen merged 4 commits into
apache:masterfrom
moonchen:config-release-et-task
Aug 7, 2026
Merged

Destroy replaced configs on ET_TASK#13491
moonchen merged 4 commits into
apache:masterfrom
moonchen:config-release-et-task

Conversation

@moonchen

@moonchen moonchen commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The deferred destruction of a replaced config runs on a network thread today. ConfigProcessor::set() schedules it with schedule_in(), which defaults to ET_CALL, so 60 seconds later an ET_NET thread runs the destructor inside the drain phase of its event loop and is blocked for as long as the config takes to release, which is bounded only by the size of the config.

Move that work to ET_TASK. ConfigProcessor::release() is the only place a config is destroyed, but 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. This schedules the releaser on ET_TASK and hands 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 ConfigProcessor::get() safe, so it should not be tuned just because the destructor moved.

The config debug tag now reports the duration of each destruction and the thread that ran it.

Copilot AI lite review requested due to automatic review settings August 4, 2026 19:17
@moonchen moonchen added Performance Event System Configuration Backport Marked for backport for an LTS patch release labels Aug 4, 2026
@moonchen moonchen self-assigned this Aug 4, 2026
@moonchen moonchen added this to the 11.0.0 milestone Aug 4, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Moves destruction of replaced ConfigInfo objects off network threads by scheduling the work onto ET_TASK, and adds debug logging to report destruction duration and thread.

Changes:

  • Schedule deferred config release (ConfigInfoReleaser) on ET_TASK instead of the default event type.
  • Route last-reference destruction (ConfigProcessor::release) to ET_TASK when possible via a destroyer continuation.
  • Add debug logging for per-config destruction time and executing thread.

Comment thread src/iocore/eventsystem/ConfigProcessor.cc
Comment thread src/iocore/eventsystem/ConfigProcessor.cc Outdated
Comment thread src/iocore/eventsystem/ConfigProcessor.cc Outdated
Comment thread src/iocore/eventsystem/ConfigProcessor.cc Outdated
@moonchen
moonchen force-pushed the config-release-et-task branch from a91f7c2 to 8c28775 Compare August 4, 2026 21:26
@moonchen moonchen removed the Backport Marked for backport for an LTS patch release label Aug 4, 2026
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.
@moonchen
moonchen force-pushed the config-release-et-task branch from 8c28775 to e355bf8 Compare August 4, 2026 21:56
Copilot AI review requested due to automatic review settings August 4, 2026 21:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/iocore/eventsystem/ConfigProcessor.cc:208

  • The debug log uses %d for id, but id is an unsigned int. This varargs type mismatch is undefined behavior; use %u to match the argument type.
    Dbg(dbg_ctl_config, "Release config %d %p", id, info);

src/iocore/eventsystem/ConfigProcessor.cc:53

  • destroy_config() calls ink_get_hrtime() unconditionally, even though the timing is only used when the config debug tag is enabled. This adds avoidable overhead in the non-debug case; you can early-return when dbg_ctl_config is off and only measure/log when it’s on (and use Dbg() for consistency with the rest of the file).
destroy_config(unsigned int id, ConfigInfo *info)
{
  ink_hrtime start = ink_get_hrtime();

  delete info;

@cmcfarlen

Copy link
Copy Markdown
Contributor

[approve ci autest 1]

destroy_config_on_task_thread() also refused the hand off when
thread_group[ET_TASK]._count read 0, to cover the gap between
tasksProcessor.register_event_type() and tasksProcessor.start().  A
config cannot be destroyed in that gap.  release() reaches the destructor
only when the reference count drops to zero, and the processor gives up
its own reference only through the ConfigInfoReleaser that set() schedules
timeout_secs ahead, 60 seconds for every caller in the tree.  Both spans
where the count reads 0 are straight-line startup code on the main
thread, so nothing can drop a reference inside them.

The check was also the wrong test for that gap.  spawn_event_threads()
stores _thread[i] inside its construction loop and sets _count after the
loop, so for most of the gap the count still reads 0 while _thread[0] is
already a usable thread.
The destroyer is created and scheduled in one breath and nothing else can
reach it, so the mutex serialized nothing and cost a ProxyMutex
allocation per destroyed config.  EThread::process_event() treats a null
event mutex as already acquired, which is how FreeCallContinuation,
FreerContinuation and DereferContinuation already run.
Copilot AI review requested due to automatic review settings August 5, 2026 15:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/iocore/eventsystem/ConfigProcessor.cc:96

  • The comment says ET_TASK behaves as ET_CALL until task threads are registered and that an ET_NET caller should then destroy inline, but the code doesn’t actually detect that state. If ET_TASK still maps to ET_CALL here, schedule_imm(..., ET_TASK) can still enqueue the destructor work onto the caller’s (potentially ET_NET) loop, reintroducing the drain-phase blocking this PR aims to avoid. Consider explicitly checking whether ET_TASK threads are registered/available (e.g., via an eventProcessor query for ET_TASK thread count/availability) and returning false when they aren’t, rather than relying on is_event_type(ET_TASK).
  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;
  }

src/iocore/eventsystem/ConfigProcessor.cc:51

  • This uses PRId64 but the shown includes don’t include <inttypes.h>/<cinttypes>. Depending on transitive includes can break portability/builds. Add the appropriate standard header in this file (preferred: <cinttypes> in C++), or use an existing project-provided formatting utility if one is standard in this codebase.
    DbgPrint(dbg_ctl_config, "Destroyed config %u in %" PRId64 " ns on thread %s", id, ink_get_hrtime() - start, thread_name);

Comment thread src/iocore/eventsystem/ConfigProcessor.cc
JosiahWI
JosiahWI previously approved these changes Aug 5, 2026
The destruction path had no coverage.  Autest never reached it: a config is
destroyed only after ConfigProcessor::set() releases the config it
replaced, and that timeout is a compile time constant of 60 seconds, which
outlives every existing test.

The test replaces two configs, one through the reload framework and one
from a network thread by way of a record update, waits out the timeout and
checks that the debug line reports an ET_TASK thread for both.  It also
fails if any config is destroyed on a network thread.

The 60 second wait keeps it out of CI, so it is skipped the same way
post_slow_server is.
Copilot AI review requested due to automatic review settings August 5, 2026 18:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

src/iocore/eventsystem/ConfigProcessor.cc:88

  • destroy_config_on_task_thread() returns false when this_ethread() is nullptr, which forces inline destruction even though eventProcessor.schedule_imm(..., ET_TASK) can be called from non-EThread contexts. This can still block whatever thread is dropping the last ref (including non-event threads). Consider removing the ethread == nullptr early-return and instead only falling back to inline destruction when ET_TASK scheduling is unavailable (e.g., during shutdown / no task threads).
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;
  }

tests/gold_tests/config_processor/config_destroy_thread.test.py:29

  • This test is unconditionally skipped, so it won’t prevent regressions in automated runs. Instead of a hard skip, consider gating it behind an env/flag (e.g., Condition.EnvironmentVariableSet(...)) or placing it in an explicitly-run slow/soak suite so it can be executed in at least one CI lane.
Test.SkipIf(Condition.true("Test takes over 60 seconds to run."))

Comment thread src/iocore/eventsystem/ConfigProcessor.cc

@JosiahWI JosiahWI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, although I do not think it is wise to lock this in as a requirement. I don't think this should have an AuTest. This is a performance optimization and we should be able to change our minds and do something else without waiting for a major release. I would argue that adding the AuTest here is an unofficial guarantee that users can rely on this behavior.

@moonchen

moonchen commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Approving, although I do not think it is wise to lock this in as a requirement. I don't think this should have an AuTest. This is a performance optimization and we should be able to change our minds and do something else without waiting for a major release. I would argue that adding the AuTest here is an unofficial guarantee that users can rely on this behavior.

I understand your concern. I'm not trying to guarantee this behavior going forward with the autest. My goal with the test is to ensure that the code change does what it's supposed to.

My understanding about autest is that they're not set in stone, even within one major release, and they can be changed when the expected behavior of the code changes.

@moonchen
moonchen merged commit 46be2f5 into apache:master Aug 7, 2026
15 checks passed
@github-project-automation github-project-automation Bot moved this to For v10.2.0 in ATS v10.2.x Aug 7, 2026
@moonchen
moonchen deleted the config-release-et-task branch August 7, 2026 14:56
cmcfarlen pushed a commit that referenced this pull request Aug 9, 2026
* 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.

(cherry picked from commit 46be2f5)
@cmcfarlen cmcfarlen moved this from For v10.2.0 to Picked v10.2.0 in ATS v10.2.x Aug 9, 2026
@cmcfarlen cmcfarlen modified the milestones: 11.0.0, 10.2.0 Aug 9, 2026
@cmcfarlen

Copy link
Copy Markdown
Contributor

Cherry-picked to the 10.2.x branch as 745b2e7 for the 10.2.0 release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Picked v10.2.0

Development

Successfully merging this pull request may close these issues.

4 participants