Meilleures pratiques pour les fonctions durables de Lambda - AWS Lambda

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Meilleures pratiques pour les fonctions durables de Lambda

Les fonctions durables utilisent un modèle d'exécution basé sur le replay qui nécessite des modèles différents de ceux des fonctions Lambda traditionnelles. Suivez ces meilleures pratiques pour créer des flux de travail fiables et rentables.

Écrire du code déterministe

Pendant la réexécution, votre fonction s'exécute depuis le début et doit suivre le même chemin d'exécution que l'exécution initiale. Le code en dehors des opérations durables doit être déterministe et produire les mêmes résultats avec les mêmes entrées.

Répartissez les opérations non déterministes par étapes :

  • Génération de nombres aléatoires et UUIDs

  • Heure actuelle ou horodatage

  • Appels d'API externes et requêtes de base de données

  • Opérations du système de fichiers

TypeScript
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js'; import { randomUUID } from 'crypto'; export const handler = withDurableExecution( async (event: any, context: DurableContext) => { // Generate transaction ID inside a step const transactionId = await context.step('generate-transaction-id', async () => { return randomUUID(); }); // Use the same ID throughout execution, even during replay const payment = await context.step('process-payment', async () => { return processPayment(event.amount, transactionId); }); return { statusCode: 200, transactionId, payment }; } );
Python
from aws_durable_execution_sdk_python import durable_execution, DurableContext import uuid @durable_execution def handler(event, context: DurableContext): # Generate transaction ID inside a step transaction_id = context.step( lambda _: str(uuid.uuid4()), name='generate-transaction-id' ) # Use the same ID throughout execution, even during replay payment = context.step( lambda _: process_payment(event['amount'], transaction_id), name='process-payment' ) return {'statusCode': 200, 'transactionId': transaction_id, 'payment': payment}
Important

N'utilisez pas de variables globales ou de fermetures pour partager l'état entre les étapes. Transmettez les données via les valeurs de retour. L'état global se brise pendant la rediffusion car les étapes renvoient les résultats mis en cache mais les variables globales sont réinitialisées.

Évitez les mutations de fermeture : les variables capturées lors des fermetures peuvent perdre des mutations pendant la rediffusion. Les étapes renvoient les résultats mis en cache, mais les mises à jour des variables en dehors de l'étape ne sont pas reproduites.

TypeScript
// ❌ WRONG: Mutations lost on replay export const handler = withDurableExecution(async (event, context) => { let total = 0; for (const item of items) { await context.step(async () => { total += item.price; // ⚠️ Mutation lost on replay! return saveItem(item); }); } return { total }; // Inconsistent value! }); // ✅ CORRECT: Accumulate with return values export const handler = withDurableExecution(async (event, context) => { let total = 0; for (const item of items) { total = await context.step(async () => { const newTotal = total + item.price; await saveItem(item); return newTotal; // Return updated value }); } return { total }; // Consistent! }); // ✅ EVEN BETTER: Use map for parallel processing export const handler = withDurableExecution(async (event, context) => { const results = await context.map( items, async (ctx, item) => { await ctx.step(async () => saveItem(item)); return item.price; } ); const total = results.getResults().reduce((sum, price) => sum + price, 0); return { total }; });
Python
# ❌ WRONG: Mutations lost on replay @durable_execution def handler(event, context: DurableContext): total = 0 for item in items: context.step( lambda _: save_item_and_mutate(item, total), # ⚠️ Mutation lost on replay! name=f'save-item-{item["id"]}' ) return {'total': total} # Inconsistent value! # ✅ CORRECT: Accumulate with return values @durable_execution def handler(event, context: DurableContext): total = 0 for item in items: total = context.step( lambda _: save_item_and_return_total(item, total), name=f'save-item-{item["id"]}' ) return {'total': total} # Consistent! # ✅ EVEN BETTER: Use map for parallel processing @durable_execution def handler(event, context: DurableContext): def process_item(ctx, item): ctx.step(lambda _: save_item(item)) return item['price'] results = context.map(items, process_item) total = sum(results.get_results()) return {'total': total}

Conception axée sur l'idempuissance

Les opérations peuvent être exécutées plusieurs fois en raison de nouvelles tentatives ou de rediffusions. Les opérations non idempotentes entraînent des effets secondaires dupliqués, comme le fait de facturer deux fois les clients ou d'envoyer plusieurs e-mails.

Utilisez des jetons d'idempuissance : générez des jetons en quelques étapes et incluez-les dans les appels d'API externes pour éviter les opérations dupliquées.

TypeScript
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js'; export const handler = withDurableExecution( async (event: any, context: DurableContext) => { // Generate idempotency token once const idempotencyToken = await context.step('generate-idempotency-token', async () => { return crypto.randomUUID(); }); // Use token to prevent duplicate charges const charge = await context.step('charge-payment', async () => { return paymentService.charge({ amount: event.amount, cardToken: event.cardToken, idempotencyKey: idempotencyToken }); }); return { statusCode: 200, charge }; } );
Python
from aws_durable_execution_sdk_python import durable_execution, DurableContext import uuid @durable_execution def handler(event, context: DurableContext): # Generate idempotency token once idempotency_token = context.step( lambda _: str(uuid.uuid4()), name='generate-idempotency-token' ) # Use token to prevent duplicate charges def charge_payment(_): return payment_service.charge( amount=event['amount'], card_token=event['cardToken'], idempotency_key=idempotency_token ) charge = context.step(charge_payment, name='charge-payment') return {'statusCode': 200, 'charge': charge}

Utilisez la at-most-once sémantique : pour les opérations critiques qui ne doivent jamais être dupliquées (transactions financières, déductions d'inventaire), configurez at-most-once le mode d'exécution.

TypeScript
// Critical operation that must not duplicate await context.step('deduct-inventory', async () => { return inventoryService.deduct(event.productId, event.quantity); }, { executionMode: 'AT_MOST_ONCE_PER_RETRY' });
Python
# Critical operation that must not duplicate context.step( lambda _: inventory_service.deduct(event['productId'], event['quantity']), name='deduct-inventory', config=StepConfig(execution_mode='AT_MOST_ONCE_PER_RETRY') )

Identité de la base de données : utilisez check-before-write des modèles, des mises à jour conditionnelles ou des opérations de modification pour éviter les doublons d'enregistrements.

Gérez efficacement l'état

Chaque point de contrôle enregistre l'état dans un stockage permanent. Les objets d'état volumineux augmentent les coûts, ralentissent le pointage des points de contrôle et ont un impact sur les performances. Stockez uniquement les données essentielles de coordination du flux de travail.

Maintenez l'état au minimum :

  • Magasin IDs et références, pas des objets complets

  • Récupérez des données détaillées en quelques étapes, selon les besoins

  • Utilisez Amazon S3 ou DynamoDB pour les données volumineuses, transmettez les références dans l'état

  • Évitez de transmettre de grosses charges utiles entre les étapes

TypeScript
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js'; export const handler = withDurableExecution( async (event: any, context: DurableContext) => { // Store only the order ID, not the full order object const orderId = event.orderId; // Fetch data within each step as needed await context.step('validate-order', async () => { const order = await orderService.getOrder(orderId); return validateOrder(order); }); await context.step('process-payment', async () => { const order = await orderService.getOrder(orderId); return processPayment(order); }); return { statusCode: 200, orderId }; } );
Python
from aws_durable_execution_sdk_python import durable_execution, DurableContext @durable_execution def handler(event, context: DurableContext): # Store only the order ID, not the full order object order_id = event['orderId'] # Fetch data within each step as needed context.step( lambda _: validate_order(order_service.get_order(order_id)), name='validate-order' ) context.step( lambda _: process_payment(order_service.get_order(order_id)), name='process-payment' ) return {'statusCode': 200, 'orderId': order_id}

Concevez des étapes efficaces

Les étapes sont l'unité de travail fondamentale des fonctions durables. Des étapes bien conçues facilitent la compréhension, le débogage et la maintenance des flux de travail.

Principes de conception des étapes :

  • Utilisez des noms descriptifs : les noms tels que validate-order « step1 instead » facilitent la compréhension des journaux et des erreurs

  • Conservez les noms statiques : n'utilisez pas de noms dynamiques avec des horodatages ou des valeurs aléatoires. Les noms des étapes doivent être déterministes pour la rediffusion

  • Équilibrez la granularité : divisez les opérations complexes en étapes ciblées, mais évitez les petites étapes excessives qui augmentent la charge des points de contrôle

  • Opérations liées au groupe - Les opérations qui devraient réussir ou échouer ensemble appartiennent à la même étape

Utilisez efficacement les opérations d'attente

Les opérations d'attente suspendent l'exécution sans consommer de ressources ni entraîner de coûts. Utilisez-les au lieu de faire fonctionner Lambda.

Attentes basées sur le temps : à utiliser context.wait() pour les retards au lieu de setTimeout ou. sleep

Rappels externes : à utiliser context.waitForCallback() lorsque vous attendez des systèmes externes. Définissez toujours des délais d'attente pour éviter les temps d'attente indéfinis.

Sondage : context.waitForCondition() à utiliser avec un recul exponentiel pour interroger les services externes sans les surcharger.

TypeScript
// Wait 24 hours without cost await context.wait({ seconds: 86400 }); // Wait for external callback with timeout const result = await context.waitForCallback( 'external-job', async (callbackId) => { await externalService.submitJob({ data: event.data, webhookUrl: `https://api.example.com/callbacks/${callbackId}` }); }, { timeout: { seconds: 3600 } } );
Python
# Wait 24 hours without cost context.wait(86400) # Wait for external callback with timeout result = context.wait_for_callback( lambda callback_id: external_service.submit_job( data=event['data'], webhook_url=f'https://api.example.com/callbacks/{callback_id}' ), name='external-job', config=WaitForCallbackConfig(timeout_seconds=3600) )

Considérations supplémentaires

Gestion des erreurs : réessayez les échecs transitoires tels que les délais d'expiration du réseau et les limites de débit. Ne réessayez pas en cas d'échec permanent, comme une saisie non valide ou une erreur d'authentification. Configurez des stratégies de nouvelles tentatives avec des taux de tentatives et d'interruption maximaux appropriés. Pour des exemples détaillés, voir Gestion des erreurs et nouvelles tentatives.

Performances : minimisez la taille des points de contrôle en stockant des références plutôt que des charges utiles complètes. Utilisez context.parallel() et context.map() pour exécuter simultanément des opérations indépendantes. Opérations liées au batch pour réduire la surcharge des points de contrôle.

Gestion des versions : invoquez des fonctions avec des numéros de version ou des alias pour attribuer les exécutions à des versions de code spécifiques. Assurez-vous que les nouvelles versions du code peuvent gérer l'état des anciennes versions. Ne renommez pas les étapes et ne modifiez pas leur comportement de manière à interrompre la rediffusion.

Sérialisation : utilisez des types compatibles JSON pour les entrées et les résultats des opérations. Convertissez les dates en chaînes ISO et les objets personnalisés en objets simples avant de les transmettre à des opérations durables.

Surveillance : activez la journalisation structurée avec les noms d'exécution IDs et d'étape. Configurez CloudWatch des alarmes pour les taux d'erreur et la durée d'exécution. Utilisez le traçage pour identifier les goulots d'étranglement. Pour obtenir des instructions détaillées, consultez la section Surveillance et débogage.

Tests : testez le chemin heureux, la gestion des erreurs et le comportement des rediffusions. Testez des scénarios de temporisation pour les rappels et les temps d'attente. Utilisez des tests locaux pour réduire le temps d'itération. Pour obtenir des instructions détaillées, voir Tester des fonctions durables.

Erreurs courantes à éviter : n'imbriquez pas les context.step() appels, utilisez plutôt des contextes enfantins. Enveloppez les opérations non déterministes par étapes. Définissez toujours des délais d'expiration pour les rappels. Équilibrez la granularité des étapes avec la surcharge des points de contrôle. Stockez les références plutôt que les objets volumineux dans l'état.

Ressources supplémentaires