# Waiting Strategies and Utilities

Robust container readiness detection, log monitoring, and condition waiting utilities for reliable test execution across different container types and startup behaviors. Essential for ensuring containers are fully ready before test execution begins.

## Capabilities

### Container Readiness Decorator

Decorator for automatic retry logic when connecting to containers, handling transient errors and ensuring reliable container readiness detection.

```python { .api }
def wait_container_is_ready(*transient_exceptions: type[BaseException]) -> Callable:
    """
    Decorator for container readiness checks with retry logic.
    
    Automatically retries decorated function until success or timeout.
    Handles common transient exceptions plus any additional specified exceptions.
    
    Args:
        *transient_exceptions: Additional exception types to treat as transient
        
    Returns:
        Decorator function that wraps the target method
        
    Usage:
        @wait_container_is_ready(CustomException)
        def _connect(self):
            # Connection logic that may fail transiently
            pass
    """
```

### Log-Based Waiting

Wait for specific log output to appear in container logs, supporting both string patterns and custom predicates.

```python { .api }
def wait_for_logs(
    container: DockerContainer,
    predicate: Union[Callable[..., bool], str],
    timeout: Union[float, None] = None,
    interval: float = 1,
    predicate_streams_and: bool = False,
    raise_on_exit: bool = False
) -> float:
    """
    Wait for specific log output from container.
    
    Args:
        container: Container to monitor
        predicate: String to search for or callable returning bool
        timeout: Maximum wait time in seconds
        interval: Polling interval in seconds
        predicate_streams_and: Apply predicate to both stdout and stderr
        raise_on_exit: Raise exception if container exits
        
    Returns:
        Time elapsed until condition was met
        
    Raises:
        TimeoutError: If timeout reached without condition being met
        ContainerStartException: If container exits unexpectedly
    """
```

### Generic Condition Waiting

Wait for arbitrary conditions to be met with configurable timeout and polling intervals.

```python { .api }
def wait_for(
    condition: Callable[[], bool],
    timeout: float = 120,
    interval: float = 1
) -> bool:
    """
    Wait for generic condition to be met.
    
    Args:
        condition: Function returning True when condition is met
        timeout: Maximum wait time in seconds
        interval: Polling interval in seconds
        
    Returns:
        True if condition was met, False if timeout reached
    """
```

## Configuration

### Global Timeout Settings

Container readiness waiting behavior is controlled by global configuration:

```python { .api }
from testcontainers.core.config import testcontainers_config

# Configure waiting behavior
testcontainers_config.max_tries: int = 120    # Maximum retry attempts
testcontainers_config.sleep_time: int = 1     # Sleep between retries (seconds)
testcontainers_config.timeout: int = 120      # Total timeout (seconds)
```

### Transient Exceptions

Default transient exceptions that trigger automatic retries:

```python { .api }
TRANSIENT_EXCEPTIONS = (TimeoutError, ConnectionError)
```

## Usage Examples

### Basic Log Waiting

```python
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs

# Wait for application startup message
with DockerContainer("my-app:latest") as container:
    # Wait for specific log message indicating readiness
    delay = wait_for_logs(container, "Server started successfully")
    print(f"Application ready after {delay:.2f} seconds")
    
    # Now safe to connect to the application
    app_port = container.get_exposed_port(8080)
    # Make requests to the application...
```

### Pattern Matching in Logs

```python
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs
import re

with DockerContainer("postgres:13") as postgres:
    postgres.with_env("POSTGRES_PASSWORD", "test")
    
    # Wait for PostgreSQL to be ready using regex pattern
    def postgres_ready(log_line):
        return re.search(r"database system is ready to accept connections", log_line) is not None
    
    delay = wait_for_logs(postgres, postgres_ready, timeout=30)
    print(f"PostgreSQL ready after {delay:.2f} seconds")
```

### Custom Condition Waiting

```python
from testcontainers.redis import RedisContainer
from testcontainers.core.waiting_utils import wait_for
import redis
import time

with RedisContainer() as redis_container:
    redis_client = redis_container.get_client()
    
    # Wait for Redis to accept connections
    def redis_ready():
        try:
            return redis_client.ping()
        except:
            return False
    
    success = wait_for(redis_ready, timeout=30, interval=0.5)
    if success:
        print("Redis is ready for connections")
    else:
        print("Redis failed to become ready within timeout")
```

### HTTP Endpoint Waiting

```python
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for
import requests

with DockerContainer("nginx:alpine") as web_server:
    web_server.with_exposed_ports(80)
    
    host = web_server.get_container_host_ip()
    port = web_server.get_exposed_port(80)
    
    # Wait for HTTP endpoint to respond
    def http_ready():
        try:
            response = requests.get(f"http://{host}:{port}/", timeout=1)
            return response.status_code == 200
        except:
            return False
    
    if wait_for(http_ready, timeout=60, interval=2):
        print("Web server is responding to HTTP requests")
        # Proceed with tests...
```

### Database Connection Waiting

```python
from testcontainers.postgres import PostgresContainer
from testcontainers.core.waiting_utils import wait_container_is_ready
import psycopg2

class CustomPostgresContainer(PostgresContainer):
    @wait_container_is_ready(psycopg2.OperationalError)
    def _connect(self):
        """Custom connection method with automatic retry."""
        conn = psycopg2.connect(self.get_connection_url())
        cursor = conn.cursor()
        cursor.execute("SELECT 1")
        cursor.fetchone()
        conn.close()

# Use custom container with automatic connection retry
with CustomPostgresContainer("postgres:13") as postgres:
    # Container automatically waits for successful connection
    connection_url = postgres.get_connection_url()
    print(f"PostgreSQL ready at: {connection_url}")
```

### Complex Readiness Checking

```python
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs, wait_for
import requests
import time

class WebAppContainer(DockerContainer):
    def __init__(self, image):
        super().__init__(image)
        self.with_exposed_ports(8080)
    
    def wait_for_readiness(self):
        """Wait for multiple readiness conditions."""
        # First, wait for application startup logs
        wait_for_logs(self, "Application started", timeout=60)
        
        # Then wait for health endpoint to respond
        host = self.get_container_host_ip()
        port = self.get_exposed_port(8080)
        
        def health_check():
            try:
                response = requests.get(f"http://{host}:{port}/health", timeout=2)
                return response.status_code == 200 and response.json().get("status") == "healthy"
            except:
                return False
        
        if not wait_for(health_check, timeout=30):
            raise Exception("Application failed health check")
        
        print("Application is fully ready")

# Use comprehensive readiness checking
with WebAppContainer("my-web-app:latest") as app:
    app.wait_for_readiness()
    # Application is now fully ready for testing
```

### Waiting with Custom Timeouts

```python
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs

# Different containers may need different timeout strategies
containers = [
    ("redis:6", "Ready to accept connections", 15),
    ("postgres:13", "database system is ready", 45),
    ("elasticsearch:7.15.0", "started", 120)
]

for image, log_pattern, timeout in containers:
    with DockerContainer(image) as container:
        try:
            delay = wait_for_logs(container, log_pattern, timeout=timeout)
            print(f"{image} ready after {delay:.2f}s")
        except TimeoutError:
            print(f"{image} failed to start within {timeout}s")
            # Handle timeout appropriately
```

### Parallel Container Startup

```python
from testcontainers.postgres import PostgresContainer
from testcontainers.redis import RedisContainer  
from testcontainers.core.waiting_utils import wait_for
import threading
import time

def start_and_wait(container, name):
    """Start container and wait for readiness."""
    container.start()
    
    # Different waiting strategies per container type
    if isinstance(container, PostgresContainer):
        def pg_ready():
            try:
                import psycopg2
                conn = psycopg2.connect(container.get_connection_url())
                conn.close()
                return True
            except:
                return False
        wait_for(pg_ready, timeout=45)
    
    elif isinstance(container, RedisContainer):
        client = container.get_client()
        wait_for(lambda: client.ping(), timeout=15)
    
    print(f"{name} is ready")

# Start containers in parallel
postgres = PostgresContainer("postgres:13")
redis = RedisContainer("redis:6")

threads = [
    threading.Thread(target=start_and_wait, args=(postgres, "PostgreSQL")),
    threading.Thread(target=start_and_wait, args=(redis, "Redis"))
]

start_time = time.time()
for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print(f"All containers ready in {time.time() - start_time:.2f} seconds")

# Clean up
postgres.stop()
redis.stop()
```

## Error Handling

### Common Exceptions

```python { .api }
from testcontainers.core.exceptions import (
    ContainerStartException,
    ContainerConnectException,
    TimeoutError
)

try:
    with DockerContainer("problematic-image") as container:
        wait_for_logs(container, "ready", timeout=30)
except ContainerStartException:
    print("Container failed to start")
except TimeoutError:
    print("Container did not become ready within timeout")
except ContainerConnectException:
    print("Failed to connect to container")
```

### Graceful Timeout Handling

```python
from testcontainers.core.waiting_utils import wait_for
import logging

def wait_with_fallback(condition, primary_timeout=60, fallback_timeout=30):
    """Wait with fallback strategy."""
    try:
        if wait_for(condition, timeout=primary_timeout):
            return True
        else:
            logging.warning(f"Primary wait timed out after {primary_timeout}s, trying fallback")
            return wait_for(condition, timeout=fallback_timeout, interval=0.1)
    except Exception as e:
        logging.error(f"Wait failed: {e}")
        return False
```