Lions du désert et introspection

Probablement, presque tous les habitants de Habr savent ce qu'est une dichotomie et comment l'utiliser pour attraper un lion dans le désert. Les erreurs dans les programmes peuvent également être détectées par une dichotomie, en particulier en l'absence d'informations de diagnostic sensées.

image

Une fois le débogage de mon projet en PHP / Laravel, j'ai vu cette erreur dans le navigateur:

image

C'était, au moins, étrange, car, à en juger par la description de la RFC 2616, une erreur 502 signifie que «le serveur, agissant comme passerelle ou proxy, a reçu une réponse incorrecte du serveur en amont». Dans mon cas, il n'y avait pas de passerelles, il n'y avait pas de proxy entre le serveur Web et le navigateur, le serveur Web fonctionnait sous nginx sous virtualbox et livrait directement du contenu Web, sans aucun intermédiaire. Les journaux nginx avaient ceci:

2018/06/20 13:42:41 [error] 2791#2791: *2206 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 192.168.10.1, server: colg.test, request: "GET / HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php7.1-fpm.sock:", host: "colg.test"

Les mots «serveur en amont» dans la description de l'erreur 502 («serveur en amont» dans la version anglaise originale de la RFC) suggéraient des serveurs réseau supplémentaires sur le chemin de demande du navigateur à nginx, mais, apparemment, dans ce cas, celui mentionné dans le message le module PHP-FPM, étant un programme serveur, agit comme ce serveur très en amont. Dans les journaux PHP, c'était:

[20-Jun-2018 13:42:41] WARNING: [pool www] child 26098 exited on signal 11 (SIGSEGV - core dumped) after 102247.908379 seconds from start

Il est maintenant clair où le problème se pose, mais sa cause n'est pas claire. PHP vient de tomber dans le vidage de mémoire, n'affichant aucune information sur le moment où dans l'interprétation du programme PHP une erreur s'est produite. Le moment est donc venu d'attraper un lion dans le désert - d'utiliser ma méthode préférée de débogage par dichotomie dans de tels cas. Anticipant les objections dans les commentaires, je constate que l'on pourrait utiliser ici un débogueur, par exemple le même XDebug, mais la dichotomie était plus intéressante. De plus, ce sera le tour de XDebug.

Ainsi, dans la manière de traiter la requête Web, j'ai défini la sortie de diagnostic la plus simple, avec la poursuite de l'exécution du programme, pour m'assurer qu'aucune erreur ne se produit au lieu de son installation:

 echo “I am here”; die(); 

Maintenant, la mauvaise page ressemblait à ceci:

image

Après avoir mis la commande écrite ci-dessus, d'abord au début, puis à la fin du chemin de traitement des demandes Web, j'ai découvert qu'une erreur (qui en douterait!) Se produit quelque part entre ces deux points. Après avoir défini les diagnostics au milieu du chemin de demande Web, j'ai découvert que l'erreur apparaît quelque part vers la fin. Après quelques itérations, j'ai réalisé que l'erreur ne se produit pas dans le contrôleur de l'architecture Laravel MVC elle-même, mais déjà à la sortie de celle-ci, lors du rendu de la vue, qui est la plus simple ici, dans cet esprit:

 @extends('layouts.app') @section('content') <div> <div class="panel-heading">Myservice</div> <div class="panel-body"></div> </div> @endsection 
Comme vous pouvez le voir, le modèle de vue ne contient pas de code PHP (le moteur de modèle Laravel vous permet d'utiliser du code PHP en vue), et les problèmes ne sont certainement pas là. Mais ci-dessus, nous voyons que cette vue hérite du modèle layouts.app, alors regardez-le. C'est déjà plus compliqué: il y a des éléments de navigation, des formulaires de connexion et d'autres choses communes à toutes les pages du service. En omettant tout ce qui est là, je ne donnerai qu'une ligne, à cause de laquelle un échec est survenu, on a trouvé tout de même la dichotomie. Voici la ligne:

 <script> window.bkConst = {!! (new App\Src\Helpers\UtilsHelper())->loadBackendConstantsAsJSData() !!}; </script> 

Ici, juste dans le code du modèle de vue, PHP a été utilisé. C'était mon «charme» - la dérivation de constantes backend, sous forme de code JS, pour les utiliser sur le frontend, au nom du principe DRY. La méthode loadBackendConstantsAsJSData répertorie plusieurs classes avec les constantes nécessaires sur le frontend. L'erreur s'est produite dans la méthode addClassConstants utilisée par lui, où l'introspection PHP a été utilisée pour obtenir une liste des constantes de classe:

 /** * add all class constants to resulted JSON * @param string $classFullName */ private function addClassConstants(string $classFullName, array &$constantsArray) { $r = new ReflectionClass($classFullName); $result = []; $className = $r->getShortName(); $classConstants = $r->getConstants(); foreach($classConstants as $name => $value) { if (is_array($value) || is_object($value)) { continue; } $result["$className::$name"] = $value; } $constantsArray = array_merge($constantsArray, $result); } 

Après avoir recherché parmi les classes avec des constantes passées à cette méthode, il s'est avéré que la raison de tout - cette classe avec des constantes - est le chemin vers les méthodes de l'API REST.

 class APIPath { const API_BASE_PATH = '/api/v1'; const DATA_API = self::API_BASE_PATH . "/data"; ... const DATA_ADDITIONAL_API = DATA_API . "/additional"; } 

Il y a pas mal de lignes et pour trouver la bonne, une dichotomie a de nouveau été utile. Maintenant, j'espère que tout le monde a remarqué que self :: manque dans la définition de la constante devant le nom constant DATA_API. Après l'avoir ajouté à sa place légitime, tout a fonctionné.

Ayant décidé que le problème est dans le mécanisme d'introspection, j'ai commencé à écrire un exemple minimal pour reproduire un bug:

 class SomeConstants { const SOME_CONSTANT = SOME_NONSENSE; } $r = new \ReflectionClass(SomeConstants::class); $r->getConstants(); 

Cependant, lors de l'exécution de ce script, PHP n'allait pas planter, mais a émis un avertissement complètement sain d'esprit.

PHP Warning: Use of undefined constant SOME_NONSENSE - assumed 'SOME_NONSENSE' (this will throw an Error in a future version of PHP) in /home/vagrant/code/colg/_tmp/1.php on line 17

À ce stade, j'étais déjà convaincu que le problème se manifeste non seulement lors du chargement du site, mais également lors de l'exécution du code écrit ci-dessus via la ligne de commande. La seule différence entre le runtime et le script minimal était la présence du contexte Laravel: le code du problème a été exécuté via son utilitaire artisan. Donc sous Laravel il y avait une sorte de différence. Pour comprendre ce que c'est, il est temps d'utiliser le débogueur. En exécutant le code sous xdebug, j'ai vu que le crash se produit après avoir appelé la méthode ReflectionClass :: getConstants dans la méthode Illuminate \ Foundation \ Bootstrap \ HandleExceptions :: handleError, qui semble très simple:

 public function handleError($level, $message, $file = '', $line = 0, $context = []) { if (error_reporting() & $level) { throw new ErrorException($message, 0, $level, $file, $line); } } 

Le thread d'exécution est arrivé après avoir levé une exception en raison de l'erreur même dans la description de la constante à partir de laquelle tout a commencé, et PHP s'est écrasé lors de la tentative de lancer une exception ErrorException. Une exception dans le gestionnaire d'exceptions ... Je me suis immédiatement souvenu de la fameuse Double faute . Donc, pour provoquer un échec, vous devez installer des gestionnaires d'exceptions similaires à ceux de Laravel. Un peu plus haut dans le code était juste la méthode de bootstrap qui faisait cela:

Maintenant, l'exemple minimal finalisé ressemblait à ceci:

 <?php class SomeConstants { const SOME_CONSTANT = SOME_NONSENSE; } function handleError() { throw new ErrorException(); } set_error_handler('handleError'); set_exception_handler('handleError'); $r = new \ReflectionClass(SomeConstants::class); $r->getConstants(); 

et son lancement a régulièrement empaqueté l'interpréteur PHP version 7.2.4 dans le vidage de mémoire.

Il semble qu'il y ait une récursivité sans fin ici - lors de la gestion d'une exception de l'erreur d'origine, l'exception suivante est levée dans handleException, gérée à nouveau dans handleException, et ainsi de suite à l'infini. De plus, pour reproduire l'échec, vous devez définir à la fois error_handler et exception_handler, si un seul d'entre eux est défini, le problème ne se produit pas. Il a également échoué à simplement lancer une exception, au lieu de lancer une erreur, il semble que ce ne soit pas une récursion tout à fait ordinaire, mais quelque chose comme une dépendance circulaire.

Après cela, j'ai recherché un problème sous différentes versions de PHP (merci, Docker!). Il s'est avéré que l'échec ne se manifeste que, à partir de la version de PHP 7.1, les versions antérieures de PHP fonctionnent correctement - elles ne jurent que par l'exception ErrorException non interceptée.

Quelles conclusions peut-on tirer de tout cela?

  1. Débogage par dichotomie, bien que ce soit une méthode antédiluvienne de débogage, mais parfois elle peut être nécessaire, surtout en cas de manque d'informations diagnostiques
  2. À mon avis, les erreurs 502 sont inintelligibles, à la fois le message à ce sujet («Bad gateway») et son décodage dans le RFC sur la «réponse incorrecte du serveur en amont». Bien que, si vous considérez les modules connectés au serveur Web comme des programmes serveur, vous pouvez comprendre la signification du décodage d'erreur dans RFC. Cependant, disons que le même PHP-FPM dans la documentation est appelé un module et non un serveur.
  3. L'analyseur statique entraîne, il signalerait immédiatement une erreur dans la description de la constante. Mais alors le bug ne serait pas détecté.

Permettez-moi de terminer sur ce point, merci à tous pour votre attention!

Bagreport - envoyé .
UPD: le bug est corrigé . A en juger par le code, il s'est néanmoins retrouvé dans le mécanisme de réflexion - dans la gestion des erreurs de la méthode ReflectionClass :: getConstants

Source: https://habr.com/ru/post/fr414709/


All Articles