Un traitement d’images enchaîne un téléchargement, une conversion, puis l’écriture dans un répertoire. Pourtant, si le téléchargement échoue, la conversion peut tout de même être lancée ; et lorsque l’utilisateur annule, l’écriture des fichiers peut se poursuivre. Ces deux situations ne révèlent généralement pas un dysfonctionnement de la file, mais une confusion entre « dépendance terminée », « réussite métier » et « demande d’annulation ». Cet article reproduit ces cas limites dans un programme Swift autonome sur un Mac dans le cloud, puis transpose les vérifications dans une cible de tests iOS, sans accès réseau ni identifiants de signature.
Ce que la file ne prend pas en charge
Les dépendances d’OperationQueue imposent seulement que les opérations préalables soient terminées. La file n’interprète pas leurs résultats métier, comme un échec du téléchargement ou de la conversion. De même, cancel() positionne un indicateur d’annulation, sans interrompre de force une fermeture en cours d’exécution.
Définissez quatre états finaux pour chaque étape métier : réussite, échec, étape ignorée et annulation. Si une dépendance échoue, marquez l’étape comme ignorée. Si une étape déjà démarrée reçoit une demande d’annulation, faites-la sortir à un point sûr. N’utilisez pas un même booléen pour représenter à la fois « non exécuté » et « exécuté en échec ».
isFinishedindique que le cycle de vie de l’opération est terminé, pas que le traitement métier a réussi ;isCancelledindique qu’une annulation a été demandée, pas que le travail s’est arrêté.
Cet exemple utilise uniquement des instances synchrones de BlockOperation. Si une fermeture lance une requête asynchrone puis retourne immédiatement, la file considère son exécution comme terminée : les dépendances présentées ici ne permettent alors pas d’attendre le rappel de la requête. Une opération asynchrone nécessite une implémentation distincte qui gère correctement les états de son cycle de vie.
Empêcher la poursuite après un échec grâce à des résultats explicites
Enregistrez le contenu suivant dans QueueContract.swift. Un verrou protège la lecture et l’écriture des résultats pour éviter les accès concurrents non synchronisés si vous ajoutez ensuite des étapes parallèles. L’exemple ne repose pas sur un ordre d’ordonnancement fortuit.
import Foundation
enum Outcome: Equatable {
case pending
case succeeded
case failed
case skipped
}
final class State: @unchecked Sendable {
private let lock = NSLock()
private var values: [String: Outcome] = [:]
func set(_ key: String, _ value: Outcome) {
lock.lock()
defer { lock.unlock() }
values[key] = value
}
func get(_ key: String) -> Outcome {
lock.lock()
defer { lock.unlock() }
return values[key] ?? .pending
}
}
let state = State()
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
let download = BlockOperation {
state.set("download", .failed)
}
let convert = BlockOperation {
guard state.get("download") == .succeeded else {
state.set("convert", .skipped)
return
}
state.set("convert", .succeeded)
}
convert.addDependency(download)
queue.addOperations([convert, download], waitUntilFinished: true)
precondition(state.get("download") == .failed)
precondition(state.get("convert") == .skipped)
precondition(download.isFinished && convert.isFinished)
Compilez et exécutez le programme dans le terminal :
xcrun swiftc QueueContract.swift -o QueueContract
./QueueContract
Un code de sortie nul indique que les assertions sont vérifiées ; il n’est pas nécessaire d’afficher un message de réussite. Créez aussi un cas où le téléchargement réussit, avec une assertion vérifiant alors la réussite de la conversion, afin de couvrir les deux issues. @unchecked Sendable n’est qu’une garantie manuelle de sûreté vis-à-vis des accès concurrents : elle suppose que tous les accès à l’état partagé passent par le verrou et que le dictionnaire interne ne soit pas exposé.
Tester séparément l’annulation en attente et en cours d’exécution
Commencez par vérifier le cas d’une tâche qui n’a pas encore été exécutée. Ajoutez le fragment suivant au même fichier, puis recompilez. Une file distincte évite d’interférer avec le cas précédent.
let heldQueue = OperationQueue()
heldQueue.isSuspended = true
let cancelledState = State()
let waiting = BlockOperation {
cancelledState.set("body", .succeeded)
}
heldQueue.addOperation(waiting)
waiting.cancel()
heldQueue.isSuspended = false
heldQueue.waitUntilAllOperationsAreFinished()
precondition(waiting.isCancelled)
precondition(waiting.isFinished)
precondition(cancelledState.get("body") == .pending)
La suspension de la file garantit que l’annulation intervient avant l’exécution, sans avoir à deviner un délai en millisecondes. Les assertions vérifient à la fois l’indicateur d’annulation, l’achèvement final et l’absence d’exécution de la fermeture. Omettre l’une de ces vérifications laisserait un angle mort.
Prévoyez un cas distinct pour une tâche en cours d’exécution : elle émet un signal « démarré » une fois entrée dans sa boucle de traitement. Le test attend ce signal pour demander l’annulation, puis autorise l’itération suivante. À chaque itération, vérifiez isCancelled avant de lire les données et avant de valider les résultats. L’attente des signaux doit comporter un délai maximal, dont le dépassement fait échouer le test : celui-ci ne doit jamais rester bloqué indéfiniment.
Une fermeture qui n’a pas encore été lancée au moment de l’annulation ne consignera pas elle-même son état métier final. C’est donc à la couche d’orchestration d’enregistrer l’annulation. Ne confiez pas le nettoyage à une fermeture qui ne s’est jamais exécutée.
Préserver la frontière de validation des écritures
L’annulation coopérative ne prend effet qu’aux points de contrôle. Si une conversion est longue, le délai de prise en compte de l’annulation peut au moins s’étendre sur toute l’étape de conversion en cours. Découper les opérations coûteuses en lots de taille adaptée est plus utile que d’interroger fréquemment l’indicateur dans une boucle vide.
Écrivez d’abord les fichiers dans un emplacement temporaire propre à la tâche, puis validez-les après avoir fermé les descripteurs. En cas d’échec ou d’annulation, supprimez uniquement les fichiers temporaires de la tâche, pas le répertoire partagé. Si une demande d’annulation arrive après une validation réussie, conservez l’état final « réussite » au lieu de le remplacer rétroactivement par « annulation » : l’appelant pourrait sinon croire qu’aucun fichier n’a été produit.
L’annulation et la validation peuvent survenir simultanément. Si les règles métier exigent qu’elles soient strictement exclusives, faites passer les deux décisions par le même verrou ou par une machine à états sérialisée. Une simple vérification de l’indicateur avant l’écriture ne supprime pas la course susceptible de se produire ensuite.
Vérifier l’état final complet dans les tests de non-régression
Lorsque vous transposez les assertions de l’exemple dans XCTest, remplacez precondition par des assertions de test. Attendre la fin de la file ne pose pas de problème dans une tâche en ligne de commande sur un Mac dans le cloud. En revanche, ne bloquez pas le thread principal d’une application par une attente synchrone, surtout si les tâches attendent elles-mêmes un rappel sur ce thread.
Chaque campagne doit couvrir au minimum les cas suivants : réussite de l’opération préalable, échec de l’opération préalable, annulation avant exécution, annulation en cours d’exécution et annulation après validation. Pour chaque cas, vérifiez à la fois l’état métier final, la présence ou l’absence du résultat produit et le nombre de notifications de fin. La notification de fin doit être envoyée une seule fois ; l’absence de rappel comme les rappels multiples doivent faire échouer le test.
Imposez l’ordre des événements à l’aide de signaux contrôlables, sans recourir à sleep pour créer un timing prétendument stable. Vous obtenez ainsi un contrat reproductible : aucune validation indue après l’échec d’une dépendance, une exécution qui peut se terminer après annulation et un nettoyage qui n’intervient qu’après la fin. C’est en garantissant ces conditions que vous pourrez ensuite augmenter le parallélisme sur des bases fiables.
Questions fréquentes
Une opération dépendante est-elle ignorée si son prérequis échoue ?
Non. La dépendance impose seulement un ordre d’exécution. Transmettez un résultat protégé contre les accès concurrents et faites ignorer explicitement l’étape suivante en cas d’échec.
Peut-on supprimer les fichiers dès l’appel à cancelAllOperations ?
Non. Les opérations en cours peuvent encore les utiliser. Demandez l’annulation, attendez leur arrêt coopératif hors du thread principal, puis supprimez les fichiers.
Préparez votre prochain build sur un Mac dans le cloud
Choisissez le modèle et la durée selon votre charge de travail. Profitez d’un nœud physique Apple Silicon dédié, et non d’une machine virtuelle.
Louer un Mac dans le cloud