Archiv für den Monat: November 2016

Mehrere Rückgabewerte eines HTTP-Requests

Manchmal ist es notwendig, dass ein Webservice mehrere Ergebnisse nacheinander zurückliefert. Zum Beispiel, wenn ein Request abgesetzt wird, der zunächst validiert werden muss, bevor tatsächlich eine Prozessierung beginnt und die Validierung selbst eine längere Zeit dauert. Dann soll der Server nach Möglichkeit den Request direkt beenden und die Validierung im Hintergrund ablaufen. Andernfalls ist die Webseite nicht responsiv, was den Nutzer verunsichern könnte und keine gute User Experience ist. In einem Spring Boot basierten Webservice kann das zum Beispiel mit asynchroner Ausführung und long polling zusammen mit einem Deferred Result erreicht werden, wie im folgenden beschrieben.

Das Szenario

Im folgenden soll ein kleiner Webservice in Spring Boot implementiert werden, der eine Anfrage auslöst, die zwei Stadien durchläuft. Direkt bei der Anfrage wird in einen Zustand INITIALIZING gewechselt, bis nach einigen Sekunden der Zustand PHASE1 erreicht wird. Den Abschluss markiert der Zustand DONE. Die zugehörige Webseite soll jeweils den aktuellen Status anzeigen und bei einem Statuswechsel automatisch aktualisiert werden.

Eine Beispielimplementation des Webservers habe ich auf GitHub hochgeladen. Es kann nach dem Auschecken mit mvn spring-boot:run gestartet werden, der Webservice ist unter http://localhost:8080 erreichbar.

Beispiel-Service für asynchrone Ausführung und Long-Polling

Beispiel-Service für asynchrone Ausführung und Long-Polling

Bei einem Klick auf die Schaltfläche geht es los.

Die serverseitige Implementierung

Zu Beginn wird ein POST-Request über an /request geschickt. Die Methode soll sofort ein Ergebnis zurückliefern, auch wenn die auszuführende Aktion länger dauert. Dazu wird nicht der tatsächliche Ergebnistyp zurückgeliefert sondern ein DeferredResult. Die Webseite kann daraufhin direkt aktualisiert werden, da die Anfrage schnell bearbeitet wird, das tatsächliche Ergebnis ist im Ergebnis erst vorhanden, sobald setResult aufgerufen wird.

@RequestMapping(name = "/request", method = RequestMethod.POST)
@ResponseBody
public DeferredResult request() {
    // Prepare already for the first state change
    DeferredResult result = service.getStatus();
 
    // Actually let the asynchronous service do something
    service.doSomething();
 
    // Return the deferred result that will be set in the above asynchronous call
    return result;
}
Die Anfrage wurde abgesetzt.

Die Anfrage wurde abgesetzt.

Beim Long Polling kann der Webserver verspätet antworten. Typischerweise wird ein Timeout gesetzt, so dass bei einem Verbindungsabbruch eine neue Anfrage gestartet werden kann. Da unklar ist, wie lange die Antwort tatsächlich dauert, sollte mit dem Verbindungsabbruch gerechnet werden. Die Statusabfrage für das Long Polling wird über den Endpunkt /poll durchgeführt. Das Ergebnis wird vom ausführenden Service geholt und zurückgeliefert. Das zurückgelieferte Object vom Typ DeferredResult wird automatisch so lange zurückgehalten, bis der Service über einen Aufruf von setStatus() ein Ergebnis bereitstellt.

@RequestMapping(name = "/poll", method = RequestMethod.GET)
@ResponseBody
public DeferredResult requestStatus() {
    // Simply forward the request to the service
    return service.getStatus();
}

Asynchrone Ausführung der Anfrage

Damit der ganze Prozess funktioniert, muss die Arbeit des Services (service.doSomething() im Beispiel oben) asynchron ablaufen. Das heißt, nach dem Aufruf von doSomething kann direkt etwas zurückgeliefert werden. Spring Boot ermöglicht durch die Annotation @Async, dass eine Methode eines Services automatisch in einem anderen Thread asynchron ausgeführt wird. Die folgende Methode simuliert, dass der Status zunächst auf INITIALIZING gesetzt wird, anschließend eine längere Zeit gearbeitet wird und zuletzt beendet wird sobald der Status auf DONE steht.

@Async
void doSomething() {
    // Initialize the current status and notify any listener that is already present
    currentStatus = LongRequestStatus.INITIALIZING;
    publishStatus(currentStatus);
 
    // Perform some work until the final state is reached.
    do {
        currentStatus = doNecessaryWork(currentStatus);
        publishStatus(currentStatus);
    } while (currentStatus != LongRequestStatus.DONE);
}

Die Methode wird bei einem POST-Request an /request aufgerufen. Den Status der parallel ausgeführten Methode wird mit publishStatus gesetzt. Falls es in der Zwischenzeit eine eventuell vorhandene Long Polling-Anfrage (mit einem Status als DeferredResult) gab, wird der Status gesetzt.

private synchronized void publishStatus(LongRequestStatus nextToReturn) {
    if (waitingResponse != null) {
        waitingResponse.setResult(nextToReturn.toString());
    }
    waitingResponse = null;
}
Die Anfrage wird gerade Bearbeitet.

Die Anfrage wird gerade Bearbeitet.

Eine Polling-Anfrage ruft ausgehend vom Controller die Methode getStatus auf. Diese erzeugt das DeferredResult waitingResponse, das direkt beantwortet wird, wenn bereits Informationen da sind. Andernfalls wird die Anfrage gespeichert und das Ergebnis gegebenenfalls in obiger Methode publishStatus gesetzt.

public synchronized DeferredResult getStatus() {
    waitingResponse = new DeferredResult<>();
 
    // If the request completed, immediately return the status because it will never change
    if (currentStatus == LongRequestStatus.DONE) {
        waitingResponse.setResult(currentStatus.toString());
    }
 
return waitingResponse;

Die Webseite

Auf der Clientseite wird das Statusupdate mit JavaScript durchgeführt. Bei einem Click auf die Schaltfläche wird zunächst wird die Anfrage ausgeführt und bei Erfolg die Long Polling-Abfrage gestartet.

function request() {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/request", true);
    xhr.onreadystatechange = function() {
        if(xhr.readyState === 4 && xhr.status === 200) {
            // Do something with the response
            console.log(xhr.responseText);
 
            // Start long polling and get the next status
            poll();
        }
    }
    xhr.send();
}

Die Methode poll sendet die Anfrage an den Server um den aktuellen Status abzuholen. Die Anfrage wird solange erneut ausgeführt, bis das Endergebnis DONE zurückgeliefert wird. Als Timeout ist eine Sekunde gesetzt, was relativ kurz ist und dafür sorgt, dass jede Sekunde erneut beim Server nachgefragt wird.

function poll() {
    var xhr = new XMLHttpRequest();
 
    xhr.open("GET", "/poll", true);
 
    xhr.onreadystatechange = function() {
        if(xhr.readyState == 4 && xhr.status == 200) {
            if (xhr.responseText !== "DONE") {
                // Start the next polling request
                setTimeout(poll, 1000);
            } else {
                // Stop polling
            }
        }
    }
    xhr.send();
}
Die Anfrage wurde komplett bearbeitet.

Die Anfrage wurde komplett bearbeitet.

Was fehlt?

Das Grundgerüst für mehrere konsekutive Ergebnisse, die auf eine Anfrage zurückgegeben werden sollen steht damit. Es schließen sich nun einige Möglichkeiten der Erweiterung an.

Da Server normalerweise mehrere Nutzer haben, sollte es natürlich möglich sein, dass mehrere Anfragen parallel ablaufen. Dazu müssen für alle diese Anfragen die Stati gespeichert werden. Der erste Request kann dann ein Token (zum Beispiel eine UUID) zurückgeben, mit der anschließend beim Polling der Status der passenden Anfrage abgefragt werden kann. Gegebenenfalls muss hier natürlich noch mit Authentifizierung gearbeitet werden, so dass jeder Nutzer nur seine eigenen Requests anfragen kann.

Geschrieben von Kap. Zuletzt geändert am 16. Februar 2019.

Von Videokodierung und Bitraten

Ich bin nun in den „Genuss“ gekommen einige Filme zu kompriemieren und als DVD zu brennen. Bei einigen der Filme gab es verschiedene Probleme…

Typische Programme wie DVD-Flick oder DeVeDe machen das mit wenigen klicks. DeVeDe nutzt jedoch den verfügbaren Platz weniger als zur Hälfte aus und die Version von ffmpeg DVD-Flick kam mit dem (neuen) Inputformat nicht klar.

So habe ich habe kurzerhand beschlossen, selbst die passende Ziel-Bitrate zu berechnen. Eigentlich eine einfache Aufgabe, die Größe einer DVD ist fest, es sollte eine Tonspur enthalten sein und die Filmdaten nach möglichkeit den ganzen Rest ausfüllen.

Überraschenderweise war jedoch die endgültige Version größer als die DVD und auch größer als die Video- und die Audiodaten zusammen. Das konnte ich natürlich leicht durch ein erneutes Kodieren der Videodaten korrigieren. Den Grund für den zusätzlichen Speicherverbrauch konnte ich jedoch nicht rausfinden.

Ein Blick in den Sourcecode von DVD-Flick 2 (ein Fork des nicht weiter verfolgten Ursprungsprojektes) zeigt jedoch, dass willkürlich 4% zusätzlicher Platz für das Muxen eingeplant wird. Ist zwar in der aktuellen Version auskommentiert, funktioniert aber in der Praxis gut. Damit kommen wohl auch die sehr genauen Videogrößen von DVD-Flick zustande, mit denen etwa 20-30 MiB auf dem Medium freibleiben.

  ' Room for muxing overhead is 4% (which is a lot for a 4.3 Gb DVD)
  discSize = 0.96 * discSize

Die Zielbitrate (inklusive wählbarem Mux-Overhead) kann mit folgendem Skript leicht bestimmt werden:

Bei Unterschieden (wie DVD+R und DVD-R) wird die kleine Größe gewählt.
Angenommen, codierung von 5.1 Sound als AAC mit 448 kbit/s
Zusätzlich eingeplanter Platz für das Muxen.
Falls die Abspielgeschwindigkeit von Quell- und Zielmedium unterschiedlich ist, z. B. 23,976 bei BluRay-Quellen und 25 bei PAL DVDs
Aktualisiert die unten angegebenen Werte basierend auf den Eingaben.

Berechnet unter der Anname von Tonspuren kodiert als AC3 mit 448 Kb/s:

Benötigter Platz für Tonspuren: 555,23 MiB
Verfügbarer Platz für Video: 3748,09 MiB
Maximale Bitrate für Video: 5906,72 Kb/s

Unberücksichtigt bleibt, dass für DVDs die maximale Bitrate 9000 ist.

Geschrieben von Kap. Zuletzt geändert am 19. November 2016.