Serialize gateway log compaction safely

This commit is contained in:
HumbleTurtle
2026-08-01 04:21:56 -03:00
parent 5289965772
commit ffbe28798b
3 changed files with 41 additions and 29 deletions
-1
View File
@@ -863,7 +863,6 @@ def list_service(kind):
def service_logs(kind, args): def service_logs(kind, args):
max_lines = int(cli_option(args, "--lines", "--tail", default="120")) max_lines = int(cli_option(args, "--lines", "--tail", default="120"))
path = service_log_path(kind) path = service_log_path(kind)
compact_log_file(path)
lines = log_tail_lines(path, max_lines=max_lines) lines = log_tail_lines(path, max_lines=max_lines)
status = "ok" if path.exists() else "missing" status = "ok" if path.exists() else "missing"
return { return {
+29 -28
View File
@@ -614,34 +614,35 @@ def compact_log_file(path, max_bytes):
if max_bytes <= 0: if max_bytes <= 0:
return return
file_path = pathlib.Path(path) file_path = pathlib.Path(path)
try: with GATEWAY_LOG_LOCK:
size = file_path.stat().st_size try:
except FileNotFoundError: size = file_path.stat().st_size
return except FileNotFoundError:
except OSError: return
return except OSError:
if size <= max_bytes: return
return if size <= max_bytes:
try: return
with file_path.open("rb") as handle: try:
handle.seek(-max_bytes, os.SEEK_END) with file_path.open("rb") as handle:
chunk = handle.read(max_bytes) handle.seek(-max_bytes, os.SEEK_END)
except OSError: chunk = handle.read(max_bytes)
return except OSError:
newline_index = chunk.find(b"\n") return
if newline_index >= 0 and newline_index + 1 < len(chunk): newline_index = chunk.find(b"\n")
chunk = chunk[newline_index + 1 :] if newline_index >= 0 and newline_index + 1 < len(chunk):
try: chunk = chunk[newline_index + 1 :]
# Keep the inode stable: the daemon's stdout/stderr already point to try:
# this file. Replacing the pathname would leave the live process # Keep the inode stable: the daemon's stdout/stderr already point
# writing to a deleted inode and make `nap gateway logs` stale. # to this file. The same lock covers the read/truncate interval so
with GATEWAY_LOG_LOCK, file_path.open("r+b") as handle: # a concurrent gateway_log append cannot be discarded.
handle.seek(0) with file_path.open("r+b") as handle:
handle.write(chunk) handle.seek(0)
handle.truncate() handle.write(chunk)
handle.flush() handle.truncate()
except OSError: handle.flush()
return except OSError:
return
def start_gateway_log_compactor_thread(): def start_gateway_log_compactor_thread():
+12
View File
@@ -329,3 +329,15 @@ def test_cli_log_compaction_preserves_live_writer_inode(tmp_path):
content = log_path.read_bytes() content = log_path.read_bytes()
assert content.endswith(b"after-cli-compaction\n") assert content.endswith(b"after-cli-compaction\n")
assert b"recent-line\n" in content assert b"recent-line\n" in content
def test_logs_command_never_compacts_a_running_service_log(tmp_path, monkeypatch):
module = load_installer()
log_path = tmp_path / "gateway.log"
log_path.write_text("one\ntwo\n", encoding="utf-8")
monkeypatch.setattr(module, "service_log_path", lambda kind: log_path)
monkeypatch.setattr(module, "compact_log_file", fail_if_called)
result = module.service_logs("gateway", ["--lines", "1"])
assert result["lines"] == ["two"]