Build and testing environment at Celonis

Entwicklungs- und Testumgebung bei Celonis: Unsere Erfahrungen im Laufe der Zeit

maximillian springer celonis blog
von Maximilian Springer
November 11, 2019
Lesezeit: 10 Minuten

Seit vier Jahren entwickelt Celonis eine eigene Query Engine zur Beantwortung von Process-Mining-Abfragen. Die Anforderungen an eine Entwicklungs- und Testinfrastruktur haben sich seit den Anfängen als kleines C++-Projekt auf mehreren Plattformen deutlich verändert. Entwicklungs- und TestlÜsungen sind in C++ nicht so etabliert wie in anderen Programmiersprachen wie Java; darum haben wir es nach und nach mit verschiedenen Strategien probiert: Im Folgenden präsentieren wir Ihnen unsere Erfahrungen gruppiert anhand von fßnf Kategorien: Entwicklungssystem, Abhängigkeitsmanagement, Entwicklung, Tests und Bereitstellung.

Entwicklungssystem

CMake ist heute de facto das Standardentwicklungssystem fßr plattformunabhängige C++-Projekte. Da es als Meta-Entwicklungssystem fungiert, lassen sich plattformspezifische Build-Dateien erzeugen. Zwar werden neuere Entwicklungssysteme immer beliebter, doch sind mehr Entwickler mit CMake vertraut, ermÜglichen mehr Abhängigkeiten eine nahtlose Integration und ist die Software deutlich reifer geworden.

Anfänglich verwendeten wir CMake fßr UNIX-Builds (Linux & macOS), fßr Windows hingegen nutzten wir direkt die Visual-Studio-Projektdateien. Dafßr gab es mehrere Grßnde:

Erstens musste die Verzeichnisstruktur fßr Source- und Header-Dateien manuell eingerichtet werden, da Visual Studio diese Struktur nicht selbst erkennen konnte. Das Ergebnis war eine Liste von Source- und Header-Dateien in der IDE. Das Verfahren wurde erst mit CMake 3.8 vereinfacht.

Zweitens ist das Importieren vorhandener VS-Projektdateien kein leichtes Unterfangen. Viele der Einstellungen ließen sich nicht einfach in CMake-Anweisungen übersetzen. Ein Beispiel dafür ist eine Multi-Core-Kompilierung, bei der nicht ganz klar ist, welchen Effekt die Einstellungen in der Benutzeroberfläche von Visual Studio tatsächlich haben und wie sie sich in Visual-Studio-Dateien einrichten bzw. später via MSBuild aufrufen lassen. Eine sorgfältige Portierung aller Compiler- und Abhängigkeitseinstellungen schien die Mühe nicht wert.

Als größere Änderungen an der Abhängigkeits- und Projektstruktur sowieso zahlreiche Anpassungen an den verschiedenen Entwicklungssystemen erforderlich machten, führten wir die beiden Entwicklungssystemansätze endlich zusammen und verwendeten nur noch CMake.

Abhängigkeitsmanagement

Zu Beginn des Projekts wurden Abhängigkeiten zusammen mit dem Code in das Repository eingecheckt. Das ist eine einfache Methode, mit der Entwickler die Möglichkeit erhalten, den Code mit den erforderlichen Abhängigkeiten auszuchecken, ohne sich Gedanken über Details machen zu müssen. Für Windows gab es sogar vordefinierte, eingecheckte Binärdateien. Die Erkenntnis, dass damit zahlreiche Nachteile verbunden sind, kam relativ schnell. Das Repository wurde größer, Zusammenführungskonflikte wurden komplizierter, und die Verknüpfung von Code und Abhängigkeiten war ein aktuelles Risiko.

Um diese Herausforderungen pragmatisch zu lÜsen, wechselten wir zum Klonen von Abhängigkeiten und vordefinierten Binärdateien via Shell-Skript. Dabei verloren wir die automatische Synchronizität der eingecheckten Variante, da fßr Abhängigkeitsversionen an einem bestimmten Commit-Zeitpunkt nicht mehr strikt garantiert werden konnte, dass beide Versionen identisch sind.

Das Shell-Skript hätte eine Abhängigkeit zu einem spezifischen Commit klonen mßssen. Das Referenzieren von Verzweigungen oder Tags war deswegen riskant, weil sich diese später ändern konnten, was eine Reproduktion älterer Builds erschweren wßrde. Abhängigkeitsmanagement ähnelte also stark Git-Submodulen. In beiden Fällen wßrde der Entwickler einen weiteren Befehl ausfßhren mßssen (oder zumindest einen Befehl zum Klonen anpassen); bei Git-Submodulen wurde die Versionsverwaltung fßr Abhängigkeiten jedoch strikt von Git ßbernommen.

Darum entschieden wir uns schnell fßr den Einsatz von Git-Submodulen. Beim Hinzufßgen einer Abhängigkeit als Git-Submodul wßrde diese am spezifischen Commit hinzugefßgt, auf den HEAD zu dem Zeitpunkt verwiesen hat, selbst wenn HEAD eine symbolische Referenz war. So sind Commits des Codes mit Commits der Abhängigkeit verbunden und sollten in der Regel die MÜglichkeit bieten, ältere Versionen der Software zu reproduzieren.

Beispiel:

$ git clone https://github.com/boostorg/boost.git

$ cd boost

$ git symbolic-ref HEAD

refs/heads/master

$ git show-ref refs/heads/master

3d189428991b0434aa1f2236d18dac1584e6ab84 refs/heads/master

$ cd ../test-repo

$ git submodule add https://github.com/boostorg/boost.git

Wenn diese Änderungen nun als Commit übernommen werden und eine andere Person den Code zu einem späteren Punkt auscheckt, wird Folgendes angezeigt:

$ cd test-repo

$ git submodule update —init

$ cd boost

$ git symbolic-ref HEAD

fatal: ref HEAD is not a symbolic ref

$ git show-ref HEAD

3d189428991b0434aa1f2236d18dac1584e6ab84 refs/remotes/origin/HEAD

Wir sehen also, dass HEAD die Informationen Ăźber eine symbolische Referenz verloren hat und sich nur an 3d189428991b0434aa1f2236d18dac1584e6ab84 erinnert.

Zwar ist das theoretisch eine tolle Garantie, doch wurde das Verfahren von Entwicklern nur selten genutzt. Das Aktualisieren von Abhängigkeiten war ein Prozess, der mehrere Schritte und Commits in verschiedenen Repositories beinhaltete. Intelligentere Git-Befehle schienen entweder nicht besonders gut dokumentiert oder oftmals eine Funktion späterer Git-Versionen zu sein.

Außerdem erwiesen sich Git-Submodule damals als nicht besonders zuverlässig und beliebt, da durch CI sowie den Mangel an Tools und Support Probleme entstanden. Ein Wechseln von Verzweigungen sorgte bei Git-Submodulen ebenfalls für Schwierigkeiten, sodass die Gesamterfahrung für Entwickler eher schlecht war. Zudem kam es zu Engpässen bei der Aktualisierung von Abhängigkeiten, da dies in der Regel nur von wenigen Personen vorgenommen wurde. Darüber hinaus wurde die Integration mit den Entwicklungssystemen, die eine andere Bereitstellung erforderten, nicht angegangen.

Angesichts dieser Erfahrungen schien es eine gute Idee zu sein, sich um ein Abhängigkeitsmanagement zu bemühen, das die Verwaltung von Versionen, Artefakten und Erstellung zusammenführen würde. Aus diesem Grund stiegen wir auf Conan um. Conan ist ein Python-basierter C++-Paket-Manager, der alle drei Anforderungen erfüllt. Jedes Paket wird von einer Python-Datei beschrieben, die sämtliche Metadaten und Befehle zum Erstellen, Testen und Bereitstellen enthält. Vordefinierte Pakete lassen sich in einem Artefakt-Repository so bereitstellen, dass Entwickler und die kontinuierliche Integration keine konstante Erstellung vornehmen müssen. Außerdem verfügt Conan über eine gut ausgereifte CMake-Integration, die das Importieren von Paketen als CMake-Zielen besonders leicht macht.

Entwicklung

Am Anfang eines Softwareprojekts ist die Entwicklung noch kein Problem. Wenn das Team auf wenige Personen begrenzt ist und es keine richtigen Freigabeprozesse gibt, wird die Software meist von den Entwicklern selbst entwickelt. So haben wir auch angefangen. Freizugebende Builds wurden von einem der Entwickler entwickelt. Deswegen kam es schnell zu Engpässen. Angesichts der steigenden Zahl von Entwicklern und Tag für Tag mehr übermittelter Aufgaben führten Änderungen oftmals zu Regressionen, die erst sehr spät erkannt wurden.

Darum gewann eine kontinuierliche Entwicklung relevanter Verzweigungen an Bedeutung. Da wir fĂźr Repositories Produkte von Atlassian (Bitbucket Server) sowie Ticket Management (Jira) verwendeten, war die logische Wahl fĂźr einen Entwicklungsserver das entsprechende Atlassian-Produkt Bamboo. Das bietet den Vorteil, dass Builds mit Verzweigungen und Tickets mit Builds verknĂźpft sind (wenn Verzweigungen richtig benannt werden).

Angesichts einer wachsenden Heterogenität der erforderlichen Entwicklungsumgebungen fßr immer mehr Projekte, die mit dem Bamboo-Server entwickelt werden sollten, wurde rasch der Einsatz von Docker-basierten Builds nÜtig. Darum begannen wir, den von Bamboo seit Version 6.4 bereitgestellten Docker Runner intensiv zu nutzen. Während anfänglich nur die Hauptentwicklungsverzweigung sowie mehrere Release-Verzweigungen entwickelt wurden, weiten wir den Umfang der entwickelten Verzweigungen nun immer weiter aus. Je nach Projekt kann es sich dabei um Verzweigungen, die ßber offene Pull-Anfragen verfßgen, bzw. um sämtliche Verzweigungen handeln, die im Push-Verfahren in das Remote-Repository ßbermittelt werden.

Tests

Das Entwickeln von Verzweigungen ist nur dann ein effektives Mittel gegen Regressionen, wenn es ein umfassendes Testprotokoll zum Testen der einzelnen Builds gibt. Andernfalls wäre lediglich eine Suche nach Kompilierungsfehlern möglich. Da die Query Engine ein Datenbankprodukt ist, besteht die offensichtliche Testmethode aus dem Laden einiger Daten und dem anschließenden Ausführen verschiedener Abfragen für diese Daten. Auf diese Weise begannen wir mit dem Testen; diese Methode macht noch heute den größten Teil der inzwischen über 2.300 Tests aus.

Doch nicht alle Abschnitte lassen sich mit Abfragen effizient erreichen. Für die Routinen im Datenmanagement, interne Puffer oder Komprimierung wurden zusätzliche Verfahren benötigt, die gründlich getestet werden mussten. Darum entschieden wir uns für den Einsatz von Catch (und später Catch2) als C++-Test-Framework – besonders mit Blick auf diese internen, aber extrem wichtigen Bestandteile unserer Software. Außerdem begannen wir damit, Abdeckungsdaten von llcm-cov zu nutzen, um jenen Abschnitt des Codes zu ermitteln, den wir mit unserem Test ausführen. Dadurch erhalten wir Hinweise darauf, wo wir Tests noch ausbauen müssen.

Darßber hinaus werden jede Nacht Tests unter Verwendung von Säuberern ausgefßhrt. So kÜnnen wir Probleme, die durch nicht definiertes Verhalten bzw. spezifische Adress- oder Threading-Bedingungen entstehen, verhindern.

Bereitstellung

Zu Beginn des Projekts war die Query Engine eine einzelne ausfĂźhrbare Datei, deren Integration und Nutzung von der Software Ăźbernommen werden musste, in die sie eingebettet war.

Bei Linux konnte jeder Entwickler einfach die ausfĂźhrbare Datei lokal erstellen und dann die Query Engine verwenden. Bei Windows und macOS war das nicht mĂśglich, da es vor der Einrichtung eines Entwicklungsservers keine Option fĂźr Entwickler gab, Entwicklungsumgebungen fĂźr diese Plattformen einzurichten.

Darum entschieden wir uns für ein Einchecken der Binärdateien in das Repository. Das hieß, dass Entwickler, die die Query Engine einer der beiden Plattformen in einem anderen Projekt verwenden wollten, nur noch den neuesten Code auschecken mussten, so eine aktuelle Version davon eingecheckt wurde. Dies führte rasch dazu, dass reihenweise Binärdateien in das Repository committet wurden. Dadurch dauerten Klon- und andere Vorgänge im Repository deutlich länger. Da die Query Engine in einem Java-Projekt die meiste Zeit Verwendung findet, entschieden wir uns nun für die Erstellung eines entsprechenden Maven-Pakets. So werden die plattformspezifischen ausführbaren C++-Dateien separat verpackt.

Fazit

Unsere empfohlenen und aktuellen Best Practices fĂźr die Einrichtung eines C++-Entwicklungssystems sehen wie folgt aus:

  • Sie sollten in fast jedem Fall CMake verwenden.

  • Sie sollten wahrscheinlich Conan verwenden.

  • Ab einer Teamgröße von drei Personen sollten Sie wahrscheinlich Ăźber einige automatische Builds verfĂźgen.

  • Fehlgeschlagene Tests sollten den Build stoppen.

Grundsätzlich versuchen wir, uns an zwei Dinge zu erinnern, wenn wir im Zusammenhang mit dem Entwicklungssystem Änderungen vornehmen: Iterative Änderungen und Entwicklererfahrung.

Wir wollen Änderungen ausschließlich iterativ vornehmen, damit die Builds stets so stabil wie möglich sind. Das wird umso wichtiger, je größer das Team und je höher die Komplexität der Software ist.

Außerdem sollte die Entwicklererfahrung der Hauptfaktor bei den Entscheidungsprozessen für Änderungen am Entwicklungssystem sein. Unsere Erfahrung mit Git-Submodulen hat gezeigt, dass die Einführung von Technologie, die keine gute Benutzererfahrung bietet, Folgen für die individuelle Produktverantwortung von Entwicklern sowie die Agilität des ganzen Teams im Ganzen hat.

maximillian springer celonis blog
Maximilian Springer
Software Engineering, Celonis

Student, Software Engineer, C++/Databases by choice, Build infrastructure/DevOps by accident.

Abonnieren Sie unseren monatlichen Newsletter

We've received your submission
Please fill in all the fields

Indem Sie dieses Formular absenden, stimmen Sie der Speicherung und Verarbeitung Ihrer personenbezogenen Daten durch Celonis gemäß unserer Datenschutzrichtlinie zu.
Lieber Besucher, wir haben festgestellt, dass Sie eine veraltete Browser-Version verwenden. Teile dieser Seite werden von Ihrem Browser nicht korrekt dargestellt. FĂźr eine korrekte Funktionsweise der Seite empfehlen wir Ihnen, einen alternativen Browser zu verwenden oder Ihren Browser auf eine unterstĂźtzte Version anzuheben.