Skip to content

Add standing OHLCV collector service - #70

Merged
outerlook merged 7 commits into
developfrom
feat/ohlcv-collector-service
Jul 16, 2026
Merged

Add standing OHLCV collector service#70
outerlook merged 7 commits into
developfrom
feat/ohlcv-collector-service

Conversation

@outerlook

@outerlook outerlook commented Jul 16, 2026

Copy link
Copy Markdown
Member

What

Promotes the OHLCV archive example seeder into a production-grade standing collector service, so market_data.candles is continuously populated by a deployed service instead of ad-hoc/dev tooling.

  • services/ohlcv-collector/ — single container running a keyless, loopback-only public broker in-process together with a supervised collector loop (no exchange credentials required; public market streams only). Mirrors the archive-forwarder packaging.
  • Config-driven subscription set: CEX_BROKER_OHLCV_COLLECTOR_CONFIG points at a JSON array of {exchange, symbol, timeframe} (timeframe defaults to 1m). Parsing is strict and fail-closed: malformed entries, unknown keys, or duplicates abort startup.
  • Supervised streams: one Subscribe(OHLCV) per pair with capped exponential backoff + jitter on stream error/end; a failing pair never affects other pairs or the process. Gap self-heal relies on the broker's existing bootstrapOhlcvHistory on every (re)subscribe — the Docker image defaults CEX_BROKER_OHLCV_ARCHIVE_BOOTSTRAP_LIMIT=1000 (~16h coverage at 1m). No new write path: archiving stays broker-side via capture-on-subscription.
  • Health signals: per-pair structured logs plus OTel counters cex_ohlcv_collector_bars_received_total and cex_ohlcv_collector_reconnects_total.
  • Bounded shutdown: SIGTERM/SIGINT close all resources with a 2s deadline per path; any path that cannot complete (ccxt's connection timer cannot always be cancelled) is logged before exit.
  • Removes the superseded examples/archive-ohlcv-subscribe.ts.

Subscribe handler lifecycle fix (shared code)

Request-scoped brokers created by the Subscribe handler (public or metadata-keyed) were never closed, leaking ccxt instances/websockets per stream. They are now tracked by SubscribeBrokerLifecycle and closed only on genuine termination: gRPC cancelled, terminal error, handler completion, or service shutdown. Pool-owned account brokers are never closed. A bare Writable close event is explicitly NOT treated as cancellation — a regression test proves an active subscription survives it (this event can fire early on real TCP streams and previously killed capture silently when acted upon).

Verification

  • bun test: 452 pass / 0 fail (includes new suites: config parsing, reconnect/resubscribe over a real local gRPC server, pair-fault isolation, and real-entrypoint SIGTERM subprocess tests for clean and bounded shutdown).
  • bunx tsc and bunx biome lint: clean (117 pre-existing warnings unchanged).
  • docker build -f services/ohlcv-collector/Dockerfile .: builds; running without config exits 1 with a clear error.
  • Live end-to-end against a local archive-forwarder + ClickHouse with real Binance data (binance BTC/USDT 1m, bootstrap 1000): 811 rows landed in market_data.candles within a 60s run (bootstrap ~16h + live bars), candles_closed readable, and SIGTERM exited in ~2.1s with the shutdown paths logged.

Notes for deployment

  • OHLCV only by design: orderbook capture already happens via existing subscriptions; trades/ticker deferred until a consumer exists.
  • Table names unchanged (market_data.candles / candles_closed, forwarder DDL is authority).
  • The collector image should ride the same release train as broker/forwarder.

Summary by CodeRabbit

  • New Features
    • Added an OHLCV collector service with gRPC streaming, automatic resubscription, and metrics.
    • Introduced JSON subscription configuration with validation, normalization, and defaults.
    • Added a container image and a new startup command for running the collector.
    • Added required OHLCV collector settings to the sample environment file.
  • Bug Fixes
    • Improved broker lifecycle handling to ensure proper cleanup on cancellation, errors, and shutdown.
    • Ensured shutdown is bounded even if exchange closure hangs.
  • Tests
    • Added/expanded Bun tests for config validation, reconnect behavior, subscription isolation, and SIGTERM shutdown.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 54dc19c8-a660-4631-81ad-5067f64504f8

📥 Commits

Reviewing files that changed from the base of the PR and between 8b4f8d3 and 92c72cf.

📒 Files selected for processing (5)
  • .env.sample
  • services/ohlcv-collector/Dockerfile
  • src/handlers/subscribe/broker-lifecycle.ts
  • src/handlers/subscribe/handler.ts
  • test/subscribe-broker-lifecycle.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/ohlcv-collector/Dockerfile
  • .env.sample
  • src/handlers/subscribe/handler.ts
📜 Recent review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-04-14T06:47:01.283Z
Learnt from: csmithington
Repo: usherlabs/cex-broker PR: 38
File: src/client.dev.ts:0-0
Timestamp: 2026-04-14T06:47:01.283Z
Learning: In this codebase, gRPC action constants such as `FetchTicker`, `FetchFees`, and `FetchAccountId` should be sourced from `src/helpers/constants.ts` and imported from there (e.g., used by both `src/client.dev.ts` and `src/server.ts`). Do not import or reference generated proto TypeScript artifacts (for example `./proto/cex_broker/Action`), since those generated files are git-ignored and won’t be available/committed consistently.

Applied to files:

  • src/handlers/subscribe/broker-lifecycle.ts
🔇 Additional comments (1)
src/handlers/subscribe/broker-lifecycle.ts (1)

59-69: Close the final registration race.

closeAll() can resolve after observing both maps empty, then a queued registration can see #shuttingDown and start an unawaited close(). That broker is outside the shutdown deadline. Make final draining and registration mutually exclusive—for example, enter a terminal state that rejects new registrations before resolving.


Walkthrough

The PR adds an OHLCV collector service with validated JSON configuration, supervised gRPC subscriptions, metrics, Docker and package entrypoints, bounded shutdown, and broker lifecycle management integrated into subscribe handling.

Changes

OHLCV collector service

Layer / File(s) Summary
Collector configuration and runtime packaging
services/ohlcv-collector/config.ts, .env.sample, package.json, services/ohlcv-collector/Dockerfile, biome.json
Adds strict subscription parsing and loading, environment settings, a startup command, Docker packaging, and formatter coverage.
OHLCV stream supervision and metrics
services/ohlcv-collector/collector.ts
Adds concurrent per-subscription gRPC streams, OHLCV bar counting, metrics, reconnect handling, exponential backoff, and abort support.
Service entrypoint and bounded shutdown
services/ohlcv-collector/index.ts
Starts the public broker server and collector, handles termination signals, and closes resources with bounded deadlines.
Subscribe broker ownership and lifecycle wiring
src/handlers/subscribe/broker-lifecycle.ts, src/handlers/subscribe/handler.ts, src/handlers/subscribe/index.ts, src/server.ts
Tracks owned brokers, closes them on cancellation or shutdown, and wires lifecycle management through server construction.
Collector and stream lifecycle tests
test/ohlcv-collector*.test.ts, test/fixtures/ohlcv-collector-fake-exchange.ts, test/subscribe-handler.test.ts, test/subscribe-broker-lifecycle.test.ts
Tests configuration validation, reconnect and isolation behavior, bounded shutdown, broker lifecycle handling, and cancellation-aware stream handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Signal
  participant OhlcvCollector
  participant SubscribeBroker
  participant Exchange
  Signal->>OhlcvCollector: request shutdown
  OhlcvCollector->>SubscribeBroker: cancel subscription stream
  SubscribeBroker->>Exchange: close owned broker
  Exchange-->>SubscribeBroker: close result or timeout
  OhlcvCollector-->>Signal: complete bounded shutdown
Loading

Possibly related PRs

Poem

I’m a rabbit watching bars stream by,
With reconnect hops beneath the sky.
Brokers close when signals call,
Metrics count each bar and all.
Config blooms, and shutdown’s neat—
Bun-powered service, quick on its feet!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an OHLCV collector service.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ohlcv-collector-service

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
test/subscribe-handler.test.ts (1)

191-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that data is still delivered after the premature close.

The test only checks that polling continues. It would still pass if post-close results were discarded; inspect state.writes to verify the stream remains functional.

Proposed assertion
-		const { call } = createSubscribeCall({
+		const { call, state } = createSubscribeCall({
...
 		controlledWatch.resolvers[0]?.([{ id: "trade-1" }]);
 		await waitFor(() => controlledWatch.calls.length === 2);
+		expect(state.writes).toHaveLength(1);
+		expect(state.writes[0]?.data).toContain("trade-1");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/subscribe-handler.test.ts` around lines 191 - 215, Update the test
“keeps a subscription active when close fires without cancellation” to inspect
state.writes after each controlled watch resolution and assert that trade data
is delivered despite the premature close. Retain the existing
polling-continuation check and cancellation flow, verifying the post-close
result is written before completing the handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.sample:
- Around line 36-39: Reorder the OHLCV environment keys in .env.sample so
CEX_BROKER_OHLCV_ARCHIVE_BOOTSTRAP_LIMIT appears before
CEX_BROKER_OHLCV_COLLECTOR_CONFIG, preserving their existing values and
comments.

In `@services/ohlcv-collector/Dockerfile`:
- Around line 18-25: Update the Dockerfile after preparing the application files
to switch execution to the base image’s non-root bun user, ensuring the existing
collector CMD runs without elevated privileges.

In `@src/handlers/subscribe/broker-lifecycle.ts`:
- Around line 36-40: Update closeAll() in
src/handlers/subscribe/broker-lifecycle.ts to retain broker close failures while
still attempting every broker, then reject or return an aggregate failure result
instead of suppressing errors in the catch block. In
services/ohlcv-collector/index.ts, consume that result around the
subscribe_brokers shutdown path so the component is recorded as incomplete and
forced shutdown remains bounded.
- Around line 14-18: The shutdown flow must wait for brokers registered during
shutdown instead of fire-and-forget closing them. Update register and the
closeAll/close coordination in the broker lifecycle collector to add a
registration barrier or drain loop, ensuring shutdown does not resolve until
every shutdown-time registration’s close completes and remains within the
existing deadline.

---

Nitpick comments:
In `@test/subscribe-handler.test.ts`:
- Around line 191-215: Update the test “keeps a subscription active when close
fires without cancellation” to inspect state.writes after each controlled watch
resolution and assert that trade data is delivered despite the premature close.
Retain the existing polling-continuation check and cancellation flow, verifying
the post-close result is written before completing the handler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fccb49cd-fcf6-431a-b7f9-a488aa1bb33c

📥 Commits

Reviewing files that changed from the base of the PR and between 615a450 and 8b4f8d3.

📒 Files selected for processing (17)
  • .env.sample
  • biome.json
  • examples/archive-ohlcv-subscribe.ts
  • package.json
  • services/ohlcv-collector/Dockerfile
  • services/ohlcv-collector/collector.ts
  • services/ohlcv-collector/config.ts
  • services/ohlcv-collector/index.ts
  • src/handlers/subscribe/broker-lifecycle.ts
  • src/handlers/subscribe/handler.ts
  • src/handlers/subscribe/index.ts
  • src/server.ts
  • test/fixtures/ohlcv-collector-fake-exchange.ts
  • test/ohlcv-collector-config.test.ts
  • test/ohlcv-collector-shutdown.test.ts
  • test/ohlcv-collector.test.ts
  • test/subscribe-handler.test.ts
💤 Files with no reviewable changes (1)
  • examples/archive-ohlcv-subscribe.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-04-14T06:47:01.283Z
Learnt from: csmithington
Repo: usherlabs/cex-broker PR: 38
File: src/client.dev.ts:0-0
Timestamp: 2026-04-14T06:47:01.283Z
Learning: In this codebase, gRPC action constants such as `FetchTicker`, `FetchFees`, and `FetchAccountId` should be sourced from `src/helpers/constants.ts` and imported from there (e.g., used by both `src/client.dev.ts` and `src/server.ts`). Do not import or reference generated proto TypeScript artifacts (for example `./proto/cex_broker/Action`), since those generated files are git-ignored and won’t be available/committed consistently.

Applied to files:

  • src/handlers/subscribe/index.ts
  • src/server.ts
  • src/handlers/subscribe/broker-lifecycle.ts
  • src/handlers/subscribe/handler.ts
🪛 dotenv-linter (4.0.0)
.env.sample

[warning] 39-39: [UnorderedKey] The CEX_BROKER_OHLCV_ARCHIVE_BOOTSTRAP_LIMIT key should go before the CEX_BROKER_OHLCV_COLLECTOR_CONFIG key

(UnorderedKey)

🪛 Trivy (0.69.3)
services/ohlcv-collector/Dockerfile

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)


[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)

🔇 Additional comments (14)
services/ohlcv-collector/config.ts (1)

1-78: LGTM!

package.json (1)

39-39: LGTM!

biome.json (1)

12-17: LGTM!

services/ohlcv-collector/collector.ts (1)

1-272: LGTM!

test/fixtures/ohlcv-collector-fake-exchange.ts (1)

1-50: LGTM!

test/ohlcv-collector-config.test.ts (1)

1-55: LGTM!

test/ohlcv-collector-shutdown.test.ts (1)

1-127: LGTM!

test/ohlcv-collector.test.ts (1)

1-222: LGTM!

test/subscribe-handler.test.ts (1)

36-36: LGTM!

Also applies to: 69-75, 152-152, 398-398, 487-487

services/ohlcv-collector/index.ts (1)

1-120: LGTM!

Also applies to: 136-155

src/handlers/subscribe/broker-lifecycle.ts (1)

1-13: LGTM!

Also applies to: 21-35, 41-48

src/handlers/subscribe/handler.ts (1)

35-54: LGTM!

Also applies to: 312-343, 431-441, 801-804

src/handlers/subscribe/index.ts (1)

1-1: LGTM!

src/server.ts (1)

3-3: LGTM!

Also applies to: 36-36, 57-57

Comment thread .env.sample
Comment thread services/ohlcv-collector/Dockerfile
Comment thread src/handlers/subscribe/broker-lifecycle.ts
Comment thread src/handlers/subscribe/broker-lifecycle.ts
- SubscribeBrokerLifecycle.closeAll now drains registrations that race
  shutdown and rejects when any broker close fails, so the collector
  records subscribe_brokers as incomplete and force-exits within its
  bounded deadline instead of hanging on leaked exchange handles.
- Add lifecycle unit tests for both behaviors.
- Run the collector container as the non-root bun user.
- Reorder OHLCV keys in .env.sample for dotenv-linter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant