Prácticas recomendadas para las funciones duraderas de Lambda - AWS Lambda

Prácticas recomendadas para las funciones duraderas de Lambda

Las funciones duraderas utilizan un modelo de ejecución basado en la reproducción que requiere patrones diferentes a los de las funciones de Lambda tradicionales. Siga estas prácticas recomendadas para crear flujos de trabajo fiables y rentables.

Escritura de código determinista

Durante la reproducción, la función se ejecuta desde el principio y debe seguir la misma ruta de ejecución que la ejecución original. El código ajeno a las operaciones duraderas debe ser determinista y producir los mismos resultados con las mismas entradas.

Encapsule las operaciones no deterministas en pasos:

  • Generación de números al azar y UUID

  • Hora o marcas de tiempo actuales

  • Llamadas a la API externas y consultas a las bases de datos

  • Operaciones del sistema de archivos

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}
Importante

No utilice variables globales ni cierre para compartir el estado entre los pasos. Transfiera los datos a través de los valores devueltos. El estado global se interrumpe durante la reproducción porque los pasos devuelven resultados en caché, pero las variables globales se restablecen.

Evite las mutaciones de cierre: las variables capturadas en los cierres pueden perder mutaciones durante la reproducción. Los pasos devuelven los resultados guardados en caché, pero las actualizaciones de variables ajenas al paso no se reproducen.

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}

Diseño de idempotencia

Las operaciones pueden ejecutarse varias veces debido a los reintentos o a la reproducción. Las operaciones no idempotentes provocan efectos secundarios duplicados, como cobrar dos veces a los clientes o enviar varios correos electrónicos.

Utilice tokens de idempotencia: genere tokens dentro de los pasos e inclúyelos en las llamadas a la API externas para evitar la duplicación de operaciones.

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}

Utilice la semántica de como máximo una vez: para las operaciones críticas que nunca deben duplicarse (transacciones financieras, deducciones de inventario), configure el modo de ejecución como máximo una vez.

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') )

Idempotencia de la base de datos: utilice patrones de verificación antes de escribir, actualizaciones condicionales u operaciones de modificación para evitar la duplicación de registros.

Administración del estado de manera eficiente

Cada punto de control guarda el estado en el almacenamiento persistente. Los objetos de gran tamaño aumentan los costos, ralentizan la creación de puntos de control y afectan el rendimiento. Almacene solo los datos esenciales de coordinación del flujo de trabajo.

Mantenga el estado mínimo:

  • Almacene los identificadores y las referencias, no los objetos completos.

  • Obtenga datos detallados dentro de los pasos, según sea necesario.

  • Utilice Amazon S3 o DynamoDB para datos de gran tamaño y pase las referencias en estado.

  • Evite pasar grandes cargas útiles entre pasos.

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}

Diseñe pasos efectivos.

Los pasos son la unidad de trabajo fundamental de las funciones duraderas. Los pasos bien diseñados facilitan la comprensión, la depuración y el mantenimiento de los flujos de trabajo.

Principios de diseño de pasos:

  • Utilice nombres descriptivos: los nombres como validate-order en lugar de step1 facilita la comprensión de los registros y los errores.

  • Mantenga los nombres estáticos: no utilice nombres dinámicos con marcas de tiempo o valores al azar. Los nombres de los pasos deben ser deterministas para poder reproducirlos.

  • Equilibre la granularidad: divida las operaciones complejas en pasos específicos, pero evite los pasos demasiado pequeños que aumentan la sobrecarga de los puntos de control.

  • Operaciones relacionadas por grupo: las operaciones que deberían tener éxito o fracasar juntas pertenecen al mismo paso.

Utilice las operaciones de espera de forma eficiente.

Las operaciones de espera suspenden la ejecución sin consumir recursos ni incurrir en costos. Úselos en lugar de mantener la ejecución de Lambda.

Esperas basadas en el tiempo: utilice context.wait() para los retrasos en lugar de setTimeout o sleep.

Devoluciones de llamada externas: utilice context.waitForCallback() cuando se espera por sistemas externos. Establezca siempre tiempos de espera para evitar esperas indefinidas.

Sondeo: utilice context.waitForCondition() con un retroceso exponencial para sondear los servicios externos sin sobrecargarlos.

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) )

Consideraciones adicionales

Gestión de errores: reintente errores transitorios, como los tiempos de espera de la red y los límites de tasa. No reintente con errores permanentes, como errores de autenticación o de entrada inválida. Configure estrategias de reintentos con el máximo de intentos y las tasas de retroceso adecuadas. Para ver ejemplos detallados, consulte Gestión de errores y reintentos.

Rendimiento: minimice el tamaño de los puntos de control; para ello, almacene las referencias en lugar de las cargas útiles completas. Utilice context.parallel() y context.map() para ejecutar operaciones independientes de forma simultánea. Operaciones relacionadas con lotes para reducir la sobrecarga de los puntos de control.

Control de versiones: invoque funciones con alias o números de versión para fijar las ejecuciones a versiones de código específicas. Asegúrese de que las nuevas versiones del código puedan gestionar el estado de las versiones anteriores. No cambie el nombre de los pasos ni su comportamiento de forma que interfiera con la reproducción.

Serialización: utilice tipos compatibles con JSON para las entradas y los resultados de las operaciones. Convierta las fechas en cadenas ISO y los objetos personalizados en objetos simples antes de pasarlos a operaciones duraderas.

Supervisión: habilite el registro estructurado con los identificadores de ejecución y los nombres de los pasos. Configuración de las alarmas de CloudWatch para conocer las tasas de error y la duración de la ejecución. Uso de la trazabilidad para identificar cuellos de botella. Para obtener una guía detallada, consulte Supervisión y depuración.

Pruebas: pruebe el flujo ideal, la gestión de errores y el comportamiento de reproducción. Pruebe los escenarios de tiempo de espera para detectar devoluciones de llamada y esperas. Utilice pruebas locales para reducir el tiempo de iteración. Para obtener una guía detallada, consulte Prueba de funciones duraderas.

Errores comunes que debe evitar: no anide las llamadas de context.step(); en cambio, utilice contextos secundarios. Encapsule las operaciones no deterministas en pasos. Establezca siempre tiempos de espera para las devoluciones de llamada. Equilibre la granularidad de los pasos con la sobrecarga de los puntos de control. Guarde referencias en lugar de objetos grandes en su estado.

Recursos adicionales