Swift-Adapter von Callback zu async auf dem Cloud-Mac testen

Swift-Adapter von Callback zu async auf dem Cloud-Mac testen

Swift-Adapter von Callback zu async auf dem Cloud-Mac testen

Der Abschluss-Callback eines älteren SDKs funktioniert in lokalen Tests zuverlässig. Nach dem Umzug auf einen Cloud-Mac und der parallelen Ausführung wird eine Continuation jedoch gelegentlich mehrfach fortgesetzt – oder ein abgebrochener Task wartet vergeblich auf ein Ergebnis. Meist liegt das nicht an der Rechenleistung, sondern daran, dass der Adapter zwei Annahmen als gegeben behandelt: Der Callback kommt genau einmal und wird immer asynchron aufgerufen. Im Folgenden beseitigen wir beide Annahmen und prüfen die Grenzfälle mit einem eigenständigen Swift-Programm, ohne echte Netzwerkzugriffe oder zufällig gewählte Wartezeiten.

Zuerst festlegen, wer das endgültige Ergebnis bestimmt

Hier geht es um einmalige Operationen: Von Erfolg, Fehler und Abbruch gilt das Ergebnis, das zuerst in der Zustandsmaschine festgeschrieben wird. Spätere Benachrichtigungen ändern dieses Ergebnis nicht mehr und dürfen die wartende Seite auch nicht erneut fortsetzen. Für Streaming-Schnittstellen oder Schnittstellen mit mehreren Aktualisierungen eignet sich dieser Vertrag nicht.

Dafür müssen zwei Arten von Zustand verwaltet werden: eine noch zu hinterlegende Continuation und ein bereits feststehendes Endergebnis. Ein Abbruch kann eintreffen, bevor die Continuation hinterlegt wurde. Deshalb lässt sich nicht allein anhand einer optionalen Variablen entscheiden, ob die Operation bereits abgeschlossen ist.

Eine Checked Continuation erkennt fehlerhafte Verwendung, verhindert aber weder doppelte Abschlüsse noch übersetzt sie einen Task-Abbruch automatisch in den Abbruch der zugrunde liegenden Operation.

Hier gilt „das zuerst festgeschriebene Ergebnis gewinnt“, nicht „Abbruch hat immer Vorrang“. Wurde der Erfolg bereits festgeschrieben, darf ein späterer Abbruch ihn nicht überschreiben – selbst wenn die Fortsetzung noch nicht zur Ausführung gekommen ist. Verlangt die Anwendungslogik einen strengeren Vorrang für Abbrüche, muss sie dafür einen eigenen verbindlichen Entscheidungspunkt definieren, statt nachträglich beliebige Prüfungen um den Rückgabewert herum einzubauen.

Den Abschlusszustand mit kurzen kritischen Abschnitten schützen

Speichern Sie den folgenden Code als BridgeCheck.swift. Das Beispiel verwendet Swift 6 und ein macOS SDK, das NSLock.withLock bereitstellt. Die Sperre schützt ausschließlich das Lesen und Schreiben des Zustands. Externe Callbacks, das Starten der Anfrage und das Fortsetzen der Continuation finden außerhalb der Sperre statt.

import Foundation

final class CompletionGate<Value: Sendable>: @unchecked Sendable {
    private let lock = NSLock()
    private var outcome: Result<Value, any Error>?
    private var waiter: CheckedContinuation<Value, any Error>?

    func install(_ continuation: CheckedContinuation<Value, any Error>) {
        let ready: Result<Value, any Error>? = lock.withLock {
            if let outcome {
                return outcome
            }
            precondition(waiter == nil)
            waiter = continuation
            return nil
        }
        if let ready {
            continuation.resume(with: ready)
        }
    }

    func finish(_ result: Result<Value, any Error>) {
        let continuation: CheckedContinuation<Value, any Error>? =
            lock.withLock {
                guard outcome == nil else {
                    return nil
                }
                outcome = result
                let current = waiter
                waiter = nil
                return current
            }
        continuation?.resume(with: result)
    }
}

func bridge(
    start: @Sendable (
        @escaping @Sendable (Result<Int, any Error>) -> Void
    ) -> Void
) async throws -> Int {
    let gate = CompletionGate<Int>()
    return try await withTaskCancellationHandler {
        try await withCheckedThrowingContinuation {
            (continuation: CheckedContinuation<Int, any Error>) in
            gate.install(continuation)
            start { result in
                gate.finish(result)
            }
        }
    } onCancel: {
        gate.finish(.failure(CancellationError()))
    }
}

@unchecked Sendable bedeutet nicht, dass man die Prüfung abschaltet und sich anschließend nicht mehr darum kümmern muss. Damit übernehmen die Verantwortlichen die Garantie, dass alle gemeinsam genutzten veränderlichen Felder durch dieselbe Sperre geschützt sind und der Ergebniswert Sendable erfüllt. Werden später Zähler oder Handles ergänzt, muss auch deren Synchronisierung geprüft werden.

Für das Hinterlegen und den Abschluss gibt es nur zwei mögliche Reihenfolgen: Wird die Continuation zuerst hinterlegt, wird sie beim Abschluss entnommen und fortgesetzt. Erfolgt der Abschluss zuerst, wird das Ergebnis gespeichert und die Continuation beim späteren Hinterlegen fortgesetzt. Das Leeren von waiter gibt die Referenz frühzeitig frei. Ob die Operation abgeschlossen ist, entscheidet aber immer der Ergebniszustand. Ein leerer waiter darf nicht als erneute Freigabe für einen weiteren Abschluss verstanden werden.

Die entscheidenden Reihenfolgen deterministisch testen

Ergänzen Sie dieselbe Datei um den folgenden Programmeinstieg. Der erste Test ruft den Callback absichtlich zweimal synchron auf. Der zweite startet eine Operation, die niemals einen Callback auslöst, und bricht anschließend den wartenden Task ab. Der gesamte Ablauf kommt ohne Schlafpausen aus, mit denen man den Zeitpunkt der Ausführung zu erraten versucht.

@main
struct BridgeCheck {
    static func main() async throws {
        let value = try await bridge { complete in
            complete(.success(7))
            complete(.success(9))
        }
        precondition(value == 7)

        let pending = Task {
            try await bridge { _ in }
        }
        pending.cancel()

        do {
            _ = try await pending.value
            preconditionFailure("Expected cancellation")
        } catch is CancellationError {
        }
    }
}
xcrun swiftc -parse-as-library -swift-version 6 \
  BridgeCheck.swift -o BridgeCheck
./BridgeCheck

Bei korrekter Ausführung endet das Programm mit Exitcode null. Der synchrone Abschluss deckt fehlerhafte Implementierungen auf, die zuerst die Operation starten und erst danach die Continuation speichern. Die doppelte Benachrichtigung erkennt eine ungeschützte zweite Fortsetzung. Der Abbruchtest prüft, ob der wartende Task auch ohne Callback beendet werden kann.

Nach der Übernahme in die Projekttests sollte die Zustandsmaschine außerdem direkt mit der Reihenfolge „erst abschließen, dann Continuation hinterlegen“ getestet werden. Lassen Sie Erfolg und Fehler zudem in beiden entgegengesetzten Reihenfolgen eintreffen. Nebenläufige Stresstests können die Abdeckung ergänzen, ersetzen aber keine Tests mit ausdrücklich vorgegebener Reihenfolge.

Das Warten abzubrechen stoppt nicht die zugrunde liegende Operation

Dieser Adapter stellt sicher, dass die wartende Seite beendet wird. Die zugrunde liegende Arbeit stoppt er jedoch nicht. Trifft der Abbruch zuerst ein, kann start trotzdem noch ausgeführt werden; spätere Benachrichtigungen werden ignoriert. Diese Grenze ist bewusst gesetzt. Das Beispiel sollte daher nicht unverändert als vollständiger Netzwerkclient verwendet werden.

Erweiterung für Schnittstellen mit Abbruch-Handle

Gibt die tatsächliche Schnittstelle ein Abbruch-Handle zurück, muss es im selben geschützten Zustand registriert werden. Trifft der Abbruch zuerst ein, wird dies durch eine Markierung festgehalten. Sobald das Handle später zurückgegeben wird, wird es sofort entnommen und der Abbruch ausgelöst. Liegt das Handle bereits vor, wird es vom Abbruchpfad entnommen. Der eigentliche Abbruchaufruf erfolgt weiterhin außerhalb der Sperre, da er den Abschluss-Callback synchron auslösen kann.

Prüfen Sie den Abbruchzustand nicht nur einmal vor dem Start. Zwischen einer erfolgreichen Prüfung und der Rückgabe des Handles besteht weiterhin ein Wettlauf. Rufen Sie die Abbruchschnittstelle auch nicht innerhalb der Sperre auf: Tritt ein synchroner Callback erneut in die Zustandsmaschine ein, kann es zu einem Deadlock kommen.

Liefert die zugrunde liegende Operation niemals eine Benachrichtigung und bricht auch der Aufrufer nicht ab, wird weiterhin gewartet. Ein anwendungsseitiges Timeout muss denselben Abschlussweg verwenden und zugleich die Freigabe der zugrunde liegenden Ressourcen berücksichtigen. Eine Continuation bringt kein automatisches Timeout mit.

Die Prüfung in den Cloud-Ausführungsablauf aufnehmen

Kompilieren und starten Sie diese Prüfung auf dem Cloud-Mac vor dem eigentlichen Build. Protokollieren Sie dabei die Compilerversion, die Commit-Kennung und den Exitcode. Das Beispiel benötigt keine grafische Sitzung und lässt sich daher gut wiederholt über die Kommandozeile ausführen.

Prüfen Sie vor dem Merge vier Punkte: Jeder Aufruf besitzt eine eigene Zustandsmaschine; synchrone Callbacks gehen nicht verloren; doppelte Benachrichtigungen führen nicht zu mehrfacher Fortsetzung; nach einem Abbruch bleibt kein wartender Task hängen. Ergänzen Sie für die tatsächliche Schnittstelle mit Handle einen gezielten Test für den Fall „Abbruch vor Rückgabe des Handles“.

Prüfen Sie abschließend, welche Referenzen die Closures halten. Selbst wenn das Warten bereits beendet ist, kann die zugrunde liegende Operation die Abschluss-Closure weiterhin speichern. Das Verhindern doppelter Abschlüsse in der Zustandsmaschine löst nicht alle Lebenszyklusprobleme. Erst wenn der Abschlussvertrag, der Vertrag zum Stoppen der Operation und die Ressourcenfreigabe getrennt geprüft wurden, ist die asynchrone Kapselung wirklich vollständig abgesichert.

Häufig gestellte Fragen

Ignoriert eine Checked Continuation doppelte Callbacks?

Nein. Ein zweites resume verletzt den Vertrag. Ein synchronisierter Zustand muss genau ein Ergebnis auswählen; die Continuation wird erst nach Freigabe der Sperre fortgesetzt.

Stoppt der Task-Abbruch auch die zugrunde liegende Anfrage?

Nicht automatisch. Der Adapter beendet das Warten mit einem Abbruchfehler. Die Operation benötigt eine eigene Abbruchfunktion, die auch einen Abbruch vor Rückgabe ihres Handles berücksichtigt.

Den nächsten Build auf einem exklusiven Cloud-Mac ausführen

MacFTP bietet exklusive Apple-Silicon-Physikserver mit nativem macOS. Wählen Sie eine Laufzeit von einem Tag, einer Woche, einem Monat oder einem Quartal. Prüfen Sie zuerst die Konfiguration und binden Sie anschließend Ihre vorhandene Toolchain an den Server an. Den aktuellen Verfügbarkeitsstatus sehen Sie in Echtzeit im Portal.

Jetzt Cloud-Mac mieten

Vom Lesen zurück zum Projekt

Erst Konfiguration prüfen, dann Bereitstellung planen

Vergleichen Sie Modelle und Laufzeiten oder lesen Sie weitere technische Beiträge im Blog.