From 4a79cbb6e7492d1f4f61c0058a3a1743a52042d9 Mon Sep 17 00:00:00 2001 From: sunpengcheng05 Date: Mon, 20 Jul 2026 18:17:39 +0800 Subject: [PATCH] fix(session): tolerate unsupported directory fsync --- nanobot/session/manager.py | 11 +++++--- tests/session/test_session_fsync.py | 42 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index dedf25cb..3587ef2e 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -1,6 +1,7 @@ """Session management for conversation history.""" import base64 +import errno import json import os import re @@ -688,12 +689,16 @@ class SessionManager: if fsync: # fsync the directory so the rename is durable. # On Windows, opening a directory with O_RDONLY raises - # PermissionError — skip the dir sync there (NTFS - # journals metadata synchronously). + # PermissionError; some shared filesystems allow the open but + # reject directory fsync with EINVAL. with suppress(PermissionError): fd = os.open(str(path.parent), os.O_RDONLY) try: - os.fsync(fd) + try: + os.fsync(fd) + except OSError as exc: + if exc.errno != errno.EINVAL: + raise finally: os.close(fd) except BaseException: diff --git a/tests/session/test_session_fsync.py b/tests/session/test_session_fsync.py index 3194cf95..ba18581b 100644 --- a/tests/session/test_session_fsync.py +++ b/tests/session/test_session_fsync.py @@ -2,6 +2,8 @@ from __future__ import annotations +import errno +import os import sys from pathlib import Path from unittest.mock import patch @@ -55,6 +57,46 @@ class TestSaveFsync: manager.save(session) mock_fsync.assert_not_called() + def test_save_ignores_unsupported_directory_fsync( + self, manager: SessionManager + ) -> None: + """Shared filesystems may open directories but reject directory fsync.""" + session = manager.get_or_create("test:unsupported-directory-fsync") + session.add_message("user", "hello") + directory_fd = 987654 + with ( + patch("nanobot.session.manager.os.open", return_value=directory_fd) as open_dir, + patch( + "nanobot.session.manager.os.fsync", + side_effect=[None, OSError(errno.EINVAL, "Invalid argument")], + ), + patch("nanobot.session.manager.os.close") as close_dir, + ): + manager.save(session, fsync=True) + + assert manager._get_session_path(session.key).exists() + open_dir.assert_called_once_with(str(manager.sessions_dir), os.O_RDONLY) + close_dir.assert_called_once_with(directory_fd) + + def test_save_propagates_other_directory_fsync_errors( + self, manager: SessionManager + ) -> None: + """Only EINVAL is an expected unsupported-directory-fsync result.""" + session = manager.get_or_create("test:directory-fsync-io-error") + directory_fd = 987654 + with ( + patch("nanobot.session.manager.os.open", return_value=directory_fd), + patch( + "nanobot.session.manager.os.fsync", + side_effect=[None, OSError(errno.EIO, "I/O error")], + ), + patch("nanobot.session.manager.os.close") as close_dir, + pytest.raises(OSError, match="I/O error"), + ): + manager.save(session, fsync=True) + + close_dir.assert_called_once_with(directory_fd) + class TestFlushAll: """Verify flush_all re-saves all cached sessions with fsync."""