Planifier plusieurs programmes en un seul clic — Dépasser la limite de 15 minutes d'AWS Lambda
Un opérateur de canal FAST planifie un mois de programmes et clique une fois sur Deploy. Derrière ce simple clic, des centaines de programmes deviennent des milliers d'actions de planification MediaLive qui doivent être déployées dans AWS dans l'ordre, le tout dans la limite d'exécution de quinze minutes de Lambda — et toute défaillance en cours de route laisse un canal en direct avec des lacunes. Voici comment nous avons rendu fiable le déploiement en un seul clic de grandes listes de lecture, et pourquoi la prochaine étape consiste à utiliser un runtime différent, et non un Lambda plus grand.
Aperçu rapide
| Aspect | Détail |
|---|---|
| Runtime | AWS Lambda (invocation unique), timeout backend de 615s |
| Cible de sortie | MediaLive BatchUpdateScheduleCommand |
| Traitement par lots | Jusqu'à 200 actions MediaLive par requête — ~25 programmes en pratique |
| Actions par programme | ~7–8 (changement d'entrée + 4 filigranes par rendu + 2 SCTE-35), plus avec les coupures publicitaires |
| Mécanisme de secours | Réessai par programme en cas de rejet de lot |
| Observé | 195 programmes déployés en ~44 secondes (mesure locale) |
| Objectif documenté | 360 programmes en ≤90 secondes |
| Voie de mise à l'échelle horizontale | Découpage par Step Functions pour les listes de lecture de >1 mois — prévu, non livré |
Le Défi
Le déploiement d'une liste de lecture n'est pas une écriture — c'est une orchestration. Pour chaque programme planifié par l'opérateur, MediaLive doit savoir exactement quand changer d'entrée, quand activer le filigrane pour chaque rendu, quand insérer des points de repère SCTE-35 et quand insérer des publicités. Les contraintes structurelles sont :
- Le nombre d'actions est multiplicatif, pas additif. Chaque programme émet ~7–8 actions avant les coupures publicitaires — un changement d'entrée, quatre activations de filigrane (une par rendu car StaticImageOutputActivate utilise des coordonnées de pixels de sortie), et deux marqueurs SCTE-35. Les coupures publicitaires ajoutent trois actions supplémentaires chacune. Un mois de 360 programmes représente environ 2 800 actions sur le réseau.
- MediaLive impose l'ordre par canal. Les actions de planification sont ancrées dans le temps et se référencent mutuellement ; vous ne pouvez pas paralléliser les écritures vers le même canal sans que l'API ne les rejette comme conflictuelles.
- AWS Lambda a une limite stricte de 15 minutes. Pas une limite souple, pas une configuration. L'orchestrateur doit terminer dans ce délai, sinon le canal se retrouve à moitié déployé.
- La défaillance partielle est opérationnellement inacceptable. Si le programme 174 sur 360 échoue et interrompt l'exécution, l'opérateur n'a aucune vue des différences entre ce qui a été déployé et ce qui ne l'a pas été. Le canal est mis en ligne avec des lacunes ; les téléspectateurs voient un écran noir là où ils attendaient du contenu.
- La première version envoyait un BatchUpdateSchedule par programme avec une pause de 200ms entre les programmes. Cela représente environ 2,5 secondes de temps réel par programme. Avec 360 programmes, vous dépassez déjà la limite de Lambda avant même que MediaLive n'ait effectué un travail réel.
Le travail, alors, n'est pas d'écrire plus vite. Il s'agit d'écrire moins souvent, de survivre aux défaillances partielles, et de rester dans une seule invocation — sans perdre les garanties d'ordonnancement que MediaLive exige.
Pourquoi les approches existantes échouent
Les échappatoires évidentes échouent toutes pour des raisons structurelles, et non des problèmes de réglage.
- "Augmentez simplement le timeout de Lambda." C'est impossible. Quinze minutes est une limite stricte imposée par AWS pour l'exécution de Lambda ; ce n'est pas un réglage dans la console. Même si c'était le cas, le coût par programme augmente avec le catalogue — acheter plus de temps réel ne fait que reporter la prochaine limite.
- "Passez à ECS Fargate ou EC2." Nous l'avons envisagé et rejeté. Les conteneurs de longue durée signifient que nous gérons le runtime : contrôles de santé, autoscaling, compromis entre démarrage à froid et pool chaud, IAM scoping, et rotation d'astreinte pour un service qui fonctionne par à -coups. Lambda nous offre une isolation par invocation et un coût d'inactivité nul pour une charge de travail intrinsèquement par à -coups. Nous n'étions pas prêts à y renoncer pour résoudre un seul goulot d'étranglement.
- "Parallélisez les écritures MediaLive." MediaLive sérialise les mises à jour de planification par canal. Les appels BatchUpdateSchedule concurrents sur le même canal entrent en conflit sur la chronologie des actions et sont rejetés. Le seul parallélisme légitime est au sein d'un lot, et non entre les lots.
- "Laissez-le planter et laissez l'opérateur réessayer." C'est la pire option. Lorsqu'un déploiement est interrompu au programme N, le canal est dans un état que personne ne peut décrire depuis l'interface utilisateur. Les opérateurs n'obtiennent pas de différences ; ils obtiennent une boîte noire et un canal en direct avec des lacunes. Le système doit soit tout déployer, soit déployer des résultats partiels avec un statut par programme sur lequel l'opérateur peut agir.
Le levier qui nous restait était la nature même du travail : moins d'écritures, de plus grande taille, exécutées en série, avec un mécanisme de secours qui se dégrade en granularité par ligne uniquement lorsqu'un lot est rejeté.
Notre Solution
Déplacer le coût hors de Lambda avant le début de la boucle, fusionner les écritures MediaLive à l'intérieur de la boucle, et ne se dégrader à une soumission par programme que lorsqu'un lot échoue. Trois idées, dans cet ordre — et une décision délibérée selon laquelle la limite de 15 minutes est acceptable pour les tailles de catalogue que nous déployons actuellement. Lorsque les listes de lecture dépassent une seule invocation, la réponse n'est pas un Lambda plus grand ; c'est un runtime différent.

Architecture
- Backend NestJS (schedule.service.ts) — préchauffe le déploiement : un aller-retour Mongo pour toutes les vidéos uniques, un pour tous les marqueurs publicitaires, puis un ffprobe parallèle sur les URL vidéo uniques avec les résultats mis en cache pour la boucle d'enrichissement.
- Orchestrateur Lambda (fastChannel-lambda-fun/index.js) — gère la boucle de traitement par lots, la limitation par lot, le mécanisme de secours par programme et le nettoyage des orphelins.
- MediaLive BatchUpdateScheduleCommand — la seule surface d'écriture. Chaque action — changement d'entrée, filigrane, SCTE-35, insertion publicitaire — passe par elle.
- MongoDB — la source de vérité pour les programmes, vidéos et marqueurs publicitaires ; jamais interrogée dans la boucle interne.
- scheduleResults map — statut scheduled / error par programme renvoyé au backend afin que l'opérateur obtienne une différence, et non une trace de pile.
Décisions d'ingénierie clés
1. Préchauffer les éléments lents avant que la boucle ne s'exécute. Le code original effectuait une recherche Mongo par planification et un appel ffprobe par planification à l'intérieur de la boucle d'enrichissement — un classique N+1 payé deux fois. Le backend actuel récupère en masse chaque vidéo unique et marqueur publicitaire en un seul aller-retour chacun, puis exécute ffprobe en parallèle sur des URL uniques et stocke le résultat dans un cache de résolution. La boucle interne devient un succès de cache. La latence de ffprobe n'est payée qu'une seule fois par URL unique, et non en série dans la boucle.
2. Regrouper les écritures MediaLive en lots d'environ 25. À l'intérieur de l'orchestrateur, les actions s'accumulent en une seule BatchUpdateScheduleCommand jusqu'à ce que 200 actions soient en file d'attente ou que le dernier programme soit atteint. Étant donné que chaque programme produit environ 7 à 8 actions, les lots regroupent naturellement environ 25 programmes chacun. La limite de 200 actions a été choisie de manière conservatrice pour rester bien en dessous des limites de charge utile par requête de MediaLive et réduire les chances qu'un lot soit rejeté en raison de sa taille — suffisamment grande pour amortir le coût réseau, suffisamment petite pour que le mécanisme de secours par programme (décision suivante) soit peu coûteux lorsqu'il doit être exécuté. Un aller-retour réseau remplace vingt-cinq. La limitation de 200 ms qui se situait auparavant entre chaque programme se situe désormais entre chaque lot.
3. Traitement par lots pour la vitesse, mécanisme de secours pour la correction. Le regroupement n'est sûr que si un seul mauvais programme n'infecte pas les vingt-quatre autres de son lot. Lorsque submitProgramBatch échoue (throws), le bloc catch invoque retryBatchAsIndividuals, qui resoumet chaque programme du lot échoué en tant que sa propre BatchUpdateScheduleCommand, enregistre le status: 'scheduled' ou status: 'error' par programme, attend 200 ms entre chaque tentative, et ré-ancre la chronologie des actions après chaque succès partiel. Le chemin rapide est traité par lots. Le chemin de récupération est granulaire. L'opérateur obtient une différence par programme dans tous les cas.
4. Supprimer les orphelins à la fin, ne pas les empêcher en cours de vol. sweepIncompleteProgramGroups s'exécute une fois à la fin du déploiement et supprime tout groupe d'actions qui n'a pas atteint un état terminal propre. Nous ne cherchons délibérément pas à maintenir la cohérence interne du canal pendant la boucle — cela impliquerait un chemin de restauration qui devrait lui-même s'intégrer dans le budget de 15 minutes. Le nettoyage est un balayage unique, pas une transaction.
5. N'agrandissez pas le Lambda ; remplacez-le lorsque le catalogue le fait. Pour tout ce que nous déployons aujourd'hui, le chemin d'invocation unique se termine bien dans le budget. La mise à l'échelle horizontale honnête est le découpage par Step Functions — partitionner la liste de lecture, exécuter les blocs comme des machines d'état parallèles, réassembler. C'est la voie pour les listes de lecture de plus d'un mois et de 6 mois. C'est conçu, non déployé. L'appeler la prochaine étape est plus utile que de prétendre qu'il est déjà en cours d'exécution.
Résultats
- Lors de tests internes, un déploiement de 195 programmes a été achevé en ~44 secondes — contre la base de référence de plusieurs minutes, programme par programme.
- L'objectif documenté pour une liste de lecture de 360 programmes (environ 1 mois) est de ≤90 secondes, confortablement dans le timeout backend de 615 secondes et la limite de 15 minutes de Lambda.
- Un programme défectueux dans un lot n'interrompt plus les 24 autres. L'opérateur reçoit une carte d'état par programme après chaque déploiement.
- Les parties lentes de la requête — les recherches Mongo et ffprobe — sont payées une seule fois par ressource unique, et non une fois par entrée de planification.
- La limite de 15 minutes a cessé d'être le facteur limitant pour les tailles de catalogue que nous livrons réellement. Lorsqu'elle le redeviendra, le découpage par Step Functions sera la réponse, et non un Lambda plus grand.
Pile Technologique : AWS Lambda · AWS MediaLive · NestJS · MongoDB · Node 18 · ffprobe · TypeScript · AWS SDK v3

