From 8745b136e6b1d15de51db507ff6f5f8b01758487 Mon Sep 17 00:00:00 2001 From: Sahithi Ginjupalli Date: Wed, 24 Dec 2025 14:51:36 +0530 Subject: [PATCH] Publishing Durable Function ESM and Chaining Pattern --- lambda-durable-esm-and-chaining/README.md | 144 +++++++++++++++++ .../architecture.png | Bin 0 -> 45737 bytes .../example-pattern.json | 67 ++++++++ .../src/durable_pipeline/handler.py | 100 ++++++++++++ .../src/durable_pipeline/requirements.txt | 1 + .../src/storage/handler.py | 47 ++++++ .../src/transformation/handler.py | 22 +++ .../src/validation/handler.py | 22 +++ lambda-durable-esm-and-chaining/template.yaml | 148 ++++++++++++++++++ 9 files changed, 551 insertions(+) create mode 100644 lambda-durable-esm-and-chaining/README.md create mode 100644 lambda-durable-esm-and-chaining/architecture.png create mode 100644 lambda-durable-esm-and-chaining/example-pattern.json create mode 100644 lambda-durable-esm-and-chaining/src/durable_pipeline/handler.py create mode 100644 lambda-durable-esm-and-chaining/src/durable_pipeline/requirements.txt create mode 100644 lambda-durable-esm-and-chaining/src/storage/handler.py create mode 100644 lambda-durable-esm-and-chaining/src/transformation/handler.py create mode 100644 lambda-durable-esm-and-chaining/src/validation/handler.py create mode 100644 lambda-durable-esm-and-chaining/template.yaml diff --git a/lambda-durable-esm-and-chaining/README.md b/lambda-durable-esm-and-chaining/README.md new file mode 100644 index 000000000..d3a45816c --- /dev/null +++ b/lambda-durable-esm-and-chaining/README.md @@ -0,0 +1,144 @@ +# Event-Driven Data Pipeline with Lambda Durable Functions + +This serverless pattern demonstrates how to build an event-driven data processing pipeline using AWS Lambda Durable Functions with **direct SQS Event Source Mapping** and Lambda invoke chaining. + +## How It Works + +This pattern demonstrates an event-driven data processing pipeline using AWS Lambda Durable Functions with direct SQS Event Source Mapping. When a message arrives in the SQS queue, it directly triggers the durable function (no intermediary Lambda needed). The durable function then orchestrates a series of specialized processing steps using Lambda invoke chaining - first validating the incoming data, then transforming it (converting data_source to uppercase), and finally storing the processed results in DynamoDB. Throughout this process, the durable function automatically creates checkpoints, enabling fault-tolerant execution that can recover from failures without losing progress. The entire pipeline operates within the 15-minute ESM execution limit, making it ideal for reliable batch processing workflows. + +## Architecture Overview + +The pattern showcases two key Durable Functions capabilities: +1. **Direct Event Source Mapping**: SQS directly triggers the durable function (15-minute limit) +2. **Lambda Invoke Chaining**: Orchestrates specialized processing functions + +![Architecture Diagram](architecture-diagram.png) + +## Key Features + +- **Direct ESM Integration**: No intermediary function needed +- **15-minute execution constraint**: Demonstrates ESM time limits +- **Fault-tolerant processing**: Automatic checkpointing and recovery +- **Microservices coordination**: Chains specialized Lambda functions +- **Batch processing**: Handles multiple SQS records per invocation +- **Simple storage**: Uses DynamoDB for processed data + +## Important ESM Constraints + +⚠️ **15-Minute Execution Limit**: When using Event Source Mapping with Durable Functions, the total execution time cannot exceed 15 minutes. This includes: +- All processing steps +- Function invocations +- No long wait operations + +## Use Cases + +- ETL pipelines with validation and transformation +- Event-driven microservices orchestration +- Batch processing with fault tolerance +- Data processing workflows requiring checkpointing + +## Prerequisites + +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) configured with appropriate permissions +- [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) latest version installed +- [Python 3.14](https://www.python.org/downloads/release/python-3140/) runtime installed + +## Deployment + +1. **Build the application**: + ```bash + sam build + ``` + +2. **Deploy to AWS**: + ```bash + sam deploy --guided + ``` + + Note the outputs after deployment: + - `DataProcessingQueueUrl`: Use this for `` + - `ProcessedDataTable`: Use this for `` + +3. **Test the pipeline**: + ```bash + # Send a test message to SQS + aws sqs send-message \ + --queue-url \ + --message-body '{"data_source": "test.csv", "processing_type": "standard"}' + ``` + +4. **Verify successful processing**: + ```bash + # Check if data was processed and stored in DynamoDB + aws dynamodb scan --table-name --query 'Items[*]' + ``` + + **Success indicators:** + - You should see at least one item in the DynamoDB table + - Original input data: `"data_source": "test.csv"` + - Transformed data: `"data_source": "TEST.CSV"` (uppercase transformation applied) + - Execution tracking with unique `execution_id` + - Timestamps showing when data was processed and stored + + This confirms the entire pipeline worked: SQS → Durable Function → Validation → Transformation → Storage → DynamoDB + +## Components + +### 1. Durable Pipeline Function (`src/durable_pipeline/`) +- **Direct SQS Event Source Mapping**: Receives SQS events directly +- **15-minute execution limit**: Must complete all processing within ESM constraints +- **Batch processing**: Handles multiple SQS records per invocation +- **Lambda invoke chaining**: Orchestrates validation, transformation, and storage +- **Automatic checkpointing**: Recovers from failures without losing progress + +### 2. Specialized Processing Functions +- **Validation Function**: Simple data validation checks +- **Transformation Function**: Basic data transformation +- **Storage Function**: Persists processed data to DynamoDB + +## Monitoring + +- CloudWatch Logs for execution tracking +- DynamoDB table for processed data +- SQS DLQ for failed messages + +## Configuration + +Key environment variables: +- `ENVIRONMENT`: Deployment environment (dev/prod) +- `PROCESSED_DATA_TABLE`: DynamoDB table for processed data +- `VALIDATION_FUNCTION_ARN`: ARN of validation function +- `TRANSFORMATION_FUNCTION_ARN`: ARN of transformation function +- `STORAGE_FUNCTION_ARN`: ARN of storage function + +## ESM-Specific Considerations + +- **Execution Timeout**: Set to 900 seconds (15 minutes) maximum +- **Batch Size**: Configured for optimal processing (5 records) +- **Error Handling**: Uses SQS DLQ for failed batches +- **Efficient Processing**: Optimized for speed to stay within time limits + +## Error Handling + +- Automatic retries with exponential backoff +- Dead Letter Queue for failed messages +- Partial batch failure support +- Checkpoint-based recovery + +## Cost Optimization + +- Pay only for active compute time +- Efficient batch processing +- Automatic scaling based on queue depth + +## Cleanup + +```bash +sam delete +``` + +## Learn More + +- [AWS Lambda Durable Functions Documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) +- [Event Source Mappings with Durable Functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-invoking-esm.html) +- [Lambda Invoke Chaining](https://docs.aws.amazon.com/lambda/latest/dg/durable-examples.html#durable-examples-chained-invocations) diff --git a/lambda-durable-esm-and-chaining/architecture.png b/lambda-durable-esm-and-chaining/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..3971b1d18734ae1028c3b60d70a9a841dcc9d25f GIT binary patch literal 45737 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 aALDk=RBF#)+X`{OkGA@4wF*_6r~d=Xc%n-H literal 0 HcmV?d00001 diff --git a/lambda-durable-esm-and-chaining/example-pattern.json b/lambda-durable-esm-and-chaining/example-pattern.json new file mode 100644 index 000000000..23538ec51 --- /dev/null +++ b/lambda-durable-esm-and-chaining/example-pattern.json @@ -0,0 +1,67 @@ +{ + "title": "Event-Driven Data Pipeline with Lambda Durable Functions", + "description": "This serverless pattern demonstrates how to build an event-driven data processing pipeline using AWS Lambda Durable Functions with direct SQS Event Source Mapping and Lambda invoke chaining.", + "language": "Python", + "level": "200", + "framework": "SAM", + "services": ["sqs","lambda", "dynamoDB"], + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates an event-driven data processing pipeline using AWS Lambda Durable Functions with direct SQS Event Source Mapping. When a message arrives in the SQS queue, it directly triggers the durable function (no intermediary Lambda needed). The durable function then orchestrates a series of specialized processing steps using Lambda invoke chaining - first validating the incoming data, then transforming it (converting data_source to uppercase), and finally storing the processed results in DynamoDB. Throughout this process, the durable function automatically creates checkpoints, enabling fault-tolerant execution that can recover from failures without losing progress. The entire pipeline operates within the 15-minute ESM execution limit, making it ideal for reliable batch processing workflows." + ] + }, + "testing": { + "headline": "Testing", + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "headline": "Cleanup", + "text": [ + "Delete the stack: sam delete." + ] + }, + "deploy": { + "text": [ + "sam build", + "sam deploy --guided" + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-esm-and-chaining", + "templateURL":"serverles-patterns/lambda-durable-esm-and-chaining", + "templateFile": "template.yaml", + "projectFolder": "lambda-durable-esm-and-chaining" + } + }, + "resources": { + "headline": "Additional resources", + "bullets": [ + { + "text": "AWS Lambda Durable Functions Documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "Event Source Mappings with Durable Functions", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-invoking-esm.html" + }, + { + "text": "Durbale Function Lambda Invoke Chaining", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-examples.html#durable-examples-chained-invocations" + } + ] + }, + "authors": [ + { + "name": "Sahithi Ginjupalli", + "image": "https://drive.google.com/file/d/1YcKYuGz3LfzSxiwb2lWJfpyi49SbvOSr/view?usp=sharing", + "bio": "Cloud Engineer at AWS with a passion for diving deep into cloud and AI services to build innovative serverless applications.", + "linkedin": "ginjupalli-sahithi-37460a18b", + "twitter": "" + } + ] + } + \ No newline at end of file diff --git a/lambda-durable-esm-and-chaining/src/durable_pipeline/handler.py b/lambda-durable-esm-and-chaining/src/durable_pipeline/handler.py new file mode 100644 index 000000000..ac529e02b --- /dev/null +++ b/lambda-durable-esm-and-chaining/src/durable_pipeline/handler.py @@ -0,0 +1,100 @@ +import json +import os +import boto3 +from datetime import datetime +from typing import Dict, Any, List +from aws_durable_execution_sdk_python import DurableContext, durable_execution + +# Initialize AWS clients +dynamodb = boto3.resource('dynamodb') +lambda_client = boto3.client('lambda') + +@durable_execution +def lambda_handler(event: Dict[str, Any], context: DurableContext) -> Dict[str, Any]: + """ + Main durable pipeline function that processes SQS events directly via ESM. + Demonstrates lambda invoke chaining with checkpointing and recovery. + Limited to 15 minutes total execution time due to ESM constraints. + """ + + # Extract configuration from environment + validation_function_arn = os.environ['VALIDATION_FUNCTION_ARN'] + transformation_function_arn = os.environ['TRANSFORMATION_FUNCTION_ARN'] + storage_function_arn = os.environ['STORAGE_FUNCTION_ARN'] + processed_data_table = os.environ['PROCESSED_DATA_TABLE'] + environment = os.environ.get('ENVIRONMENT', 'dev') + + print(f"Processing SQS batch with {len(event.get('Records', []))} records") + + # Process each SQS record in the batch + batch_results = [] + + for record in event.get('Records', []): + try: + # Extract data from SQS record + message_id = record['messageId'] + data = json.loads(record['body']) + execution_name = f"{environment}-esm-{message_id}" + + print(f"Processing record: {message_id}") + + # Step 1: Validate data by invoking validation function + validation_result = context.invoke( + validation_function_arn, + {'data': data, 'execution_id': execution_name}, + name=f'validate-data-{message_id}' + ) + + if not validation_result.get('is_valid', False): + batch_results.append({ + 'message_id': message_id, + 'status': 'failed', + 'reason': 'validation_failed' + }) + continue + + # Step 2: Transform data by invoking transformation function + transformation_result = context.invoke( + transformation_function_arn, + {'data': data, 'execution_id': execution_name}, + name=f'transform-data-{message_id}' + ) + + # Step 3: Store processed data by invoking storage function + storage_result = context.invoke( + storage_function_arn, + { + 'transformed_data': transformation_result, + 'execution_id': execution_name, + 'original_data': data + }, + name=f'store-data-{message_id}' + ) + + batch_results.append({ + 'message_id': message_id, + 'status': 'completed', + 'execution_id': execution_name + }) + + except Exception as e: + print(f"Error processing record {record.get('messageId', 'unknown')}: {str(e)}") + batch_results.append({ + 'message_id': record.get('messageId', 'unknown'), + 'status': 'error', + 'error': str(e) + }) + + # Return batch processing summary + successful_records = len([r for r in batch_results if r['status'] == 'completed']) + failed_records = len([r for r in batch_results if r['status'] in ['failed', 'error']]) + + return { + 'batch_summary': { + 'total_records': len(batch_results), + 'successful_records': successful_records, + 'failed_records': failed_records + }, + 'record_results': batch_results, + 'processed_at': datetime.utcnow().isoformat() + } diff --git a/lambda-durable-esm-and-chaining/src/durable_pipeline/requirements.txt b/lambda-durable-esm-and-chaining/src/durable_pipeline/requirements.txt new file mode 100644 index 000000000..85d9afabd --- /dev/null +++ b/lambda-durable-esm-and-chaining/src/durable_pipeline/requirements.txt @@ -0,0 +1 @@ +aws-durable-execution-sdk-python diff --git a/lambda-durable-esm-and-chaining/src/storage/handler.py b/lambda-durable-esm-and-chaining/src/storage/handler.py new file mode 100644 index 000000000..e901e3c2c --- /dev/null +++ b/lambda-durable-esm-and-chaining/src/storage/handler.py @@ -0,0 +1,47 @@ +import boto3 +import os +from typing import Dict, Any +from datetime import datetime + +dynamodb = boto3.resource('dynamodb') + +def lambda_handler(event: Dict[str, Any], context) -> Dict[str, Any]: + """Simple data storage function that saves to DynamoDB""" + + transformed_data = event['transformed_data'] + execution_id = event['execution_id'] + original_data = event['original_data'] + + table_name = os.environ['PROCESSED_DATA_TABLE'] + + print(f"Storing processed data for execution: {execution_id}") + + try: + table = dynamodb.Table(table_name) + + # Store processed data in DynamoDB + item = { + 'execution_id': execution_id, + 'original_data': original_data, + 'transformed_data': transformed_data, + 'stored_at': datetime.utcnow().isoformat(), + 'data_source': original_data.get('data_source', 'unknown'), + 'processing_type': original_data.get('processing_type', 'standard') + } + + table.put_item(Item=item) + + return { + 'success': True, + 'execution_id': execution_id, + 'table_name': table_name, + 'stored_at': datetime.utcnow().isoformat() + } + + except Exception as e: + print(f"Error storing data: {str(e)}") + return { + 'success': False, + 'execution_id': execution_id, + 'error': str(e) + } diff --git a/lambda-durable-esm-and-chaining/src/transformation/handler.py b/lambda-durable-esm-and-chaining/src/transformation/handler.py new file mode 100644 index 000000000..2a9d018f3 --- /dev/null +++ b/lambda-durable-esm-and-chaining/src/transformation/handler.py @@ -0,0 +1,22 @@ +from typing import Dict, Any +from datetime import datetime + +def lambda_handler(event: Dict[str, Any], context) -> Dict[str, Any]: + """Simple data transformation function""" + + data = event['data'] + execution_id = event['execution_id'] + + print(f"Transforming data for execution: {execution_id}") + + # Simple transformation: add processing metadata and uppercase data_source + transformed_data = { + 'original_data': data, + 'data_source': data.get('data_source', '').upper(), + 'processing_type': data.get('processing_type', 'standard'), + 'processed_at': datetime.utcnow().isoformat(), + 'execution_id': execution_id, + 'transformation_applied': 'uppercase_data_source' + } + + return transformed_data diff --git a/lambda-durable-esm-and-chaining/src/validation/handler.py b/lambda-durable-esm-and-chaining/src/validation/handler.py new file mode 100644 index 000000000..3b97786d5 --- /dev/null +++ b/lambda-durable-esm-and-chaining/src/validation/handler.py @@ -0,0 +1,22 @@ +from typing import Dict, Any + +def lambda_handler(event: Dict[str, Any], context) -> Dict[str, Any]: + """Simple data validation function""" + + data = event['data'] + execution_id = event['execution_id'] + + print(f"Validating data for execution: {execution_id}") + + # Simple validation: check if required fields exist + required_fields = ['data_source', 'processing_type'] + missing_fields = [field for field in required_fields if not data.get(field)] + + is_valid = len(missing_fields) == 0 + + return { + 'is_valid': is_valid, + 'execution_id': execution_id, + 'missing_fields': missing_fields, + 'data_source': data.get('data_source', 'unknown') + } diff --git a/lambda-durable-esm-and-chaining/template.yaml b/lambda-durable-esm-and-chaining/template.yaml new file mode 100644 index 000000000..ca11770de --- /dev/null +++ b/lambda-durable-esm-and-chaining/template.yaml @@ -0,0 +1,148 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: 'Event-driven data pipeline with Lambda Durable Functions, SQS Event Source Mapping, and invoke chaining' + +Globals: + Function: + Timeout: 900 + MemorySize: 512 + Runtime: python3.14 + +Parameters: + Environment: + Type: String + Default: dev + Description: Environment name + +Resources: + # SQS Queue for incoming data processing requests + DataProcessingQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: !Sub '${Environment}-data-processing-queue' + VisibilityTimeout: 960 + MessageRetentionPeriod: 1209600 + RedrivePolicy: + deadLetterTargetArn: !GetAtt DataProcessingDLQ.Arn + maxReceiveCount: 3 + + # Dead Letter Queue + DataProcessingDLQ: + Type: AWS::SQS::Queue + Properties: + QueueName: !Sub '${Environment}-data-processing-dlq' + + # DynamoDB table for storing processed data + ProcessedDataTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: !Sub '${Environment}-processed-data' + BillingMode: PAY_PER_REQUEST + AttributeDefinitions: + - AttributeName: execution_id + AttributeType: S + KeySchema: + - AttributeName: execution_id + KeyType: HASH + + # Main Durable Function (orchestrates the pipeline with direct ESM) + DurableFunction: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: python3.14 + Properties: + FunctionName: !Sub '${Environment}-durable-data-pipeline' + Runtime: python3.14 + Handler: handler.lambda_handler + CodeUri: src/durable_pipeline/ + Timeout: 900 + DurableConfig: + ExecutionTimeout: 10 + RetentionPeriodInDays: 1 + Policies: + Statement: + - Effect: Allow + Action: + - lambda:CheckpointDurableExecutions + - lambda:GetDurableExecutionState + Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${Environment}-durable-data-pipeline' + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: + - !GetAtt ValidationFunction.Arn + - !GetAtt TransformationFunction.Arn + - !GetAtt StorageFunction.Arn + - Effect: Allow + Action: + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:UpdateItem + - dynamodb:DeleteItem + - dynamodb:Query + - dynamodb:Scan + Resource: !GetAtt ProcessedDataTable.Arn + AutoPublishAlias: prod + Events: + SQSEvent: + Type: SQS + Properties: + Queue: !GetAtt DataProcessingQueue.Arn + BatchSize: 5 + MaximumBatchingWindowInSeconds: 10 + Environment: + Variables: + VALIDATION_FUNCTION_ARN: !GetAtt ValidationFunction.Arn + TRANSFORMATION_FUNCTION_ARN: !GetAtt TransformationFunction.Arn + STORAGE_FUNCTION_ARN: !GetAtt StorageFunction.Arn + PROCESSED_DATA_TABLE: !Ref ProcessedDataTable + ENVIRONMENT: !Ref Environment + + # Data Validation Function + ValidationFunction: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: python3.14 + Properties: + FunctionName: !Sub '${Environment}-data-validator' + CodeUri: src/validation/ + Handler: handler.lambda_handler + + # Data Transformation Function + TransformationFunction: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: python3.14 + Properties: + FunctionName: !Sub '${Environment}-data-transformer' + CodeUri: src/transformation/ + Handler: handler.lambda_handler + + # Data Storage Function + StorageFunction: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: python3.14 + Properties: + FunctionName: !Sub '${Environment}-data-storage' + CodeUri: src/storage/ + Handler: handler.lambda_handler + Environment: + Variables: + PROCESSED_DATA_TABLE: !Ref ProcessedDataTable + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ProcessedDataTable + +Outputs: + DataProcessingQueueUrl: + Description: 'SQS Queue URL for data processing requests' + Value: !Ref DataProcessingQueue + + ProcessedDataTable: + Description: 'DynamoDB Table for processed data' + Value: !Ref ProcessedDataTable + + DurableFunctionArn: + Description: 'Durable Function ARN' + Value: !GetAtt DurableFunction.Arn