2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-11 19:27:02 +00:00

Make plugin registry hash independent of plugin discovery order (#12151)

* Make plugin registry hash independent of plugin discovery order

calculate_plugin_hash() iterates self.plugins.items() in insertion
order, which is the plugin discovery order of the local process. Two
processes can hold the same registry state (same plugins, versions,
active flags) in a different order and compute different hashes,
ping-ponging the _PLUGIN_REGISTRY_HASH setting and triggering endless
registry reloads in check_reload().

Sort by slug before hashing so the hash represents the registry state
rather than the iteration order of any particular process. Add a
regression test that reverses the plugin dict and asserts the hash is
unchanged.

* Address review comments: explicit sort key, guard against vacuous test

---------

Co-authored-by: Nasawa <christopher@anigeek.com>
This commit is contained in:
Christopher Johnson
2026-06-10 18:26:26 -06:00
committed by GitHub
parent 73bfa53a35
commit 7cca9cb326
2 changed files with 29 additions and 1 deletions
+5 -1
View File
@@ -1027,7 +1027,11 @@ class PluginsRegistry:
data = md5()
# Hash for all loaded plugins
for slug, plug in self.plugins.items():
# Note: Sort by slug, so the hash is independent of discovery order.
# Different processes can discover the same plugins in a different
# order, and the hash must represent the registry *state*, not the
# iteration order of any particular process.
for slug, plug in sorted(self.plugins.items(), key=lambda item: item[0]):
data.update(str(slug).encode())
data.update(str(plug.name).encode())
data.update(str(plug.version).encode())
@@ -538,6 +538,30 @@ class RegistryTests(TestQueryMixin, PluginRegistryMixin, TestCase):
registry.registry_hash = 'abc'
self.assertTrue(registry.check_reload())
def test_registry_hash_order_independence(self):
"""Test that the registry hash does not depend on plugin iteration order.
Different processes (gunicorn workers, background worker, shell) can
discover the same set of plugins in a different order. If the hash
depends on iteration order, processes disagree about the hash for the
same registry state, and ping-pong each other into endless reloads
via check_reload.
"""
original_plugins = registry.plugins
# Reversing a dict with fewer than 2 entries would not change anything
self.assertGreater(len(original_plugins), 1)
try:
hash_original = registry.calculate_plugin_hash()
# Simulate a process which discovered the same plugins in reverse order
registry.plugins = dict(reversed(list(original_plugins.items())))
self.assertEqual(hash_original, registry.calculate_plugin_hash())
finally:
registry.plugins = original_plugins
def test_builtin_mandatory_plugins(self):
"""Test that mandatory builtin plugins are always loaded."""
from plugin.models import PluginConfig