SPB Git

spb/trouve-ka Public

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

Python 76.8% TypeScript 15.7% SQL 3.9% Shell 1.4% CSS 1.3% Dockerfile 0.7%
2.7 KB · 72 lines python
Raw Blame History
1# Trouve-KA — coordination Redis (politesse, pause, enrichissement)2# Author: Simon-Pierre Boucher3# Contact: contact@spboucher.ai45"""Coordination inter-workers via Redis.67- Politesse par hôte : lock SET NX PX — un seul fetch par hôte par fenêtre,8  quel que soit le nombre de workers.9- Pause globale du crawler : simple clé drapeau.10- Enrichissement asynchrone : Redis Stream (jamais bloquant pour l'indexation).11"""1213import json14from typing import Any1516import redis.asyncio as aioredis1718PAUSE_KEY = "trouveka:crawler:paused"19HOST_LOCK_PREFIX = "trouveka:host-lock:"20ENRICH_STREAM = "trouveka:enrich"212223class Coordination:24    def __init__(self, redis_url: str):25        self.redis: aioredis.Redis = aioredis.from_url(redis_url, decode_responses=True)2627    async def close(self) -> None:28        await self.redis.aclose()2930    # ------------------------------------------------------------- politesse31    async def acquire_host_slot(self, host: str, delay_seconds: float) -> bool:32        """Réserve le droit de fetcher cet hôte. False = trop tôt, repasser plus tard."""33        px = max(int(delay_seconds * 1000), 100)34        return bool(await self.redis.set(HOST_LOCK_PREFIX + host, "1", nx=True, px=px))3536    # ------------------------------------------------------------- pause37    async def pause_crawler(self) -> None:38        await self.redis.set(PAUSE_KEY, "1")3940    async def resume_crawler(self) -> None:41        await self.redis.delete(PAUSE_KEY)4243    async def is_paused(self) -> bool:44        return await self.redis.exists(PAUSE_KEY) == 14546    # ------------------------------------------------------------- enrichissement47    async def enqueue_enrichment(self, payload: dict[str, Any]) -> None:48        await self.redis.xadd(ENRICH_STREAM, {"data": json.dumps(payload, default=str)}, maxlen=100_000)4950    async def read_enrichment(51        self, group: str, consumer: str, count: int = 10, block_ms: int = 500052    ) -> list[tuple[str, dict[str, Any]]]:53        try:54            await self.redis.xgroup_create(ENRICH_STREAM, group, id="0", mkstream=True)55        except aioredis.ResponseError as exc:56            if "BUSYGROUP" not in str(exc):57                raise58        entries = await self.redis.xreadgroup(59            group, consumer, {ENRICH_STREAM: ">"}, count=count, block=block_ms60        )61        out: list[tuple[str, dict[str, Any]]] = []62        for _stream, items in entries or []:63            for msg_id, fields in items:64                out.append((msg_id, json.loads(fields["data"])))65        return out6667    async def ack_enrichment(self, group: str, msg_id: str) -> None:68        await self.redis.xack(ENRICH_STREAM, group, msg_id)6970    async def enrich_backlog(self) -> int:71        return await self.redis.xlen(ENRICH_STREAM)72