Serialize gateway log compaction safely
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user