# Story 4.28: Non-Blocking Audit Trail File I/O

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

---

## Context / Background

The GPT-5.2 audit identified that `AuditTrail._store_event()` performs synchronous file I/O inside an async method, potentially blocking the event loop during high-volume audit scenarios.

**Current behavior in `packages/fapilog-audit/src/fapilog_audit/audit.py:516-543`:**

```python
async def _store_event(self, event: AuditEvent) -> None:
    # ... serialization under async lock ...

    # Store to file (default implementation)
    date_str = event.timestamp.strftime("%Y-%m-%d")
    log_file = self.storage_path / f"audit_{date_str}.jsonl"

    with open(log_file, "a", encoding="utf-8") as f:  # BLOCKING
        f.write(json_line + "\n")                      # BLOCKING
```

**Problem:**
- `open()` and `f.write()` are synchronous I/O operations
- When called frequently, this blocks the event loop
- Can cause latency spikes in async applications

---

## Scope (In / Out)

### In Scope

- Convert file I/O to non-blocking using `asyncio.to_thread()`
- Maintain atomic write semantics (single append operation)
- Preserve existing file rotation by date behavior
- Benchmark improvement

### Out of Scope

- Changing to async file libraries (aiofiles) - adds dependency
- Buffered/batched writes - changes semantics (future story)
- Alternative storage backends (already covered by 4.10)

---

## Acceptance Criteria

### AC1: File I/O Uses asyncio.to_thread

**Description:** The `_store_event` method offloads file operations to a thread pool.

**Validation:**
```python
# packages/fapilog-audit/src/fapilog_audit/audit.py
async def _store_event(self, event: AuditEvent) -> None:
    # ...
    await asyncio.to_thread(self._write_to_file, log_file, json_line)

def _write_to_file(self, path: Path, content: str) -> None:
    with open(path, "a", encoding="utf-8") as f:
        f.write(content + "\n")
```

### AC2: Event Loop Not Blocked

**Description:** Concurrent audit logging does not block other async operations.

**Validation:**
```python
from fapilog_audit import AuditTrail, AuditEventType

async def test_audit_does_not_block_loop():
    trail = AuditTrail()
    await trail.start()

    # Measure time for concurrent operations
    async def other_work():
        await asyncio.sleep(0.001)
        return True

    async def audit_burst():
        for i in range(100):
            await trail.log_event(AuditEventType.DATA_ACCESS, f"event {i}")

    # Both should complete without one blocking the other
    results = await asyncio.gather(audit_burst(), other_work())
    assert results[1] is True  # other_work completed
```

### AC3: Integrity Chain Preserved

**Description:** Hash chain integrity verification still works correctly.

**Validation:**
```python
from fapilog_audit import AuditTrail, AuditEventType

async def test_chain_integrity_after_async_writes():
    trail = AuditTrail()
    await trail.start()

    for i in range(10):
        await trail.log_event(AuditEventType.DATA_ACCESS, f"event {i}")

    await trail.drain()
    result = await trail.verify_chain_from_storage()
    assert result.valid is True
```

### AC4: Error Handling Preserved

**Description:** File write errors are still contained and don't crash the audit system.

**Validation:**
```python
from pathlib import Path
from fapilog_audit import AuditTrail, AuditEventType

async def test_write_error_contained():
    trail = AuditTrail(storage_path=Path("/nonexistent/path"))
    await trail.start()

    # Should not raise, error is contained
    await trail.log_event(AuditEventType.DATA_ACCESS, "test")
    await trail.drain()
```

---

## Implementation Notes

### File Changes

```
packages/fapilog-audit/src/fapilog_audit/audit.py (MODIFIED - async file I/O)
packages/fapilog-audit/tests/test_audit_async_io.py (NEW)
```

### Key Code Change

```python
async def _store_event(self, event: AuditEvent) -> None:
    """Store audit event to configured storage."""
    try:
        async with self._lock:
            self._seq_counter += 1
            event.sequence_number = self._seq_counter
            event.previous_hash = self._last_hash

            payload = event.model_dump(mode="json", exclude_none=False)
            checksum = self._compute_checksum(payload)
            event.checksum = checksum
            payload["checksum"] = checksum
            payload["previous_hash"] = event.previous_hash
            payload["sequence_number"] = event.sequence_number
            self._last_hash = checksum

            json_line = self._canonical_json(payload)

        # Offload file I/O to thread pool (non-blocking)
        date_str = event.timestamp.strftime("%Y-%m-%d")
        log_file = self.storage_path / f"audit_{date_str}.jsonl"
        await asyncio.to_thread(self._write_to_file, log_file, json_line)

    except Exception:
        # Storage failure - critical for compliance but contained
        pass

def _write_to_file(self, path: Path, content: str) -> None:
    """Synchronous file write, called via to_thread."""
    with open(path, "a", encoding="utf-8") as f:
        f.write(content + "\n")
```

---

## Tasks

### Phase 1: Core Implementation

- [ ] Extract file write to separate sync method `_write_to_file`
- [ ] Wrap call in `asyncio.to_thread()`
- [ ] Ensure lock only covers serialization, not I/O

### Phase 2: Testing

- [ ] Add unit test for non-blocking behavior
- [ ] Add test for chain integrity preservation
- [ ] Add test for error containment
- [ ] Verify existing audit tests pass

### Phase 3: Documentation

- [ ] Update docstring for `_store_event`
- [ ] Update CHANGELOG

---

## Tests

### Unit Tests

- `packages/fapilog-audit/tests/test_audit_async_io.py`
  - `test_store_event_uses_thread_pool`
  - `test_concurrent_audit_does_not_block`
  - `test_chain_integrity_preserved`
  - `test_write_error_contained`

### Integration Tests

- Existing `packages/fapilog-audit/tests/` audit tests should continue to pass

---

## Definition of Done

### Code Complete

- [ ] File I/O offloaded to thread pool
- [ ] Lock scope reduced to serialization only
- [ ] Error handling preserved

### Quality Assurance

- [ ] All audit tests pass
- [ ] New async I/O tests pass
- [ ] `ruff check` passes
- [ ] `mypy` passes

### Documentation

- [ ] Docstrings updated
- [ ] CHANGELOG updated

---

## Risks / Rollback

### Risks

1. **Risk:** Thread pool exhaustion under extreme load
   - **Mitigation:** Default thread pool is sufficient; document for high-volume users

2. **Risk:** Subtle ordering issues with to_thread
   - **Mitigation:** Lock still serializes the hash chain computation

### Rollback Plan

If issues occur:
1. Revert to synchronous writes
2. Investigate async file libraries as alternative

---

## Related Stories

- **Related:** Story 4.10 - Pluggable audit storage (alternative backends)
- **Related:** Story 4.11 - Audit integrity HMAC

---

## Code Review

**Date:** 2026-01-18
**Reviewer:** Claude
**Verdict:** OK to PR

### Summary

Reviewed implementation of non-blocking audit trail file I/O using `asyncio.to_thread()`.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: File I/O Uses asyncio.to_thread | `audit.py:537` - `await asyncio.to_thread(self._write_to_file, ...)` |
| AC2: Event Loop Not Blocked | `test_audit_async_io.py:45-66` - concurrent audit + sleep both complete |
| AC3: Integrity Chain Preserved | `audit.py:659-660` - sorts by sequence_number; test verifies 10-event chain |
| AC4: Error Handling Preserved | `audit.py:539-541` - exception contained; test verifies trail operational after failure |

### Quality Gates

- [x] ruff check passed
- [x] ruff format passed
- [x] mypy passed
- [x] diff-cover 100%
- [x] No weak assertions
- [x] All 64 audit tests pass

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-18 | Implementation complete, code review passed | Claude |
| 2026-01-16 | Initial draft from GPT-5.2 audit | Claude |
