refactor: collapse prune, _free_dest and the on_start collision loop

T3-21: prune() is retier() with no compressed tier (keeping its
backup_count <= 0 no-op); _free_dest was a no-op behind the tiered namer,
which already mints a free plain name; rotate_on_start repeated
make_history_namer's stamp + collision loop and now calls it. every
retention scenario (on_start/size/daily, legacy and tiered, same-second
collisions, foreign and legacy names) lists identically. one difference: a
crash-left plain twin of a .gz is now dropped before counting, so prune keeps
backup_count real logs instead of spending a slot on the duplicate.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
dsql committed 2026-10-16 04:40:06 -04:00
1 parent 5e53a7cd6c
commit 5b27df65e7
1 file changed
+9 -52
+9 -52
View File
@@ -35,22 +35,6 @@ def _move(source: str, dest: str) -> None:
shutil.move(source, dest)
def _free_dest(dest: str) -> str:
"""return `dest`, or a `.N`-suffixed variant if it (or its .gz twin) already exists
used by the tiered rotator so a second same-stamp roll doesn't clobber the earlier
file; checks both the plain and .gz forms of each candidate.
"""
if not os.path.exists(dest) and not os.path.exists(dest + ".gz"):
return dest
counter = 1
while True:
candidate = f"{dest}.{counter}"
if not os.path.exists(candidate) and not os.path.exists(candidate + ".gz"):
return candidate
counter += 1
def _gzip_file(source: str, dest: str) -> None:
"""atomically gzip source into dest, then remove source - or leave source untouched
@@ -169,10 +153,9 @@ def make_rotator(
if not os.path.exists(source):
return
if tiered:
# strip the namer's .gz suffix so the roll lands plain and retier decides
# its tier; disambiguate a same-interval collision via _free_dest
plain_dest = _free_dest(dest[:-3] if dest.endswith(".gz") else dest)
_move(source, plain_dest)
# the tiered namer (plain=True) already minted a free, plain .log name;
# retier decides its tier
_move(source, dest)
if log_dir is not None and prune_stem is not None:
retier(log_dir, prune_stem, keep_uncompressed or 0, keep_compressed or 0, live_stem)
return
@@ -214,21 +197,8 @@ def rotate_on_start(
tiered = keep_uncompressed is not None or keep_compressed is not None
live_stem = os.path.splitext(os.path.basename(live_path))[0]
stem = os.path.basename(history_stem) if history_stem else live_stem
stamp = time.strftime("%Y-%m-%d_%H-%M-%S", clock())
suffix = ".log.gz" if (compress and not tiered) else ".log"
# a 1-second stamp collision (rapid crash-restart) is disambiguated with a
# counter; check BOTH .log and .log.gz since a tiered same-stamp roll may
# already be compressed
def _taken(path: str) -> bool:
base = path[:-3] if path.endswith(".gz") else path
return os.path.exists(base) or os.path.exists(base + ".gz")
dest = os.path.join(log_dir, f"{stem}.{stamp}{suffix}")
counter = 1
while _taken(dest):
dest = os.path.join(log_dir, f"{stem}.{stamp}.{counter}{suffix}")
counter += 1
# same name + same-second collision counter as the size/daily rolls
dest = make_history_namer(stem, log_dir, compress, plain=tiered, clock=clock)(live_path)
if compress and not tiered:
_gzip_file(live_path, dest)
else:
@@ -368,26 +338,13 @@ def prune(log_dir: str, stem: str, backup_count: int, live_stem: Optional[str] =
(`<stem>.<stamp>[.N].log[.gz]`, see `_is_rolled_name`) - a foreign file that only
shares the `<stem>.` prefix (e.g. a project's own `<stem>.audit.log` dropped into
the same log_dir) is left alone rather than counted and deleted as a roll.
this is `retier` with no compressed tier (so a crash-left plain twin of a .gz is
dropped first, not counted); `backup_count <= 0` leaves history alone.
"""
if backup_count <= 0:
return
stem = os.path.basename(stem)
live_stem = os.path.basename(live_stem) if live_stem else None
try:
entries = [
os.path.join(log_dir, name)
for name in os.listdir(log_dir)
if name != f"{stem}.log" and _is_rolled_name(stem, name, live_stem)
]
except OSError:
return
files = [(p, _safe_mtime(p), _roll_counter(p)) for p in entries if os.path.isfile(p)]
files.sort(key=lambda t: (t[1], t[2]), reverse=True)
for path, _, _ in files[backup_count:]:
try:
os.remove(path)
except OSError:
pass
retier(log_dir, stem, backup_count, 0, live_stem)
def _safe_mtime(path: str) -> float: