start adding tests that need docker for mysql management

This commit is contained in:
Bryan Stitt 2023-06-29 21:28:31 -07:00
parent 31c611f4ff
commit 5da334fcb7
8 changed files with 239 additions and 40 deletions

View File

@ -12,6 +12,7 @@ deadlock_detection = ["parking_lot/deadlock_detection"]
mimalloc = ["dep:mimalloc"] mimalloc = ["dep:mimalloc"]
tokio-console = ["dep:tokio-console", "dep:console-subscriber"] tokio-console = ["dep:tokio-console", "dep:console-subscriber"]
rdkafka-src = ["rdkafka/cmake-build", "rdkafka/libz", "rdkafka/ssl-vendored", "rdkafka/zstd-pkg-config"] rdkafka-src = ["rdkafka/cmake-build", "rdkafka/libz", "rdkafka/ssl-vendored", "rdkafka/zstd-pkg-config"]
tests-needing-docker = []
[dependencies] [dependencies]
deferred-rate-limiter = { path = "../deferred-rate-limiter" } deferred-rate-limiter = { path = "../deferred-rate-limiter" }

View File

@ -1,3 +1,4 @@
use anyhow::Context;
use derive_more::From; use derive_more::From;
use migration::sea_orm::{self, ConnectionTrait, Database}; use migration::sea_orm::{self, ConnectionTrait, Database};
use migration::sea_query::table::ColumnDef; use migration::sea_query::table::ColumnDef;
@ -31,24 +32,28 @@ pub async fn get_db(
let mut db_opt = sea_orm::ConnectOptions::new(db_url); let mut db_opt = sea_orm::ConnectOptions::new(db_url);
// TODO: load all these options from the config file. i think mysql default max is 100 // TODO: load all these options from the config file. i think docker mysql default max is 100
// TODO: sqlx logging only in debug. way too verbose for production // TODO: sqlx info logging is way too verbose for production.
db_opt db_opt
.connect_timeout(Duration::from_secs(30)) .acquire_timeout(Duration::from_secs(5))
.connect_timeout(Duration::from_secs(5))
.min_connections(min_connections) .min_connections(min_connections)
.max_connections(max_connections) .max_connections(max_connections)
.sqlx_logging(false); .sqlx_logging_level(tracing::log::LevelFilter::Warn)
// .sqlx_logging_level(log::LevelFilter::Info); .sqlx_logging(true);
Database::connect(db_opt).await Database::connect(db_opt).await
} }
pub async fn drop_migration_lock(db_conn: &DatabaseConnection) -> Result<(), DbErr> { pub async fn drop_migration_lock(db_conn: &DatabaseConnection) -> anyhow::Result<()> {
let db_backend = db_conn.get_database_backend(); let db_backend = db_conn.get_database_backend();
let drop_lock_statment = db_backend.build(Table::drop().table(Alias::new("migration_lock"))); let drop_lock_statment = db_backend.build(Table::drop().table(Alias::new("migration_lock")));
db_conn.execute(drop_lock_statment).await?; db_conn
.execute(drop_lock_statment)
.await
.context("dropping lock")?;
debug!("migration lock unlocked"); debug!("migration lock unlocked");
@ -59,7 +64,7 @@ pub async fn drop_migration_lock(db_conn: &DatabaseConnection) -> Result<(), DbE
pub async fn migrate_db( pub async fn migrate_db(
db_conn: &DatabaseConnection, db_conn: &DatabaseConnection,
override_existing_lock: bool, override_existing_lock: bool,
) -> Result<(), DbErr> { ) -> anyhow::Result<()> {
let db_backend = db_conn.get_database_backend(); let db_backend = db_conn.get_database_backend();
// TODO: put the timestamp and hostname into this as columns? // TODO: put the timestamp and hostname into this as columns?
@ -75,6 +80,8 @@ pub async fn migrate_db(
return Ok(()); return Ok(());
} }
info!("waiting for migration lock...");
// there are migrations to apply // there are migrations to apply
// acquire a lock // acquire a lock
if let Err(err) = db_conn.execute(create_lock_statment.clone()).await { if let Err(err) = db_conn.execute(create_lock_statment.clone()).await {
@ -96,13 +103,15 @@ pub async fn migrate_db(
break; break;
} }
info!("migrating...");
let migration_result = Migrator::up(db_conn, None).await; let migration_result = Migrator::up(db_conn, None).await;
// drop the distributed lock // drop the distributed lock
drop_migration_lock(db_conn).await?; drop_migration_lock(db_conn).await?;
// return if migrations erred // return if migrations erred
migration_result migration_result.map_err(Into::into)
} }
/// Connect to the database and run migrations /// Connect to the database and run migrations
@ -110,11 +119,13 @@ pub async fn get_migrated_db(
db_url: String, db_url: String,
min_connections: u32, min_connections: u32,
max_connections: u32, max_connections: u32,
) -> Result<DatabaseConnection, DbErr> { ) -> anyhow::Result<DatabaseConnection> {
// TODO: this seems to fail silently // TODO: this seems to fail silently
let db_conn = get_db(db_url, min_connections, max_connections).await?; let db_conn = get_db(db_url, min_connections, max_connections)
.await
.context("getting db")?;
migrate_db(&db_conn, false).await?; migrate_db(&db_conn, false).await.context("migrating db")?;
Ok(db_conn) Ok(db_conn)
} }

View File

@ -1565,7 +1565,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
dbg!(&x); info!(?x);
assert!(matches!(x, OpenRequestResult::NotReady)); assert!(matches!(x, OpenRequestResult::NotReady));

View File

@ -148,23 +148,23 @@ impl RpcAccountingSubCommand {
.signed_duration_since(stats.first_period_datetime) .signed_duration_since(stats.first_period_datetime)
.num_seconds() .num_seconds()
.into(); .into();
dbg!(query_seconds); info!(%query_seconds);
let avg_request_per_second = (stats.total_frontend_requests / query_seconds).round_dp(2); let avg_request_per_second = (stats.total_frontend_requests / query_seconds).round_dp(2);
dbg!(avg_request_per_second); info!(%avg_request_per_second);
let cache_hit_rate = (stats.total_cache_hits / stats.total_frontend_requests let cache_hit_rate = (stats.total_cache_hits / stats.total_frontend_requests
* Decimal::from(100)) * Decimal::from(100))
.round_dp(2); .round_dp(2);
dbg!(cache_hit_rate); info!(%cache_hit_rate);
let avg_response_millis = let avg_response_millis =
(stats.total_response_millis / stats.total_frontend_requests).round_dp(3); (stats.total_response_millis / stats.total_frontend_requests).round_dp(3);
dbg!(avg_response_millis); info!(%avg_response_millis);
let avg_response_bytes = let avg_response_bytes =
(stats.total_response_bytes / stats.total_frontend_requests).round(); (stats.total_response_bytes / stats.total_frontend_requests).round();
dbg!(avg_response_bytes); info!(%avg_response_bytes);
Ok(()) Ok(())
} }

View File

@ -1,5 +1,8 @@
use ethers::{ use ethers::{
prelude::{Http, Provider}, prelude::{
rand::{self, distributions::Alphanumeric, Rng},
Http, Provider,
},
signers::LocalWallet, signers::LocalWallet,
types::Address, types::Address,
utils::{Anvil, AnvilInstance}, utils::{Anvil, AnvilInstance},
@ -8,21 +11,30 @@ use hashbrown::HashMap;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
env, env,
process::Command as SyncCommand,
str::FromStr, str::FromStr,
sync::atomic::{AtomicU16, Ordering}, sync::atomic::{AtomicU16, Ordering},
}; };
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use tokio::{ use tokio::{
net::TcpStream,
process::Command as AsyncCommand,
sync::broadcast::{self, error::SendError}, sync::broadcast::{self, error::SendError},
task::JoinHandle, task::JoinHandle,
time::{sleep, Instant}, time::{sleep, Instant},
}; };
use tracing::info; use tracing::{info, trace};
use web3_proxy::{ use web3_proxy::{
config::{AppConfig, TopConfig, Web3RpcConfig}, config::{AppConfig, TopConfig, Web3RpcConfig},
relational_db::get_migrated_db,
sub_commands::ProxydSubCommand, sub_commands::ProxydSubCommand,
}; };
pub struct DbData {
container_name: String,
url: Option<String>,
}
pub struct TestApp { pub struct TestApp {
/// anvil shuts down when this guard is dropped. /// anvil shuts down when this guard is dropped.
pub anvil: AnvilInstance, pub anvil: AnvilInstance,
@ -30,8 +42,11 @@ pub struct TestApp {
/// connection to anvil. /// connection to anvil.
pub anvil_provider: Provider<Http>, pub anvil_provider: Provider<Http>,
/// keep track of the database so it can be stopped on drop
pub db: Option<DbData>,
/// spawn handle for the proxy. /// spawn handle for the proxy.
pub handle: Mutex<Option<JoinHandle<anyhow::Result<()>>>>, pub proxy_handle: Mutex<Option<JoinHandle<anyhow::Result<()>>>>,
/// connection to the proxy that is connected to anil. /// connection to the proxy that is connected to anil.
pub proxy_provider: Provider<Http>, pub proxy_provider: Provider<Http>,
@ -41,7 +56,7 @@ pub struct TestApp {
} }
impl TestApp { impl TestApp {
pub async fn spawn() -> Self { pub async fn spawn(setup_db: bool) -> Self {
let num_workers = 2; let num_workers = 2;
// TODO: move basic setup into a test fixture // TODO: move basic setup into a test fixture
@ -58,14 +73,146 @@ impl TestApp {
let anvil_provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap(); let anvil_provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
let db = if setup_db {
// sqlite doesn't seem to work. our migrations are written for mysql
// so lets use docker to start mysql
let password: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect();
let random: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect();
let db_container_name = format!("web3-proxy-test-{}", random);
info!(%db_container_name);
// create the db_data as soon as the url is known
// when this is dropped, the db will be stopped
let mut db_data = DbData {
container_name: db_container_name.clone(),
url: None,
};
let _ = AsyncCommand::new("docker")
.args([
"run",
"--name",
&db_container_name,
"--rm",
"-d",
"-e",
&format!("MYSQL_ROOT_PASSWORD={}", password),
"-e",
"MYSQL_DATABASE=web3_proxy_test",
"-p",
"0:3306",
"mysql",
])
.output()
.await
.expect("failed to start db");
// give the db a second to start
// TODO: wait until docker says it is healthy
sleep(Duration::from_secs(1)).await;
// TODO: why is this always empty?!
let docker_inspect_output = AsyncCommand::new("docker")
.args(["inspect", &db_container_name])
.output()
.await
.unwrap();
let docker_inspect_json = String::from_utf8(docker_inspect_output.stdout).unwrap();
trace!(%docker_inspect_json);
let docker_inspect_json: serde_json::Value =
serde_json::from_str(&docker_inspect_json).unwrap();
let mysql_ports = docker_inspect_json
.get(0)
.unwrap()
.get("NetworkSettings")
.unwrap()
.get("Ports")
.unwrap()
.get("3306/tcp")
.unwrap()
.get(0)
.unwrap();
trace!(?mysql_ports);
let mysql_port: u64 = mysql_ports
.get("HostPort")
.expect("unable to determine mysql port")
.as_str()
.unwrap()
.parse()
.unwrap();
let mysql_ip = mysql_ports
.get("HostIp")
.and_then(|x| x.as_str())
.expect("unable to determine mysql ip");
// let mysql_ip = "localhost";
// let mysql_ip = "127.0.0.1";
let db_url = format!(
"mysql://root:{}@{}:{}/web3_proxy_test",
password, mysql_ip, mysql_port
);
info!(%db_url, "waiting for start");
db_data.url = Some(db_url.clone());
let start = Instant::now();
let max_wait = Duration::from_secs(30);
loop {
if start.elapsed() > max_wait {
panic!("db took too long to start");
}
if TcpStream::connect(format!("{}:{}", mysql_ip, mysql_port))
.await
.is_ok()
{
break;
};
// not open wait. sleep and then try again
sleep(Duration::from_secs(1)).await;
}
info!(%db_url, "db is ready for connections");
// try to migrate
let _ = get_migrated_db(db_url, 1, 1)
.await
.expect("failed migration");
Some(db_data)
} else {
None
};
let db_url = db.as_ref().and_then(|x| x.url.clone());
// make a test TopConfig // make a test TopConfig
// TODO: test influx // TODO: test influx
// TODO: test redis // TODO: test redis
let top_config = TopConfig { let top_config = TopConfig {
app: AppConfig { app: AppConfig {
chain_id: 31337, chain_id: 31337,
// TODO: [make sqlite work](<https://www.sea-ql.org/SeaORM/docs/write-test/sqlite/>) db_url,
// db_url: Some("sqlite::memory:".into()),
default_user_max_requests_per_period: Some(6_000_000), default_user_max_requests_per_period: Some(6_000_000),
deposit_factory_contract: Address::from_str( deposit_factory_contract: Address::from_str(
"4e3BC2054788De923A04936C6ADdB99A05B0Ea36", "4e3BC2054788De923A04936C6ADdB99A05B0Ea36",
@ -111,7 +258,8 @@ impl TestApp {
let mut frontend_port = frontend_port_arc.load(Ordering::Relaxed); let mut frontend_port = frontend_port_arc.load(Ordering::Relaxed);
let start = Instant::now(); let start = Instant::now();
while frontend_port == 0 { while frontend_port == 0 {
if start.elapsed() > Duration::from_secs(1) { // we have to give it some time because it might have to do migrations
if start.elapsed() > Duration::from_secs(10) {
panic!("took too long to start!"); panic!("took too long to start!");
} }
@ -126,7 +274,8 @@ impl TestApp {
Self { Self {
anvil, anvil,
anvil_provider, anvil_provider,
handle: Mutex::new(Some(handle)), db,
proxy_handle: Mutex::new(Some(handle)),
proxy_provider, proxy_provider,
shutdown_sender, shutdown_sender,
} }
@ -136,18 +285,20 @@ impl TestApp {
self.shutdown_sender.send(()) self.shutdown_sender.send(())
} }
#[allow(unused)]
pub async fn wait(&self) { pub async fn wait(&self) {
let _ = self.stop();
// TODO: lock+take feels weird, but it works // TODO: lock+take feels weird, but it works
let handle = self.handle.lock().take(); let handle = self.proxy_handle.lock().take();
if let Some(handle) = handle { if let Some(handle) = handle {
let _ = self.stop();
info!("waiting for the app to stop..."); info!("waiting for the app to stop...");
handle.await.unwrap().unwrap(); handle.await.unwrap().unwrap();
} }
} }
#[allow(unused)]
pub fn wallet(&self, id: usize) -> LocalWallet { pub fn wallet(&self, id: usize) -> LocalWallet {
self.anvil.keys()[id].clone().into() self.anvil.keys()[id].clone().into()
} }
@ -160,3 +311,14 @@ impl Drop for TestApp {
// TODO: do we care about waiting for it to stop? it will slow our tests down so we probably only care about waiting in some tests // TODO: do we care about waiting for it to stop? it will slow our tests down so we probably only care about waiting in some tests
} }
} }
impl Drop for DbData {
fn drop(&mut self) {
// TODO: this doesn't seem to run
info!(%self.container_name, "killing db");
let _ = SyncCommand::new("docker")
.args(["kill", "-s", "9", &self.container_name])
.output();
}
}

View File

@ -2,26 +2,29 @@ mod common;
use crate::common::TestApp; use crate::common::TestApp;
#[ignore] #[cfg_attr(not(feature = "tests-needing-docker"), ignore)]
#[ignore = "under construction"]
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn test_admin_imitate_user() { async fn test_admin_imitate_user() {
let x = TestApp::spawn().await; let x = TestApp::spawn(true).await;
todo!(); todo!();
} }
#[ignore] #[cfg_attr(not(feature = "tests-needing-docker"), ignore)]
#[ignore = "under construction"]
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn test_admin_grant_credits() { async fn test_admin_grant_credits() {
let x = TestApp::spawn().await; let x = TestApp::spawn(true).await;
todo!(); todo!();
} }
#[ignore] #[cfg_attr(not(feature = "tests-needing-docker"), ignore)]
#[ignore = "under construction"]
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn test_admin_change_user_tier() { async fn test_admin_change_user_tier() {
let x = TestApp::spawn().await; let x = TestApp::spawn(true).await;
todo!(); todo!();
} }

View File

@ -10,9 +10,16 @@ use tokio::{
}; };
use web3_proxy::rpcs::blockchain::ArcBlock; use web3_proxy::rpcs::blockchain::ArcBlock;
#[cfg_attr(not(feature = "tests-needing-docker"), ignore)]
#[ignore = "under construction"]
#[test_log::test(tokio::test)]
async fn it_migrates_the_db() {
TestApp::spawn(true).await;
}
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn it_starts_and_stops() { async fn it_starts_and_stops() {
let x = TestApp::spawn().await; let x = TestApp::spawn(false).await;
let anvil_provider = &x.anvil_provider; let anvil_provider = &x.anvil_provider;
let proxy_provider = &x.proxy_provider; let proxy_provider = &x.proxy_provider;

View File

@ -1,21 +1,36 @@
mod common; mod common;
use crate::common::TestApp; use crate::common::TestApp;
use ethers::signers::Signer;
use tracing::info;
#[ignore] /// TODO: 191 and the other message formats in another test
#[cfg_attr(not(feature = "tests-needing-docker"), ignore)]
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn test_log_in_and_out() { async fn test_log_in_and_out() {
let x = TestApp::spawn().await; let x = TestApp::spawn(true).await;
let w = x.wallet(0); let w = x.wallet(0);
let login_url = format!("{}user/login/{:?}", x.proxy_provider.url(), w.address());
let login_response = reqwest::get(login_url).await.unwrap();
info!(?login_response);
// TODO: sign the message and POST it
// TODO: get bearer token out of response
// TODO: log out
todo!(); todo!();
} }
#[ignore] #[cfg_attr(not(feature = "tests-needing-docker"), ignore)]
#[ignore = "under construction"]
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn test_referral_bonus() { async fn test_referral_bonus() {
let x = TestApp::spawn().await; let x = TestApp::spawn(true).await;
todo!(); todo!();
} }