Arquitectura¶
TelegramMe es una capa fina y tipada sobre las vistas previas web públicas de t.me. Python contiene la lógica de negocio; las partes costosas (E/S de red, TLS, HTTP/2, análisis de HTML, JSON, el servidor HTTP) se ejecutan en código nativo.
Capas¶
flowchart TB
subgraph granian["granian (Rust): servidor HTTP, procesos worker"]
subgraph app["aplicación FastAPI"]
MW["middleware<br/>límite de solicitudes · cabeceras · CORS"]
R["api/routes.py"]
MW --> R
end
end
R --> CS["services/channels.py<br/>ChannelService"]
R --> FS["services/feed.py<br/>FeedService"]
FS --> CS
RF["FeedRefresher<br/>(integrado o worker)"] --> CS
CS --> C["cache.py<br/>en el proceso (Rust) + Redis"]
CS --> SF["services/coalesce.py<br/>SingleFlight"]
SF --> TC["telegram/client.py<br/>TelegramClient"]
TC --> F["BrowserFetcher<br/>wreq (Rust: tokio · BoringSSL)"]
F --> T(("t.me"))
CS --> P["telegram/parsing<br/>telegram-me-native (Rust)"]
CS --> V["schemas<br/>pydantic-core (Rust)"]
FS --> NF["build_feed<br/>telegram-me-native (Rust)"]
| Paquete | Responsabilidad |
|---|---|
api/ |
Solo asuntos HTTP: rutas, validación de parámetros, feature flags, correspondencia error → estado, middleware ASGI puro |
services/ |
Lógica de negocio: descargar, guardar en caché y validar páginas, el feed, la puntuación, la popularidad, la agrupación de solicitudes |
telegram/ |
Todo lo específico de t.me: el cliente HTTP con sus reglas de reintento y concurrencia, los parsers de HTML, los tipos de error |
schemas/ |
Modelos públicos de respuesta y de solicitud |
cache.py |
Niveles dentro del proceso y Redis con antigüedad de las entradas, circuit breaker |
container.py |
Crea y cierra los objetos de larga duración; el único lugar que sabe cómo está conectado todo |
app.py, server.py |
Fábrica de la aplicación con lifespan; puntos de entrada de los procesos |
Las dependencias solo apuntan hacia abajo: api → services → telegram,
cache. Nada por debajo de api importa FastAPI.
Ciclo de vida de una solicitud¶
Tomando GET /v1/body/durov como ejemplo:
- granian acepta la conexión y entrega la solicitud a un worker.
- Middleware: límite de solicitudes (si está activado), luego cabeceras de
respuesta (
X-App-Node,X-Process-Time,Cache-Controlpara Cloudflare) y luego CORS. - FastAPI valida
channelcontra el patrón de nombres de usuario (→422). ChannelService.body_jsonbusca la respuesta ya renderizada en la caché. Un acierto se devuelve como bytes: sin análisis ni validación.- En caso de fallo, SingleFlight se asegura de que solo una corrutina
descargue
t.me/s/durov; las solicitudes concurrentes esperan a la misma tarea. Con Redis, esa corrutina toma una concesión breve para que, de todos los servidores, solo un proceso la descargue; los demás recogen su resultado de la caché (consulta Caché). TelegramClienttoma un hueco de la puerta hacia el origen del proceso (justa entre canales y solicitudes de feed,503cuando su cola está llena) y pide la página aBrowserFetcher; wreq realiza la solicitud sobre una conexión HTTP/2 del pool en Rust, fuera del GIL, con el handshake TLS y las cabeceras de un navegador (Cliente HTTP), y devuelve el cuerpo decodificado. Una conexión caída se reintenta una vez.- En uno de los dos hilos de análisis,
parse_channel_pageentrega el HTML al parser de Rust (con el GIL liberado), que construye el árbol una sola vez con html5ever y extrae el perfil del canal, la paginación y cada publicación; una publicación que falla se omite y se registra en el log. - Todavía en ese hilo, la página analizada se valida contra
ChannelBody(las publicaciones que no validan se descartan) y pydantic-core la serializa; de vuelta en el event loop se guarda en caché (para/v1/posty el feed) junto con la respuesta renderizada. - La ruta devuelve directamente los bytes JSON, sin pasar por la validación de respuestas propia de FastAPI.
Lifespan¶
create_app() construye la app de FastAPI; su lifespan abre un
Container
(caché, cliente HTTP, servicios) y, si el feed está activado en modo
embedded, arranca la tarea del actualizador. Al apagarse, todo se cierra en
orden inverso: actualizador, cliente HTTP, caché.
Modelo de procesos¶
telegram-me serve arranca granian con WEB_CONCURRENCY procesos worker. Cada
worker tiene su propio bucle de eventos (uvloop), su cliente HTTP y su caché
dentro del proceso (delante de Redis cuando está definido). granian reinicia los workers que fallan, puede
reciclar workers según su memoria y reenvía SIGTERM para un apagado
ordenado.
telegram-me worker ejecuta solo el actualizador del feed, para los
despliegues que mantienen el trabajo en segundo plano fuera de los procesos de
la API.
Componentes nativos¶
| Tarea | Biblioteca | Implementado en |
|---|---|---|
| Servidor HTTP, supervisión de workers | granian | Rust |
| Bucle de eventos | uvloop | C (libuv) |
| HTTP/2 saliente, TLS, pool de conexiones, descompresión | wreq (telegram-me-native); pyreqwest, sin uso en producción |
Rust (tokio, BoringSSL) |
| Análisis y extracción de HTML | telegram-me-native (html5ever, selectors, regex) |
Rust |
| Validación, JSON (respuestas y caché) | pydantic-core | Rust |
| Protocolo de Redis | hiredis | C |
| Montaje del feed: señales, clasificación, duplicados, temas, respuesta | telegram-me-native (este proyecto) |
Rust (PyO3) |
| Caché dentro del proceso | telegram-me-native (moka, libzstd) |
Rust (PyO3), C |
| Planificación de solicitudes: vuelos, puerta justa hacia el origen, puntos de renovación | telegram-me-native (este proyecto) |
Rust (PyO3) |
El código nativo escrito para este proyecto está en el crate de Rust
rust/telegram-me-native,
compilado con maturin como miembro del workspace de uv. Python sigue siendo la
capa de orquestación: descarga, caché, enrutamiento y unión de los resultados.
Lo que aún queda en Python en la ruta crítica, y el orden en que pasa a Rust,
está en la Hoja de ruta hacia Rust.
El efecto medido está en la página de Rendimiento.