Práticas recomendadas para funções duráveis do Lambda - AWS Lambda

Práticas recomendadas para funções duráveis do Lambda

As funções duráveis usam um modelo de execução baseado em reprodução que exige padrões diferentes das funções do Lambda tradicionais. Siga essas melhores práticas para criar fluxos de trabalho confiáveis e econômicos.

Escreva código determinístico

Durante a reprodução, sua função é executada desde o início e deve seguir o mesmo caminho de execução da execução original. O código fora das operações duráveis deve ser determinístico, produzindo os mesmos resultados com as mesmas entradas.

Encapsule as operações não determinísticas em etapas:

  • Geração de números aleatórios e UUIDs

  • Hora atual ou timestamps

  • Chamadas de API externas e consultas de banco de dados

  • Operações do sistema de arquivos

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

Não use variáveis globais ou fechamentos para compartilhar o estado entre as etapas. Passe dados por meio de valores de retorno. O estado global é interrompido durante a reprodução, pois as etapas retornam resultados em cache, mas as variáveis globais são redefinidas.

Evite mutações de encerramento: as variáveis capturadas em fechamentos podem perder mutações durante a reprodução. As etapas retornam resultados em cache, mas as atualizações de variáveis fora da etapa não são reproduzidas.

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}

Design para idempotência

As operações podem ser executadas várias vezes devido a novas tentativas ou repetições. Operações não idempotentes causam efeitos colaterais duplicados, como cobrar duas vezes dos clientes ou enviar vários e-mails.

Use tokens de idempotência: gere tokens dentro das etapas e inclua-os em chamadas externas de API para evitar operações duplicadas.

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}

Use a semântica de no máximo uma vez: para operações críticas que nunca devem ser duplicadas (transações financeiras, deduções de estoque), configure o modo de execução de no máximo uma 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') )

Idempotência do banco de dados: use padrões de verificação antes da gravação, atualizações condicionais ou operações de atualização para evitar registros duplicados.

Gerencie o estado de forma eficiente

Cada ponto de verificação salva o estado no armazenamento persistente. Objetos de estado grandes aumentam os custos, diminuem o controle e afetam a performance. Armazene somente dados essenciais de coordenação do fluxo de trabalho.

Mantenha o estado mínimo:

  • Armazene IDs e referências, não objetos completos

  • Obtenha dados detalhados em etapas, conforme necessário

  • Use o Amazon S3 ou o DynamoDB para dados grandes, passe referências no estado

  • Evite passar grandes cargas úteis entre as etapas

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}

Projete etapas eficazes

As etapas são a unidade fundamental de trabalho em funções duráveis. Etapas bem projetadas facilitam a compreensão, a depuração e a manutenção dos fluxos de trabalho.

Princípios do projeto de etapas

  • Use nomes descritivos: nomes como validate-order em vez de step1 tornam os logs e os erros mais fáceis de entender

  • Mantenha os nomes estáticos: não use nomes dinâmicos com timestamps ou valores aleatórios. Os nomes das etapas devem ser determinísticos para a reprodução

  • Equilibre a granularidade: divida operações complexas em etapas focadas, mas evite pequenas etapas excessivas que aumentem a sobrecarga do ponto de verificação

  • Operações relacionadas a grupos: as operações que devem ter êxito ou falhar juntas pertencem à mesma etapa

Use as operações de espera com eficiência

As operações de espera suspendem a execução sem consumir recursos ou incorrer em custos. Use-as em vez de manter o Lambda em execução.

Esperas com base no tempo: use context.wait() para retardos em vez de setTimeout ou sleep.

Retornos de chamada externos: use context.waitForCallback() ao esperar por sistemas externos. Sempre defina tempos limite para evitar esperas indefinidas.

Sondagem: use context.waitForCondition() com recuo exponencial para sondar serviços externos sem sobrecarregá-los.

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

Considerações adicionais

Tratamento de erros: faça uma nova tentativa em falhas transitórias, como tempos limite de rede e limites de taxa. Não tente novamente falhas permanentes, como entrada inválida ou erros de autenticação. Configure estratégias de novas tentativas com o máximo de tentativas e taxas de recuo apropriadas. Para obter exemplos detalhados, consulte Tratamento de erros e novas tentativas.

Performance: minimize o tamanho do ponto de verificação armazenando referências em vez de cargas úteis completas. Use context.parallel() e context.map() para executar operações independentes simultaneamente. Execute operações relacionadas em lotes para reduzir a sobrecarga dos pontos de verificação.

Versionamento: invoque funções com números de versão ou aliases para fixar execuções em versões de código específicas. Certifique-se de que as novas versões do código possam tratar o estado das versões mais antigas. Não renomeie etapas nem altere seu comportamento de forma a interromper a reprodução.

Serialização: use tipos compatíveis com JSON para entradas e resultados da operação. Converta datas em sequências ISO e objetos personalizados em objetos simples antes de passá-los para operações duráveis.

Monitoramento: habilite o registro em log estruturado com IDs de execução e nomes de etapas. Configure os alarmes do CloudWatch para as taxas de erro e a duração da execução. Use rastreamento para identificar gargalos. Para obter orientação detalhada, consulte Monitoramento e depuração.

Testes: teste o caminho ideal, o tratamento de erros e o comportamento de reprodução. Teste cenários de tempo limite para retornos de chamada e esperas. Use testes locais para reduzir o tempo de iteração. Para obter orientação detalhada, consulte Testes de funções duráveis.

Erros comuns a serem evitados: não aninhe chamadas context.step(), use contextos secundários. Encapsule as operações não determinísticas em etapas. Sempre defina tempos limite para retornos de chamada. Equilibre a granularidade das etapas com a sobrecarga do ponto de verificação. Armazene referências em vez de objetos grandes no estado.

Recursos adicionais