Harden cleanup rollback

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-25 15:40:08 +08:00
parent 9e33ec63ac
commit 4bc5923960
4 changed files with 108 additions and 15 deletions
+19 -12
View File
@@ -215,23 +215,30 @@ class Mixins:
"""Handle cleanup all for the cleanup on exit."""
Mixins.CleanupOnExit.ObjectsPool.prune(Mixins.qObjectIsValid)
for ob in list(Mixins.CleanupOnExit.ObjectsPool):
assert isinstance(ob, Mixins.CleanupOnExit)
try:
for ob in list(Mixins.CleanupOnExit.ObjectsPool):
assert isinstance(ob, Mixins.CleanupOnExit)
if ob.uniqueCleanup:
obtype = str(type(ob))
if ob.uniqueCleanup:
obtype = str(type(ob))
if not Mixins.CleanupOnExit.VisitedType.get(obtype, False):
ob.cleanup()
if Mixins.CleanupOnExit.VisitedType.get(obtype, False):
continue
Mixins.CleanupOnExit.VisitedType[obtype] = True
else:
pass
else:
ob.cleanup()
Mixins.CleanupOnExit.ObjectsPool.clear()
Mixins.CleanupOnExit.VisitedType.clear()
try:
ob.cleanup()
except Exception:
# Any non-exit exceptions
# Cleanup is an isolation boundary. One failed owner
# must not prevent unrelated resources from being
# released during application shutdown.
logger.exception(f'cleanup failed for {type(ob).__name__}')
finally:
Mixins.CleanupOnExit.ObjectsPool.clear()
Mixins.CleanupOnExit.VisitedType.clear()
class QSetDisabledContext:
"""Manage the q set disabled context."""
+13 -3
View File
@@ -1334,10 +1334,20 @@ class _PluginRegistryManager:
"""Build a registry with host plugins taking discovery precedence."""
registry = PluginRegistry()
for pluginType in pluginTypes:
registry.register(pluginType())
try:
for pluginType in pluginTypes:
registry.register(pluginType())
registry.discover()
registry.discover()
except Exception:
# Any non-exit exceptions
# The manager publishes the registry only after this method
# succeeds. Release plugins initialized earlier in this attempt
# before abandoning the unpublished registry.
registry.shutdown()
raise
return registry
+41
View File
@@ -123,6 +123,47 @@ class FrozenlibQtContextTest(unittest.TestCase):
processQtEvents()
class CleanupOnExitTest(unittest.TestCase):
"""Verify application cleanup isolates independently owned resources."""
def testCleanupFailureDoesNotSkipLaterOwners(self):
"""Continue cleanup after one participant raises and reset registry state."""
calls = []
poolType = type(Mixins.CleanupOnExit.ObjectsPool)
class Participant(Mixins.CleanupOnExit):
def __init__(self, name: str, raises=False):
self.name = name
self.raises = raises
super().__init__(uniqueCleanup=False)
def cleanup(self):
calls.append(self.name)
if self.raises:
raise RuntimeError('cleanup fixture')
with mock.patch.object(
Mixins.CleanupOnExit,
'ObjectsPool',
poolType(),
), mock.patch.object(Mixins.CleanupOnExit, 'VisitedType', {}):
participants = (
Participant('first'),
Participant('failing', raises=True),
Participant('last'),
)
with self.assertLogs('Furious.Frozenlib.Mixins', level='ERROR'):
Mixins.CleanupOnExit.cleanupAll()
self.assertEqual(calls, ['first', 'failing', 'last'])
self.assertEqual(len(Mixins.CleanupOnExit.ObjectsPool), 0)
self.assertEqual(Mixins.CleanupOnExit.VisitedType, {})
self.assertEqual(len(participants), 3)
class FrozenlibUtilityTest(unittest.TestCase):
"""Verify bounded caches, commands, throttling, and dual-stack probes."""
+35
View File
@@ -266,6 +266,41 @@ class PluginRegistryManagerTest(unittest.TestCase):
with self.assertRaisesRegex(ValueError, 'already registered'):
PluginRegistryModule.initializePluginRegistry((ConflictingPlugin,))
def testFailedInitialCreationShutsDownEarlierHostPlugins(self):
"""Release initialized plugins when a later host plugin fails."""
initializedPlugin = FixturePlugin()
class InitializedPluginType:
"""Return the observable plugin instance owned by this test."""
def __new__(cls):
return initializedPlugin
class FailingPluginType:
"""Fail before the process registry can be published."""
def __new__(cls):
raise RuntimeError('host plugin construction failed')
with mock.patch.object(PluginRegistry, 'discover') as discover:
with self.assertRaisesRegex(
RuntimeError,
'host plugin construction failed',
):
PluginRegistryModule.initializePluginRegistry(
(InitializedPluginType, FailingPluginType)
)
self.assertEqual(initializedPlugin.initialized, 1)
self.assertEqual(initializedPlugin.stopped, 1)
discover.assert_not_called()
with mock.patch.object(PluginRegistry, 'discover') as discover:
registry = PluginRegistryModule.initializePluginRegistry()
self.addCleanup(registry.shutdown)
discover.assert_called_once_with()
class PluginRegistryTest(unittest.TestCase):
"""Exercise capability dispatch without the process-wide registry."""