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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user