11import logging
2+ import threading
23
34from django .apps import apps
45from netbox .plugins import get_plugin_config
910
1011logger = logging .getLogger ("netbox_custom_objects_tab" )
1112
13+ # ── Deferred initialisation (avoids DB queries during ready()) ───────────
14+ # Typed-tab registration queries CustomObjectTypeField and ContentType.
15+ # Running those queries in AppConfig.ready() triggers warnings from Django
16+ # ("Accessing the database during app initialization is discouraged") and
17+ # from netbox_branching ("Routing database query … before branching support
18+ # is initialized"). We defer that work to the first HTTP request via
19+ # Django's request_started signal. See GitHub issue #4.
20+
21+ _deferred_init_lock = threading .Lock ()
22+ _deferred_init_done = False
23+ _deferred_config = {} # populated in register_tabs(), consumed in _deferred_typed_init()
24+
1225
1326def _resolve_dynamic_custom_object_models ():
1427 """
@@ -162,10 +175,52 @@ def _deduplicate_registry():
162175 model_map [model_name ] = deduped
163176
164177
178+ def _deferred_typed_init (sender = None , ** kwargs ):
179+ """
180+ One-shot ``request_started`` handler that performs DB-dependent initialisation.
181+
182+ Registers typed tabs, injects CO URL patterns, and deduplicates the view
183+ registry. Runs exactly once on the first HTTP request, then disconnects
184+ itself so subsequent requests pay no overhead.
185+ """
186+ global _deferred_init_done
187+ with _deferred_init_lock :
188+ if _deferred_init_done :
189+ return
190+ _deferred_init_done = True
191+
192+ from django .core .signals import request_started
193+
194+ request_started .disconnect (_deferred_typed_init , dispatch_uid = "netbox_custom_objects_tab_deferred" )
195+
196+ config = _deferred_config
197+ combined_models = config .get ("combined_models" , [])
198+ typed_labels = config .get ("typed_labels" , [])
199+ typed_weight = config .get ("typed_weight" , 2100 )
200+
201+ typed_models = []
202+ if typed_labels :
203+ typed_models = _resolve_model_labels (typed_labels )
204+ register_typed_tabs (typed_models , typed_weight )
205+
206+ # Inject URL patterns for CO dynamic models (combined + typed).
207+ if any (m ._meta .app_label == _CUSTOM_OBJECTS_APP for m in combined_models + typed_models ):
208+ _inject_co_urls ()
209+
210+ # Deduplicate the registry — netbox_custom_objects re-registers journal/changelog
211+ # on every get_model() cache miss, producing duplicate tabs.
212+ _deduplicate_registry ()
213+
214+
165215def register_tabs ():
166216 """
167217 Read plugin config and register both combined and typed tabs.
168218 Called from AppConfig.ready().
219+
220+ Combined tabs are registered immediately (no DB queries needed).
221+ Typed tabs and other DB-dependent work are deferred to the first HTTP
222+ request via the ``request_started`` signal to avoid DB access during
223+ app initialisation (GitHub issue #4).
169224 """
170225 try :
171226 combined_labels = get_plugin_config ("netbox_custom_objects_tab" , "combined_models" )
@@ -177,21 +232,22 @@ def register_tabs():
177232 logger .exception ("Could not read netbox_custom_objects_tab plugin config" )
178233 return
179234
235+ # Phase 1 — immediate: combined tabs do not query the database.
180236 combined_models = []
181237 if combined_labels :
182238 combined_models = _resolve_model_labels (combined_labels )
183239 register_combined_tabs (combined_models , combined_label , combined_weight )
184240
185- typed_models = []
186- if typed_labels :
187- typed_models = _resolve_model_labels (typed_labels )
188- register_typed_tabs (typed_models , typed_weight )
241+ # Phase 2 — deferred: typed tabs, CO URL injection, and registry dedup
242+ # all require DB access and must wait until the first request.
243+ _deferred_config .update (
244+ {
245+ "combined_models" : combined_models ,
246+ "typed_labels" : typed_labels ,
247+ "typed_weight" : typed_weight ,
248+ }
249+ )
189250
190- # For any CO dynamic models we registered tabs for, inject URL patterns into
191- # netbox_custom_objects.urls so that tab URL reversal works.
192- if any (m ._meta .app_label == _CUSTOM_OBJECTS_APP for m in combined_models + typed_models ):
193- _inject_co_urls ()
251+ from django .core .signals import request_started
194252
195- # Deduplicate the registry — netbox_custom_objects re-registers journal/changelog
196- # on every get_model() cache miss, producing duplicate tabs.
197- _deduplicate_registry ()
253+ request_started .connect (_deferred_typed_init , dispatch_uid = "netbox_custom_objects_tab_deferred" )
0 commit comments