DE:Konzept OpenSlides 4 - OpenSlides/OpenSlides GitHub Wiki

OpenSlides 4

Achtung: Dieses Dokument beinhaltet ursprüngliche Ideen für OpenSlides 4 um November 2019 herum. Es ist nicht mehr aktuell und nur für Interessenten für die Hintergründe um OpenSlides 4 noch vorhanden.

System architecture of current OpenSlides 4

Inhaltsverzeichnis

Aufbau des Dokumentes

Als erstes werden neue Features (Kapitel 1) und Größenordnungen vorgestellt die als feste Anorderungen für OpenSlides 4 im Vordergrund stehen. Dann folgen architektonische Grundlagen und Ideen (Kapitel II), um die Anforderungen aus einer Systemsicht zu erfüllen. Als drittes gibt es logische Betrachtungen (Kapitel III) vor allem von den Datenmodellen (wie werden Anträge gespeichert, wie ist der Meta-Aufbau aus Gremien, Veranstaltungen, Nutzern...) für existierende Features aus OpenSlides 3 und auch Modellierungsvorschläge für neue Features. Kapitel IV umfasst alle Änderungen am Client.

Erfahrungen von Instanzgrößen aus OpenSlides 3

Aus zwei großen Konferenzen kann man die Nutzung gut anhand der Chronik beurteilen. Diese umfasst mehr als 99% der Datenbank und spiegelt zeitaufgelöst die Nutzung von OpenSlides im Bezug auf verschiedene Teile der Anwendung wieder:

(V):

  • Instanz ca. 5 Monate genutzt, währenddessen 187000 History-Einträge, 415 MB
  • In aktiver Konferenzzeit (7 Tage): 32000 Einträge/80 MB
  • Gesamte Einträge > 1%: Anträge 75000 (40%), Item 45000 (24%), Redeliste 41000 (22%), Nutzer 9600 (5.1%), persönliche Notiz 9000 (4.8%), Projektor 2400 (1.3%)
  • Gesamte Bytes > 1%: Anträge 317 MB (74.4%), persönliche Notiz 53 MB (12.4%), Projektor 23.4 MB (5.5%), Item 14.7 MB (3.4%), Redeliste 10 MB (2.4%), Nutzer 4.7 MB (1.1%)

(I):

  • Instanz ca. 7.5 Monate genutzt, währenddessen 218000 History-Einträge, 496 MB
  • In aktiver Konferenzzeit (7 Tage): 30000 Einträge/76 MB
  • Gesamte Einträge > 1%: Anträge 133000 (61%), Item 43000 (19.8%), Redeliste 26000 (12%), Nutzer 7000 (3.2%), persönliche Notiz 4500 (2.1%)
  • Gesamte Bytes > 1%: Anträge 445 MB (87%), Projektor 20.5 MB (4%), persönliche Notiz 17 MB (3.5%), Item 14 MB (2.9%), Redeliste 6.3MB (1.3%)

Anmerkung: Die Daten sind bereinigt (#5121): Persönliche Notizen mit >1MB wurden herausgenommen.

I) Features

Ansich werden alle Features von OpenSlides 3 weiter behalten, sodass es kein Featureverlust gibt. OpenSlides soll um eine "Metaebene" über den klassischen OS3-Instanzen erweitert werden:

  • Gremien können angelegt werden, die ein oder mehrere OS3-Instanzen enthalten. Diese Instanzen sind nun Veranstaltungen, um regelmäßig wiederkehrende Sitzungen zu ermöglichen.
  • Zentrale Nutzerverwaltung. Die Nutzer lassen sich Gremien und Veranstaltungen zuweisen, sind aber nicht gremien- oder veranstaltungspezifisch.
  • Verbindende Elemente zwischen Veranstaltungen: Weiterleiten/Vertagen von Anträgen, geteilte Nutzergruppen für Gremien und Veranstaltungen, ...
  • Die Chronik soll nicht nur für Anträge, sondern alle Anwendungsbereiche funktionieren.
  • Importieren und Exportieren von Veranstaltungen und Importer für OS3 Datenbanken (Möglicherweise über "Export to OS4" Option in OS3).
  • Intrinsische Skalierbarkeit, mehr Logging
  • persönliche Benachrichtigungen
  • größere Personifizierung: Persönliche Notizen und Favoriten für mehr Objekte, User-Label statt Tags:
    • Farbe, Tagname
    • Zuweisbar zu Anträgen, Topics, ...
    • Es muss nach Labels sortierbar und filterbar sein
  • Sicherheitszentrale für jeden Nutzer und 2FA

1) Veranstaltungen

Veranstaltungen lassen sich klonen. Damit kann man diese Szenarien umsetzen: Eine Veranstaltung kann als Vorlage dienen, die man in eine neue Veranstaltung klonen kann. Ein Veranstaltungsbackup kann erstellt werden (zum Experimentieren, soll nicht Chronik ersetzten). Es ist ein Im- und Export von Veranstaltungen geplant (Klonen kann systeminternes Exportieren und Importieren sein). Zweck der Gremien ist es, dass zwischen Ihnen eine Struktur der Kompetenzbereiche aufgebaut werden kann, sodass Anträge weitergeleitet werden können. Dabei kann zu jedem Gremium die logisch höheren Organisaitonen angegeben werden. Weitergeleitete Anträge werden dem Verwalter eines Gremiums angezeigt, der diese dann die passenden Veranstaltung zuweist. Auch sollen Anträge von einem Termin in einen anderen vertagt werden können.

2) Nutzerverwaltung

Zunächst werden nur lokale Nutzer verwaltet, d.h. es ist keine externe Authentifizierung vorgesehen.

Gruppen und Berechtigungen sind für jede Veranstaltung einzeln einstellbar. Auf der organizationsebene gibt es wenige Berechtigungen, die in Rollen zusammengefasst werden. Es muss Nutzer auf organizationsebene geben, die alle Nutzer verwalten dürfen und zu Gremien zuordnen können. Diese Mitglieder eines Gremiums können innerhalb des Gremiums Verwalter sein. Innerhalb eines Gremiums können nur die Verwalter alle Mitglieder sehen und Veranstaltungen zuweisen. Innerhalb der Veranstaltung kann durch can_see_names die Sichtbarkeit von Teilnehmenden in der Veranstaltung geregelt werden.

3) Persönliche Benachrichtigungen

(Anmerkung: Optional) Nutzer sollen Benachrichtigungen pro Objekte einstellen können. Von einem Nutzern erstellte oder modifizierte Objekte sind automatisch abonniert solange sie nicht explizit deabonniert werden. Ein Nutzer erhält Benachrichtigungen zu jedem abonnierten Objekt bezogen auf die aktive Veranstaltung: Innerhalb einer Veranstaltung werden nur Benachrichtigungen der entsprechenden Veranstaltung angezeigt. Der Client erhält ein Benachrichtigungs-Center mit einer Übersicht über alle Benachrichtigungen pro Veranstaltung. Dabei gibt es zwei Möglichkeiten:

  1. Alle Benachrichtigungen erhalten ein "gesehen"-Flag, sodass die vollständige Benachrichtigungshistorie abgespeichert wird (ähnlich zu Facebook)
  2. Benachrichtigungen werden gelöscht, nachdem sie angesehen wurden (ähnlich zu Github)

Neben einem manuellen Setzen des "gelesen"-Flags (oder dementsprechend dem Löschen einer Benachrichtigung), sollte eine Logik existieren, die beim Aufrufen einer Seite, die entsprechenden Benachrichtigungen als gelesen markiert. Gibt es Benachrichtigungen zu einer Redeliste, soll diese Benachrichtigung markiert werden, wenn der Nutzer die Redeliste öffnet. In dem Benachrichtigungs-Center sollen alle Benachrichtigungen (gesamt und pro Veranstaltung) als gelesen markiert werden können, sowie (bei Option 1) der Benachrichtigungsverlauf angezeigt abgerufen werden können.

4) Sicherheitszentrale und 2FA

Jeder Nutzer hat die Möglichkeit seine Logins und Logouts nachzuverfolgen. Zudem werden alle aktiven Nutzersessions aufgelistet, sodass ein Nutzer diese Beenden kann. Im Gegensatz zu OpenSlides 3 soll es in den betroffenen Clients sofort ersichtlich sein, dass man ausgeloggt wurde.

Als weitere Sicherheitsmaßnahme können Zwei-Faktor-Authentisierungen (2FA) angeboten werden. Ein nativ unterstütztes Verfahren sollte eine Verifikation per E-Mail sein. Benutzer können ein Verfahren aus den angebotenen auswählen. (TODO: möglicherweise möchte man Nutzer zwingen eine 2FA zu nutzen). TODO: Welche Verfahren werden noch angeboten? Es ist angedacht, eine Zwei-Faktor-Authentifizierung unter Verwendung von FIDO2 mit den nachfolgenden Verfahren zu unterstützen:

  • PIN mittels SMS empfangen (sofern eine kostenfreie Möglichkeit gesichtet wird)
  • PIN mittels E-Mail empfangen (sofern der kostenfreie Versand von SMS nicht angeboten werden kann)
  • TOTP mittels Authenticator-App

Zurzeit befindet sich eine Zwei-Faktor-Authentifizierung in Planung. Ein Konzept zur Implementierung erfolgt.

II) Architektur

Hier geht es hauptsächlich um die Organization des Servers: Welche Komponenten gibt es, wie sind einige davon aufgebaut/strukturiert und wie kommunizieren diese Komponenten miteinander. Zudem wird die Datenbanktechnologie festgelegt und Richtlinien/Beschränkungen für die Repräsentation von Modellen gegeben.

Motivation

Folgende Ziele werden mit einer Neukonzipierung des Servers und der Client-Server-Kommunikation verfolgt:

  1. Nachvollziehbarkeit: Chronik für das ganze System -> Event log
  2. Weniger Daten für Autoupdates und gezieltere Autoupdates, um weniger Overhead bei personifizierten Daten zu haben:
  3. Persönliche Benachichtigungen, Labels und Notizen (nicht nur für Anträge).
  4. Papierkorb für alle Elemente
  5. Klare Request-Routen
  6. Expliziter Offlinemodus
  7. Trennen von Verantwortungen in einzelne Services
  8. Vor-Ort Cache für große Konferenzen
  9. Im- und Export von Veranstaltungen

Die groben Lösungen sind hier angegeben:

  1. Eventsourcing als grundlegende DB-Architektur
  2. Nur einzelne Keys im Autoupdate und beschränkte Modellstruktur
  3. Pub-Sub Autoupdates und weiterleiten von Events aus dem Eventsourcing
  4. Alle Modelle bekommen ein deleted-Flag. Entgültige Löschung durch Admins möglich.
  5. Aktionen und Domain-Logik
  6. Explizite Routen, um Daten abzufragen
  7. Service-Orientiertes, orchestriertes Design (kein Monolith, aber nicht zwangsläufig Microservices) und Code-Organization
  8. Docker
  9. JSON-Serialisierung einer Veranstaltung.

Es schließen sich weitere Vorgaben aus den vorgestellten Konzepten an:

  1. Mediafiles
  2. Volltextsuche
  3. Presenter
  4. Projektor
  5. Authservice
  6. Servereinstellungen
  7. API-Gateway
  8. Inter-Client-Kommunikation (ICC)

Nicht-Service spezifisch, aber wichtig!

  1. Logging
  2. Einheitliche Namensgebung (Architektur-Ebene)
  3. Fehlerbehandlung
  4. Testing
  5. Codestyle und Design innerhalb der Services
  6. Migrationen
  7. Inter-Service-Kommunikation

Generell sollte über folgendes Nutzungsmuster nachgedacht werden: Es werden i.A. wenig Requests abgesendet, die geänderten Daten betreffen jedoch potentiell viele Nutzer. Es wird also wenig geschrieben aber viel gelesen, sodass der Server einen großen "Fanout" im Bezug auf die Datenmenge hat. Deutlich wird dies durch die Zahlen von (I) und (V) (siehe Abschnitt "Erfahrungen von Instanzgrößen aus OS3"): Bei 7 Tagen und angenommenen 18 Stunden ergibt dies ca. 250 History-Einträge pro Stunde. Dabei werden die Requests pro Stunde deutlich niedriger sein, da pro Request mehrere Einträge geschrieben werden können.

Folglich können Schreibzugriffe aufwändiger sein, um beim Lesen möglichen zusätzlichen Aufwand (z.B dem Folgen von Rückwärtsbeziehungen) zu minimieren. Die Latenz des Systems soll möglichst gering gehalten werden.

1) Eventsourcing

Als Einleitung: https://www.youtube.com/watch?v=STKCRSUsyP0

Der Eventstore wird selbst implementiert und mit Postgresql auf eine zuverlässige Datenbank zum Speichern von Events gesetzt. Der Eventstore ist mit dem Messagebus verbunden und gibt die Events nach dem erfolgreichen Schreiben an das System weiter (dazu unten mehr). Dabei bleibt die Datenbank die einzige Quelle der Wahrheit. Daraus folgen zwei Beobachtungen:

  1. Der Eventstore ist für die Generierung von Position zuständig und hält Eindeutigkeitsbedingungen dieser Werte ein.
  2. Erst nach einem erfolgreichen Schreiben in die Datenbank darf in den Messagebus geschrieben werden.

Schreiben und Optimisic Concurrency Control (OCC): Damit auf einer pro-Feld-Basis auf verpasste Änderungen geprüft werden kann, wird der Schreibpart des Eventstores als Single-Writer ausgelegt. Um Konflikte zu erkennen wird zu den geänderten Feldes die Position mit angegeben.

Nach einem erfolgreichen Schreiben in die Datenbank werden alle betroffenen Modelle berechnet. D.h. es wird die aktuelle Version der Modelle aus der Datenbank zusammengestellt, um diese in den Cache zu sichern (Nach einem Update wird es Autoupdates geben, die genau diese Daten benötigen). Danach werden die Modelle in die Views geschrieben (siehe unten). Als letztes werden Events über den Message-Bus verteilt und alle weiteren Dienste über Änderungen informiert.

Eine Schreibanfrage kann mehrere Events beinhalten, z.B. Das Gleichzeitige Update eines Modells und das Löschen von Feldern des selben Modells. Der Eventstore muss sicherstellen, dass jedes Feld eindeutig verwendet wird. Da diese Events mehrere Positionen ergeben werden zu den oben genannten Events eine Positionsspanne mit angegeben.

Nach einer bestimmten Metrik werden Snapshots eines Modelles angelegt: Das berechnete Modell wird in einer zweiten Snapshot-Tabelle gesichert, sodass das Berechnen von späteren Versionen immer auf dem letzten Snapshoteintrag vor der zu berechnenden Version basieren kann. Ist das Erstellkriterium erfüllt, wird ein neuer Eintrag in der Snaptshottabelle angelegt. Das Ziel dabei ist es, so viel Arbeit wie möglich von dem Eventstore abzukoppeln.

Eine Metrik wäre das Anlagen nach X Events eines Modells. Der Wert 50 ist aktuell ein guter Wert aus Erfahrungen der großen Konferenzen. Anders könnte man Snapshots jede Nacht anlegen (soweit es genügend Änderungen gaben), oder die Zeit der Wiederherstellung eines Objektes messen. Ist dort die Zeit zu groß, wird ein Snapshot angelegt.

Damit alle Implementationen unabhängig der speziellen Eigenschaften des Eventsourcings sind, erhält jedes Modell ein Meta-Feld: In diesem wird z.B: die aktuelle Position des Modells abgelegt. Dies ermöglicht herauszufinden, welche Position eines Modells bei einer Abfrage einer View vorliegt. Diese Daten setzt der Eventstore bei jedem herausgegebenen Datensatz.

2) Autoupdates: Server-Client-Kommunikation

2a) Modellstruktur

  • Modelle sind Dokumentenbasiert mit beliebigen Schlüsseln aufgebaut. Nicht unterstützt werden verschachtelte Modelle in Hinsicht auf Autoupdates und DB-Änderungen (siehe Datenevents). D.h. das kleinst mögliche Autoupdate beinhaltet ein Field-Value-Paar eines Modells, jedoch kann der Wert aus komplexen Daten (JSON-Struktur) bestehen.
  • Felder können strukturiert sein.
  • Referenzen zwischen zwei Modellen A und B werden in beiden gespeichert. Folglich kann B_id in A eine Zahl oder ein Array an Zahlen sein (analog für B), wobei anhand des Types (1:1, 1:N, N:1, N:M) erkannt werden kann, welches von beiden Varianten bei welchem Model erwartet wird. Schreibrequests benötigen daher mehr Zeit, um alle Referenzen anzupassen und erzeugen dabei wohl möglich viele Dantenevents und Autoupdates. Ein Beispiel ist das Löschen einer Kategorie, bei der die category_id aller Anträge der Kategorie geändert werden müssen. Durch einen anderen Autoupdate-Ansatz wird jedoch keine Flut an Autoupdates zu den Clients ausgelöst und nur serverintern viele Datenevents generiert.
  • Jedes Modell hat ein reservierten Feld "meta" (siehe 1 Eventsourcing), der nicht geschrieben werden darf und in dem strukturiert Informationen zu dem Modell (z.B: Position und Gelöscht-Status) gespeichert sind. Dabei ist "meta" als Prefix zu verwenden (z.B: "meta_position").
  • Jedes Modell muss ein id Feld haben. Die Id ist eine ganzzahlig Zahl.
  • Die Modelle sind in Collections unterteilt. Dabei fällt im Vergleich zu OS3 der App-prefix weg, da dieser meist unnötig ist und sich von der App-Einteilung gelöst werden kann. Beispiel: topic, projector, motion-state. workflow und state müssen mit motion- geprefixed werden, da es möglicherweise mehrere Status- und Workflow-Modelle für andere Objekte geben kann.
  • Der Zusammenschluss aus collection und id bezeichnet genau eine Modellinstanz und wird mit fqid (full-qualified-id; nicht element_id, da nicht immer klar ist, was ein Element überhaut ist) bezeichnet. Als Trenner wird / benutzt. Um ein Feld anzugeben (fqfield), wird dieser ebenfalls mit einem / angehängt. Beispiel: motions/3982/amendment_paragraphs_62. Dies hat den Vorteil, dass man fqids und fqfields als URL verwenden kann.
  • Ein Field, unter dem ein String gespeichert ist, kann in drei Kategorien geteilt werden:
    1. Der String wird vom Server geschrieben und niemals vom Client
    2. Der Client kann den String schreiben, enthält aber kein HTML
    3. Wie 2, nur mit HTML. Diese enthalten im Allgemeinen viel Text. Strings vom Typ 2 und 3 erhalten eine Maximallänge. Diese ist fest vorgegeben: Typ 2 ist maximal 256 Zeichen Lang und Typ 3 ist maximal 100.000 Zeichen lang. Neben einer HTML-Validierung von Typ 3 verhindert dies ungewolltes Hochladen von Megabytes an Texten und einer Flutung des Systems. Dies stellt der Application-Dienst sicher. Für bestimmte Veranstaltungen muss das Limit für Typ 3 geändert werden können. TODO: Entweder in den Settings oder der Veranstaltungsverwaltung - muss geklärt werden.

2b) Autoupdates

Anstatt dem wahllosen Senden jeder geänderten Daten an jeden (berechtigten) Client können die Autoupdates selektiver stattfinden. In OpenSlides gibt es grundsätzlich verschiedene Ansichten (z.B. Listen- und Detailansichten), und jeder Client hat von diesen genau eine geöffnet. Die Liste eines Modells oder ein spezifisches Modell liegt somit im Fokus. Dieser wird vom Client bestimmt.

Der Client kann bestimmen, von welchen Modellen (dem fokussierten oder den fokussierten) welche Felder bekommen möchte und in wie weit die Fremdrelationen gefolgt werden sollen. Z.B. kann in der Kategory-Liste alle Kategorien nur mit Prefix und Titel angefordert werden, oder auch alle Anträge dazu, aber von diesen nur Titel und Identifier. Auch möglich wären zu allen Anträgen ebenfalls deren Status, sodass die Abhängigkeitskette Kategory->Antrag->Status für den Client relevant ist, jedoch nicht die Antragsblöcke eines Antrages.

Der Client kann mherere Anfragen parallel stellen, d.h. es können mehrere Subscriptions gleichzeitig stattfinden, wobei jede Subscription durch ein Handle gekennzeichnet ist und Subscriptions einzeln verwaltet werden können. Der Server führt folglich eine Liste an aktiven Subscriptions pro Client. Der Einsatzzweck für mehrere Subscriptions wäre beispielsweise der im Client angemeldete Nutzer (Operator) und in der Projector-Detail-View alle Countdowns, Messages und ein Projektor. Die Anzahl an subscriptions soll der Möglichkeit nach gering gehalten werden.

Wird nun eine Ansicht im Client geöffnet, abonniert der Client die Daten für das fokussierte Objekt in Form des ModelRequests. Der Server sendet das gesamte Modell inklusive der Fremdrelationen und alle folgenden Events werden ebenfalls zu dem Client gesendet. Caching wird im ersten Konzept nicht berücksichtigt, da die Notwendigkeit fraglich ist (siehe ausführliche Diskussion unten) und der Aufwand nicht zu groß seien soll.

Die referenzierten Modelle sollen nicht verschachtelt gesendet werden. Negativbeispiel: Die Liste von N Anträgen wird mit deren Kategorien abonniert und alle Anträge haben die selbe Kategorie. Für jeden Antrag soll die Kategorie-Id unter category_id inkludiert werden und die einzige Kategory ein Mal als selbstständiges Modell in der angeforderten Form anstatt N Mal in insgesamt jedem Antrag inkludiert zu sein. Eine Form der Art {<collection>: {<id>: Modell | null}} ist ausreichend, um auch das Löschen eines Modells zu berücksichtigen. Vermieden werden sollte die Notwendigkeit ein Modell auf zwei unterschiedlichen Darstellungsformen in einer Subscription anzufordern, da möglicherweise unterschiedliche Keys benöigt werden. Ein Fall der funktioniert ist die Subscription zum Operator (bei dem z.B. nur die Gruppen wichtig sind) und in der Nutzer-Detail-View der selbe Nutzer, nur mit mehr Informationen. Wird der Nutzer geupdated, erhalten beide Subscriptions die Daten, die unterschiedlich aufbereitet sind. Der Client kann genau dem Listener der Subscription die Daten geben, die er benötigt. Solche Dopplungen sollten bis auf den Operator nicht vorkommen.

Es erweist sich als sinnvoll, die Schlüssel einer Relation in beiden Modellen zu speichern. Diese redundante Datenhaltung erleichtert lesende Zugriffe und das Autoupdatesystem muss nicht mehr auf Relationen hören. Wird eine Nutzer-Gruppen-Relation geändert wird der Nutzer und die Gruppe geupdated, sodass kein Update verpasst wird. Diese Denormalisierung ist hierbei Notwendig und auch ein Resltat des dokumentenbasierten Speicherns von Modellen. Eine weitere Denormalisierung wäre das (wieder-)vereinen von Redeliste und Item. Da (a) kein Schema vorgegeben ist, welches eine Trennung befürwortet und (b) Autoupdates nur per Schlüssel sind und daher kein Overhead besteht, könnnen bestimmte Modelle wieder vereint werden.

Es gibt einen statuslosen Restrictionsservice, der einen ModelRequest, eine Nutzer-Id und optional eine Position entgegennimmt, und daraus die beschnittenen Daten (authentication und nur angefragte Keys) aus dem aktuellen Datenstand (bzw. Datenstand zum Zeitpunkt der Position) berechnet. Diese Ergenisse lassen sich optional Cachen, wobei die Möglichkeiten und vielseitigkeit der Anfragen wahrscheinlich zu groß ist, sodass die Cach-Hit-Rate sehr gering seien wird.

Was zuletzt noch betrachtet werden muss ist die Notwendigkeit vom Caching. Caching in OS3 hat folgende Gründe:

  • Schnelles initiales Laden
  • Offline Modus Beide Aspekte sind in dieser Form in diesem Konzept nicht mehr vorhanden. Zudem wird das Caching wesentlich komplizierter, da durch den Pub-Sub Ansatz, man die Positionen aller in einer Relationen stehenden Objekte senden müsste.

In der ersten Implementation wird ganz auf Caching dieser Art verzichtet.

Um die Clients zu entlasten und die optimale Zustellung aller Daten an alle Clients zu gewährleisten, wird von einem Pushen des Servers abgesehen. Die Clients geben Empfangsbereitschaft an (kein traditionelles Polling), sodass der Client die Flusssteuerung übernimmt. Dieses soll Pufferüberläufe am Server verhindern, sodass Verbindungen zu langsamen Clients nicht abgebrochen werden. Der Server geht somit individueller auf jeden Client ein, denn es ist inakzeptabel, falls einige Clients benachteiligt werden, wenn die Verbindung dauerhaft abbricht. Zudem können Autoupdates gebündelt werden, indem der Client möglicherweise einige Sekunden wartet, bis er eine Empfangsbereitschaft signalisiert. Auch der Server könnte ein kleines Delay einbauen, um den Paketoverhead zu minimieren und nahe unter 1500 Bytes pro Autoupdate kommt. Fällt ein Client zu weit zurück (Wert für "zurückfallen", zeitlich, Datenmenge oder anhand der Events, muss ausprobiert werden), wird schnell ausgeholt, und analog zu einer Subscription alle Werte neu gesendet.

Der Autoupdateservice hört auf Logout-Events des Authservices (Siehe 13). Falls eine Verbindung zum Client mit dem im Logout-Event gegebenen Session aufgebaut wurde, wird die Verbindung zum Client getrennt. Der Autoupdate muss sich sen Session-Identifier (siehe 13) jeder Verbindung merken.

3) Persönliche Benachichtigungen

In einer Datenbank des Benachrichtigungsservice wird ein Mapping von Nutzer zu Objekten gehalten, bei dem drei Möglichkeiten modelliert werden:

  • Status "?": Ein Mapping zwischen Nutzer und Objekt existiert nicht.
  • Status "ja": Ein Mapping existiert mit einer Eigenschaft die "ja" repräsentiert (z.B. ein Boolean)
  • Status "nein": Siehe "ja", nur für "nein". Der Service hört auf den Eventstream und fragt für jedes Objekt alle Nutzer mit Status "ja" ab. für diese wird eine Benachrichtigung generiert. Der Nutzer, der für dieses Event verantwortlich war wird abgefragt: Ist der Status "?", wird der Status zu "ja" geändert, da der Nutzer "Interesse" in dem Objekt gezeigt hat, es aber nicht explizit deabonniert hat. Im "?" Status steht dem Server die Entscheidung zu, ob ein Nutzer das Objekt abonnieren soll, oder nicht.

Alle Events werden in eine Tabelle mit den Spalten (id, user-id, object-id, assembly-id, seen, timestamp, message) gespeichert (Je nach Modellierung in I.3 kann seen wegfallen). Für Objekte, die keiner Veranstaltung zugeordnet werden können (z.B.: Veranstaltungen selbst, Gremien oder Nutzern) ist assembly-id=null. Es gibt mehrere Request-Endpunkte:

  • Abonnieren und Deabonnieren eines Objektes
  • Den Abonnementstatus eines Objektes abfragen ("?" ist dabei "nein")
  • Das Seen-Flag einer Benachrichtigung setzen
  • Abfragen aller Benachrichtigungen (für das Benachrichtigungs-Center). Dabei kann können alle nicht gelesenen Benachrichtigungen abgefragt werden oder, (Siehe I.3) falls die Benachrichtigungshistorie gewünscht ist, alle Events nach einem Timestamp X oder die letzten X Benachrichtigungen unabhängig des Seen-Flags abgefragt werden.
  • Bulk-Requests zum Setzen der Seen-Flags und zum (De-) Abonnieren.

Neue Benachrichtigungen (=Zeilen in der Benachrichtigungstabelle) werden in den Message-Bus geschrieben und über das Autoupdatesystem an die passenden Nutzer verteilt, solange dieser in der passenden aktiven Veranstaltung ist. Im Benachrichtigungscenter kann er allgemeine Benachrichtigungen und alle Benachrichtigung pro Veranstaltung sehen und verwalten.

4) Papierkorb

Da Objekte im Eventstore nicht gelöscht werden, sondern ein letztes Delete-Event gespeichert wird, kann dies Rückgängig gemacht werden. Ein Restore-Event kann das Delete-Event aufheben. Der Normalfall ist, dass Nutzer gelöschte Elemente nicht sehen können. Ein Admin kann über spezielle Views (nicht über das Autoupdate, für Views siehe 11), gelöschte Daten anfragen und potentiell wiederherstellen. Dabei kann es passieren, dass die Daten (z.B. durch Eindeutigkeitsbedingungen) mit dem aktuellen Datensatz inkompatibel sind.

Das Wiederherstellen übernimmt der Application-Service. Das Objekt wird gelesen und für alle Referenzen und unique-Felder wird überprüft, ob die Objekte existieren bzw. die Bedingungen eingehalten werden. Ist dies der Fall, kann das Element wiederhergestellt werden. Der Eventstore sendet ein create-Event auf den Messagebus, sodass alle anderen Dienste das Objekt wie ein neu erstelltes behandeln können. Gibt es Konflikte (da ein Objekt nicht vorhanden ist oder eine Bedingung verletzt wird), wird dem Client das Modell gesendet. Dabei werden alle fehlerhaften Referenzen entfernt und alle Felder mit verletzten Bedingungen auf None gesetzt. Gleichzeitig wird dem Client eine Liste an verletzten Felder mitgegeben. Der Nutzer kann die Werte ändern und dem Server erneut eine Anfrage auf Wiederherstellung des Modells schicken, jedoch sendet er die geänderten Felder mit. Es dürfen nur Felder geändert werden, die als verletzt markiert sind. Der Server startet einen neuen Wiederherstellungsvorgang und ersetzt bei jedem Konflikt den Wert des verletzten Felder mit dem Wert des gleichen übergebenen Feld. Wird ein Feld nicht gefunden, muss der Vorgang wiederholt werden. Ansonsten schließt die Wiederherstellung erfolgreich ab.

5) Aktionen und Domain-Logik

Neben ein paar speziellen Routen (auth, mediafiles, Benachrichtigungen, ...) gibt es eine zentrale Route für die Domain-Logik. Diese wird von dem Action-Service ausgeführt. Die Requests der Clients enthalten Aktionen:

Eine Aktion beschreibt eine atomare Aktion auf den Daten (die meist zu einer Änderung führen). Beispiel: motion/set_state, user/add_group, group/create, agenda/speak, assignment/add_to_agenda, ... Eine Aktion kann durch einen Identifikator (Vorschlag: Das Namensschema wie oben: /) aufgerufen werden und erwartet Daten, die vom Client gegeben werden (auch None). Das Wort "atomar" bezieht sich hierbei nicht nur auf die Logik, sondern auch auf die Datenveränderung. Dies kann durch Optimistic Concurrency (OCC) in dem verteilten System sichergestellt werden. Dazu unten mehr.

Eine Aktion ist wie folgt aufgebaut: Sie besitzt zwei Methoden validate(data, user_id) und execute(data, user_id) und ist dabei statefull. D.h. falls beim Validierungsschritt schon Daten aus der DB gelesen werden, müssen diese in der Ausführungsphase nicht noch ein weiteres Mal gelesen werden. data wird vom Client gesendet, die Nutzerid wird von dem Authentifizierungsdienst gestellt.

Die Route nimmt nicht nur eine Aktion, sondern ein Array an Aktionen an (z.B. {"actions": [{action: "agenda/delete", data: {id: 8}}, {action: "motion/set_category", data: {motion_id: 2, category_id: 3}}], "mode": 2}). Falls mehrere Aktionen gegeben sind, gibt es verschiedene Möglichkeiten, mit Fehlern umzugehen:

  • Sukzessive bearbeiten, nach dem Validieren sofort ausführen, beim ersten Fehler abbrechen
  • Sukzessive bearbeiten, nach dem Validieren sofort ausführen, alle Fehler melden (aber nicht abbrechen)
  • Erst alle Validieren, dann alle ausführen, alle Fehler melden (diese können nur durch Race-Conditions entstehen) Die Fehler können in einem Array zurückgegeben werden, sodass der Client jeden Fehler zu der gesendeten Action zuordnen kann (None im Array für Erfolgsfall). Das Array muss nicht gesendet werden, falls kein Fehler auftrat.

Das Ausführen der Aktionen führt letztendlich zu einer Menge an Datenevents pro Aktion, die in die Datenbank geschrieben werden und per Autoupdate gesendet werden. Ein oft in OpenSlides 3 bemängelter Aspekt ist die hohe Latenz für den Nutzer, der den Request abgesendet hat. Dieser bekommt möglicherweise sehr spät das Autoupdate, falls viele andere Clients vor ihm sind, und erhält erst spät eine Reaktion auf den Request. Die generierten Datenevents können speziell für die Aktion und den Request-Nutzer mit dem Restrictionsservice wie ein normales Autoupdate behandelt werden, sodass der Nutzer direkt in der Antwort das Autoupdate erhält.

Im 3. Modus (bulk-Validieren und bulk-Ausführen) kann es gerade durch die OCC zu Problemen kommen. Wenn zwei Aktionen den selben Feld der selben Modellinstanz bearbeiten ist klar, das die letztere Aktion beim Schreiben in den Eventstore fehlschlagen wird. Dabei ist es vom Client die Aufgabe sicherzustellen, dass dies nicht vorkommt. Gibt es Anfragen, die dies benötigen, muss eine passende Aktion dafür geschrieben werden (z.B. Hinzufügen eines Submitters und Sortieren der neuen Liste). Der Application-Service kann diese Fälle auch vor dem Speichern erkennen und eine Warnung loggen, sodass diese Fälle von Entwicklern behandelt werden können.

6) Offlinemodus

In OpenSlides 3 wurde ein impliziter Offlinemodus genutzt: Da der Client jederzeit immer alle Daten hatte, konnte er offline agieren. Durch die partiellen Autoupdates ist dies nicht mehr der Fall. Ein Nutzer kann explizit Snapshots des Systems zu dem aktuellen Zeitpunkt laden, d.h. ein Service (siehe 11) bietet alle Daten (analog zum initialen Autoupdate) dem Nutzer an. Der Client speichert sich diese Daten und die dazugehörige Position und kann von Zeit zu Zeit diese Daten aktualisieren (z.B. jede Stunde), falls es vom Nutzer gewünscht ist. Der Nutzer kann, falls gewünscht, sofort in den Offlinemodus wechseln, sodass die Verbindungen zum Server getrennt werden. Dies sind viele Details für die Clientimplementation, sodass hier nur wichtig ist, dass Daten bereitgestellt werden, sowie eine View existiert zum Abfragen der aktuellen Datenposition.

7) Docker

Das OpenSlides-Repository wird nur für Meta-Informationen (Authors, Changelog, ...) verwendet und enthält Scripte und Dateien zur Orchestrierung der einzelnen Services. Jeder Service wird in einem eigenen Repository mit dem Namensschema openslides-<Servicename> verwaltet.

Das OpenSlides-Repository bindet über eine Komposition aus docker-compose-Dateien alle Services ein (Beachte möglicherweise #3874). Unterstützende Scripts helfen bei Setup und Verwaltung. Es muss möglich sein (wenn Docker installiert ist) nach dem Klonen des Repositories mit einem einzigen Befehl OpenSlides zu starten (produktiv sowie zum Entwickeln).

Jeder Service soll ein Skript run-tests.sh (TODO: oder über make?) zur Verfügung stellen, um alle Tests des Services in einem Dockercontainer auszuführen. Generell soll ein Entwickler keine zusätzlichen Bibliotheken/Compiler/... installieren, d.h. dass alle Abhängigkeiten im Dockercontainer selbst sind.

Anmerkung: Da einige IDEs sich schwer tun, ohne die Dateien der Abhängigkeiten eine Codeanalyse durchzuführen, sollte ein Script existieren, dass die Dependencies aus einem laufenden Dockercontainer auf das Hostsystem kopiert. Dies vermeidet, dass ein Entwickler weiteren Abhängigkeiten installieren müssen, jedoch mit einer gewohnten Entwicklungsumgebung arbeiten können. Dieser Schritt ist optional und falls Abhängigkeiten auf dem Hostsystem benötigt werden, muss der Befehl nur nach Updates der Abhängigkeiten ausgeführt werden.

8) Vor-Ort-Cache

Idee: Einen Vor-Ort Server in den Docker-Swarm integrieren und wichtige Services (z.B: Messagebus, Autoupdate-Service, Eventstore-Views) auf diesen beschränken. Welche Ressourcen dieser benötigt und ob das wirklich hilft muss getestet werden. Zumindest wird die Anzahl der Events des Messagebus über den Uplink gering sein, da erst die Autoupdate-Services die Events an die Clients verteilen. Gleichzeitig stellen diese jedoch viele Anfragen an die Eventstore-Views, sodass die Views (die selbst wiederum wenig Daten aus dem Messagebus und der dem Eventstore zugrunde liegender Datenbank benötigen) eine Art Vor-Ort-Cache bilden.

9) JSON-Serialisierung einer Veranstaltung

Veranstaltungen können exportiert werden indem alle Modelle (inkl. der Veranstaltung selbst und alle Nutzer) in ein JSON-Objekt gepackt werden. Diese Veranstaltung lassen sich in ein Gremium importieren. Beim Importieren muss auf das Verwalten der Nutzer geachtet werden. In OpenSlides 3 wird eine Option zum Extrahieren der Daten in genau diesem Format angeboten.

10) Mediafiles

Die Mediafiles werden analog zu OS3 in Postgresql gespeichert. Damit ein Mediafile sofort abrufbar ist, wird es (optimistisch) in Postgresql gespeichert bevor es im Eventstore angelegt wird. Scheitert das Erstellen im Eventstore, wird die Datei wieder gelöscht.

Ein Mediafile-Service kümmert sich um die Auslieferung der Mediafiles: Über eine einzige Route /media/<assembly_id>/<requested-path-to-mediafile> wird das angefragte Mediafile ausgeliefert, falls es existiert und Zugriff gewährt ist. Dieser Service sammelt die benötigten Daten (Nutzer authentisieren, Pfad zur Mediafile, die Datei selbst, ist sie ein Logo/Schriftart/projiziert, Berechtigungen?), cached diese pro Mediafile und hört auf Events, die eines dieser Attribute ändern. Die Struktur ist soweit ähnlich zu der in OS3, jedoch kann der Service Erstell-Requests entgegennehmen. Damit die Mediafiles zu jedem Zeitpunkt der Historie valide sind, dürfen die Mediafiles nicht gelöscht werden. Für allgemeine Mediafiles (z.B. Logo der Organization) ist assembly_id 0.

11) Volltextsuche

Da der Client nicht mehr alle Daten hat, muss eine serverseitige Volltextsuche implementiert werden.

Es kann nicht einfach die Full-Data-View durchsucht werden, da möglicherweise nach Inhalten gesucht wird, die der Nutzer durch seine Berechtigungen nicht sehen darf.

TODO: 3 Vorschläge:

  1. Da Volltextsuchen möglicherweise (spekuliert; das kann sich durch OS4 auch ändern...) selten sind, können die Daten einer Veranstaltung kopiert und vollständig beschnitten werden. Dann kann in diesen Daten gesucht werden. Dies ist nicht performant, da die Daten nicht indiziert sind, sowie hierbei die Volltextsuche Veranstaltungsspezifisch ist. Auch ist es eine wage Prämisse, dass die Suche wenig verwendet wird.
  2. Für einen Nutzer könnte man eine Query erstellen, die zunächst die Beschneidungen in der DB "simuliert" werden. Dabei ist die Abfrage nicht auf eine Veranstaltung beschränkt. Das muss getestet werden (auch in Kombination mit dem 3. Vorschlag)
  3. Die Full-Data-View kann modifiziert in eine Suchengine (z.B. Elasticsearch) geklont werden, sodass nur Texte (oder "wichtige Informationen"; TODO) indiziert werden und Abfragen nur spezifisch stattfinden. Damit ist gemeint, dass beispielsweise immer Antragstexte, aber keine Kommentare durchsucht werden, sodass die Anfragen nicht mehr (oder nur wenig) nutzerspezifisch sind. Nutzerspezifische Aspekte können durch geeignete Abfragen oder nachträglich gefiltert werden.

12) Presenter

Werden mehr Daten benötigt als in der fokussierten View des Clients, stellt ein Datenservice Abfragemöglichkeiten für Clients bereit. Diese können genutzt werden, falls einmalig Daten benötigt werden, z.B. zum Generieren von Antragslisten als PDF. Das Anfrageformat ist das selbe wie zum Autoupdateservice, jedoch ist dies nicht an Subscriptions gebunden. Die Daten werden einmalig berechnet (auch zu einer gegebenen Position) und dem Client übertragen. Dieser Dienst bietet neben einer Nutzerauthentifizierung eine Schnittstelle zum Restricter-Service.

Ein weiterer Aspekt ist der explizite Offlinemodus, für den der Datenservice die Daten einer Veranstaltung zusammenstellen kann und dem Client inklusive der aktuellen Position zurückschicken kann. Auch muss eine View angeboten werden, die nur die aktuelle Position zurückgibt, sodass der Client abfragen kann, wie weit die Events vergangen sind (vllt. auch zeitlich).

13) Projektor

Ein Projektorservice übernimmt das Erstellen von Projektordaten. Die Projektorelemente werden gezwungen genau auf ein content-objekt zu verweisen, d.h. jede Folie ist an ein Element gekoppelt, sodass die element_id verpflichtend ist. Alle weiteren Werte der Projektorelemente sind optional. Jede Folie für die Collection spezifiziert (deklarativ) Abhängigkeiten zu benötigten Objekten. Der Projektorservice kann nun für alle aktuellen Folien auf den Eventstram hören und falls Änderungen eines Objektes selbst oder einer Anhängigkeit festgestellt werden, werden die Projektordaten neu berechnet. Die Projektordaten werden über den Messagebus versendet.

Der Autoupdateservice hört auf dieses Topic des Messagebus und leitet die Nachrichten einfach an die Clients weiter. Dabei kann jeder Client (analog zu OS3) spezifizieren, welche Projektoren er gerne empfangen würde. Dies kann durch ein Array an Projektorids realisiert werden.

14) Authservice

Der Authservice authentifiziert Benutzer und verwaltet die Sitzungen der Benutzer. Er ist über verschiedene Routen erreichbar, die in interne und externe Routen aufgeteilt sind. Zur Kommunikation zwischen verteilten Services wird eine Kombination aus Cookies und Tokens verwendet, um eine erfolgreiche Nutzerauthentifizierung zu bestätigen. Dadurch ist die gesamte Anwendung resistent gegen XSS- und CSRF-Angriffe. Nach einer erfolgreichen Authentifizierung erhält der Client eines Benutzers ein Cookie, das eine SessionId speichert, und ein Token, das sowohl die SessionId als auch die UserId des Benutzers speichert. Jeder Benutzer ist durch eine eindeutige UserId erkennbar. Des Weiteren wird jede Sitzung (Session) eines Benutzers durch eine eindeutige SessionId zugeordnet. Der Authservice cached persistent die Zuordnung von Benutzern und SessionIds. Unter einer internen Route (/authenticate/) kann der Authservice angesprochen werden, um Anfragen von Benutzern zu verifizieren. Dabei erwartet der AuthService ausschließlich den HTTP-Header "Authentication" und ein Cookie "refreshId". Der AuthService kann durch andere Authenticationbackends, in Form von Bibliotheken, ersetzt werden, welche die internen Routen des AuthServices bereitstellen. Der Authservice verfügt über eine interne Schnittstelle, die SessionIds jedes Nutzers abzufragen und eine Session (gegeben durch eine SessionId) zu beenden. Für jedes Logout soll ein Event veröffentlicht werden. Zur Authentifizierung wird zurzeit eine Benutzername-Passwort-Kombination verwendet. Dabei werden sämtliche Daten eines Benutzers, das heißt auch Benutzername und Passwort, vom Datastore-Service persistiert.

Mithilfe einer Bibliothek werden JSONWebToken (JWT) verwendet. Signierte JWT werden dabei als Token und Cookie verwendet. Das heißt, sie liegen als String vor und werden mit dem Präfix "Bearer " versehen, um zu kennzeichnen, dass sie vom AuthService ausgestellt wurden. Zur Vereinheitlichung sind sowohl Cookies als auch Tokens als Header in einer Antwort enthalten. Cookies sind dabei unter der Kennung "refreshId" zu lesen, Tokens werden unter dem Header "Authentication" abgelegt. Als Schutzmaßnahme vor XSS-Angriffe werden solche Cookies mit der Eigenschaft "httpOnly" erstellt, wodurch sie nur serverseitig gelesen werden können. JWT im Authentication-Header sind nur 10 Minuten lang gültig. Warum welche Entscheidungen getroffen wurden und weitere Informationen können in folgendem Dokument gelesen werden: Implementierung eines Authentifizierungsdienstes

Im Nachfolgenden sind die internen und externen Routen aufgelistet und beschrieben, was sie bewirken:

Externe Routen

  • login: Authentifiziert Benutzer durch korrekte Zuordnung von Benutzername und Passwort. Bei erfolgreicher Authentifizierung wird ein Cookie und ein Token ausgestellt.
  • secure/logout: Benutzer melden sich hiermit wieder ab, dabei wird die zugehörige Session (und damit das Cookie, das die SessionId innehielt) gelöscht. Ohne das Cookie ist das zugehörige Token nicht mehr gültig.
  • who-am-i: Gibt Informationen über eine aktuelle Session eines Benutzers zurück. Dabei wird ein neues Token ausgestellt und als Antwort zurückgegeben.
  • secure/list-sessions: Gibt eine Liste mit zurzeit aktiven Sessions zurück.
  • secure/clear-session-by-id: Erwartet die SessionId einer spezifischen Session. Diese wird vom AuthService gelöscht.
  • secure/clear-all-sessions-except-themselves: Löscht sämtliche Sessions eines Benutzers, außer die, die die Anfrage stellt.

Interne Routen

  • authenticate: Verifiziert JWT, die sowohl als refreshId als auch im Authentication-Header abgelegt sind. Das heißt, die Signaturen und Gültigkeit beider JWT wird geprüft. Ist ein JWT im Authentication-Header abgelaufen, wird ein neues ausgestellt und als Antwort zurückgegeben.
  • hash: Erwartet einen zu hashenden String. Der String wird mit dem Sha512-Algorithmus gehashed.
  • is-equals: Erwartet einen zu hashenden String sowie einen Hash-Wert als Vergleich. Der AuthService hashed den String und prüft, ob der Hash-Wert mit dem Vergleichswert übereinstimmt.

Interne Routen sind nicht vom Client erreichbar. Des Weiteren können Routen mit dem Präfix "secure" nur mit einer gültigen Kombination aus refreshId und Authentication-Header angefragt werden. Andernfalls wird ein 403-Fehler zurückgegeben.

Noch nicht implementiert

Ein AuthCenterService (abgekoppelt vom Authservice für die Flexibilität wechselbarer Authenticationbackends) protokolliert alle Logins und Logouts jedes Nutzers, indem die Events in eine Datenbank gespeichert werden. Zusätzlich wird ein timestamp und der User-Agent des Nutzers gespeichert. Dem Client werden Routen bereitgestellt den Loginverlauf des Nutzers abzufragen. Zudem können alle aktiven Sessions abgefragt werden, welche durch Timestamp und User-Agent charakterisiert werden und dem Client mit einem eindeutigen Handle versehen werden (z.B. md5 der Session-Id). Ein Client kann nun ausgewählte Sessions (gegeben durch das Handle) beenden. Der AuthCenterService loggt diese Session über den Authservice anhand der Session-Id aus. Für diese Aktionen sind die beiden internen Routen des Authservices vorgesehen.

15) Servereinstellungen

Eine globale settings.py wird es in dieser Form nicht geben, da der Aufwand diese Datei für alle Services synchron zu halten (in Docker) schwierig ist. Ein Einstellungs-Dienst (Singleton?) stellt eine Route zum Abfragen von Settings zur Verfügung. Änderungen werden über den Messagebus verteilt. Live-Änderungen werden somit möglich, werden dem Client jedoch nicht verfügbar gemacht. Ein Script kann im Docker-context ausgeführt werden, welches eine bestimmte Nachricht in den Messagebus injiziert, auf die der Einstellungs-Dienst hört.

16) API-Gateway

Um viele Anfragen des Clients zu bündeln kann ein API-Gateway benutzt werden. Dieses wird zunächst nicht weiter ausgeführt und erst implementiert, wenn die Client-Server-Kommunikation "zu gesprächig" ist. Dieses Gateway kann für HTTP-Requests, aber auch für die bidirektionale Kommunikation vor dem Autoupdateservice verwendet werden, falls die Kommunikation dort aus zu vielen kleinen (logisch gekoppelten) Nachrichten besteht. Dies soll nur als Gedankenstütze dienen.

17) Inter-Client-Kommunikation (ICC)

Dieser Dienst behandlet "Notify" aus OS3. Um eine Namenstrennung zum Notifikation Service zu bekommen, ist dieses Feature nach Inter-Client-Kommunikation (ICC) umbenannt worden. Dieser Service hört auf alle Client-Nachrichten, die der Autoupdateservice einfach in den Messagebus weitergibt. Dann wird die Nachricht geprüft, Berechtigungen geprüft, die Menge an betreffenden Nutzern ermittelt und passend in den Messagebus geschrieben. ICC-Nachrichten sind Veranstaltungsgebunden. Der Autoupdateservice gibt eine ICC-Nachricht an die Clients weiter, wenn die aktive Veranstaltung und der Nutzer stimmt.

18) Logging

Docker erlaubt eine einheitliche und zentrale Sammlung von Stdout/Stderr jeder Container. Ein ELK-Stack wird zur Visualisierung, Speicherung und Auswertung aller Logs verwendet. Dabei sollte auf ein ausreichendes, aber nicht zu gesprächiges/unnötiges Logging geachtet werden. Dazu zählen:

  • Logging von Konfigurationen beim Startup (einmalige Logs). Diese dienen v.a. zur Verifikation der richtigen Einstellungen und werden nicht visualisiert.
  • Loggen von externen Requests inkl. der Authentifizierung.
  • Loggen von Security-Events, z.B. Login eines Nutzers (Nuter-Id) mit der internen Session-Id, Logout.
  • Loggen zum Erstellen von Performance-Metriken. Dies sollte hauptsächlich vom Messagebus geschehen. Auch Dienste sollen Statistiken über Dauern wichtiger und zentraler Vorgänge loggen.
  • Loggen zum Erstellung von Nutzungs-Metriken: Anzahl an verbundenen Clients, eingeloggten Nutzern, Sessionlänge, Projektoren, Schreibe-Requests aufgeteilt nach Veranstaltung/Gremium/Collection...
  • Loggen von Fehlerfällen aller Art (siehe 18).

TODO: Einheitliches Loggingformat. Dazu sollte geklärt werden, welche Meta-Informationen Docker den Zeilen mitgibt (Node, Container, Service, ...).

19) Einheitliche Namensgebung

Eine konsistente Benennung aller Konzepte und deren Eigenschaften ist ein wichtiger Bestandteil eines einheitliches Gesamtsystems. An folgende Namen sollten sich alle Dienste halten:

  • datastore: Die Datenspeicher und -Versionierungseinheit (Konzept: Eventsourcing). Implementiert als Eventstore. Dazu folgende weitere Begriffe:
    • writer: Schreib-Part des Datastores
    • database: In Bezug auf den Datastore die Speichereinheit
    • reader: Zugriffsdienst des Datastores zum Bereitstellen von Daten.
    • position: Generelle Eventposition. Eindeutig im ganzen System.
  • messagebus: Der Bus, über dem die Events verteilt werden. Dabei gibt es Producer und Consumer, sowie die Nachrichten in Topics gegliedert werden.
  • event: Bezieht sich auf Events, nicht auf Veranstaltungen
  • meeting: Eine Veranstaltung. Um Verwirrung zu vermeiden wird nicht "Event" genutzt.
  • committee: Ein Gremium. Weiterhin gibt es is_committee als Flag pro User.
  • organization: Die Organization der OS4-Instanz.
  • action: Eine definierte Aktion in der Domain-Logik. Eine schreib-View.
  • presenter: Eine lese-View.
  • configuration (config): Servereinstellungen (OS3: settings.py)
  • settings: Einstellungen im Client auf organizations-, Gremien- und Veranstaltungsebene. Betreffen alle Nutzer.
  • preferences: Nutzerspezifische Präferenzen (Einstellungen). Noch nicht vorgesehen, aber der Begriff ist reserviert. z.B. Speichern der persönlichen Filtereinstellungen, oder Dark-Mode.
  • group: Rechtegruppe auf Veranstaltnugsebene
  • role: Rechtegruppe (oder auch Rechterolle) auf organizationebene
  • collection: Modelltyp, z.B: motion, item. Elaubte Namen: [a-z]+ | [a-z][a-z_]*[a-z]. Also: Kein Unterstrich am Anfang oder Ende.
  • id: Eine numerische ID eines Modells. Erlaubte IDs: [1-9][0-9]*
  • field: Ein Feld eines Modells, z.B. username in der user collection. Erlaubte Namen: [a-z][a-z0-9_]*
  • fqid (full-qualified id): Zusammensetzung von collection und einer ID. Trenner ist ein Slash: <collection>/<id>
  • fqfield: (full-qualified field): Zusammensetzung von collection, ID und einem Field. Trenner ist ein Slash: <collection>/<id>/<field>
  • collectionField: Zusmmensetzung aus collection und field. Trenner ist ein Slash: <collection>/<field>
  • component: Logische (Software-)Komponente. Sollte als ein Modul abgekapselt sein. Auf Architekturebene sind Komponenten Größer zu fassen (z.B. Datastore) als in der Eigentlichen Implementierung (Executor des Writers des Eventstores). Komponenten haben per se nicht mit Services zutun.
  • service: Eine strukturelle Zusammensetzung mehrerer Komponenten, die als ein Programm ausgeliefert werden.

Namen, die nicht einfach so genutzt werden sollten:

  • database (im allgemeinen/losen Kontext): Ist in diesem Fall zu allgemein, da das System mehrere Datenbanken und -technologien verwendet.
  • webserver: Damit ist nicht klar, welcher dienst gemeint ist, ob eine Menge an Diensten gemeint ist oder der ganze Server.
  • collectionstring: collection ist besser und enthält redundanten string-Teil nicht.
  • change_id: Durch position ersetzt. Ist keine ID in Sinne des Eventsourcings.

20) Fehlerbehandlung

Mit der in diesem Kapitel angesprochenen Fehlerbehandlung ist die Behandlung und Weitergabe an Fehlern zwischen Services gemeint. Bei Fehlern soll (analog zu HTTP) zwischen Client- und Serverfehler unterschieden werden. Clientfehler können nur bei externen Requests (=Requests vom Client) auftreten. Diese werden dem Client gemeldet, sodass er diese behandlen kann. Diese Fehler sind Nutzerfehler (Client als Benutzer des Servers) und müssen nicht geloggt werden. Tritt ein Serverfehler (interner Fehler) auf, muss die aktuelle Operation abgebrochen werden und der Fehler geloggt werden. Mögliche Fehler müssen immer in der API mit spezifiziert werden. Wichtig ist eine konsequente Behandlung aller Fehler. Tritt ein Fehler auf, sollte der Code so gebaut sein, dass kein Recovery notwendig ist, d.h. es muss auf atomare Operationen geachtet werden. Vor allem in Bezug auf den Eventstore ist es nicht möglich, geschriebene Event zu entfernen. Man kann diese zwar Rückgängig machen, jedoch könnte das System in diesem Fall kurzzeitig in einem inkonsistenten Zustand sein. Serverfehler lassen sich grob in Kategorien teilen:

  1. Inter-Service Kommunikationsfehler ("Auf Docker/Netzwerk-Ebene"): Ein benötigter Dienst ist nicht erreichbar, oder der Timeout der Anfrage ist abgelaufen. Dies sollte zunächst geloggt werden, sodass Sysadmins diese Fehler in der Visualisierung sehen können. Weiterhin muss die aktuelle Operation abgebrochen werden.
  2. Programmierfehler ("Auf Service-Ebene"): Werden wie Inter-Service-Kommunikationsfehler behandelt.
  3. Logikfehler: Diese können möglicherweise behandlet werden. Falls nicht, kann ein solcher Fehler an den Client weitergegeben werden (bei einem externen Request; Propagation von Services zum initialen Service, sodass aus dem Serverfehler ein Clientfehler erstellt werden kann), oder dieser wird die 1) und 2) behandelt.

Durch den verteilten Aufbau werden wesentlich mehr Inter-Service Requests durchgeführt. Im Allgemeinen sollte auf ein eher geringes Timeout geachtet werden. Falls die Aktion idempotent war, kann mehrfach versucht werden, die Aktion druchzuführen. Bei nicht-idempotenten Aktionen ist es möglich, dass diese ausgeführt wurden, jedoch die Antwort nicht übermittelt wurde. Folglich darf diese Aktion nicht wiederholt werden.

21) Testing

Test Driven Development (TDD) wird für jeden Service verpflichtend (!) vorausgesetzt. Die drei Gesetze des TDD (nach Robert C. Martin):

  1. Erst Produktionscode schreiben, wenn ein scheiternder Unit-Test geschrieben wurde.
  2. Der Unit-Test darf nicht mehr Code enthalten, als für das Scheitern (und Kompilieren) des Testes notwendig ist.
  3. Nur soviel Produktionscode schreiben, wie für das Bestehen des scheiternden Tests erforderlich ist.

Für Unit-Tests sollte das Build-Check-Operate-Pattern eingehalten werden. Um gleichzeitig die Tests einfach zu halten, muss eine domänenspezifische Testsprache entwickelt werden. Für Unit-Tests sollte das F.I.R.S.T.-Prinzip eingehalten werden. Für jeden behobenen Fehler (im späteren Entwicklungszyklus) muss ein Regression-Test geschrieben werden.

22) Codestyle und Design innerhalb der Services

Die Smells und Heuristiken aus Clean Code werden als grundsätzliche Regeln für den Codestyle verwendet. Für jede Sprache müssen nicht behandelte (oder passende) Regeln geklärt, dokumentiert und in einem Code-Formatting-Tool implementiert werden!

Das Design eines Services muss die fünf Design-Prinzipien von SOLID einhalten. Dies wird durch das TDD begünstigt, da man durch die Tests gezwungen ist, eine überzeugende Abstraktion und De-Isolation im Quelltext zu haben.

Siehe Coding-Guidelines, um einen Überblick zu bekommen.

23) Migrationen

Zwei Vorschläge (TODO):

  1. Einführen einer schema_id für jedes Event. Da Events nicht geändert werden dürfen, werden bei einer Migration Events erstellt, die z.B. alte Keys löschen und die Daten zu neuen Keys eines Modells konvertieren (=Migrationslogik ausführen). Dabei muss das System offline genommen werden. Die aktuelle Version der Daten entspricht dem aktuellen Schema-Stand. Falls vergangene Daten abgerufen werden sollen, müssen die Schema-Stände der einzelnen Modelle Schritt für Schritt migriert werden, sodass alle Modelle im Anschluss auf dem aktuellen Stand sind.
  2. Eine Datenabstraktionsschicht wrappt die Views des Eventstores (oder pro View?). Es werden die Daten immer mit der aktuellen Version zum Lesen angeboten und zum Schreiben entgegengenommen. Intern (hinter der Schicht) können alte Datenstände verarbeitet werden, sodass die Schicht beim ausgeben der Daten mögliche Migrationen durchführt. Beim Schreiben wird der Datenbestand langsam migriert: Ist ein altes Schema in den Daten vorhanden wird es gelöscht und die neue Form geschrieben. Dieses Prinzip ermöglicht Schemaänderungen und Updates des Systems, ohne dass es offline genommen werden muss. Die Daten werden wieder versioniert, d.h. Services fragen Daten in einer bestimmten Version an. Nach einer Schemaänderung kann die alte und neue Version aus der Datenabstraktionsschicht gelesen werden. Im 2. Schritt werden alle Services auf das neue Schema umgestellt. Als drittes wird das alte Schema zum Lesen und Schreiben aus der Datenabstraktionsschicht entfernt.

Gibt es noch weitere Möglichkeiten?

24) Inter-Service-Kommunikation

Etwas Generelles vorweg: Einige Services müssen einen HTTP-Server implementieren, soweit sie eine Interaktion mit dem Client bieten. Dies sind v.a. die Services, zu denen im Schaubild ein Pfeil vom HaProxy ausgeht. Dementsprechend sollte HTTP die erste Wahl sein, da es gute HTTP-Frameworks (für den Webserver) und Request-Bibliotheken für alle Sprachen gibt. Bibliotheken, um Anfragen über andere Protokolle zu stellen, sollten sich in der Regel ebenfalls gut einbinden lassen. Bedacht sollte auf Services gelegt werden, die Schnittstellen für mehrere Protokolle implementieren: Falls die Selbe Schnittstelle auf mehreren Wegen angesprochen werden kann, muss für eine passende Abstraktion im Quelltext gesorgt werden. Wenn zwei Technologien verwendet werden, die jeweils einen Server (=Code der auf einen Socket binded) verwenden, muss geprüft werden, ob beide Server in einer Programminstanz laufen oder ebenfalls die Schnittstellen abstrahiert und zwei Versionen des Services gestartet werden muss. Dabei gibt es eine Version für Protokoll A, weine weitere für Protokoll B, usw.

III) Modellierungen

Organisation, Gremien, Veranstaltungen

ER-Diagramm

Schaubild

Die Organisation stellt die höchste Verwaltungsstruktur dar. Innerhalb dieser gibt es mehrere Gremien und in diesen Veranstaltungen.

Berechtigungen in der Organisationsstruktur

Rollen auf Organisationsebene sind analog zu Gruppen auf Veanstaltungsebene zu sehen. Jedoch kann ein Nutzer nur genau einer Rolle zugewiesen werden, da nur wenige Rollen erwartet werden. Folgende Rollenberechtigungen können einer Rolle zugewiesen werden:

  • can_manage_users: Darf Nutzer erstellen, temporär zu fest, bearbeiten, löschen, sperren (aktiv setzen), Rollen verwalten, Nutzer Rollen zuweisen,Passworte managen (zurücksetzen (auf das Default), ändern, Passwortwiederherstellung einleiten).
  • can_manage_committees: Darf alle Gremien sehen, erstellen, löschen und die Weiterleitungsstruktur bearbeiten.
  • can_manage_organization: Darf Einstellungen der Organisation ändern, z.B. Name, und die Ressourcen verwalten.
  • is_superadmin (Technisch: Nicht als Berechtigung gespeichert): Besitzt alle Rollenrechte und besitzt in allen Gremien und Veranstaltungen transitiv alle Rechte.

Nutzer zu Gremien zuzuweisen und Nutzer als Verwalter im Gremium ernennen können nur Nutzer mit can_manage_users und can_manage_organization. Es Gibt keine Abstufung Nutzer auf der Organisationebene sehen zu können.

In Gremien:

  • Mitglieder können Verwalter sein.
  • Superadmins auf Organisationsebene sind Verwalter.
  • Verwalter können Veranstaltungen verwalten: Erstellen, Löschen, Kopieren, Vorlage auswählen, Importieren, Exportieren, Eigenschaften der Veranstaltung ändern.
  • Mitglieder und Gäste sehen alle/ausgewählte Veranstaltungen.

In Veranstaltungen:

  • Rechte auf Basis der Gruppenzuweisung des Gremiums.
  • Superadmins auf Organisationsebene haben alle Rechte.
  • Gremienverwalter haben alle Rechte.
  • Es gibt eine Superadmingruppe.
  • Admins können Nutzer Gruppen zuweisen und Gruppenberechtigungen einstellen.
  • Nutzer bekommen neues Feld "Funktion", damit die Gruppen nur für die Rechtezuweisungen benutzt werden.

Weiterleitungen

Jedem Gremium können mehrere Gremien zugewiesen werden, an die Weitergeleitet werden kann. Zyklische Abhängigkeiten und Selbstzuweisungen sind dabei erlaubt.

Nutzerverwaltung

  • Gästegruppe als Default
  • Temporäre Nutzer: Admins in Veranstaltungen können temporäre Nutzer anlegen. Diese sind normale Nutzer, haben ein temporär-Flag, nur einen Nutzernamen, haben Gruppenzuweisungen, können sich einloggen (optional, wenn Password vergeben) und sind genau einer Veranstaltung zugeordnet. Nur diese Veranstaltung können temporäre Nutzer sehen. Admins können diese Nutzer löschen. In der zentralen Nutzerverwaltung können temporäre Nutzer in "echte" Nutzer umgewandelt werden. Dabei muss es möglich sein, mehrere temporäre Nutzer mehrerer Veranstaltungen zu einem Nutzer zusammenzufassen, sodass alle Nutzerreferenzen angepasst werden müssen.

URLs

Eine Übersicht über das URL-Schema im Server:

  • org.os.com/system/ ist die einzige URL, die vom HaProxy zu den internen Services geroutet wird
  • Alle anderen Requests werden an den Client-Service (=Nginx o.ä.) weitergeleitet, der die Single-Page-Application ausliefert
  • /system/<service>/ Gibt an, zu welchem Service geroutet wird. Z.B. /system/action/, /system/presenter/ oder /system/autoupdate/
  • Unter /system/ liegen nur öffentlich zugängliche Endpunkte.
  • Unter /internal/<service>/ liegen interne Endpunkte.
  • Jeder Service kann daraufhin die URL weiter strukturieren. Z.B.:
    • /internal/datastore/writer/write
    • /internal/datastore/reader/filter
    • /system/action/handle_request
    • /system/autoupdate/get_data
    • /system/auth/login
    • /internal/auth/check_token
  • URLs von Endpunkten haben keinen trailing-slash
  • <part>.org.os.com wird vom HaProxy nach org.os.com/part/ umgeschrieben. Der Client modifiziert die URL nicht, behandelt es jedoch gleich

Für jeden Service:

  • /system/<servicename>/ und /internal/<servicename>/ sind für die eigentliche Schnittstelle vorgesehen
  • /health ist ein Dummy-Endpunkt, der einen 200-OK zurückgeben soll. Spezifikation folgt später
  • /discovery ist ein Dummy-Endpunkt. Spezifikation folgt später

Routing im Client:

  • org.os.com/: Automagischer Dispatch zum Dashboard oder der aktuellen Veranstaltung
  • Es gibt reservierte "Haupturls": org.os.com/dashboard/, org.os.com/users/, org.os.com/login/, ...
  • Unter org.os.com/<meeting_id | meeting_identificator>/ wird ein Meeting aufgerufen (=Catch-All für org.os.com/*/)

Default ports

  • HaProxy 9000 (intern), z.B. 8000 (extern)
  • Client (Nginx) 9001
  • Action 9002
  • Presenter 9003
  • Authentication 9004
  • Permission 9005
  • Mediafile 9006
  • ICC 9007
  • Configuration 9008
  • Search 9009
  • Datastore Reader 9010
  • Datastore Writer 9011
  • Autoupdate 9012
  • Vote 9013
  • Vote-decrypt 9014
  • WSProxy 9015

Anonymous

Der Anonymous kann pro Veranstaltung ein- und ausgeschaltet werden. Das Gremien/Veranstaltungs-Dashboard ist für anonymous nicht sichtbar. Ein Nutzer muss die URL der Veranstaltung kennen, um diese zu öffnen. Der Anonymous wird als Nutzer mit Id 0 (analog zu OS3) behandelt, ist (implizit) in der Gäste-Gruppe und hat den Rollenlevel 0.

OS3 Config-Variablen

Da es nun Modelle für die Organisation, Gremien und Veranstaltungen gibt, können alle Config-Variablen aus OS3, die diesen zugeordnet sind, als Modelleigenschaften behandelt werden. Es gibt kein eigenes Config-Modell mehr. Logos und Schriftarten werden als Ressourcen nun anders verwaltet.

Aufteilung der OS3-Config-Werte: Für die Organisation (Aus Allgemeineinstellungen):

  • Organisationsname
  • Impressum
  • Datenschutz
  • Login-Text
  • Theme

Nutzerverwaltung (Ebenfalls unter dem Config-Modell gespeichert):

  • Alles von PDF-Export und Email

Für jedes Gremium

  • Gremienname

Für jede Veranstaltung

  • Teilnehmende: Allgemein
  • Rest von Allgemein:
    • Veranstaltungstitel
    • Veranstaltungsbeschreibung
    • Veranstaltungsort
    • Dauer
    • Startseitentitel
    • Startseitentext
    • Anonyme Nutzer zulassen (!)
    • Export-Sektion
  • Anträge, Wahlen
  • Benutzerdefinierte Übersetzungen

IV) Client

  • Gremien-Dashboard
  • Ausgelagerte Nutzerverwaltung
  • Verwaltung des Dashboards/der Organization (Logos, Schriftarten, ...)
  • Umbau des Repository-Systems
  • erweiterte Fehlerbehandlung: Abgebrochene Requests, da Dienste nicht verfügbar sind, ...
⚠️ **GitHub.com Fallback** ⚠️