From 119c721c3d8502455720aae93bd9118cc4986a52 Mon Sep 17 00:00:00 2001 From: "Austin (Ngoc Thang) Pham" Date: Wed, 15 Jul 2026 16:36:04 +1000 Subject: [PATCH 1/3] fix(ansible): serialize module resolution Copilot-Session: 9e6480f2-5e3b-41f8-8a45-2652945024a4 Signed-off-by: Austin (Ngoc Thang) Pham --- tests/common/devices/base.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/common/devices/base.py b/tests/common/devices/base.py index 494e63c1193..0e07339f030 100644 --- a/tests/common/devices/base.py +++ b/tests/common/devices/base.py @@ -42,6 +42,9 @@ def ansible_tqm_has_signal_registration(): _signal_patch_lock = threading.RLock() _signal_patch_ref_count = 0 _original_signal = None +# pytest-ansible resolves modules through Ansible's process-global, mutable +# plugin-loader caches. +_ansible_module_resolution_lock = threading.RLock() @contextmanager @@ -158,7 +161,9 @@ def __init__(self, ansible_adhoc, hostname, *args, **kwargs): self.hostname = hostname def __getattr__(self, module_name): - if self.host.has_module(module_name): + with _ansible_module_resolution_lock: + has_module = self.host.has_module(module_name) + if has_module: def _run_wrapper(*module_args, **kwargs): return self._run(module_name, *module_args, **kwargs) return _run_wrapper @@ -166,13 +171,17 @@ def _run_wrapper(*module_args, **kwargs): "'%s' object has no attribute '%s'" % (self.__class__, module_name) ) + def _get_ansible_module(self, module_name): + with _ansible_module_resolution_lock: + return getattr(self.host, module_name) + def _run(self, module_name, *module_args, **complex_args): previous_frame = inspect.currentframe().f_back filename, line_number, function_name, lines, index = inspect.getframeinfo(previous_frame) verbose = complex_args.pop('verbose', True) - module = getattr(self.host, module_name) + module = self._get_ansible_module(module_name) if verbose: logger.debug( "{}::{}#{}: [{}] AnsibleModule::{}, args={}, kwargs={}".format( From b4868c955b799ed049741e3eb24fd818dd720652 Mon Sep 17 00:00:00 2001 From: "Austin (Ngoc Thang) Pham" Date: Mon, 20 Jul 2026 13:45:42 +1000 Subject: [PATCH 2/3] docs(ansible): clarify module loader lock scope Copilot-Session: 92f46056-4a3c-4630-8a22-fd305a1c813f Signed-off-by: Austin (Ngoc Thang) Pham --- tests/common/devices/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/common/devices/base.py b/tests/common/devices/base.py index 0e07339f030..a8fc0968304 100644 --- a/tests/common/devices/base.py +++ b/tests/common/devices/base.py @@ -42,8 +42,8 @@ def ansible_tqm_has_signal_registration(): _signal_patch_lock = threading.RLock() _signal_patch_ref_count = 0 _original_signal = None -# pytest-ansible resolves modules through Ansible's process-global, mutable -# plugin-loader caches. +# pytest-ansible resolves modules through Ansible's mutable plugin-loader +# caches, which are shared by all threads in the current Python process. _ansible_module_resolution_lock = threading.RLock() From e7bc497b1770db5264db5c9b14ddd87c58b9abbb Mon Sep 17 00:00:00 2001 From: "Austin (Ngoc Thang) Pham" Date: Tue, 21 Jul 2026 12:09:56 +1000 Subject: [PATCH 3/3] fix(ansible): reset module lock after fork Copilot-Session: addc76b1-5255-4e31-b096-11378f519f23 Signed-off-by: Austin (Ngoc Thang) Pham --- tests/common/devices/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/common/devices/base.py b/tests/common/devices/base.py index a8fc0968304..78b760395a0 100644 --- a/tests/common/devices/base.py +++ b/tests/common/devices/base.py @@ -2,6 +2,7 @@ import json import logging import collections +import os import signal import threading from contextlib import contextmanager @@ -42,9 +43,11 @@ def ansible_tqm_has_signal_registration(): _signal_patch_lock = threading.RLock() _signal_patch_ref_count = 0 _original_signal = None -# pytest-ansible resolves modules through Ansible's mutable plugin-loader -# caches, which are shared by all threads in the current Python process. +# pytest-ansible resolves modules through mutable plugin-loader caches shared +# by threads in each Python process. Reset the process-local lock after fork so +# a child cannot inherit it while held by a vanished parent thread. _ansible_module_resolution_lock = threading.RLock() +os.register_at_fork(after_in_child=_ansible_module_resolution_lock._at_fork_reinit) @contextmanager