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:
1 parent
5e53a7cd6c
commit
5b27df65e7
1 file changed
+9
-52
@@ -35,22 +35,6 @@ def _move(source: str, dest: str) -> None:
|
|||||||
shutil.move(source, dest)
|
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:
|
def _gzip_file(source: str, dest: str) -> None:
|
||||||
"""atomically gzip source into dest, then remove source - or leave source untouched
|
"""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):
|
if not os.path.exists(source):
|
||||||
return
|
return
|
||||||
if tiered:
|
if tiered:
|
||||||
# strip the namer's .gz suffix so the roll lands plain and retier decides
|
# the tiered namer (plain=True) already minted a free, plain .log name;
|
||||||
# its tier; disambiguate a same-interval collision via _free_dest
|
# retier decides its tier
|
||||||
plain_dest = _free_dest(dest[:-3] if dest.endswith(".gz") else dest)
|
_move(source, dest)
|
||||||
_move(source, plain_dest)
|
|
||||||
if log_dir is not None and prune_stem is not None:
|
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)
|
retier(log_dir, prune_stem, keep_uncompressed or 0, keep_compressed or 0, live_stem)
|
||||||
return
|
return
|
||||||
@@ -214,21 +197,8 @@ def rotate_on_start(
|
|||||||
tiered = keep_uncompressed is not None or keep_compressed is not None
|
tiered = keep_uncompressed is not None or keep_compressed is not None
|
||||||
live_stem = os.path.splitext(os.path.basename(live_path))[0]
|
live_stem = os.path.splitext(os.path.basename(live_path))[0]
|
||||||
stem = os.path.basename(history_stem) if history_stem else live_stem
|
stem = os.path.basename(history_stem) if history_stem else live_stem
|
||||||
stamp = time.strftime("%Y-%m-%d_%H-%M-%S", clock())
|
# same name + same-second collision counter as the size/daily rolls
|
||||||
suffix = ".log.gz" if (compress and not tiered) else ".log"
|
dest = make_history_namer(stem, log_dir, compress, plain=tiered, clock=clock)(live_path)
|
||||||
|
|
||||||
# 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
|
|
||||||
if compress and not tiered:
|
if compress and not tiered:
|
||||||
_gzip_file(live_path, dest)
|
_gzip_file(live_path, dest)
|
||||||
else:
|
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
|
(`<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
|
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.
|
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:
|
if backup_count <= 0:
|
||||||
return
|
return
|
||||||
stem = os.path.basename(stem)
|
retier(log_dir, stem, backup_count, 0, live_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
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_mtime(path: str) -> float:
|
def _safe_mtime(path: str) -> float:
|
||||||
|
|||||||
Reference in new issue
Block a user