Add docker-compose, README, fix SSE dispatch
Use Undertow dispatch for SSE handler to avoid Ring adapter conflict. Add docker-compose.yml (port 4101 to coexist with local dev on 4100). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,16 @@ Bitcoin data backend — Clojure Kit application. Fetches BTC prices from Binanc
|
|||||||
- `make repl` — Start nREPL
|
- `make repl` — Start nREPL
|
||||||
- `make test` — Run tests
|
- `make test` — Run tests
|
||||||
- `make uberjar` — Build production JAR
|
- `make uberjar` — Build production JAR
|
||||||
|
- `docker compose up -d` — Start btcdata container
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
Docker runs alongside local dev on different ports and databases:
|
||||||
|
|
||||||
|
| | Local dev | Docker |
|
||||||
|
|--|----------------|----------------|
|
||||||
|
| **Port** | localhost:4100 | localhost:4101 |
|
||||||
|
| **Database** | btcprice | btcprod |
|
||||||
|
|
||||||
## REPL Commands
|
## REPL Commands
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# btcdata
|
||||||
|
|
||||||
|
Bitcoin data backend service. Fetches BTC prices from Binance (WebSocket) and Kraken (HTTP polling), stores them in PostgreSQL, and exposes a JSON API with Server-Sent Events for live price streaming.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Endpoint | Description |
|
||||||
|
|---|---|
|
||||||
|
| `GET /api/price/stream` | SSE stream — emits `price-update` events with JSON `{price, prev_price, recorded_at}` |
|
||||||
|
| `GET /api/price/latest` | Latest price as JSON |
|
||||||
|
| `GET /api/health` | Health check |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local dev (requires PostgreSQL)
|
||||||
|
cp .env.example .env # edit JDBC_URL
|
||||||
|
make run # starts on port 4100
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker compose up -d # starts on port 4101
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `BTCDATA_PORT` | `4100` | HTTP server port |
|
||||||
|
| `JDBC_URL` | — | PostgreSQL connection string |
|
||||||
|
| `CORS_ORIGIN` | `*` | Allowed origin for CORS headers |
|
||||||
|
|
||||||
|
## Data Sources
|
||||||
|
|
||||||
|
- **Binance** — WebSocket trade stream (`btcusdt@trade`), throttled to 5s writes
|
||||||
|
- **Kraken hourly** — OHLC candles polled at HH:01:00 UTC
|
||||||
|
- **Kraken daily** — OHLC candles polled at 00:01:00 UTC
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
Clojure · Kit · Undertow · Reitit · PostgreSQL · conman · Migratus
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
btcdata:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "4101:4100"
|
||||||
|
environment:
|
||||||
|
BTCDATA_PORT: "4100"
|
||||||
|
JDBC_URL: "jdbc:postgresql://postgres:5432/btcprod?user=postgres&password=ratata,123"
|
||||||
|
CORS_ORIGIN: "http://localhost:4041"
|
||||||
|
extra_hosts:
|
||||||
|
- "postgres:host-gateway"
|
||||||
@@ -28,11 +28,8 @@
|
|||||||
:prev_price (when prev-price (str prev-price))
|
:prev_price (when prev-price (str prev-price))
|
||||||
:recorded_at (str recorded-at)}))
|
:recorded_at (str recorded-at)}))
|
||||||
|
|
||||||
(defn- price-stream-handler [{:keys [binance]} req]
|
(defn- run-sse-loop! [^HttpServerExchange exchange price-atom cors-origin]
|
||||||
(let [^HttpServerExchange exchange (:server-exchange req)
|
(let [headers (.getResponseHeaders exchange)]
|
||||||
price-atom (:latest-price binance)
|
|
||||||
headers (.getResponseHeaders exchange)
|
|
||||||
cors-origin (or (System/getenv "CORS_ORIGIN") "*")]
|
|
||||||
(.setStatusCode exchange 200)
|
(.setStatusCode exchange 200)
|
||||||
(.put headers (HttpString. "Content-Type") "text/event-stream")
|
(.put headers (HttpString. "Content-Type") "text/event-stream")
|
||||||
(.put headers (HttpString. "Cache-Control") "no-cache")
|
(.put headers (HttpString. "Cache-Control") "no-cache")
|
||||||
@@ -59,7 +56,21 @@
|
|||||||
(catch Exception _)
|
(catch Exception _)
|
||||||
(finally
|
(finally
|
||||||
(remove-watch price-atom wkey)
|
(remove-watch price-atom wkey)
|
||||||
(try (.close writer) (catch Exception _)))))
|
(try (.close writer) (catch Exception _))
|
||||||
|
(when-not (.isComplete exchange)
|
||||||
|
(.endExchange exchange)))))))
|
||||||
|
|
||||||
|
(defn- price-stream-handler [{:keys [binance]} req]
|
||||||
|
(let [^HttpServerExchange exchange (:server-exchange req)
|
||||||
|
price-atom (:latest-price binance)
|
||||||
|
cors-origin (or (System/getenv "CORS_ORIGIN") "*")]
|
||||||
|
;; Dispatch to a worker thread so the Ring adapter never processes
|
||||||
|
;; the return value — Undertow handles the exchange lifecycle directly.
|
||||||
|
(.dispatch exchange
|
||||||
|
(reify io.undertow.server.HttpHandler
|
||||||
|
(handleRequest [_ ex]
|
||||||
|
(run-sse-loop! ex price-atom cors-origin))))
|
||||||
|
;; Return nil; exchange is dispatched so adapter won't touch it.
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
(defn- latest-price-handler [{:keys [binance]} _req]
|
(defn- latest-price-handler [{:keys [binance]} _req]
|
||||||
|
|||||||
Reference in New Issue
Block a user