fix: reduce chess-results.com request volume by ~20-50x
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 5s

- check_all_subscriptions: quick pre-check (1 req, art=4 only) before
  full fetch; full fetch only when results_count or current_round changed.
  Interval 60s → 300s. Saves ~10,000 req/day per active subscription.
- rescan_existing_tournaments: filter to tournaments ended within last 30
  days (or no end_date). Drops from ~2000 cached TNRs to ~30-100 active
  ones per run, saving ~22,000 req/day.
- warmup: sleep 1s per TNR instead of 20ms per 500 — spreads initial
  full-scan over hours rather than minutes.
- parser: extract _normalize_url helper, add fetch_tournament_quick().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
vrubel 2026-06-21 07:47:38 +00:00
parent 66b10d9425
commit 8058080c3a
3 changed files with 65 additions and 12 deletions

View file

@ -734,7 +734,7 @@ def main():
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url))
app.job_queue.run_repeating(
tracker.check_all_subscriptions, interval=60, first=10)
tracker.check_all_subscriptions, interval=300, first=10)
app.job_queue.run_repeating(
tracker.rescan_new_tournaments, interval=3600, first=60)
app.job_queue.run_repeating(

View file

@ -768,21 +768,31 @@ def _warmup_cache_sync():
if tnr % 100 == 0:
set_tnr_state('warmup_tnr', str(tnr))
if tnr % 500 == 0:
elapsed = (start_tnr - tnr) // 500 * 500 # rough
print(f'warmup: progress TNR {tnr} (started at {start_tnr})', file=sys.stderr)
time.sleep(0.02)
# Rate-limit: ~1 req/sec to stay within chess-results.com daily limit.
# Cached TNRs return instantly (no HTTP), so this only applies to misses.
time.sleep(1.0)
# Mark warmup as complete — next run will see this and skip
set_tnr_state('warmup_tnr', 'done')
async def rescan_existing_tournaments(context):
"""Every 2h: refetch art=0 for all cached TNRs, detect new players."""
"""Every 2h: refetch art=0 for recent/active cached TNRs, detect new players.
Only rescans tournaments that ended within the last 30 days (or have no
end_date). Old completed tournaments never get new players, so skipping
them keeps daily request volume within chess-results.com rate limits.
"""
import time
from datetime import timedelta
from bots.client_bot import _md_escape as _md_esc
cutoff = (date.today() - timedelta(days=30)).isoformat()
conn = _get_conn()
cached_tnrs = [r[0] for r in conn.execute('SELECT tnr FROM tnr_cache ORDER BY tnr DESC').fetchall()]
cached_tnrs = [r[0] for r in conn.execute(
'SELECT tnr FROM tnr_cache WHERE end_date = "" OR end_date >= ? ORDER BY tnr DESC',
(cutoff,)).fetchall()]
conn.close()
if not cached_tnrs:
return
@ -840,7 +850,7 @@ async def rescan_existing_tournaments(context):
async def check_all_subscriptions(context):
from swiss_calc.parser import fetch_tournament
from swiss_calc.parser import fetch_tournament, fetch_tournament_quick
from swiss_calc.swiss import calculate_next_round
from bots.client_bot import format_pairings, _render_chunks
from telegram.constants import ParseMode
@ -849,10 +859,22 @@ async def check_all_subscriptions(context):
if not subs:
return
today_str = date.today().isoformat()
for sub in subs:
if sub['tournament_url'] == 'fide_only':
continue
# Quick check: 1 request — skip full fetch if nothing changed
try:
quick = fetch_tournament_quick(sub['tournament_url'])
except Exception:
continue
quick_player = find_player(quick, sub['player_name'], sub['player_sno'])
quick_results = len(quick_player.get('results', [])) if quick_player else 0
quick_rd = quick.get('current_round', 0)
if quick_results <= sub['last_results_count'] and quick_rd <= sub['last_round_done']:
continue
# Something changed — do full fetch for accurate data and pairing calc
try:
tournament = fetch_tournament(sub['tournament_url'])
except Exception:

View file

@ -334,16 +334,47 @@ def extract_tournament_meta(html: str) -> dict:
# ── Full tournament fetch ─────────────────────────────────────
def _normalize_url(url: str) -> str:
base = re.sub(r'[&?]art=\d+', '', url)
base = re.sub(r'[&?]rd=\d+', '', base)
base = re.sub(r'[&?]turdet=\w+', '', base)
base = re.sub(r'[&?]SNode=\w+', '', base)
return base
def fetch_tournament_quick(url: str) -> dict:
"""Lightweight change-detection fetch: only art=4 (1 HTTP request).
Returns standings from the page directly (no art=2 cross-reference).
Use to check if results_count or current_round changed before doing a
full fetch_tournament() call.
"""
base_url = _normalize_url(url)
try:
html = fetch_url(base_url + '&art=4&turdet=YES')
except Exception as e:
raise RuntimeError(f'Не удалось загрузить турнир: {e}')
meta = extract_tournament_meta(html)
standings = parse_standings(html)
current_round = meta['current_round']
if standings:
max_results = max(len(s['results']) for s in standings)
if max_results > current_round:
current_round = max_results
return {
'name': meta['name'],
'num_rounds': meta['num_rounds'],
'current_round': current_round,
'standings': standings,
}
def fetch_tournament(url: str) -> dict:
"""Fetch full tournament data from any chess-results.com URL.
Detects current state automatically (any number of completed rounds).
"""
# Normalize URL to base
base_url = re.sub(r'[&?]art=\d+', '', url)
base_url = re.sub(r'[&?]rd=\d+', '', base_url)
base_url = re.sub(r'[&?]turdet=\w+', '', base_url)
base_url = re.sub(r'[&?]SNode=\w+', '', base_url)
base_url = _normalize_url(url)
# Fetch standings
standings_url = base_url + '&art=4&turdet=YES'