fix(cron): re-validate stale cron-output entries before deletion (#37721)
quick() and dry_run() previously trusted the stored category from tracked.json without re-validating at delete time. Stale entries from before #34840 could carry category="cron-output" for cron control-plane paths (e.g. cron/jobs.json), causing quick() to delete the live scheduler registry. Fix: - Fix guess_category() to only classify cron/output/** as cron-output (was classifying ALL cron/* paths, missing the #34840 fix). - Re-validate cron-output entries via guess_category() at delete time in quick() and dry_run(); stale entries that are no longer classified as cron-output are skipped and removed from tracked.json. - Add _is_protected_cron_path() as a hard defense-in-depth guard that blocks deletion of cron/cronjobs directories and known control-plane files (jobs.json, .tick.lock) regardless of stored category. - Update test_cron_subtree_categorised to match fixed guess_category (only cron/output/* is cron-output, not all of cron/). Tests: add 5 regression tests in TestStaleCronEntryMigration.
This commit is contained in:
@@ -145,6 +145,33 @@ ALLOWED_CATEGORIES = {
|
||||
}
|
||||
|
||||
|
||||
# Paths under $HERMES_HOME that must NEVER be deleted by quick(),
|
||||
# regardless of what the stored category says. This is a defense-in-depth
|
||||
# guard against stale tracked.json entries from before #34840.
|
||||
_PROTECTED_CRON_PATHS: set[str] = set()
|
||||
|
||||
|
||||
def _is_protected_cron_path(p: Path) -> bool:
|
||||
"""Return True if *p* is a cron control-plane file/directory that must
|
||||
never be deleted.
|
||||
|
||||
This only matches the directory itself and known control-plane files
|
||||
(``jobs.json``, ``.tick.lock``) — it does NOT blanket-protect
|
||||
everything under ``cron/`` because ``cron/output/`` is disposable.
|
||||
"""
|
||||
# Lazily build the set once per process so HERMES_HOME is resolved
|
||||
# exactly once.
|
||||
if not _PROTECTED_CRON_PATHS:
|
||||
hermes_home = get_hermes_home()
|
||||
for parent in ("cron", "cronjobs"):
|
||||
base = hermes_home / parent
|
||||
_PROTECTED_CRON_PATHS.add(str(base))
|
||||
_PROTECTED_CRON_PATHS.add(str(base / "jobs.json"))
|
||||
_PROTECTED_CRON_PATHS.add(str(base / ".tick.lock"))
|
||||
resolved = str(p.resolve())
|
||||
return resolved in _PROTECTED_CRON_PATHS
|
||||
|
||||
|
||||
def fmt_size(n: float) -> str:
|
||||
for unit in ("B", "KB", "MB", "GB", "TB"):
|
||||
if n < 1024:
|
||||
@@ -226,6 +253,14 @@ def dry_run() -> Tuple[List[Dict], List[Dict]]:
|
||||
cat = item["category"]
|
||||
size = item["size"]
|
||||
|
||||
# Re-validate stale "cron-output" entries (fixes #37721).
|
||||
if cat == "cron-output":
|
||||
re_cat = guess_category(p)
|
||||
if re_cat != "cron-output":
|
||||
# Stale entry — would be skipped by quick(); omit from
|
||||
# dry-run output too.
|
||||
continue
|
||||
|
||||
if cat == "test":
|
||||
auto.append(item)
|
||||
elif cat == "temp" and age > 7:
|
||||
@@ -269,6 +304,28 @@ def quick() -> Dict[str, Any]:
|
||||
|
||||
age = (now - datetime.fromisoformat(item["timestamp"])).days
|
||||
|
||||
# ---- stale-state migration (fixes #37721) ----
|
||||
# Old tracked.json entries may carry a "cron-output" category for
|
||||
# paths that are NOT under cron/output/ (e.g. cron/jobs.json).
|
||||
# guess_category() was fixed in #34840, but existing entries are
|
||||
# never re-validated. Re-classify here so stale entries for cron
|
||||
# control-plane state are not deleted.
|
||||
if cat == "cron-output":
|
||||
re_cat = guess_category(p)
|
||||
if re_cat != "cron-output":
|
||||
_log(
|
||||
f"SKIP stale cron-output entry: {p} "
|
||||
f"(re-classified as {re_cat!r})"
|
||||
)
|
||||
# Drop the stale entry — it was misclassified.
|
||||
continue
|
||||
|
||||
# Hard safety net: never delete cron control-plane state even if
|
||||
# the category somehow slipped through re-validation above.
|
||||
if _is_protected_cron_path(p):
|
||||
_log(f"SKIP protected cron path: {p}")
|
||||
continue
|
||||
|
||||
should_delete = (
|
||||
cat == "test"
|
||||
or (cat == "temp" and age > 7)
|
||||
|
||||
Reference in New Issue
Block a user