Skip to content

Commit 55a07d0

Browse files
desertaxleclaude
andcommitted
Update sync dependency examples to avoid blocking I/O
Replace examples showing blocking operations (file I/O, locks) with non-blocking patterns: - Pure computations (config merging, param building) - In-memory operations (dict/set access) - Quick transformations Add clear guidance that sync dependencies should NEVER include blocking I/O operations. All I/O must use async dependencies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a1f0bac commit 55a07d0

File tree

2 files changed

+122
-57
lines changed

2 files changed

+122
-57
lines changed

docs/dependencies.md

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,40 @@ Timeouts work alongside retries. If a task times out, it can be retried accordin
160160

161161
## Custom Dependencies
162162

163-
Create your own dependencies using `Depends()` for reusable resources and patterns. Dependencies can be either synchronous or asynchronous:
163+
Create your own dependencies using `Depends()` for reusable resources and patterns. Dependencies can be either synchronous or asynchronous.
164+
165+
**Important**: Synchronous dependencies should **NOT** include blocking I/O operations (file access, network calls, database queries, etc.). Use async dependencies for any I/O. Sync dependencies are best for:
166+
- Pure computations
167+
- In-memory data structure access
168+
- Configuration lookups from memory
169+
- Non-blocking transformations
164170

165171
### Synchronous Dependencies
166172

173+
Use sync dependencies for pure computations and in-memory operations:
174+
167175
```python
168176
from docket import Depends
169177

178+
# In-memory config lookup - no I/O
170179
def get_config() -> dict:
171-
"""Simple sync dependency that returns configuration."""
172-
return {"api_key": "secret", "timeout": 30}
180+
"""Access configuration from memory."""
181+
return {"api_url": "https://api.example.com", "timeout": 30}
182+
183+
# Pure computation - no I/O
184+
def build_request_headers(config: dict = Depends(get_config)) -> dict:
185+
"""Construct headers from config."""
186+
return {
187+
"User-Agent": "MyApp/1.0",
188+
"Timeout": str(config["timeout"])
189+
}
173190

174-
async def call_api(config: dict = Depends(get_config)) -> None:
175-
# Config is provided automatically
176-
api_key = config["api_key"]
177-
# ... make API call ...
191+
async def call_api(
192+
headers: dict = Depends(build_request_headers)
193+
) -> None:
194+
# Headers are computed without blocking
195+
# Network I/O happens here (async)
196+
response = await http_client.get(url, headers=headers)
178197
```
179198

180199
### Asynchronous Dependencies
@@ -203,56 +222,60 @@ async def process_user_data(
203222

204223
### Synchronous Context Managers
205224

225+
Use sync context managers only for managing in-memory resources or quick non-blocking operations:
226+
206227
```python
207228
from contextlib import contextmanager
208229
from docket import Depends
209230

231+
# In-memory resource tracking - no I/O
210232
@contextmanager
211-
def get_file_lock(filename: str = "data.txt"):
212-
"""Sync context manager for file locking."""
213-
lock = acquire_lock(filename)
233+
def track_operation(operation_name: str):
234+
"""Track operation execution without blocking."""
235+
operations_in_progress.add(operation_name) # In-memory set
214236
try:
215-
yield lock
237+
yield operation_name
216238
finally:
217-
release_lock(filename)
239+
operations_in_progress.remove(operation_name)
218240

219-
async def write_data(
220-
data: str,
221-
lock=Depends(lambda: get_file_lock("shared.txt"))
241+
async def process_data(
242+
tracker=Depends(lambda: track_operation("data_processing"))
222243
) -> None:
223-
# File is locked before task starts, unlocked after completion
224-
with open("shared.txt", "a") as f:
225-
f.write(data)
244+
# Operation tracked in memory, no blocking
245+
await perform_async_work()
226246
```
227247

228248
### Mixed Sync and Async Dependencies
229249

230-
You can freely mix synchronous and asynchronous dependencies in the same task:
250+
You can freely mix synchronous and asynchronous dependencies in the same task. Use sync for computations, async for I/O:
231251

232252
```python
253+
# Sync - in-memory config lookup
233254
def get_local_config() -> dict:
234-
"""Sync dependency - no I/O needed."""
235-
return {"setting": "value"}
255+
"""Access local config from memory - no I/O."""
256+
return {"retry_count": 3, "batch_size": 100}
236257

258+
# Async - network I/O
237259
async def get_remote_config() -> dict:
238-
"""Async dependency - requires network I/O."""
239-
response = await http_client.get("/config")
260+
"""Fetch remote config via network - requires I/O."""
261+
response = await http_client.get("/api/config")
240262
return await response.json()
241263

242-
@contextmanager
243-
def get_temp_file():
244-
"""Sync context manager."""
245-
with tempfile.NamedTemporaryFile() as f:
246-
yield f
247-
248-
async def complex_task(
264+
# Sync - pure computation
265+
def merge_configs(
249266
local: dict = Depends(get_local_config),
250-
remote: dict = Depends(get_remote_config),
251-
temp_file=Depends(get_temp_file)
267+
remote: dict = Depends(get_remote_config)
268+
) -> dict:
269+
"""Merge configs without blocking - pure computation."""
270+
return {**local, **remote}
271+
272+
async def process_batch(
273+
config: dict = Depends(merge_configs)
252274
) -> None:
253-
# All dependencies are resolved correctly
254-
config = {**local, **remote}
255-
temp_file.write(json.dumps(config).encode())
275+
# Config is computed/fetched appropriately
276+
# Now do the actual I/O work
277+
for i in range(config["batch_size"]):
278+
await process_item(i, retries=config["retry_count"])
256279
```
257280

258281
### Nested Dependencies
@@ -374,6 +397,41 @@ If `unreliable_dependency` fails, the task won't execute and the error will be l
374397

375398
## Dependency Guidelines
376399

400+
### Choose Sync vs Async Appropriately
401+
402+
**Use synchronous dependencies for:**
403+
- Pure computations (math, string manipulation, data transformations)
404+
- In-memory data structure access (dicts, lists, sets)
405+
- Configuration lookups from memory
406+
- Non-blocking operations that complete instantly
407+
408+
**Use asynchronous dependencies for:**
409+
- Network I/O (HTTP requests, API calls)
410+
- File I/O (reading/writing files)
411+
- Database queries
412+
- Any operation that involves `await`
413+
- Resource management requiring async cleanup
414+
415+
```python
416+
# ✅ Good: Sync for pure computation
417+
def calculate_batch_size(item_count: int) -> int:
418+
return min(item_count, 1000)
419+
420+
# ✅ Good: Async for I/O
421+
async def fetch_user_data(user_id: int) -> dict:
422+
return await api_client.get(f"/users/{user_id}")
423+
424+
# ❌ Bad: Sync with blocking I/O
425+
def load_config_from_file() -> dict:
426+
with open("config.json") as f: # Blocks the event loop!
427+
return json.load(f)
428+
429+
# ✅ Good: Use async for file I/O instead
430+
async def load_config_from_file() -> dict:
431+
async with aiofiles.open("config.json") as f:
432+
return json.loads(await f.read())
433+
```
434+
377435
### Design for Reusability
378436

379437
Create dependencies that can be used across multiple tasks:

src/docket/dependencies.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -511,36 +511,43 @@ def Depends(dependency: DependencyFunction[R]) -> R:
511511
- Asynchronous context managers (using @asynccontextmanager)
512512
513513
If a dependency returns a context manager, it will be entered and exited around
514-
the task, giving an opportunity to control the lifetime of a resource, like a
515-
database connection.
514+
the task, giving an opportunity to control the lifetime of a resource.
515+
516+
**Important**: Synchronous dependencies should NOT include blocking I/O operations
517+
(file access, network calls, database queries, etc.). Use async dependencies for
518+
any I/O. Sync dependencies are best for:
519+
- Pure computations
520+
- In-memory data structure access
521+
- Configuration lookups from memory
522+
- Non-blocking transformations
516523
517524
Examples:
518525
519526
```python
520-
# Sync function dependency
527+
# Sync dependency - pure computation, no I/O
521528
def get_config() -> dict:
522-
return {"setting": "value"}
523-
524-
# Async function dependency
529+
# Access in-memory config, no I/O
530+
return {"api_url": "https://api.example.com", "timeout": 30}
531+
532+
# Sync dependency - compute value from arguments
533+
def build_query_params(
534+
user_id: int = TaskArgument(),
535+
config: dict = Depends(get_config)
536+
) -> dict:
537+
# Pure computation, no I/O
538+
return {"user_id": user_id, "timeout": config["timeout"]}
539+
540+
# Async dependency - I/O operations
525541
async def get_user(user_id: int = TaskArgument()) -> User:
526-
return await fetch_user(user_id)
527-
528-
# Sync context manager dependency
529-
from contextlib import contextmanager
530-
531-
@contextmanager
532-
def get_file_handle():
533-
f = open("data.txt")
534-
try:
535-
yield f
536-
finally:
537-
f.close()
542+
# Network I/O - must be async
543+
return await fetch_user_from_api(user_id)
538544
539-
# Async context manager dependency
545+
# Async context manager - I/O resource management
540546
from contextlib import asynccontextmanager
541547
542548
@asynccontextmanager
543549
async def get_db_connection():
550+
# I/O operations - must be async
544551
conn = await db.connect()
545552
try:
546553
yield conn
@@ -549,11 +556,11 @@ async def get_db_connection():
549556
550557
@task
551558
async def my_task(
552-
config: dict = Depends(get_config),
559+
params: dict = Depends(build_query_params),
560+
user: User = Depends(get_user),
553561
db: Connection = Depends(get_db_connection),
554562
) -> None:
555-
print(config)
556-
await db.execute("SELECT 1")
563+
await db.execute("UPDATE users SET ...", params)
557564
```
558565
"""
559566
return cast(R, _Depends(dependency))

0 commit comments

Comments
 (0)