|
| 1 | +"""Reproduces issue where map with minSuccessful loses failure count.""" |
| 2 | + |
| 3 | +from typing import Any |
| 4 | + |
| 5 | +from aws_durable_execution_sdk_python.config import ( |
| 6 | + CompletionConfig, |
| 7 | + MapConfig, |
| 8 | + StepConfig, |
| 9 | + Duration, |
| 10 | +) |
| 11 | +from aws_durable_execution_sdk_python.context import DurableContext |
| 12 | +from aws_durable_execution_sdk_python.execution import durable_execution |
| 13 | +from aws_durable_execution_sdk_python.retries import ( |
| 14 | + RetryStrategyConfig, |
| 15 | + create_retry_strategy, |
| 16 | +) |
| 17 | + |
| 18 | + |
| 19 | +@durable_execution |
| 20 | +def handler(_event: Any, context: DurableContext) -> dict[str, Any]: |
| 21 | + """Handler demonstrating map with completion config issue.""" |
| 22 | + # Test data: Items 2 and 4 will fail (40% failure rate) |
| 23 | + items = [ |
| 24 | + {"id": 1, "shouldFail": False}, |
| 25 | + {"id": 2, "shouldFail": True}, # Will fail |
| 26 | + {"id": 3, "shouldFail": False}, |
| 27 | + {"id": 4, "shouldFail": True}, # Will fail |
| 28 | + {"id": 5, "shouldFail": False}, |
| 29 | + ] |
| 30 | + |
| 31 | + # Fixed completion config that causes the issue |
| 32 | + completion_config = CompletionConfig( |
| 33 | + min_successful=2, |
| 34 | + tolerated_failure_percentage=50, |
| 35 | + ) |
| 36 | + |
| 37 | + context.logger.info( |
| 38 | + f"Starting map with config: min_successful=2, tolerated_failure_percentage=50" |
| 39 | + ) |
| 40 | + context.logger.info( |
| 41 | + f"Items pattern: {', '.join(['FAIL' if i['shouldFail'] else 'SUCCESS' for i in items])}" |
| 42 | + ) |
| 43 | + |
| 44 | + def process_item( |
| 45 | + ctx: DurableContext, item: dict[str, Any], index: int, _ |
| 46 | + ) -> dict[str, Any]: |
| 47 | + """Process each item in the map.""" |
| 48 | + context.logger.info( |
| 49 | + f"Processing item {item['id']} (index {index}), shouldFail: {item['shouldFail']}" |
| 50 | + ) |
| 51 | + |
| 52 | + retry_config = RetryStrategyConfig( |
| 53 | + max_attempts=2, |
| 54 | + initial_delay=Duration.from_seconds(1), |
| 55 | + max_delay=Duration.from_seconds(1), |
| 56 | + ) |
| 57 | + step_config = StepConfig(retry_strategy=create_retry_strategy(retry_config)) |
| 58 | + |
| 59 | + def step_function(_: DurableContext) -> dict[str, Any]: |
| 60 | + """Step that processes or fails based on item.""" |
| 61 | + if item["shouldFail"]: |
| 62 | + raise Exception(f"Processing failed for item {item['id']}") |
| 63 | + return { |
| 64 | + "itemId": item["id"], |
| 65 | + "processed": True, |
| 66 | + "result": f"Item {item['id']} processed successfully", |
| 67 | + } |
| 68 | + |
| 69 | + return ctx.step( |
| 70 | + step_function, |
| 71 | + name=f"process-item-{index}", |
| 72 | + config=step_config, |
| 73 | + ) |
| 74 | + |
| 75 | + config = MapConfig( |
| 76 | + max_concurrency=3, |
| 77 | + completion_config=completion_config, |
| 78 | + ) |
| 79 | + |
| 80 | + results = context.map( |
| 81 | + inputs=items, |
| 82 | + func=process_item, |
| 83 | + name="completion-config-items", |
| 84 | + config=config, |
| 85 | + ) |
| 86 | + |
| 87 | + context.logger.info("Map completed with results:") |
| 88 | + context.logger.info(f"Total items processed: {results.total_count}") |
| 89 | + context.logger.info(f"Successful items: {results.success_count}") |
| 90 | + context.logger.info(f"Failed items: {results.failure_count}") |
| 91 | + context.logger.info(f"Has failures: {results.has_failure}") |
| 92 | + context.logger.info(f"Batch status: {results.status}") |
| 93 | + context.logger.info(f"Completion reason: {results.completion_reason}") |
| 94 | + |
| 95 | + return { |
| 96 | + "totalItems": results.total_count, |
| 97 | + "successfulCount": results.success_count, |
| 98 | + "failedCount": results.failure_count, |
| 99 | + "hasFailures": results.has_failure, |
| 100 | + "batchStatus": str(results.status), |
| 101 | + "completionReason": str(results.completion_reason), |
| 102 | + "successfulItems": [ |
| 103 | + { |
| 104 | + "index": item.index, |
| 105 | + "itemId": items[item.index]["id"], |
| 106 | + } |
| 107 | + for item in results.succeeded() |
| 108 | + ], |
| 109 | + "failedItems": [ |
| 110 | + { |
| 111 | + "index": item.index, |
| 112 | + "itemId": items[item.index]["id"], |
| 113 | + "error": str(item.error), |
| 114 | + } |
| 115 | + for item in results.failed() |
| 116 | + ], |
| 117 | + } |
0 commit comments