Avoid falling back to listening for all states when a template render raises an exception (#89392)

When a template render raised an exception we would start listening for
all states until the template did not raise an exception anymore. This
was not needed since the entity that is causing the exception was
already in the tracker.

Re-rendering on all state changes can be extremely expensive and can
bring an instance into a sluggish or unresponsive state when updating
from a much older version that did not raise ValueError when a default
was missing.
pull/89366/head^2
J. Nick Koston 2023-03-08 10:50:34 -10:00 committed by GitHub
parent 84b5ea8ac0
commit cefba7c638
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 68 additions and 7 deletions

View File

@ -838,6 +838,10 @@ class TrackTemplateResultInfo:
self._track_state_changes: _TrackStateChangeFiltered | None = None
self._time_listeners: dict[Template, Callable[[], None]] = {}
def __repr__(self) -> str:
"""Return the representation."""
return f"<TrackTemplateResultInfo {self._info}>"
def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None:
"""Activation of template tracking."""
block_render = False
@ -1651,12 +1655,6 @@ def _render_infos_needs_all_listener(render_infos: Iterable[RenderInfo]) -> bool
if render_info.all_states or render_info.all_states_lifecycle:
return True
# Previous call had an exception
# so we do not know which states
# to track
if render_info.exception:
return True
return False

View File

@ -274,6 +274,8 @@ class RenderInfo:
f" entities={self.entities}"
f" rate_limit={self.rate_limit}"
f" has_time={self.has_time}"
f" exception={self.exception}"
f" is_static={self.is_static}"
">"
)

View File

@ -2209,7 +2209,7 @@ async def test_track_template_result_errors(
hass.states.async_set("switch.not_exist", "on")
await hass.async_block_till_done()
assert len(syntax_error_runs) == 1
assert len(syntax_error_runs) == 0
assert len(not_exist_runs) == 2
assert not_exist_runs[1][0].data.get("entity_id") == "switch.not_exist"
assert not_exist_runs[1][1] == template_not_exist
@ -2229,6 +2229,56 @@ async def test_track_template_result_errors(
assert isinstance(not_exist_runs[2][3], TemplateError)
async def test_track_template_result_transient_errors(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test tracking template with transient errors in the template."""
hass.states.async_set("sensor.error", "unknown")
template_that_raises_sometimes = Template(
"{{ states('sensor.error') | float }}", hass
)
sometimes_error_runs = []
@ha.callback
def sometimes_error_listener(event, updates):
track_result = updates.pop()
sometimes_error_runs.append(
(
event,
track_result.template,
track_result.last_result,
track_result.result,
)
)
info = async_track_template_result(
hass,
[TrackTemplate(template_that_raises_sometimes, None)],
sometimes_error_listener,
)
await hass.async_block_till_done()
assert sometimes_error_runs == []
assert "ValueError" in caplog.text
assert "ValueError" in repr(info)
caplog.clear()
hass.states.async_set("sensor.error", "unavailable")
await hass.async_block_till_done()
assert len(sometimes_error_runs) == 1
assert isinstance(sometimes_error_runs[0][3], TemplateError)
sometimes_error_runs.clear()
assert "ValueError" in repr(info)
hass.states.async_set("sensor.error", "4")
await hass.async_block_till_done()
assert len(sometimes_error_runs) == 1
assert sometimes_error_runs[0][3] == 4.0
sometimes_error_runs.clear()
assert "ValueError" not in repr(info)
async def test_static_string(hass: HomeAssistant) -> None:
"""Test a static string."""
template_refresh = Template("{{ 'static' }}", hass)

View File

@ -4323,3 +4323,14 @@ def test_contains(hass: HomeAssistant, seq, value, expected) -> None:
)
== expected
)
async def test_render_to_info_with_exception(hass: HomeAssistant) -> None:
"""Test info is still available if the template has an exception."""
hass.states.async_set("test_domain.object", "dog")
info = render_to_info(hass, '{{ states("test_domain.object") | float }}')
with pytest.raises(TemplateError, match="no default was specified"):
info.result()
assert info.all_states is False
assert info.entities == {"test_domain.object"}