# Story 5.29: Refactor Sink Writer Closures to Class

**Status:** Complete
**Priority:** High
**Depends on:** Story 5.25 (Extract Config Builders)

---

## Context / Background

The `_fanout_writer()` function in `src/fapilog/__init__.py:568-710` creates nested closures with captured state that are difficult to reason about:

```python
def _fanout_writer(sinks, *, parallel, circuit_config):
    writers = [_make_sink_writer(s) for s in sinks]
    breakers: dict[int, SinkCircuitBreaker] = {}

    async def _write_one(sink, write_fn, entry):  # Nested closure
        breaker = breakers.get(id(sink))
        # ... 40+ lines of error handling

    async def _write_sequential(entry):  # Another nested closure
        for i, (write, _) in enumerate(writers):
            await _write_one(sinks[i], write, entry)

    async def _write_parallel(entry):  # Yet another closure
        # ...
```

Problems with this pattern:
1. **Closure state is non-obvious:** `sinks`, `writers`, `breakers` are captured from outer scope
2. **Deep nesting:** Function inside function inside function (3 levels)
3. **Lazy imports:** Error handling imports `handle_sink_write_failure` inside try/except blocks
4. **Hard to debug:** "Where does sink write error go?" requires tracing through closures
5. **Hard to test:** Can't unit test `_write_one` in isolation

The Human Understandability Audit identified this as a **P1 issue** - nested closures create cognitive load.

---

## Scope (In / Out)

### In Scope

- Create `SinkWriterGroup` class (or similar) to replace closure-based approach
- Move `_write_one`, `_write_sequential`, `_write_parallel` logic to methods
- Make circuit breakers explicit instance state
- Move lazy imports to module level
- Maintain identical behavior

### Out of Scope

- Changing circuit breaker logic itself
- Modifying routing behavior (`_routing_or_fanout_writer`)
- Performance optimizations beyond structural refactoring

---

## Acceptance Criteria

### AC1: Class-Based Writer

**Description:** A class encapsulates sink writing logic with explicit state.

**Validation:**
```python
class SinkWriterGroup:
    def __init__(self, sinks: list, *, parallel: bool, circuit_config):
        self._sinks = sinks
        self._writers = [_make_sink_writer(s) for s in sinks]
        self._breakers = self._init_breakers(circuit_config)
        self._parallel = parallel

    async def write(self, entry: dict) -> None:
        """Write to all sinks (parallel or sequential)."""
        ...

    async def write_serialized(self, view: SerializedView) -> None:
        """Write serialized view to all sinks."""
        ...
```

### AC2: No Nested Functions

**Description:** Maximum function nesting depth is 1 (methods inside class).

- [ ] No `def` inside `def` patterns
- [ ] All write logic is in named methods
- [ ] Closure captures replaced with instance attributes

### AC3: Explicit Imports

**Description:** All imports at module top level.

- [ ] `handle_sink_write_failure` imported at module top
- [ ] No imports inside try/except blocks
- [ ] Comment added if lazy import truly required (circular dependency)

### AC4: Behavior Unchanged

**Description:** All existing tests pass without modification.

- [ ] Parallel write behavior identical
- [ ] Sequential write behavior identical
- [ ] Circuit breaker behavior identical
- [ ] Error containment behavior identical

---

## Implementation Notes

### File Changes

```
src/fapilog/__init__.py (MODIFIED - remove closures, use new class)
src/fapilog/core/sink_writers.py (NEW - SinkWriterGroup class)
tests/unit/core/test_sink_writers.py (NEW)
```

### Key Design

```python
# src/fapilog/core/sink_writers.py

from ..plugins.sinks.fallback import handle_sink_write_failure  # Top-level import

class SinkWriterGroup:
    """Manages writing to multiple sinks with circuit breaker protection."""

    __slots__ = ("_sinks", "_writers", "_breakers", "_parallel")

    def __init__(
        self,
        sinks: list[object],
        *,
        parallel: bool = False,
        circuit_config: SinkCircuitBreakerConfig | None = None,
    ) -> None:
        self._sinks = sinks
        self._writers = [_make_sink_writer(s) for s in sinks]
        self._breakers = self._create_breakers(circuit_config)
        self._parallel = parallel

    async def write(self, entry: dict[str, Any]) -> None:
        if self._parallel and len(self._writers) > 1:
            await self._write_parallel(entry)
        else:
            await self._write_sequential(entry)

    async def _write_one(self, idx: int, entry: dict) -> None:
        """Write to a single sink with circuit breaker protection."""
        sink = self._sinks[idx]
        write_fn = self._writers[idx][0]
        breaker = self._breakers.get(id(sink))

        if breaker and not breaker.should_allow():
            return  # Circuit open

        try:
            result = await write_fn(entry)
            if result is False:
                if breaker:
                    breaker.record_failure()
                await handle_sink_write_failure(entry, sink=sink, ...)
            elif breaker:
                breaker.record_success()
        except Exception as exc:
            if breaker:
                breaker.record_failure()
            await handle_sink_write_failure(entry, sink=sink, error=exc, ...)
```

---

## Tasks

### Phase 1: Create Class Structure

- [ ] Create `src/fapilog/core/sink_writers.py`
- [ ] Define `SinkWriterGroup` class with `__init__`
- [ ] Move `_make_sink_writer()` to new module
- [ ] Add type hints and docstrings

### Phase 2: Migrate Write Logic

- [ ] Implement `_write_one()` method
- [ ] Implement `_write_sequential()` method
- [ ] Implement `_write_parallel()` method
- [ ] Implement `write()` and `write_serialized()` public methods

### Phase 3: Integration

- [ ] Update `_fanout_writer()` to use `SinkWriterGroup`
- [ ] Move lazy imports to module top level
- [ ] Update `_routing_or_fanout_writer()` if needed
- [ ] Run full test suite

### Phase 4: Cleanup

- [ ] Remove old closure-based code from `__init__.py`
- [ ] Add unit tests for `SinkWriterGroup`
- [ ] Verify line count reduction in `__init__.py`

---

## Tests

### Unit Tests

- `tests/unit/core/test_sink_writers.py`
  - `SinkWriterGroup` instantiation with various configs
  - `_write_one` records success/failure correctly
  - Circuit breaker integration (open/closed states)
  - Parallel vs sequential write selection

### Integration Tests

- Existing sink write tests should pass unchanged
- Multi-sink fanout tests
- Circuit breaker end-to-end tests

---

## Definition of Done

### Code Complete

- [ ] `SinkWriterGroup` class implemented
- [ ] No nested function definitions in sink writer code
- [ ] All imports at module top level
- [ ] `__init__.py` reduced by ~150 lines

### Quality Assurance

- [ ] Unit tests for new class (>90% coverage)
- [ ] All existing tests pass
- [ ] `ruff check` passes
- [ ] `mypy` passes

### Documentation

- [ ] Class docstring explains purpose and usage
- [ ] Method docstrings explain behavior

---

## Risks / Rollback

### Risks

1. **Risk:** Subtle behavior change in error handling order
   - **Mitigation:** Compare execution traces before/after; add logging in tests

2. **Risk:** Performance regression from class overhead
   - **Mitigation:** Benchmark shows closure vs class negligible; use `__slots__`

### Rollback Plan

1. Revert commit
2. No data migration needed

---

## Related Stories

- **Depends on:** Story 5.25 - config builders extracted first
- **Enables:** Easier testing of sink write logic
- **Related:** Story 5.30 - similar code clarity improvements

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-14 | Initial draft | Claude |
