Réécrire le code hérité comme un voyage chez le dentiste semble être que tout le monde comprend qu'ils devraient y aller, mais ils tergiversent et tentent de retarder l'inévitable, car ils savent que cela fera mal. Dans notre cas, les choses étaient encore pires: nous avons dû réécrire la partie clé du système, et en raison de circonstances externes, nous ne pouvions pas remplacer les anciens morceaux de code par de nouvelles parties en parties, uniquement en une seule fois et en totalité. Et tout cela dans des conditions de manque de temps, de ressources et de documentation, mais avec l'exigence de gestion qu'en raison de «l'opération» aucun client ne devrait souffrir.
Sous la coupe, l'histoire de la façon dont nous avons réécrit le composant principal du produit avec une histoire de 17 ans (!) De Scheme à Clojure, et tout a fonctionné tout de suite (enfin, presque :)).

17 ans dans la "Watch"
Solar Dozor est un système DLP avec une très longue histoire. La première version est apparue en 2001 comme un service relativement petit pour filtrer le trafic de messagerie. En 17 ans, le produit est devenu un grand progiciel qui collecte, filtre et analyse des informations hétérogènes au sein de l'organisation et protège l'entreprise des clients contre les menaces internes.
Lors du développement de la 6e version de Solar Dozor, nous avons secoué le produit de manière décisive, jeté les vieilles béquilles du code
et les remplacé par de nouvelles , mis à jour l'interface, examiné la fonctionnalité dans le sens des réalités modernes - en général, rendu le produit architectural et conceptuellement plus holistique.
À cette époque, sous le capot du Solar Dozor mis à jour, il y avait une énorme couche de code hérité monolithique - le service de filtrage très, qui pendant toutes ces 17 années est progressivement devenu de nouvelles fonctionnalités, incarnant à la fois des solutions à long terme et des tâches commerciales à court terme, mais a réussi à rester dans l'architecture originale paradigmes.
Service de filtrageIl va sans dire que l'introduction de toute modification d'un code aussi ancien nécessitait une délicatesse particulière. Les développeurs ont dû être extrêmement prudents pour ne pas ruiner accidentellement les fonctionnalités créées il y a dix ans. En outre, de toutes nouvelles solutions intéressantes ont été forcées de se faufiler dans le lit d'architecture procrustienne, inventé à l'aube de l'époque.
Comprendre que le besoin de mise à jour du système est apparu est apparu il y a longtemps. Mais l'esprit de toucher un service système énorme et ancien faisait clairement défaut.
Ne pas essayer de retarder l'inévitable
Les produits avec une longue histoire de développement ont une caractéristique intéressante. Peu importe à quel point une fonctionnalité peut sembler étrange, si elle a survécu avec succès jusqu'à nos jours, cela signifie qu'elle a été créée non pas à partir des idées théoriques des développeurs, mais en réponse aux besoins spécifiques des clients.
Dans cette situation, il ne pouvait être question de remplacement progressif. Il était impossible de couper et de refaire la fonctionnalité des pièces, car toutes ces pièces étaient demandées par les clients, et nous ne pouvions pas les «fermer pour reconstruction». Il était nécessaire de retirer soigneusement l'ancien service et de lui fournir un remplacement complet. Seulement dans son ensemble, seulement à la fois.
L'amélioration du processus de développement de produits, la rapidité des changements et l'amélioration de la qualité dans son ensemble étaient une condition nécessaire mais non suffisante. La direction se demandait quels avantages le changement apporterait à nos clients. La réponse a été d'élargir l'ensemble d'interfaces pour interagir avec les nouveaux systèmes d'interception, ce qui fournirait une rétroaction rapide et permettrait aux intercepteurs de répondre plus rapidement aux incidents.
Nous avons également dû nous battre pour réduire la consommation des ressources, en maintenant (et idéalement en augmentant) le taux de traitement actuel.
Un peu de farce
Tout au long du parcours de développement de produits, l'équipe de Solar Dozor a tendu vers une approche fonctionnelle. Cela conduit à un choix plutôt non standard de langages de programmation pour une industrie mature. À différentes étapes de la vie du système, il s'agissait de Scheme, OCaml, Scala, Clojure, en plus du C (++) traditionnel et de Java.
Le principal service de filtrage et d'autres services qui aident à recevoir et à transmettre des messages ont été écrits et développés dans le langage Scheme dans ses différentes implémentations (ce dernier a été utilisé par Racket). Peu importe combien l'on veut chanter les louanges de la simplicité et de l'élégance de cette langue, on ne peut que reconnaître que son développement rencontre plus d'intérêts académiques que industriels. Le décalage est particulièrement notable par rapport à d'autres services Solar Dozor plus modernes, développés principalement sur Scala et Clojure. Le nouveau service a également été décidé d'être mis en œuvre à Clojure.
Clojure?!
Ici, bien sûr, je dois dire quelques mots sur les raisons pour lesquelles nous avons choisi Clojure comme langage de mise en œuvre principal.
Tout d'abord, je ne voulais pas perdre l'expérience unique de l'équipe développant sur Scheme. Clojure est également un membre moderne de la famille des langues Lisp, et passer d'un Lisp à un autre est généralement assez simple.
Deuxièmement, grâce à un engagement envers les principes fonctionnels et un certain nombre de solutions architecturales uniques, Clojure offre une facilité sans précédent de manipulation des flux de données. Il est également important que Clojure fonctionne sur la plate-forme JVM, ce qui signifie que vous pouvez utiliser une base de données commune avec d'autres services en Java et Scala, ainsi que de nombreux outils de profilage et de débogage.
Troisièmement, Clojure est un langage concis et expressif. Cela facilite la lecture du code de quelqu'un d'autre et facilite la transmission du code à un coéquipier.
Enfin, nous apprécions Clojure pour sa facilité de prototypage et son développement dit REPL-centric. Dans presque toutes les situations où il existe un doute, vous pouvez simplement créer un prototype et poursuivre la discussion de manière plus approfondie, avec de nouvelles données. Le développement orienté REPL donne un retour rapide, car pour vérifier la fonctionnalité d'une fonction, il est non seulement nécessaire de recompiler le programme, mais même de le redémarrer (même si le programme est un service situé sur un serveur distant).
Pour l'avenir, je peux dire: je pense que nous n'avons pas perdu le choix.
Mettre la fonctionnalité petit à petit
Lorsque nous parlons d'un remplacement complet, la première question qui se pose est la collecte d'informations sur les fonctionnalités existantes.
C'est devenu une tâche assez intéressante. Il semblerait qu'il existe un système qui fonctionne, voici la documentation qui s'y rapporte, voici des personnes - des experts qui travaillent en étroite collaboration avec le système et en informent les autres. Mais pour obtenir une image complète de toute la variété, et plus encore, les exigences de développement ne se sont pas avérées si simples.
La collecte des exigences n'est pas en vain considérée comme une discipline d'ingénierie distincte. La mise en œuvre existante s'avère paradoxalement être le rôle d'une «norme corrompue». Il montre comment et comment cela devrait fonctionner, mais en même temps, les développeurs devraient que la nouvelle version soit meilleure que l'original. Il est nécessaire de séparer les moments nécessaires à la mise en œuvre (généralement liés aux interfaces externes) de ceux qui peuvent être améliorés en fonction des attentes des utilisateurs.
Processus de filtrage des messagesLa documentation ne suffit pas
Quelle est la fonctionnalité réelle du système? La réponse à cette question est donnée par diverses descriptions, telles que la documentation utilisateur, les manuels et les documents architecturaux, reflétant la structure du service sous divers aspects. Mais en fin de compte, vous comprenez très bien à quel point les idées et la réalité divergent, combien de nuances et de possibilités inexpliquées contiennent l'ancien code.
Je souhaite contacter tous les développeurs. Prenez soin de votre code! C'est votre atout le plus important. Ne vous fiez pas à la documentation. Ne faites confiance qu'au code source.
Heureusement pour nous, le code Scheme, en raison de la nature même du langage créé pour l'enseignement de la programmation, est assez facile à lire même pour une personne non formée. L'essentiel est de s'habituer à certaines formes individuelles qui portent une touche légère de lisp-archaïque.
Construire un processus
Le volume de travail était énorme et l'équipe est très petite. Ce n'était donc pas sans difficultés d'organisation. Le flux de travail des bogues et des demandes de correction (et des améliorations mineures) de l'ancien service de filtrage ne s'est même pas arrêté. Les développeurs devaient régulièrement être distraits par ces tâches.
Heureusement, il a été possible de repousser les demandes d'intégration de nouveaux éléments de grande fonctionnalité dans l'ancien filtre. Certes, sous la promesse d'intégrer cette fonctionnalité dans un nouveau service. Cependant, l'ensemble des tâches de publication augmentait lentement mais sûrement.
Un autre facteur qui a ajouté beaucoup de problèmes était les dépendances externes du service. Composant central, le service de filtrage utilise de nombreux services pour décompresser et analyser les contenus (textes, images, empreintes digitales, etc.). Le travail avec eux a été partiellement guidé par de vieilles décisions architecturales. Au cours du processus de développement, il a également été nécessaire de réécrire certains composants de manière moderne (et certains dans un langage moderne).
Dans ces conditions, un système de tests fonctionnels pas à pas a été construit. Nous avons en quelque sorte fait passer le service à un certain état, qui a été renforcé par des tests actifs, puis nous sommes passés à la mise en œuvre d'un nouveau.
Commencer le développement
Tout d'abord, le cadre principal du service, les mécanismes de base pour la réception des messages et le déballage des fichiers ont été mis en place. C'était le minimum absolu nécessaire pour commencer à tester la vitesse et l'exactitude du futur service.
Ici, il faut préciser que le déballage fait référence au processus récursif consistant à obtenir des pièces d'un fichier et à en extraire des informations utiles. Ainsi, par exemple, un document Word peut contenir non seulement du texte, mais également des images, un document Excel incorporé, des objets OLE et bien plus encore.
Le mécanisme de décompression ne fait pas de distinction entre l'utilisation de bibliothèques internes, de programmes externes ou de services tiers, fournissant une interface unique pour organiser les pipelines de décompression.
Un autre compliment envers Clojure: nous avons obtenu un prototype fonctionnel, dans lequel nous avons décrit les contours de la future fonctionnalité, dans les plus brefs délais.
DSL pour la politique
La deuxième étape consistait à ajouter la validation des messages à l'aide de stratégies de filtrage.
Pour décrire les politiques, une DSL spéciale a été créée - un langage simple sans fioritures, qui nous a permis de présenter les règles et conditions de la politique sous une forme plus ou moins lisible par l'homme. Il s'appelle MFLang.
Le script sur MFLang "à la volée" est interprété dans le code Clojure, met en cache les résultats des vérifications sur le message, maintient un journal de travail détaillé (et, franchement, mérite un article séparé).
L'utilisation de DSL a séduit les testeurs. En bas avec creuser dans la base de données ou le format d'exportation! Il était maintenant possible d'envoyer simplement la règle générée pour vérification, et immédiatement il devenait clair quelles conditions étaient vérifiées. Il était également possible d'obtenir un journal de vérification des messages détaillé, à partir duquel il est clair quelles données ont été prises pour la vérification et quels résultats ont été renvoyés par la fonction de comparaison.
Nous pouvons dire avec confiance que MFLang s'est avéré être un outil absolument inestimable pour la fonctionnalité de débogage.
En pleine force
À la troisième étape, un mécanisme a été ajouté pour appliquer les actions définies par la politique de sécurité au message, ainsi que des liaisons de service qui permettent l'inclusion de nouveaux composants dans le complexe Solar Dozor. Enfin, nous avons pu lancer le service et observer le résultat du travail dans toute sa diversité.
La question principale, bien sûr, était de savoir comment la fonctionnalité implémentée correspond à ce qui était attendu et dans quelle mesure elle l'implémente.
Je note que si le besoin de tests unitaires n'a pas été remis en question depuis longtemps (bien que les pratiques TDD elles-mêmes suscitent toujours un débat animé), l'introduction de tests automatisés de la fonctionnalité du système se heurte souvent à une résistance ouverte.
Le développement des autotests aide tous les membres de l'équipe à mieux comprendre le processus du produit, économise de l'énergie lors de la régression, instaure une certaine confiance dans les performances du produit. Mais le processus de leur création se heurte à un certain nombre de difficultés - collecte des données nécessaires, détermination des indicateurs d'intérêt et test des options. Les programmeurs perçoivent inévitablement la création d'autotests comme un travail parallèle facultatif, qu'il vaut mieux éviter si possible.
Mais si vous parvenez à vaincre la résistance, une fondation assez solide est créée qui vous permet de vous faire une idée de la santé du système.
Nous remplaçons
Et puis un moment important est venu: nous avons inclus le service dans le colis de livraison. Jusqu'à présent, avec l'ancien. Ainsi, une équipe pourrait effectuer un changement de version et comparer le comportement des services.
Dans ce mode parallèle, le nouveau service de filtrage a duré une version. Pendant ce temps, nous avons réussi à collecter des statistiques supplémentaires sur le travail, esquisser et mettre en œuvre les améliorations nécessaires.
Enfin, après avoir rassemblé nos forces, nous avons supprimé l'ancien service de filtrage du produit. La dernière étape de l'acceptation interne est allée, les bugs ont été corrigés, les développeurs ont commencé à passer progressivement à d'autres tâches. D'une manière ou d'une autre imperceptiblement, sans fanfare et applaudissements, un produit est sorti avec un nouveau service.
Et ce n'est que lorsque les questions ont commencé à venir de l'équipe de mise en œuvre que la compréhension est venue - le service sur lequel nous travaillions depuis si longtemps était déjà sur les sites et ... fonctionnait!
Bien sûr, il y a eu des bugs et des améliorations mineures, néanmoins, après un mois d'utilisation active, les clients ont rendu un verdict: l'introduction d'un produit avec une nouvelle version du service de filtrage a causé moins de problèmes que l'introduction des versions précédentes. Hé! On dirait que nous l'avons fait!
En fin de compte
Le développement d'un nouveau service de filtration a pris environ un an et demi. Plus long qu'on ne le pensait à l'origine, mais pas critique, d'autant plus que l'intensité réelle du travail du travail a coïncidé avec l'évaluation initiale. Plus important encore, nous avons pu répondre aux attentes de la direction et des clients et jeter les bases de futures améliorations de produits. Déjà dans l'état actuel, vous pouvez voir une réduction significative de la consommation de ressources - malgré le fait que le produit a encore de nombreuses possibilités d'optimisation.
Je peux ajouter quelques impressions personnelles.
Remplacer un composant central par une longue histoire est une bouffée d'air frais pour le développement. Pour la première fois depuis longtemps, il est certain que le contrôle des produits revient entre nos mains.
Il est difficile de surestimer les avantages d'un processus de communication et de développement correctement organisé. Dans ce cas, il était important d'établir un travail non pas tant au sein de l'équipe, qu'avec de nombreux consommateurs du produit, qui avaient à la fois des préférences et des attentes claires du système et des souhaits plutôt vagues.
Pour nous, c'était la première expérience dans le développement d'un projet d'une telle envergure sur Clojure. Au départ, il y avait des préoccupations liées à la nature dynamique du langage, à la vitesse et à la tolérance aux erreurs. Heureusement, ils ne se sont pas matérialisés.
Il ne reste plus qu'à souhaiter que le nouveau composant fonctionne aussi longtemps et avec succès que son prédécesseur.