mirror of
https://github.com/home-assistant/core.git
synced 2026-09-04 18:15:04 -05:00
Add asyncio-level timeout to Backblaze B2 uploads (#160468)
This commit is contained in:
committed by
Bram Kragten
parent
00ad44cb91
commit
5ccdfda747
@@ -36,6 +36,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# Cache TTL for backup list (in seconds)
|
||||
CACHE_TTL = 300
|
||||
|
||||
# Timeout for upload operations (in seconds)
|
||||
# This prevents uploads from hanging indefinitely
|
||||
UPLOAD_TIMEOUT = 43200 # 12 hours (matches B2 HTTP timeout)
|
||||
|
||||
|
||||
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
|
||||
"""Return the suggested filenames for the backup and metadata files."""
|
||||
@@ -329,13 +333,28 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
_LOGGER.debug("Uploading backup file %s with streaming", filename)
|
||||
try:
|
||||
content_type, _ = mimetypes.guess_type(filename)
|
||||
file_version = await self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
file_version = await asyncio.wait_for(
|
||||
self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
),
|
||||
timeout=UPLOAD_TIMEOUT,
|
||||
)
|
||||
except TimeoutError:
|
||||
_LOGGER.error(
|
||||
"Upload of %s timed out after %s seconds", filename, UPLOAD_TIMEOUT
|
||||
)
|
||||
reader.abort()
|
||||
raise BackupAgentError(
|
||||
f"Upload timed out after {UPLOAD_TIMEOUT} seconds"
|
||||
) from None
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.warning("Upload of %s was cancelled", filename)
|
||||
reader.abort()
|
||||
raise
|
||||
finally:
|
||||
reader.close()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Backblaze B2 backup agent tests."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from io import StringIO
|
||||
import json
|
||||
@@ -863,3 +864,94 @@ async def test_metadata_downloads_are_sequential(
|
||||
assert response["success"]
|
||||
# Verify downloads were sequential (max 1 at a time)
|
||||
assert max_concurrent == 1
|
||||
|
||||
|
||||
async def test_upload_timeout(
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test upload timeout handling."""
|
||||
client = await hass_client()
|
||||
|
||||
mock_file_info = Mock()
|
||||
mock_file_info.delete = Mock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
|
||||
return_value=TEST_BACKUP,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.read_backup",
|
||||
return_value=TEST_BACKUP,
|
||||
),
|
||||
patch("pathlib.Path.open") as mocked_open,
|
||||
patch(
|
||||
"homeassistant.components.backblaze_b2.backup.BackblazeBackupAgent._upload_unbound_stream_sync",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backblaze_b2.backup.asyncio.wait_for",
|
||||
side_effect=TimeoutError,
|
||||
),
|
||||
patch.object(
|
||||
BucketSimulator,
|
||||
"get_file_info_by_name",
|
||||
return_value=mock_file_info,
|
||||
),
|
||||
caplog.at_level(logging.ERROR),
|
||||
):
|
||||
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
|
||||
resp = await client.post(
|
||||
f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.entry_id}",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
|
||||
assert resp.status == 201
|
||||
assert any("timed out" in msg for msg in caplog.messages)
|
||||
|
||||
|
||||
async def test_upload_cancelled(
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test upload cancellation handling."""
|
||||
client = await hass_client()
|
||||
|
||||
mock_file_info = Mock()
|
||||
mock_file_info.delete = Mock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
|
||||
return_value=TEST_BACKUP,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.read_backup",
|
||||
return_value=TEST_BACKUP,
|
||||
),
|
||||
patch("pathlib.Path.open") as mocked_open,
|
||||
patch(
|
||||
"homeassistant.components.backblaze_b2.backup.BackblazeBackupAgent._upload_unbound_stream_sync",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backblaze_b2.backup.asyncio.wait_for",
|
||||
side_effect=asyncio.CancelledError,
|
||||
),
|
||||
patch.object(
|
||||
BucketSimulator,
|
||||
"get_file_info_by_name",
|
||||
return_value=mock_file_info,
|
||||
),
|
||||
caplog.at_level(logging.WARNING),
|
||||
):
|
||||
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
|
||||
resp = await client.post(
|
||||
f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.entry_id}",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
|
||||
# CancelledError propagates up and causes a 500 error
|
||||
assert resp.status == 500
|
||||
assert any("cancelled" in msg for msg in caplog.messages)
|
||||
|
||||
Reference in New Issue
Block a user