Fix periodic tasks not running after pickle/unpickle

The pickle fix [0] excluded some unpicklable
objects from __getstate__ but missed thread pool executors and
periodic task objects. This caused periodic tasks like
_send_sensor_data to silently fail after oslo.service pickled and
unpickled the conductor manager for spawn mode.

When _executor, _reserved_executor, _periodic_tasks, and
_periodic_tasks_worker were pickled, their internal threading
primitives were corrupted, preventing periodic tasks from being
scheduled or executed in the child process.

Exclude these objects from __getstate__ and set them to None in
__setstate__ so they are properly recreated during prepare_host()
and init_host().

Also update tests to verify these objects are excluded from pickle
and properly set to None after unpickling.

[0] https://review.opendev.org/c/openstack/ironic/+/977266

Change-Id: I6d6c236493f82f78ce7846a3118777520ca04aa9
Signed-off-by: Riccardo Pittau <elfosardo@gmail.com>
This commit is contained in:
Riccardo Pittau
2026-05-13 16:27:14 +02:00
parent d0d2a36ef5
commit df3440c5eb
2 changed files with 37 additions and 1 deletions
+11 -1
View File
@@ -71,12 +71,18 @@ class BaseConductorManager(object):
``_shutdown`` is a threading.Event (contains ``_thread.lock``);
``sensors_notifier`` and ``dbapi`` may contain unpicklable
transport objects and connection-pool locks respectively.
All three are recreated during startup.
``_executor``, ``_reserved_executor``, ``_periodic_tasks``, and
``_periodic_tasks_worker`` contain thread pools and futures.
All are recreated during startup.
"""
state = self.__dict__.copy()
state['_shutdown'] = self._shutdown.is_set()
state.pop('sensors_notifier', None)
state.pop('dbapi', None)
state.pop('_executor', None)
state.pop('_reserved_executor', None)
state.pop('_periodic_tasks', None)
state.pop('_periodic_tasks_worker', None)
return state
def __setstate__(self, state):
@@ -87,6 +93,10 @@ class BaseConductorManager(object):
self._shutdown.set()
self.sensors_notifier = None # recreated in init_host()
self.dbapi = None # recreated in prepare_host()/init_host()
self._executor = None # recreated in prepare_host()
self._reserved_executor = None # recreated in prepare_host()
self._periodic_tasks = None # recreated in init_host()
self._periodic_tasks_worker = None # recreated in init_host()
def prepare_host(self):
"""Prepares host for initialization
@@ -323,6 +323,13 @@ class TestRPCService(db_base.DbTestCase):
self.assertEqual(self.rpc_svc.manager.topic, restored.manager.topic)
# threading.Event should be recreated as unset
self.assertFalse(restored.manager._shutdown.is_set())
# Thread pool executors and periodic tasks should be excluded
# from pickle and set to None after unpickling (to be recreated
# during startup)
self.assertIsNone(restored.manager._executor)
self.assertIsNone(restored.manager._reserved_executor)
self.assertIsNone(restored.manager._periodic_tasks)
self.assertIsNone(restored.manager._periodic_tasks_worker)
def test_getstate_includes_argv(self):
"""__getstate__ saves sys.argv so __setstate__ can restore CONF."""
@@ -333,6 +340,25 @@ class TestRPCService(db_base.DbTestCase):
# tg must be excluded (contains un-picklable threading objects)
self.assertNotIn('tg', state)
def test_manager_getstate_excludes_unpicklable(self):
"""Manager __getstate__ excludes thread pools and periodic tasks.
Thread pool executors and periodic task objects contain threading
primitives that cannot be pickled. Verify they are excluded from
the pickled state and will be recreated during startup.
"""
state = self.rpc_svc.manager.__getstate__()
# Threading objects must be excluded
self.assertNotIn('_executor', state)
self.assertNotIn('_reserved_executor', state)
self.assertNotIn('_periodic_tasks', state)
self.assertNotIn('_periodic_tasks_worker', state)
self.assertNotIn('sensors_notifier', state)
self.assertNotIn('dbapi', state)
# _shutdown should be converted to bool
self.assertIn('_shutdown', state)
self.assertIsInstance(state['_shutdown'], bool)
@mock.patch('ironic.common.service.prepare_command', autospec=True)
def test_setstate_calls_prepare_command(self, mock_prepare):
"""__setstate__ re-configures CONF when _argv is present.