Fence market-data writes and recover stalled Binance streams
pr / test (pull_request) Successful in 1m22s
pr / test (pull_request) Successful in 1m22s
This commit is contained in:
@@ -1,26 +1,24 @@
|
||||
(ns pmagnus.btcdata.web.controllers.health-test
|
||||
(:require
|
||||
[clojure.test :refer [deftest is testing]]
|
||||
[pmagnus.btcdata.web.controllers.health :as health]))
|
||||
(:require [clojure.test :refer [deftest is]]
|
||||
[pmagnus.btcdata.web.controllers.health :as health]))
|
||||
|
||||
(def disabled-stream {:state (atom {:enabled? false}) :latest-price (atom nil)})
|
||||
|
||||
(deftest healthcheck-is-process-only
|
||||
(let [response (health/healthcheck! {})]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= "up" (get-in response [:body :app :status])))))
|
||||
(is (= 200 (:status (health/healthcheck! {})))))
|
||||
|
||||
(deftest readiness-check-tests-database
|
||||
(testing "ready when the database query succeeds"
|
||||
(let [called (atom nil)
|
||||
query-fn (fn [query params]
|
||||
(reset! called [query params])
|
||||
{:healthy 1})
|
||||
response (health/readiness-check! query-fn {})]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= {:status "ready"} (:body response)))
|
||||
(is (= [:check-database {}] @called))))
|
||||
(let [called (atom nil)
|
||||
query-fn (fn [query params] (reset! called [query params]))]
|
||||
(is (= 200 (:status (health/readiness-check! query-fn disabled-stream {}))))
|
||||
(is (= [:check-database {}] @called)))
|
||||
(is (= 503 (:status (health/readiness-check!
|
||||
(fn [_ _] (throw (ex-info "database unavailable" {})))
|
||||
disabled-stream {})))))
|
||||
|
||||
(testing "not ready when the database query fails"
|
||||
(let [query-fn (fn [_ _] (throw (ex-info "database unavailable" {})))
|
||||
response (health/readiness-check! query-fn {})]
|
||||
(is (= 503 (:status response)))
|
||||
(is (= {:status "not ready"} (:body response))))))
|
||||
(deftest stale-stream-is-not-ready
|
||||
(is (= 503 (:status (health/readiness-check!
|
||||
(fn [_ _] {})
|
||||
{:state (atom {:enabled? true :connected? true})
|
||||
:latest-price (atom {:recorded-at java.time.Instant/EPOCH})}
|
||||
{})))))
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
(ns pmagnus.btcdata.writer-test
|
||||
(:require [clojure.test :refer [deftest is]]
|
||||
[integrant.core :as ig]
|
||||
[conman.core :as conman]
|
||||
[next.jdbc :as jdbc]
|
||||
[pmagnus.btcdata.writer :as writer]))
|
||||
|
||||
(deftest queries-use-lock-session
|
||||
(let [connection (Object.)
|
||||
seen (atom nil)
|
||||
query (writer/guarded-query connection
|
||||
(fn [& args] (reset! seen args) :result))]
|
||||
(is (= :result (query :insert-price {:price 1})))
|
||||
(is (= [connection :insert-price {:price 1}] @seen))))
|
||||
|
||||
(deftest postgres-session-fencing
|
||||
;; Optional disposable PostgreSQL integration test; never use production here.
|
||||
(if-let [url (System/getenv "BTCDATA_TEST_JDBC_URL")]
|
||||
(let [ds (jdbc/get-datasource {:jdbcUrl url})
|
||||
opts {:conn ds :query-fn (fn [conn _ _]
|
||||
(jdbc/execute-one! conn ["select 1 as healthy"]))}
|
||||
first-writer (ig/init-key :writer/query-fn opts)]
|
||||
(try
|
||||
(is (= 1 (:healthy (first-writer :check {}))))
|
||||
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Another btcdata writer"
|
||||
(ig/init-key :writer/query-fn opts)))
|
||||
;; Closing the owning session cannot transparently reconnect for writes.
|
||||
(.close (:connection (meta first-writer)))
|
||||
(is (thrown? Exception (first-writer :check {})))
|
||||
(let [replacement (ig/init-key :writer/query-fn opts)]
|
||||
(try
|
||||
(is (= 1 (:healthy (replacement :check {}))))
|
||||
(finally (ig/halt-key! :writer/query-fn replacement))))
|
||||
(finally
|
||||
(when-not (.isClosed (:connection (meta first-writer)))
|
||||
(ig/halt-key! :writer/query-fn first-writer)))))
|
||||
(println "SKIP PostgreSQL fencing integration: set BTCDATA_TEST_JDBC_URL")))
|
||||
|
||||
(deftest pooled-session-releases-lock-on-halt
|
||||
(when-let [url (System/getenv "BTCDATA_TEST_JDBC_URL")]
|
||||
(let [pool (conman/connect! {:jdbc-url url :maximum-pool-size 3})
|
||||
opts {:conn pool :query-fn (fn [_ _ _] :ok)}]
|
||||
(try
|
||||
(let [leader (ig/init-key :writer/query-fn opts)]
|
||||
(is (thrown? Exception (ig/init-key :writer/query-fn opts)))
|
||||
(ig/halt-key! :writer/query-fn leader))
|
||||
;; An independent connection can acquire after pooled-session shutdown.
|
||||
(let [other-opts (assoc opts :conn (jdbc/get-datasource {:jdbcUrl url}))
|
||||
leader (ig/init-key :writer/query-fn other-opts)]
|
||||
(is (= :ok (leader :check {})))
|
||||
(ig/halt-key! :writer/query-fn leader))
|
||||
(finally (conman/disconnect! pool))))))
|
||||
@@ -0,0 +1,41 @@
|
||||
(ns pmagnus.btcdata.ws.binance-test
|
||||
(:require [clojure.test :refer [deftest is]]
|
||||
[integrant.core :as ig]
|
||||
[pmagnus.btcdata.ws.binance :as binance])
|
||||
(:import [java.time Instant]))
|
||||
|
||||
(deftest freshness-boundaries
|
||||
(let [state (atom {:enabled? true :connected? true})
|
||||
price (atom {:recorded-at (Instant/ofEpochMilli 100000)})
|
||||
component {:state state :latest-price price}]
|
||||
(is (binance/fresh? component 100000))
|
||||
(is (binance/fresh? component 160000))
|
||||
(is (not (binance/fresh? component 160001)))
|
||||
(is (not (binance/fresh? component 99999)))
|
||||
(swap! state assoc :connected? false)
|
||||
(is (not (binance/fresh? component 100000)))
|
||||
(reset! price nil)
|
||||
(is (not (binance/fresh? component 100000)))))
|
||||
|
||||
(deftest disabled-stream-does-not-connect
|
||||
(let [component (ig/init-key :ws/binance {:enabled? "false" :uri "invalid"})]
|
||||
(is (binance/fresh? component))
|
||||
(is (nil? (:worker component)))
|
||||
(ig/halt-key! :ws/binance component)
|
||||
(is (false? (:running? @(:state component))))))
|
||||
|
||||
(deftest initial-failure-is-retried-and-worker-stops
|
||||
(let [attempts (atom 0)
|
||||
connect-var (ns-resolve 'pmagnus.btcdata.ws.binance 'connect!)]
|
||||
(with-redefs-fn
|
||||
{connect-var (fn [& _] (swap! attempts inc) (throw (ex-info "test failure" {})))}
|
||||
(fn []
|
||||
(let [component (ig/init-key :ws/binance {:enabled? "true" :uri "test"})]
|
||||
(try
|
||||
(let [deadline (+ (System/currentTimeMillis) 6000)]
|
||||
(while (and (< @attempts 2) (< (System/currentTimeMillis) deadline))
|
||||
(Thread/sleep 20)))
|
||||
(is (>= @attempts 2))
|
||||
(is (not (binance/fresh? component)))
|
||||
(finally (ig/halt-key! :ws/binance component)))
|
||||
(is (future-cancelled? (:worker component))))))))
|
||||
Reference in New Issue
Block a user