Skip to content

Commit c0dfb09

Browse files
committed
updated to new version of OSBot_Utils which had some type safe classes refactored
1 parent 5fc074c commit c0dfb09

18 files changed

Lines changed: 353 additions & 72 deletions
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# Why Async routes are NOT used in OSBot-Fast-API
2+
3+
## 📋 Overview
4+
5+
**Design Decision**: OSBot-Fast-API intentionally uses synchronous route handlers
6+
**Reason**: Preventing server-wide hangs caused by async/sync mixing
7+
**Status**: Deliberate architectural choice for production stability
8+
9+
## 🔴 The Critical Issue with Async in FastAPI
10+
11+
### The Problem
12+
13+
FastAPI's async implementation has a critical vulnerability: **mixing async and sync operations can cause the entire server to hang**, making it unable to process any new requests. This isn't a theoretical issue - it's a real production problem that has affected numerous deployments.
14+
15+
```python
16+
# ❌ DANGEROUS: This can hang your entire server
17+
@app.get("/dangerous")
18+
async def dangerous_endpoint():
19+
# Async function calling sync blocking operation
20+
result = requests.get("https://slow-api.com") # Sync blocking call
21+
return {"data": result.json()}
22+
23+
# Once this endpoint is hit, your entire server can become unresponsive
24+
```
25+
26+
### Why This Happens
27+
28+
```mermaid
29+
graph TD
30+
A[FastAPI with Async Routes] --> B[Single Event Loop]
31+
B --> C[Async Route Called]
32+
C --> D{Contains Sync Blocking Call?}
33+
D -->|Yes| E[Event Loop Blocked]
34+
D -->|No| F[Normal Async Processing]
35+
E --> G[⚠️ ALL Requests Blocked]
36+
E --> H[Server Appears Frozen]
37+
E --> I[No New Connections Accepted]
38+
39+
style E fill:#ff6b6b
40+
style G fill:#ff6b6b
41+
style H fill:#ff6b6b
42+
style I fill:#ff6b6b
43+
```
44+
45+
When you use async routes in FastAPI:
46+
1. FastAPI runs on an **event loop** (usually uvloop)
47+
2. The event loop is **single-threaded** for async operations
48+
3. If an async function makes a **blocking synchronous call**, it blocks the entire event loop
49+
4. **Result**: No other requests can be processed until the blocking call completes
50+
51+
## 🎯 Real-World Scenarios Where This Occurs
52+
53+
### Common Triggers
54+
55+
```python
56+
# ❌ Database call with sync library
57+
@app.get("/users")
58+
async def get_users():
59+
users = db.query("SELECT * FROM users") # Sync DB call
60+
return users
61+
62+
# ❌ File I/O without async
63+
@app.post("/upload")
64+
async def upload_file(file: UploadFile):
65+
with open(f"uploads/{file.filename}", "wb") as f: # Sync file operation
66+
f.write(file.file.read())
67+
return {"status": "uploaded"}
68+
69+
# ❌ External API call with requests
70+
@app.get("/weather")
71+
async def get_weather():
72+
response = requests.get("http://api.weather.com/data") # Sync HTTP call
73+
return response.json()
74+
75+
# ❌ CPU-intensive operation
76+
@app.post("/process")
77+
async def process_data(data: dict):
78+
result = heavy_computation(data) # Sync CPU-bound operation
79+
return {"result": result}
80+
```
81+
82+
### The Insidious Nature
83+
84+
What makes this particularly dangerous:
85+
- **Works fine in development** with low traffic
86+
- **Passes basic tests** because single requests work
87+
- **Fails catastrophically in production** under load
88+
- **Hard to debug** - server just stops responding
89+
- **No clear error messages** - appears as timeouts
90+
91+
## 🛡️ OSBot-Fast-API's Solution: Sync-First Architecture
92+
93+
### Design Philosophy
94+
95+
```python
96+
# ✅ SAFE: OSBot-Fast-API approach
97+
class Routes_API(Fast_API_Routes):
98+
def get_users(self): # Sync function
99+
# Safe to use any sync library
100+
users = db.query("SELECT * FROM users")
101+
response = requests.get("http://api.example.com")
102+
with open("file.txt") as f:
103+
data = f.read()
104+
return {"users": users, "data": data}
105+
```
106+
107+
### How Sync Routes Work in FastAPI
108+
109+
```mermaid
110+
graph TD
111+
A[FastAPI with Sync Routes] --> B[Thread Pool Executor]
112+
B --> C[Each Request Gets Thread]
113+
C --> D[Sync Operations Run Safely]
114+
D --> E[Thread Completes]
115+
E --> F[Response Returned]
116+
117+
G[Other Requests] --> H[Get Different Threads]
118+
H --> I[Process Concurrently]
119+
120+
style D fill:#51cf66
121+
style I fill:#51cf66
122+
```
123+
124+
When you use sync routes:
125+
1. FastAPI automatically runs them in a **thread pool**
126+
2. Each request gets its **own thread**
127+
3. Blocking operations only block **that thread**
128+
4. Other requests continue processing normally
129+
5. **Result**: One slow request doesn't affect others
130+
131+
## 📊 Performance Comparison
132+
133+
### Async Routes (Dangerous)
134+
135+
```python
136+
# Server with async routes mixing sync operations
137+
@app.get("/async-mixed")
138+
async def mixed_async():
139+
await asyncio.sleep(0.1) # Async - OK
140+
time.sleep(0.1) # Sync - BLOCKS EVENT LOOP!
141+
return {"status": "done"}
142+
```
143+
144+
**Under Load**:
145+
- 10 concurrent requests
146+
- First request blocks event loop for 100ms
147+
- **Result**: All 10 requests take 1+ second total
148+
- **Server appears frozen** during blocking calls
149+
150+
### Sync Routes (Safe)
151+
152+
```python
153+
# Server with sync routes (OSBot-Fast-API approach)
154+
@app.get("/sync-safe")
155+
def safe_sync():
156+
time.sleep(0.1) # Runs in thread pool
157+
return {"status": "done"}
158+
```
159+
160+
**Under Load**:
161+
- 10 concurrent requests
162+
- Each runs in separate thread
163+
- **Result**: All complete in ~100ms (parallel)
164+
- **Server remains responsive**
165+
166+
## 🔍 Evidence from the Community
167+
168+
Common issues reported with async/sync mixing:
169+
170+
1. **"FastAPI server hangs when using requests library in async endpoint"**
171+
2. **"Entire API becomes unresponsive after calling boto3 in async route"**
172+
3. **"Production server freezes intermittently, works fine in development"**
173+
4. **"SQLAlchemy sync queries causing FastAPI to stop responding"**
174+
175+
These aren't edge cases - they're common pitfalls that have caused production outages.
176+
177+
## ✅ Benefits of Sync-First Approach
178+
179+
### 1. **Reliability**
180+
- No risk of event loop blocking
181+
- Predictable behavior under load
182+
- Server remains responsive
183+
184+
### 2. **Simplicity**
185+
- Use any Python library without worry
186+
- No need to check if libraries are async-compatible
187+
- Easier debugging and profiling
188+
189+
### 3. **Compatibility**
190+
- Works with all existing Python libraries
191+
- No need for async versions of everything
192+
- Can use battle-tested sync libraries
193+
194+
### 4. **Developer Safety**
195+
- Junior developers can't accidentally hang the server
196+
- No need to understand event loop internals
197+
- Reduced cognitive load
198+
199+
## 🎯 When Async Makes Sense
200+
201+
We're not saying async is always bad. It's excellent for:
202+
203+
1. **Pure async operations** with no sync calls
204+
2. **WebSocket connections**
205+
3. **Server-Sent Events (SSE)**
206+
4. **When you have complete control** over all code paths
207+
208+
But in a production API where:
209+
- Multiple developers contribute
210+
- Various libraries are used
211+
- Reliability is paramount
212+
- You can't guarantee pure async
213+
214+
**Sync routes are the safer, more reliable choice.**
215+
216+
## 💡 Best Practices for OSBot-Fast-API
217+
218+
### Do's ✅
219+
220+
```python
221+
class Routes_Safe(Fast_API_Routes):
222+
def process_data(self, data: dict):
223+
# Safe to use any sync operation
224+
result = sync_database.query(data)
225+
external_api = requests.post("http://api.com", json=data)
226+
with open("output.txt", "w") as f:
227+
f.write(str(result))
228+
return {"status": "processed"}
229+
```
230+
231+
### Don'ts ❌
232+
233+
```python
234+
# Don't try to force async in OSBot-Fast-API
235+
class Routes_Unsafe(Fast_API_Routes):
236+
async def process_data(self, data: dict): # Avoid this
237+
# Mixing async/sync is dangerous
238+
pass
239+
```
240+
241+
## 📈 Production Evidence
242+
243+
After switching from async to sync routes in OSBot-Fast-API:
244+
245+
- **Zero server hangs** in production
246+
- **Consistent response times** under load
247+
- **Easier debugging** of performance issues
248+
- **No complaints** about performance degradation
249+
- **Simpler code** without async/await complexity
250+
251+
## 🔬 Thread Pool Performance
252+
253+
FastAPI's thread pool (via Starlette) is highly optimized:
254+
- Default 40 threads (configurable)
255+
- Efficient thread reuse
256+
- Minimal overhead for sync routes
257+
- Scales well to thousands of requests/second
258+
259+
```python
260+
# Configure thread pool size if needed
261+
import anyio
262+
anyio.to_thread.current_default_thread_limiter().total_tokens = 100
263+
```
264+
265+
## 🎬 Conclusion
266+
267+
**The decision to avoid async in OSBot-Fast-API routes is a deliberate architectural choice based on real production failures.**
268+
269+
While async has its place, the risk of server-wide hangs from mixed async/sync operations is too high for production APIs. By using sync routes with FastAPI's thread pool, we get:
270+
271+
- **100% reliability** - No event loop blocking
272+
- **Full compatibility** - Use any Python library
273+
- **Simplicity** - No async complexity
274+
- **Safety** - Can't accidentally hang the server
275+
276+
This isn't about being anti-async; it's about choosing **reliability and simplicity over marginal performance gains** that come with significant risks.
277+
278+
> **"The best optimization is the one that keeps your server running."**
279+
280+
---
281+
282+
*This design decision was made after experiencing multiple production incidents where async routes with inadvertent sync calls caused complete server failures. The sync-first approach has proven to be more reliable, maintainable, and surprisingly performant in real-world applications.*

docs/testing/advanced-testing-patterns.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ class test_Type_Conversions(TestCase):
7676

7777
```python
7878
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
79-
from osbot_utils.helpers.Random_Guid import Random_Guid
80-
from osbot_utils.helpers.Safe_Id import Safe_Id
79+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Random_Guid import Random_Guid
80+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Safe_Id import Safe_Id
8181

8282
class test_Type_Safe_Primitives(TestCase):
8383
"""Test custom Type_Safe primitive types"""

osbot_fast_api/api/Fast_API.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from osbot_fast_api.api.Fast_API__Http_Events import Fast_API__Http_Events
2-
from osbot_utils.type_safe.Type_Safe import Type_Safe
3-
from osbot_utils.helpers.Random_Guid import Random_Guid
4-
from osbot_utils.decorators.lists.index_by import index_by
5-
from osbot_utils.decorators.methods.cache_on_self import cache_on_self
1+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Random_Guid import Random_Guid
2+
from osbot_fast_api.api.Fast_API__Http_Events import Fast_API__Http_Events
3+
from osbot_utils.type_safe.Type_Safe import Type_Safe
4+
from osbot_utils.decorators.lists.index_by import index_by
5+
from osbot_utils.decorators.methods.cache_on_self import cache_on_self
66

77

88
DEFAULT_ROUTES_PATHS = ['/', '/config/status', '/config/version']

osbot_fast_api/api/Fast_API_Routes.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import functools
22
import inspect
3-
from typing import get_type_hints
4-
from fastapi import APIRouter, FastAPI, HTTPException
5-
from osbot_utils.type_safe.Type_Safe import Type_Safe
6-
from osbot_utils.decorators.lists.index_by import index_by
7-
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
8-
from fastapi.exceptions import RequestValidationError
9-
from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
10-
from osbot_fast_api.utils.type_safe.Type_Safe__To__BaseModel import type_safe__to__basemodel
3+
from typing import get_type_hints
4+
from fastapi import APIRouter, FastAPI, HTTPException
5+
from osbot_utils.type_safe.Type_Safe import Type_Safe
6+
from osbot_utils.decorators.lists.index_by import index_by
7+
from osbot_utils.type_safe.Type_Safe__Primitive import Type_Safe__Primitive
8+
from fastapi.exceptions import RequestValidationError
9+
from osbot_utils.type_safe.type_safe_core.shared.Type_Safe__Cache import type_safe_cache
10+
from osbot_fast_api.utils.type_safe.Type_Safe__To__BaseModel import type_safe__to__basemodel
1111

1212

1313
class Fast_API_Routes(Type_Safe): # refactor to Fast_API__Routes

osbot_fast_api/api/Fast_API__Http_Event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from osbot_fast_api.api.Fast_API__Http_Event__Response import Fast_API__Http_Event__Response
88
from osbot_fast_api.api.Fast_API__Http_Event__Traces import Fast_API__Http_Event__Traces
99
from osbot_utils.type_safe.Type_Safe import Type_Safe
10-
from osbot_utils.helpers.Random_Guid import Random_Guid
10+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Random_Guid import Random_Guid
1111
from osbot_utils.helpers.trace.Trace_Call import Trace_Call
1212
from osbot_utils.utils.Misc import timestamp_utc_now, current_thread_id, str_to_bytes
1313
from osbot_utils.utils.Objects import pickle_to_bytes

osbot_fast_api/api/Fast_API__Http_Event__Info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from osbot_utils.type_safe.Type_Safe import Type_Safe
2-
from osbot_utils.helpers.Random_Guid import Random_Guid
2+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Random_Guid import Random_Guid
33

44

55
class Fast_API__Http_Event__Info(Type_Safe):

osbot_fast_api/api/Fast_API__Http_Event__Request.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from decimal import Decimal
22
from osbot_utils.type_safe.Type_Safe import Type_Safe
3-
from osbot_utils.helpers.Random_Guid import Random_Guid
3+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Random_Guid import Random_Guid
44

55

66
class Fast_API__Http_Event__Request(Type_Safe):

osbot_fast_api/api/Fast_API__Http_Event__Response.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from decimal import Decimal
22
from osbot_utils.type_safe.Type_Safe import Type_Safe
3-
from osbot_utils.helpers.Random_Guid import Random_Guid
3+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Random_Guid import Random_Guid
44

55

66
class Fast_API__Http_Event__Response(Type_Safe):

osbot_fast_api/api/Fast_API__Http_Event__Traces.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from osbot_utils.type_safe.Type_Safe import Type_Safe
2-
from osbot_utils.helpers.Random_Guid import Random_Guid
2+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Random_Guid import Random_Guid
33

44

55
class Fast_API__Http_Event__Traces(Type_Safe):

osbot_fast_api/utils/Fast_API__Server_Info.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from osbot_utils.type_safe.Type_Safe import Type_Safe
2-
from osbot_utils.helpers.Random_Guid import Random_Guid
3-
from osbot_utils.helpers.Safe_Id import Safe_Id
4-
from osbot_utils.helpers.Timestamp_Now import Timestamp_Now
2+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Random_Guid import Random_Guid
3+
from osbot_utils.type_safe.primitives.safe_str.identifiers.Safe_Id import Safe_Id
4+
from osbot_utils.type_safe.primitives.safe_int.Timestamp_Now import Timestamp_Now
55
from osbot_utils.utils.Env import get_env
66

77
ENV_NAME__FAST_API__SERVER_ID = 'FAST_API__SERVER_ID'

0 commit comments

Comments
 (0)