Tester un adaptateur callback vers async en Swift sur un Mac cloud

Tester un adaptateur callback vers async en Swift sur un Mac cloud

Tester un adaptateur callback vers async en Swift sur un Mac cloud

Le callback de fin d’un ancien SDK fonctionne parfaitement dans les tests locaux. Pourtant, une fois les exécutions parallélisées sur un Mac cloud, il arrive qu’une continuation soit reprise deux fois ou qu’une tâche annulée reste indéfiniment en attente d’un résultat. Le problème tient généralement moins aux performances de la machine qu’à deux hypothèses de l’adaptateur : le callback ne serait appelé qu’une seule fois, et toujours de manière asynchrone. Nous allons supprimer ces deux hypothèses et vérifier les cas limites avec un programme Swift autonome, sans réseau réel ni temporisations arbitraires.

Définir d’abord qui peut fixer le résultat final

Nous traitons ici une opération à résultat unique : parmi la réussite, l’échec et l’annulation, le premier résultat enregistré dans la machine à états l’emporte. Les notifications suivantes ne modifient plus ce résultat et ne doivent pas reprendre une seconde fois le code en attente. Ce contrat ne convient pas aux interfaces de streaming ni à celles qui produisent plusieurs mises à jour.

Il faut conserver deux éléments d’état : la continuation, qui peut ne pas encore être installée, et le résultat final, qui peut déjà être fixé. L’annulation peut précéder l’installation de la continuation ; une seule variable optionnelle ne suffit donc pas à déterminer si la tâche est terminée.

Une checked continuation détecte les usages incorrects, mais elle ne déduplique pas les notifications à votre place et ne propage pas automatiquement l’annulation de la tâche à l’opération sous-jacente.

La règle retenue est « le premier résultat enregistré l’emporte », et non « l’annulation est toujours prioritaire ». Une fois la réussite enregistrée, une annulation ultérieure ne peut plus la remplacer, même si la reprise n’a pas encore été exécutée par l’ordonnanceur. Si les exigences métier imposent une priorité plus stricte à l’annulation, il faut définir un autre point de validation plutôt que d’ajouter des vérifications ad hoc autour de la valeur renvoyée.

Protéger l’état de fin avec une section critique courte

Enregistrez le code suivant dans BridgeCheck.swift. Cet exemple utilise Swift 6 et un SDK macOS qui fournit NSLock.withLock. Le verrou protège uniquement les lectures et les écritures de l’état : ni les callbacks externes, ni le lancement de la requête, ni la reprise de la continuation ne s’exécutent sous verrou.

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 ne signifie pas que l’on désactive les vérifications pour ne plus s’en soucier. Cette annotation engage la personne qui maintient le code : tous les champs mutables partagés sont protégés par le même verrou, et la valeur du résultat respecte Sendable. Tout ajout ultérieur d’un compteur ou d’un handle doit également faire l’objet d’une vérification de sa synchronisation.

L’installation et la fin de l’opération ne peuvent se produire que dans deux ordres : si l’installation arrive d’abord, la continuation en attente est récupérée puis reprise ; si la fin arrive d’abord, le résultat est conservé et la continuation sera reprise lors de son installation. Effacer la référence à la continuation permet de la libérer rapidement, mais c’est toujours l’état du résultat qui indique si l’opération est terminée. Une continuation remise à nil ne signifie pas que l’opération accepte de nouveau un résultat.

Couvrir les ordres critiques avec des tests déterministes

Ajoutez le point d’entrée suivant au même fichier. Le premier cas appelle volontairement le callback deux fois de manière synchrone ; le second lance une opération qui ne rappelle jamais, puis annule la tâche en attente. Aucun délai artificiel n’est utilisé pour tenter de deviner l’ordre d’exécution.

@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

Une exécution normale doit se terminer avec un code de sortie nul. Le cas de fin synchrone détecte les implémentations incorrectes qui lancent l’opération avant d’enregistrer la continuation. Les notifications répétées révèlent les secondes reprises non protégées. Le cas d’annulation vérifie que l’attente peut se terminer même si aucun callback n’arrive.

Une fois cet exemple intégré aux tests du projet, testez aussi directement la machine à états dans l’ordre « fin avant installation », puis vérifiez les deux ordres possibles entre réussite et échec. Les tests de charge concurrente peuvent compléter cette couverture, mais ne remplacent pas les cas dont l’ordre est explicitement imposé.

Annuler l’attente ne signifie pas arrêter l’opération sous-jacente

Cet adaptateur garantit la fin de l’attente, mais n’arrête pas le travail sous-jacent. Si l’annulation arrive en premier, start peut tout de même s’exécuter ; les notifications ultérieures seront alors ignorées. Cette limite est volontaire : l’exemple ne doit pas être considéré tel quel comme un client réseau complet.

Étendre l’adaptateur lorsqu’un handle d’annulation est disponible

Si l’interface réelle renvoie un handle d’annulation, il faut l’enregistrer dans le même état protégé. Si l’annulation arrive d’abord, mémorisez-la ; lorsque le handle est renvoyé, récupérez-le immédiatement pour annuler l’opération. Si le handle arrive en premier, c’est le chemin d’annulation qui le récupère. L’appel effectif à l’annulation doit toujours se faire hors du verrou, car il peut déclencher le callback de fin de manière synchrone.

Ne vous contentez pas de vérifier l’état d’annulation une seule fois avant le lancement. Une course reste possible entre cette vérification et le retour du handle. N’appelez pas non plus l’interface d’annulation sous verrou : si un callback synchrone entre à nouveau dans la machine à états, un interblocage peut se produire.

Si l’opération sous-jacente ne notifie jamais sa fin et que l’appelant n’annule pas non plus, l’attente se poursuit. Tout délai d’expiration métier doit passer par le même point d’entrée de fin et prévoir aussi la libération des ressources sous-jacentes. Il ne faut pas supposer qu’une continuation possède un délai d’expiration intégré.

Intégrer la validation au point d’entrée de l’exécution cloud

Compilez et exécutez cette vérification lors de l’étape préalable au build sur le Mac cloud, en enregistrant la version du compilateur, l’identifiant du commit et le code de sortie. L’exemple ne nécessite aucune session graphique et se prête à des exécutions répétées en ligne de commande.

Avant de fusionner les changements, vérifiez quatre points : chaque appel dispose de sa propre machine à états ; aucun callback synchrone n’est perdu ; les notifications répétées ne provoquent pas plusieurs reprises ; aucune attente ne reste bloquée après une annulation. Pour une interface réelle dotée d’un handle, ajoutez un test spécifique où l’annulation précède le retour du handle.

Enfin, examinez les relations de rétention des closures. Même une fois l’attente terminée, l’opération sous-jacente peut conserver la closure de fin ; la déduplication assurée par la machine à états ne résout pas tous les problèmes de cycle de vie. L’adaptation asynchrone n’est véritablement maîtrisée qu’après avoir validé séparément le contrat de fin, le contrat d’arrêt et la libération des ressources.

Questions fréquentes

Une checked continuation ignore-t-elle le second callback ?

Non. Un second resume viole le contrat. Il faut sélectionner un résultat unique sous verrou, puis reprendre la continuation après avoir libéré le verrou.

Annuler la tâche arrête-t-il la requête sous-jacente ?

Pas automatiquement. Cet adaptateur termine l’attente avec une erreur d’annulation. L’opération nécessite son propre mécanisme d’arrêt, y compris si l’annulation précède le retour de son handle.

Confiez votre prochain build à un Mac dédié dans le cloud

MacFTP propose des machines physiques dédiées Apple Silicon exécutant macOS en natif, avec des locations à la journée, à la semaine, au mois ou au trimestre. Vérifiez la configuration, puis connectez votre chaîne d’outils au nœud ; le statut réel de disponibilité est indiqué en temps réel dans la console.

Louer un Mac dans le cloud

De la lecture à votre projet

Vérifiez la configuration avant de planifier la livraison

Comparez les modèles et les durées de location, ou retournez sur le blog pour consulter d’autres articles techniques.