fix(pairing): restore durable atomic writes
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
@@ -447,8 +448,17 @@ def _cleanup_tool_result_buckets(root: Path, current_bucket: Path) -> None:
|
|||||||
def _write_text_atomic(path: Path, content: str) -> None:
|
def _write_text_atomic(path: Path, content: str) -> None:
|
||||||
tmp = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
|
tmp = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
|
||||||
try:
|
try:
|
||||||
tmp.write_text(content, encoding="utf-8")
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
tmp.replace(path)
|
tmp.replace(path)
|
||||||
|
with suppress(OSError, NotImplementedError):
|
||||||
|
dfd = os.open(path.parent, os.O_RDONLY)
|
||||||
|
try:
|
||||||
|
os.fsync(dfd)
|
||||||
|
finally:
|
||||||
|
os.close(dfd)
|
||||||
finally:
|
finally:
|
||||||
if tmp.exists():
|
if tmp.exists():
|
||||||
tmp.unlink(missing_ok=True)
|
tmp.unlink(missing_ok=True)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import tiktoken
|
import tiktoken
|
||||||
|
|
||||||
from nanobot.utils.helpers import split_message, truncate_text_to_tokens
|
from nanobot.utils import helpers
|
||||||
|
from nanobot.utils.helpers import _write_text_atomic, split_message, truncate_text_to_tokens
|
||||||
|
|
||||||
|
|
||||||
def test_split_message_no_code_blocks_unchanged():
|
def test_split_message_no_code_blocks_unchanged():
|
||||||
@@ -31,3 +34,44 @@ def test_truncate_text_to_tokens_non_positive_budget_returns_text():
|
|||||||
text = "anything"
|
text = "anything"
|
||||||
|
|
||||||
assert truncate_text_to_tokens(text, 0) == text
|
assert truncate_text_to_tokens(text, 0) == text
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_text_atomic_fsyncs_file_and_parent_directory(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
target = tmp_path / "pairing.json"
|
||||||
|
fsync_calls: list[int] = []
|
||||||
|
closed_fds: list[int] = []
|
||||||
|
|
||||||
|
def fake_fsync(fd: int) -> None:
|
||||||
|
fsync_calls.append(fd)
|
||||||
|
|
||||||
|
monkeypatch.setattr(helpers.os, "fsync", fake_fsync)
|
||||||
|
monkeypatch.setattr(helpers.os, "open", lambda path, flags: 12345)
|
||||||
|
monkeypatch.setattr(helpers.os, "close", lambda fd: closed_fds.append(fd))
|
||||||
|
|
||||||
|
_write_text_atomic(target, '{"approved": {}}')
|
||||||
|
|
||||||
|
assert target.read_text(encoding="utf-8") == '{"approved": {}}'
|
||||||
|
assert len(fsync_calls) == 2
|
||||||
|
assert fsync_calls[0] != 12345
|
||||||
|
assert fsync_calls[1] == 12345
|
||||||
|
assert closed_fds == [12345]
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_text_atomic_keeps_file_when_directory_fsync_is_unsupported(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
target = tmp_path / "pairing.json"
|
||||||
|
fsync_calls: list[int] = []
|
||||||
|
|
||||||
|
def fake_open(path, flags):
|
||||||
|
raise OSError("directory fsync unsupported")
|
||||||
|
|
||||||
|
monkeypatch.setattr(helpers.os, "fsync", lambda fd: fsync_calls.append(fd))
|
||||||
|
monkeypatch.setattr(helpers.os, "open", fake_open)
|
||||||
|
|
||||||
|
_write_text_atomic(target, '{"pending": {}}')
|
||||||
|
|
||||||
|
assert target.read_text(encoding="utf-8") == '{"pending": {}}'
|
||||||
|
assert len(fsync_calls) == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user