# Story 5.30: Document Event Loop Lifecycle Logic

**Status:** Complete
**Priority:** High
**Depends on:** None

---

## Context / Background

The event loop detection and management logic in fapilog is complex but necessary. Two key locations contain this logic:

1. **`_start_plugins_sync()`** (`src/fapilog/__init__.py:843-886`) - Detects whether an event loop is running and chooses the appropriate strategy for starting plugins synchronously.

2. **`SyncLoggerFacade.start()`** (`src/fapilog/core/logger.py:136-193`) - Determines whether to bind to an existing event loop or start a dedicated worker thread.

The code handles multiple scenarios:
- No event loop running (simple case)
- Event loop running in current thread (must offload to thread)
- Event loop in different thread (can use directly)

**The problem:** The code is correct but lacks explanation of *why* each branch exists. Without comments, developers may:
- "Simplify" the code and break edge cases
- Not understand why their logger isn't starting
- Introduce bugs when modifying startup logic

The Human Understandability Audit identified this as a **P1 issue** - the logic is non-obvious and needs documentation.

---

## Scope (In / Out)

### In Scope

- Add comprehensive inline comments to `_start_plugins_sync()`
- Add comprehensive inline comments to `SyncLoggerFacade.start()`
- Add docstrings explaining threading constraints
- Create architecture doc explaining async/sync boundary

### Out of Scope

- Changing the event loop handling logic
- Refactoring the code structure
- Adding new event loop handling modes

---

## Acceptance Criteria

### AC1: _start_plugins_sync() Documented

**Description:** Each branch in `_start_plugins_sync()` has a comment explaining when and why it executes.

**Validation:**
```python
def _start_plugins_sync(...):
    """Start plugins from a sync context.

    Threading Constraints:
    - If called from a thread with a running event loop, we cannot
      use asyncio.run() (would raise "loop already running")
    - Solution: offload to a ThreadPoolExecutor that has no loop

    Scenarios handled:
    1. No event loop -> use asyncio.run() directly
    2. Event loop running -> offload to thread pool
    3. Any exception -> return original plugins (fail-open)
    """

    try:
        asyncio.get_running_loop()
        # Case 2: Loop is running - cannot use asyncio.run() here.
        # Offload to a thread that has no event loop.
        import concurrent.futures
        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
            ...
    except RuntimeError:
        # Case 1: No running loop - safe to use asyncio.run()
        try:
            return _run_sync()
        except Exception:
            # Case 3: Startup failed - return originals (fail-open for logging)
            return enrichers, redactors, processors, filters
```

### AC2: SyncLoggerFacade.start() Documented

**Description:** The `start()` method explains the two modes (bound loop vs. thread loop).

**Validation:**
```python
def start(self) -> None:
    """Start the logger worker.

    Two Modes:
    1. Bound Loop Mode: If an event loop is already running, bind to it.
       The worker task runs in that loop. Shutdown requires the loop to
       still be running.

    2. Thread Loop Mode: If no event loop exists, create a dedicated
       thread with its own event loop. This is the common case for sync
       code (CLI tools, scripts, non-async web frameworks).

    Why Thread Mode Exists:
    - Sync code (non-async) has no event loop
    - We need an event loop for async sinks and workers
    - Solution: dedicated background thread with its own loop
    """
```

### AC3: Architecture Documentation

**Description:** New doc explains the async/sync boundary design.

- [ ] Create `docs/architecture/async-sync-boundary.md`
- [ ] Explain why SyncLoggerFacade wraps async internals
- [ ] Diagram showing thread/loop relationships
- [ ] Document common debugging scenarios

### AC4: No Code Behavior Changes

- [ ] All existing tests pass
- [ ] No functional changes to startup logic
- [ ] Only comments and documentation added

---

## Implementation Notes

### File Changes

```
src/fapilog/__init__.py (MODIFIED - add comments to _start_plugins_sync)
src/fapilog/core/logger.py (MODIFIED - add comments to start())
docs/architecture/async-sync-boundary.md (NEW)
```

### Key Comments to Add

**In `_start_plugins_sync()`:**
```python
def _start_plugins_sync(...) -> tuple[...]:
    """Start plugins synchronously, handling event loop edge cases.

    This function must work in three scenarios:

    1. Called from sync code with no event loop:
       - asyncio.get_running_loop() raises RuntimeError
       - Safe to use asyncio.run() directly

    2. Called from within a running event loop (e.g., Jupyter, async framework):
       - asyncio.get_running_loop() succeeds
       - Cannot use asyncio.run() (raises "loop already running")
       - Must offload to a separate thread with no loop

    3. Startup fails for any reason:
       - Return original unstarted plugins
       - Fail-open: logging should never crash the application

    Threading Safety:
    - ThreadPoolExecutor is used with max_workers=1 to serialize startup
    - 5-second timeout prevents hangs if plugins are slow
    """
```

**In `SyncLoggerFacade.start()`:**
```python
def start(self) -> None:
    """Start the background worker.

    Determines the appropriate mode based on event loop state:

    BOUND LOOP MODE (loop already running):
    - Detected via asyncio.get_running_loop() succeeding
    - Worker task created in the existing loop
    - Used when: running inside an async framework, Jupyter, etc.
    - Shutdown: must happen while loop is still running

    THREAD LOOP MODE (no running loop):
    - Detected via asyncio.get_running_loop() raising RuntimeError
    - Creates a dedicated background thread with its own event loop
    - Used when: sync scripts, CLI tools, non-async web frameworks
    - Shutdown: thread is signaled to stop, then joined

    Why not always use thread mode?
    - Existing event loops expect tasks to run in them
    - Thread mode would prevent proper integration with async frameworks
    - Bound mode allows drain() to work correctly with the caller's loop
    """
```

---

## Tasks

### Phase 1: Code Comments

- [ ] Add module-level docstring update explaining async/sync boundary
- [ ] Add comprehensive docstring to `_start_plugins_sync()`
- [ ] Add inline comments for each branch in `_start_plugins_sync()`
- [ ] Add comprehensive docstring to `SyncLoggerFacade.start()`
- [ ] Add inline comments for bound vs thread mode logic

### Phase 2: Architecture Documentation

- [ ] Create `docs/architecture/async-sync-boundary.md`
- [ ] Add diagram showing event loop scenarios
- [ ] Document "Why does my logger hang?" debugging guide
- [ ] Link from code comments to architecture doc

### Phase 3: Cross-References

- [ ] Add "See Also" references in logger.py pointing to docs
- [ ] Update any existing architecture docs that reference startup

---

## Tests

No new tests needed - this is documentation only.

### Manual Verification

- [ ] Read through all new comments and verify technical accuracy
- [ ] Have someone unfamiliar with codebase review for clarity
- [ ] Verify architecture doc matches actual code behavior

---

## Definition of Done

### Code Complete

- [ ] All comments added to `_start_plugins_sync()`
- [ ] All comments added to `SyncLoggerFacade.start()`
- [ ] No linting errors (comment formatting)

### Quality Assurance

- [ ] All existing tests still pass
- [ ] Comments reviewed for technical accuracy
- [ ] No functional changes introduced

### Documentation

- [ ] Architecture doc created
- [ ] Diagram included
- [ ] Cross-references in code

---

## Risks / Rollback

### Risks

1. **Risk:** Comments become stale if logic changes
   - **Mitigation:** Add note in CLAUDE.md or contributing guide to update comments when modifying startup logic

### Rollback Plan

1. Revert commit (documentation-only, no risk)

---

## Related Stories

- **Related:** Story 5.27 - Pipeline documentation (similar pattern)
- **Related:** Story 5.29 - Refactor sink writer closures to class
- **Enables:** Easier debugging of startup issues

---

## Change Log

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