Skip to content

Commit 310821e

Browse files
committed
Clean up failed storage probe files
1 parent c674d2f commit 310821e

File tree

2 files changed

+47
-17
lines changed

2 files changed

+47
-17
lines changed

health_check/checks.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,23 @@ def get_file_name(self):
253253
def get_file_content(self):
254254
return f"# generated by health_check.Storage at {datetime.datetime.now().timestamp()}".encode()
255255

256-
def check_save(self, file_name, file_content):
257-
# save the file
258-
file_name = self.storage.save(file_name, ContentFile(content=file_content))
256+
def check_written_file(self, file_name, file_content):
259257
# read the file and compare
260258
if not self.storage.exists(file_name):
261259
raise ServiceUnavailable("File does not exist")
262260
with self.storage.open(file_name) as f:
263261
if not f.read() == file_content:
264262
raise ServiceUnavailable("File content does not match")
265-
return file_name
263+
264+
def cleanup_file(self, file_name):
265+
try:
266+
self.storage.delete(file_name)
267+
except Exception:
268+
logger.warning(
269+
"Failed to clean up storage probe file %r after a failed health check.",
270+
file_name,
271+
exc_info=True,
272+
)
266273

267274
def check_delete(self, file_name):
268275
# delete the file and make sure it is gone
@@ -274,5 +281,10 @@ def run(self):
274281
# write the file to the storage backend
275282
file_name = self.get_file_name()
276283
file_content = self.get_file_content()
277-
file_name = self.check_save(file_name, file_content)
284+
file_name = self.storage.save(file_name, ContentFile(content=file_content))
285+
try:
286+
self.check_written_file(file_name, file_content)
287+
except Exception:
288+
self.cleanup_file(file_name)
289+
raise
278290
self.check_delete(file_name)

tests/test_checks.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -483,37 +483,55 @@ async def test_check_status__not_deleted(self):
483483
assert "File was not deleted" in str(result.error)
484484

485485
@pytest.mark.asyncio
486-
async def test_check_status__file_not_saved(self):
487-
"""Raise ServiceUnavailable when file does not exist after save."""
486+
@pytest.mark.parametrize(
487+
("exists_result", "read_result", "expected_message"),
488+
[
489+
(False, None, "does not exist"),
490+
(True, b"wrong content", "does not match"),
491+
],
492+
)
493+
async def test_check_status__failed_validation_cleans_up_saved_file(
494+
self, exists_result, read_result, expected_message
495+
):
496+
"""Delete the saved probe file when validation fails after write."""
488497
with mock.patch("health_check.checks.storages") as mock_storages:
489498
mock_storage = mock.MagicMock()
490499
mock_storages.__getitem__.return_value = mock_storage
491500
mock_storage.save.return_value = "test-file.txt"
492-
mock_storage.exists.return_value = False
501+
mock_storage.exists.return_value = exists_result
502+
if read_result is not None:
503+
mock_file = mock.MagicMock()
504+
mock_file.read.return_value = read_result
505+
mock_storage.open.return_value.__enter__.return_value = mock_file
493506

494507
check = Storage()
495508
result = await check.get_result()
496509
assert result.error is not None
497510
assert isinstance(result.error, ServiceUnavailable)
498-
assert "does not exist" in str(result.error)
511+
assert expected_message in str(result.error)
512+
mock_storage.delete.assert_called_once_with("test-file.txt")
499513

500514
@pytest.mark.asyncio
501-
async def test_check_status__file_content_mismatch(self):
502-
"""Raise ServiceUnavailable when file content does not match."""
503-
with mock.patch("health_check.checks.storages") as mock_storages:
515+
async def test_check_status__cleanup_failure_does_not_mask_validation_error(self):
516+
"""Log cleanup failure and return the original validation error."""
517+
with (
518+
mock.patch("health_check.checks.storages") as mock_storages,
519+
mock.patch("health_check.checks.logger") as mock_logger,
520+
):
504521
mock_storage = mock.MagicMock()
505522
mock_storages.__getitem__.return_value = mock_storage
506523
mock_storage.save.return_value = "test-file.txt"
507-
mock_storage.exists.return_value = True
508-
mock_file = mock.MagicMock()
509-
mock_file.read.return_value = b"wrong content"
510-
mock_storage.open.return_value.__enter__.return_value = mock_file
524+
mock_storage.exists.return_value = False
525+
mock_storage.delete.side_effect = OSError("delete failed")
511526

512527
check = Storage()
513528
result = await check.get_result()
529+
514530
assert result.error is not None
515531
assert isinstance(result.error, ServiceUnavailable)
516-
assert "does not match" in str(result.error)
532+
assert "does not exist" in str(result.error)
533+
mock_storage.delete.assert_called_once_with("test-file.txt")
534+
mock_logger.warning.assert_called_once()
517535

518536
@pytest.mark.asyncio
519537
async def test_check_status__service_unavailable_passthrough(self):

0 commit comments

Comments
 (0)