Parent: #109
Scope
Native async client using asyncio + aiohttp or stdlib asyncio streams, removing the need for asyncio.to_thread() wrappers in async callers like veilrender's StorageManager.
Motivation
Current approach (and Phase 1/2) wraps synchronous operations in asyncio.to_thread(). This works but ties up thread pool workers for I/O-bound operations. A native async client avoids the overhead and integrates more naturally with async frameworks (FastAPI, aiohttp, etc.).
Deliverables
AsyncS3Client
Mirrors the S3Client API with async def methods:
class AsyncS3Client:
async def bucket_exists(self, bucket: str) -> bool: ...
async def make_bucket(self, bucket: str) -> None: ...
async def get_object(self, bucket: str, key: str) -> AsyncResponse: ...
async def put_object(self, bucket: str, key: str, data, length: int, ...) -> None: ...
# + Phase 2 ops as async
HTTP backend options (TBD)
- Option A:
aiohttp (external dep, feature-rich, widely used)
- Option B: stdlib
asyncio streams + manual HTTP/1.1 (zero deps, more work)
- Option C:
httpx async (external dep, clean API)
Decision: evaluate whether an external dep is acceptable for the async variant, or if zerodep philosophy requires stdlib-only. Could ship as a separate optional module (s3_async).
Shared signing logic
SigV4 signing from Phase 1 is sync/CPU-only, reusable as-is in async context — no changes needed there.
AsyncResponse
class AsyncResponse:
async def read(self) -> bytes: ...
async def aiter_bytes(self, chunk_size=65536) -> AsyncIterator[bytes]: ...
async def aclose(self) -> None: ...
async def __aenter__(self): ...
async def __aexit__(self, *args): ...
Notes
- If stdlib-only is required,
asyncio.open_connection() + manual HTTP/1.1 framing is feasible but non-trivial for chunked responses
- Streaming large
get_object responses without buffering is the main async advantage
- veilrender
StorageManager would drop all asyncio.to_thread() wrappers
Parent: #109
Scope
Native async client using
asyncio+aiohttpor stdlibasynciostreams, removing the need forasyncio.to_thread()wrappers in async callers like veilrender'sStorageManager.Motivation
Current approach (and Phase 1/2) wraps synchronous operations in
asyncio.to_thread(). This works but ties up thread pool workers for I/O-bound operations. A native async client avoids the overhead and integrates more naturally with async frameworks (FastAPI, aiohttp, etc.).Deliverables
AsyncS3ClientMirrors the
S3ClientAPI withasync defmethods:HTTP backend options (TBD)
aiohttp(external dep, feature-rich, widely used)asynciostreams + manual HTTP/1.1 (zero deps, more work)httpxasync (external dep, clean API)Decision: evaluate whether an external dep is acceptable for the async variant, or if zerodep philosophy requires stdlib-only. Could ship as a separate optional module (
s3_async).Shared signing logic
SigV4 signing from Phase 1 is sync/CPU-only, reusable as-is in async context — no changes needed there.
AsyncResponseNotes
asyncio.open_connection()+ manual HTTP/1.1 framing is feasible but non-trivial for chunked responsesget_objectresponses without buffering is the main async advantageStorageManagerwould drop allasyncio.to_thread()wrappers