fix(news): rotar feeds de Reddit por run + loguear HTTP >=400
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:
ChemaVX
2026-07-03 07:18:15 +00:00
co-authored by Claude Fable 5
parent 480d86e975
commit dcfe674322
+29
View File
@@ -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", ""))