import datetime import os import sqlite3 import time def read_positive_int(name, default, maximum=None): value = int(os.environ.get(name, default)) if value < 1 or maximum is not None and value > maximum: raise ValueError(f"invalid {name}") return value DB_PATH = os.environ.get("DB_PATH", "/data/puhososik.db") DELETE_AFTER = read_positive_int("DELETE_AFTER", "1800") SLEEP = read_positive_int("SLEEP", "300") BATCH_SIZE = read_positive_int("BATCH_SIZE", "400", 400) def placeholders(values): return ",".join("?" for _ in values) def delete_expired_orphans(connection, table, foreign_key, cutoff): rows = connection.execute( f"SELECT id FROM {table} " f"WHERE created_at < ? " f"AND NOT EXISTS (" f"SELECT 1 FROM cleaning_tasks " f"WHERE cleaning_tasks.{foreign_key} = {table}.id" f") " f"ORDER BY created_at LIMIT ?", (cutoff, BATCH_SIZE) ).fetchall() ids = tuple(row[0] for row in rows) if not ids: return 0 connection.execute( f"DELETE FROM {table} WHERE id IN ({placeholders(ids)})", ids ) return len(ids) def delete_expired_users(connection, cutoff): rows = connection.execute( "SELECT id FROM users " "WHERE created_at < ? " "AND NOT EXISTS (" "SELECT 1 FROM territories " "WHERE territories.owner_id = users.id" ") " "AND NOT EXISTS (" "SELECT 1 FROM puhososiks " "WHERE puhososiks.owner_id = users.id" ") " "ORDER BY created_at LIMIT ?", (cutoff, BATCH_SIZE) ).fetchall() ids = tuple(row[0] for row in rows) if not ids: return 0 connection.execute( f"DELETE FROM users WHERE id IN ({placeholders(ids)})", ids ) return len(ids) def cleanup(): if not os.path.isfile(DB_PATH): return 0 cutoff = ( datetime.datetime.utcnow() - datetime.timedelta(seconds=DELETE_AFTER) ).strftime("%Y-%m-%d %H:%M:%S.%f") connection = sqlite3.connect(DB_PATH, timeout=5) try: connection.execute("PRAGMA busy_timeout=5000") connection.execute("PRAGMA foreign_keys=ON") schema_exists = connection.execute( "SELECT 1 FROM sqlite_master " "WHERE type = 'table' AND name = 'cleaning_tasks'" ).fetchone() if schema_exists is None: return 0 for table in ("users", "territories", "puhososiks"): columns = { row[1] for row in connection.execute(f"PRAGMA table_info({table})") } if "created_at" not in columns: return 0 connection.execute("BEGIN IMMEDIATE") rows = connection.execute( "SELECT task.id FROM cleaning_tasks AS task " "WHERE (task.status = 'completed' " "AND task.completed_at IS NOT NULL " "AND task.completed_at < ?) " "OR (task.status = 'in_progress' AND task.created_at < ?) " "ORDER BY task.created_at " "LIMIT ?", (cutoff, cutoff, BATCH_SIZE) ).fetchall() task_ids = tuple(row[0] for row in rows) deleted = 0 if task_ids: connection.execute( f"DELETE FROM cleaning_tasks " f"WHERE id IN ({placeholders(task_ids)})", task_ids ) deleted += len(task_ids) deleted += delete_expired_orphans( connection, "territories", "territory_id", cutoff ) deleted += delete_expired_orphans( connection, "puhososiks", "puhososik_id", cutoff ) deleted += delete_expired_users(connection, cutoff) connection.commit() return deleted except Exception: connection.rollback() raise finally: connection.close() if __name__ == "__main__": while True: deleted = 0 try: deleted = cleanup() if deleted: print(f"cleaner deleted {deleted} expired rows", flush=True) except Exception as error: print(f"cleaner error: {error}", flush=True) time.sleep(1 if deleted >= BATCH_SIZE else SLEEP)