---
phase: 36-fan-out-pyo3-bridge
plan: "01"
subsystem: streaming
tags: [rust, barsink, clickhouse, fan-out, pre-write-guards, live-engine]

requires:
  - phase: 34-clickhouse-writer-core
    provides: ClickHouseWriterSink, BarSink trait, FlushThreadMetrics
  - phase: 35-dead-letter-niffler
    provides: Dead-letter Parquet fallback for backpressure

provides:
  - Pre-write guard validation (validate_bar_for_write, dispatch_to_sinks)
  - Fan-out wiring in LiveBarEngine dispatching bars to BarSink vector
  - ExtraSinks type alias for shared sink access across symbol tasks
  - ch_writer_metrics() for flush thread metrics access from Python bindings

affects: [36-02-pyo3-bridge, sidecar, streaming-bindings]

tech-stack:
  added: []
  patterns:
    - "ExtraSinks type alias with cfg(feature) for clean dual-build"
    - "dispatch_to_sinks() centralizes guard + fan-out in one call"
    - "Arc<Mutex<Vec<Box<dyn BarSink>>>> for shared sink access across async tasks"

key-files:
  created:
    - crates/opendeviationbar-streaming/src/clickhouse_writer/guards.rs
  modified:
    - crates/opendeviationbar-streaming/src/clickhouse_writer/mod.rs
    - crates/opendeviationbar-streaming/src/clickhouse_writer/sink.rs
    - crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs
    - crates/opendeviationbar-streaming/src/live_engine/mod.rs
    - crates/opendeviationbar-streaming/src/live_engine/symbol_task.rs
    - crates/opendeviationbar-streaming/src/live_engine/puck.rs

key-decisions:
  - "ExtraSinks type alias (Option<Arc<Mutex<...>>>) avoids duplicating function signatures with #[cfg]"
  - "dispatch_to_sinks() helper in guards.rs centralizes guard+fan-out logic, called from 3 emission sites"
  - "CompletedBar.clone() at emission sites for sink dispatch (bar already cloned for ring buffer push)"

patterns-established:
  - "ExtraSinks = None when clickhouse-sink disabled, Option<Arc<Mutex<Vec<Box<dyn BarSink>>>>> when enabled"
  - "Ring buffer push ALWAYS before sink dispatch -- Python never blocked by sink errors"
  - "let _ = &extra_sinks; to suppress unused warnings without cfg_attr on parameters"

requirements-completed: [INTEG-01, INTEG-02, INTEG-05, INTEG-07]

duration: 12min
completed: 2026-03-29
---

# Phase 36 Plan 01: Pre-write Guards + Fan-Out Wiring Summary

**Pre-write guard validation and fan-out dispatch wiring in LiveBarEngine -- bars now dispatch to both ring buffer (Python) and ClickHouseWriterSink with fault isolation**

## Performance

- **Duration:** 12 min
- **Started:** 2026-03-29T10:35:49Z
- **Completed:** 2026-03-29T10:48:00Z
- **Tasks:** 2
- **Files modified:** 7

## Accomplishments

- Pre-write guards (INTEG-05): timestamp range [1e15, 2.5e15] us and negative volume rejection ported from Python bulk_operations.py
- Fan-out wiring (INTEG-01): completed bars dispatch to optional BarSink vector alongside ring buffer in all 3 emission paths (trade_dispatch, puck_fill, fill_from_rest)
- Conditional ClickHouseWriterSink creation (INTEG-02): from OPENDEVIATIONBAR_CH_HOSTS env var, with metrics access via ch_writer_metrics()
- Feature-flag hygiene: compiles cleanly with and without clickhouse-sink (181 tests pass, 0 clippy warnings)

## Task Commits

Each task was committed atomically:

1. **Task 1: Pre-write guards module** - `5c30447` (feat) -- TDD: guards.rs with 6 unit tests
2. **Task 2: Fan-out wiring in LiveBarEngine and trade_dispatch** - `a2c1c94` (feat) -- ExtraSinks, dispatch_to_sinks, all call sites wired

## Files Created/Modified

- `crates/opendeviationbar-streaming/src/clickhouse_writer/guards.rs` - Pre-write guard validation + dispatch_to_sinks helper
- `crates/opendeviationbar-streaming/src/clickhouse_writer/mod.rs` - Added pub mod guards
- `crates/opendeviationbar-streaming/src/clickhouse_writer/sink.rs` - Added shared_metrics() for Arc extraction
- `crates/opendeviationbar-streaming/src/live_engine/trade_dispatch.rs` - ExtraSinks type alias + extra_sinks parameter + fan-out after bar emission
- `crates/opendeviationbar-streaming/src/live_engine/mod.rs` - LiveBarEngine extra_sinks field, conditional ClickHouseWriterSink creation, ch_writer_metrics(), fan-out in fill_from_rest
- `crates/opendeviationbar-streaming/src/live_engine/symbol_task.rs` - extra_sinks parameter threaded to all process_trade and puck_fill calls
- `crates/opendeviationbar-streaming/src/live_engine/puck.rs` - extra_sinks parameter + fan-out after bar emission in gap fill

## Decisions Made

- **ExtraSinks type alias**: Used `Option<Arc<Mutex<Vec<Box<dyn BarSink>>>>>` with `Option<()>` when feature disabled. Avoids duplicating 12-parameter function signatures with `#[cfg]` blocks. Single parameter always present, unused suppressed with `let _ = &extra_sinks;`.
- **dispatch_to_sinks() helper**: Centralized guard + fan-out logic in guards.rs rather than inlining at each emission site. Called from trade_dispatch.rs, puck.rs, and mod.rs (fill_from_rest).
- **CompletedBar.clone() at emission**: Added `.clone()` at bar emission sites to pass bar to sinks after ring buffer push. CompletedBar is `Arc<str>` + `OpenDeviationBar` (value type), clone is cheap.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] Added dispatch_to_sinks() to puck.rs and fill_from_rest**

- **Found during:** Task 2 (Fan-out wiring)
- **Issue:** Plan focused on trade_dispatch.rs and symbol_task.rs, but puck_fill and fill_from_rest also have independent bar emission paths that bypass process_trade_through_processors
- **Fix:** Added extra_sinks parameter and fan-out dispatch to puck_fill() and fill_from_rest() emission sites
- **Files modified:** puck.rs, mod.rs
- **Verification:** All 181 tests pass, clippy clean
- **Committed in:** a2c1c94 (Task 2 commit)

---

**Total deviations:** 1 auto-fixed (Rule 3 - blocking)
**Impact on plan:** Essential for correctness -- without puck.rs and fill_from_rest fan-out, gap-fill and REST-backfill bars would bypass ClickHouseWriterSink.

## Issues Encountered

- Rust does not support `#[cfg]` on individual function parameters -- resolved by using ExtraSinks type alias pattern (Option<Arc<...>> vs Option<()>)
- FlushThreadMetrics contains AtomicU64 (not Clone) -- resolved by adding shared_metrics() returning Arc clone before sink is boxed

## User Setup Required

None - no external service configuration required.

## Next Phase Readiness

- Fan-out wiring complete: LiveBarEngine dispatches to BarSink vector
- Ready for Plan 02 (PyO3 bridge): expose ch_writer_metrics() and ClickHouseWriterSink creation to Python via streaming_bindings.rs
- INTEG-07 (aion hardcoded) already done in Phase 34 row.rs

## Known Stubs

None - all wiring is functional with no placeholder data.

---

_Phase: 36-fan-out-pyo3-bridge_
_Completed: 2026-03-29_
