Skip to content

s3 Phase 3: Async-native client (AsyncS3Client) #112

Description

@elena-oaklight

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Priority 3 - Medium-Low

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions