Skip to content

Commit 596bedb

Browse files
committed
feat: add typed service locator to SimulationContext
Add get_service(cls) / set_service(cls, instance) — a lightweight typed singleton registry on SimulationContext, keyed by service class. This lets backend-specific caches (e.g. Fabric hierarchy handles) register themselves without polluting SimulationContext with backend-specific fields or imports. Services with a close() method are automatically closed: - On replacement via set_service() - On teardown via clear_instance() No existing behavior changes — this is purely additive.
1 parent 3d10e74 commit 596bedb

1 file changed

Lines changed: 46 additions & 1 deletion

File tree

source/isaaclab/isaaclab/sim/simulation_context.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import traceback
1212
from collections.abc import Iterator
1313
from contextlib import contextmanager
14-
from typing import TYPE_CHECKING, Any
14+
from typing import TYPE_CHECKING, Any, TypeVar
1515

1616
import toml
1717
import torch
@@ -38,6 +38,8 @@
3838
if TYPE_CHECKING:
3939
from isaaclab.cloner.clone_plan import ClonePlan
4040

41+
_T = TypeVar("_T")
42+
4143
from .simulation_cfg import SimulationCfg
4244
from .spawners import DomeLightCfg, GroundPlaneCfg
4345

@@ -214,6 +216,11 @@ def __init__(self, cfg: SimulationCfg | None = None):
214216
order=5,
215217
)
216218

219+
# Singleton service registry — backend-specific caches register themselves
220+
# here, keyed by their class. Services with a ``close()`` method are closed
221+
# when the SimulationContext is torn down via ``clear_instance()``.
222+
self._services: dict[type, object] = {}
223+
217224
type(self)._instance = self # Mark as valid singleton only after successful init
218225

219226
def _apply_render_cfg_settings(self) -> None:
@@ -852,6 +859,38 @@ def get_setting(self, name: str) -> Any:
852859
"""Get a setting value."""
853860
return self._settings_helper.get(name)
854861

862+
# ------------------------------------------------------------------
863+
# Service locator
864+
# ------------------------------------------------------------------
865+
866+
def get_service(self, cls: type[_T]) -> _T | None:
867+
"""Retrieve a registered singleton service by its class.
868+
869+
Args:
870+
cls: The service class used as key.
871+
872+
Returns:
873+
The registered instance, or ``None`` if not registered.
874+
"""
875+
return self._services.get(cls) # type: ignore[return-value]
876+
877+
def set_service(self, cls: type[_T], instance: _T) -> None:
878+
"""Register a singleton service, keyed by its class.
879+
880+
Overwrites any previously registered instance for the same class.
881+
If the old instance has a ``close()`` method it is called before
882+
replacement. Services are automatically closed and cleared when
883+
:meth:`clear_instance` is called.
884+
885+
Args:
886+
cls: The service class used as key.
887+
instance: The service instance to register.
888+
"""
889+
old = self._services.get(cls)
890+
if old is not None and old is not instance and hasattr(old, "close"):
891+
old.close()
892+
self._services[cls] = instance
893+
855894
@classmethod
856895
def clear_instance(cls) -> None:
857896
"""Clean up resources and clear the singleton instance."""
@@ -865,6 +904,12 @@ def clear_instance(cls) -> None:
865904
viz.close()
866905
cls._instance._visualizers.clear()
867906

907+
# Close and drop all registered singleton services
908+
for service in cls._instance._services.values():
909+
if hasattr(service, "close"):
910+
service.close()
911+
cls._instance._services.clear()
912+
868913
# Tear down the stage. We skip clear_stage() (prim-by-prim deletion) since
869914
# close_stage() + app shutdown destroy the entire stage at once.
870915
stage_utils.close_stage()

0 commit comments

Comments
 (0)