Vos tests Selenium/Python plantent par vagues sur Windows Server (AWS) avec des erreurs aléatoires « connection refused » et des crashs ChromeDriver ? Voici un guide opérationnel, pas à pas, pour stabiliser durablement vos exécutions parallèles.
Problème : exécution de lots de tests Selenium/Python instables sur un serveur AWS Windows
Vue d’ensemble
- Symptôme : lors de l’exécution de 20–30 tests via Pytest ou Jenkins, certains échouent de manière aléatoire avec
MaxRetryError / NewConnectionError
(WinError 10061 : « connection refused ») au moment des appels HTTP à ChromeDriver. - Constat dans les journaux : le processus
chromedriver.exe
se termine inopinément (exception0xc0000005
), tandis qu’un même test exécuté seul réussit. - Environnement : Windows Server sur AWS, Chrome 123.0.6312.59 et ChromeDriver de même version.
Comprendre les erreurs WinError 10061 et le code d’exception 0xc0000005
Le client Selenium (votre script Python) dialogue avec chromedriver.exe
via une socket locale (HTTP). L’erreur WinError 10061 (« connection refused ») signifie que personne n’écoute sur le port cible à l’instant T : soit ChromeDriver n’a pas encore écouté, soit il a crashé ou est mort entre-temps, soit le port a été réaffecté ou bloqué par le système. Le code d’exception 0xc0000005 désigne une Access Violation (accès mémoire invalide) qui peut résulter d’un binaire instable, d’injections de pilotes/antivirus, d’extensions, ou d’une pression mémoire.
Causes techniques probables
Catégorie | Explication succincte |
---|---|
Incompatibilité ou bug Chrome / ChromeDriver | Version très récente, binaire corrompu ou régression provoquant des crashs sporadiques. |
Conflit de ports locaux | Chaque instance ChromeDriver écoute sur un port aléatoire ; une forte parallélisation épuise les ports éphémères ou réutilise des sockets encore en TIME_WAIT . |
Ressources serveur insuffisantes | Pics CPU/RAM provoquant la fermeture du processus ou l’échec de la liaison socket dans les délais. |
Parallélisme excessif | pytest-xdist /Jenkins ouvrent de nombreux navigateurs ; si driver.quit() n’est pas systématiquement appelé, des processus « zombies » persistent. |
Interférences système | Antivirus, politiques de groupe, pare-feu ou outils EDR tuent/bloquent le processus ou ses connexions locales. |
Stratégie de résolution (playbook complet)
Sécuriser le socle navigateur
- Vérifier la parité exacte Chrome / ChromeDriver. N’utilisez que des versions assorties. Deux options robustes :
- Selenium Manager (intégré à Selenium 4+) récupère automatiquement le driver compatible.
- WebDriverManager (lib Python) télécharge la version correspondante.
# Option A : Selenium Manager (recommandée) from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options opts = Options() opts.add_argument("--headless=new") # si exécution headless service = Service(port=0) # laisse l'OS choisir un port libre driver = webdriver.Chrome(service=service, options=opts) driver.get("[https://example.com](https://example.com)") driver.quit() # Option B : WebDriverManager (si vous gérez vous-mêmes le cache) from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from webdriver_manager.chrome import ChromeDriverManager opts = Options() opts.add_argument("--headless=new") service = Service(ChromeDriverManager().install(), port=0) driver = webdriver.Chrome(service=service, options=opts) driver.quit()
- Tester une version précédente/suivante de Chrome/ChromeDriver pour isoler un bug régressif. Conservez la version stable dans un cache d’artefacts (CI/CD) pour la reproductibilité.
Gérer les ports et le parallélisme
- Limiter ou séquencer la concurrence. Sur des hôtes Windows généraux, visez ~1 vCPU et 1.5–2 Go RAM par navigateur au minimum. Démarrez prudemment (ex.
-n 4
) puis montez.# Pytest (xdist) pytest -n 4 --dist loadfile # Jenkins Pipeline (déclaratif) : limiter le parallélisme du job options { disableConcurrentBuilds() // ou plugin Throttle Concurrent Builds si disponible }
- Fermer proprement chaque session. Utilisez une fixture yield assurant
quit()
même en cas d’échec.# conftest.py import tempfile, shutil, time import contextlib import itertools from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options def create_driver(): opts = Options() opts.add_argument("--headless=new") opts.add_argument("--disable-extensions") opts.add_argument("--disable-gpu") opts.add_argument("--no-sandbox") # surtout utile sur Linux ; inoffensif sur Windows opts.add_argument("--disable-dev-shm-usage") # idem # Profil isolé par test (supprime toute contention de profil) user_data_dir = tempfile.mkdtemp(prefix="chrome_profile_") opts.add_argument(f"--user-data-dir={user_data_dir}") service = Service(port=0) drv = webdriver.Chrome(service=service, options=opts) # Attache le chemin pour cleanup drv._user_data_dir = user_data_dir return drv def retry_create_driver(attempts=3, base_sleep=0.8): for i in range(1, attempts+1): try: return create_driver() except Exception as e: if i == attempts: raise time.sleep(base_sleep * i) # backoff linéaire (simple & efficace) import pytest @pytest.fixture def driver(): d = retry_create_driver() yield d with contextlib.suppress(Exception): d.quit() with contextlib.suppress(Exception): shutil.rmtree(getattr(d, "_user_data_dir", ""), ignore_errors=True)
- Étendre la plage de ports éphémères et réduire
TIME_WAIT
. Sur Windows Server récents, préféreznetsh
pour la plage dynamique, et ajustez les registres si besoin. Afficher la plage actuellenetsh int ipv4 show dynamicport tcp netsh int ipv4 show dynamicport udp
Étendre la plage (exemple)rem Démarre à 10000, 55535 ports (jusqu'à 65535) netsh int ipv4 set dynamicport tcp start=10000 num=55535 netsh int ipv4 set dynamicport udp start=10000 num=55535
Accélérer la libération de ports (TIME_WAIT)reg add HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters ^ /v TcpTimedWaitDelay /t REG_DWORD /d 30 /f rem Optionnel (compat.) : augmenter le nombre de ports utilisables reg add HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters ^ /v MaxUserPort /t REG_DWORD /d 65534 /f
Un redémarrage est recommandé après ces modifications.
Renforcer la robustesse ChromeDriver
- Démarrer
chromedriver
viaService
en demandant un port libre. Port 0 confie le choix à l’OS, évitant la collision.from selenium.webdriver.chrome.service import Service service = Service(port=0)
- Passer des flags de stabilité. Selon l’OS :
--disable-dev-shm-usage
,--no-sandbox
,--disable-extensions
,--headless=new
. Sous Windows GUI,--disable-gpu
évite certains plantages graphiques. - Isoler chaque profil Chrome.
--user-data-dir
unique par test empêche les verrous de profil (source fréquente de crashs silencieux). - Activer des logs exploitables. Demarrez ChromeDriver en mode verbeux et archivez les logs en artefacts CI.
rem Exemple (démarrage manuel pour debug ciblé) start "" chromedriver.exe --port=0 --verbose --log-path=%WORKSPACE%\logs\chromedriver_%RANDOM%.log
Réduire la charge système
- Surveiller CPU/RAM pendant l’exécution. Activez PerfMon/CloudWatch Agent pour remonter :
Processor(_Total)\% Processor Time
,System\Processor Queue Length
Memory\Available MBytes
,Process(chrome)\Private Bytes
TCPv4\Connections Established
,TCPv4\Connections Passive
# Activer la gestion automatique du fichier d'échange wmic computersystem where name="%computername%" set AutomaticManagedPagefile=True
- Exclure les binaires de l’antivirus/EDR. Déclarez en exclusion le répertoire des drivers et des profils temporaires.
# Windows Defender : exemples d'exclusion Add-MpPreference -ExclusionPath "C:\Tools\selenium\drivers" Add-MpPreference -ExclusionPath "C:\Jenkins\workspace" Add-MpPreference -ExclusionProcess "chromedriver.exe" Add-MpPreference -ExclusionProcess "chrome.exe"
Vérifiez également les politiques AppLocker/Software Restriction Policies qui pourraient bloquerchromedriver.exe
.
Alternatives d’architecture
- Selenium Grid / RemoteWebDriver. Externalisez le navigateur vers un Grid Linux (EC2 auto-scalé), ce qui isole les ports et évite la pression locale.
- Dockeriser l’exécution. Utilisez des conteneurs éphémères, chacun avec son Chrome/ChromeDriver. Sur Windows, préférez des workers Linux pour la simplicité (lancement en agents Jenkins Linux) ou WSL 2 si vous restez sur hôte Windows.
Runbook opérationnel (check-list rapide)
Étape | Action concrète | Objectif | Temps estimé |
---|---|---|---|
1 : Parité versions | Basculer sur Selenium Manager ou verrouiller Chrome/ChromeDriver assortis | Éliminer les crashs dûs à un mismatch | ~15 min |
2 : Parallélisme | Réduire à -n 4 , observer, augmenter progressivement | Limiter contention CPU/RAM/ports | ~30 min |
3 : Ports | Configurer netsh + TcpTimedWaitDelay | Empêcher l’épuisement des ports éphémères | ~20 min + reboot |
4 : Antivirus | Créer des exclusions Defender/EDR | Éviter les kill/scan bloquants | ~15 min |
5 : Nettoyage | Post-build : purge des process orphelins et des profils | Supprimer les verrous de port et fuites | ~10 min |
Implémentations références (copier-coller)
Fixture Pytest robuste avec retries à la création du WebDriver
# conftest.py
import os, time, tempfile, shutil, contextlib
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
MAX_CREATE_ATTEMPTS = 3
def *new_chrome():
opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--disable-extensions")
opts.add_argument("--disable-gpu")
opts.add_argument("--no-sandbox")
opts.add_argument("--disable-dev-shm-usage")
# profil isolé
profile_dir = tempfile.mkdtemp(prefix="chrome_profile*")
opts.add_argument(f"--user-data-dir={profile_dir}")
```
service = Service(port=0)
drv = webdriver.Chrome(service=service, options=opts)
drv._profile_dir = profile_dir
return drv
```
def make_driver_with_retry():
last_exc = None
for attempt in range(1, MAX_CREATE_ATTEMPTS+1):
try:
return _new_chrome()
except Exception as e:
last_exc = e
time.sleep(0.7 * attempt)
raise last_exc
@pytest.fixture
def driver():
drv = make_driver_with_retry()
yield drv
with contextlib.suppress(Exception):
drv.quit()
with contextlib.suppress(Exception):
shutil.rmtree(getattr(drv, "_profile_dir", ""), ignore_errors=True)
Nettoyage systématique entre deux builds (Windows Batch)
echo Nettoyage des processus Chrome/ChromeDriver...
taskkill /IM chrome.exe /F 2>nul
taskkill /IM chromedriver.exe /F 2>nul
echo Purge des profils temporaires créés par les tests...
for /d %%D in ("%WORKSPACE%\chrome_profile_*") do rd /s /q "%%~fD"
echo Etat des ports en TIME_WAIT (diagnostic) :
netstat -an | find "TIME_WAIT" | find ":127.0.0.1" | find ":http" /c
Jenkins Pipeline : throttling et artefacts
pipeline {
agent { label 'windows' }
options {
disableConcurrentBuilds() // empêche les builds parallèles du même job
buildDiscarder(logRotator(numToKeepStr: '20'))
timestamps()
}
stages {
stage('Install') {
steps {
bat 'python -m pip install -U pip'
bat 'pip install -r requirements.txt'
}
}
stage('Test') {
steps {
bat 'pytest -n 4 -q --maxfail=1 --durations=10'
}
post {
always {
archiveArtifacts artifacts: 'logs/**, reports/**, **/chromedriver_*.log', fingerprint: true
bat 'call cleanup.bat'
}
}
}
}
}
Détection d’épuisement de ports (PowerShell)
$tw = Get-NetTCPConnection -State TimeWait
$est = Get-NetTCPConnection -State Established
"TIME_WAIT: {0} | Established: {1}" -f ($tw.Count), ($est.Count)
Observabilité et preuves de rétablissement
- Journaux ChromeDriver verbeux + captation des logs du pipeline (archiver en artefacts avec horodatage).
- WER (Windows Error Reporting) LocalDumps pour
chromedriver.exe
afin d’obtenir un minidump quand le crash survient :
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\chromedriver.exe" /v DumpType /t REG_DWORD /d 2 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\chromedriver.exe" /v DumpCount /t REG_DWORD /d 10 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\chromedriver.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\Dumps" /f
mkdir C:\Dumps
Corrélez la baisse des erreurs WinError 10061 avec le réglage des ports et la réduction du parallélisme pour prouver la stabilisation.
Bonnes pratiques complémentaires
- Journaliser création/terminaison des sessions. Ajoutez un identifiant de test au nom du fichier log afin d’isoler rapidement l’origine des fuites.
- Retries ciblés. Appliquez 2–3 tentatives sur la création du WebDriver uniquement (backoff progressif). Évitez les retries aveugles sur toutes les actions, qui masquent les vrais problèmes.
- Mises à jour régulières. Gardez Selenium Python à jour et retestez sur une branche « canari » avant de promouvoir.
- Chrome « headless » moderne. Privilégiez
--headless=new
, plus proche du rendu réel. - Politique de profil. Un profil propre par test évite les « profile already in use » qui dégénèrent en crashs.
- Nettoyage post-build. Script systématique
taskkill /IM chromedriver.exe /F
et purge des profils temporaires.
Débogage ciblé quand ça coince encore
- Valider qu’un port écoute. Lors d’un crash, vérifiez si
chromedriver.exe
écoute bien sur127.0.0.1:PORT
(vianetstat -ano
), puis corrélez avec l’heure du test en échec. - Comparer le comportement en séquentiel. Si 1 test à la fois passe 100% du temps, le problème vient quasi sûr du parallélisme (ressources/ports).
- Tester une exécution sans antivirus/EDR. Sur un hôte clone, désactivez temporairement la protection pour valider l’hypothèse d’interférence (puis rétablissez-la avec exclusions fines).
- Vérifier l’intégrité du binaire. Supprimez le cache des drivers et redémarrez l’agent : un exécutable corrompu peut provoquer
0xc0000005
. - Inspecter les minidumps. Si
C:\Dumps
contient des dumps ChromeDriver récents, signalez le module fautif le plus fréquent (ex. DLL d’EDR, pilote graphique) et mettez-le à jour.
Architecture alternative recommandée pour gros volumes
Au-delà de ~8–12 navigateurs par hôte Windows généraliste, migrez vers un Selenium Grid Linux scalable :
- Des nœuds conteneurisés (Docker) offrant un isolement fort des ports et de la mémoire.
- Un orchestrateur (EC2 Auto Scaling ou Kubernetes) pour élasticité selon la charge.
- Collecte standardisée des logs et vidéos (optionnel) par conteneur.
FAQ rapide
Q : Pourquoi limiter le parallélisme si l’instance a beaucoup de CPU ?
R : Les ports éphémères et TIME_WAIT
deviennent des goulots d’étranglement même sur des hôtes puissants. De plus, Chrome consomme des pics mémoires imprévisibles.
Q : Faut-il lancer Chrome en non-headless pour plus de stabilité ?
R : Non. Le mode --headless=new
est stable et plus simple à exploiter en CI, surtout sans GPU.
Q : Les erreurs WinError 10061 ont disparu en diminuant -n
, dois-je faire autre chose ?
R : Oui : étendez la plage de ports, réglez TcpTimedWaitDelay
, isolez les profils et mettez en place le nettoyage. Vous pourrez ensuite remonter le parallélisme en sécurité.
Récapitulatif exécutif
- Quick wins : parité Chrome/ChromeDriver,
port=0
,-n 4
puis ramp-up, profil unique par test, exclusions antivirus, nettoyage systématique. - Renfort système : plage de ports dynamique plus large,
TcpTimedWaitDelay
réduit, surveillance CPU/RAM/TCP, page file automatique. - Montée en charge : conteneurisation et/ou Selenium Grid sur Linux pour isoler les ressources quand le volume de tests croît.
En appliquant d’abord la mise à niveau Chrome/ChromeDriver et la réduction du parallélisme, la majorité des équipes voient disparaître les erreurs WinError 10061. Les optimisations réseau et système (plage de ports, mémoire, antivirus) complètent la stabilisation à mesure que la volumétrie augmente.
Annexes utiles
Matrix « Symptômes → Actions »
Symptôme | Diagnostic rapide | Actions prioritaires |
---|---|---|
WinError 10061 dès la création du driver | ChromeDriver n’écoute pas ou port indisponible | Service(port=0) , étendre ports éphémères, réduire -n , retries à la création |
Crash 0xc0000005 aléatoire à mi-test | Conflit extension/antivirus/ressources | Exclusions AV/EDR, --disable-extensions , surveillance RAM, maj driver |
Forte variabilité de durée | CPU en surchauffe, IO élevés | Limiter parallélisme, calibrer vCPU/Go/test, upgrade instance |
Ports en TIME_WAIT qui explosent | Netstat montre > des milliers de TIME_WAIT | Régler TcpTimedWaitDelay , étendre plage, cleanup post-build |
Règles de pare-feu Windows (si verrouillages stricts)
Les connexions sont locales (127.0.0.1
) et ne devraient pas être bloquées. Si votre politique est extrême, autorisez explicitement chromedriver.exe
en inbound/outbound sur le profil « Domaine ».
New-NetFirewallRule -DisplayName "Allow ChromeDriver Loopback" `
-Direction Inbound -Program "C:\Tools\selenium\drivers\chromedriver.exe" `
-Action Allow -Profile Domain,Private,Public
Durcissement de la fiabilité Pytest
# pytest.ini
[pytest]
addopts = -q –maxfail=1 –durations=10 # Envisagez pytest-rerunfailures pour la création de driver UNIQUEMENT # exemple: @pytest.mark.flaky(reruns=2, reruns_delay=1)
Conseils de profilage
- Activez Background Intelligent Transfer Service (BITS) uniquement si nécessaire ; il peut injecter des IO inattendus.
- Désactivez les extensions et « policies » d’entreprise dans Chrome pendant les tests.
- Vérifiez que l’agent Jenkins n’exécute pas d’autres outils gourmands (antivirus scans planifiés, sauvegardes).
Conclusion : la combinaison « versions assorties » + « ports bien configurés » + « parallélisme maîtrisé » + « cleanup strict » suffit à faire disparaître la quasi-totalité des WinError 10061
et des crashs 0xc0000005
sur Windows Server (AWS). Au-delà, l’industrialisation via Grid/Docker scelle définitivement la stabilité à grande échelle.