A02: Atomic cooldown state with exclusive lock and flush
- _write_state: write to temp file, fsync, rename atomically - Acquire exclusive lock before any file operations - Flush and fsync before unlock to prevent data loss - Remove stale .tmp file after successful write - Add test for atomic write behavior - 109 Python tests pass
This commit is contained in:
@@ -6,6 +6,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import tempfile
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
@@ -55,14 +56,22 @@ def _read_state(state_file: Path) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _write_state(state_file: Path, state: dict) -> None:
|
def _write_state(state_file: Path, state: dict) -> None:
|
||||||
"""Write state file atomically with exclusive lock."""
|
"""A02: Atomic write with exclusive lock, flush before unlock.
|
||||||
|
|
||||||
|
Writes to temp file first, then renames atomically. Lock is acquired
|
||||||
|
before any file operations to prevent race conditions.
|
||||||
|
"""
|
||||||
state_file.parent.mkdir(parents=True, exist_ok=True)
|
state_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(state_file, "w") as f:
|
temp_file = state_file.with_suffix(".tmp")
|
||||||
|
with open(temp_file, "w") as f:
|
||||||
fcntl.flock(f, fcntl.LOCK_EX)
|
fcntl.flock(f, fcntl.LOCK_EX)
|
||||||
try:
|
try:
|
||||||
f.write(json.dumps(state, sort_keys=True))
|
f.write(json.dumps(state, sort_keys=True))
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
finally:
|
finally:
|
||||||
fcntl.flock(f, fcntl.LOCK_UN)
|
fcntl.flock(f, fcntl.LOCK_UN)
|
||||||
|
temp_file.replace(state_file)
|
||||||
|
|
||||||
|
|
||||||
def fetch_site_key(url: str) -> str:
|
def fetch_site_key(url: str) -> str:
|
||||||
|
|||||||
@@ -57,3 +57,20 @@ def test_validate_url_host_rejects_missing_hostname() -> None:
|
|||||||
_validate_url_host("not-a-valid-url")
|
_validate_url_host("not-a-valid-url")
|
||||||
with pytest.raises(ValueError, match="valid hostname"):
|
with pytest.raises(ValueError, match="valid hostname"):
|
||||||
_validate_url_host("")
|
_validate_url_host("")
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_state_is_atomic_with_flush(tmp_path: Path) -> None:
|
||||||
|
"""A02: _write_state uses exclusive lock, flush, and atomic rename."""
|
||||||
|
from rf4_research.community_cli import _read_state, _write_state
|
||||||
|
|
||||||
|
state_file = tmp_path / "state.json"
|
||||||
|
_write_state(state_file, {"key1": "value1"})
|
||||||
|
assert state_file.exists()
|
||||||
|
assert not (state_file.with_suffix(".tmp")).exists()
|
||||||
|
state = _read_state(state_file)
|
||||||
|
assert state == {"key1": "value1"}
|
||||||
|
|
||||||
|
_write_state(state_file, {"key1": "value2", "key2": "value3"})
|
||||||
|
state = _read_state(state_file)
|
||||||
|
assert state == {"key1": "value2", "key2": "value3"}
|
||||||
|
assert not (state_file.with_suffix(".tmp")).exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user