8 wichtige Grundregeln zur Schnittstellenprogrammierung

Development Geschrieben von Alexander Onea

„Anything that can go wrong, will go wrong.“
Murphy’s Law

Unsere Reporting-Plattform hat viele Schnittstellen zu externen Systemen, die mehrmals täglich abgefragt werden. Die gelieferten Daten werden in regelmäßigen Abständen über Importe in unsere Systeme übertragen. Sowohl unsere Importsysteme als auch die Schnittstellen werden permanent von Menschen gepflegt und aktualisiert. Dadurch gibt es immer wieder Fehler beim Import von Daten. Wir müssen davon ausgehen, dass – egal wie gut unsere Software ist – wir diese Fehler nicht in allen Fällen vermeiden können.

Alexander Onea

8 wichtige Grundregeln zur Schnittstellenprogrammierung

Interessanterweise entdecken wir bei Schnittstellenfehlern immer wieder ähnliche Ursachen – meist aufgrund mangelnder Erfahrung der Entwickler.
Im Folgenden haben wir zusammengestellt, was sich nach mehreren Jahren Schnittstellen-Integration bei uns als „Best Practice“ etabliert hat. Und weil viele dieser Dinge auf der Basis von XMV (Xunder Menschenverstand) zu betrachten sind, könnte es u.U. für den ein oder anderen mit Big Data beschäftigten Menschen eine hilfreiche Aufstellung sein.

Die Einhaltung dieser Regeln geht oft auf Kosten von Speichermenge und Ausführungsgeschwindigkeit. Auch die (einmaligen) Entwicklungskosten steigen leicht. Die Missachtung dieser Regeln führt aber dazu, dass die Fehlersuche im Livebetrieb sehr aufwendig wird und manche Probleme gar nicht reproduzierbar sind. Ärger intern und mit Kunden ist vorprogrammiert. Die Kosten, die hierdurch entstehen, sind um 2-3 Größenordnungen schlimmer als Hardware- und Entwicklungskosten!

Robustheit und Nachvollziehbarkeit ist wichtiger als Geschwindigkeit und Speicherplatz.
Die angeführten Beispiele gehen von Java-Code aus, analog dazu funktionieren die Tipps für jede gängige andere Sprache auch.

Grundansatz

Fehler in den Daten gehören zum daily business und sind als NORMALFALL zu betrachten. Je mehr Nutzer und Daten wir verwalten, desto wahrscheinlicher ist es, dass an einem Tag ein oder mehrere Fehler in den Daten bestehen. Das kann unmöglich verhindert werden. Unser Ziel ist es also, die Ursache des Fehlers möglichst schnell und mit möglichst wenig Frust zu finden und in kurzer Zeit eine geeignete Lösung anzubieten.

Im Folgenden also eine Liste von Features die eine „gute Schnittstelle“ hat, um die Fehlersuche, Nachvollziehbarkeit und Robustheit eines Systems zu erleichtern.

1. Schnittstellen sind robust und fehlertolerant
2. Fehler in den Schnittstellen lassen sich gut nachvollziehen
3. Fehler lassen sich möglichst leicht korrigieren.

8 wichtige Grundregeln zur Schnittstellenprogrammierung

 

Eine gute Schnittstelle ist kommunikativ

Beispiel Java: Es wird natürlich nichts nach System.out geloggt, sondern es wird ein Logger verwendet (log4j, slf4j oder Java Logging sind im Einsatz). Im Gegensatz zu System.out kann man die Logger nämlich geeignet konfigurieren (um z.b. aus Log.ERROR eine Mail zu senden o.ä.), selektiv im Livebetrieb zwischen TRACE, DEBUG, INFO und WARN auswählen und Inhalte geeignet filtern:

Best practice:

Log.INFO:

  • sollte man „mitlesen“ können, während das Programm läuft – d.h. genügend info, um arbeiten zu können, aber nicht so viel dass man Informationen nicht mehr „gut“ erkennen kann.
  • sollte bei länger dauernden Prozessen eine Info darüber geben, was gerade getan wird und wie lange es voraussichtlich dauert
  • Hat Heartbeat-Charakter: mindestens alle 20 Sekunden ein Output z.B. „Imported 50000 of 20000000 datasets…“
  • Im Zweifel lieber zu viel Info ausgeben. Die Balance zwischen „zu viel“ und „zu wenig“ Information zu halten ist Geschmackssache. Als Entwickler kennt man den Code und erkennt eine Log-Information schnell, jemand der den Fehler sucht ist aber nicht unbedingt mit dem Quellcode vertraut.

Log.WARN:

informiert über „bekannte“ Fehler, z.B.

  • Fehler deren Ursache und Lösung bekannt ist
  • auf einen oder wenige Kunden beschränkt sind
  • laut spec behandelt werden

Log.ERROR:

  • informiert über „unerwartete“ Fehler, d.h. Fehler welche die Schnittstelle so nicht erwartet und u.U. sogar zum Totalabsturz führen
    z.B. Nichterreichbarkeit von internen/externen Systemen Datenbanken / Hosts etc
  • unbekannter Fehler, der die gesamte Schnittstelle (nicht nur einen Kunden) betrifft.

Eine gute Schnittstelle speichert die importierten Rohdaten, ohne sie zu verändern.

Häufig ist der Download der Datei (FTP, HTTP etc) simpel genug und wird mit stabilen Tools gemacht. Es kommt aber vor, dass eine bestimmte Datei, die am <Samstag> heruntergeladen wird, nicht die selben Informationen enthält ist wie die, welche am <Montag> heruntergeladen wurde. Der Import vom Samstag schlägt fehl, der vom Montag nicht. In dem Fall ist es nicht einfach, einen Fehler nachzuvollziehen oder zu reproduzieren. Warum?
Um Nachvollziehbarkeit zu gewährleisten, benötigen wir also dringend die zum Import-Lauf passende Originaldatei.

Best practice:

  • Download und Speichern der Rohdaten im Dateisystem, z.B. <importername>/<jahr>/<monat>/<tag>/<kundennummer>_<uhrzeit>_<randomString>.file
  • Wenn der Download nicht funktioniert, bitte möglichst ausführlich die Fehlersituation (gerne auch das stack trace) loggen.
  • Validieren und Verarbeiten der Rohdaten findet nach dem Speichern der Daten vom lokalen Dateisystem aus statt
  • Dabei im Log.INFO markieren, welche Datei gerade importiert wird.

Eine gute Schnittstelle läuft immer durch und beendet korrekt

…auch wenn der Datensatz eines bestimmten Kunden nicht importiert werden kann.
Häufig ist es so, dass eine Schnittstelle alle Kundendatensätze auf einmal importiert. Wenn von 1000 Kunden einer einen Setupfehler hat und ihre Daten nicht importiert werden können, sollten die anderen 999 Kunden davon nichts mitbekommen.

Best practice:

z.B. eine Transaktion pro Kunde. Wenn eine Transaktion fehlschlägt, Logging betreiben und mit dem nächsten Kunden weitermachen.

Eine gute Schnittstelle erzeugt System.exit() ausschließlich in der main-Methode

… nie in anderen Methoden der Main-Klasse, auf keinen Fall in irgendwelchen anderen Klassen.

Best practice:

  • System.exit(0) => OK => alles in Ordnung. Keine Fehler erkannt.
  • System.exit(1) => FAILED => Importer ist aufgrund von Fehlfunktion abgestürzt.
  • System.exit(2) => UNSTABLE => Nicht alle Kunden konnten importiert werden, es gab fehlerhafte Importe.

Anhand dieser Logik können regelmäßig wiederholende Jobs so konfiguriert werden, dass Automatismen die Beobachtung der Schnittstellen übernehmen und z.B. automatisiert Fehlermeldungen anhand des Exit-Code generieren.

Eine gute Schnittstelle unterstützt simples Regression-Testing

Eine gute Schnittstelle hat bereits Basis-Routinen in den Unit Tests zur Verfügung, die es erlauben, eine Datei in src/test/resources/ zu packen und diese mit einem neuen TestCase testweise zu importieren. Damit kann ein neuer Fehler oder eine neue Anforderung sofort in der Entwicklung bearbeitet werden. Das Setup eines neuen Regression Tests sollte so einfach sein, dass man es in der täglichen Routine machen kann und nicht erst im „stressigen“ Fehlersuchmodus erst die gesamte Prozedur des Test-Bootstrapping (Test-Datenbanken, Migrationen, Status-Updates) aufsetzen muss.

Eine gute Schnittstelle erlaubt den Nachimport aus vorliegenden Rohdaten

Notwendig, wenn ein vollständiger Import aller Daten zu lange braucht.

Häufig kann der Fehlerfall korrigiert werden, indem ein Anwendungsprogramm ein Update bekommt. In diesem Fall wird dann der Importer im nächsten Release neu deployt und es sollte ein erneuter Versuch gestartet werden, die Dateien zu importieren. Die Frage ist häufig „ab wann wird nachimportiert“, u.U. gab’s auch weitere historische Fälle, die inzwischen dadurch korrigiert werden.
Dies kann je nach Schnittstellenkomplexität etliche Konsequenzen haben.

Zu beachten:
Welche anderen Datenverarbeitungs-Jobs sind diesem Job dann nachgelagert? Müssen diese auch aktualisieren oder nicht? Wenn ja, ab wann? Gleiches Datum, vorheriges Datum etc?

Eine gute Schnittstelle erlaubt Nach-Downloads

Notwendig, wenn ein vollständiger Import aller Daten zu lange braucht.

Vor allem wenn der Download nicht trivial ist (z.B. XML-Schnittstelle mit Handshake etc)

  • Von bestimmten Zeiträumen
  • Die Original-Datei wird hierbei niemals überschrieben, es entsteht eine neue Datei
  • Muss/soll nicht automatisch zu einem Nachimport führen

Eine gute Schnittstelle unterstützt, wenn nötig, Korrekturen

Nowendig, wenn Datenkorrekturen erst innerhalb unserer Plattform möglich sind (z.B. wenn der Datenzulieferer nicht mehr korrigieren kann)

Es gibt komplexe Fälle, bei denen Setup-Fehler passieren. Dieser Setup-Fehler schlägt sich über die API durch und kann durch die API-Owner nicht rückgängig gemacht werden. Dadurch schlägt sich der Datenfehler in den User Interfaces durch und Analysen werden erschwert. Endkunden sollen aber von diesem Fehler nichts mitbekommen.

Die Korrektur direkt in der Rohdatei ist absolut verboten.
Das kommt im Grunde Datenmanipulation mit Datenverlust gleich, denn so eine Korrektur lässt sich sehr schwer nachvollziehen.

Ein Korrekturmodus erlaubt das Hinterlegen von zusätzlichen Dateien, z.B.
<importername>/<jahr>/<monat>/<tag>/<kundennummer>_<uhrzeit>_<randomString>_V002.file

Der Nachimport eines Tages berücksichtigt dann die Korrekturdaten mit.
Rohdaten dürfen NICHT verändert werden.

Unsere Erkenntnis daraus:

Mithilfe dieser recht einfachen Methoden konnten wir innerhalb von vier Jahren über 200 (!) Schnittstellen implementieren. Über 160 dieser Schnittstellenprogramme laufen immer noch mindestens täglich. Wir erkennen Fehler im Normalfall 30 Minuten, nachdem sie verursacht wurden und können Korrekturen meist noch am selben Werktag in den Betrieb übernehmen.