mpm: Fixed reference leaks by clearing references to bound methods

Upon reset, MPM Server tries to destroy the current instance of
Periperal Manager and re-create a new one.
Due to external references to bound methods, Peripheral Manager
and its member DioControl could not get destroyed which resulted
in reference leaks and file desciptor leaks.

This PR clears all external references to bound methods such that
PeripheralManager and its members are cleanly destroyed before
re-creation.

This fixes the "Too Many Open Files" when running 'uhd_image_loader'
repeatedly.
This commit is contained in:
Andre Zoch 2026-02-26 17:24:02 +01:00 committed by Jörg Hofrichter
parent cfb9840727
commit 908236e28f
2 changed files with 52 additions and 13 deletions

View File

@ -7,6 +7,7 @@
import re
import signal
import weakref
from multiprocessing import Event, Process, Value, current_process
from usrp_mpm.mpmutils import (
@ -316,8 +317,15 @@ class DioControl:
name="DIO1 fault monitor",
args=("B", "DIO_INT1", self._tear_down_monitor, self._dio_fault["PORTB"]),
)
register_chained_signal_handler(signal.SIGINT, self._monitor_int_handler)
register_chained_signal_handler(signal.SIGTERM, self._monitor_int_handler)
# Use a weakref-based signal handler wrapper so the global signal
# handler chain does not keep this DioControl instance alive.
self_ref = weakref.ref(self)
def _weak_monitor_int_handler(signum, frame):
obj = self_ref()
if obj is not None:
obj._monitor_int_handler(signum, frame)
register_chained_signal_handler(signal.SIGINT, _weak_monitor_int_handler)
register_chained_signal_handler(signal.SIGTERM, _weak_monitor_int_handler)
self._dio0_fault_monitor.start()
self._dio1_fault_monitor.start()

View File

@ -11,6 +11,7 @@ import signal
import sys
import threading
import traceback
import weakref
from contextlib import contextmanager
from multiprocessing import Process, RLock, current_process
from random import choice
@ -111,14 +112,7 @@ class MPMServer(RPCServer):
First clears out all previously registered RPC calls.
"""
# Clear old calls:
for meth_list in (self._db_methods, self._mb_methods):
for method in meth_list:
if hasattr(self, method):
delattr(self, method)
else:
self.log.warning("Attempted to remove non-existent method: %s", method)
self._db_methods = []
self._mb_methods = []
self._clear_rpc_calls()
# Register new ones:
self._update_component_commands(mgr, "", "_mb_methods")
for db_slot, dboard in enumerate(mgr.dboards):
@ -129,6 +123,22 @@ class MPMServer(RPCServer):
len(self._mb_methods),
len(self._db_methods),
)
def _clear_rpc_calls(self):
"""Clear all RPC calls.
This is a helper function that clears all RPC calls. This is used in the
case where we want to reset the peripheral manager, and we want to make
sure that no calls to the old peripheral manager are possible.
"""
for meth_list in (self._db_methods, self._mb_methods):
for method in meth_list:
if hasattr(self, method):
delattr(self, method)
else:
self.log.warning("Attempted to remove non-existent method: %s", method)
self._db_methods = []
self._mb_methods = []
def _update_component_commands(self, component, namespace, storage):
"""Detect available methods for an object and add them to the RPC server.
@ -515,13 +525,34 @@ class MPMServer(RPCServer):
The peripheral manager is torn down and re-initialized from scratch.
"""
self.log.info("Resetting peripheral manager.")
# Keep a weak reference so we can verify the old manager is truly
# destroyed after we clear all references. A weak reference does not
# prevent garbage collection, so if it returns None afterwards we know
# __del__ was called and all handles were released.
_old_mgr_ref = weakref.ref(self.periph_manager)
self.periph_manager.tear_down()
self.periph_manager = None
# Remove the RPC method closures to remove references to old
# periph_manager.
# There are TWO places where old periph_manager references hide:
# 1. Instance attributes: _add_claimed_command/_add_safe_command attach
# closures via setattr(self, command, fn). Each closure captures a
# bound method of the old periph_manager (via __self__).
self._clear_rpc_calls()
# 2. mprpc RPCServer._methods dict (Cython cdef field): _parse_request()
# caches getattr(self, method_name) on first call. Those cached
# closures also hold a reference to the old periph_manager.
# clear_method_registry() empties that dict.
self.clear_method_registry()
if _old_mgr_ref() is not None:
self.log.warning(
"Peripheral manager was NOT garbage collected after reset! "
"There is probably a lingering reference keeping it alive. "
"GPIO/SPI/pipe handles may have leaked."
)
# Create a new manager and register RPC calls for it.
self.periph_manager = self._mgr_generator()
self._init_rpc_calls(self.periph_manager)
# Clear the method cache in order to remove stale references to
# methods from the old peripheral manager (the one before reset)
self.clear_method_registry()
# update the FPGA type information in the state
device_info = self.periph_manager.get_device_info()
self._state.dev_fpga_type.value = to_binary_str(device_info.get("fpga", "n/a"))