|
|
|
|
@ -6,7 +6,9 @@ Only fetches dates not already in the database.
|
|
|
|
|
Uses Wikimedia REST API range endpoint for efficiency.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import logging
|
|
|
|
|
import time
|
|
|
|
|
from datetime import date, timedelta
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
@ -25,6 +27,15 @@ _WIKI_HEADERS = {
|
|
|
|
|
_LOOKBACK_DAYS = 20
|
|
|
|
|
_LOOKAHEAD_DAYS = 2
|
|
|
|
|
|
|
|
|
|
# Process-wide concurrency + rate limiting for Wikimedia REST API.
|
|
|
|
|
# Prevents burst-induced 429 floods that correlate with container OOM.
|
|
|
|
|
_WIKI_SEMAPHORE = asyncio.Semaphore(2)
|
|
|
|
|
_WIKI_MIN_INTERVAL = 0.25 # seconds between requests (≈ 4 req/s ceiling)
|
|
|
|
|
_WIKI_RATE_LOCK = asyncio.Lock()
|
|
|
|
|
_WIKI_LAST_CALL_TS: float = 0.0
|
|
|
|
|
_WIKI_429_LAST_LOG_TS: float = 0.0
|
|
|
|
|
_WIKI_429_LOG_WINDOW = 30.0 # log at most once per 30 s during a burst
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _date_range(start: date, end: date) -> list[date]:
|
|
|
|
|
"""Return list of dates from start to end inclusive."""
|
|
|
|
|
@ -91,15 +102,30 @@ async def collect_wiki_pageviews(
|
|
|
|
|
|
|
|
|
|
logger.info("Fetching wiki pageviews for %r (%s → %s)", wiki_title, start_str, end_str)
|
|
|
|
|
|
|
|
|
|
global _WIKI_LAST_CALL_TS, _WIKI_429_LAST_LOG_TS
|
|
|
|
|
try:
|
|
|
|
|
async with _WIKI_SEMAPHORE:
|
|
|
|
|
async with _WIKI_RATE_LOCK:
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
wait = _WIKI_MIN_INTERVAL - (now - _WIKI_LAST_CALL_TS)
|
|
|
|
|
if wait > 0:
|
|
|
|
|
await asyncio.sleep(wait)
|
|
|
|
|
_WIKI_LAST_CALL_TS = time.monotonic()
|
|
|
|
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=15.0, headers=_WIKI_HEADERS) as client:
|
|
|
|
|
resp = await client.get(url)
|
|
|
|
|
resp.raise_for_status()
|
|
|
|
|
data = resp.json()
|
|
|
|
|
except httpx.HTTPStatusError as exc:
|
|
|
|
|
if exc.response.status_code == 404:
|
|
|
|
|
status = exc.response.status_code
|
|
|
|
|
if status == 404:
|
|
|
|
|
logger.warning("Wikipedia page not found: %r", wiki_title)
|
|
|
|
|
return 0
|
|
|
|
|
if status == 429:
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
if now - _WIKI_429_LAST_LOG_TS > _WIKI_429_LOG_WINDOW:
|
|
|
|
|
logger.warning("Wikimedia rate limit 429 for %r (burst suppressed %ds)", wiki_title, int(_WIKI_429_LOG_WINDOW))
|
|
|
|
|
_WIKI_429_LAST_LOG_TS = now
|
|
|
|
|
raise
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.error("Wiki API request failed: %s", exc)
|
|
|
|
|
|