Compare commits

...
19 Commits
Author SHA1 Message Date
Greg Heartsfield 95748647f0 build: bump version to 0.7.13 2022-12-22 16:27:34 -06:00
Greg Heartsfield 25480e837f fix: do not block writers for more than 1 second during checkpoints 2022-12-22 16:10:49 -06:00
Greg Heartsfield b80b54cd9d improvement: reduce logging, especially for database pool size 2022-12-22 15:47:33 -06:00
Greg Heartsfield 8ea732cbe5 feat: perform regular database maintenance (60sec), without blocking main writer thread 2022-12-22 15:16:21 -06:00
Greg Heartsfield 0f68c4e5c2 refactor: formatting 2022-12-22 15:15:45 -06:00
Greg Heartsfield dab2cd5792 wip: future changes to rustfmt 2022-12-22 15:13:54 -06:00
Greg Heartsfield f411aa6fc2 fix: do not re-verify NIP-05 entries where metadata was deleted 2022-12-22 13:01:48 -06:00
Greg Heartsfield d31bbda087 improvement: reduce lifetime of database connections 2022-12-22 13:01:12 -06:00
Greg Heartsfield 5917bc53b2 improvement: run maintenance every 60 seconds instead of by event count 2022-12-22 11:40:17 -06:00
Greg Heartsfield 91177c61a1 improvement: log reason for new event creation from nip05 2022-12-22 10:48:30 -06:00
Greg Heartsfield 53c2a8051c improvement: reduce logging 2022-12-22 10:29:27 -06:00
Greg Heartsfield 168cf513ac feat: perform full checkpoints and truncate WAL every 2k events 2022-12-22 10:11:05 -06:00
Greg Heartsfield ea204761c9 fix: do not show slow queries more than once per sub 2022-12-20 15:41:50 -06:00
Greg Heartsfield c270ae1434 improvement: reduce event count for db writer pauses 2022-12-20 15:25:24 -06:00
Greg Heartsfield 64bd983cb6 perf: every 5000 persisted events, pause for 500ms for backups
I have observed backups running for a very long time under heavy load,
this introduces some artificial delay to give the online backup enough
time to make progress.
2022-12-20 15:05:04 -06:00
Greg Heartsfield 1c153bc784 perf: shed DB query load when queue gets large 2022-12-20 13:23:21 -06:00
Greg Heartsfield dc11d9a619 improvement: explicitly rollback transaction on duplicate event 2022-12-20 13:23:04 -06:00
Greg Heartsfield cd1557787b improvement: log write pool 2022-12-20 13:21:57 -06:00
Greg Heartsfield 86bb7aeb9a improvement: function to check pool capacity 2022-12-20 10:07:01 -06:00
7 changed files with 147 additions and 40 deletions
Generated
+1 -1
View File
@@ -1096,7 +1096,7 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
[[package]] [[package]]
name = "nostr-rs-relay" name = "nostr-rs-relay"
version = "0.7.12" version = "0.7.13"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitcoin_hashes", "bitcoin_hashes",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nostr-rs-relay" name = "nostr-rs-relay"
version = "0.7.12" version = "0.7.13"
edition = "2021" edition = "2021"
authors = ["Greg Heartsfield <scsibug@imap.cc>"] authors = ["Greg Heartsfield <scsibug@imap.cc>"]
description = "A relay implementation for the Nostr protocol" description = "A relay implementation for the Nostr protocol"
+3
View File
@@ -1 +1,4 @@
edition = "2021" edition = "2021"
#max_width = 140
#chain_width = 100
#fn_call_width = 100
+5 -5
View File
@@ -5,7 +5,7 @@ use crate::error::Result;
use crate::subscription::Subscription; use crate::subscription::Subscription;
use std::collections::HashMap; use std::collections::HashMap;
use tracing::{debug, info}; use tracing::{debug, trace};
use uuid::Uuid; use uuid::Uuid;
/// A subscription identifier has a maximum length /// A subscription identifier has a maximum length
@@ -74,7 +74,7 @@ impl ClientConn {
// prevent arbitrarily long subscription identifiers from // prevent arbitrarily long subscription identifiers from
// being used. // being used.
if sub_id_len > MAX_SUBSCRIPTION_ID_LEN { if sub_id_len > MAX_SUBSCRIPTION_ID_LEN {
info!( debug!(
"ignoring sub request with excessive length: ({})", "ignoring sub request with excessive length: ({})",
sub_id_len sub_id_len
); );
@@ -84,7 +84,7 @@ impl ClientConn {
if self.subscriptions.contains_key(&k) { if self.subscriptions.contains_key(&k) {
self.subscriptions.remove(&k); self.subscriptions.remove(&k);
self.subscriptions.insert(k, s.clone()); self.subscriptions.insert(k, s.clone());
debug!( trace!(
"replaced existing subscription (cid: {}, sub: {:?})", "replaced existing subscription (cid: {}, sub: {:?})",
self.get_client_prefix(), self.get_client_prefix(),
s.get_id() s.get_id()
@@ -98,7 +98,7 @@ impl ClientConn {
} }
// add subscription // add subscription
self.subscriptions.insert(k, s); self.subscriptions.insert(k, s);
debug!( trace!(
"registered new subscription, currently have {} active subs (cid: {})", "registered new subscription, currently have {} active subs (cid: {})",
self.subscriptions.len(), self.subscriptions.len(),
self.get_client_prefix(), self.get_client_prefix(),
@@ -110,7 +110,7 @@ impl ClientConn {
pub fn unsubscribe(&mut self, c: &Close) { pub fn unsubscribe(&mut self, c: &Close) {
// TODO: return notice if subscription did not exist. // TODO: return notice if subscription did not exist.
self.subscriptions.remove(&c.id); self.subscriptions.remove(&c.id);
debug!( trace!(
"removed subscription, currently have {} active subs (cid: {})", "removed subscription, currently have {} active subs (cid: {})",
self.subscriptions.len(), self.subscriptions.len(),
self.get_client_prefix(), self.get_client_prefix(),
+115 -23
View File
@@ -38,8 +38,14 @@ pub struct SubmittedEvent {
/// Database file /// Database file
pub const DB_FILE: &str = "nostr.db"; pub const DB_FILE: &str = "nostr.db";
/// How many persisted events before optimization is triggered
pub const EVENT_COUNT_OPTIMIZE_TRIGGER: usize = 500; /// How frequently to run maintenance
/// How many persisted events before DB maintenannce is triggered.
pub const EVENT_MAINTENANCE_FREQ_SEC: u64 = 60;
/// How many persisted events before we pause for backups.
/// It isn't clear this is enough to make the online backup API work yet.
pub const EVENT_COUNT_BACKUP_PAUSE_TRIGGER: usize = 1000;
/// Build a database connection pool. /// Build a database connection pool.
/// # Panics /// # Panics
@@ -78,7 +84,7 @@ pub fn build_pool(
.test_on_check_out(true) // no noticeable performance hit .test_on_check_out(true) // no noticeable performance hit
.min_idle(Some(min_size)) .min_idle(Some(min_size))
.max_size(max_size) .max_size(max_size)
.max_lifetime(Some(Duration::from_secs(60))) .max_lifetime(Some(Duration::from_secs(30)))
.build(manager) .build(manager)
.unwrap(); .unwrap();
info!( info!(
@@ -90,7 +96,41 @@ pub fn build_pool(
/// Perform normal maintenance /// Perform normal maintenance
pub fn optimize_db(conn: &mut PooledConnection) -> Result<()> { pub fn optimize_db(conn: &mut PooledConnection) -> Result<()> {
let start = Instant::now();
conn.execute_batch("PRAGMA optimize;")?; conn.execute_batch("PRAGMA optimize;")?;
info!("optimize ran in {:?}", start.elapsed());
Ok(())
}
#[derive(Debug)]
enum SqliteReturnStatus {
SqliteOk,
SqliteBusy,
SqliteError,
SqliteOther(u64),
}
/// Checkpoint/Truncate WAL
pub fn checkpoint_db(conn: &mut PooledConnection) -> Result<()> {
let query = "PRAGMA wal_checkpoint(TRUNCATE);";
let start = Instant::now();
let (cp_result, wal_size, _frames_checkpointed) = conn.query_row(query, [], |row| {
let checkpoint_result: u64 = row.get(0)?;
let wal_size: u64 = row.get(1)?;
let frames_checkpointed: u64 = row.get(2)?;
Ok((checkpoint_result, wal_size, frames_checkpointed))
})?;
let result = match cp_result {
0 => SqliteReturnStatus::SqliteOk,
1 => SqliteReturnStatus::SqliteBusy,
2 => SqliteReturnStatus::SqliteError,
x => SqliteReturnStatus::SqliteOther(x),
};
info!(
"checkpoint ran in {:?} (result: {:?}, WAL size: {})",
start.elapsed(),
result,
wal_size
);
Ok(()) Ok(())
} }
@@ -133,8 +173,10 @@ pub async fn db_writer(
let rps_setting = settings.limits.messages_per_sec; let rps_setting = settings.limits.messages_per_sec;
let mut most_recent_rate_limit = Instant::now(); let mut most_recent_rate_limit = Instant::now();
let mut lim_opt = None; let mut lim_opt = None;
// Keep rough track of events so we can run optimize eventually. // Constant writing has interfered with online backups. Keep
let mut optimize_counter: usize = 0; // track of how long since we've given the backups a chance to
// run.
let mut backup_pause_counter: usize = 0;
let clock = governor::clock::QuantaClock::default(); let clock = governor::clock::QuantaClock::default();
if let Some(rps) = rps_setting { if let Some(rps) = rps_setting {
if rps > 0 { if rps > 0 {
@@ -199,7 +241,8 @@ pub async fn db_writer(
event.get_author_prefix() event.get_author_prefix()
); );
} else { } else {
info!("rejecting event, author ({:?} / {:?}) verification invalid (expired/wrong domain)", info!(
"rejecting event, author ({:?} / {:?}) verification invalid (expired/wrong domain)",
uv.name.to_string(), uv.name.to_string(),
event.get_author_prefix() event.get_author_prefix()
); );
@@ -243,6 +286,7 @@ pub async fn db_writer(
); );
event_write = true event_write = true
} else { } else {
log_pool_stats("writer", &pool);
match write_event(&mut pool.get()?, &event) { match write_event(&mut pool.get()?, &event) {
Ok(updated) => { Ok(updated) => {
if updated == 0 { if updated == 0 {
@@ -267,12 +311,11 @@ pub async fn db_writer(
notice_tx.try_send(Notice::error(event.id, msg)).ok(); notice_tx.try_send(Notice::error(event.id, msg)).ok();
} }
} }
// Use this as a trigger to do optimization backup_pause_counter += 1;
optimize_counter += 1; if backup_pause_counter > EVENT_COUNT_BACKUP_PAUSE_TRIGGER {
if optimize_counter > EVENT_COUNT_OPTIMIZE_TRIGGER { info!("pausing db write thread for a moment...");
info!("running database optimizer"); thread::sleep(Duration::from_millis(500));
optimize_counter = 0; backup_pause_counter = 0
optimize_db(&mut pool.get()?).ok();
} }
} }
@@ -320,7 +363,8 @@ pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
)?; )?;
if ins_count == 0 { if ins_count == 0 {
// if the event was a duplicate, no need to insert event or // if the event was a duplicate, no need to insert event or
// pubkey references. This will abort the txn. // pubkey references.
tx.rollback().ok();
return Ok(ins_count); return Ok(ins_count);
} }
// remember primary key of the event most recently inserted. // remember primary key of the event most recently inserted.
@@ -555,7 +599,10 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
let str_clause = format!("value IN ({})", repeat_vars(str_vals.len())); let str_clause = format!("value IN ({})", repeat_vars(str_vals.len()));
let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len())); let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len()));
// find evidence of the target tag name/value existing for this event. // find evidence of the target tag name/value existing for this event.
let tag_clause = format!("e.id IN (SELECT e.id FROM event e LEFT JOIN tag t on e.id=t.event_id WHERE hidden!=TRUE and (name=? AND ({} OR {})))", str_clause, blob_clause); let tag_clause = format!(
"e.id IN (SELECT e.id FROM event e LEFT JOIN tag t on e.id=t.event_id WHERE hidden!=TRUE and (name=? AND ({} OR {})))",
str_clause, blob_clause
);
// add the tag name as the first parameter // add the tag name as the first parameter
params.push(Box::new(key.to_string())); params.push(Box::new(key.to_string()));
// add all tag values that are plain strings as params // add all tag values that are plain strings as params
@@ -614,13 +661,47 @@ fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
(query, params) (query, params)
} }
fn log_pool_stats(pool: &SqlitePool) { /// Check if the pool is fully utilized
fn _pool_at_capacity(pool: &SqlitePool) -> bool {
let state: r2d2::State = pool.state();
state.idle_connections == 0
}
/// Log pool stats
fn log_pool_stats(name: &str, pool: &SqlitePool) {
let state: r2d2::State = pool.state(); let state: r2d2::State = pool.state();
let in_use_cxns = state.connections - state.idle_connections; let in_use_cxns = state.connections - state.idle_connections;
debug!( trace!(
"DB pool usage (in_use: {}, available: {})", "DB pool {:?} usage (in_use: {}, available: {})",
in_use_cxns, state.connections name,
in_use_cxns,
state.connections
); );
if state.connections == in_use_cxns {
debug!("DB pool {:?} is empty (in_use: {})", name, in_use_cxns);
}
}
/// Perform database maintenance on a regular basis
pub async fn db_maintenance(pool: SqlitePool) {
tokio::task::spawn(async move {
loop {
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(EVENT_MAINTENANCE_FREQ_SEC)) => {
if let Ok(mut conn) = pool.get() {
// the busy timer will block writers, so don't set
// this any higher than you want max latency for event
// writes.
conn.busy_timeout(Duration::from_secs(1)).ok();
debug!("running database optimizer");
optimize_db(&mut conn).ok();
debug!("running wal_checkpoint(TRUNCATE)");
checkpoint_db(&mut conn).ok();
}
}
};
}
});
} }
/// Perform a database query using a subscription. /// Perform a database query using a subscription.
@@ -639,8 +720,16 @@ pub async fn db_query(
let pre_spawn_start = Instant::now(); let pre_spawn_start = Instant::now();
task::spawn_blocking(move || { task::spawn_blocking(move || {
let db_queue_time = pre_spawn_start.elapsed(); let db_queue_time = pre_spawn_start.elapsed();
// report queuing time if it is slow // if the queue time was very long (>5 seconds), spare the DB and abort.
if db_queue_time > Duration::from_secs(1) { if db_queue_time > Duration::from_secs(5) {
info!(
"shedding DB query load from {:?} (cid: {}, sub: {:?})",
db_queue_time, client_id, sub.id
);
return Ok(());
}
// otherwise, report queuing time if it is slow
else if db_queue_time > Duration::from_secs(1) {
debug!( debug!(
"(slow) DB query queued for {:?} (cid: {}, sub: {:?})", "(slow) DB query queued for {:?} (cid: {}, sub: {:?})",
db_queue_time, client_id, sub.id db_queue_time, client_id, sub.id
@@ -650,9 +739,12 @@ pub async fn db_query(
let mut row_count: usize = 0; let mut row_count: usize = 0;
// generate SQL query // generate SQL query
let (q, p) = query_from_sub(&sub); let (q, p) = query_from_sub(&sub);
debug!("SQL generated in {:?}", start.elapsed()); let sql_gen_elapsed = start.elapsed();
if sql_gen_elapsed > Duration::from_millis(10) {
debug!("SQL (slow) generated in {:?}", start.elapsed());
}
// show pool stats // show pool stats
log_pool_stats(&pool); log_pool_stats("reader", &pool);
// cutoff for displaying slow queries // cutoff for displaying slow queries
let slow_cutoff = Duration::from_millis(2000); let slow_cutoff = Duration::from_millis(2000);
// any client that doesn't cause us to generate new rows in 5 // any client that doesn't cause us to generate new rows in 5
@@ -678,7 +770,7 @@ pub async fn db_query(
} }
// logging for slow queries; show sub and SQL. // logging for slow queries; show sub and SQL.
// to reduce logging; only show 1/16th of clients (leading 0) // to reduce logging; only show 1/16th of clients (leading 0)
if slow_first_event && client_id.starts_with("00") { if row_count == 0 && slow_first_event && client_id.starts_with("0") {
debug!( debug!(
"query req (slow): {:?} (cid: {}, sub: {:?})", "query req (slow): {:?} (cid: {}, sub: {:?})",
sub, client_id, sub.id sub, client_id, sub.id
+2 -2
View File
@@ -517,7 +517,7 @@ impl Verifier {
Ok(updated) => { Ok(updated) => {
if updated != 0 { if updated != 0 {
info!( info!(
"persisted event: {:?} in {:?}", "persisted event (new verified pubkey): {:?} in {:?}",
event.get_event_id_prefix(), event.get_event_id_prefix(),
start.elapsed() start.elapsed()
); );
@@ -721,7 +721,7 @@ pub fn query_oldest_user_verification(
earliest: u64, earliest: u64,
) -> Result<VerificationRecord> { ) -> Result<VerificationRecord> {
let tx = conn.transaction()?; let tx = conn.transaction()?;
let query = "SELECT v.id, v.name, e.event_hash, e.author, e.created_at, v.verified_at, v.failed_at, v.failure_count FROM user_verification v LEFT JOIN event e ON e.id=v.metadata_event WHERE (v.verified_at < ? OR v.verified_at IS NULL) AND (v.failed_at < ? OR v.failed_at IS NULL) ORDER BY v.verified_at ASC, v.failed_at ASC LIMIT 1;"; let query = "SELECT v.id, v.name, e.event_hash, e.author, e.created_at, v.verified_at, v.failed_at, v.failure_count FROM user_verification v INNER JOIN event e ON e.id=v.metadata_event WHERE (v.verified_at < ? OR v.verified_at IS NULL) AND (v.failed_at < ? OR v.failed_at IS NULL) ORDER BY v.verified_at ASC, v.failed_at ASC LIMIT 1;";
let mut stmt = tx.prepare_cached(query)?; let mut stmt = tx.prepare_cached(query)?;
let fields = stmt.query_row(params![earliest, earliest], |r| { let fields = stmt.query_row(params![earliest, earliest], |r| {
let rowid: u64 = r.get(0)?; let rowid: u64 = r.get(0)?;
+15 -3
View File
@@ -22,6 +22,7 @@ use hyper::upgrade::Upgraded;
use hyper::{ use hyper::{
header, server::conn::AddrStream, upgrade, Body, Request, Response, Server, StatusCode, header, server::conn::AddrStream, upgrade, Body, Request, Response, Server, StatusCode,
}; };
use rusqlite::OpenFlags;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
@@ -248,10 +249,10 @@ pub fn start_server(settings: Settings, shutdown_rx: MpscReceiver<()>) -> Result
// limit concurrent SQLite blocking threads // limit concurrent SQLite blocking threads
.max_blocking_threads(settings.limits.max_blocking_threads) .max_blocking_threads(settings.limits.max_blocking_threads)
.on_thread_start(|| { .on_thread_start(|| {
debug!("started new thread"); trace!("started new thread");
}) })
.on_thread_stop(|| { .on_thread_stop(|| {
debug!("stopping thread"); trace!("stopping thread");
}) })
.build() .build()
.unwrap(); .unwrap();
@@ -310,6 +311,17 @@ pub fn start_server(settings: Settings, shutdown_rx: MpscReceiver<()>) -> Result
} }
} }
} }
// build a connection pool for DB maintenance
let maintenance_pool = db::build_pool(
"maintenance writer",
&settings,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
1,
1,
false,
);
db::db_maintenance(maintenance_pool).await;
// listen for (external to tokio) shutdown request // listen for (external to tokio) shutdown request
let controlled_shutdown = invoke_shutdown.clone(); let controlled_shutdown = invoke_shutdown.clone();
tokio::spawn(async move { tokio::spawn(async move {
@@ -538,7 +550,7 @@ async fn nostr_server(
// TODO: serialize at broadcast time, instead of // TODO: serialize at broadcast time, instead of
// once for each consumer. // once for each consumer.
if let Ok(event_str) = serde_json::to_string(&global_event) { if let Ok(event_str) = serde_json::to_string(&global_event) {
debug!("sub match for client: {}, sub: {:?}, event: {:?}", trace!("sub match for client: {}, sub: {:?}, event: {:?}",
cid, s, cid, s,
global_event.get_event_id_prefix()); global_event.get_event_id_prefix());
// create an event response and send it // create an event response and send it