Optimistische iOS-Updates auf einem Cloud-Mac prüfen

Optimistische iOS-Updates auf einem Cloud-Mac prüfen

Die Favoriten-Schaltfläche ändert beim Antippen sofort ihre Farbe, landet nach einem Netzwerkfehler aber im falschen Zustand. Oder eine alte Anfrage verändert nach dem Ab- und erneuten Anmelden die Oberfläche des neuen Benutzers. Um solche Fehler aufzuspüren, muss man nicht auf ein langsames Netz hoffen. In einem Build-Job auf einem Cloud-Mac lassen sie sich zuverlässig reproduzieren: Dazu wird das optimistische Update als eigenständige Zustandsmaschine modelliert und mit Erfolgs-, Fehler- und Sitzungswechselereignissen in einer festgelegten Reihenfolge durchgespielt.

Zuerst den Vertrag für einen einzigen laufenden Schreibvorgang festlegen

Dieser Artikel verwendet eine Strategie, die sich besonders einfach überprüfen lässt: Pro Datensatz darf höchstens ein Schreibvorgang gleichzeitig laufen. Ein erneutes Antippen während der laufenden Anfrage löst keine weitere Anfrage aus. Die Oberfläche behält den vorläufigen Wert bei und zeigt an, dass gespeichert wird. Damit entfällt zwar das schnelle mehrfache Umschalten, dafür steht eindeutig fest, auf welchen Wert bei einem Fehler zurückgesetzt werden muss.

Der Zustand besteht aus drei Teilen: dem bestätigten Wert, dem aktuell angezeigten Wert und dem Ticket der laufenden Anfrage. Beim Start wird nur der angezeigte Wert geändert. Bei Erfolg werden beide Werte mit dem Rückgabewert des Servers aktualisiert; bei einem eindeutigen Fehlschlag wird der bestätigte Wert wiederhergestellt. Jeder Datensatz in einer Liste besitzt seinen eigenen Zustand. Ein globaler boolescher Schalter kommt nicht zum Einsatz.

Ein optimistisches Update zeigt lediglich die Benutzerabsicht vorab an. Es beweist nicht, dass das Speichern bereits erfolgreich war. Auch ein clientseitiges Ticket ersetzt keine serverseitige Steuerung der Schreibreihenfolge.

Ungewisse Ergebnisse klar abgrenzen

Ein Timeout oder ein Verbindungsabbruch bedeutet nicht, dass der Server die Anfrage abgelehnt hat. In diesem Fall kann die vorläufige Rückmeldung zurückgenommen werden. Der Datensatz muss jedoch in einen Zustand wechseln, der eine Synchronisierung erfordert. Erst nach dem erneuten Einlesen der Serverdaten dürfen weitere Aktionen freigegeben werden. Der folgende Fehlerzweig steht ausschließlich für eine fachliche Ablehnung, bei der feststeht, dass die Änderung nicht übernommen wurde. Er ist keine allgemeine Behandlung für sämtliche Netzwerkfehler.

Eine Zustandsmaschine ohne Abhängigkeit von der Oberfläche implementieren

Den folgenden Inhalt als optimistic-check.swift speichern. Die Tickets erhalten jeweils eine eigene Kennung und werden bei einem Sitzungswechsel nicht wiederverwendet. Das Modell selbst startet keine Netzwerkanfragen und verwendet kein UI-Framework. Es lässt sich deshalb direkt auf einem Knoten ausführen, auf dem die Kommandozeilentools installiert sind.

import Foundation

struct FavoriteState {
    private(set) var confirmed: Bool
    private(set) var visible: Bool
    private(set) var pending: UUID?

    init(_ value: Bool) {
        confirmed = value
        visible = value
    }

    mutating func begin(_ desired: Bool) -> UUID? {
        guard pending == nil else { return nil }
        let ticket = UUID()
        pending = ticket
        visible = desired
        return ticket
    }

    mutating func finish(_ ticket: UUID, saved: Bool?) {
        guard pending == ticket else { return }
        if let saved {
            confirmed = saved
        }
        visible = confirmed
        pending = nil
    }

    mutating func replaceSession(with value: Bool) {
        pending = nil
        confirmed = value
        visible = value
    }
}

Für den optionalen Wert in finish gilt eine eindeutige Vereinbarung: Ein vorhandener Wert bezeichnet den vom Server bestätigten Endzustand. nil bedeutet, dass die Änderung nachweislich nicht übernommen wurde. Der Server kann den angeforderten Wert aufgrund fachlicher Regeln ändern. Im Erfolgsfall darf daher nicht einfach der Zielwert übernommen werden, den der Benutzer beim Antippen gewählt hat.

Im Produktionscode gehört die Zustandsmaschine auf den Main Actor. Auch nach Abschluss einer Netzwerkanfrage muss die Ausführung dorthin zurückkehren, bevor der Zustand verändert wird. Diese Methoden dürfen nicht gleichzeitig aus zwei Callback-Queues aufgerufen werden. Ein Werttyp verhindert Datenrennen nicht automatisch.

Ereignisfolgen statt echter Netzwerkwartezeiten verwenden

Die folgenden Assertions am Ende derselben Datei ergänzen. Sie benötigen weder Wartepausen noch echte Anfragen. Obwohl die Tickets zufällig erzeugt werden, hängt die Prüfung nur von ihrer Gleichheit ab. Das Ergebnis bleibt daher deterministisch.

var state = FavoriteState(false)

let first = state.begin(true)!
precondition(state.visible && !state.confirmed)
precondition(state.begin(false) == nil)
state.finish(first, saved: nil)
precondition(!state.visible && state.pending == nil)

let second = state.begin(true)!
state.finish(second, saved: true)
precondition(state.visible && state.confirmed)
state.finish(second, saved: false)
precondition(state.visible)

let oldSession = state.begin(false)!
state.replaceSession(with: false)
let current = state.begin(true)!
state.finish(oldSession, saved: false)
precondition(state.pending == current && state.visible)
state.finish(current, saved: false)
precondition(!state.visible && !state.confirmed)

Die Ausführung erfolgt mit den folgenden Befehlen. Für diesen Kompiliervorgang dürfen keine Optimierungsoptionen verwendet werden, die Vorbedingungsprüfungen deaktivieren.

set -eu
xcrun swiftc optimistic-check.swift -o optimistic-check
./optimistic-check

Ein Exit-Code ungleich null muss den Job abbrechen. Die erste Gruppe prüft das Zurücksetzen bei einem Fehler sowie wiederholtes Antippen, die zweite doppelte Callbacks. Die dritte prüft Antworten aus einer alten Sitzung und den Fall, dass der Serverwert von der Benutzerabsicht abweicht. Kontokennungen oder vollständige Antwortinhalte müssen dafür nicht ausgegeben werden.

Bei der Netzwerkanbindung die Identität des Datensatzes sichern

Eine echte Anfrage muss die Datensatzkennung, die Identität der aktuellen Sitzung und das Ticket erfassen. Nach Eingang der Antwort ist zunächst zu prüfen, ob sie noch demselben Datensatz zugeordnet ist. Erst danach wird das Ergebnis an die zugehörige Zustandsmaschine übergeben. Werden Listenzellen wiederverwendet, darf das Ziel nicht allein anhand der Zeilennummer gesucht werden: Sortieren und Filtern verändern diese Nummern.

Bei einem Kontowechsel müssen die Zustände sämtlicher Datensätze ersetzt und die maßgeblichen Daten neu geladen werden. replaceSession zeigt im Beispiel das Zurücksetzen, nachdem der Anfangswert für die neue Sitzung vorliegt. Die Methode bedeutet nicht, dass nach dem Abmelden weiterhin alte Werte verwendet werden dürfen.

Das Abbrechen einer Anfrage reduziert lediglich unnötige Arbeit und ersetzt die Ticketprüfung nicht. Zum Zeitpunkt des Abbruchs kann der Server die Änderung bereits übernommen haben; auch der Callback kann bereits zur Ausführung eingereiht sein. Wenn mehrere Geräte denselben Datensatz gleichzeitig ändern, muss der Server zusätzlich Versionsbedingungen oder Konfliktergebnisse bereitstellen. Die lokale Beschränkung auf einen laufenden Schreibvorgang deckt geräteübergreifende Konkurrenz nicht ab.

Zustandsprüfungen zur Freigabebedingung machen

Vor dem Zusammenführen des Codes sind drei Arten von Prüfungen zu ergänzen: Nach einer fachlichen Ablehnung lässt sich die Schaltfläche erneut bedienen; bei einem ungewissen Ergebnis werden weitere Schreibvorgänge gesperrt und eine Synchronisierung ausgelöst; nach einem Kontowechsel verändern alte Callbacks den neuen Zustand nicht. Zusätzlich zu den Prüfungen der Zustandsmaschine müssen UI-Tests bestätigen, dass die Speicheranzeige, der Barrierefreiheitszustand und die Sperre weiterer Betätigungen tatsächlich an pending gebunden sind.

Tritt beim Zurücksetzen ein Fehler auf, sollten zunächst die Ereignisreihenfolge, die Datensatzkennung in anonymisierter Form und die Ergebniskategorie festgehalten werden. Anschließend ist zu prüfen, ob veraltete Zeilennummern oder Modelle aus einer anderen Sitzung verwendet wurden. Längere Wartezeiten dürfen das Problem nicht verdecken. Entscheidend für die Abnahme ist, dass jede Zustandsänderung auf ein konkretes Ereignis zurückgeht – nicht, dass die Schaltfläche am Ende zufällig richtig aussieht.

Häufig gestellte Fragen

Beweist ein Timeout, dass der Server nichts gespeichert hat?

Nein. Das Ergebnis ist unbekannt. Nach der vorläufigen Rücknahme muss der Client den Serverzustand neu lesen, bevor er weitere Schreibvorgänge zulässt.

Verhindert ein lokales Ticket konkurrierende Serveränderungen?

Nein. Es verhindert nur veraltete UI-Aktualisierungen. Serverseitig sind serialisierte Schreibvorgänge oder Versionsbedingungen erforderlich.

Einen physischen Mac in die Build-Pipeline integrieren

Wählen Sie zwischen Apple Silicon M4 und M4 Pro. Der gesamte Rechner steht exklusiv einem Kunden zur Verfügung – keine virtuelle Maschine. Wählen Sie das Modell passend zur Build-Last und den Abrechnungszeitraum, der zu Ihrem Team passt.

Physische Macs mieten

Übergeben Sie den nächsten Build einem dedizierten Knoten

Prüfen Sie zuerst die Hardwarespezifikationen und den Abrechnungszeitraum. Anschließend binden Sie einen exklusiven physischen Mac für Ihr Team an.