mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 10:16:02 -05:00
sandbox: drop a failed entry so entry_setup can be retried (Phase 6)
A failed async_setup left the rebuilt ConfigEntry in the sandbox's config_entries (only unload popped it), so main's later retry of the same entry_id was rejected with 'entry already loaded'. On both failure paths (async_setup raised / returned False) the entry is now popped before returning ok=False, so a re-sent entry_setup starts clean. Sandbox-side half of plan 1's SETUP_RETRY decision: main shipped honest SETUP_ERROR + manual reload and remains the only retry driver. The sandbox-side ConfigEntryNotReady timer is deliberately NOT enabled (the sandbox hass is never async_started).
This commit is contained in:
@@ -103,10 +103,19 @@ class EntryRunner:
|
||||
_LOGGER.exception(
|
||||
"sandbox entry_setup raised for %s (%s)", entry.title, entry.domain
|
||||
)
|
||||
# Drop the failed entry so a re-sent entry_setup for the same
|
||||
# entry_id isn't rejected with "entry already loaded". Main is the
|
||||
# only retry driver (the sandbox hass is never started, so its own
|
||||
# ConfigEntryNotReady timer never fires) — this just makes the
|
||||
# re-send start clean.
|
||||
config_entries._entries.pop(entry.entry_id, None) # noqa: SLF001
|
||||
return pb.EntrySetupResult(
|
||||
ok=False, reason=str(err) or err.__class__.__name__
|
||||
)
|
||||
if not ok:
|
||||
# Same cleanup on a plain failed setup (returns False / SETUP_ERROR
|
||||
# / SETUP_RETRY) so the entry_id is free for main's retry.
|
||||
config_entries._entries.pop(entry.entry_id, None) # noqa: SLF001
|
||||
return pb.EntrySetupResult(
|
||||
ok=False, reason=entry.reason or f"async_setup returned {ok!r}"
|
||||
)
|
||||
|
||||
@@ -173,6 +173,87 @@ async def test_entry_setup_reports_failure_reason(
|
||||
assert result.HasField("reason")
|
||||
|
||||
|
||||
async def test_failed_entry_setup_is_retryable(
|
||||
channels: tuple[Channel, Channel], runner: EntryRunner
|
||||
) -> None:
|
||||
"""A failed setup frees the entry_id so main can re-send entry_setup.
|
||||
|
||||
The first attempt fails; the entry must not linger in the sandbox's
|
||||
config_entries (else the retry is rejected with "entry already loaded").
|
||||
The second attempt for the same entry_id then succeeds.
|
||||
"""
|
||||
main, sandbox = channels
|
||||
runner.register(sandbox)
|
||||
main.start()
|
||||
sandbox.start()
|
||||
|
||||
attempts: list[str] = []
|
||||
|
||||
async def _async_setup_entry(hass: Any, entry: ConfigEntry) -> bool:
|
||||
attempts.append(entry.entry_id)
|
||||
# Fail the first attempt, succeed the second.
|
||||
return len(attempts) >= 2
|
||||
|
||||
async def _async_unload_entry(_hass: Any, _entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
class _DemoFlow(ConfigFlow, domain="demo_retry"):
|
||||
VERSION = 1
|
||||
|
||||
assert "demo_retry" in ha_config_entries.HANDLERS
|
||||
|
||||
module = ModuleType("homeassistant.components.demo_retry")
|
||||
module.DOMAIN = "demo_retry"
|
||||
module.async_setup_entry = _async_setup_entry # type: ignore[attr-defined]
|
||||
module.async_unload_entry = _async_unload_entry # type: ignore[attr-defined]
|
||||
config_flow_module = ModuleType("homeassistant.components.demo_retry.config_flow")
|
||||
runner.hass.data[ha_loader.DATA_COMPONENTS]["demo_retry"] = module
|
||||
runner.hass.data[ha_loader.DATA_COMPONENTS]["demo_retry.config_flow"] = (
|
||||
config_flow_module
|
||||
)
|
||||
|
||||
integration = ha_loader.Integration(
|
||||
runner.hass,
|
||||
"homeassistant.components.demo_retry",
|
||||
None,
|
||||
{
|
||||
"domain": "demo_retry",
|
||||
"name": "Demo Retry",
|
||||
"config_flow": True,
|
||||
"documentation": "https://example.com",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"codeowners": [],
|
||||
},
|
||||
None,
|
||||
)
|
||||
runner.hass.data[ha_loader.DATA_INTEGRATIONS] = runner.hass.data.get(
|
||||
ha_loader.DATA_INTEGRATIONS, {}
|
||||
)
|
||||
runner.hass.data[ha_loader.DATA_INTEGRATIONS]["demo_retry"] = integration
|
||||
|
||||
payload = pb.EntrySetup(
|
||||
entry_id="retry_entry_id",
|
||||
domain="demo_retry",
|
||||
title="Demo Retry",
|
||||
source="user",
|
||||
version=1,
|
||||
minor_version=1,
|
||||
)
|
||||
|
||||
# First attempt fails → ok=False and the entry is dropped.
|
||||
result1 = await main.call("sandbox/entry_setup", payload)
|
||||
assert result1.ok is False
|
||||
assert runner.hass.config_entries.async_get_entry("retry_entry_id") is None
|
||||
|
||||
# Retry with the same entry_id succeeds — no "entry already loaded".
|
||||
result2 = await main.call("sandbox/entry_setup", payload)
|
||||
assert result2.ok is True
|
||||
assert not result2.HasField("reason")
|
||||
assert attempts == ["retry_entry_id", "retry_entry_id"]
|
||||
|
||||
|
||||
async def test_call_service_dispatches_through_services(
|
||||
channels: tuple[Channel, Channel], runner: EntryRunner
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user