156 lines
4.3 KiB
Python
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)
|