So, das hat mich jetzt 2h gekostet, nur weil ich nicht genau gelesen habe, was der Support schreibt:

Die Mailadresse des Kunden ist markus.van.holter.@veryimportant.com und das System kann einfach keine Mail dorthin senden!

Tatsächlich verweigert das System das Senden an diese Mailadresse. Bloss warum?

Manche Dinge erscheinen auf den ersten Blick simpel. Nehmen wir einmal eine Mailadresse, ihr wisst schon, dieses Relikt aus der Zeit vor WhatsUp :). Wie prüfe ich, ob mein Benutzer auch wirklich eine Mailadresse eingegeben hat?

Der simple Weg

Nichts leichter als das oder? Ich meine wir alle wissen, wie eine Mailadresse aussieht!

thomas.vonwinkel@mail.de

Also erst irgendwas, dann ein @, gefolgt von irgendwas, einem Punkt und am Schluss 1-3 Buchstaben oder so richtig? Interessanterweise gilt: In den allermeisten Fällen ja! Und in der Tat gibt es bestimmt viele Webseiten und Programme, die eine Mailadresse etwa so prüfen:

code   
  1. DATA: lv_email TYPE STRING,
  2. lv_local TYPE STRING,
  3. lv_host TYPE STRING,
  4. lv_tld TYPE STRING.
  5.  
  6. lv_email = 'thomas.zumwinkel@mail.de'.
  7.  
  8. "Zerlege die Mailadresse in ihre Bestandteile
  9. FIND REGEX '(.*)@(.*)\.(.*)' IN lv_email SUBMATCHES lv_local lv_host lv_tld.
  10.  
  11. "Prüfe ob eines der Felder leer ist
  12. IF lv_local IS INITIAL OR lv_host IS INITIAL OR lv_tld IS INITIAL.
  13. WRITE: / 'Mailadresse ungültig!'.
  14. WRITE: / 'Mailadresse ok'.

Wir zerlegen mittels eines regulären Ausdrucks (Regex -> Ein sehr mächtiges Werkzeug. Wer das noch nicht kennt, sollte sich unbedingt einlesen!) die Mailadresse in ihre drei Bestandteile und prüfen dann, ob jeder Teil vorhanden ist. Das ist ein guter Anfang und ich schätze damit erwischen wir 95% aller Mailadressen.

ABER, eben nur 95%. Beispielsweise sind das hier auch gültige Mailadressen nach RFC 5321:

  • „meinname“@domain.ch
    • Wobei die Anführungszeichen nicht zur Mailadresse gehören
  • blue1922@[IPv6:2001:e18::1]
    • Klar, ich geb gerne die IP meiner Domain an
  • hans.xn--mller-kva@bla.com
    • Eigentlich hans.müller@bla.com. Ja Umlaute 🙂
  • „hans ich liebe leerzeichen“@bla.com
    • Jep Leerzeichen (und noch ne Menge sonstiger Müll -.-)
  • info(Hier bekommen Sie Infos)@bla.com
    • Das Zeug zwischen den Klammern ist ein Kommentar

OK, also Umlaute vorne und hinten (Danke für Unicode-Domains an der Stelle… nicht). Dazu Kommentare, IPs statt Domains sowie Anführungszeichen. Ausserdem gibt es noch einige komische Zusatzregeln (Und eine davon betrifft unsere schöne Kundenadresse von oben):

  • Kein Punkt am Ende oder am Anfang des Lokalteil
    • hans.meier.@bla.com ist also eine inkorrekte Adresse
  • Die TLD muss mindestens zweistellig sein
    • Also example.ch ist ok, example.c nicht

Fazit

Es gibt in praktisch jeder Programmiersprache Bibliotheken zur Prüfung von Mailadressen. Nutzt die. Punkt. Wenn ihr wirklich eine Prüfung machen müsst und keine Bibliothek dafür einbinden könnt, dann benutzt diesen Regex (Offiziell aus RFC 5322):

(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_
`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\
x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0
-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0
-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3
}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0
-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\
[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])

Mit dem funktionieren bestimmt 99%. Ja du hast richtig gelesen: Der Regex im offiziellen RFC zu Mailadressen validiert nicht zu 100% korrekt! Also bleibt schlussendlich nur noch eine Möglichkeit: Sendet dem Kunden eine Aktivierungsmail um zu sehen, ob seine Mailadresse korrekt ist (Ist sowieso usus das zu tun…)

Wie merkt ein Programmierer, dass er zuviel im Code rumwuselt? Daran das er mitten in der Nacht aufsteht um einen Performance-Bug zu korrigieren :). Und dabei war der Bug noch nicht einmal in meinem Code. Aber ich sollte vielleicht von vorne beginnen:

Die betroffene Funktion dient dazu, die Erscheinungsdaten einer Publikation auszulesen, damit das Frontend dem Kunden eine Auswahl an möglichen Bestelldaten anbieten kann. Bei einer Sonntagszeitung würde man also eine Liste von Sonntagen zurück bekommen. Nehmen wir das als Beispiel und sagen wir weiterhin, wir wollen ab dem 08.07.2016 starten und 20 Tage in die Zukunft schauen. Das System würde dann diese Liste zurückliefern:

  • 10.07.2016
  • 17.07.2016
  • 24.07.2016

Und hier kommt der Code dazu:

code   
"Erscheinungsdaten für Publikation und Zeitspanne ausgeben
lr_publication->get_publication_calendar_range(
EXPORTING
    ip_start_date           = lv_startdate
    ip_end_date             = lv_enddate
    ip_drerz                = lv_drerz
    ip_pva                  = lv_pva
RECEIVING
    rt_publication_dates    = lt_publication_dates ).
 
"Sortiere die Liste aufsteigend
SORT lt_publication_dates ASCENDING.
 
"Weise die fertige Liste unserer Exportstruktur zu
export_json-possible_dates = lt_publication_dates.

Soweit so einfach oder? Die Methode get_publication_calendar_range ruft die SAP Funktion ISP_SUBSCRIPTED_VAS_GET auf und diese macht nicht viel mehr, als im Tabellenview JDVVA diese Daten auszulesen. Der Zugriff ist dank Datenbankindex simpel und sehr schnell ausgeführt.

Jetzt kam aber als neue Anforderung hinzu, dass auch der Annahmeschluss beachtet werden sollte.  Und wir Entwickler wissen: Neue Anforderungen machen es nie einfacher…

Annahme…was?

Ich muss vielleicht für alle nicht SAP M/SD-Entwickler erklären, was ein Annahmeschluss im aktuellen Kontext ist: Die Zeitschrift oder Zeitung muss von irgendjemandem in die Briefkästen verteilt werden. Davor müssen vom Druckzentrum aus die Zeitungen an Lagerstellen verteilt werden (von wo die Verträger die Zeitungen dann abholen) und vorher sollte die Zeitung ja auch noch gedruckt werden :). Das alles braucht Zeit, weshalb es einen Zeitpunkt gibt (normalerweise ist das irgendwann nachmittags) an dem das „Bestellfenster“ für die morgige Ausgabe geschlossen wird. Nach diesem Zeitpunkt kann der Kunde zwar bestellen, aber eben nicht mehr die Ausgabe von morgen.

Natürlich gilt das Ganze in erster Linie für physische Produkte. Aber von denen hat ja jedes Medienhaus, trotz Digitalstrategie, noch so einige im Angebot :). So und damit zurück zur neuen Anforderung.

Die neue Anforderung

Kunden sollen kein Datum auswählen können, für das der Annahmeschluss schon gesetzt ist

Tja, wie macht man das in SAP? Mit einem Funktionsbaustein natürlich! Könnt ihr euch merken, dass ist die Standardantwort auf beinahe jedes Problem in SAP. Meistens ist dann das Finden des korrekten Bausteins die Herausforderung :). Aber ich schweife ab, hier ist der korrekte Funktionsbaustein um den Annahmeschluss eines M/SD-Produktes auszulesen: ISP_PACKAGING_START_DATE_GET.

Also einfach oder? Wir erweitern einfach unseren Aufruf etwas:

code   
"Erscheinungsdaten für Publikation und Zeitspanne ausgeben
lr_publication->get_publication_calendar_range(
EXPORTING
    ip_start_date           = lv_startdate
    ip_end_date             = lv_enddate
    ip_drerz                = lv_drerz
    ip_pva                  = lv_pva
RECEIVING
    rt_publication_dates    = lt_publication_dates ).
 
"Iteriere und prüfe, ob der Annahmeschluss gesetzt ist
LOOP AT lt_publication_dates INTO ls_publication_date.
 
    lr_publication->check_deadline
    EXPORTING
        ip_date                 = ls_publication_date
    IMPORTING
        has_deadline            = lx_deadline.
 
    IF lx_deadline IS INITIAL. "'false' für alle nicht SAPler 🙂
        APPEND ls_publication_date TO export_json-possible_dates.
    ENDIF.
 
ENDLOOP.
 
"Sortiere die Liste aufsteigend
SORT export_json-possible_dates ASCENDING.

Funktioniert doch?

„Das sieht nach funktionierendem Code aus“, werden jetzt einige SAPler sagen. Und so ist es auch, der Code funktioniert, er ist nur ein extremer Bremsklotz. Der Grund dafür ist zum einen der Funktionsbaustein ISP_PACKAGING_START_DATE_GET, der einige sehr „teure“ Datenbankabfragen vornimmt. Zum anderen prüfen wir hier viel zu viel!

Und genau der Gedanke war es, der mir nachts um zwei plötzlich in den Kopf schoss: Der Annahmeschluss definiert, dass die Ausgabe vom 16.02.2016 und alle noch älteren nicht mehr bestellt werden können. Er definiert aber auch, dass alle Daten grösser dem Annahmeschluss nicht mehr geprüft werden müssen, weil es nur einen Annahmeschluss zur gegeben Zeit geben kann (Er wird quasi jeden Tag nach vorne verschoben).

Die Lösung

Mit dieser Erkenntnis im Hinterkopf habe ich die Funktion umgeschrieben:

code   
"Erscheinungsdaten für Publikation und Zeitspanne ausgeben
lr_publication->get_publication_calendar_range(
EXPORTING
    ip_start_date             = lv_startdate
    ip_end_date               = lv_enddate
    ip_drerz                  = lv_drerz
    ip_pva                    = lv_pva
RECEIVING
    rt_publication_dates      = lt_publication_dates ).
 
"Sortiere die Liste aufsteigend
SORT lt_publication_dates ASCENDING.
 
"Fülle die Exportstruktur mit allen Publikationsdaten
export_json-possible_dates = lt_publication_dates
 
"Iteriere und prüfe, ob der Annahmeschluss gesetzt ist
LOOP AT lt_publication_dates INTO ls_publication_date.
 
    lr_publication->check_deadline
    EXPORTING
        ip_date                   = ls_publication_date
    IMPORTING
        has_deadline              = lx_deadline.
 
    IF lx_deadline IS INITIAL. "'false' für alle nicht SAPler 🙂
        DELETE ls_publication_date FROM export_json-possible_dates.
    ELSE.
        EXIT. "Beendet die Schleife sofort
    ENDIF.
 
ENDLOOP.

Ich fülle die Exportstruktur zu Beginn mit allen Publikationsdaten und lösche jeweils das aktuelle Datum heraus, wenn der Annahmeschluss für dieses Datum gesetzt ist. Da die Liste der Publikationsdaten aufsteigend sortiert ist, kann ich aufhören zu prüfen, sobald das erste Datum keinen Annahmeschluss mehr gesetzt hat.

Ergebnis

Ich habe bereits in der Nacht einige Messungen durchgeführt und bin dabei auf eine Geschwindigkeitssteigerung von 65% gekommen. Das Frontend reagiert entsprechend flotter und ich bin zuversichtlich, dass wir damit die durchschnittliche Antwortzeit noch weiter drücken können :). Werde dazu nochmals posten, wenn wir das auf Produktion haben und ich die echten Logdaten habe.

Fazit

Wenn du in eine bestehende Funktion neuen Code einfügst, der potenziell teure Datenbankoperationen durchführt, schau dir den umschliessenden Code an. Das Problem hier war grundsätzlich weder dir erste Umsetzung, noch die neue Funktion, sondern die unglückliche Verbindung von Beiden.