fix: source-stats z probki scen, nizsze progi autovacuum

source-stats liczyl flagi tag/obsada dla 2,5 mln par serwis-scena, czyli ok. 5 mln
losowych odczytow indeksow; trwal 22-32 min i przekroczyl limit (GOON-V). Teraz
flagi z probki 3000 scen na serwis (stala, po md5), liczba scen, swiezosc i
miniatury dalej dokladnie. 1 min 51 s, gwiazdki bez zmian, procenty +-2 pkt.

Autovacuum na domyslnych 20% nie ruszal duzych tabel od tygodni (scene_performers
11% stron widocznych, statystyki scenes z 25.08). Migracja 0045 daje 2% dla
szesciu najwiekszych tabel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-09-21 20:28:23 +02:00
parent 55adf8b1cb
commit d3713d64b9
2 changed files with 109 additions and 28 deletions

View file

@ -0,0 +1,52 @@
"""Nizsze progi autovacuum i autoanalyze dla duzych tabel.
Domyslne progi (20% zmienionych wierszy do vacuum, 10% do analyze) przy 4-23 mln wierszy
znacza, ze autovacuum prawie nie rusza tych tabel: 2026-09-21 scene_performers mialo
ostatni przebieg 27.07 i 11% stron oznaczonych jako widoczne, playback_sources 24%,
a statystyki scenes byly z 25.08. Skutek: index-only scan i tak czyta sterte (w
source-stats 94% odczytow scene_performers szlo na dysk, job przekroczyl 30 min,
Sentry GOON-V), a planista myli sie przy szukaniu po tytule (GOON-73).
2% daje przebieg co ~100 tys. zmian. Autovacuum jest dlawiony (cost_delay 2 ms), wiec
czesciej znaczy krocej, nie ciezej. SET na parametrach tabeli bierze tylko
SHARE UPDATE EXCLUSIVE, nie blokuje odczytow ani zapisow.
Revision ID: 0045_autovacuum_big_tables
Revises: 0044_playback_eporner_thumb_idx
Create Date: 2026-09-21
"""
from __future__ import annotations
from alembic import op
revision = "0045_autovacuum_big_tables"
down_revision: str | None = "0044_playback_eporner_thumb_idx"
branch_labels = None
depends_on = None
_TABLES = (
"scenes",
"playback_sources",
"scene_performers",
"scene_tags",
"scene_external_refs",
"external_records",
)
_PARAMS = (
"autovacuum_vacuum_scale_factor",
"autovacuum_vacuum_insert_scale_factor",
"autovacuum_analyze_scale_factor",
)
def upgrade() -> None:
op.execute("SET LOCAL lock_timeout = '5s'")
for table in _TABLES:
params = ", ".join(f"{p} = 0.02" for p in _PARAMS)
op.execute(f"ALTER TABLE {table} SET ({params})")
def downgrade() -> None:
op.execute("SET LOCAL lock_timeout = '5s'")
for table in _TABLES:
op.execute(f"ALTER TABLE {table} RESET ({', '.join(_PARAMS)})")

View file

@ -33,6 +33,12 @@ _TELEMETRY_WINDOW_DAYS = 7
# mniejszej próbce lecimy na proxy/heurystykę (nie zerujemy gwiazdek znanemu-dobremu). # mniejszej próbce lecimy na proxy/heurystykę (nie zerujemy gwiazdek znanemu-dobremu).
_TELEMETRY_MIN_ATTEMPTS = 25 _TELEMETRY_MIN_ATTEMPTS = 25
_EVENT_RETENTION_DAYS = 30 _EVENT_RETENTION_DAYS = 30
# Flagi tag/perf/desc/studio/dur liczone z probki scen na origin, nie ze wszystkich.
# Pelny przebieg to ~5 mln losowych odczytow indeksow scene_tags/scene_performers
# (2,5 mln par origin-scena) i 22-32 min; 2026-09-21 przekroczyl 30 min limitu
# (Sentry GOON-V). Probka po md5(scene_id) jest stala miedzy przebiegami, wiec
# gwiazdki nie skacza; blad procentu przy 3000 scen to ok. 1 pkt.
_RICHNESS_SAMPLE = 3000
# Richness: wagi składowych (suma=1.0). thumb to minimum higieny, canonical-bogactwo # Richness: wagi składowych (suma=1.0). thumb to minimum higieny, canonical-bogactwo
# (desc/studio/tag/perf) waży więcej. dur średnio. # (desc/studio/tag/perf) waży więcej. dur średnio.
_RICHNESS_WEIGHTS = { _RICHNESS_WEIGHTS = {
@ -137,43 +143,65 @@ def run_source_stats() -> dict:
win_start = now - timedelta(days=_TELEMETRY_WINDOW_DAYS) win_start = now - timedelta(days=_TELEMETRY_WINDOW_DAYS)
with session_scope() as session: with session_scope() as session:
# 1) Freshness + richness: agregat po DISTINCT (origin, scene) z live sources, # 1a) Freshness + miniatury: dokladnie, po DISTINCT (origin, scene) z live sources.
# join do flag scen (has_tag/perf/desc/studio/dur policzone raz).
rows = session.execute( rows = session.execute(
text( text(
""" """
WITH scene_flags AS ( WITH live AS (
SELECT s.id,
(s.description IS NOT NULL AND length(btrim(s.description))>0) AS has_desc,
(s.studio_id IS NOT NULL) AS has_studio,
(s.duration_sec IS NOT NULL) AS has_dur,
EXISTS(SELECT 1 FROM scene_tags st WHERE st.scene_id=s.id) AS has_tag,
EXISTS(SELECT 1 FROM scene_performers sp WHERE sp.scene_id=s.id) AS has_perf
FROM scenes s
),
live AS (
SELECT DISTINCT ON (origin, scene_id) origin, scene_id, SELECT DISTINCT ON (origin, scene_id) origin, scene_id,
(thumbnail_url IS NOT NULL) AS has_thumb, created_at (thumbnail_url IS NOT NULL) AS has_thumb, created_at
FROM playback_sources FROM playback_sources
WHERE dead_at IS NULL AND origin LIKE 'tube:%' WHERE dead_at IS NULL AND origin LIKE 'tube:%'
) )
SELECT l.origin, SELECT origin,
count(*) AS scenes, count(*) AS scenes,
count(*) FILTER (WHERE l.created_at > :win) AS new_7d, count(*) FILTER (WHERE created_at > :win) AS new_7d,
max(l.created_at) AS newest_at, max(created_at) AS newest_at,
100.0*avg(l.has_thumb::int) AS thumb, 100.0*avg(has_thumb::int) AS thumb
100.0*avg(f.has_tag::int) AS tag, FROM live
100.0*avg(f.has_perf::int) AS perf, GROUP BY origin
100.0*avg(f.has_desc::int) AS descr,
100.0*avg(f.has_studio::int) AS studio,
100.0*avg(f.has_dur::int) AS dur
FROM live l JOIN scene_flags f ON f.id = l.scene_id
GROUP BY l.origin
""" """
), ),
{"win": win_start}, {"win": win_start},
).all() ).all()
# 1b) Reszta richness z probki scen (patrz _RICHNESS_SAMPLE).
flags = {
r.origin: r
for r in session.execute(
text(
"""
WITH live AS (
SELECT DISTINCT origin, scene_id
FROM playback_sources
WHERE dead_at IS NULL AND origin LIKE 'tube:%'
),
sample AS (
SELECT origin, scene_id FROM (
SELECT origin, scene_id, row_number() OVER (
PARTITION BY origin ORDER BY md5(scene_id::text)
) AS rn
FROM live
) ranked
WHERE rn <= :n
)
SELECT sm.origin,
100.0*avg((EXISTS(SELECT 1 FROM scene_tags st
WHERE st.scene_id=s.id))::int) AS tag,
100.0*avg((EXISTS(SELECT 1 FROM scene_performers sp
WHERE sp.scene_id=s.id))::int) AS perf,
100.0*avg((s.description IS NOT NULL
AND length(btrim(s.description))>0)::int) AS descr,
100.0*avg((s.studio_id IS NOT NULL)::int) AS studio,
100.0*avg((s.duration_sec IS NOT NULL)::int) AS dur
FROM sample sm JOIN scenes s ON s.id = sm.scene_id
GROUP BY sm.origin
"""
),
{"n": _RICHNESS_SAMPLE},
).all()
}
# 2) Telemetria health per origin (okno 7d). # 2) Telemetria health per origin (okno 7d).
tele = { tele = {
r.origin: r r.origin: r
@ -198,13 +226,14 @@ def run_source_stats() -> dict:
for r in rows: for r in rows:
origin = r.origin origin = r.origin
sitetag = origin.split(":", 1)[1] if ":" in origin else origin sitetag = origin.split(":", 1)[1] if ":" in origin else origin
f = flags.get(origin)
pcts = { pcts = {
"thumb": float(r.thumb or 0), "thumb": float(r.thumb or 0),
"tag": float(r.tag or 0), "tag": float(f.tag or 0) if f else 0.0,
"perf": float(r.perf or 0), "perf": float(f.perf or 0) if f else 0.0,
"desc": float(r.descr or 0), "desc": float(f.descr or 0) if f else 0.0,
"studio": float(r.studio or 0), "studio": float(f.studio or 0) if f else 0.0,
"dur": float(r.dur or 0), "dur": float(f.dur or 0) if f else 0.0,
} }
freshness = _freshness_score(r.newest_at, int(r.new_7d), int(r.scenes)) freshness = _freshness_score(r.newest_at, int(r.new_7d), int(r.scenes))
richness = _richness_score(pcts) richness = _richness_score(pcts)