# Story 6.7: Simplify Resilience Infrastructure + Add Tenacity Compatibility

## Status: Complete

## Summary

Simplify the orphaned resilience infrastructure by removing unused code, streamlining the retry module to logging-appropriate defaults, and adding a protocol that enables Tenacity integration for power users—all without adding dependencies.

## Decision

**Chosen Approach: Lean Internal + Protocol Escape Hatch**

After analysis, the decision is:

1. **Remove** `fallback.py` entirely (638 lines) — unused, adds complexity
2. **Remove** `AsyncCircuitBreaker` and `CircuitBreakerManager` (~413 lines) — unused
3. **Keep** `SinkCircuitBreaker` (80 lines) — used internally for sink fault isolation
4. **Simplify** `retry.py` to logging-appropriate essentials (~250 lines vs ~530 lines)
5. **Add** `RetryProtocol` so enterprise users can plug in Tenacity if needed

## Motivation

### Why This Approach?

**Logging libraries are special.** They're loaded early in every application and should minimize dependencies:

| Library          | Required Dependencies |
| ---------------- | --------------------- |
| stdlib `logging` | 0                     |
| structlog        | 0                     |
| loguru           | 0                     |
| Fapilog (today)  | 2 (pydantic, pyyaml)  |

Adding Tenacity as a dependency would increase conflict risk. But not supporting it frustrates enterprise users who already use it.

### Retry for Logging is Different

Retry in a logging context has narrow requirements:

- **2-3 attempts** (logs shouldn't block the app)
- **Exponential backoff + jitter** (prevent thundering herd)
- **Fast failure** (drop logs rather than hang)

Fibonacci sequences, decorrelated jitter, and callback chains are for application-level retries (API calls, payments). For logging sinks, simple is correct.

## Implementation

### Phase 1: Delete Unused Code

**Delete `fallback.py` (638 lines):**

- `FallbackStrategy` enum
- `FallbackTrigger` enum
- `FallbackConfig` dataclass
- `FallbackProvider` ABC + 4 implementations
- `AsyncFallbackWrapper`
- `FallbackManager`
- `@fallback` decorator

**Delete from `circuit_breaker.py` (~413 lines):**

- `AsyncCircuitBreaker` class
- `CircuitBreakerManager` class
- `CircuitBreakerConfig` dataclass (used by AsyncCircuitBreaker)

**Keep in `circuit_breaker.py` (~80 lines):**

- `SinkCircuitBreaker` (used internally)
- `SinkCircuitBreakerConfig`

### Phase 2: Simplify `retry.py`

**Keep:**

```
RetryConfig (simplified fields):
  - max_attempts: int = 3
  - base_delay: float = 1.0
  - max_delay: float = 60.0
  - multiplier: float = 2.0
  - timeout_per_attempt: float | None
  - retryable_exceptions: list[type[Exception]]

AsyncRetrier (core implementation)
RetryExhaustedError
RetryStats
EXPONENTIAL strategy (default, only one needed)
EQUAL jitter (default)
FULL jitter (alternative)
```

**Remove:**

```
FIBONACCI strategy (unused, Tenacity has it)
LINEAR strategy (unused, just use multiplier=1 with EXPONENTIAL)
FIXED strategy (unused, set base_delay=max_delay)
DECORRELATED jitter (edge case)
@retry decorator (Tenacity's is better)
Predefined configs (document patterns instead)
RetryStrategy enum (only EXPONENTIAL remains)
```

**Result:** ~250 lines (down from ~530)

### Phase 3: Add Protocol for Tenacity Compatibility

**Add to `retry.py`:**

```python
from typing import Protocol, TypeVar, Callable, Awaitable

T = TypeVar("T")

class RetryCallable(Protocol):
    """Protocol for retry implementations.

    Fapilog's AsyncRetrier implements this protocol.
    Tenacity's AsyncRetrying can be adapted to it.
    """
    async def __call__(
        self,
        func: Callable[..., Awaitable[T]],
        *args,
        **kwargs
    ) -> T: ...
```

**Update sinks to accept protocol:**

```python
# In http_client.py, webhook.py
class HttpSinkConfig(BaseModel):
    retry: RetryCallable | RetryConfig | None = None
```

### Phase 4: Document Tenacity Integration

Add to documentation (not code):

```python
# Using Tenacity with Fapilog sinks
from tenacity import AsyncRetrying, stop_after_attempt, wait_exponential

class TenacityAdapter:
    """Adapt Tenacity to Fapilog's RetryCallable protocol."""

    def __init__(self, retrying: AsyncRetrying):
        self._retrying = retrying

    async def __call__(self, func, *args, **kwargs):
        async for attempt in self._retrying:
            with attempt:
                return await func(*args, **kwargs)

# Usage
from fapilog.plugins.sinks import HttpSink

sink = HttpSink(
    endpoint="https://api.example.com/logs",
    retry=TenacityAdapter(AsyncRetrying(
        stop=stop_after_attempt(5),
        wait=wait_exponential(multiplier=1, max=30),
    ))
)
```

## Files Changed

| File                                       | Action                 | Lines |
| ------------------------------------------ | ---------------------- | ----- |
| `src/fapilog/core/fallback.py`             | DELETE                 | -638  |
| `src/fapilog/core/circuit_breaker.py`      | MODIFY (remove unused) | -413  |
| `src/fapilog/core/retry.py`                | SIMPLIFY               | -280  |
| `src/fapilog/core/__init__.py`             | UPDATE exports         | ~10   |
| `src/fapilog/plugins/sinks/http_client.py` | Accept protocol        | ~5    |
| `src/fapilog/plugins/sinks/webhook.py`     | Accept protocol        | ~5    |
| `docs/guides/tenacity-integration.md`      | CREATE                 | ~50   |

**Net reduction:** ~1,300 lines

## Acceptance Criteria

- [ ] `fallback.py` deleted
- [ ] `AsyncCircuitBreaker`, `CircuitBreakerManager`, `CircuitBreakerConfig` deleted
- [ ] `SinkCircuitBreaker` and `SinkCircuitBreakerConfig` retained
- [ ] `retry.py` simplified to ~250 lines
- [ ] `RetryCallable` protocol defined
- [ ] Sinks accept `RetryCallable | RetryConfig | None`
- [ ] `@retry` decorator removed
- [ ] `RetryStrategy` and `JitterType` enums removed (hardcode EXPONENTIAL/EQUAL)
- [ ] All imports in codebase updated
- [ ] All tests pass
- [ ] Tenacity integration documented with example

## Test Plan

1. **Existing sink tests pass** — Retry behavior unchanged for default config
2. **Protocol test** — Verify custom retry callable works with HttpSink
3. **Tenacity integration test** — Add optional test (skipped if tenacity not installed)
4. **No import errors** — `from fapilog.core import ...` works without deleted symbols

## Estimated Effort

- Phase 1 (delete unused): 1 hour
- Phase 2 (simplify retry): 2 hours
- Phase 3 (add protocol): 1 hour
- Phase 4 (documentation): 1 hour
- Testing: 2 hours

**Total: 7 hours**

## Risk Assessment

**Risk Level: Low**

- Deleted code is unused (no external breakage)
- Retry simplification preserves the happy path (EXPONENTIAL + EQUAL jitter)
- Protocol is additive
- Well-tested sinks validate retry still works

## Dependencies

- Story 6.9 (Expose Resilience Patterns): **CANCELLED** — this story supersedes it
- Story 6.1 (Remove Unused Concurrency): Can be done in parallel

## Decision Record

**Decision:** Lean Internal + Protocol Escape Hatch
**Date:** 2026-01-07
**Rationale:**

1. Logging libraries should minimize dependencies
2. Retry for logging has narrow requirements (simple exponential backoff suffices)
3. Protocol enables Tenacity for the ~5% who need it without forcing a dependency
4. Removes ~1,300 lines of unused/over-engineered code
5. Maintains "better than structlog" positioning (structlog has no retry at all)
