2022-10-10 07:15:07 +03:00
|
|
|
use crate::frontend::authorization::{AuthorizedKey, RequestMetadata};
|
2022-10-10 08:35:25 +03:00
|
|
|
use anyhow::Context;
|
|
|
|
use chrono::{TimeZone, Utc};
|
2022-10-03 23:02:05 +03:00
|
|
|
use derive_more::From;
|
2022-10-10 07:15:07 +03:00
|
|
|
use entities::rpc_accounting;
|
|
|
|
use moka::future::{Cache, CacheBuilder};
|
2022-10-10 08:35:25 +03:00
|
|
|
use sea_orm::{ActiveModelTrait, DatabaseConnection};
|
|
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
2022-10-10 07:15:07 +03:00
|
|
|
use std::sync::Arc;
|
|
|
|
use std::{
|
|
|
|
sync::atomic::{AtomicU32, AtomicU64},
|
|
|
|
time::Duration,
|
|
|
|
};
|
2022-10-03 21:08:01 +03:00
|
|
|
use tokio::task::JoinHandle;
|
2022-10-10 07:15:07 +03:00
|
|
|
use tracing::{error, info, trace};
|
2022-10-03 23:02:05 +03:00
|
|
|
|
2022-10-10 07:15:07 +03:00
|
|
|
/// TODO: where should this be defined?
|
|
|
|
/// TODO: can we use something inside sea_orm instead?
|
2022-10-03 23:02:05 +03:00
|
|
|
#[derive(Debug)]
|
2022-10-10 07:15:07 +03:00
|
|
|
pub struct ProxyResponseStat {
|
|
|
|
user_key_id: u64,
|
|
|
|
method: String,
|
|
|
|
metadata: RequestMetadata,
|
2022-10-03 23:02:05 +03:00
|
|
|
}
|
|
|
|
|
2022-10-10 07:15:07 +03:00
|
|
|
// TODO: impl From for our database model
|
|
|
|
#[derive(Default)]
|
|
|
|
pub struct ProxyResponseAggregate {
|
|
|
|
// user_key_id: u64,
|
|
|
|
// method: String,
|
|
|
|
// error_response: bool,
|
2022-10-10 08:35:25 +03:00
|
|
|
first_timestamp: u64,
|
2022-10-10 07:15:07 +03:00
|
|
|
frontend_requests: AtomicU32,
|
|
|
|
backend_requests: AtomicU32,
|
2022-10-10 08:35:25 +03:00
|
|
|
last_timestamp: AtomicU64,
|
2022-10-10 07:15:07 +03:00
|
|
|
first_response_millis: u32,
|
|
|
|
sum_response_millis: AtomicU32,
|
2022-10-10 08:35:25 +03:00
|
|
|
sum_request_bytes: AtomicUsize,
|
|
|
|
sum_response_bytes: AtomicUsize,
|
2022-10-03 23:02:05 +03:00
|
|
|
}
|
|
|
|
|
2022-10-10 07:15:07 +03:00
|
|
|
/// key is the (user_key_id, method, error_response)
|
|
|
|
pub type UserProxyResponseCache = Cache<
|
|
|
|
(u64, String, bool),
|
|
|
|
Arc<ProxyResponseAggregate>,
|
|
|
|
hashbrown::hash_map::DefaultHashBuilder,
|
|
|
|
>;
|
|
|
|
/// key is the "time bucket" (timestamp / period)
|
|
|
|
pub type TimeProxyResponseCache =
|
|
|
|
Cache<u64, UserProxyResponseCache, hashbrown::hash_map::DefaultHashBuilder>;
|
|
|
|
|
|
|
|
pub struct StatEmitter {
|
|
|
|
chain_id: u64,
|
|
|
|
db_conn: DatabaseConnection,
|
|
|
|
period_seconds: u64,
|
|
|
|
/// the outer cache has a TTL and a handler for expiration
|
|
|
|
aggregated_proxy_responses: TimeProxyResponseCache,
|
2022-10-10 08:35:25 +03:00
|
|
|
save_rx: flume::Receiver<UserProxyResponseCache>,
|
2022-10-03 23:02:05 +03:00
|
|
|
}
|
|
|
|
|
2022-10-10 07:15:07 +03:00
|
|
|
/// A stat that we aggregate and then store in a database.
|
2022-10-03 23:02:05 +03:00
|
|
|
#[derive(Debug, From)]
|
2022-10-03 21:08:01 +03:00
|
|
|
pub enum Web3ProxyStat {
|
2022-10-03 23:02:05 +03:00
|
|
|
ProxyResponse(ProxyResponseStat),
|
2022-10-03 21:08:01 +03:00
|
|
|
}
|
|
|
|
|
2022-10-10 07:15:07 +03:00
|
|
|
impl ProxyResponseStat {
|
|
|
|
// TODO: should RequestMetadata be in an arc? or can we handle refs here?
|
|
|
|
pub fn new(method: String, authorized_key: AuthorizedKey, metadata: RequestMetadata) -> Self {
|
|
|
|
Self {
|
|
|
|
user_key_id: authorized_key.user_key_id,
|
|
|
|
method,
|
|
|
|
metadata,
|
2022-10-03 21:08:01 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl StatEmitter {
|
2022-10-10 07:15:07 +03:00
|
|
|
pub fn new(chain_id: u64, db_conn: DatabaseConnection, period_seconds: u64) -> Arc<Self> {
|
2022-10-10 08:35:25 +03:00
|
|
|
let (save_tx, save_rx) = flume::unbounded();
|
|
|
|
|
2022-10-10 07:15:07 +03:00
|
|
|
let aggregated_proxy_responses = CacheBuilder::default()
|
|
|
|
.time_to_live(Duration::from_secs(period_seconds * 3 / 2))
|
2022-10-10 08:35:25 +03:00
|
|
|
.eviction_listener_with_queued_delivery_mode(move |k, v, r| {
|
|
|
|
if let Err(err) = save_tx.send(v) {
|
|
|
|
error!(?err, "unable to save. sender closed!");
|
|
|
|
}
|
|
|
|
})
|
2022-10-10 07:15:07 +03:00
|
|
|
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::new());
|
|
|
|
|
|
|
|
let s = Self {
|
|
|
|
chain_id,
|
|
|
|
db_conn,
|
|
|
|
period_seconds,
|
|
|
|
aggregated_proxy_responses,
|
2022-10-10 08:35:25 +03:00
|
|
|
save_rx,
|
2022-10-10 07:15:07 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
Arc::new(s)
|
|
|
|
}
|
|
|
|
|
2022-10-07 05:15:53 +03:00
|
|
|
pub async fn spawn(
|
2022-10-10 07:15:07 +03:00
|
|
|
self: Arc<Self>,
|
2022-10-10 08:35:25 +03:00
|
|
|
) -> anyhow::Result<(
|
|
|
|
flume::Sender<Web3ProxyStat>,
|
|
|
|
JoinHandle<anyhow::Result<()>>,
|
|
|
|
JoinHandle<anyhow::Result<()>>,
|
|
|
|
)> {
|
|
|
|
let (aggregate_tx, aggregate_rx) = flume::unbounded::<Web3ProxyStat>();
|
2022-10-03 21:08:01 +03:00
|
|
|
|
2022-10-07 05:15:53 +03:00
|
|
|
// simple future that reads the channel and emits stats
|
2022-10-10 08:35:25 +03:00
|
|
|
let aggregate_f = {
|
|
|
|
let aggregated_proxy_responses = self.aggregated_proxy_responses.clone();
|
|
|
|
let clone = self.clone();
|
|
|
|
async move {
|
|
|
|
// TODO: select on shutdown handle so we can be sure to save every aggregate!
|
|
|
|
while let Ok(x) = aggregate_rx.recv_async().await {
|
|
|
|
trace!(?x, "aggregating stat");
|
|
|
|
|
|
|
|
// TODO: increment global stats (in redis? in local cache for prometheus?)
|
|
|
|
|
|
|
|
// TODO: batch stats? spawn this?
|
|
|
|
// TODO: where can we wait on this handle?
|
|
|
|
let clone = clone.clone();
|
|
|
|
tokio::spawn(async move { clone.aggregate_stat(x).await });
|
2022-10-07 05:15:53 +03:00
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
// no need to save manually. they save on expire
|
|
|
|
}
|
2022-10-03 23:02:05 +03:00
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
// shutting down. force a save
|
|
|
|
// TODO: this is handled by a background thread! we need to make sure the thread survives long enough to do its work!
|
|
|
|
aggregated_proxy_responses.invalidate_all();
|
2022-10-03 23:02:05 +03:00
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
info!("stat aggregator exited");
|
2022-10-10 07:15:07 +03:00
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
Ok(())
|
2022-10-03 21:08:01 +03:00
|
|
|
}
|
2022-10-10 08:35:25 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
let save_f = {
|
|
|
|
let db_conn = self.db_conn.clone();
|
|
|
|
let save_rx = self.save_rx.clone();
|
|
|
|
let chain_id = self.chain_id;
|
|
|
|
async move {
|
|
|
|
while let Ok(x) = save_rx.recv_async().await {
|
|
|
|
// TODO: batch these
|
|
|
|
for (k, v) in x.into_iter() {
|
|
|
|
// TODO: try_unwrap()?
|
|
|
|
let (user_key_id, method, error_response) = k.as_ref();
|
|
|
|
|
|
|
|
info!(?user_key_id, ?method, ?error_response, "saving");
|
|
|
|
|
|
|
|
let first_timestamp = Utc.timestamp(v.first_timestamp as i64, 0);
|
|
|
|
let frontend_requests = v.frontend_requests.load(Ordering::Acquire);
|
|
|
|
let backend_requests = v.backend_requests.load(Ordering::Acquire);
|
|
|
|
let first_response_millis = v.first_response_millis;
|
|
|
|
let sum_request_bytes = v.sum_request_bytes.load(Ordering::Acquire);
|
|
|
|
let sum_response_millis = v.sum_response_millis.load(Ordering::Acquire);
|
|
|
|
let sum_response_bytes = v.sum_response_bytes.load(Ordering::Acquire);
|
|
|
|
|
|
|
|
let stat = rpc_accounting::ActiveModel {
|
|
|
|
user_key_id: sea_orm::Set(*user_key_id),
|
|
|
|
chain_id: sea_orm::Set(chain_id),
|
|
|
|
method: sea_orm::Set(method.clone()),
|
|
|
|
error_response: sea_orm::Set(*error_response),
|
|
|
|
first_timestamp: sea_orm::Set(first_timestamp),
|
|
|
|
frontend_requests: sea_orm::Set(frontend_requests)
|
|
|
|
backend_requests: sea_orm::Set(backend_requests),
|
|
|
|
first_query_millis: sea_orm::Set(first_query_millis),
|
|
|
|
sum_request_bytes: sea_orm::Set(sum_request_bytes),
|
|
|
|
sum_response_millis: sea_orm::Set(sum_response_millis),
|
|
|
|
sum_response_bytes: sea_orm::Set(sum_response_bytes),
|
|
|
|
..Default::default()
|
|
|
|
};
|
2022-10-03 21:08:01 +03:00
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
// TODO: if this fails, rever adding the user, too
|
|
|
|
if let Err(err) = stat
|
|
|
|
.save(&db_conn)
|
|
|
|
.await
|
|
|
|
.context("Saving rpc_accounting stat")
|
|
|
|
{
|
|
|
|
error!(?err, "unable to save aggregated stats");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-10-10 07:15:07 +03:00
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
info!("stat saver exited");
|
2022-10-03 21:08:01 +03:00
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
Ok(())
|
|
|
|
}
|
2022-10-03 21:08:01 +03:00
|
|
|
};
|
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
// TODO: join and flatten these handles
|
|
|
|
let aggregate_handle = tokio::spawn(aggregate_f);
|
|
|
|
let save_handle = tokio::spawn(save_f);
|
2022-10-03 21:08:01 +03:00
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
Ok((aggregate_tx, aggregate_handle, save_handle))
|
2022-10-03 21:08:01 +03:00
|
|
|
}
|
2022-10-10 07:15:07 +03:00
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
pub async fn aggregate_stat(&self, stat: Web3ProxyStat) -> anyhow::Result<()> {
|
|
|
|
trace!(?stat, "aggregating");
|
2022-10-10 07:15:07 +03:00
|
|
|
match stat {
|
|
|
|
Web3ProxyStat::ProxyResponse(x) => {
|
|
|
|
// TODO: move this into another function?
|
|
|
|
|
|
|
|
// get the user cache for the current time bucket
|
2022-10-10 08:35:25 +03:00
|
|
|
let time_bucket = x.metadata.timestamp / self.period_seconds;
|
2022-10-10 07:15:07 +03:00
|
|
|
let user_cache = self
|
|
|
|
.aggregated_proxy_responses
|
|
|
|
.get_with(time_bucket, async move {
|
|
|
|
CacheBuilder::default()
|
|
|
|
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::new())
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
|
|
|
let error_response = x.metadata.error_response.load(Ordering::Acquire);
|
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
let key = (x.user_key_id, x.method, error_response);
|
|
|
|
|
|
|
|
let timestamp = x.metadata.timestamp;
|
|
|
|
let response_millis = x.metadata.response_millis.load(Ordering::Acquire);
|
2022-10-10 07:15:07 +03:00
|
|
|
|
|
|
|
let user_aggregate = user_cache
|
|
|
|
.get_with(key, async move {
|
2022-10-10 08:35:25 +03:00
|
|
|
let last_timestamp = timestamp.into();
|
2022-10-10 07:15:07 +03:00
|
|
|
|
|
|
|
let aggregate = ProxyResponseAggregate {
|
2022-10-10 08:35:25 +03:00
|
|
|
first_timestamp: timestamp,
|
|
|
|
first_response_millis: response_millis,
|
2022-10-10 07:15:07 +03:00
|
|
|
last_timestamp,
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
Arc::new(aggregate)
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2022-10-10 08:35:25 +03:00
|
|
|
user_aggregate
|
|
|
|
.backend_requests
|
|
|
|
.fetch_add(1, Ordering::Acquire);
|
|
|
|
|
|
|
|
user_aggregate
|
|
|
|
.frontend_requests
|
|
|
|
.fetch_add(1, Ordering::Acquire);
|
|
|
|
|
|
|
|
let request_bytes = x.metadata.request_bytes.load(Ordering::Acquire);
|
|
|
|
user_aggregate
|
|
|
|
.sum_request_bytes
|
|
|
|
.fetch_add(request_bytes, Ordering::Release);
|
|
|
|
|
|
|
|
let response_bytes = x.metadata.response_bytes.load(Ordering::Acquire);
|
|
|
|
user_aggregate
|
|
|
|
.sum_response_bytes
|
|
|
|
.fetch_add(response_bytes, Ordering::Release);
|
|
|
|
|
|
|
|
user_aggregate
|
|
|
|
.sum_response_millis
|
|
|
|
.fetch_add(response_millis, Ordering::Release);
|
|
|
|
|
|
|
|
user_aggregate
|
|
|
|
.last_timestamp
|
|
|
|
.fetch_max(x.metadata.timestamp, Ordering::Release);
|
2022-10-10 07:15:07 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2022-10-03 21:08:01 +03:00
|
|
|
}
|