Files
ad_fun/puhososik/cleaner/cleaner.py
T
2026-07-15 13:29:08 +03:00

156 lines
4.3 KiB
Python

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)