fix(plugins): abandon only nested plugin loads still running at outer timeout

Gate finding BN10.r1.2c (Low): the inline re-entrant branch appended the
nested ctx to the outer load's scope and never removed it, so a nested
load that had already finished (and was recorded enabled under its own
key) got fenced by _abandon_load() when the outer load later timed out.
Keep the ctx in scope only while its fn() runs.

The kept nested-inline test now times the outer load out with one finished
and one hung nested load, asserting only outer + hung are abandoned; this
also pins the nested-abandon behaviour of the previous commit.
This commit is contained in:
kshitijk4poor
2026-10-03 15:12:24 +05:30
committed by kshitij
parent 0e77d05974
commit f7bbbc474e
2 changed files with 24 additions and 9 deletions

View File

@@ -116,7 +116,10 @@ def run_with_load_deadline(plugin_key: str, ctx: "PluginContext", fn: Callable[[
scope = _IN_PLUGIN_LOAD.get()
if scope is not None:
scope.append(ctx)
return fn()
try:
return fn()
finally:
scope.remove(ctx)
timeout = _resolve_plugin_load_timeout()
if timeout <= 0:
return fn()

View File

@@ -1,24 +1,36 @@
from hermes_cli import plugins_loader
import threading
import pytest
from hermes_cli import plugins_loader
def test_nested_plugin_load_runs_inline_on_deadline_worker(monkeypatch):
monkeypatch.setattr(plugins_loader, "_resolve_plugin_load_timeout", lambda: 1.0)
monkeypatch.setattr(plugins_loader, "_resolve_plugin_load_timeout", lambda: 0.3)
abandoned = []
class Context:
def __init__(self, name):
self.name = name
def _abandon_load(self):
raise AssertionError("unexpected timeout")
abandoned.append(self.name)
observed_threads = []
def nested_load():
observed_threads.append(threading.current_thread())
release = threading.Event()
def outer_load():
observed_threads.append(threading.current_thread())
plugins_loader.run_with_load_deadline("nested", Context(), nested_load)
plugins_loader.run_with_load_deadline(
"done", Context("done"), lambda: observed_threads.append(threading.current_thread()),
)
plugins_loader.run_with_load_deadline("hung", Context("hung"), release.wait)
plugins_loader.run_with_load_deadline("outer", Context(), outer_load)
with pytest.raises(plugins_loader.PluginLoadTimeout):
plugins_loader.run_with_load_deadline("outer", Context("outer"), outer_load)
release.set()
assert len(observed_threads) == 2
assert observed_threads[0] is observed_threads[1]
# The outer timeout abandons only contexts still loading, not a nested load that already finished.
assert abandoned == ["outer", "hung"]