Skip to content

Commit 070c907

Browse files
committed
Document erlang.async_call() in threading guide and changelog
1 parent c65e2ba commit 070c907

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22

3+
## 1.2.1 (2026-02-16)
4+
5+
### Added
6+
7+
- **Asyncio Support** - New `erlang.async_call()` for asyncio-compatible callbacks
8+
- `await erlang.async_call('func', arg1, arg2)` - Call Erlang from async Python code
9+
- Integrates with asyncio event loop via `add_reader()`
10+
- No exceptions raised for control flow (unlike `erlang.call()`)
11+
- Releases dirty NIF thread while waiting (non-blocking)
12+
- Works with FastAPI, Starlette, aiohttp, and other ASGI frameworks
13+
- Supports concurrent calls via `asyncio.gather()`
14+
- New test: `test_async_call` in `py_reentrant_SUITE.erl`
15+
- New test module: `test/py_test_async.py`
16+
- Updated documentation: `docs/threading.md` - Added Asyncio Support section
17+
18+
### Fixed
19+
20+
- **Flag-based callback detection in replay path** - Fixed SuspensionRequired exceptions
21+
leaking when ASGI middleware catches and re-raises exceptions. The replay path in
22+
`nif_resume_callback_dirty` now uses flag-based detection (checking `tl_pending_callback`)
23+
instead of exception-type detection.
24+
325
## 1.2.0 (2026-02-15)
426

527
### Added

docs/threading.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,93 @@ for t in threads:
156156
t.join()
157157
```
158158

159+
## Asyncio Support
160+
161+
For asyncio applications (FastAPI, Starlette, aiohttp, etc.), use `erlang.async_call()`
162+
instead of `erlang.call()`. This integrates properly with asyncio's event loop without
163+
blocking or raising exceptions for control flow.
164+
165+
### Basic Usage
166+
167+
```python
168+
import asyncio
169+
import erlang
170+
171+
async def fetch_data():
172+
result = await erlang.async_call('get_user', user_id)
173+
return result
174+
175+
# Run in asyncio context
176+
asyncio.run(fetch_data())
177+
```
178+
179+
### With FastAPI/Starlette
180+
181+
```python
182+
from fastapi import FastAPI
183+
import erlang
184+
185+
app = FastAPI()
186+
187+
@app.get("/user/{user_id}")
188+
async def get_user(user_id: int):
189+
# Use async_call for asyncio compatibility
190+
user = await erlang.async_call('get_user', user_id)
191+
return {"user": user}
192+
193+
@app.websocket("/ws")
194+
async def websocket_endpoint(websocket):
195+
await websocket.accept()
196+
while True:
197+
data = await websocket.receive_text()
198+
# Safe to use in WebSocket handlers
199+
result = await erlang.async_call('process_message', data)
200+
await websocket.send_text(result)
201+
```
202+
203+
### Concurrent Async Calls
204+
205+
```python
206+
import asyncio
207+
import erlang
208+
209+
async def parallel_queries():
210+
# Run multiple Erlang calls concurrently
211+
results = await asyncio.gather(
212+
erlang.async_call('query_a', param1),
213+
erlang.async_call('query_b', param2),
214+
erlang.async_call('query_c', param3)
215+
)
216+
return results
217+
```
218+
219+
### Why Use async_call?
220+
221+
The standard `erlang.call()` uses a suspension mechanism that raises a
222+
`SuspensionRequired` exception internally. While this works in most contexts,
223+
it can cause issues with ASGI middleware that catches and handles exceptions:
224+
225+
- ASGI middleware (Starlette, FastAPI) catches exceptions during request handling
226+
- The `SuspensionRequired` exception propagates through middleware layers
227+
- asyncio logs "Task exception was never retrieved" warnings
228+
229+
`erlang.async_call()` avoids these issues by:
230+
- Not raising exceptions for control flow
231+
- Integrating with asyncio's event loop via `add_reader()`
232+
- Releasing the dirty NIF thread while waiting (non-blocking)
233+
- Using a Future that resolves when the Erlang callback completes
234+
235+
### When to Use Each
236+
237+
| Context | Recommended API |
238+
|---------|-----------------|
239+
| Synchronous Python code | `erlang.call()` |
240+
| Threading (`threading.Thread`) | `erlang.call()` |
241+
| ThreadPoolExecutor | `erlang.call()` |
242+
| Asyncio (async/await) | `erlang.async_call()` |
243+
| FastAPI/Starlette | `erlang.async_call()` |
244+
| ASGI applications | `erlang.async_call()` |
245+
159246
## Error Handling
160247

161248
Errors from Erlang functions are raised as Python exceptions:
@@ -174,3 +261,18 @@ thread = threading.Thread(target=worker)
174261
thread.start()
175262
thread.join()
176263
```
264+
265+
### Async Error Handling
266+
267+
```python
268+
import asyncio
269+
import erlang
270+
271+
async def safe_call():
272+
try:
273+
result = await erlang.async_call('maybe_fail', 42)
274+
return result
275+
except RuntimeError as e:
276+
print(f"Erlang error: {e}")
277+
return None
278+
```

0 commit comments

Comments
 (0)