Artikel

Die Admin-API wird eigenständig

Wenn der Bot abstürzt, soll man trotzdem nachsehen und steuern können. Warum ich die Verwaltung vom laufenden Discord-Client entkoppelt habe — und wie Befehle an einen abwesenden Bot funktionieren.

BlackZackBlackzack

944 Wörter · 5 Min. Lesezeit

  • yurna
  • architektur
  • api

Mein System hatte einen Konstruktionsfehler, der mir vier Jahre lang egal war und der genau dann zum Problem wurde, als es darauf ankam: Alles hing am Bot.

Wenn der Bot lief, konnte ich alles. Wenn er nicht lief, konnte ich fast nichts — nicht nachsehen, wie der Zustand ist, nicht Einstellungen ändern, nicht einmal herausfinden, warum er nicht läuft, ohne mich auf den Server zu verbinden.

Die Idee

Eine eigene Anwendung, die nur eines tut: Verwaltung. Sie liest die Datenbank direkt, ist unabhängig vom Bot-Prozess und läuft auch dann, wenn sonst alles liegt.

                    ┌──────────────┐
   Dashboard  ────► │              │
                    │  Datenbank   │
   Admin-API  ────► │              │
                    └──────▲───────┘
                           │
   Bot ────────────────────┘

Drei Prozesse, keiner davon spricht direkt mit einem anderen. Die Datenbank ist der einzige gemeinsame Punkt. Das klingt nach einer Einschränkung und ist die Stärke des Aufbaus: Es gibt keine Reihenfolge beim Starten, keine Abhängigkeit beim Ausfall und keine Verbindung, die abreißen kann.

Wie man einem abwesenden Bot etwas sagt

Die naheliegende Frage: Wenn die Verwaltung den Bot nicht anspricht, wie startet man ihn dann neu oder lässt ihn eine Ankündigung schicken?

Die Antwort ist eine Warteschlange in der Datenbank.

CREATE TABLE bot_befehle (
  id          INTEGER PRIMARY KEY,
  aktion      VARCHAR(40) NOT NULL,   -- restart, resync-commands, broadcast, ...
  nutzlast    TEXT NULL,              -- JSON mit Zusatzangaben
  status      VARCHAR(20) NOT NULL DEFAULT 'offen',
  erstellt_am DATETIME NOT NULL,
  erledigt_am DATETIME NULL,
  ergebnis    TEXT NULL
);

Die Verwaltung legt einen Eintrag an und antwortet mit „angenommen" — nicht mit „erledigt". Das ist ein wichtiger Unterschied, und er zeigt sich im Statuscode: 202 statt 200. Der Bot holt sich in kurzen Abständen offene Einträge, führt sie aus und schreibt das Ergebnis zurück.

Der Gewinn: Ein Befehl an einen abgestürzten Bot geht nicht verloren. Er wird ausgeführt, sobald der Bot wieder da ist. Und wenn er nicht wiederkommt, sieht man in der Warteschlange, dass etwas offen ist.

Der Preis: Nichts passiert sofort. Wer den Zustand wissen will, muss nachfragen. Die Verwaltung zeigt deshalb bei jeder Aktion an, dass sie eingereiht ist, und aktualisiert den Status, wenn er sich ändert.

Woher man weiß, ob der Bot lebt

Über eine einzelne Zeile in der Datenbank, die der Bot regelmäßig aktualisiert:

CREATE TABLE bot_status (
  id             INTEGER PRIMARY KEY CHECK (id = 1),
  zuletzt_gesehen DATETIME NOT NULL,
  gestartet_am    DATETIME NOT NULL,
  version         VARCHAR(20) NOT NULL,
  gilden          INT NOT NULL,
  angemeldet      BOOLEAN NOT NULL
);

Die Verwaltung liest diese Zeile und entscheidet nach dem Alter des Zeitstempels: jünger als eine Minute heißt gesund, älter als fünf Minuten heißt offline.

Wichtig ist das Feld angemeldet. Ein Bot-Prozess kann laufen, ohne mit Discord verbunden zu sein — genau dann ist ein reines Lebenszeichen irreführend. Der Zeitstempel wird deshalb nur geschrieben, wenn die Verbindung tatsächlich steht.

Infobox

Die Einschränkung CHECK (id = 1) ist ein kleiner Kniff, den ich seitdem öfter benutze: Sie erzwingt, dass es genau eine Zeile geben kann. Ohne sie legt irgendwann jemand — oder ein fehlerhafter Codepfad — eine zweite an, und dann gibt es zwei Wahrheiten über denselben Zustand.

Die Authentifizierung

Die Verwaltungsschnittstelle ist mächtig: Sie kann Einstellungen ändern, Befehle absetzen, Daten einsehen. Sie ist deshalb bewusst schlicht abgesichert und nicht öffentlich erreichbar.

  • Ein Bearer-Token, mindestens sechzehn Zeichen, aus einer Umgebungsvariablen.
  • Zeitkonstanter Vergleich, damit die Antwortzeit nichts über den richtigen Wert verrät.
  • Fail-closed: Ist kein Token konfiguriert, startet die Anwendung nicht. Es gibt keinen Zustand „ohne Token läuft es offen".
  • Optional ein zweiter Token nur für lesende Zugriffe, für Überwachung und Anzeigen.
  • Sie lauscht nur lokal. Erreichbar ist sie über den Proxy, unter einem eigenen Pfad, oder gar nicht.

Der dritte Punkt ist der, den ich am wichtigsten finde. Es gibt viele Systeme, die ohne Konfiguration in einem offenen Zustand starten, „damit man erst mal loslegen kann". Genau daraus entstehen die Fälle, in denen jemand eine Verwaltungsschnittstelle ungeschützt im Internet stehen hat.

Was die Warteschlange sonst noch gelöst hat

Die Befehlswarteschlange war als Notlösung gedacht, weil es keine direkte Verbindung gibt. Sie hat sich als besser erwiesen als eine direkte Verbindung, und zwar aus Gründen, die ich vorher nicht bedacht hatte.

Es gibt eine Historie. Jeder Befehl, der jemals abgesetzt wurde, steht mit Zeitpunkt, Auslöser und Ergebnis in einer Tabelle. Bei einer direkten Verbindung wäre das ein zusätzlicher Protokolleintrag, den man vergessen kann.

Befehle sind nachvollziehbar wiederholbar. Wenn etwas schiefgeht, sieht man, was angefordert wurde, und kann es erneut einreihen.

Die Reihenfolge ist definiert. Zwei Befehle, die gleichzeitig abgesetzt werden, laufen nacheinander. Bei direkten Aufrufen wäre das ein Nebenläufigkeitsproblem.

Es gibt einen Zustand „steht aus". Der wichtigste Punkt für die Bedienung: Wer einen Neustart auslöst, sieht, dass er eingereiht ist, und danach, dass er ausgeführt wurde. Bei einem direkten Aufruf gibt es nur vorher und nachher.

Die Grenze dieses Musters

Damit es nicht wie eine Universallösung klingt: Eine Warteschlange über eine Datenbank hat eine deutliche Obergrenze.

Sie taugt für seltene, wichtige Vorgänge — Verwaltungsbefehle, Neustarts, Ankündigungen. Sie taugt nicht für alles, was häufig ist: Jeder Abruf ist eine Abfrage, und bei kurzen Abständen erzeugt das dauerhafte Last für den seltenen Fall, dass etwas anliegt.

Bei mir liegt das Abrufintervall bei wenigen Sekunden, und die Abfrage ist so gebaut, dass sie im Normalfall keine Zeile findet und praktisch nichts kostet.

Wer hunderte Vorgänge pro Sekunde hat, braucht etwas anderes. Wer ein paar pro Tag hat — und das ist bei Verwaltungsaufgaben der Normalfall —, bekommt hier eine Lösung ohne zusätzlichen Dienst.

Was das im Alltag verändert hat

Ich kann nachsehen, wenn etwas kaputt ist. Das klingt trivial und war der ganze Punkt. Vorher hieß Fehlersuche: auf den Server verbinden, Protokolle lesen. Jetzt sehe ich in einer Übersicht, ob der Bot angemeldet ist, wann er zuletzt gelebt hat, was in der Warteschlange steht.

Ein Build-Fehler im Dashboard nimmt mir nicht die Verwaltung. Genau das ist später mehrfach eingetreten — und war jedes Mal ein Ärgernis statt eines Notfalls.

Eine mobile App wurde möglich. Weil es eine saubere Schnittstelle gibt, die nichts mit der Weboberfläche zu tun hat. Das war zu diesem Zeitpunkt nicht geplant und kam wenige Monate später.

Diese dritte Wirkung ist die, die ich mir merke: Eine klare Grenze schafft Möglichkeiten, an die man beim Ziehen der Grenze nicht gedacht hat.