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)
|
||||
|
||||
|
||||
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:
|
||||
|
||||
Reference in new issue
Block a user