|
1 | 1 | import json |
2 | 2 | import os |
| 3 | +import time |
| 4 | +import uuid |
| 5 | +from datetime import datetime, timezone |
3 | 6 | import boto3 |
4 | 7 |
|
5 | | -dynamodb = boto3.resource("dynamodb", endpoint_url=os.environ.get("AWS_ENDPOINT_URL")) |
6 | | -s3 = boto3.client("s3", endpoint_url=os.environ.get("AWS_ENDPOINT_URL")) |
| 8 | +dynamodb = boto3.resource("dynamodb") |
| 9 | +s3 = boto3.client("s3") |
| 10 | +sfn = boto3.client("stepfunctions") |
7 | 11 |
|
8 | 12 | TABLE_NAME = os.environ["ORDERS_TABLE"] |
9 | 13 | RECEIPTS_BUCKET = os.environ["RECEIPTS_BUCKET"] |
| 14 | +STATE_MACHINE_ARN = os.environ["STATE_MACHINE_ARN"] |
| 15 | + |
| 16 | +TERMINAL_STATUSES = {"fulfilled", "failed"} |
| 17 | + |
| 18 | + |
| 19 | +def now(): |
| 20 | + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") |
| 21 | + |
| 22 | + |
| 23 | +def set_status(order_id, status): |
| 24 | + ts_key = { |
| 25 | + "validating": "validating_at", |
| 26 | + "payment_processing": "payment_at", |
| 27 | + "fulfilled": "fulfilled_at", |
| 28 | + "failed": "failed_at", |
| 29 | + }.get(status) |
| 30 | + |
| 31 | + expression = "SET #s = :s" |
| 32 | + names = {"#s": "status"} |
| 33 | + values = {":s": status} |
| 34 | + |
| 35 | + if ts_key: |
| 36 | + expression += ", #ts = :ts" |
| 37 | + names["#ts"] = ts_key |
| 38 | + values[":ts"] = now() |
| 39 | + |
| 40 | + dynamodb.Table(TABLE_NAME).update_item( |
| 41 | + Key={"order_id": order_id}, |
| 42 | + UpdateExpression=expression, |
| 43 | + ExpressionAttributeNames=names, |
| 44 | + ExpressionAttributeValues=values, |
| 45 | + ) |
10 | 46 |
|
11 | 47 |
|
12 | 48 | def handler(event, context): |
13 | | - for record in event["Records"]: |
14 | | - order = json.loads(record["body"]) |
15 | | - order_id = order["order_id"] |
16 | | - |
17 | | - # Update order status in DynamoDB |
18 | | - table = dynamodb.Table(TABLE_NAME) |
19 | | - table.update_item( |
20 | | - Key={"order_id": order_id}, |
21 | | - UpdateExpression="SET #s = :s", |
22 | | - ExpressionAttributeNames={"#s": "status"}, |
23 | | - ExpressionAttributeValues={":s": "processed"}, |
24 | | - ) |
25 | | - |
26 | | - # Store receipt in S3 |
27 | | - receipt = { |
28 | | - "order_id": order_id, |
29 | | - "item": order.get("item"), |
30 | | - "quantity": order.get("quantity"), |
31 | | - "status": "processed", |
32 | | - } |
33 | | - s3.put_object( |
34 | | - Bucket=RECEIPTS_BUCKET, |
35 | | - Key=f"receipts/{order_id}.json", |
36 | | - Body=json.dumps(receipt), |
37 | | - ContentType="application/json", |
38 | | - ) |
| 49 | + # Triggered by SQS: start a state machine execution per order |
| 50 | + if "Records" in event: |
| 51 | + for record in event["Records"]: |
| 52 | + order = json.loads(record["body"]) |
| 53 | + set_status(order["order_id"], "validating") # fails fast if DDB is faulted → SQS retry → DLQ |
| 54 | + sfn.start_execution( |
| 55 | + stateMachineArn=STATE_MACHINE_ARN, |
| 56 | + name=f"order-{order['order_id']}-{uuid.uuid4().hex[:8]}", |
| 57 | + input=json.dumps({"order": order}), |
| 58 | + ) |
| 59 | + return |
| 60 | + |
| 61 | + # Invoked by Step Functions |
| 62 | + step = event["step"] |
| 63 | + order = event["order"] |
| 64 | + |
| 65 | + if step == "validate": return validate(order) |
| 66 | + if step == "process_payment": return process_payment(order) |
| 67 | + if step == "fulfill": return fulfill(order) |
| 68 | + if step == "handle_failure": return handle_failure(order) |
| 69 | + |
| 70 | + raise ValueError(f"Unknown step: {step}") |
| 71 | + |
| 72 | + |
| 73 | +def validate(order): |
| 74 | + time.sleep(2) |
| 75 | + set_status(order["order_id"], "validating") |
| 76 | + return order |
| 77 | + |
| 78 | + |
| 79 | +def process_payment(order): |
| 80 | + time.sleep(3) |
| 81 | + set_status(order["order_id"], "payment_processing") |
| 82 | + return order |
| 83 | + |
| 84 | + |
| 85 | +def fulfill(order): |
| 86 | + time.sleep(2) |
| 87 | + set_status(order["order_id"], "fulfilled") |
| 88 | + receipt = {k: order[k] for k in ("order_id", "item", "quantity")} |
| 89 | + receipt["status"] = "fulfilled" |
| 90 | + s3.put_object( |
| 91 | + Bucket=RECEIPTS_BUCKET, |
| 92 | + Key=f"receipts/{order['order_id']}.json", |
| 93 | + Body=json.dumps(receipt), |
| 94 | + ContentType="application/json", |
| 95 | + ) |
| 96 | + return order |
| 97 | + |
| 98 | + |
| 99 | +def handle_failure(order): |
| 100 | + set_status(order["order_id"], "failed") |
| 101 | + return order |
0 commit comments