---
phase: 34-core-clickhouse-writer
plan: 02
subsystem: streaming/clickhouse_writer
tags: [rust, clickhouse, streaming, barsink, flush-thread, retry]
dependency_graph:
  requires:
    [
      ClickHouseBarRow,
      compute_dedup_token,
      ClickHouseWriterConfig,
      ClickHouseSummary,
    ]
  provides:
    [
      ClickHouseWriterSink,
      spawn_flush_thread,
      FlushThreadMetrics,
      discover_plugin_columns,
    ]
  affects: [crates/opendeviationbar-streaming]
tech_stack:
  added: [backon-1.6]
  patterns:
    [
      dedicated-flush-thread,
      bounded-channel-backpressure,
      transient-error-classification,
    ]
key_files:
  created:
    - crates/opendeviationbar-streaming/src/clickhouse_writer/flush_thread.rs
    - crates/opendeviationbar-streaming/src/clickhouse_writer/sink.rs
    - crates/opendeviationbar-streaming/src/clickhouse_writer/schema.rs
  modified:
    - crates/opendeviationbar-streaming/Cargo.toml
    - crates/opendeviationbar-streaming/src/clickhouse_writer/mod.rs
    - crates/opendeviationbar-streaming/src/clickhouse_writer/row.rs
    - crates/opendeviationbar-streaming/src/engine/mod.rs
decisions:
  - Box<ClickHouseBarRow> in FlushCommand::Bar to avoid large enum variant (clippy)
  - std::mem::take instead of drain().collect() for buffer ownership transfer
  - Transient error classification via BadResponse string parsing (clickhouse crate abstracts HTTP status)
  - clickhouse::Row derive added to ClickHouseBarRow (required for insert API)
  - tokio dev-dependency added for async test support
metrics:
  duration: 747s
  completed: "2026-03-29T08:34:09Z"
  tasks_completed: 2
  tasks_total: 2
  tests_added: 18
  files_created: 3
  files_modified: 4
---

# Phase 34 Plan 02: Flush Thread + BarSink Summary

Dedicated flush thread with triple-trigger batching (N/T/shutdown), exponential backoff retry via backon, transient error classification for HTTP 429/503, and ClickHouseWriterSink implementing BarSink with non-blocking channel-based on_bar().

## Tasks Completed

### Task 1: Flush thread with batch buffering, HTTP POST, retry, and summary verification

- Created `flush_thread.rs` with `FlushCommand` enum, `FlushThreadMetrics` atomic counters, and `spawn_flush_thread()`
- Dedicated `std::thread` with single-threaded tokio runtime (clickhouse crate is async)
- Triple-trigger flush: buffer >= `max_rows` (N), `flush_period_ms` timeout (T), explicit Flush/Shutdown command
- Per-batch dedup token computed via `compute_batch_dedup_token()` and set as `insert_deduplication_token` option
- `source_start_ts`/`source_end_ts` and `cache_key` populated on all rows before insert
- Retry via `backon::ExponentialBuilder` (1s min, 16s max delay, configurable max_times)
- `is_transient_error()` classifies: 429/500/502/503/network/timeout -> retry; 400/401/4xx -> fail immediately
- On all-retries-exhausted: increment `bars_failed` + `dead_letter_count` (dead-letter writing is Phase 35)
- Added `clickhouse::Row` derive to `ClickHouseBarRow` (required by `client.insert()` API)
- Added `backon` and `tokio` (dev-dep) to Cargo.toml
- 14 unit tests: error classification (8), flush triggers (4), dedup token, summary mismatch

**Commit:** 5943f80

### Task 2: ClickHouseWriterSink BarSink implementation + plugin column discovery

- `ClickHouseWriterSink` implementing `BarSink` with `SyncSender<FlushCommand>` for non-blocking `on_bar()`
- `try_send()` semantics: `Ok` -> bars_sent++, `Full` -> bars_dropped++ + `SinkError::Recoverable`, `Disconnected` -> `SinkError::Unrecoverable`
- `flush()` sends `FlushCommand::Flush` to the thread
- `Drop` sends `Shutdown` and joins the flush thread for graceful cleanup
- `name()` returns `"clickhouse"`
- `discover_plugin_columns()` queries `system.columns` and filters out `CORE_COLUMNS` + computed columns
- Re-export `ClickHouseWriterSink` from `engine::mod` behind `#[cfg(feature = "clickhouse-sink")]`
- 4 sink tests: name, on_bar send, backpressure, graceful shutdown

**Commit:** 207d382

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] ClickHouseBarRow missing clickhouse::Row derive**

- **Found during:** Task 1
- **Issue:** `client.insert::<ClickHouseBarRow>()` requires the `clickhouse::Row` trait, but Plan 01 only derived `serde::Serialize`
- **Fix:** Added `#[derive(clickhouse::Row)]` to `ClickHouseBarRow` in `row.rs`
- **Files modified:** `crates/opendeviationbar-streaming/src/clickhouse_writer/row.rs`

**2. [Rule 1 - Bug] Clippy: large enum variant, drain_collect, redundant_closure**

- **Found during:** Task 1
- **Issue:** `FlushCommand::Bar(ClickHouseBarRow)` triggered clippy `large_size_difference_between_variants` (ClickHouseBarRow is 1000+ bytes). Also `drain(..).collect()` and `|e| is_transient_error(e)`.
- **Fix:** Boxed the Bar variant (`Box<ClickHouseBarRow>`), used `std::mem::take()`, and passed function directly
- **Files modified:** `flush_thread.rs`, `sink.rs`

**3. [Rule 2 - Missing] Transient error classification via string parsing**

- **Found during:** Task 1
- **Issue:** The `clickhouse` crate wraps HTTP errors as `BadResponse(String)` with no structured status code access. Plan assumed direct HTTP status classification.
- **Fix:** Parse status code from BadResponse string prefix (e.g., "429 Too Many Requests" -> transient). Network/timeout errors classified as transient. All other patterns classified as fatal.
- **Files modified:** `flush_thread.rs`

## Verification

- All 156 streaming crate tests pass (including 18 new clickhouse_writer tests)
- Workspace compiles without `clickhouse-sink` feature (feature gate works)
- Clippy passes with `-D warnings`
- Graceful shutdown verified: Drop sends Shutdown, joins flush thread cleanly

## Known Stubs

None - all functionality is fully implemented with no placeholder data.

## Self-Check: PASSED

- All 3 created files exist (flush_thread.rs, sink.rs, schema.rs)
- Both commits (5943f80, 207d382) verified in git log
- 156/156 tests pass with clickhouse-sink feature
- Clippy clean with -D warnings
