# Scenario A1: Sync Application Calling Async Libraries
**Role**: Application Developer
**Current State**: Writing sync application (CLI, script, multiprocess/multithread)
**Goal**: Use modern async libraries (httpx, aiofiles, async DB drivers)
**Challenge**: Don't want to deal with `asyncio.run()` everywhere
---
## Overview
You have a **synchronous application** (CLI tool, automation script, data processing pipeline) but want to use **modern async libraries** that offer better performance and features than their sync counterparts.
**Examples of async libraries you want to use**:
- `httpx.AsyncClient` (vs `requests`)
- `aiofiles` (vs built-in `open()`)
- `asyncpg` (vs `psycopg2`)
- `aiohttp` (vs `urllib`)
**Problem**: These libraries require async context, but your app is sync.
---
## Without SmartAsync
### Traditional Approach 1: Manual `asyncio.run()` Everywhere
```python
import asyncio
import httpx
class DataFetcher:
async def _fetch_async(self, url: str):
"""Internal async implementation."""
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
def fetch(self, url: str):
"""Public sync wrapper."""
return asyncio.run(self._fetch_async(url))
# Usage in sync code
fetcher = DataFetcher()
data = fetcher.fetch("https://api.example.com/users")
print(data)
```
**Pain Points**:
- ❌ Every method needs sync wrapper
- ❌ Code duplication
- ❌ Boilerplate: private async + public sync
- ❌ Easy to forget wrapper somewhere
- ❌ Verbose and repetitive
---
### Traditional Approach 2: Stay Sync with Sync Libraries
```python
import requests
class DataFetcher:
def fetch(self, url: str):
"""Use sync library instead."""
response = requests.get(url)
return response.json()
# Usage
fetcher = DataFetcher()
data = fetcher.fetch("https://api.example.com/users")
```
**Pain Points**:
- ❌ Can't use modern async libraries
- ❌ Missing features (connection pooling, HTTP/2, etc.)
- ❌ Lower performance
- ❌ Code won't work if moved to async context later
---
### Traditional Approach 3: Make Everything Async
```python
import asyncio
import httpx
class DataFetcher:
async def fetch(self, url: str):
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
# Usage - need to run event loop
async def main():
fetcher = DataFetcher()
data = await fetcher.fetch("https://api.example.com/users")
print(data)
if __name__ == "__main__":
asyncio.run(main())
```
**Pain Points**:
- ❌ Entire app becomes async
- ❌ All functions need `async def`
- ❌ All calls need `await`
- ❌ Complex for simple scripts
- ❌ Can't easily integrate with sync libraries
---
## With SmartAsync
### Solution
```python
import httpx
from smartasync import smartasync
class DataFetcher:
@smartasync
async def fetch(self, url: str):
"""Single implementation - works in both contexts!"""
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
# Usage in sync code - NO await needed!
fetcher = DataFetcher()
data = fetcher.fetch("https://api.example.com/users")
print(data)
# Also works in async context (if you migrate later)
async def process():
fetcher = DataFetcher()
data = await fetcher.fetch("https://api.example.com/users")
return data
```
**Benefits**:
- ✅ Single implementation
- ✅ Use async libraries from sync code
- ✅ No boilerplate
- ✅ Future-proof (works if you go async later)
- ✅ Clean, readable code
---
## Issues Impact on This Scenario
Here are the issues affecting this scenario:
### Issue #1: `_sync_mode` Ignored
**Impact**: ⚪ **NO IMPACT**
**Why**: Sync context is auto-detected correctly without needing `_sync_mode`.
**Status**: No fix needed for this scenario.
---
### Issue #2: Not Thread-Safe
**Impact**: 🟡 **DEGRADED** (if multi-threaded)
**Severity**: MEDIUM
**Problem**: If your sync app uses **multiple threads**, the cache can have race conditions.
#### Single-Threaded (NO PROBLEM)
```python
# ✅ SAFE: Single-threaded CLI
fetcher = DataFetcher()
for url in urls:
data = fetcher.fetch(url)
process(data)
```
#### Multi-Threaded (PROBLEM!)
```python
# ❌ UNSAFE: Multiple threads sharing instance
import threading
fetcher = DataFetcher() # Shared across threads
def worker(url):
data = fetcher.fetch(url) # Race condition!
process(data)
threads = [threading.Thread(target=worker, args=(url,)) for url in urls]
for t in threads:
t.start()
```
#### Solution 1: Per-Thread Instances (Recommended)
```python
# ✅ SAFE: Each thread has its own instance
import threading
def worker(url):
fetcher = DataFetcher() # New instance per thread
data = fetcher.fetch(url)
process(data)
threads = [threading.Thread(target=worker, args=(url,)) for url in urls]
```
#### Solution 2: Thread-Local Storage (Advanced)
```python
# ✅ SAFE: Thread-local storage
import threading
thread_local = threading.local()
def get_fetcher():
"""Get or create thread-local fetcher."""
if not hasattr(thread_local, 'fetcher'):
thread_local.fetcher = DataFetcher()
return thread_local.fetcher
def worker(url):
fetcher = get_fetcher()
data = fetcher.fetch(url)
process(data)
```
#### Solution 3: Multiprocessing (If Applicable)
```python
# ✅ SAFE: Separate processes (no shared cache)
import multiprocessing
def worker(url):
fetcher = DataFetcher()
data = fetcher.fetch(url)
process(data)
if __name__ == "__main__":
with multiprocessing.Pool(processes=4) as pool:
results = pool.map(worker, urls)
```
---
### Issue #3: Missing Edge Case Tests
**Impact**: 🟢 **MINOR**
**Why**: Missing tests in SmartAsync library don't affect your app's runtime behavior.
**Status**: No action needed for users.
---
### Issue #4: Missing "When to Use" Documentation
**Impact**: 🟡 **DEGRADED**
**Problem**: You might not discover SmartAsync as a solution!
**Status**: This document addresses that gap.
---
## Decision Matrix
| Factor | Approach 1
(Manual `asyncio.run()`) | Approach 2
(Stay Sync) | Approach 3
(All Async) | **SmartAsync** |
|--------|-------|-------|-------|-------|
| Code duplication | High | None | None | **None** ✅ |
| Modern libraries | ✅ Yes | ❌ No | ✅ Yes | **✅ Yes** |
| Boilerplate | High | None | Medium | **None** ✅ |
| Future-proof | Medium | Low | High | **High** ✅ |
| Learning curve | Low | Low | High | **Low** ✅ |
| Thread safety | Good | Good | Good | **Needs care** ⚠️ |
| Performance (sync) | Good | Good | Good | **Good** ✅ |
| Multiprocess | Good | Good | Good | **Good** ✅ |
---
## When to Use SmartAsync for This Scenario
### ✅ USE SmartAsync if:
1. **Single-threaded sync app**
- CLI tools
- Sequential scripts
- Single-process workers
2. **Multi-threaded with per-thread instances**
- Thread pools with instance per thread
- Thread-local storage pattern
3. **Multiprocess application**
- Each process has separate cache
- No shared state
4. **Want to use modern async libraries**
- httpx, aiofiles, asyncpg, etc.
- Better performance and features
5. **Future-proofing for async migration**
- May go async later
- Want compatible API
### ❌ DON'T use SmartAsync if:
1. **Heavy multi-threading with shared instances**
- Many threads accessing same instance
- Can't use per-thread instances
2. **Need guaranteed thread safety**
- Critical applications
- Can't tolerate race conditions
3. **Performance-critical tight loops**
- Calling method millions of times
- 100μs overhead per call matters
### 🤔 Consider Alternatives if:
1. **Simple use case**: Stay with sync libraries (requests, etc.)
2. **Already thread-safe**: Stick with current approach
3. **High-frequency calls**: Use explicit sync/async separation
---
## Real-World Example: Data Processing Pipeline
### Scenario
You're building a **data processing pipeline** that:
- Fetches data from REST API
- Downloads files from S3
- Writes results to database
- Runs in CLI (single-threaded)
### Before SmartAsync
```python
# pipeline.py
import asyncio
import httpx
import aiofiles
import asyncpg
class DataPipeline:
async def _fetch_data_async(self, api_url: str):
async with httpx.AsyncClient() as client:
response = await client.get(api_url)
return response.json()
def fetch_data(self, api_url: str):
return asyncio.run(self._fetch_data_async(api_url))
async def _download_file_async(self, url: str, path: str):
async with httpx.AsyncClient() as client:
async with aiofiles.open(path, 'wb') as f:
async for chunk in client.stream('GET', url):
await f.write(chunk)
def download_file(self, url: str, path: str):
return asyncio.run(self._download_file_async(url, path))
async def _save_to_db_async(self, data: dict):
conn = await asyncpg.connect('postgresql://...')
await conn.execute('INSERT INTO results VALUES ($1)', data)
await conn.close()
def save_to_db(self, data: dict):
return asyncio.run(self._save_to_db_async(data))
# Usage
pipeline = DataPipeline()
data = pipeline.fetch_data("https://api.example.com/data")
pipeline.download_file("https://cdn.example.com/file.zip", "file.zip")
pipeline.save_to_db(data)
```
**Problems**:
- 3 async methods → 3 sync wrappers
- 6 methods total (2x duplication)
- Repetitive `asyncio.run()` calls
- Easy to forget wrapper
### After SmartAsync
```python
# pipeline.py
import httpx
import aiofiles
import asyncpg
from smartasync import smartasync
class DataPipeline:
@smartasync
async def fetch_data(self, api_url: str):
async with httpx.AsyncClient() as client:
response = await client.get(api_url)
return response.json()
@smartasync
async def download_file(self, url: str, path: str):
async with httpx.AsyncClient() as client:
async with aiofiles.open(path, 'wb') as f:
async for chunk in client.stream('GET', url):
await f.write(chunk)
@smartasync
async def save_to_db(self, data: dict):
conn = await asyncpg.connect('postgresql://...')
await conn.execute('INSERT INTO results VALUES ($1)', data)
await conn.close()
# Usage - same as before!
pipeline = DataPipeline()
data = pipeline.fetch_data("https://api.example.com/data")
pipeline.download_file("https://cdn.example.com/file.zip", "file.zip")
pipeline.save_to_db(data)
```
**Improvements**:
- ✅ 3 methods total (no duplication)
- ✅ Clean implementation
- ✅ No boilerplate
- ✅ Modern async libraries
- ✅ Same usage as before
---
## Migration Path
### From Sync Libraries to Async Libraries
#### Before (sync libraries)
```python
import requests
class APIClient:
def fetch_users(self):
response = requests.get("https://api.example.com/users")
return response.json()
# Usage
client = APIClient()
users = client.fetch_users()
```
#### After (async libraries + SmartAsync)
```python
import httpx
from smartasync import smartasync
class APIClient:
@smartasync
async def fetch_users(self):
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/users")
return response.json()
# Usage - SAME!
client = APIClient()
users = client.fetch_users() # No await needed!
```
**Migration steps**:
1. Install SmartAsync: `pip install smartasync`
2. Import decorator: `from smartasync import smartasync`
3. Add `@smartasync` to methods
4. Change method to `async def`
5. Use async library (httpx instead of requests)
6. Keep usage code unchanged!
---
## Anti-Patterns
### ❌ Anti-Pattern 1: Shared Instance in Multi-Threading
```python
# ❌ BAD: Shared instance across threads
fetcher = DataFetcher()
def worker(url):
data = fetcher.fetch(url) # Race condition!
process(data)
threads = [threading.Thread(target=worker, args=(url,)) for url in urls]
```
**Fix**: Use per-thread instances (see Issue #2 solutions above)
---
### ❌ Anti-Pattern 2: Nested `asyncio.run()`
```python
# ❌ BAD: Calling SmartAsync method from another async method with await
class Pipeline:
@smartasync
async def fetch(self, url: str):
...
async def process(self):
# This works but is unnecessary
data = await self.fetch(url) # Auto-detects async context correctly
```
**Fix**: This actually works fine! SmartAsync detects async context. Not an anti-pattern after all.
---
### ❌ Anti-Pattern 3: Using in Tight Performance Loop
```python
# ❌ BAD: Calling in performance-critical loop
fetcher = DataFetcher()
for i in range(1_000_000):
data = fetcher.fetch(f"https://api.example.com/item/{i}")
# 100μs × 1M = 100 seconds overhead!
```
**Fix**: Batch operations or use explicit async context:
```python
# ✅ GOOD: Batch in async context
async def fetch_all(urls):
fetcher = DataFetcher()
tasks = [fetcher.fetch(url) for url in urls]
return await asyncio.gather(*tasks)
# Run once
results = asyncio.run(fetch_all(urls))
```
---
## Testing Strategy
### Test Your Code Using SmartAsync
```python
# test_pipeline.py
import pytest
from pipeline import DataPipeline
def test_fetch_data_sync():
"""Test in sync context (fast)."""
pipeline = DataPipeline()
data = pipeline.fetch_data("https://api.example.com/test")
assert data['status'] == 'ok'
@pytest.mark.asyncio
async def test_fetch_data_async():
"""Test in async context (integration)."""
pipeline = DataPipeline()
data = await pipeline.fetch_data("https://api.example.com/test")
assert data['status'] == 'ok'
```
---
## Performance Characteristics
### Overhead Analysis
| Operation | Time | Impact |
|-----------|------|--------|
| Network request | 10-200ms | Baseline |
| SmartAsync overhead (sync) | ~0.1ms | **0.05-1%** |
| SmartAsync overhead (async) | ~0.002ms | **0.001%** |
**Conclusion**: Overhead is **negligible** for network I/O operations.
---
## Checklist for This Scenario
Before using SmartAsync in Scenario A1, verify:
- [ ] My app is **single-threaded** OR I can use **per-thread instances**
- [ ] I want to use **async libraries** (httpx, aiofiles, etc.)
- [ ] I'm okay with **~100μs overhead** per call in sync context
- [ ] I don't need **guaranteed thread safety** with shared instances
- [ ] I've read the **thread safety mitigations** (if multi-threaded)
---
## Summary
**Scenario A1** (Sync App → Async Libs) is **✅ SUITABLE** for SmartAsync with these conditions:
| Condition | Status | Notes |
|-----------|--------|-------|
| Single-threaded | ✅ PERFECT | Zero issues |
| Multi-threaded | ⚠️ CAUTION | Use per-thread instances |
| Multiprocess | ✅ PERFECT | Separate caches |
| Network I/O | ✅ PERFECT | Overhead negligible |
| Tight loops | ❌ AVOID | Use explicit async |
**Primary benefits**:
1. Use modern async libraries from sync code
2. No boilerplate or duplication
3. Future-proof for async migration
4. Clean, readable code
**Primary risks**:
1. Thread safety (mitigable)
2. Slight overhead (~100μs per sync call)
---
## Related Scenarios
- [02: Async App → Sync Libs](02-async-app-sync-libs.md) - Opposite direction
- [04: Unified Library API](04-unified-library-api.md) - Library author perspective
- [05: Gradual Migration](05-gradual-migration.md) - Migration strategies
---
## Further Reading
- [Technical Overview](../advanced/technical-overview.md) - Technical deep dive
- [Comparison](../advanced/comparison.md) - Comparison with alternatives
---
**Date**: 2025-11-10
**Scenario**: A1 - Sync App → Async Libs
**Status**: ✅ Suitable with thread safety awareness