diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/tasks')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py | 47 |
1 files changed, 44 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py index 7674c6e..c387100 100644 --- a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py +++ b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py @@ -1,13 +1,13 @@ -"""Scheduled cleanup tasks — IP log purge (1-year retention).""" +"""Scheduled cleanup tasks — IP log purge, and unhosted group collection.""" import asyncio import logging from datetime import datetime, timedelta, timezone -from sqlalchemy import delete +from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_hub.db.models import IPLog +from meshbay_hub.db.models import Group, GroupMember, IPLog log = logging.getLogger(__name__) @@ -38,3 +38,44 @@ async def cleanup_loop(get_session): await asyncio.sleep(CLEANUP_INTERVAL_HOURS * 3600) except asyncio.CancelledError: return + + +# ── Groups that never got a node ────────────────────────────────────────────── + +UNHOSTED_GRACE_DAYS = 7 + + +async def find_unhosted_groups(db: AsyncSession, grace_days: int = UNHOSTED_GRACE_DAYS): + """Groups created more than `grace_days` ago that no node has ever announced. + + `hosted_at` is set the first time a node registers claiming the group and is + never cleared, so this finds groups that were created and then abandoned — + not ones whose node happens to be offline today. That distinction is the + whole reason the column exists rather than a check against the live socket + registry, which would delete every group during a hub restart. + """ + cutoff = datetime.now(timezone.utc) - timedelta(days=grace_days) + result = await db.execute( + select(Group).where(Group.hosted_at.is_(None), Group.created_at < cutoff)) + return list(result.scalars().all()) + + +async def prune_unhosted_groups(db: AsyncSession, grace_days: int = UNHOSTED_GRACE_DAYS, + dry_run: bool = False) -> list[tuple[str, str]]: + """Delete abandoned groups. Returns [(id, name)] of what was (or would be) removed. + + Memberships go with the group — there is no cascade configured, and leaving + orphan rows behind would keep the group in everyone's /mine query through the + join. Nothing on a node is touched: the hub does not command those machines, + and by definition no node ever claimed this group anyway. + """ + doomed = await find_unhosted_groups(db, grace_days) + if not doomed or dry_run: + return [(g.id, g.name) for g in doomed] + + ids = [g.id for g in doomed] + await db.execute(delete(GroupMember).where(GroupMember.group_id.in_(ids))) + await db.execute(delete(Group).where(Group.id.in_(ids))) + await db.commit() + log.info("Pruned %d group(s) that no node ever hosted", len(doomed)) + return [(g.id, g.name) for g in doomed] |