# Async Conventions

## Overview

The SDK provides both synchronous and asynchronous APIs with identical interfaces. This document defines patterns for dual sync/async implementation.

## Dual API Pattern

### Client Structure

**Provide both sync and async clients**:

```python
# Sync client
class Client:
    """Synchronous Pinecone client."""

    def list_indexes(self) -> list[str]:
        response = self._http.get("/indexes")
        return response.json()

    def create_index(self, name: str, dimension: int) -> Index:
        response = self._http.post("/indexes", json={
            "name": name,
            "dimension": dimension
        })
        return Index.from_response(response)

# Async client
class AsyncClient:
    """Asynchronous Pinecone client."""

    async def list_indexes(self) -> list[str]:
        response = await self._http.get("/indexes")
        return response.json()

    async def create_index(self, name: str, dimension: int) -> Index:
        response = await self._http.post("/indexes", json={
            "name": name,
            "dimension": dimension
        })
        return Index.from_response(response)
```

### Method Naming

**Same names** for sync and async methods:

```python
# ✅ Good: Same method names
class Client:
    def list_indexes(self) -> list[str]: ...
    def create_index(self, name: str) -> Index: ...

class AsyncClient:
    async def list_indexes(self) -> list[str]: ...
    async def create_index(self, name: str) -> Index: ...

# ❌ Bad: Different names
class Client:
    def list_indexes(self) -> list[str]: ...

class AsyncClient:
    async def alist_indexes(self) -> list[str]: ...  # Don't add prefix
    async def list_indexes_async(self) -> list[str]: ...  # Don't add suffix
```

## Async Patterns

### Async Context Managers

**Use `async with` for async clients**:

```python
# ✅ Good: Async context manager
class AsyncClient:
    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close()
        return False

# Usage
async with AsyncPinecone(api_key="...") as pc:
    indexes = await pc.list_indexes()

# ❌ Bad: Sync context manager for async
class AsyncClient:
    def __enter__(self):  # Wrong! Use __aenter__
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):  # Wrong! Use __aexit__
        self.close()
```

### Async Iteration

**Use `async for` for iterators**:

```python
# ✅ Good: Async iterator
class AsyncQueryIterator:
    def __aiter__(self):
        return self

    async def __anext__(self) -> QueryResult:
        if not self._has_more:
            raise StopAsyncIteration
        return await self._fetch_next()

# Usage
async for result in client.query_iterator(vector=[...]):
    process(result)

# ❌ Bad: Sync iterator for async operations
class AsyncQueryIterator:
    def __iter__(self):  # Wrong! Use __aiter__
        return self

    def __next__(self):  # Wrong! Use __anext__
        return self._fetch_next()  # Missing await
```

### Async Initialization

**Use factory functions for async initialization**:

```python
# ✅ Good: Factory function for async setup
class AsyncClient:
    def __init__(self, api_key: str):
        self._api_key = api_key
        self._http: AsyncHTTPClient | None = None

    @classmethod
    async def create(cls, api_key: str) -> AsyncClient:
        """Create and initialize async client."""
        client = cls(api_key)
        client._http = await AsyncHTTPClient.create(api_key)
        return client

# Usage
pc = await AsyncPinecone.create(api_key="...")

# ❌ Bad: Async in __init__
class AsyncClient:
    async def __init__(self, api_key: str):  # Can't use async __init__
        self._http = await AsyncHTTPClient.create(api_key)
```

### Thread and Event Loop Safety

**AsyncHTTPClient uses lazy initialization**:

The SDK's `AsyncHTTPClient` defers creation of `httpx.AsyncClient` until the first async method call. This ensures the client is created in the correct async context, making it safe to instantiate anywhere and use across different event loops.

```python
# ✅ Good: Safe to create client in sync context
def setup():
    # Client can be created before event loop exists
    pc = AsyncPinecone(api_key="...")
    return pc

async def main():
    pc = setup()  # Created in sync function

    # First async call initializes httpx.AsyncClient
    response = await pc.inference.embed(
        model="multilingual-e5-large",
        texts=["Hello"],
    )

    await pc.close()

# ✅ Good: Module-level client for web frameworks
# Client at module level
pc = AsyncPinecone(api_key="...")

@app.get("/embed")
async def embed_handler(text: str):
    # httpx.AsyncClient created in request handler's event loop
    response = await pc.inference.embed(
        model="multilingual-e5-large",
        texts=[text],
    )
    return response.embeddings[0]

@app.on_event("shutdown")
async def shutdown():
    await pc.close()
```

**Always use async context managers or explicit cleanup**:

```python
# ✅ Good: Async context manager
async with AsyncPinecone(api_key="...") as pc:
    await pc.inference.embed(model="...", texts=["..."])

# ✅ Good: Explicit cleanup
pc = AsyncPinecone(api_key="...")
try:
    await pc.inference.embed(model="...", texts=["..."])
finally:
    await pc.close()

# ❌ Bad: No cleanup
pc = AsyncPinecone(api_key="...")
await pc.inference.embed(model="...", texts=["..."])
# Connections not closed!
```

## HTTP Client Layer

### Dual HTTP Clients

**Separate sync and async HTTP clients**:

```python
# Sync HTTP client
class HTTPClient:
    def __init__(self):
        self._client = httpx.Client(http2=True)

    def request(
        self,
        method: str,
        url: str,
        **kwargs
    ) -> httpx.Response:
        return self._client.request(method, url, **kwargs)

    def close(self) -> None:
        self._client.close()

# Async HTTP client
class AsyncHTTPClient:
    def __init__(self):
        self._client = httpx.AsyncClient(http2=True)

    async def request(
        self,
        method: str,
        url: str,
        **kwargs
    ) -> httpx.Response:
        return await self._client.request(method, url, **kwargs)

    async def close(self) -> None:
        await self._client.close()
```

### Shared Configuration

**Use same config for both**:

```python
# ✅ Good: Shared configuration
class HTTPConfig(Struct):
    timeout: int = 30
    max_retries: int = 3
    max_connections: int = 100

class HTTPClient:
    def __init__(self, config: HTTPConfig):
        self._config = config
        self._client = httpx.Client(
            timeout=config.timeout,
            limits=httpx.Limits(max_connections=config.max_connections)
        )

class AsyncHTTPClient:
    def __init__(self, config: HTTPConfig):
        self._config = config
        self._client = httpx.AsyncClient(
            timeout=config.timeout,
            limits=httpx.Limits(max_connections=config.max_connections)
        )

# ❌ Bad: Different configs
class HTTPClient:
    def __init__(self, timeout: int, max_connections: int): ...

class AsyncHTTPClient:
    def __init__(self, config: dict): ...  # Different interface
```

### Shared Constants

**Centralize magic strings and values in `_internal/constants.py`**:

Magic strings like API versions, header names, and other constant values used across both sync and async implementations should be defined once in a central location to prevent drift and make updates easier.

```python
# ✅ Good: Centralized constants
# pinecone/_internal/constants.py
"""Internal constants used across the SDK."""

INFERENCE_API_VERSION = "2025-10"
DEFAULT_TIMEOUT = 30.0
MAX_BATCH_SIZE = 1000

# pinecone/inference.py
from pinecone._internal.constants import INFERENCE_API_VERSION

class Inference:
    def _make_request(self, endpoint: str) -> dict:
        headers = {
            "X-Pinecone-Api-Version": INFERENCE_API_VERSION,
        }
        return self._http.post(endpoint, headers=headers)

# pinecone/async_inference.py
from pinecone._internal.constants import INFERENCE_API_VERSION

class AsyncInference:
    async def _make_request(self, endpoint: str) -> dict:
        headers = {
            "X-Pinecone-Api-Version": INFERENCE_API_VERSION,
        }
        return await self._http.post(endpoint, headers=headers)

# ❌ Bad: Duplicated constants
# pinecone/inference.py
INFERENCE_API_VERSION = "2025-10"  # Duplicated!

class Inference:
    def _make_request(self, endpoint: str) -> dict:
        headers = {
            "X-Pinecone-Api-Version": INFERENCE_API_VERSION,
        }
        return self._http.post(endpoint, headers=headers)

# pinecone/async_inference.py
INFERENCE_API_VERSION = "2025-10"  # Duplicated - can drift!

class AsyncInference:
    async def _make_request(self, endpoint: str) -> dict:
        headers = {
            "X-Pinecone-Api-Version": INFERENCE_API_VERSION,
        }
        return await self._http.post(endpoint, headers=headers)
```

**Benefits of centralized constants**:

- **Single source of truth**: Update API version in one place
- **Prevents drift**: Sync and async implementations stay in sync
- **Easier maintenance**: One change updates all usages
- **Clear ownership**: Constants live in `_internal/` with other implementation details

## Protocol Definitions

### Protocol for Sync and Async

**Use separate protocols for sync and async** (SDK pattern):

```python
# ✅ Good: Separate protocols for sync and async
@runtime_checkable
class InferenceNamespaceProtocol(Protocol):
    """Protocol for sync inference operations."""

    def embed(
        self,
        model: str,
        *,
        texts: list[str],
        parameters: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> EmbeddingResponseProtocol:
        """Generate embeddings for texts."""
        ...

    def rerank(
        self,
        model: str,
        *,
        query: str,
        documents: list[str],
        top_n: int | None = None,
        return_documents: bool = False,
        parameters: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> RerankResponseProtocol:
        """Rerank documents by relevance."""
        ...

@runtime_checkable
class AsyncInferenceNamespaceProtocol(Protocol):
    """Protocol for async inference operations."""

    async def embed(
        self,
        model: str,
        *,
        texts: list[str],
        parameters: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> EmbeddingResponseProtocol:
        """Generate embeddings for texts."""
        ...

    async def rerank(
        self,
        model: str,
        *,
        query: str,
        documents: list[str],
        top_n: int | None = None,
        return_documents: bool = False,
        parameters: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> RerankResponseProtocol:
        """Rerank documents by relevance."""
        ...

# Alternative: Single protocol with union return types
# (Not used in this SDK - harder to type check and maintain)
@runtime_checkable
class UnifiedProtocol(Protocol):
    def list(self) -> list[str] | Coroutine[Any, Any, list[str]]:
        """List indexes (sync or async)."""
        ...
```

## Async Best Practices

### Avoid Sync Code in Async

**Don't call blocking code in async functions**:

```python
# ✅ Good: Use async operations
async def fetch_data() -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get("/data")  # Non-blocking
    return response.json()

# ❌ Bad: Blocking calls in async
async def fetch_data() -> dict:
    response = httpx.get("/data")  # Blocks event loop!
    return response.json()

# ❌ Bad: time.sleep in async
async def retry_with_backoff():
    time.sleep(1)  # Blocks event loop! Use asyncio.sleep
    return await fetch()
```

### Use asyncio.sleep

**Use `asyncio.sleep` not `time.sleep`**:

```python
import asyncio

# ✅ Good: Non-blocking sleep
async def retry_with_backoff(attempt: int) -> None:
    wait_time = 2 ** attempt
    await asyncio.sleep(wait_time)

# ❌ Bad: Blocking sleep
async def retry_with_backoff(attempt: int) -> None:
    wait_time = 2 ** attempt
    time.sleep(wait_time)  # Blocks event loop!
```

### Gather for Concurrency

**Use `asyncio.gather` for concurrent requests**:

```python
# ✅ Good: Concurrent requests
async def fetch_multiple_indexes(names: list[str]) -> list[Index]:
    tasks = [client.get_index(name) for name in names]
    return await asyncio.gather(*tasks)

# ❌ Bad: Sequential async requests
async def fetch_multiple_indexes(names: list[str]) -> list[Index]:
    indexes = []
    for name in names:
        index = await client.get_index(name)  # One at a time
        indexes.append(index)
    return indexes
```

### Handle Async Exceptions

**Handle exceptions in gather**:

```python
# ✅ Good: Handle partial failures with return_exceptions
async def fetch_multiple_indexes(names: list[str]) -> list[Index | Exception]:
    tasks = [client.get_index(name) for name in names]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    return results

# ✅ Good: Filter successful results
async def fetch_multiple_indexes_filtered(names: list[str]) -> list[Index]:
    tasks = [client.get_index(name) for name in names]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    return [r for r in results if isinstance(r, Index)]

# ✅ Good: Use wrapper functions for specific error handling
async def fetch_multiple_indexes_safe(names: list[str]) -> list[Index | None]:
    tasks = [safe_get_index(name) for name in names]
    return await asyncio.gather(*tasks)

async def safe_get_index(name: str) -> Index | None:
    try:
        return await client.get_index(name)
    except NotFoundError:
        return None

# ❌ Bad: One failure stops all (default behavior)
async def fetch_multiple_indexes(names: list[str]) -> list[Index]:
    tasks = [client.get_index(name) for name in names]
    return await asyncio.gather(*tasks)  # First error propagates, stops all
```

## Rate Limiting

### Async Rate Limiter

**Implement async-safe rate limiting**:

```python
import asyncio

class AsyncRateLimiter:
    def __init__(self, tokens: int, period: float):
        self.tokens = tokens
        self.period = period
        self.available = tokens
        self.last_update = asyncio.get_event_loop().time()
        self._lock = asyncio.Lock()

    async def acquire(self) -> None:
        async with self._lock:
            now = asyncio.get_event_loop().time()
            elapsed = now - self.last_update
            self.available = min(
                self.tokens,
                self.available + elapsed * (self.tokens / self.period)
            )
            self.last_update = now

            if self.available < 1:
                wait_time = (1 - self.available) * (self.period / self.tokens)
                await asyncio.sleep(wait_time)
                self.available = 0
            else:
                self.available -= 1
```

### Sync Rate Limiter

**Implement thread-safe rate limiting**:

```python
import threading
import time

class RateLimiter:
    def __init__(self, tokens: int, period: float):
        self.tokens = tokens
        self.period = period
        self.available = tokens
        self.last_update = time.time()
        self._lock = threading.Lock()

    def acquire(self) -> None:
        with self._lock:
            now = time.time()
            elapsed = now - self.last_update
            self.available = min(
                self.tokens,
                self.available + elapsed * (self.tokens / self.period)
            )
            self.last_update = now

            if self.available < 1:
                wait_time = (1 - self.available) * (self.period / self.tokens)
                time.sleep(wait_time)
                self.available = 0
            else:
                self.available -= 1
```

## Testing Async Code

### Async Test Functions

**Use `pytest-asyncio` for async tests**:

```python
import pytest

# ✅ Good: Async test
@pytest.mark.asyncio
async def test_async_list_indexes():
    async with AsyncPinecone(api_key="test") as pc:
        indexes = await pc.list_indexes()
        assert isinstance(indexes, list)

# ❌ Bad: Sync test for async code
def test_async_list_indexes():
    pc = AsyncPinecone(api_key="test")
    indexes = pc.list_indexes()  # Missing await
    assert isinstance(indexes, list)
```

### Mock Async Functions

**Mock async functions correctly**:

```python
from unittest.mock import AsyncMock

# ✅ Good: AsyncMock for async functions
async def test_with_mock():
    pc = AsyncPinecone(api_key="test")
    pc._http.get = AsyncMock(return_value={"indexes": []})

    indexes = await pc.list_indexes()
    assert indexes == []

# ❌ Bad: Regular Mock for async
async def test_with_mock():
    pc = AsyncPinecone(api_key="test")
    pc._http.get = Mock(return_value={"indexes": []})  # Wrong!

    indexes = await pc.list_indexes()  # Doesn't work
```

## Anti-Patterns

### ❌ Mixing Sync and Async

```python
# Bad: Calling sync from async
async def fetch_data():
    result = sync_function()  # Blocks event loop
    return result

# Bad: Calling async from sync without running
def process_data():
    result = async_function()  # Returns coroutine, doesn't run
    return result
```

**Solution**: Keep sync and async separate.

```python
# Good: Async calls async
async def fetch_data():
    result = await async_function()
    return result

# Good: Sync calls sync
def process_data():
    result = sync_function()
    return result
```

### ❌ Unnecessary Async

```python
# Bad: Async with no await
async def get_config() -> dict:
    return {"timeout": 30}  # No async operations

# Bad: Wrapping sync in async
async def add_numbers(a: int, b: int) -> int:
    return a + b  # CPU-bound, no benefit from async
```

**Solution**: Only use async when you have async operations.

```python
# Good: Sync when no async operations
def get_config() -> dict:
    return {"timeout": 30}

def add_numbers(a: int, b: int) -> int:
    return a + b
```

### ❌ Not Awaiting Coroutines

```python
# Bad: Forgot await
async def fetch_data():
    data = client.get_data()  # Returns coroutine, doesn't execute
    return data  # Returns coroutine, not data

# Bad: Awaiting non-async
async def process():
    result = await sync_function()  # Can't await sync function
```

**Solution**: Await async functions, don't await sync.

```python
# Good: Await async function
async def fetch_data():
    data = await client.get_data()
    return data

# Good: Call sync function directly
async def process():
    result = sync_function()
    return result
```

## Performance Considerations

### Connection Pooling

**Reuse client instances**:

```python
# ✅ Good: Reuse client
async def main():
    async with AsyncPinecone(api_key="...") as pc:
        # Multiple requests reuse same connection pool
        await pc.list_indexes()
        await pc.create_index("index1", 1536)
        await pc.create_index("index2", 1536)

# ❌ Bad: New client per request
async def main():
    async with AsyncPinecone(api_key="...") as pc:
        await pc.list_indexes()

    async with AsyncPinecone(api_key="...") as pc:  # New connection pool
        await pc.create_index("index1", 1536)
```

### Batch Operations

**Batch requests when possible**:

```python
# ✅ Good: Batch upsert
async def upsert_many(pc: AsyncPinecone, vectors: list[Vector]) -> None:
    await pc.upsert(vectors=vectors)  # Single request

# ❌ Bad: Individual upserts
async def upsert_many(pc: AsyncPinecone, vectors: list[Vector]) -> None:
    for vector in vectors:
        await pc.upsert(vectors=[vector])  # Multiple requests
```

## References

- See [architecture.md](./architecture.md) for client organization
- See [types.md](./types.md) for async type hints
- See [testing.md](./testing.md) for async testing patterns
- See [http-client.md](./http-client.md) for HTTP client implementation
