• 
      

    Rebuild cached model relations when installing extension apps.

    Review Request #15219 — Created Aug. 7, 2026 and updated

    Information

    Djblets
    release-6.x

    Reviewers

    We currently have a bug where a model's relation tree can be cached before
    an extension providing a related model is registered, leaving the relation
    permanently missing from it.

    Django's Apps.set_installed_apps() sets Apps.ready to False before both
    of its calls to Apps.clear_cache() (its own, and the one at the end of
    Apps.populate()). However clear_cache() only expires the per-model
    Options caches when the registry is ready. Installing an app therefore
    never rebuilds those cached relation trees.

    This bug is exposed when an extension imports something from another extension
    at module scope. For example, an extension that uses Power Pack's User Roles
    in an approval hook will likely import the user role model at the module scope
    (we do this in an example in Review Board's documentation). The extension
    manager imports every extension's module before initializing any of them, so
    in this case the related model gets registered but is unreachable through
    AppConfig. Then if something accesses the model's relation tree before the
    extensions are registered, the tree is cached without the extension model's
    relation. This was bug was discovered when trying to import User Roles in
    RBCommons' team admin code.

    This change makes ExtensionManager._add_to_installed_apps() clear the cache
    itself, so that model relations can be properly populated.

    • Ran unit tests.
    • Saw the FieldError I was getting in RBCommons from the User Roles
      relation being missing go away.
    Summary ID
    Rebuild cached model relations when installing extension apps.
    We currently have a bug where a model's relation tree can be cached before an extension providing a related model is registered, leaving the relation permanently missing from it. Django's `Apps.set_installed_apps()` sets `Apps.ready` to `False` before both of its calls to `Apps.clear_cache()` -- its own, and the one at the end of `Apps.populate()` -- and `clear_cache()` only expires the per-model `Options` caches when the registry is ready. Installing an app therefore never rebuilds those cached relation trees. This bug is exposed when an extension imports something from another extension at module scope. For example, an extension that uses Power Pack's User Roles in an approval hook will likely import the user role model at the module scope (we do this in an example in Review Board's documentation). The extension manager imports every extension's module before initializing any of them, so in this case the related model gets registered but is unreachable through `AppConfig`. Then if something accesses the model's relation tree before the extensions are registered, the tree is cached without the extension model's relation. This was bug was discovered when trying to import User Roles in RBCommons' team admin code. This change makes `ExtensionManager._add_to_installed_apps()` clear the cache itself, so that model relations can be properly populated.
    5d5cec0b844399cd92d0b056dcc664e7bc0df23d
    Description From Last Updated

    I worry a little that future us will see that set_installed_apps() clears caches and we'll remove this line. Can we …

    david david

    This might be a little clearer as "Access the relation tree now, both to sanity-check that the relation is absent …

    david david

    This seems a little overly complicated. How about: self.assertEqual(user.test_extension_related_models.count(), 1) Same for the check at the end of this method.

    david david

    Can we add one more assertion between these two lines to verify that disabling removes the relation? self.extension_mgr.disable_extension(extension.id) self.assertNotIn('test_extension_related_models', {f.name …

    david david
    Checks run (2 succeeded)
    flake8 passed.
    JSHint passed.
    david
    1. 
        
    2. djblets/extensions/manager.py (Diff revision 1)
       
       
       
       
       
      Show all issues

      I worry a little that future us will see that set_installed_apps() clears caches and we'll remove this line. Can we be a little more explanatory in the mechanics?

      # Django's set_installed_apps() calls clear_cache() twice (its own,
      # and the one at the end of populate()), but both run while
      # Apps.ready is False. clear_cache() only calls _expire_cache() on
      # each model when the registry is ready, so neither call rebuilds
      # the per-model caches.
      #
      # Without this, a model's relation tree may have been cached before
      # an extension providing a related model was registered, leaving
      # that relation permanently missing from the tree.
      
    3. Show all issues

      This might be a little clearer as "Access the relation tree now, both to sanity-check that the relation is absent and to warm the cache for the tree in that state."

    4. djblets/extensions/tests/test_extension_manager.py (Diff revision 1)
       
       
       
       
       
      Show all issues

      This seems a little overly complicated. How about:

      self.assertEqual(user.test_extension_related_models.count(), 1)
      

      Same for the check at the end of this method.

    5. Show all issues

      Can we add one more assertion between these two lines to verify that disabling removes the relation?

      self.extension_mgr.disable_extension(extension.id)
      
      self.assertNotIn('test_extension_related_models',
                       {f.name for f in User._meta.get_fields()})
      
      self.extension_mgr.enable_extension(extension.id)
      
    6.