Lorsque vous gérez un courtage automobile en Azerbaïdjan et que vous importez des véhicules accidentés des États-Unis, vos problèmes logiciels ne ressemblent pas à ceux d'une startup de la Silicon Valley. Vous n'optimisez pas pour un million d'utilisateurs simultanés. Vous optimisez pour la clarté, la disponibilité et la capacité à réparer les choses vous-même à minuit tout en coordonnant une maison de vente aux enchères située à douze fuseaux horaires de distance. C'est exactement la situation dans laquelle je me suis trouvé lorsque j'ai construit AutoMakler. La plateforme gère tout, du scraping d'enchères en direct et des recherches Carfax aux estimations de livraison et au traitement des paiements. C'est un véritable système de production au service de vrais clients, et il fonctionne sur ce que la plupart des développeurs appelleraient une stack agressivement ennuyeuse.
La stack que personne n'a envie de présenter
Il n'y a pas de React. Pas de Vue. Pas de Redis, pas de Celery, et pas de serveur WebSocket. Le backend est en FastAPI avec du Python pur. La base de données est PostgreSQL. Le frontend est du HTML rendu côté serveur utilisant des templates Jinja2, Bootstrap et une pincée de JavaScript vanilla. Pour le scraping, j'utilise Playwright. Tout fonctionne comme un processus Python unique qui sert directement du HTML.
Il n'y a pas d'étape de build. Il n'y a pas de dossiers node_modules à auditer, pas de transpileurs à configurer, et pas de renouvellement incessant de frameworks frontend à suivre. Quand je déploie, je déplace des fichiers Python et des templates, je n'orchestre pas un pipeline de bundlers. Cette simplicité n'est pas un compromis. C'est tout l'intérêt.
Comment gérer une file d'attente de tâches sans broker de messages
Le scraping d'une enchère automobile en direct ne peut pas se faire de manière synchrone. Un seul scraping peut prendre plusieurs secondes pendant que Playwright charge la page, exécute le JavaScript et extrait les données. Bloquer l'utilisateur pendant ce processus n'est pas une option. La méthode standard consiste à installer Redis, configurer Celery et lancer un pool de workers. J'ai sauté toutes ces étapes.
À la place, AutoMakler utilise Postgres comme sa propre file d'attente de tâches. Lorsqu'un utilisateur déclenche un scraping, l'application écrit une nouvelle ligne dans une table tasks avec un statut pending. Une tâche de fond asyncio récupère cette ligne et lance le scraping du navigateur. Pendant ce temps, le navigateur interroge un endpoint léger toutes les trois secondes pour vérifier le statut. Lorsque la ligne passe à completed, la page s'actualise et affiche les résultats.
Ce modèle fonctionne parce que l'intervalle de polling est assez court pour être réactif, mais assez long pour éviter de saturer le serveur. Trois secondes sont une éternité pour un ordinateur et sont à peine perceptibles pour un humain qui attend sur un site d'enchères externe. La base de données gère la concurrence nativement, et comme les tâches ne sont que des lignes dans Postgres, je peux inspecter la file d'attente avec une simple requête SQL au lieu de fouiller dans les logs de Celery ou les clés Redis.
Maintenir le serveur en vie sans pool de workers
L'automatisation du navigateur est gourmande en mémoire. Lancez trop d'instances Playwright en même temps et votre serveur s'effondrera. La solution conventionnelle est un pool de workers géré avec des limites de concurrence, souvent soutenu par cette même combinaison Redis et Celery. J'utilise une seule ligne de Python : un asyncio.Semaphore.
Le sémaphore limite le nombre d'instances de navigateur pouvant s'exécuter simultanément. Lorsqu'une nouvelle demande de scraping arrive, elle récupère soit un créneau immédiatement, soit attend qu'un créneau se libère. Tout cela se passe à l'intérieur du même processus. Il n'y a pas d'orchestrateur externe qui peut échouer, pas de processus worker qui meurt silencieusement, et pas d'infrastructure supplémentaire à surveiller. Ma consommation de mémoire reste prévisible, et le code qui protège le serveur se trouve juste à côté du code qui l'utilise, et non caché dans un manifeste de déploiement.
Router l'argent avec une seule URL de callback
Le traitement des paiements a introduit une contrainte que je ne pouvais pas changer. Ma passerelle de paiement autorise exactement une seule URL de callback par compte marchand, mais je devais traiter les transactions de deux projets distincts via ce compte unique. Créer un second profil marchand aurait signifié des frais supplémentaires, une conformité accrue et de la paperasse supplémentaire dont un petit courtage n'a pas le temps de s'occuper.
La solution a été d'encoder le nom du projet directement dans la chaîne de l'ID de commande avant d'envoyer le client vers la passerelle. Lorsque le callback atteint mon serveur, AutoMakler décode cet ID, identifie à quel projet appartient le paiement et route la notification vers le gestionnaire interne approprié. La logique existante est restée intacte. C'est une conception additive : je n'ai pas réécrit le flux de paiement, j'ai simplement fait en sorte que l'identifiant porte un peu plus de contexte. C'est le genre de bidouille qui semble évidente avec le recul, mais qui fait gagner des heures de gymnastique architecturale.
Un chat qui fonctionne sans WebSockets
Le chat de support client est souvent le moment où les ingénieurs cèdent et ajoutent des WebSockets. J'avais besoin d'une messagerie intégrée, tout en maintenant une empreinte d'infrastructure minimale. J'ai donc réutilisé la même stratégie de polling que celle qui alimente les scrapes d'enchères.
Les messages sont stockés dans Postgres. Lorsqu'un utilisateur envoie un message, celui-ci est écrit dans la table. Le client effectue un polling pour rechercher des mises à jour, et l'interface utilisateur reflète les nouveaux messages et les accusés de lecture en quasi temps réel. Pour maintenir cette rapidité même lorsque la table des conversations s'agrandit, j'ai ajouté un index partiel Postgres qui ne couvre que les messages non lus des conversations actives. La base de données ne gaspille pas de cycles à scanner l'historique ancien, et le planificateur de requêtes peut satisfaire la plupart des recherches de chat grâce à un scan de plage d'index restreint.
Pour un chat de support où quelques secondes de latence sont acceptables, c'est parfaitement adéquat. Les utilisateurs reçoivent le retour dont ils ont besoin, et je n'ai jamais eu à déboguer une connexion WebSocket obsolète ni à gérer un serveur de socket distinct.
Les inconvénients réels
Cette architecture implique de réels compromis, et prétendre le contraire serait malhonnête. Le polling est « bavard ». Toutes les trois secondes, chaque client actif sollicite le serveur. La bande passante et la charge de requêtes sont plus élevées que ce qu'exigerait une connexion socket persistante. Si le processus Python redémarre, toute tâche de fond en cours s'interrompt immédiatement car il n'y a pas de worker externe pour la reprendre. J'accepte cela car les tâches sont mineures et le coût d'une nouvelle tentative est faible. Un scrape de navigateur échoué peut simplement être relancé par l'utilisateur.
Cette approche a également ses limites. Si AutoMakler doit un jour gérer des milliers de scrapes simultanés, le modèle à processus unique avec polling sera mis à rude épreuve. Mais ce n'est pas mon domaine d'activité. J'ai besoin de fiabilité pour des dizaines d'utilisateurs simultanés, pas
