Skip to content

[Bug] - Corrige l'OOM de l'import INSEE - #59

Merged
Xusifob merged 1 commit into
stagingfrom
bugfix/insee-import-oom
Aug 13, 2026
Merged

[Bug] - Corrige l'OOM de l'import INSEE#59
Xusifob merged 1 commit into
stagingfrom
bugfix/insee-import-oom

Conversation

@Xusifob

@Xusifob Xusifob commented Aug 13, 2026

Copy link
Copy Markdown
Member

Problème

app:insee:import se fait tuer par l'OOM killer pendant la phase COMMUNES, à la ligne 37 500 du fichier v_commune_2025.csv qui en compte 37 549 — c'est-à-dire au moment précis du flush final.

Causes

Deux causes distinctes, toutes deux mesurées.

1. InseeService::importData() ne flushait qu'une fois, à la fin du fichier, et n'appelait jamais clear().

L'unit of work conservait toutes les lignes du fichier courant. Surtout, clear() n'étant appelé nulle part, les entités des fichiers déjà traités restaient dans l'identity map pour toute la durée d'un run --target=all, soit environ 95 000 entités.

Le coût ne vient pas tant du persist() que du flush() : persister 37 500 entités ne prend qu'une soixantaine de mégaoctets, ce qui explique que le processus meure exactement à l'entrée du flush terminal et pas avant.

2. La ligne censée économiser de la mémoire dans la commande n'en économisait aucune.

setSQLLogger() est l'API DBAL 2 : elle est dépréciée et se contente de renseigner un champ que le middleware de debug ne lit jamais. Le vrai consommateur en mode debug est le BacktraceDebugDataHolder de doctrine-bundle, qui retient chaque requête exécutée pendant toute la vie du processus — environ 290 Mo sur un import de 95 000 INSERT.

Correctifs

  • Flush + clear() toutes les 500 lignes, selon l'idiome déjà en place dans FileParserService::processFile(). Rien dans l'import ne relit une entité persistée, le détachement est donc sans risque.
  • Le middleware de debug est câblé à la construction de la connexion et ne peut pas être retiré à chaud. La commande avertit donc quand elle tourne en mode debug et renvoie vers --no-debug.
  • La dépendance à EntityManagerInterface devenait inutilisée dans la commande, elle est retirée.
  • La ligne de progression affiche la mémoire occupée, pour qu'une régression se voie pendant l'import.

Mesures

Sur --target=all, base MySQL neuve, à nombres de lignes identiques (37 548 communes, 42 314 communes historiques, 13 534 mouvements, 95 COMER, 830 outre-mer historiques, 229 pays, 468 pays historiques) :

Pic mémoire Durée
avant 668 Mo 46 s
après (debug actif) 310 Mo 44 s
après, avec --no-debug 54 Mo 25 s

Le pic ne dépend plus de la taille des fichiers, alors qu'il croissait avec elle jusqu'ici.

Vérifications

  • Import complet rejoué : les comptages en base sont conformes, contrôles ponctuels sur Le Blanc (36018), Nuku-Hiva (98731) et Bou-Saada (91103, date_fin 1962-07-05).
  • 109 tests unitaires et d'intégration passent, dont les 5 cas d'InseeImportTest.
  • PHPStan sans erreur. Les 5 avertissements PHPCS de longueur de ligne et le point relevé par php-cs-fixer sont préexistants et hors du diff.

Note de déploiement

Les imports en masse doivent être lancés avec --no-debug.

🤖 Generated with Claude Code

L'import se faisait tuer par l'OOM killer pendant la phase COMMUNES, à la
ligne 37 500 du fichier v_commune_2025.csv qui en compte 37 549 — c'est-à-dire
au moment précis du flush final. Deux causes distinctes, toutes deux mesurées.

D'abord, importData() ne faisait qu'un seul flush, à la fin du fichier, et
n'appelait jamais clear(). L'unit of work conservait donc toutes les lignes du
fichier courant, mais surtout, clear() n'étant appelé nulle part, les entités
des fichiers déjà traités restaient dans l'identity map pour toute la durée
d'un run --target=all, soit environ 95 000 entités. Le coût ne vient pas tant
du persist() que du flush : persister 37 500 entités ne prend qu'une soixantaine
de mégaoctets, ce qui explique que le processus meure exactement à l'entrée du
flush terminal et pas avant. On flush et détache désormais toutes les 500 lignes,
selon l'idiome déjà en place dans FileParserService::processFile().

Ensuite, la ligne censée économiser de la mémoire dans la commande n'en
économisait aucune. setSQLLogger() est l'API DBAL 2 : elle est dépréciée et se
contente de renseigner un champ que le middleware de debug ne lit jamais. Le
vrai consommateur en mode debug est le BacktraceDebugDataHolder de
doctrine-bundle, qui retient chaque requête exécutée pendant toute la vie du
processus — environ 290 Mo sur un import de 95 000 INSERT. Ce middleware est
câblé à la construction de la connexion et ne peut pas être retiré à chaud ;
la commande avertit donc quand elle tourne en mode debug et renvoie vers
--no-debug. La dépendance à EntityManagerInterface devenait inutilisée dans la
commande, elle est retirée.

Mesuré sur --target=all avec une base MySQL neuve, à nombres de lignes
identiques (37 548 communes, 42 314 communes historiques, 13 534 mouvements,
95 COMER, 830 outre-mer historiques, 229 pays, 468 pays historiques) :

  avant                    668 Mo   46 s
  après (debug actif)      310 Mo   44 s
  après, avec --no-debug    54 Mo   25 s

Le pic ne dépend plus de la taille des fichiers, alors qu'il croissait avec
elle jusqu'ici. La ligne de progression affiche maintenant la mémoire occupée,
pour qu'une régression se voie pendant l'import.

Les imports en masse doivent être lancés avec --no-debug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Xusifob
Xusifob merged commit 85727df into staging Aug 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant