From ab3e2d6905e98af5c751832be6fc956335e06453 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 7 Sep 2026 03:25:22 +0500 Subject: [PATCH 1/3] fix(sync): keep the watch alive when the definition file cannot be read --- samcli/lib/utils/definition_validator.py | 22 ++++++++++++++++++- .../lib/utils/test_definition_validator.py | 16 ++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/samcli/lib/utils/definition_validator.py b/samcli/lib/utils/definition_validator.py index bab018a6324..781a7fbb768 100644 --- a/samcli/lib/utils/definition_validator.py +++ b/samcli/lib/utils/definition_validator.py @@ -7,6 +7,7 @@ import yaml from watchdog.events import FileOpenedEvent, FileSystemEvent +from samcli.lib.utils.retry import retry from samcli.yamlhelper import parse_yaml_file LOG = logging.getLogger(__name__) @@ -75,7 +76,7 @@ def validate_file(self) -> bool: return False try: - self._data = parse_yaml_file(str(self._path)) + self._data = self._parse_file() except (ValueError, yaml.YAMLError) as e: LOG.debug( "File %s failed to validate due to it file cannot be parsed. \ @@ -84,4 +85,23 @@ def validate_file(self) -> bool: exc_info=e, ) return False + except OSError as e: + LOG.warning( + "File %s failed to validate because it cannot be read. \ +It may be locked by another process. The change will not be synced until the file is saved again.", + self._path, + exc_info=e, + ) + return False return True + + @retry(exc=OSError, exc_raise=OSError, exc_raise_msg="File cannot be read.") + def _parse_file(self) -> Dict[str, Any]: + """Read and parse the definition file, retrying while it is unreadable. + + Returns + ------- + Dict[str, Any] + Parsed content of the definition file. + """ + return parse_yaml_file(str(self._path)) diff --git a/tests/unit/lib/utils/test_definition_validator.py b/tests/unit/lib/utils/test_definition_validator.py index bbb16e1a4fa..abd1a9de4ee 100644 --- a/tests/unit/lib/utils/test_definition_validator.py +++ b/tests/unit/lib/utils/test_definition_validator.py @@ -69,3 +69,19 @@ def test_detect_change_for_file_opened_event(self, parse_yaml_file_mock): validator = DefinitionValidator(self.path, detect_change=True, initialize_data=True) event = FileOpenedEvent("src_path") self.assertFalse(validator.validate_change(event)) + + @patch("samcli.lib.utils.retry.time.sleep") + @patch("samcli.lib.utils.definition_validator.parse_yaml_file") + def test_detect_change_retries_unreadable_file(self, parse_yaml_file_mock, sleep_mock): + parse_yaml_file_mock.side_effect = [{"A": 1}, PermissionError(13, "Permission denied"), {"B": 1}] + + validator = DefinitionValidator(self.path, detect_change=True, initialize_data=True) + self.assertTrue(validator.validate_change()) + + @patch("samcli.lib.utils.retry.time.sleep") + @patch("samcli.lib.utils.definition_validator.parse_yaml_file") + def test_detect_change_unreadable_file(self, parse_yaml_file_mock, sleep_mock): + parse_yaml_file_mock.side_effect = PermissionError(13, "Permission denied") + + validator = DefinitionValidator(self.path, detect_change=True, initialize_data=False) + self.assertFalse(validator.validate_change()) From 4e31182915d792cba74ca7c8350b18a89bbaa359 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 7 Sep 2026 03:39:36 +0500 Subject: [PATCH 2/3] fix(sync): retry only a locked file, and keep the original read error --- samcli/lib/utils/definition_validator.py | 20 +++++++++++++++---- .../lib/utils/test_definition_validator.py | 18 +++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/samcli/lib/utils/definition_validator.py b/samcli/lib/utils/definition_validator.py index 781a7fbb768..bd9a7301cc7 100644 --- a/samcli/lib/utils/definition_validator.py +++ b/samcli/lib/utils/definition_validator.py @@ -1,17 +1,20 @@ """DefinitionValidator for Validating YAML and JSON Files""" import logging +import time from pathlib import Path from typing import Any, Dict, Optional import yaml from watchdog.events import FileOpenedEvent, FileSystemEvent -from samcli.lib.utils.retry import retry from samcli.yamlhelper import parse_yaml_file LOG = logging.getLogger(__name__) +FILE_READ_ATTEMPTS = 3 +FILE_READ_RETRY_DELAY = 0.1 + class DefinitionValidator: _path: Path @@ -95,13 +98,22 @@ def validate_file(self) -> bool: return False return True - @retry(exc=OSError, exc_raise=OSError, exc_raise_msg="File cannot be read.") def _parse_file(self) -> Dict[str, Any]: - """Read and parse the definition file, retrying while it is unreadable. + """Read and parse the definition file, retrying while it is locked by another process. Returns ------- Dict[str, Any] Parsed content of the definition file. """ - return parse_yaml_file(str(self._path)) + remaining_attempts = FILE_READ_ATTEMPTS + delay = FILE_READ_RETRY_DELAY + while True: + try: + return parse_yaml_file(str(self._path)) + except PermissionError: + remaining_attempts -= 1 + if not remaining_attempts: + raise + time.sleep(delay) + delay *= 2 diff --git a/tests/unit/lib/utils/test_definition_validator.py b/tests/unit/lib/utils/test_definition_validator.py index abd1a9de4ee..4a203ec8c8a 100644 --- a/tests/unit/lib/utils/test_definition_validator.py +++ b/tests/unit/lib/utils/test_definition_validator.py @@ -70,18 +70,28 @@ def test_detect_change_for_file_opened_event(self, parse_yaml_file_mock): event = FileOpenedEvent("src_path") self.assertFalse(validator.validate_change(event)) - @patch("samcli.lib.utils.retry.time.sleep") + @patch("samcli.lib.utils.definition_validator.time.sleep") @patch("samcli.lib.utils.definition_validator.parse_yaml_file") - def test_detect_change_retries_unreadable_file(self, parse_yaml_file_mock, sleep_mock): + def test_detect_change_retries_locked_file(self, parse_yaml_file_mock, sleep_mock): parse_yaml_file_mock.side_effect = [{"A": 1}, PermissionError(13, "Permission denied"), {"B": 1}] validator = DefinitionValidator(self.path, detect_change=True, initialize_data=True) self.assertTrue(validator.validate_change()) - @patch("samcli.lib.utils.retry.time.sleep") + @patch("samcli.lib.utils.definition_validator.time.sleep") @patch("samcli.lib.utils.definition_validator.parse_yaml_file") - def test_detect_change_unreadable_file(self, parse_yaml_file_mock, sleep_mock): + def test_detect_change_locked_file(self, parse_yaml_file_mock, sleep_mock): parse_yaml_file_mock.side_effect = PermissionError(13, "Permission denied") validator = DefinitionValidator(self.path, detect_change=True, initialize_data=False) self.assertFalse(validator.validate_change()) + self.assertEqual(parse_yaml_file_mock.call_count, 3) + + @patch("samcli.lib.utils.definition_validator.time.sleep") + @patch("samcli.lib.utils.definition_validator.parse_yaml_file") + def test_detect_change_unreadable_file(self, parse_yaml_file_mock, sleep_mock): + parse_yaml_file_mock.side_effect = IsADirectoryError(21, "Is a directory") + + validator = DefinitionValidator(self.path, detect_change=True, initialize_data=False) + self.assertFalse(validator.validate_change()) + self.assertEqual(parse_yaml_file_mock.call_count, 1) From 3d69119ab472ce81739304720591e05419e1f97f Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 7 Sep 2026 03:58:08 +0500 Subject: [PATCH 3/3] fix(sync): drop the lock claim from the unreadable file warning --- samcli/lib/utils/definition_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samcli/lib/utils/definition_validator.py b/samcli/lib/utils/definition_validator.py index bd9a7301cc7..2a92daf47da 100644 --- a/samcli/lib/utils/definition_validator.py +++ b/samcli/lib/utils/definition_validator.py @@ -91,7 +91,7 @@ def validate_file(self) -> bool: except OSError as e: LOG.warning( "File %s failed to validate because it cannot be read. \ -It may be locked by another process. The change will not be synced until the file is saved again.", +The change will not be synced until the file is saved again.", self._path, exc_info=e, )