Textreduktion: Theorie, Nebenläufigkeit, Tests und Prozessbeschreibung gestrafft
- Newtype/parse-dont-validate auf Projektspezifik gekuerzt - Nebenlaeufigkeitsszenarien und Gegenmassnahmen als Tabellen statt Fliesstext - Testcode-Reviewbefunde (Timo Walter) zusammengefasst - Unicorn-Entwicklungsprozess-Beschreibung gekuerzt (Diagramm traegt Details) - S3-Infrastrukturbeschaffung: Bewertung entfernt, Text gestrafft - Normalisierungs-Review (Robin Noack) gekuerzt
This commit is contained in:
@@ -21,11 +21,9 @@ Darunter liegt der \texttt{DocumentsService} als fachliche Schicht mit den Regel
|
||||
|
||||
Eine Entwurfsentscheidung, die sich im Verlauf herausbildete, betrifft den Umgang mit Pfaden. Anfangs wurden Dokumentschlüssel als Zeichenketten durch die Schichten gereicht. Im Review des Downloads führte das zu wiederholten Rückfragen zur Pfadvalidierung, weil einer Zeichenkette nicht anzusehen ist, ob sie bereits geprüft wurde.
|
||||
|
||||
Der Ausweg war ein Muster, das ich außerhalb der Arbeit beim Programmieren in Rust kennengelernt habe: das \emph{Newtype-Pattern}. Ein primitiver Wert wird in einen eigenen, sonst inhaltsgleichen Typ verpackt, damit der Übersetzer zwei Werte unterscheiden kann, die als Zeichenkette identisch aussehen \autocite{rust-newtype}. In C\# lässt sich das mit \texttt{readonly record struct} ohne Laufzeitkosten nachbilden. Gerade hier lag der Nutzen auf der Hand, weil das Modul fast ausschließlich mit Schlüsseln arbeitet, die zerlegt, umgeformt und wieder zusammengesetzt werden.
|
||||
Der Ausweg war ein Muster, das ich außerhalb der Arbeit beim Programmieren in Rust kennengelernt habe: das \emph{Newtype-Pattern}, umgesetzt als \texttt{readonly record struct} ohne Laufzeitkosten \autocite{rust-newtype}. Ein primitiver Wert wird in einen eigenen Typ verpackt, damit der Übersetzer zwei Werte unterscheidet, die als Zeichenkette identisch aussehen. Ergänzt wird das durch die Haltung „parse, don’t validate“ \autocite{king-parse}: Eine Funktion, die einen \texttt{DocumentKey} entgegennimmt, muss die Gültigkeit nicht erneut prüfen — sie wäre sonst gar nicht aufrufbar gewesen.
|
||||
|
||||
Ergänzt wird das Muster durch die Haltung „parse, don’t validate“ \autocite{king-parse}: Eine Prüfung soll nicht nur ein Ja oder Nein zurückgeben, sondern das geprüfte Ergebnis in einem Typ festhalten, der die Zusicherung trägt. Eine Funktion, die einen \texttt{DocumentKey} entgegennimmt, muss die Gültigkeit nicht erneut prüfen — sie wäre sonst gar nicht aufrufbar gewesen.
|
||||
|
||||
So entstanden \texttt{DocumentKey} für den vollständigen Pfad einschließlich Kundenordner (Listing~\ref{lst:document-key}) und \texttt{DocumentRelativePath} für den Anteil darunter. Beide sind nur über eine Fabrikmethode erzeugbar, die zerlegt und dabei prüft; eine vergessene Prüfung führt zu einem Übersetzungsfehler statt zu einer Sicherheitslücke. Nach demselben Muster entstanden \texttt{DocumentType}, \texttt{DocumentName}, \texttt{DocumentId} und \texttt{OrgSlug}.
|
||||
So entstanden \texttt{DocumentKey} für den vollständigen Pfad einschließlich Kundenordner (Listing~\ref{lst:document-key}) und \texttt{DocumentRelativePath} für den Anteil darunter, beide nur über eine prüfende Fabrikmethode erzeugbar; eine vergessene Prüfung führt so zu einem Übersetzungsfehler statt zu einer Sicherheitslücke. Nach demselben Muster entstanden \texttt{DocumentType}, \texttt{DocumentName}, \texttt{DocumentId} und \texttt{OrgSlug}.
|
||||
|
||||
Der Datenfluss folgt daraus unmittelbar: Aus der Anfrage kommt eine Zeichenkette, der Speicher liefert Schlüssel als Zeichenketten zurück, beide werden einmal am Rand in das Domänenmodell geparst. Die gesamte weitere Verarbeitung — Typableitung, Filterung, Sortierung, Blätterung — arbeitet nur noch auf Typen. Erst wenn ein weiterer Speicheraufruf nötig ist, wird aus dem Modell wieder ein Schlüssel erzeugt. Zeichenketten existieren damit ausschließlich an den Systemgrenzen.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Der erwartete Ordnername entsteht aus dem Firmennamen des Benutzers. Da dieser a
|
||||
|
||||
Zwei Eigenschaften sind entscheidend. Die Normalisierung ist \textbf{deterministisch} — derselbe Eingabename ergibt stets denselben Ordnernamen, Voraussetzung für den Direktzugriff. Sie erhält \textbf{Umlaute}, weil der Ordner in Filestash von Menschen gelesen wird.
|
||||
|
||||
Im Review fragte \emph{Robin Noack}, ob die Einschränkungen ausreichen. Eine zu schwache Normalisierung führt zu Schlüsseln, die der Speicher zurückweist — das wäre erst bei einem Kunden mit ungewöhnlichem Firmennamen aufgefallen. Die Antwort verwies auf die Herstellerdokumentation \autocite{ibm-s3-naming, aws-s3-naming}.
|
||||
Im Review fragte \emph{Robin Noack}, ob die Einschränkungen ausreichen; die Zulässigkeit wurde anhand der Herstellerdokumentation bestätigt \autocite{ibm-s3-naming, aws-s3-naming}.
|
||||
|
||||
\subsection{Umsetzung des Lookups}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ Der Lesepfad des Kundenordner-Lookups ist unkritisch. Der in Abschnitt~\ref{sec:
|
||||
|
||||
Bei der Durchsicht des Codes wurden zwei Fehlerszenarien gefunden, die ausdrücklich formulierte Akzeptanzkriterien verletzen.
|
||||
|
||||
\subsection{Szenario A: Datenverlust beim gleichzeitigen Umbenennen}
|
||||
\subsection{Zwei Szenarien}
|
||||
|
||||
Das erste Szenario betrifft die Fehlerbehandlung des Umbenennens. Abbildung~\ref{fig:race-condition-a} zeigt den Ablauf.
|
||||
Abbildung~\ref{fig:race-condition-a} zeigt exemplarisch Szenario~A; Tabelle~\ref{tab:race-conditions} stellt beide Fälle einander gegenüber.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
@@ -18,30 +18,49 @@ Das erste Szenario betrifft die Fehlerbehandlung des Umbenennens. Abbildung~\ref
|
||||
\label{fig:race-condition-a}
|
||||
\end{figure}
|
||||
|
||||
Beide Anfragen halten den Zielnamen für frei, weil beide prüfen, bevor eine schreibt. Anfrage~A kopiert alle Objekte und löscht die Quelle. Anfrage~B findet beim nächsten Objekt die Quelle nicht mehr vor. Ihre Fehlerbehandlung entfernt die eigenen Teilkopien — dieselben Objekte, die A soeben geschrieben hat. Da die Quelle gelöscht ist, existiert keine Kopie mehr. Das Ergebnis ist Datenverlust.
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\begin{tabularx}{\textwidth}{@{} l X X @{}}
|
||||
\toprule
|
||||
\textbf{Szenario} & \textbf{Ursache} & \textbf{Auswirkung} \\
|
||||
\midrule
|
||||
A: Datenverlust beim Umbenennen &
|
||||
Zwei Anfragen halten den Zielnamen für frei, da beide prüfen, bevor eine schreibt. A kopiert und löscht die Quelle; B's Fehlerbehandlung entfernt daraufhin dieselben (bereits von A geschriebenen) Objekte. &
|
||||
Quelle und Kopie sind gelöscht — Datenverlust. \\
|
||||
\addlinespace
|
||||
B: Vermischung zweier Mandanten &
|
||||
Der Marker wird ohne Bedingung geschrieben. Haben zwei Organisationen denselben Firmennamen, gewinnt beim gleichzeitigen Erstzugriff der zuletzt geschriebene Marker. &
|
||||
Beide arbeiten im selben Ordner; zwischenzeitlich Abgelegtes bleibt im fremden Ordner — Vertraulichkeitsproblem, schwerwiegender als A. \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\caption{Erkannte Nebenläufigkeitsszenarien im Kundenordner-Lookup}
|
||||
\label{tab:race-conditions}
|
||||
\end{table}
|
||||
|
||||
\subsection{Szenario B: Vermischung zweier Mandanten}
|
||||
|
||||
Der Marker wird ohne Bedingung geschrieben. Haben zwei Organisationen denselben Firmennamen und greifen erstmals gleichzeitig zu, legen beide den Marker an; der zuletzt geschriebene gewinnt. Beide arbeiten anschließend im selben Ordner, obwohl das Metadatum nur einer gehört. Der Folgeaufruf korrigiert sich zwar selbst, doch zwischenzeitlich Abgelegtes verbleibt im fremden Ordner.
|
||||
|
||||
Von den beiden Szenarien ist dieses das schwerwiegendere: Datenverlust ist ein Betriebsproblem, die Offenlegung von Kundendokumenten ein Vertraulichkeitsproblem.
|
||||
|
||||
\subsection{Abgegrenzte Fälle}
|
||||
|
||||
Zur Eingrenzung wurde geprüft, welche nebenläufigen Abläufe \emph{nicht} betroffen sind. Zwei Anfragen derselben Organisation erzeugen identische Schlüssel und sind harmlos. Lesen während einer Verschiebung ist abgedeckt: Da der Marker zuletzt kopiert wird, wird ein halb gefülltes Ziel nicht als Kundenordner erkannt. Das Problem beschränkt sich auf zwei \emph{unterschiedlich weit fortgeschrittene} verändernde Vorgänge.
|
||||
Nicht betroffen sind zwei gleichzeitige Anfragen derselben Organisation (identische Schlüssel) sowie Lesezugriffe während einer Verschiebung, da der Marker zuletzt kopiert wird und ein halb gefülltes Ziel so nicht als Kundenordner erkannt wird. Das Problem beschränkt sich auf zwei \emph{unterschiedlich weit fortgeschrittene} verändernde Vorgänge.
|
||||
|
||||
\subsection{Erwogene Gegenmaßnahmen}
|
||||
|
||||
Vier Ansätze wurden formuliert:
|
||||
Tabelle~\ref{tab:race-countermeasures} stellt die vier erwogenen Ansätze gegenüber. Der erste sollte unabhängig von der Serialisierungslösung umgesetzt werden; der vierte ist bemerkenswert, da er eine organisatorische statt technische Lösung darstellt.
|
||||
|
||||
\begin{enumerate}
|
||||
\item \textbf{Absicherung der Fehlerbehandlung.} Vor dem Entfernen der Teilkopien wird geprüft, ob der Marker der Quelle noch existiert. Fehlt er, hat ein anderer Vorgang die Umbenennung abgeschlossen, und es darf nichts gelöscht werden. Das beseitigt Szenario~A weitgehend: Es bleiben überzählige Objekte zurück statt Daten verloren zu gehen.
|
||||
\item \textbf{Bedingtes Schreiben.} Wird der Marker nur unter der Bedingung geschrieben, dass er noch nicht existiert, ist das Beanspruchen eines Namens unteilbar und Szenario~B ausgeschlossen \autocite{aws-conditional-writes}. Voraussetzung ist, dass der Speicher diese vergleichsweise junge Erweiterung unterstützt — was ausdrücklich nachzuweisen wäre.
|
||||
\item \textbf{Instanzübergreifende Sperre.} Eine über die Datenbank realisierte Sperre je Organisation würde den verändernden Teil sauber serialisieren, kostet aber einen zusätzlichen Zugriff je Anfrage und erfordert ein Konzept für den Fall, dass eine Sperre nicht freigegeben wird.
|
||||
\item \textbf{Verlagerung aus dem Anfragepfad.} Bestandsordner werden einmalig kontrolliert migriert, und die Anwendung protokolliert lediglich, wenn ein Ordner vom Sollzustand abweicht. Damit verschwindet die Ursache vollständig statt abgesichert zu werden.
|
||||
\end{enumerate}
|
||||
|
||||
Der erste Ansatz sollte unabhängig von der Serialisierungslösung umgesetzt werden. Der vierte ist bemerkenswert, da er eine organisatorische statt technische Lösung darstellt.
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\begin{tabularx}{\textwidth}{@{} l X @{}}
|
||||
\toprule
|
||||
\textbf{Ansatz} & \textbf{Wirkung} \\
|
||||
\midrule
|
||||
Absicherung der Fehlerbehandlung & Prüfung, ob der Marker der Quelle vor dem Entfernen der Teilkopien noch existiert; beseitigt A weitgehend, es bleiben überzählige Objekte statt Datenverlust. \\
|
||||
\addlinespace
|
||||
Bedingtes Schreiben & Marker nur schreiben, falls noch nicht vorhanden, macht das Beanspruchen eines Namens unteilbar und schließt B aus \autocite{aws-conditional-writes}; setzt Unterstützung durch den Speicher voraus. \\
|
||||
\addlinespace
|
||||
Instanzübergreifende Sperre & Sperre je Organisation über die Datenbank serialisiert den verändernden Teil sauber, kostet aber einen zusätzlichen Zugriff je Anfrage und ein Konzept für nicht freigegebene Sperren. \\
|
||||
\addlinespace
|
||||
Verlagerung aus dem Anfragepfad & Einmalige kontrollierte Migration der Bestandsordner; die Anwendung protokolliert nur noch Abweichungen vom Sollzustand — die Ursache verschwindet vollständig. \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\caption{Erwogene Gegenmaßnahmen gegen die Nebenläufigkeitsszenarien}
|
||||
\label{tab:race-countermeasures}
|
||||
\end{table}
|
||||
|
||||
\subsection{Umgang mit dem Befund}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Die Unicorn Development arbeitet agil mit zweiwöchigen Sprints in Azure DevOps.
|
||||
\label{fig:unicorn-process}
|
||||
\end{figure}
|
||||
|
||||
Am Anfang steht eine Idee, die festgehalten und als Product Backlog Item ausformuliert wird; das Item steht dabei auf \texttt{New}. Nach der Prüfung der technischen Machbarkeit und der kaufmännischen Abwicklung — Angebot, Annahme, Auftragsbestätigung — geht es in die Freigabe (\texttt{To Approve}). Im Approval-Termin prüft das Team die \emph{Definition of Ready}, stellt Rückfragen und schätzt den Aufwand; danach gilt das Item als \texttt{Approved}. Mit der Sprintplanung wechselt es auf \texttt{Committed} und wird umgesetzt. Ist die Umsetzung abgeschlossen und in den Hauptbranch übernommen, steht es auf \texttt{Dev Completed}. Es folgen das Release ins Testsystem, das Review mit dem Kunden sowie die Tests durch Unicorn Development und Kunde; mit deren Abschluss erreicht das Item \texttt{Test Completed}. Erst danach erfolgt das Release ins Produktivsystem mit anschließender Abnahme; die abschließenden kaufmännischen Schritte — Rechnungsstellung, Abschluss in Kimai, Anpassung der Wartungsgebühr — überführen es nach \texttt{Done}.
|
||||
Am Anfang steht eine Idee, die als Product Backlog Item ausformuliert wird (\texttt{New}). Nach Prüfung von Machbarkeit und kaufmännischer Abwicklung geht es in die Freigabe (\texttt{To Approve}); im Approval-Termin prüft das Team die \emph{Definition of Ready} und schätzt den Aufwand (\texttt{Approved}). Mit der Sprintplanung wechselt es auf \texttt{Committed} und wird umgesetzt; nach Übernahme in den Hauptbranch steht es auf \texttt{Dev Completed}. Es folgen Release ins Testsystem, Review und Tests (\texttt{Test Completed}), danach Release ins Produktivsystem, Abnahme und die abschließenden kaufmännischen Schritte bis \texttt{Done}.
|
||||
|
||||
Das Dokumentenfeature wurde ohne Abweichung nach diesem Prozess bearbeitet. Da es sich um eine Erweiterung des eigenen Produkts Houston und nicht um einen Einzelauftrag handelt, entfielen lediglich die auftragsbezogenen Schritte; alle Freigabe-, Test- und Abnahmestufen wurden regulär durchlaufen.
|
||||
|
||||
|
||||
@@ -18,17 +18,8 @@ Da der Document Explorer ohnehin erst am 27.~Juli in die Umsetzung ging, entstan
|
||||
|
||||
\subsection{Freischaltung zusätzlicher Funktionen}
|
||||
|
||||
Aufwendiger war die Klärung der verfügbaren S3-Funktionen. Im Rahmen der Lookup-Recherche (siehe Abschnitt~\ref{sec:lookup-research}) kamen zwei in Betracht:
|
||||
Im Rahmen der Lookup-Recherche (siehe Abschnitt~\ref{sec:lookup-research}) kamen zwei S3-Funktionen in Betracht: \textbf{S3 Select} (\texttt{SelectObjectContent}), das Objektinhalte serverseitig per SQL-ähnlicher Abfrage filtert \autocite{aws-s3-select, storagegrid-s3-select}, und der \textbf{Search Integration Service} von StorageGRID, der Objektmetadaten in einen Elasticsearch-Index spiegelt \autocite{storagegrid-search-integration}.
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{S3 Select} (\texttt{SelectObjectContent}) erlaubt es, Inhalte einzelner Objekte serverseitig per SQL-ähnlicher Abfrage zu filtern \autocite{aws-s3-select, storagegrid-s3-select}.
|
||||
\item Der \textbf{Search Integration Service} von StorageGRID spiegelt Objektmetadaten in einen Elasticsearch-Index und ermöglicht dadurch eine echte Suche über Metadaten \autocite{storagegrid-search-integration}.
|
||||
\end{itemize}
|
||||
Am 30.~Juli beantragte ich die Freischaltung beider Funktionen. \emph{Lennart Meinert} kontaktierte am 4.~August Advanced Unibyte; am 7.~August wurde S3 Select für alle drei Umgebungen aktiviert. Für den Search Integration Service teilte Advanced Unibyte am 17.~August mit, dass die Funktion derzeit nicht angeboten werde. Da bereits eine Lösung ohne serverseitige Suche umgesetzt war (siehe Abschnitt~\ref{sec:lookup-decision}), wurde der Request geschlossen.
|
||||
|
||||
Am 30.~Juli beantragte ich die Freischaltung beider Funktionen. \emph{Lennart Meinert} kontaktierte am 4.~August Advanced Unibyte; am 7.~August wurde S3 Select für alle drei Umgebungen aktiviert.
|
||||
|
||||
Für den Search Integration Service teilte Advanced Unibyte am 17.~August mit, dass die Funktion derzeit nicht angeboten werde, und schlug am 21.~August ein Folgegespräch vor. Da bereits eine Lösung ohne serverseitige Suche umgesetzt war (siehe Abschnitt~\ref{sec:lookup-decision}), wurde der Request geschlossen.
|
||||
|
||||
\subsection{Bewertung}
|
||||
|
||||
Zwischen erstem Antrag und abschließender Klärung lagen vier Wochen — zu Projektbeginn nicht eingeplant. Ein Lösungsansatz, der auf einer noch zu beschaffenden Fremdleistung beruht, ist innerhalb eines Projektzeitraums von wenigen Wochen nicht belastbar. Die gewählte Lösung kommt daher ohne Erweiterung der Speicherfunktionen aus.
|
||||
Zwischen erstem Antrag und abschließender Klärung lagen vier Wochen — ein Lösungsansatz, der auf einer noch zu beschaffenden Fremdleistung beruht, ist innerhalb eines solchen Projektzeitraums nicht belastbar.
|
||||
|
||||
@@ -13,16 +13,8 @@ Die Tests bilden die in Abschnitt~\ref{sec:lookup-decision} beschriebenen Wege e
|
||||
|
||||
Die Anforderung an den Lookup war keine funktionale, sondern eine über den Aufwand. Ein Test, der nur das richtige Präfix prüft, bestünde auch, wenn die Implementierung weiterhin alle Ordner durchliefe. Die eigentliche Eigenschaft — dass der Regelfall mit einem einzigen Zugriff auskommt — lässt sich nur über die beobachteten Aufrufe prüfen.
|
||||
|
||||
\subsection{Testdaten}
|
||||
\subsection{Befunde aus dem Review von Testcode}
|
||||
|
||||
\emph{Timo Walter} merkte an, dass reale Kundennamen in den Tests verwendet wurden, und schlug Platzhalter vor. Der Hinweis ist klein, in der Sache aber richtig: Testdaten liegen in der Versionsverwaltung und bleiben dauerhaft lesbar; ein realer Kundenname ist eine unnötige Offenlegung, da der Test mit Platzhaltern denselben Zweck erfüllt. Die Daten wurden ersetzt.
|
||||
Zwei Reviewhinweise \emph{Timo Walters} betrafen nicht den Produktivcode, sondern die Tests selbst. Zum einen verwendeten Testdaten reale Kundennamen statt Platzhaltern — unnötig, da Versionsverwaltung dauerhaft lesbar bleibt und Platzhalter denselben Zweck erfüllen; die Daten wurden ersetzt. Zum anderen bestand ein Test aus dem falschen Grund: Er brach bereits in der ersten Zeile an einem unbekannten Typbezeichner ab, ohne die eigentlich zu prüfende Sicherheitseigenschaft — dass ein in die Adresse geschriebener Kundenname kein Ergebnis liefert — je zu erreichen.
|
||||
|
||||
\subsection{Ein Test, der das Falsche prüfte}
|
||||
|
||||
\emph{Timo Walter} bemerkte beim Lesen, dass ein Test bestand, aber aus dem falschen Grund: Der Prüfling brach in der ersten Zeile ab, weil die Pfadauswertung einen unbekannten Typbezeichner vorfand — die eigentlich zu prüfende Logik wurde nie erreicht.
|
||||
|
||||
Er leitete einen vermuteten Fehler ab: Ein Dokument ohne Typ dürfe die Auswertung nicht scheitern lassen. Die Klärung ergab, dass der Code korrekt, der Test aber missverständlich benannt war. Geprüft wurde eine Sicherheitseigenschaft: dass ein Benutzer, der den Kundennamen als Pfadbestandteil in die Adresse schreibt, kein Ergebnis erhält und keine Anfrage an den Speicher ausgelöst wird.
|
||||
|
||||
Der Vorgang zeigt zweierlei: Ein Test, der aus dem falschen Grund grün ist, ist wertlos, weil er Sicherheit suggeriert. Der Fund war nicht durch Ausführen zu erzielen, sondern nur durch Lesen — er belegt den Wert des Reviews für Testcode.
|
||||
|
||||
Als Konsequenz wurde die Absicht des Tests explizit gemacht. Diese Anforderung — dass ein Test ohne den Fix fehlschlagen muss — wurde in die Akzeptanzkriterien des Folgeitems zur Nebenläufigkeit aufgenommen (siehe Abschnitt~\ref{sec:race-conditions}).
|
||||
Der zweite Fund war nur durch Lesen, nicht durch Ausführen zu erzielen, da der Test grün war. Als Konsequenz wurde die Testabsicht explizit benannt; die Anforderung, dass ein Test ohne den jeweiligen Fix fehlschlagen muss, floss in die Akzeptanzkriterien des Folgeitems zur Nebenläufigkeit ein (siehe Abschnitt~\ref{sec:race-conditions}).
|
||||
|
||||
Reference in New Issue
Block a user