fastly-compute-async-request-reliability

Par divinevideo · divine-mobile

Corrige les échecs silencieux dans Fastly Compute@Edge lors de l'utilisation de `send_async` pour les requêtes fire-and-forget. À utiliser quand : (1) Les tâches en arrière-plan (migrations, webhooks, notifications) déclenchées depuis Compute ne s'exécutent jamais jusqu'au bout, (2) le `PendingRequest` renvoyé par `send_async` est immédiatement supprimé, (3) le pattern fire-and-forget fonctionne en local mais échoue en production, (4) un déclencheur async « s'enregistre comme déclenché » mais le service cible ne reçoit jamais la requête (notamment après un renommage de constantes backend — le hostname de l'URL est cosmétique, c'est le nom du backend qui détermine le routage, donc un renommage global peut silencieusement rediriger les appels vers le mauvais service). Couvre également les erreurs silencieuses de type backend-not-found quand `FALLBACK_BACKENDS` ou d'autres constantes backend référencent des noms non configurés dans le tableau de bord Fastly.

npx skills add https://github.com/divinevideo/divine-mobile --skill fastly-compute-async-request-reliability

Fiabilité des requêtes asynchrones Fastly Compute

Problème

Les requêtes HTTP « fire-and-forget » depuis Fastly Compute@Edge utilisant send_async échouent silencieusement car le processus worker peut se terminer avant que la requête asynchrone atteigne le backend. De plus, les noms de backends référencés dans le code mais non configurés dans le tableau de bord Fastly causent des défaillances silencieuses faciles à manquer.

Contexte / Conditions de déclenchement

  • Migration de fond, webhook ou notification déclenchés via req.send_async(backend)
  • Le PendingRequest retourné par send_async est immédiatement abandonné (non attendu)
  • La réponse principale est envoyée au client, causant la terminaison du worker Compute
  • L'erreur est avalée avec let _ = ... ou match ... { Err(_) => ... }
  • Fonctionne lors des tests locaux (fastly compute serve) mais échoue en production
  • Le nom du backend dans le code ne correspond à aucun backend dans fastly backend list --service-id

Solution

1. Utiliser le send() synchrone au lieu de send_async() pour les opérations critiques

// MAUVAIS : Fire-and-forget — le worker se termine avant que la requête se complète
match req.send_async(BACKEND) {
    Ok(_pending) => {
        // PendingRequest abandonné ici — la requête ne se complète probablement jamais !
        Ok(())
    }
    Err(e) => { /* ... */ }
}

// BON : send synchrone — attend la réponse
match req.send(BACKEND) {
    Ok(resp) => {
        let status = resp.get_status();
        if status.is_success() {
            eprintln!("[MIGRATE] Success for {}", hash);
        } else {
            eprintln!("[MIGRATE] Backend returned {}", status);
        }
        Ok(())
    }
    Err(e) => {
        eprintln!("[MIGRATE] Failed: {}", e);
        Ok(()) // Ne pas échouer la requête principale
    }
}

2. Quand le synchrone est trop lent, utiliser une couche de cache

Si une couche de cache VCL fait écran à Compute (service chaining), la latence supplémentaire du send synchrone n'affecte que les cache misses. Les requêtes suivantes frappent le cache. Cela rend le send synchrone acceptable pour des opérations comme les déclencheurs de migration.

3. Toujours vérifier l'existence du backend

# Lister les backends configurés sur le service
fastly backend list --service-id YOUR_SERVICE_ID --version latest

# Ajouter un backend manquant
fastly backend create --service-id YOUR_SERVICE_ID --version latest --autoclone \
  --name cdn_divine --address cdn.divine.video --port 443 \
  --use-ssl --ssl-sni-hostname cdn.divine.video --override-host cdn.divine.video

Vérifiez que chaque nom de backend dans votre code Rust (req.send("backend_name")) a une entrée correspondante dans le tableau de bord Fastly. La section [local_server.backends] de fastly.toml est réservée au dev local uniquement — elle ne crée PAS de backends en production.

4. Attention : le hostname de l'URL est cosmétique — le nom du backend décide du routage

Dans Fastly Compute, req.send(backend_name) / req.send_async(backend_name) ignore le hostname de l'URL pour le routage. La configuration du backend au tableau de bord (address, override_host, TLS SNI) détermine où la requête atterrit réellement. Cela signifie que deux services Cloud Run différents qui partagent une constante de backend livreront tous deux à l'adresse vers laquelle ce backend est câblé — et le call site qui semblait correct parce que son URL disait service-A.run.app frappera en réalité service-B.

Cela frappe le plus fort après une refactorisation globale. Exemple : une refactorisation remplace CLOUD_RUN_BACKEND par UPLOAD_SERVICE_BACKEND dans tout le fichier via sed. La plupart des call sites ciblaient légitimement le service d'upload, donc ils continuent à fonctionner. Mais un call site faisait POST https://divine-transcoder-XXXX.run.app/transcode et le rename le pointe vers le backend du service d'upload — la requête reçoit un 404 nginx du service d'upload, send_async retourne Ok (le backend a accepté la connexion), et la ligne de log eprintln!("[HLS] Triggered on-demand transcoding for {}", hash) est un faux positif. Le transcoder ne voit jamais la requête. Les vidéos s'accumulent dans un état « en cours » pendant des jours.

Comment attraper cette classe de bug :

  1. Quand vous voyez un rename de constante de backend, grep pour chaque call site et vérifiez que le hostname de l'URL correspond à ce vers quoi le nouveau backend est configuré pour router :

    grep -n "send(\|send_async(" src/*.rs
    grep -n "BACKEND: &str" src/*.rs  # trouver les définitions de constantes

    Tout call site où format!("https://{}/...", SOMETHING_ELSE) ne correspond pas à l'address/override_host du backend au tableau de bord est un misroutage silencieux.

  2. Sonder la destination « erronée » directement de l'extérieur avec curl en utilisant un payload synthétique. Si vous recevez un 404 ou 405 de nginx/du mauvais service, vous avez trouvé un misroutage. Exemple :

    curl -X POST https://upload.divine.video/transcode -H 'Content-Type: application/json' -d '{"hash":"0"*64}'
    # HTTP 404 <- mauvais service, backend misrouté
  3. Si vous possédez plus d'un service Cloud Run, définissez un backend par service (transcoder_backend, upload_service, transcriber_backend) et ne les regroupez jamais juste parce que les deux sont *.run.app. Le override_host sur chaque backend épingle sa destination quel que soit l'URL.

  4. Basculez les déclencheurs fire-and-forget auxquels vous tenez vers send() synchrone au moins pendant l'investigation — une véritable réponse non-2xx du mauvais service rend le bug visible immédiatement au lieu de se cacher derrière send_async.

5. Ne jamais avaler silencieusement les erreurs de backend pour les opérations importantes

// MAUVAIS : Défaillance silencieuse
let _ = trigger_migration(&hash, &source);

// BON : Logger l'erreur même si vous ne faites pas échouer la requête
if let Err(e) = trigger_migration(&hash, &source) {
    eprintln!("[MIGRATE] Failed for {}: {}", hash, e);
}

Vérification

  1. Vérifiez les logs de Compute pour les messages de succès de migration/webhook
  2. Vérifiez que l'effet de bord s'est réellement produit (par ex., blob existe dans GCS après migration)
  3. fastly backend list --service-id ID --version latest montre tous les backends attendus

Exemple

Dans Divine Blossom, la migration à la demande de Bunny CDN vers GCS n'a jamais fonctionné parce que :

  1. Le backend cdn_divine n'a jamais été configuré dans le tableau de bord Fastly (seulement dans fastly.toml)
  2. Le send_async pour la requête de migration Cloud Run a été abandonné avant la completion
  3. Les deux erreurs ont été avalées silencieusement avec let _ = ...

Correction : Ajout du backend cdn_divine via CLI, changement de send_async en send, ajout de logging.

Notes

  • fastly.toml [local_server.backends] configure seulement les backends pour fastly compute serve
  • Les backends de production doivent être configurés via le tableau de bord ou CLI (fastly backend create)
  • Dans Fastly Compute, une fois que le corps de la réponse principale commence à streamer vers le client, le worker peut être terminé à tout moment — les requêtes asynchrones en vol peuvent être annulées
  • Si vous avez besoin d'un vrai fire-and-forget, envisagez d'appeler une queue externe (Cloud Tasks, PubSub) au lieu du HTTP direct

Skills similaires