Problem
Beim Einschreiben eines Benutzers in einen Hostkurs wird zunächst geprüft, ob bereits ein aktiver Benutzer-Lernpfad existiert:
$userpath = self::buildsqlqueryuserpath(
$learningpath->id,
$params->relateduserid,
$courseid
);
if (!$userpath) {
$DB->insert_record('local_adele_path_user', ...);
}
Zwischen Leseabfrage und Insert besteht keine Datenbanksperre und keine Eindeutigkeitsbedingung.
db/install.xml definiert für local_adele_path_user lediglich den Primärschlüssel sowie Fremdschlüssel. Eine eindeutige fachliche Identität aus Benutzer, Hostkurs und Lernpfad ist nicht abgesichert.
Ursache
Es handelt sich um ein klassisches Check-then-insert-Race:
- Prozess A prüft, ob ein Datensatz existiert.
- Prozess B prüft gleichzeitig, ob ein Datensatz existiert.
- Beide erhalten keinen Treffer.
- Beide legen einen aktiven Datensatz an.
Der vorhandene Idempotenztest mit zwei nacheinander ausgeführten Events deckt diese Konkurrenzsituation nicht ab.
Lösung
Die fachliche Eindeutigkeit muss auf Datenbankebene abgesichert werden.
Für das derzeitige Datenmodell bietet sich ein Unique-Key an:
user_id, course_id, learning_path_id
Vor Einführung des Indexes müssen vorhandene Duplikate bereinigt werden.
Falls künftig mehrere historische Revisionen in derselben Tabelle gespeichert werden sollen, ist das Datenmodell vorher zu trennen, beispielsweise in:
- eine eindeutige aktuelle Benutzer-Lernpfad-Relation,
- eine separate Revisions- oder Historientabelle.
Zusätzlich sollte die Erzeugung transaktional erfolgen und einen möglichen Duplicate-Key-Konflikt kontrolliert behandeln:
- Insert versuchen.
- Bei konkurrierendem Unique-Verstoß vorhandenen Datensatz laden.
- Mit diesem Datensatz fortfahren.
- Keine doppelte Eventkette auslösen.
Manuelles Testverfahren
Deterministischer Datenbanktest
- Einen gültigen aktiven Benutzer-Lernpfad erzeugen.
- Seine IDs ermitteln.
- Einen zweiten fachlich identischen Datensatz einfügen:
INSERT INTO mdl_local_adele_path_user (
user_id,
course_id,
learning_path_id,
status,
timecreated,
timemodified,
createdby,
json,
last_seen_by_owner
)
SELECT
user_id,
course_id,
learning_path_id,
status,
timecreated,
timemodified,
createdby,
json,
last_seen_by_owner
FROM mdl_local_adele_path_user
WHERE id = <EXISTING_ID>;
- Die Anzahl kontrollieren:
SELECT
user_id,
course_id,
learning_path_id,
COUNT(*) AS records
FROM mdl_local_adele_path_user
WHERE status = 'active'
GROUP BY user_id, course_id, learning_path_id
HAVING COUNT(*) > 1;
Aktuelles Ist-Verhalten
Die Datenbank akzeptiert den zweiten fachlich identischen Datensatz.
Erwartetes Soll-Verhalten
Der zweite Insert wird durch eine Eindeutigkeitsbedingung verhindert.
Optionaler Konkurrenztest
In einer Testumgebung zwei Prozesse zeitgleich dieselbe Einschreibeverarbeitung für denselben Benutzer, Hostkurs und Lernpfad aufrufen. Nach Abschluss darf genau ein Datensatz vorhanden sein.
Upgrade-Anforderungen
- Vorhandene Duplikatgruppen identifizieren.
- Festlegen, welcher Datensatz erhalten bleibt.
- Fortschrittsinformationen kontrolliert zusammenführen.
- Abhängige Tasks und Einschreibungen bereinigen.
- Unique-Key anlegen.
- Upgrade idempotent ausführen.
Automatisierte Tests
- Sequentielle doppelte Verarbeitung.
- Simulierte konkurrierende Inserts.
- Upgrade-Test mit vorhandenen Duplikaten.
- Prüfung, dass Fortschrittsdaten bei der Bereinigung nicht verloren gehen.
- Prüfung, dass nur eine nachgelagerte Update-Kette ausgeführt wird.
Akzeptanzkriterien
Problem
Beim Einschreiben eines Benutzers in einen Hostkurs wird zunächst geprüft, ob bereits ein aktiver Benutzer-Lernpfad existiert:
Zwischen Leseabfrage und Insert besteht keine Datenbanksperre und keine Eindeutigkeitsbedingung.
db/install.xmldefiniert fürlocal_adele_path_userlediglich den Primärschlüssel sowie Fremdschlüssel. Eine eindeutige fachliche Identität aus Benutzer, Hostkurs und Lernpfad ist nicht abgesichert.Ursache
Es handelt sich um ein klassisches Check-then-insert-Race:
Der vorhandene Idempotenztest mit zwei nacheinander ausgeführten Events deckt diese Konkurrenzsituation nicht ab.
Lösung
Die fachliche Eindeutigkeit muss auf Datenbankebene abgesichert werden.
Für das derzeitige Datenmodell bietet sich ein Unique-Key an:
Vor Einführung des Indexes müssen vorhandene Duplikate bereinigt werden.
Falls künftig mehrere historische Revisionen in derselben Tabelle gespeichert werden sollen, ist das Datenmodell vorher zu trennen, beispielsweise in:
Zusätzlich sollte die Erzeugung transaktional erfolgen und einen möglichen Duplicate-Key-Konflikt kontrolliert behandeln:
Manuelles Testverfahren
Deterministischer Datenbanktest
Aktuelles Ist-Verhalten
Die Datenbank akzeptiert den zweiten fachlich identischen Datensatz.
Erwartetes Soll-Verhalten
Der zweite Insert wird durch eine Eindeutigkeitsbedingung verhindert.
Optionaler Konkurrenztest
In einer Testumgebung zwei Prozesse zeitgleich dieselbe Einschreibeverarbeitung für denselben Benutzer, Hostkurs und Lernpfad aufrufen. Nach Abschluss darf genau ein Datensatz vorhanden sein.
Upgrade-Anforderungen
Automatisierte Tests
Akzeptanzkriterien