Attempt to fix bugs related to empty vecs

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2024-03-14 22:38:20 +00:00
parent 8202255132
commit 10365745aa
25 changed files with 253 additions and 238 deletions

10
Cargo.lock generated
View File

@@ -1098,6 +1098,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
[[package]]
name = "nonempty"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "303e8749c804ccd6ca3b428de7fe0d86cb86bc7606bc15291f100fd487960bb8"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "nonzero_ext" name = "nonzero_ext"
version = "0.3.0" version = "0.3.0"
@@ -1316,6 +1325,7 @@ dependencies = [
"lazy_static", "lazy_static",
"log", "log",
"log4rs", "log4rs",
"nonempty",
"regex", "regex",
"reqwest", "reqwest",
"serde", "serde",

View File

@@ -68,3 +68,6 @@ regex = "1.10.3"
async-trait = "0.1.77" async-trait = "0.1.77"
itertools = "0.12.1" itertools = "0.12.1"
lazy_static = "1.4.0" lazy_static = "1.4.0"
nonempty = { version = "0.10.0", features = [
"serialize",
] }

View File

@@ -44,7 +44,8 @@ pub async fn rehydrate_orders(config: &Arc<Config>) {
let mut orders = vec![]; let mut orders = vec![];
let mut after = OffsetDateTime::UNIX_EPOCH; let mut after = OffsetDateTime::UNIX_EPOCH;
while let Some(message) = alpaca::orders::get( loop {
let message = alpaca::orders::get(
&config.alpaca_client, &config.alpaca_client,
&config.alpaca_rate_limiter, &config.alpaca_rate_limiter,
&types::alpaca::api::outgoing::order::Order { &types::alpaca::api::outgoing::order::Order {
@@ -56,9 +57,12 @@ pub async fn rehydrate_orders(config: &Arc<Config>) {
&ALPACA_API_BASE, &ALPACA_API_BASE,
) )
.await .await
.ok() .unwrap();
.filter(|message| !message.is_empty())
{ if message.is_empty() {
break;
}
orders.extend(message); orders.extend(message);
after = orders.last().unwrap().submitted_at; after = orders.last().unwrap().submitted_at;
} }

View File

@@ -1,4 +1,4 @@
use super::{error_to_backoff, status_error_to_backoff}; use super::error_to_backoff;
use crate::types::alpaca::api::incoming::account::Account; use crate::types::alpaca::api::incoming::account::Account;
use backoff::{future::retry_notify, ExponentialBackoff}; use backoff::{future::retry_notify, ExponentialBackoff};
use governor::DefaultDirectRateLimiter; use governor::DefaultDirectRateLimiter;
@@ -22,7 +22,7 @@ pub async fn get(
.await .await
.map_err(error_to_backoff)? .map_err(error_to_backoff)?
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Account>() .json::<Account>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)

View File

@@ -1,4 +1,4 @@
use super::{error_to_backoff, status_error_to_backoff}; use super::error_to_backoff;
use crate::types::alpaca::api::{ use crate::types::alpaca::api::{
incoming::asset::{Asset, Class}, incoming::asset::{Asset, Class},
outgoing, outgoing,
@@ -29,7 +29,7 @@ pub async fn get(
.await .await
.map_err(error_to_backoff)? .map_err(error_to_backoff)?
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Vec<Asset>>() .json::<Vec<Asset>>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)
@@ -65,7 +65,7 @@ pub async fn get_by_symbol(
.await .await
.map_err(error_to_backoff)? .map_err(error_to_backoff)?
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Asset>() .json::<Asset>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)
@@ -88,6 +88,10 @@ pub async fn get_by_symbols(
backoff: Option<ExponentialBackoff>, backoff: Option<ExponentialBackoff>,
api_base: &str, api_base: &str,
) -> Result<Vec<Asset>, Error> { ) -> Result<Vec<Asset>, Error> {
if symbols.is_empty() {
return Ok(vec![]);
}
if symbols.len() == 1 { if symbols.len() == 1 {
let asset = get_by_symbol(client, rate_limiter, &symbols[0], backoff, api_base).await?; let asset = get_by_symbol(client, rate_limiter, &symbols[0], backoff, api_base).await?;
return Ok(vec![asset]); return Ok(vec![asset]);

View File

@@ -1,4 +1,4 @@
use super::{error_to_backoff, status_error_to_backoff}; use super::error_to_backoff;
use crate::types::alpaca::api::{incoming::bar::Bar, outgoing}; use crate::types::alpaca::api::{incoming::bar::Bar, outgoing};
use backoff::{future::retry_notify, ExponentialBackoff}; use backoff::{future::retry_notify, ExponentialBackoff};
use governor::DefaultDirectRateLimiter; use governor::DefaultDirectRateLimiter;
@@ -33,7 +33,7 @@ pub async fn get(
.await .await
.map_err(error_to_backoff)? .map_err(error_to_backoff)?
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Message>() .json::<Message>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)

View File

@@ -1,4 +1,4 @@
use super::{error_to_backoff, status_error_to_backoff}; use super::error_to_backoff;
use crate::types::alpaca::api::{incoming::calendar::Calendar, outgoing}; use crate::types::alpaca::api::{incoming::calendar::Calendar, outgoing};
use backoff::{future::retry_notify, ExponentialBackoff}; use backoff::{future::retry_notify, ExponentialBackoff};
use governor::DefaultDirectRateLimiter; use governor::DefaultDirectRateLimiter;
@@ -24,7 +24,7 @@ pub async fn get(
.await .await
.map_err(error_to_backoff)? .map_err(error_to_backoff)?
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Vec<Calendar>>() .json::<Vec<Calendar>>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)

View File

@@ -1,4 +1,4 @@
use super::{error_to_backoff, status_error_to_backoff}; use super::error_to_backoff;
use crate::types::alpaca::api::incoming::clock::Clock; use crate::types::alpaca::api::incoming::clock::Clock;
use backoff::{future::retry_notify, ExponentialBackoff}; use backoff::{future::retry_notify, ExponentialBackoff};
use governor::DefaultDirectRateLimiter; use governor::DefaultDirectRateLimiter;
@@ -22,7 +22,7 @@ pub async fn get(
.await .await
.map_err(error_to_backoff)? .map_err(error_to_backoff)?
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Clock>() .json::<Clock>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)

View File

@@ -9,18 +9,13 @@ pub mod positions;
use reqwest::StatusCode; use reqwest::StatusCode;
pub fn status_error_to_backoff(err: reqwest::Error) -> backoff::Error<reqwest::Error> {
match err.status() {
Some(StatusCode::BAD_REQUEST | StatusCode::FORBIDDEN | StatusCode::NOT_FOUND) | None => {
backoff::Error::Permanent(err)
}
_ => err.into(),
}
}
pub fn error_to_backoff(err: reqwest::Error) -> backoff::Error<reqwest::Error> { pub fn error_to_backoff(err: reqwest::Error) -> backoff::Error<reqwest::Error> {
if err.is_status() { if err.is_status() {
return status_error_to_backoff(err); return match err.status() {
Some(StatusCode::BAD_REQUEST | StatusCode::FORBIDDEN | StatusCode::NOT_FOUND)
| None => backoff::Error::Permanent(err),
_ => err.into(),
};
} }
if err.is_builder() || err.is_request() || err.is_redirect() || err.is_decode() || err.is_body() if err.is_builder() || err.is_request() || err.is_redirect() || err.is_decode() || err.is_body()

View File

@@ -1,4 +1,4 @@
use super::{error_to_backoff, status_error_to_backoff}; use super::error_to_backoff;
use crate::types::alpaca::api::{incoming::news::News, outgoing, ALPACA_NEWS_DATA_API_URL}; use crate::types::alpaca::api::{incoming::news::News, outgoing, ALPACA_NEWS_DATA_API_URL};
use backoff::{future::retry_notify, ExponentialBackoff}; use backoff::{future::retry_notify, ExponentialBackoff};
use governor::DefaultDirectRateLimiter; use governor::DefaultDirectRateLimiter;
@@ -32,7 +32,7 @@ pub async fn get(
.await .await
.map_err(error_to_backoff)? .map_err(error_to_backoff)?
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Message>() .json::<Message>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)

View File

@@ -1,4 +1,4 @@
use super::{error_to_backoff, status_error_to_backoff}; use super::error_to_backoff;
use crate::types::alpaca::{api::outgoing, shared::order}; use crate::types::alpaca::{api::outgoing, shared::order};
use backoff::{future::retry_notify, ExponentialBackoff}; use backoff::{future::retry_notify, ExponentialBackoff};
use governor::DefaultDirectRateLimiter; use governor::DefaultDirectRateLimiter;
@@ -26,7 +26,7 @@ pub async fn get(
.await .await
.map_err(error_to_backoff)? .map_err(error_to_backoff)?
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Vec<Order>>() .json::<Vec<Order>>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)

View File

@@ -1,4 +1,4 @@
use super::{error_to_backoff, status_error_to_backoff}; use super::error_to_backoff;
use crate::types::alpaca::api::incoming::position::Position; use crate::types::alpaca::api::incoming::position::Position;
use backoff::{future::retry_notify, ExponentialBackoff}; use backoff::{future::retry_notify, ExponentialBackoff};
use governor::DefaultDirectRateLimiter; use governor::DefaultDirectRateLimiter;
@@ -22,7 +22,7 @@ pub async fn get(
.await .await
.map_err(error_to_backoff)? .map_err(error_to_backoff)?
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Vec<Position>>() .json::<Vec<Position>>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)
@@ -64,7 +64,7 @@ pub async fn get_by_symbol(
response response
.error_for_status() .error_for_status()
.map_err(status_error_to_backoff)? .map_err(error_to_backoff)?
.json::<Position>() .json::<Position>()
.await .await
.map_err(error_to_backoff) .map_err(error_to_backoff)
@@ -88,6 +88,10 @@ pub async fn get_by_symbols(
backoff: Option<ExponentialBackoff>, backoff: Option<ExponentialBackoff>,
api_base: &str, api_base: &str,
) -> Result<Vec<Position>, reqwest::Error> { ) -> Result<Vec<Position>, reqwest::Error> {
if symbols.is_empty() {
return Ok(vec![]);
}
if symbols.len() == 1 { if symbols.len() == 1 {
let position = get_by_symbol(client, rate_limiter, &symbols[0], backoff, api_base).await?; let position = get_by_symbol(client, rate_limiter, &symbols[0], backoff, api_base).await?;
return Ok(position.into_iter().collect()); return Ok(position.into_iter().collect());

View File

@@ -6,14 +6,14 @@ use tokio::{sync::Semaphore, try_join};
optimize!("calendar"); optimize!("calendar");
pub async fn upsert_batch_and_delete<'a, T>( pub async fn upsert_batch_and_delete<'a, I>(
client: &Client, client: &Client,
concurrency_limiter: &Arc<Semaphore>, concurrency_limiter: &Arc<Semaphore>,
records: T, records: I,
) -> Result<(), Error> ) -> Result<(), Error>
where where
T: IntoIterator<Item = &'a Calendar> + Send + Sync + Clone, I: IntoIterator<Item = &'a Calendar> + Send + Sync + Clone,
T::IntoIter: Send, I::IntoIter: Send,
{ {
let upsert_future = async { let upsert_future = async {
let mut insert = client.insert("calendar")?; let mut insert = client.insert("calendar")?;

View File

@@ -92,14 +92,14 @@ macro_rules! upsert {
#[macro_export] #[macro_export]
macro_rules! upsert_batch { macro_rules! upsert_batch {
($record:ty, $table_name:expr) => { ($record:ty, $table_name:expr) => {
pub async fn upsert_batch<'a, T>( pub async fn upsert_batch<'a, I>(
client: &clickhouse::Client, client: &clickhouse::Client,
concurrency_limiter: &std::sync::Arc<tokio::sync::Semaphore>, concurrency_limiter: &std::sync::Arc<tokio::sync::Semaphore>,
records: T, records: I,
) -> Result<(), clickhouse::error::Error> ) -> Result<(), clickhouse::error::Error>
where where
T: IntoIterator<Item = &'a $record> + Send + Sync, I: IntoIterator<Item = &'a $record> + Send + Sync,
T::IntoIter: Send, I::IntoIter: Send,
{ {
let _ = concurrency_limiter.acquire().await.unwrap(); let _ = concurrency_limiter.acquire().await.unwrap();
let mut insert = client.insert($table_name)?; let mut insert = client.insert($table_name)?;

View File

@@ -1,4 +1,5 @@
use crate::utils::ser; use crate::utils::ser;
use nonempty::NonEmpty;
use serde::Serialize; use serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
@@ -6,14 +7,14 @@ use serde::Serialize;
pub enum Market { pub enum Market {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
UsEquity { UsEquity {
bars: Vec<String>, bars: NonEmpty<String>,
updated_bars: Vec<String>, updated_bars: NonEmpty<String>,
statuses: Vec<String>, statuses: NonEmpty<String>,
}, },
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
Crypto { Crypto {
bars: Vec<String>, bars: NonEmpty<String>,
updated_bars: Vec<String>, updated_bars: NonEmpty<String>,
}, },
} }
@@ -23,12 +24,12 @@ pub enum Message {
Market(Market), Market(Market),
News { News {
#[serde(serialize_with = "ser::remove_slash_from_symbols")] #[serde(serialize_with = "ser::remove_slash_from_symbols")]
news: Vec<String>, news: NonEmpty<String>,
}, },
} }
impl Message { impl Message {
pub fn new_market_us_equity(symbols: Vec<String>) -> Self { pub fn new_market_us_equity(symbols: NonEmpty<String>) -> Self {
Self::Market(Market::UsEquity { Self::Market(Market::UsEquity {
bars: symbols.clone(), bars: symbols.clone(),
updated_bars: symbols.clone(), updated_bars: symbols.clone(),
@@ -36,14 +37,14 @@ impl Message {
}) })
} }
pub fn new_market_crypto(symbols: Vec<String>) -> Self { pub fn new_market_crypto(symbols: NonEmpty<String>) -> Self {
Self::Market(Market::Crypto { Self::Market(Market::Crypto {
bars: symbols.clone(), bars: symbols.clone(),
updated_bars: symbols, updated_bars: symbols,
}) })
} }
pub fn new_news(symbols: Vec<String>) -> Self { pub fn new_news(symbols: NonEmpty<String>) -> Self {
Self::News { news: symbols } Self::News { news: symbols }
} }
} }

View File

@@ -58,12 +58,13 @@ where
} }
} }
pub fn remove_slash_from_symbols<S>(pairs: &[String], serializer: S) -> Result<S::Ok, S::Error> pub fn remove_slash_from_symbols<'a, S, I>(pairs: I, serializer: S) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
I: IntoIterator<Item = &'a String>,
{ {
let symbols = pairs let symbols = pairs
.iter() .into_iter()
.map(|pair| remove_slash(pair)) .map(|pair| remove_slash(pair))
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View File

@@ -14,6 +14,7 @@ use config::{
use dotenv::dotenv; use dotenv::dotenv;
use log::info; use log::info;
use log4rs::config::Deserializers; use log4rs::config::Deserializers;
use nonempty::NonEmpty;
use qrust::{create_send_await, database}; use qrust::{create_send_await, database};
use tokio::{join, spawn, sync::mpsc, try_join}; use tokio::{join, spawn, sync::mpsc, try_join};
@@ -101,12 +102,14 @@ async fn main() {
spawn(threads::clock::run(config.clone(), clock_sender)); spawn(threads::clock::run(config.clone(), clock_sender));
if let Some(assets) = NonEmpty::from_vec(assets) {
create_send_await!( create_send_await!(
data_sender, data_sender,
threads::data::Message::new, threads::data::Message::new,
threads::data::Action::Enable, threads::data::Action::Enable,
assets assets
); );
}
routes::run(config, data_sender).await; routes::run(config, data_sender).await;
} }

View File

@@ -4,6 +4,7 @@ use crate::{
}; };
use axum::{extract::Path, Extension, Json}; use axum::{extract::Path, Extension, Json};
use http::StatusCode; use http::StatusCode;
use nonempty::{nonempty, NonEmpty};
use qrust::{ use qrust::{
alpaca, alpaca,
types::{self, Asset}, types::{self, Asset},
@@ -113,15 +114,17 @@ pub async fn add(
}, },
); );
if let Some(assets) = NonEmpty::from_vec(assets.clone()) {
create_send_await!( create_send_await!(
data_sender, data_sender,
threads::data::Message::new, threads::data::Message::new,
threads::data::Action::Add, threads::data::Action::Add,
assets.clone() assets
); );
}
Ok(( Ok((
StatusCode::CREATED, StatusCode::OK,
Json(AddAssetsResponse { Json(AddAssetsResponse {
added: assets.into_iter().map(|asset| asset.0).collect(), added: assets.into_iter().map(|asset| asset.0).collect(),
skipped, skipped,
@@ -173,7 +176,7 @@ pub async fn add_symbol(
data_sender, data_sender,
threads::data::Message::new, threads::data::Message::new,
threads::data::Action::Add, threads::data::Action::Add,
vec![(asset.symbol, asset.class.into())] nonempty![(asset.symbol, asset.class.into())]
); );
Ok(StatusCode::CREATED) Ok(StatusCode::CREATED)
@@ -197,7 +200,7 @@ pub async fn delete(
data_sender, data_sender,
threads::data::Message::new, threads::data::Message::new,
threads::data::Action::Remove, threads::data::Action::Remove,
vec![(asset.symbol, asset.class)] nonempty![(asset.symbol, asset.class)]
); );
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)

View File

@@ -6,6 +6,7 @@ use crate::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use log::{error, info}; use log::{error, info};
use nonempty::NonEmpty;
use qrust::{ use qrust::{
alpaca, alpaca,
types::{ types::{
@@ -98,27 +99,27 @@ impl super::Handler for Handler {
.await .await
} }
async fn queue_backfill(&self, jobs: &HashMap<String, Job>) { async fn queue_backfill(&self, jobs: &NonEmpty<Job>) {
if jobs.is_empty() || *ALPACA_SOURCE == Source::Sip { if *ALPACA_SOURCE == Source::Sip {
return; return;
} }
let fetch_to = jobs.values().map(|job| job.fetch_to).max().unwrap(); let fetch_to = jobs.maximum_by_key(|job| job.fetch_to).fetch_to;
let run_delay = duration_until(fetch_to + FIFTEEN_MINUTES + ONE_MINUTE); let run_delay = duration_until(fetch_to + FIFTEEN_MINUTES + ONE_MINUTE);
let symbols = jobs.keys().collect::<Vec<_>>(); let symbols = jobs.iter().map(|job| &job.symbol).collect::<Vec<_>>();
info!("Queing bar backfill for {:?} in {:?}.", symbols, run_delay); info!("Queing bar backfill for {:?} in {:?}.", symbols, run_delay);
sleep(run_delay).await; sleep(run_delay).await;
} }
async fn backfill(&self, jobs: HashMap<String, Job>) { async fn backfill(&self, jobs: NonEmpty<Job>) {
if jobs.is_empty() { let symbols = Vec::from(jobs.clone().map(|job| job.symbol));
return; let fetch_from = jobs.minimum_by_key(|job| job.fetch_from).fetch_from;
} let fetch_to = jobs.maximum_by_key(|job| job.fetch_to).fetch_to;
let freshness = jobs
let symbols = jobs.keys().cloned().collect::<Vec<_>>(); .into_iter()
let fetch_from = jobs.values().map(|job| job.fetch_from).min().unwrap(); .map(|job| (job.symbol, job.fresh))
let fetch_to = jobs.values().map(|job| job.fetch_to).max().unwrap(); .collect::<HashMap<_, _>>();
let mut bars = Vec::with_capacity(*CLICKHOUSE_BATCH_BARS_SIZE); let mut bars = Vec::with_capacity(*CLICKHOUSE_BATCH_BARS_SIZE);
let mut last_times = HashMap::new(); let mut last_times = HashMap::new();
@@ -173,7 +174,7 @@ impl super::Handler for Handler {
let backfilled = last_times let backfilled = last_times
.drain() .drain()
.map(|(symbol, time)| Backfill { .map(|(symbol, time)| Backfill {
fresh: jobs[&symbol].fresh, fresh: freshness[&symbol],
symbol, symbol,
time, time,
}) })

View File

@@ -4,6 +4,7 @@ pub mod news;
use async_trait::async_trait; use async_trait::async_trait;
use itertools::Itertools; use itertools::Itertools;
use log::{info, warn}; use log::{info, warn};
use nonempty::{nonempty, NonEmpty};
use qrust::{ use qrust::{
types::Backfill, types::Backfill,
utils::{last_minute, ONE_SECOND}, utils::{last_minute, ONE_SECOND},
@@ -25,12 +26,12 @@ pub enum Action {
pub struct Message { pub struct Message {
pub action: Action, pub action: Action,
pub symbols: Vec<String>, pub symbols: NonEmpty<String>,
pub response: oneshot::Sender<()>, pub response: oneshot::Sender<()>,
} }
impl Message { impl Message {
pub fn new(action: Action, symbols: Vec<String>) -> (Self, oneshot::Receiver<()>) { pub fn new(action: Action, symbols: NonEmpty<String>) -> (Self, oneshot::Receiver<()>) {
let (sender, receiver) = oneshot::channel::<()>(); let (sender, receiver) = oneshot::channel::<()>();
( (
Self { Self {
@@ -45,6 +46,7 @@ impl Message {
#[derive(Clone)] #[derive(Clone)]
pub struct Job { pub struct Job {
pub symbol: String,
pub fetch_from: OffsetDateTime, pub fetch_from: OffsetDateTime,
pub fetch_to: OffsetDateTime, pub fetch_to: OffsetDateTime,
pub fresh: bool, pub fresh: bool,
@@ -58,8 +60,8 @@ pub trait Handler: Send + Sync {
) -> Result<Vec<Backfill>, clickhouse::error::Error>; ) -> Result<Vec<Backfill>, clickhouse::error::Error>;
async fn delete_backfills(&self, symbol: &[String]) -> Result<(), clickhouse::error::Error>; async fn delete_backfills(&self, symbol: &[String]) -> Result<(), clickhouse::error::Error>;
async fn delete_data(&self, symbol: &[String]) -> Result<(), clickhouse::error::Error>; async fn delete_data(&self, symbol: &[String]) -> Result<(), clickhouse::error::Error>;
async fn queue_backfill(&self, jobs: &HashMap<String, Job>); async fn queue_backfill(&self, jobs: &NonEmpty<Job>);
async fn backfill(&self, jobs: HashMap<String, Job>); async fn backfill(&self, jobs: NonEmpty<Job>);
fn max_limit(&self) -> i64; fn max_limit(&self) -> i64;
fn log_string(&self) -> &'static str; fn log_string(&self) -> &'static str;
} }
@@ -108,7 +110,7 @@ pub async fn run(handler: Arc<Box<dyn Handler>>, mut receiver: mpsc::Receiver<Me
loop { loop {
let message = receiver.recv().await.unwrap(); let message = receiver.recv().await.unwrap();
spawn(handle_backfill_message( spawn(handle_message(
handler.clone(), handler.clone(),
backfill_jobs.clone(), backfill_jobs.clone(),
message, message,
@@ -116,13 +118,14 @@ pub async fn run(handler: Arc<Box<dyn Handler>>, mut receiver: mpsc::Receiver<Me
} }
} }
async fn handle_backfill_message( async fn handle_message(
handler: Arc<Box<dyn Handler>>, handler: Arc<Box<dyn Handler>>,
backfill_jobs: Arc<Mutex<Jobs>>, backfill_jobs: Arc<Mutex<Jobs>>,
message: Message, message: Message,
) { ) {
let backfill_jobs_clone = backfill_jobs.clone(); let backfill_jobs_clone = backfill_jobs.clone();
let mut backfill_jobs = backfill_jobs.lock().await; let mut backfill_jobs = backfill_jobs.lock().await;
let symbols = Vec::from(message.symbols);
match message.action { match message.action {
Action::Backfill => { Action::Backfill => {
@@ -130,16 +133,16 @@ async fn handle_backfill_message(
let max_limit = handler.max_limit(); let max_limit = handler.max_limit();
let backfills = handler let backfills = handler
.select_latest_backfills(&message.symbols) .select_latest_backfills(&symbols)
.await .await
.unwrap() .unwrap()
.into_iter() .into_iter()
.map(|backfill| (backfill.symbol.clone(), backfill)) .map(|backfill| (backfill.symbol.clone(), backfill))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let mut jobs = Vec::with_capacity(message.symbols.len()); let mut jobs = Vec::with_capacity(symbols.len());
for symbol in message.symbols { for symbol in symbols {
if backfill_jobs.contains_key(&symbol) { if backfill_jobs.contains_key(&symbol) {
warn!( warn!(
"Backfill for {} {} is already running, skipping.", "Backfill for {} {} is already running, skipping.",
@@ -148,9 +151,9 @@ async fn handle_backfill_message(
continue; continue;
} }
let fetch_from = backfills let backfill = backfills.get(&symbol);
.get(&symbol)
.map_or(OffsetDateTime::UNIX_EPOCH, |backfill| { let fetch_from = backfill.map_or(OffsetDateTime::UNIX_EPOCH, |backfill| {
backfill.time + ONE_SECOND backfill.time + ONE_SECOND
}); });
@@ -161,50 +164,42 @@ async fn handle_backfill_message(
return; return;
} }
let fresh = backfills let fresh = backfill.map_or(false, |backfill| backfill.fresh);
.get(&symbol)
.map_or(false, |backfill| backfill.fresh);
jobs.push(( jobs.push(Job {
symbol, symbol,
Job {
fetch_from, fetch_from,
fetch_to, fetch_to,
fresh, fresh,
}, });
));
}
if jobs.is_empty() {
return;
} }
let jobs = jobs let jobs = jobs
.into_iter() .into_iter()
.sorted_unstable_by_key(|job| job.1.fetch_from) .sorted_unstable_by_key(|job| job.fetch_from)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut job_groups = vec![HashMap::new()]; let mut job_groups: Vec<NonEmpty<Job>> = vec![];
let mut current_minutes = 0; let mut current_minutes = 0;
for job in jobs { for job in jobs {
let minutes = (job.1.fetch_to - job.1.fetch_from).whole_minutes(); let minutes = (job.fetch_to - job.fetch_from).whole_minutes();
if job_groups.last().unwrap().is_empty() || (current_minutes + minutes) <= max_limit if job_groups.last().is_some() && current_minutes + minutes <= max_limit {
{
let job_group = job_groups.last_mut().unwrap(); let job_group = job_groups.last_mut().unwrap();
job_group.insert(job.0, job.1); job_group.push(job);
current_minutes += minutes; current_minutes += minutes;
} else { } else {
let mut job_group = HashMap::new(); job_groups.push(nonempty![job]);
job_group.insert(job.0, job.1);
job_groups.push(job_group);
current_minutes = minutes; current_minutes = minutes;
} }
} }
for job_group in job_groups { for job_group in job_groups {
let symbols = job_group.keys().cloned().collect::<Vec<_>>(); let symbols = job_group
.iter()
.map(|job| job.symbol.clone())
.collect::<Vec<_>>();
let handler = handler.clone(); let handler = handler.clone();
let symbols_clone = symbols.clone(); let symbols_clone = symbols.clone();
@@ -220,7 +215,7 @@ async fn handle_backfill_message(
} }
} }
Action::Purge => { Action::Purge => {
for symbol in &message.symbols { for symbol in &symbols {
if let Some(job) = backfill_jobs.remove(symbol) { if let Some(job) = backfill_jobs.remove(symbol) {
job.abort(); job.abort();
let _ = job.await; let _ = job.await;
@@ -228,8 +223,8 @@ async fn handle_backfill_message(
} }
try_join!( try_join!(
handler.delete_backfills(&message.symbols), handler.delete_backfills(&symbols),
handler.delete_data(&message.symbols) handler.delete_data(&symbols)
) )
.unwrap(); .unwrap();
} }

View File

@@ -5,6 +5,7 @@ use crate::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use log::{error, info}; use log::{error, info};
use nonempty::NonEmpty;
use qrust::{ use qrust::{
alpaca, alpaca,
types::{ types::{
@@ -56,14 +57,14 @@ impl super::Handler for Handler {
.await .await
} }
async fn queue_backfill(&self, jobs: &HashMap<String, Job>) { async fn queue_backfill(&self, jobs: &NonEmpty<Job>) {
if jobs.is_empty() || *ALPACA_SOURCE == Source::Sip { if *ALPACA_SOURCE == Source::Sip {
return; return;
} }
let fetch_to = jobs.values().map(|job| job.fetch_to).max().unwrap(); let fetch_to = jobs.maximum_by_key(|job| job.fetch_to).fetch_to;
let run_delay = duration_until(fetch_to + FIFTEEN_MINUTES + ONE_MINUTE); let run_delay = duration_until(fetch_to + FIFTEEN_MINUTES + ONE_MINUTE);
let symbols = jobs.keys().cloned().collect::<Vec<_>>(); let symbols = jobs.iter().map(|job| &job.symbol).collect::<Vec<_>>();
info!("Queing news backfill for {:?} in {:?}.", symbols, run_delay); info!("Queing news backfill for {:?} in {:?}.", symbols, run_delay);
sleep(run_delay).await; sleep(run_delay).await;
@@ -71,15 +72,15 @@ impl super::Handler for Handler {
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
#[allow(clippy::iter_with_drain)] #[allow(clippy::iter_with_drain)]
async fn backfill(&self, jobs: HashMap<String, Job>) { async fn backfill(&self, jobs: NonEmpty<Job>) {
if jobs.is_empty() { let symbols = Vec::from(jobs.clone().map(|job| job.symbol));
return;
}
let symbols = jobs.keys().cloned().collect::<Vec<_>>();
let symbols_set = symbols.clone().into_iter().collect::<HashSet<_>>(); let symbols_set = symbols.clone().into_iter().collect::<HashSet<_>>();
let fetch_from = jobs.values().map(|job| job.fetch_from).min().unwrap(); let fetch_from = jobs.minimum_by_key(|job| job.fetch_from).fetch_from;
let fetch_to = jobs.values().map(|job| job.fetch_to).max().unwrap(); let fetch_to = jobs.maximum_by_key(|job| job.fetch_to).fetch_to;
let freshness = jobs
.into_iter()
.map(|job| (job.symbol, job.fresh))
.collect::<HashMap<_, _>>();
let mut news = Vec::with_capacity(*CLICKHOUSE_BATCH_NEWS_SIZE); let mut news = Vec::with_capacity(*CLICKHOUSE_BATCH_NEWS_SIZE);
let mut last_times = HashMap::new(); let mut last_times = HashMap::new();
@@ -137,7 +138,7 @@ impl super::Handler for Handler {
let backfilled = last_times let backfilled = last_times
.drain() .drain()
.map(|(symbol, time)| Backfill { .map(|(symbol, time)| Backfill {
fresh: jobs[&symbol].fresh, fresh: freshness[&symbol],
symbol, symbol,
time, time,
}) })

View File

@@ -8,6 +8,7 @@ use crate::{
}; };
use itertools::{Either, Itertools}; use itertools::{Either, Itertools};
use log::error; use log::error;
use nonempty::NonEmpty;
use qrust::{ use qrust::{
alpaca, alpaca,
types::{ types::{
@@ -35,12 +36,12 @@ pub enum Action {
pub struct Message { pub struct Message {
pub action: Action, pub action: Action,
pub assets: Vec<(String, Class)>, pub assets: NonEmpty<(String, Class)>,
pub response: oneshot::Sender<()>, pub response: oneshot::Sender<()>,
} }
impl Message { impl Message {
pub fn new(action: Action, assets: Vec<(String, Class)>) -> (Self, oneshot::Receiver<()>) { pub fn new(action: Action, assets: NonEmpty<(String, Class)>) -> (Self, oneshot::Receiver<()>) {
let (sender, receiver) = oneshot::channel(); let (sender, receiver) = oneshot::channel();
( (
Self { Self {
@@ -150,11 +151,6 @@ async fn handle_message(
news_backfill_sender: mpsc::Sender<backfill::Message>, news_backfill_sender: mpsc::Sender<backfill::Message>,
message: Message, message: Message,
) { ) {
if message.assets.is_empty() {
message.response.send(()).unwrap();
return;
}
let (us_equity_symbols, crypto_symbols): (Vec<_>, Vec<_>) = message let (us_equity_symbols, crypto_symbols): (Vec<_>, Vec<_>) = message
.assets .assets
.clone() .clone()
@@ -164,36 +160,28 @@ async fn handle_message(
Class::Crypto => Either::Right(asset.0), Class::Crypto => Either::Right(asset.0),
}); });
let symbols = message let symbols = message.assets.map(|(symbol, _)| symbol);
.assets
.into_iter()
.map(|(symbol, _)| symbol)
.collect::<Vec<_>>();
let bars_us_equity_future = async { let bars_us_equity_future = async {
if us_equity_symbols.is_empty() { if let Some(us_equity_symbols) = NonEmpty::from_vec(us_equity_symbols.clone()) {
return;
}
create_send_await!( create_send_await!(
bars_us_equity_websocket_sender, bars_us_equity_websocket_sender,
websocket::Message::new, websocket::Message::new,
message.action.into(), message.action.into(),
us_equity_symbols.clone() us_equity_symbols
); );
}
}; };
let bars_crypto_future = async { let bars_crypto_future = async {
if crypto_symbols.is_empty() { if let Some(crypto_symbols) = NonEmpty::from_vec(crypto_symbols.clone()) {
return;
}
create_send_await!( create_send_await!(
bars_crypto_websocket_sender, bars_crypto_websocket_sender,
websocket::Message::new, websocket::Message::new,
message.action.into(), message.action.into(),
crypto_symbols.clone() crypto_symbols
); );
}
}; };
let news_future = async { let news_future = async {
@@ -207,8 +195,15 @@ async fn handle_message(
join!(bars_us_equity_future, bars_crypto_future, news_future); join!(bars_us_equity_future, bars_crypto_future, news_future);
if message.action == Action::Disable {
message.response.send(()).unwrap();
return;
}
match message.action { match message.action {
Action::Add => { Action::Add | Action::Enable => {
let symbols = Vec::from(symbols.clone());
let assets = async { let assets = async {
alpaca::assets::get_by_symbols( alpaca::assets::get_by_symbols(
&config.alpaca_client, &config.alpaca_client,
@@ -264,24 +259,16 @@ async fn handle_message(
database::assets::delete_where_symbols( database::assets::delete_where_symbols(
&config.clickhouse_client, &config.clickhouse_client,
&config.clickhouse_concurrency_limiter, &config.clickhouse_concurrency_limiter,
&symbols, &Vec::from(symbols.clone()),
) )
.await .await
.unwrap(); .unwrap();
} }
_ => {} Action::Disable => unreachable!(),
}
if message.action == Action::Disable {
message.response.send(()).unwrap();
return;
} }
let bars_us_equity_future = async { let bars_us_equity_future = async {
if us_equity_symbols.is_empty() { if let Some(us_equity_symbols) = NonEmpty::from_vec(us_equity_symbols) {
return;
}
create_send_await!( create_send_await!(
bars_us_equity_backfill_sender, bars_us_equity_backfill_sender,
backfill::Message::new, backfill::Message::new,
@@ -292,13 +279,11 @@ async fn handle_message(
}, },
us_equity_symbols us_equity_symbols
); );
}
}; };
let bars_crypto_future = async { let bars_crypto_future = async {
if crypto_symbols.is_empty() { if let Some(crypto_symbols) = NonEmpty::from_vec(crypto_symbols) {
return;
}
create_send_await!( create_send_await!(
bars_crypto_backfill_sender, bars_crypto_backfill_sender,
backfill::Message::new, backfill::Message::new,
@@ -309,6 +294,7 @@ async fn handle_message(
}, },
crypto_symbols crypto_symbols
); );
}
}; };
let news_future = async { let news_future = async {
@@ -363,30 +349,36 @@ async fn handle_clock_message(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let bars_us_equity_future = async { let bars_us_equity_future = async {
if let Some(us_equity_symbols) = NonEmpty::from_vec(us_equity_symbols) {
create_send_await!( create_send_await!(
bars_us_equity_backfill_sender, bars_us_equity_backfill_sender,
backfill::Message::new, backfill::Message::new,
backfill::Action::Backfill, backfill::Action::Backfill,
us_equity_symbols us_equity_symbols
); );
}
}; };
let bars_crypto_future = async { let bars_crypto_future = async {
if let Some(crypto_symbols) = NonEmpty::from_vec(crypto_symbols) {
create_send_await!( create_send_await!(
bars_crypto_backfill_sender, bars_crypto_backfill_sender,
backfill::Message::new, backfill::Message::new,
backfill::Action::Backfill, backfill::Action::Backfill,
crypto_symbols crypto_symbols
); );
}
}; };
let news_future = async { let news_future = async {
if let Some(symbols) = NonEmpty::from_vec(symbols) {
create_send_await!( create_send_await!(
news_backfill_sender, news_backfill_sender,
backfill::Message::new, backfill::Message::new,
backfill::Action::Backfill, backfill::Action::Backfill,
symbols symbols
); );
}
}; };
join!(bars_us_equity_future, bars_crypto_future, news_future); join!(bars_us_equity_future, bars_crypto_future, news_future);

View File

@@ -7,6 +7,7 @@ use crate::{
use async_trait::async_trait; use async_trait::async_trait;
use clickhouse::inserter::Inserter; use clickhouse::inserter::Inserter;
use log::{debug, error, info}; use log::{debug, error, info};
use nonempty::NonEmpty;
use qrust::{ use qrust::{
types::{alpaca::websocket, Bar, Class}, types::{alpaca::websocket, Bar, Class},
utils::ONE_SECOND, utils::ONE_SECOND,
@@ -21,14 +22,14 @@ pub struct Handler {
pub config: Arc<Config>, pub config: Arc<Config>,
pub inserter: Arc<Mutex<Inserter<Bar>>>, pub inserter: Arc<Mutex<Inserter<Bar>>>,
pub subscription_message_constructor: pub subscription_message_constructor:
fn(Vec<String>) -> websocket::data::outgoing::subscribe::Message, fn(NonEmpty<String>) -> websocket::data::outgoing::subscribe::Message,
} }
#[async_trait] #[async_trait]
impl super::Handler for Handler { impl super::Handler for Handler {
fn create_subscription_message( fn create_subscription_message(
&self, &self,
symbols: Vec<String>, symbols: NonEmpty<String>,
) -> websocket::data::outgoing::subscribe::Message { ) -> websocket::data::outgoing::subscribe::Message {
(self.subscription_message_constructor)(symbols) (self.subscription_message_constructor)(symbols)
} }

View File

@@ -7,6 +7,7 @@ use backoff::{future::retry_notify, ExponentialBackoff};
use clickhouse::{inserter::Inserter, Row}; use clickhouse::{inserter::Inserter, Row};
use futures_util::{future::join_all, SinkExt, StreamExt}; use futures_util::{future::join_all, SinkExt, StreamExt};
use log::error; use log::error;
use nonempty::NonEmpty;
use qrust::types::alpaca::{self, websocket}; use qrust::types::alpaca::{self, websocket};
use serde::Serialize; use serde::Serialize;
use serde_json::{from_str, to_string}; use serde_json::{from_str, to_string};
@@ -38,12 +39,12 @@ impl From<super::Action> for Option<Action> {
pub struct Message { pub struct Message {
pub action: Option<Action>, pub action: Option<Action>,
pub symbols: Vec<String>, pub symbols: NonEmpty<String>,
pub response: oneshot::Sender<()>, pub response: oneshot::Sender<()>,
} }
impl Message { impl Message {
pub fn new(action: Option<Action>, symbols: Vec<String>) -> (Self, oneshot::Receiver<()>) { pub fn new(action: Option<Action>, symbols: NonEmpty<String>) -> (Self, oneshot::Receiver<()>) {
let (sender, receiver) = oneshot::channel(); let (sender, receiver) = oneshot::channel();
( (
Self { Self {
@@ -66,7 +67,7 @@ pub struct State {
pub trait Handler: Send + Sync + 'static { pub trait Handler: Send + Sync + 'static {
fn create_subscription_message( fn create_subscription_message(
&self, &self,
symbols: Vec<String>, symbols: NonEmpty<String>,
) -> websocket::data::outgoing::subscribe::Message; ) -> websocket::data::outgoing::subscribe::Message;
async fn handle_websocket_message( async fn handle_websocket_message(
&self, &self,
@@ -206,7 +207,7 @@ async fn run_connection(
drop(state); drop(state);
if !pending_subscriptions.is_empty() { if let Some(pending_subscriptions) = NonEmpty::from_vec(pending_subscriptions) {
if let Err(err) = sink if let Err(err) = sink
.send(tungstenite::Message::Text( .send(tungstenite::Message::Text(
to_string(&websocket::data::outgoing::Message::Subscribe( to_string(&websocket::data::outgoing::Message::Subscribe(
@@ -277,11 +278,6 @@ async fn handle_message(
sink_sender: mpsc::Sender<tungstenite::Message>, sink_sender: mpsc::Sender<tungstenite::Message>,
message: Message, message: Message,
) { ) {
if message.symbols.is_empty() {
message.response.send(()).unwrap();
return;
}
match message.action { match message.action {
Some(Action::Subscribe) => { Some(Action::Subscribe) => {
let (pending_subscriptions, receivers) = message let (pending_subscriptions, receivers) = message

View File

@@ -3,6 +3,7 @@ use crate::config::{Config, CLICKHOUSE_BATCH_NEWS_SIZE};
use async_trait::async_trait; use async_trait::async_trait;
use clickhouse::inserter::Inserter; use clickhouse::inserter::Inserter;
use log::{debug, error, info}; use log::{debug, error, info};
use nonempty::NonEmpty;
use qrust::{ use qrust::{
types::{alpaca::websocket, News}, types::{alpaca::websocket, News},
utils::ONE_SECOND, utils::ONE_SECOND,
@@ -19,7 +20,7 @@ pub struct Handler {
impl super::Handler for Handler { impl super::Handler for Handler {
fn create_subscription_message( fn create_subscription_message(
&self, &self,
symbols: Vec<String>, symbols: NonEmpty<String>,
) -> websocket::data::outgoing::subscribe::Message { ) -> websocket::data::outgoing::subscribe::Message {
websocket::data::outgoing::subscribe::Message::new_news(symbols) websocket::data::outgoing::subscribe::Message::new_news(symbols)
} }