mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
Harden cleanup rollback
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
+19
-12
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user