concurrency-guard 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. concurrency_guard-0.1.0/.github/workflows/ci.yml +42 -0
  2. concurrency_guard-0.1.0/.gitignore +10 -0
  3. concurrency_guard-0.1.0/PKG-INFO +191 -0
  4. concurrency_guard-0.1.0/README.md +171 -0
  5. concurrency_guard-0.1.0/action.yml +45 -0
  6. concurrency_guard-0.1.0/examples/real_world_ecommerce/__init__.py +2 -0
  7. concurrency_guard-0.1.0/examples/real_world_ecommerce/checkout_router.py +100 -0
  8. concurrency_guard-0.1.0/examples/real_world_ecommerce/models.py +55 -0
  9. concurrency_guard-0.1.0/examples/real_world_ecommerce/tasks.py +27 -0
  10. concurrency_guard-0.1.0/examples/vulnerable_inventory.py +97 -0
  11. concurrency_guard-0.1.0/examples/vulnerable_user_service.py +115 -0
  12. concurrency_guard-0.1.0/examples/vulnerable_wallet.py +91 -0
  13. concurrency_guard-0.1.0/pyproject.toml +52 -0
  14. concurrency_guard-0.1.0/src/concurrency_guard/__init__.py +4 -0
  15. concurrency_guard-0.1.0/src/concurrency_guard/cli.py +420 -0
  16. concurrency_guard-0.1.0/src/concurrency_guard/patcher/__init__.py +17 -0
  17. concurrency_guard-0.1.0/src/concurrency_guard/patcher/cst_fixer.py +324 -0
  18. concurrency_guard-0.1.0/src/concurrency_guard/reporter/__init__.py +6 -0
  19. concurrency_guard-0.1.0/src/concurrency_guard/reporter/github_pr.py +104 -0
  20. concurrency_guard-0.1.0/src/concurrency_guard/runner/__init__.py +16 -0
  21. concurrency_guard-0.1.0/src/concurrency_guard/runner/barrier_engine.py +421 -0
  22. concurrency_guard-0.1.0/src/concurrency_guard/runner/route_inspector.py +150 -0
  23. concurrency_guard-0.1.0/src/concurrency_guard/runner/schema_synthesizer.py +79 -0
  24. concurrency_guard-0.1.0/src/concurrency_guard/scanner/__init__.py +19 -0
  25. concurrency_guard-0.1.0/src/concurrency_guard/scanner/ast_rules.py +476 -0
  26. concurrency_guard-0.1.0/tests/test_phase1_ast.py +66 -0
  27. concurrency_guard-0.1.0/tests/test_phase2_ast.py +225 -0
  28. concurrency_guard-0.1.0/tests/test_phase3_barrier.py +165 -0
  29. concurrency_guard-0.1.0/tests/test_phase4_fixer.py +80 -0
  30. concurrency_guard-0.1.0/tests/test_phase5_multi_fixer.py +152 -0
  31. concurrency_guard-0.1.0/tests/test_phase6_reporter_and_cg005.py +124 -0
  32. concurrency_guard-0.1.0/tests/test_phase7_dynamic_runner.py +122 -0
  33. concurrency_guard-0.1.0/tests/test_phase8_production_hardening.py +100 -0
@@ -0,0 +1,42 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ name: Lint, Type Check, and Test
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ python-version: ["3.11", "3.12"]
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Install dependencies
27
+ run: |
28
+ python -m pip install --upgrade pip
29
+ pip install .[dev]
30
+
31
+ - name: Run Ruff Linter
32
+ run: |
33
+ ruff check .
34
+
35
+ - name: Run Mypy Type Checker
36
+ run: |
37
+ mypy src/
38
+
39
+ - name: Run Pytest Suite
40
+ run: |
41
+ pytest -v
42
+
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.5
2
+ Name: concurrency-guard
3
+ Version: 0.1.0
4
+ Summary: Deterministic AST + Runtime Concurrency & Idempotency Verifier for Python FastAPI/SQLAlchemy backends
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: aiosqlite>=0.20.0
7
+ Requires-Dist: fastapi>=0.111.0
8
+ Requires-Dist: greenlet>=3.0.0
9
+ Requires-Dist: httpx>=0.27.0
10
+ Requires-Dist: libcst>=1.4.0
11
+ Requires-Dist: rich>=13.7.0
12
+ Requires-Dist: sqlalchemy>=2.0.30
13
+ Requires-Dist: typer>=0.12.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
16
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
17
+ Requires-Dist: pytest>=8.2.0; extra == 'dev'
18
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # ConcurrencyGuard (`cguard`)
22
+
23
+ [![CI](https://github.com/AyushkhatiDev/concurrency-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/AyushkhatiDev/concurrency-guard/actions/workflows/ci.yml)
24
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
25
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
26
+
27
+ **Deterministic AST + Runtime Concurrency & Idempotency Verifier for Python FastAPI, SQLAlchemy, and Celery Backends.**
28
+
29
+ ---
30
+
31
+ ## ⚡ Why ConcurrencyGuard?
32
+
33
+ Modern async Python services frequently suffer from silent concurrency bugs, double-spend race conditions, lost updates, and duplicate background tasks. Traditional linters only check syntax and type hints, while external LLM tools hallucinate or miss non-local control flow races.
34
+
35
+ **ConcurrencyGuard (`cguard`)** solves this with a **100% deterministic pipeline**:
36
+ 1. **Pure AST Static Scanning**: Catches race patterns, missing idempotency keys, N+1 queries, uniqueness races, and unsafe Celery retry tasks.
37
+ 2. **Runtime `asyncio.Barrier` Exploit Engine**: Proves race conditions empirically by synchronizing concurrent coroutines to hit endpoints at the exact same microsecond.
38
+ 3. **Deterministic `LibCST` Auto-Patcher**: Safely rewrites code to apply database row locks (`.with_for_update()`), inject FastAPI `Idempotency-Key` headers, and wrap transactions in `IntegrityError` guards.
39
+ 4. **Automated GitHub PR Reporter**: Emits rich Markdown comments with exploit proofs and diff previews for pull requests.
40
+
41
+ ---
42
+
43
+ ## 📋 Rule Catalog
44
+
45
+ | Rule ID | Name | Target / Trigger Condition | Automated Fix |
46
+ |:---|:---|:---|:---|
47
+ | **`CG-001`** | **Unlocked Read-Modify-Write Race** | Async route reads database state with `select()`, mutates an attribute, and calls `commit()` without `.with_for_update()` locking. | Appends `.with_for_update()` row-level locks via LibCST. |
48
+ | **`CG-002`** | **Missing Idempotency Guard** | Async `@app.post` or `@router.post` route mutates state without an `idempotency_key` parameter. | Injects `idempotency_key: str = Header(..., alias="Idempotency-Key")` into route parameters. |
49
+ | **`CG-003`** | **Async Loop N+1 Query Explode** | `db.execute(...)` or `session.execute(...)` call located directly inside a `for` or `async for` loop body. | Flags query explosion; suggests batching or eager loading (`joinedload`/`selectinload`). |
50
+ | **`CG-004`** | **Check-Then-Act Uniqueness Race** | Async route checks existence with `select()`, calls `add()`, and commits without catching `sqlalchemy.exc.IntegrityError`. | Wraps commit statements in `try...except IntegrityError` with session rollback. |
51
+ | **`CG-005`** | **Non-Idempotent Celery Retry Task** | Celery task (`@app.task`, `@shared_task`) with `autoretry_for` or `max_retries` executes mutations (`.add()`, `.commit()`, `.post()`) without `acks_late=True` or an idempotency lock. | Recommends `acks_late=True` and distributed deduplication lock before side-effects. |
52
+
53
+ ---
54
+
55
+ ## 🚀 Installation
56
+
57
+ ```bash
58
+ # Clone and install in virtual environment
59
+ git clone https://github.com/AyushkhatiDev/concurrency-guard.git
60
+ cd concurrency-guard
61
+ python -m venv .venv
62
+ source .venv/bin/activate
63
+ pip install -e .
64
+ ```
65
+
66
+ To install with development and testing dependencies:
67
+ ```bash
68
+ pip install -e ".[dev]"
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 🛠️ CLI Usage
74
+
75
+ ### 1. `cguard scan` — Static AST Analysis
76
+ Scan files or entire directories for concurrency vulnerabilities:
77
+ ```bash
78
+ # Scan a single file
79
+ cguard scan examples/vulnerable_wallet.py
80
+
81
+ # Scan an entire codebase
82
+ cguard scan src/
83
+ ```
84
+
85
+ ### 2. `cguard verify` — Runtime `asyncio.Barrier` Verification
86
+ Simulate high-concurrency traffic using synchronized barrier workers to prove whether an invariant holds or breaks under load:
87
+ ```bash
88
+ cguard verify examples/vulnerable_wallet.py --workers 20
89
+ ```
90
+
91
+ Sample exploit proof output:
92
+ ```
93
+ ❌ CONCURRENCY INVARIANT VIOLATED (RACE PROVEN)
94
+ Target File: examples/vulnerable_wallet.py
95
+ Target Endpoint: POST /wallet/withdraw
96
+ Concurrent Barrier Workers: 20
97
+ Initial Wallet Balance: $50
98
+ Successful Withdrawals ($50 each): 20
99
+ Final Resulting Balance: $-950
100
+
101
+ INVARIANT VIOLATION PROVEN:
102
+ Multiple concurrent withdrawals succeeded against a $50 balance due to an unlocked
103
+ Read-Modify-Write race condition (CG-001).
104
+ ```
105
+
106
+ ### 3. `cguard fix` — Deterministic LibCST Auto-Patcher
107
+ Inspect and apply deterministic code patches:
108
+ ```bash
109
+ # Preview auto-fixes (dry-run)
110
+ cguard fix examples/vulnerable_wallet.py
111
+
112
+ # Apply specific rule fix
113
+ cguard fix examples/vulnerable_wallet.py --rule CG-001 --write
114
+
115
+ # Apply all available auto-fixes directly to disk
116
+ cguard fix examples/vulnerable_wallet.py --write
117
+ ```
118
+
119
+ ### 4. `cguard report` — GitHub Pull Request Markdown Reporter
120
+ Generate comprehensive Markdown summaries for CI/CD:
121
+ ```bash
122
+ # Print report to stdout
123
+ cguard report examples/vulnerable_wallet.py
124
+
125
+ # Write report to markdown file for GitHub Actions PR comment
126
+ cguard report examples/vulnerable_wallet.py --output pr_report.md
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 🤖 GitHub Action Integration
132
+
133
+ Add ConcurrencyGuard to your pull request workflows:
134
+
135
+ ```yaml
136
+ name: Concurrency Guard CI
137
+
138
+ on:
139
+ pull_request:
140
+ branches: [main]
141
+
142
+ jobs:
143
+ concurrency-audit:
144
+ runs-on: ubuntu-latest
145
+ steps:
146
+ - uses: actions/checkout@v4
147
+
148
+ - name: Run ConcurrencyGuard Action
149
+ uses: AyushkhatiDev/concurrency-guard@main
150
+ with:
151
+ path: "app"
152
+ report-file: "app/main.py"
153
+ output: "pr-report.md"
154
+
155
+ - name: Post PR Comment
156
+ if: failure() || success()
157
+ uses: actions/github-script@v7
158
+ with:
159
+ script: |
160
+ const fs = require('fs');
161
+ if (fs.existsSync('pr-report.md')) {
162
+ const body = fs.readFileSync('pr-report.md', 'utf8');
163
+ github.rest.issues.createComment({
164
+ issue_number: context.issue.number,
165
+ owner: context.repo.owner,
166
+ repo: context.repo.repo,
167
+ body: body
168
+ });
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 🧪 Testing
175
+
176
+ ConcurrencyGuard is thoroughly tested with unit tests and runtime stress verifications:
177
+
178
+ ```bash
179
+ # Run test suite
180
+ pytest -v
181
+
182
+ # Run lint and type checking
183
+ ruff check .
184
+ mypy src/
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 📄 License
190
+
191
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,171 @@
1
+ # ConcurrencyGuard (`cguard`)
2
+
3
+ [![CI](https://github.com/AyushkhatiDev/concurrency-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/AyushkhatiDev/concurrency-guard/actions/workflows/ci.yml)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ **Deterministic AST + Runtime Concurrency & Idempotency Verifier for Python FastAPI, SQLAlchemy, and Celery Backends.**
8
+
9
+ ---
10
+
11
+ ## ⚡ Why ConcurrencyGuard?
12
+
13
+ Modern async Python services frequently suffer from silent concurrency bugs, double-spend race conditions, lost updates, and duplicate background tasks. Traditional linters only check syntax and type hints, while external LLM tools hallucinate or miss non-local control flow races.
14
+
15
+ **ConcurrencyGuard (`cguard`)** solves this with a **100% deterministic pipeline**:
16
+ 1. **Pure AST Static Scanning**: Catches race patterns, missing idempotency keys, N+1 queries, uniqueness races, and unsafe Celery retry tasks.
17
+ 2. **Runtime `asyncio.Barrier` Exploit Engine**: Proves race conditions empirically by synchronizing concurrent coroutines to hit endpoints at the exact same microsecond.
18
+ 3. **Deterministic `LibCST` Auto-Patcher**: Safely rewrites code to apply database row locks (`.with_for_update()`), inject FastAPI `Idempotency-Key` headers, and wrap transactions in `IntegrityError` guards.
19
+ 4. **Automated GitHub PR Reporter**: Emits rich Markdown comments with exploit proofs and diff previews for pull requests.
20
+
21
+ ---
22
+
23
+ ## 📋 Rule Catalog
24
+
25
+ | Rule ID | Name | Target / Trigger Condition | Automated Fix |
26
+ |:---|:---|:---|:---|
27
+ | **`CG-001`** | **Unlocked Read-Modify-Write Race** | Async route reads database state with `select()`, mutates an attribute, and calls `commit()` without `.with_for_update()` locking. | Appends `.with_for_update()` row-level locks via LibCST. |
28
+ | **`CG-002`** | **Missing Idempotency Guard** | Async `@app.post` or `@router.post` route mutates state without an `idempotency_key` parameter. | Injects `idempotency_key: str = Header(..., alias="Idempotency-Key")` into route parameters. |
29
+ | **`CG-003`** | **Async Loop N+1 Query Explode** | `db.execute(...)` or `session.execute(...)` call located directly inside a `for` or `async for` loop body. | Flags query explosion; suggests batching or eager loading (`joinedload`/`selectinload`). |
30
+ | **`CG-004`** | **Check-Then-Act Uniqueness Race** | Async route checks existence with `select()`, calls `add()`, and commits without catching `sqlalchemy.exc.IntegrityError`. | Wraps commit statements in `try...except IntegrityError` with session rollback. |
31
+ | **`CG-005`** | **Non-Idempotent Celery Retry Task** | Celery task (`@app.task`, `@shared_task`) with `autoretry_for` or `max_retries` executes mutations (`.add()`, `.commit()`, `.post()`) without `acks_late=True` or an idempotency lock. | Recommends `acks_late=True` and distributed deduplication lock before side-effects. |
32
+
33
+ ---
34
+
35
+ ## 🚀 Installation
36
+
37
+ ```bash
38
+ # Clone and install in virtual environment
39
+ git clone https://github.com/AyushkhatiDev/concurrency-guard.git
40
+ cd concurrency-guard
41
+ python -m venv .venv
42
+ source .venv/bin/activate
43
+ pip install -e .
44
+ ```
45
+
46
+ To install with development and testing dependencies:
47
+ ```bash
48
+ pip install -e ".[dev]"
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 🛠️ CLI Usage
54
+
55
+ ### 1. `cguard scan` — Static AST Analysis
56
+ Scan files or entire directories for concurrency vulnerabilities:
57
+ ```bash
58
+ # Scan a single file
59
+ cguard scan examples/vulnerable_wallet.py
60
+
61
+ # Scan an entire codebase
62
+ cguard scan src/
63
+ ```
64
+
65
+ ### 2. `cguard verify` — Runtime `asyncio.Barrier` Verification
66
+ Simulate high-concurrency traffic using synchronized barrier workers to prove whether an invariant holds or breaks under load:
67
+ ```bash
68
+ cguard verify examples/vulnerable_wallet.py --workers 20
69
+ ```
70
+
71
+ Sample exploit proof output:
72
+ ```
73
+ ❌ CONCURRENCY INVARIANT VIOLATED (RACE PROVEN)
74
+ Target File: examples/vulnerable_wallet.py
75
+ Target Endpoint: POST /wallet/withdraw
76
+ Concurrent Barrier Workers: 20
77
+ Initial Wallet Balance: $50
78
+ Successful Withdrawals ($50 each): 20
79
+ Final Resulting Balance: $-950
80
+
81
+ INVARIANT VIOLATION PROVEN:
82
+ Multiple concurrent withdrawals succeeded against a $50 balance due to an unlocked
83
+ Read-Modify-Write race condition (CG-001).
84
+ ```
85
+
86
+ ### 3. `cguard fix` — Deterministic LibCST Auto-Patcher
87
+ Inspect and apply deterministic code patches:
88
+ ```bash
89
+ # Preview auto-fixes (dry-run)
90
+ cguard fix examples/vulnerable_wallet.py
91
+
92
+ # Apply specific rule fix
93
+ cguard fix examples/vulnerable_wallet.py --rule CG-001 --write
94
+
95
+ # Apply all available auto-fixes directly to disk
96
+ cguard fix examples/vulnerable_wallet.py --write
97
+ ```
98
+
99
+ ### 4. `cguard report` — GitHub Pull Request Markdown Reporter
100
+ Generate comprehensive Markdown summaries for CI/CD:
101
+ ```bash
102
+ # Print report to stdout
103
+ cguard report examples/vulnerable_wallet.py
104
+
105
+ # Write report to markdown file for GitHub Actions PR comment
106
+ cguard report examples/vulnerable_wallet.py --output pr_report.md
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 🤖 GitHub Action Integration
112
+
113
+ Add ConcurrencyGuard to your pull request workflows:
114
+
115
+ ```yaml
116
+ name: Concurrency Guard CI
117
+
118
+ on:
119
+ pull_request:
120
+ branches: [main]
121
+
122
+ jobs:
123
+ concurrency-audit:
124
+ runs-on: ubuntu-latest
125
+ steps:
126
+ - uses: actions/checkout@v4
127
+
128
+ - name: Run ConcurrencyGuard Action
129
+ uses: AyushkhatiDev/concurrency-guard@main
130
+ with:
131
+ path: "app"
132
+ report-file: "app/main.py"
133
+ output: "pr-report.md"
134
+
135
+ - name: Post PR Comment
136
+ if: failure() || success()
137
+ uses: actions/github-script@v7
138
+ with:
139
+ script: |
140
+ const fs = require('fs');
141
+ if (fs.existsSync('pr-report.md')) {
142
+ const body = fs.readFileSync('pr-report.md', 'utf8');
143
+ github.rest.issues.createComment({
144
+ issue_number: context.issue.number,
145
+ owner: context.repo.owner,
146
+ repo: context.repo.repo,
147
+ body: body
148
+ });
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## 🧪 Testing
155
+
156
+ ConcurrencyGuard is thoroughly tested with unit tests and runtime stress verifications:
157
+
158
+ ```bash
159
+ # Run test suite
160
+ pytest -v
161
+
162
+ # Run lint and type checking
163
+ ruff check .
164
+ mypy src/
165
+ ```
166
+
167
+ ---
168
+
169
+ ## 📄 License
170
+
171
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,45 @@
1
+ name: "ConcurrencyGuard Action"
2
+ description: "Deterministic AST scanner and runtime race condition verifier for Python FastAPI & SQLAlchemy"
3
+ author: "AyushkhatiDev"
4
+ branding:
5
+ icon: "shield"
6
+ color: "blue"
7
+
8
+ inputs:
9
+ path:
10
+ description: "Path to file or directory to scan"
11
+ required: false
12
+ default: "."
13
+ report-file:
14
+ description: "File to generate GitHub PR Markdown report for"
15
+ required: false
16
+ default: ""
17
+ output:
18
+ description: "Output path for the Markdown report"
19
+ required: false
20
+ default: "concurrency-guard-report.md"
21
+
22
+ runs:
23
+ using: "composite"
24
+ steps:
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v5
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Install ConcurrencyGuard
31
+ shell: bash
32
+ run: |
33
+ pip install .
34
+
35
+ - name: Run ConcurrencyGuard Scan
36
+ shell: bash
37
+ run: |
38
+ cguard scan ${{ inputs.path }}
39
+
40
+ - name: Run ConcurrencyGuard Report
41
+ if: ${{ inputs.report-file != '' }}
42
+ shell: bash
43
+ run: |
44
+ cguard report ${{ inputs.report-file }} --output ${{ inputs.output }} || true
45
+
@@ -0,0 +1,2 @@
1
+ """Real-world multi-file FastAPI e-commerce benchmark application."""
2
+
@@ -0,0 +1,100 @@
1
+ """FastAPI checkout and payment router demonstrating real-world concurrency issues."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncGenerator
6
+ from typing import Any
7
+
8
+ from fastapi import APIRouter, Depends, Header, HTTPException, status
9
+ from pydantic import BaseModel
10
+ from sqlalchemy import select
11
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
12
+
13
+ from examples.real_world_ecommerce.models import Coupon, Order, OrderItem, WebhookEvent
14
+
15
+ DATABASE_URL = "sqlite+aiosqlite:///:memory:"
16
+ engine = create_async_engine(DATABASE_URL, echo=False)
17
+ async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
18
+
19
+
20
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
21
+ async with async_session() as session:
22
+ yield session
23
+
24
+
25
+ router = APIRouter()
26
+
27
+
28
+ class RedeemCouponRequest(BaseModel):
29
+ code: str
30
+ user_id: int
31
+
32
+
33
+ @router.post("/coupons/redeem")
34
+ async def redeem_coupon(
35
+ request: RedeemCouponRequest,
36
+ db: AsyncSession = Depends(get_db),
37
+ ) -> dict[str, str | int]:
38
+ """Redeem coupon using SQLAlchemy 2.0 await db.scalar without row-locking or idempotency."""
39
+ stmt = select(Coupon).where(Coupon.code == request.code)
40
+ coupon = await db.scalar(stmt)
41
+
42
+ if coupon is None:
43
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Coupon not found")
44
+
45
+ if coupon.remaining_uses > 0:
46
+ # Race condition: Unlocked Read-Modify-Write via db.scalar (CG-001)
47
+ # Missing Idempotency-Key guard (CG-002)
48
+ coupon.remaining_uses -= 1
49
+ await db.commit()
50
+ return {"status": "redeemed", "remaining": coupon.remaining_uses}
51
+
52
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Coupon exhausted")
53
+
54
+
55
+ @router.get("/orders/with-items")
56
+ async def get_orders_with_items(
57
+ db: AsyncSession = Depends(get_db),
58
+ ) -> list[dict[str, Any]]:
59
+ """Fetch orders and query items in a loop: Async Loop N+1 Query Explode (CG-003)."""
60
+ result = await db.execute(select(Order))
61
+ orders = result.scalars().all()
62
+ orders_data: list[dict[str, Any]] = []
63
+
64
+ for order in orders:
65
+ stmt = select(OrderItem).where(OrderItem.order_id == order.id)
66
+ # N+1 query: executes inside loop
67
+ items_result = await db.scalars(stmt)
68
+ items = items_result.all()
69
+ orders_data.append({"order_id": order.id, "items": len(items)})
70
+
71
+ return orders_data
72
+
73
+
74
+ class StripeWebhookPayload(BaseModel):
75
+ event_id: str
76
+ event_type: str
77
+
78
+
79
+ @router.post("/webhooks/stripe")
80
+ async def handle_stripe_webhook(
81
+ payload: StripeWebhookPayload,
82
+ idempotency_key: str = Header(..., alias="Idempotency-Key"),
83
+ db: AsyncSession = Depends(get_db),
84
+ ) -> dict[str, str]:
85
+ """Check-then-act uniqueness race on webhook event ingestion (CG-004)."""
86
+ stmt = select(WebhookEvent).where(WebhookEvent.event_id == payload.event_id)
87
+ existing = await db.scalar(stmt)
88
+ if existing is not None:
89
+ return {"status": "already_processed"}
90
+
91
+ event = WebhookEvent(
92
+ event_id=payload.event_id,
93
+ event_type=payload.event_type,
94
+ processed=True,
95
+ )
96
+ db.add(event)
97
+ # Unprotected commit: violates uniqueness under race conditions
98
+ await db.commit()
99
+ return {"status": "processed"}
100
+
@@ -0,0 +1,55 @@
1
+ """SQLAlchemy 2.0 declarative models for e-commerce benchmark."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from sqlalchemy import Float, Integer, String
6
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
7
+
8
+
9
+ class Base(DeclarativeBase):
10
+ pass
11
+
12
+
13
+ class Coupon(Base):
14
+ __tablename__ = "coupons"
15
+
16
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
17
+ code: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False)
18
+ remaining_uses: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
19
+ discount_percent: Mapped[float] = mapped_column(Float, default=10.0, nullable=False)
20
+
21
+
22
+ class Order(Base):
23
+ __tablename__ = "orders"
24
+
25
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
26
+ user_id: Mapped[int] = mapped_column(Integer, nullable=False)
27
+ total_amount: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
28
+ status: Mapped[str] = mapped_column(String(50), default="PENDING", nullable=False)
29
+
30
+
31
+ class OrderItem(Base):
32
+ __tablename__ = "order_items"
33
+
34
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
35
+ order_id: Mapped[int] = mapped_column(Integer, nullable=False)
36
+ product_name: Mapped[str] = mapped_column(String(100), nullable=False)
37
+ price: Mapped[float] = mapped_column(Float, nullable=False)
38
+
39
+
40
+ class WebhookEvent(Base):
41
+ __tablename__ = "webhook_events"
42
+
43
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
44
+ event_id: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
45
+ event_type: Mapped[str] = mapped_column(String(100), nullable=False)
46
+ processed: Mapped[bool] = mapped_column(default=False, nullable=False)
47
+
48
+
49
+ class Product(Base):
50
+ __tablename__ = "products"
51
+
52
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
53
+ name: Mapped[str] = mapped_column(String(100), nullable=False)
54
+ price: Mapped[float] = mapped_column(Float, nullable=False)
55
+
@@ -0,0 +1,27 @@
1
+ """Celery background tasks demonstrating retry non-idempotency."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from celery import shared_task
6
+ from sqlalchemy import create_engine
7
+ from sqlalchemy.orm import Session, sessionmaker
8
+
9
+ from examples.real_world_ecommerce.models import Order
10
+
11
+ sync_engine = create_engine("sqlite:///:memory:")
12
+ SyncSessionLocal = sessionmaker(bind=sync_engine, class_=Session)
13
+
14
+
15
+ @shared_task(autoretry_for=(Exception,), max_retries=5)
16
+ def process_refund(order_id: int) -> dict[str, str]:
17
+ """Process order refund with retries but without idempotency protection (CG-005)."""
18
+ session = SyncSessionLocal()
19
+ try:
20
+ order = session.get(Order, order_id)
21
+ if order is not None:
22
+ order.status = "REFUNDED"
23
+ session.commit()
24
+ finally:
25
+ session.close()
26
+ return {"status": "refunded"}
27
+