fix(news): rotar feeds de Reddit por run + loguear HTTP >=400
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
Reddit sin OAuth ratelimita por IP con ventana larga: la 2ª petición seguida a reddit.com devuelve 429 siempre (verificado: 30s de pausa no bastan). Como r/UFOs va antes en FEEDS, r/UFOB recibía 429 en cada poll desde F1 y nunca llegó a sembrar (0 filas en news_seen). - Solo se pollea un feed de reddit por run, rotando (_reddit_turn por proceso). Con poll de 6h cada subreddit se refresca cada 12h. - El 429 llegaba con bozo=False y 0 entries, invisible para el guard existente: ahora status >= 400 se loguea como warning y se salta. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
480d86e975
commit
dcfe674322
@@ -36,6 +36,16 @@ _SHORT_TOKEN_MAXLEN = 4
|
||||
# Límite de Telegram 4096; troceamos por debajo con margen.
|
||||
_TG_LIMIT = 4000
|
||||
|
||||
# Reddit sin OAuth ratelimita por IP con ventana larga (>30s verificado): la 2ª
|
||||
# petición seguida a reddit.com devuelve 429 SIEMPRE, así que solo se pollea un
|
||||
# feed de reddit por run, rotando. Contador por proceso: tras un reinicio se
|
||||
# empieza por el primero, lo cual es inocuo.
|
||||
_reddit_turn = 0
|
||||
|
||||
|
||||
def _is_reddit(feed: dict) -> bool:
|
||||
return "reddit.com" in feed["url"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class NewsItem:
|
||||
@@ -89,13 +99,32 @@ async def poll_feeds(db, settings) -> list[NewsItem]:
|
||||
Best-effort: cada feed va en su propio try/except — uno caído nunca tumba el
|
||||
run. feedparser.parse es bloqueante (I/O de red), así que se ejecuta en un
|
||||
hilo para no congelar el event loop del bot durante el poll.
|
||||
|
||||
Reddit: solo se pollea UN feed de reddit.com por run, rotando entre ellos
|
||||
(ver _reddit_turn) — dos peticiones seguidas garantizan un 429 en la 2ª.
|
||||
"""
|
||||
global _reddit_turn
|
||||
keywords = [k.strip() for k in settings.NEWS_KEYWORDS.split(",") if k.strip()]
|
||||
new_items: list[NewsItem] = []
|
||||
|
||||
reddit_feeds = [f for f in FEEDS if _is_reddit(f)]
|
||||
skip: set[str] = set()
|
||||
if len(reddit_feeds) > 1:
|
||||
chosen = reddit_feeds[_reddit_turn % len(reddit_feeds)]
|
||||
_reddit_turn += 1
|
||||
skip = {f["name"] for f in reddit_feeds if f["name"] != chosen["name"]}
|
||||
|
||||
for feed in FEEDS:
|
||||
if feed["name"] in skip:
|
||||
continue
|
||||
try:
|
||||
parsed = await asyncio.to_thread(feedparser.parse, feed["url"], agent=UA)
|
||||
status = getattr(parsed, "status", None)
|
||||
# Un 429/403 llega con bozo=False y 0 entries: sin este guard se
|
||||
# tragaba en silencio (así estuvo r/UFOB sin sembrar desde F1).
|
||||
if status and status >= 400:
|
||||
log.warning("feed %s HTTP %s; salto", feed["name"], status)
|
||||
continue
|
||||
if parsed.bozo and not parsed.entries:
|
||||
log.warning("feed bozo sin entries: %s (%s)",
|
||||
feed["name"], getattr(parsed, "bozo_exception", ""))
|
||||
|
||||
Reference in New Issue
Block a user