diff --git a/python/ray/serve/_private/deployment_state.py b/python/ray/serve/_private/deployment_state.py index 37c7a5f90d4c..940276c7a73c 100644 --- a/python/ray/serve/_private/deployment_state.py +++ b/python/ray/serve/_private/deployment_state.py @@ -1473,10 +1473,23 @@ def update_state(self, state: ReplicaState) -> None: """Updates state in actor details.""" self.update_actor_details(state=state) + _SENTINEL = object() + def update_actor_details(self, **kwargs) -> None: - details_kwargs = self._actor_details.dict() - details_kwargs.update(kwargs) - self._actor_details = ReplicaDetails(**details_kwargs) + # Fast path: skip if all provided values are already current. + # This avoids unnecessary object creation on every tick when the + # pop-iterate-readd pattern re-adds replicas without state changes. + # We use _SENTINEL (not None) as the getattr default so that an + # invalid field name always fails the check and falls through to + # .copy(), which will raise an appropriate error. + if all( + getattr(self._actor_details, k, self._SENTINEL) == v + for k, v in kwargs.items() + ): + return + # Use .copy(update=...) instead of .dict() + reconstruction to avoid + # full Pydantic serialization and validation on every update. + self._actor_details = self._actor_details.copy(update=kwargs) def resource_requirements(self) -> Tuple[str, str]: """Returns required and currently available resources.