From ac941a801ab0c1cf38202adfc651236b19120d12 Mon Sep 17 00:00:00 2001
From: Jason Volk <jason@zemos.net>
Date: Fri, 2 Aug 2024 01:40:41 +0000
Subject: [PATCH] add interface to query rocksdb properties w/ admin cmd

Signed-off-by: Jason Volk <jason@zemos.net>
---
 src/admin/debug/commands.rs | 21 +++++++++++++++++++++
 src/admin/debug/mod.rs      |  8 ++++++++
 src/database/engine.rs      | 18 +++++++++++++++++-
 src/database/map.rs         |  6 +++++-
 4 files changed, 51 insertions(+), 2 deletions(-)

diff --git a/src/admin/debug/commands.rs b/src/admin/debug/commands.rs
index 53f4cf641..014ba79b5 100644
--- a/src/admin/debug/commands.rs
+++ b/src/admin/debug/commands.rs
@@ -828,3 +828,24 @@ pub(super) async fn list_dependencies(&self, names: bool) -> Result<RoomMessageE
 
 	Ok(RoomMessageEventContent::notice_markdown(out))
 }
+
+#[admin_command]
+pub(super) async fn database_stats(
+	&self, property: Option<String>, map: Option<String>,
+) -> Result<RoomMessageEventContent> {
+	let property = property.unwrap_or_else(|| "rocksdb.stats".to_owned());
+	let map_name = map.as_ref().map_or(utils::string::EMPTY, String::as_str);
+
+	let mut out = String::new();
+	for (name, map) in self.services.db.iter_maps() {
+		if !map_name.is_empty() && *map_name != *name {
+			continue;
+		}
+
+		let res = map.property(&property)?;
+		let res = res.trim();
+		writeln!(out, "##### {name}:\n```\n{res}\n```")?;
+	}
+
+	Ok(RoomMessageEventContent::notice_markdown(out))
+}
diff --git a/src/admin/debug/mod.rs b/src/admin/debug/mod.rs
index fbe6fd264..1f51a35e4 100644
--- a/src/admin/debug/mod.rs
+++ b/src/admin/debug/mod.rs
@@ -184,6 +184,14 @@ pub(super) enum DebugCommand {
 		names: bool,
 	},
 
+	/// - Get database statistics
+	DatabaseStats {
+		property: Option<String>,
+
+		#[arg(short, long, alias("column"))]
+		map: Option<String>,
+	},
+
 	/// - Developer test stubs
 	#[command(subcommand)]
 	#[allow(non_snake_case)]
diff --git a/src/database/engine.rs b/src/database/engine.rs
index bf172551f..3975d3d9a 100644
--- a/src/database/engine.rs
+++ b/src/database/engine.rs
@@ -1,5 +1,6 @@
 use std::{
 	collections::{BTreeSet, HashMap},
+	ffi::CStr,
 	fmt::Write,
 	path::PathBuf,
 	sync::{atomic::AtomicU32, Arc, Mutex, RwLock},
@@ -9,7 +10,8 @@
 use rocksdb::{
 	backup::{BackupEngine, BackupEngineOptions},
 	perf::get_memory_usage_stats,
-	BoundColumnFamily, Cache, ColumnFamilyDescriptor, DBCommon, DBWithThreadMode, Env, MultiThreaded, Options,
+	AsColumnFamilyRef, BoundColumnFamily, Cache, ColumnFamilyDescriptor, DBCommon, DBWithThreadMode, Env,
+	MultiThreaded, Options,
 };
 
 use crate::{
@@ -240,6 +242,20 @@ pub fn file_list(&self) -> Result<String> {
 			},
 		}
 	}
+
+	/// Query for database property by null-terminated name which is expected to
+	/// have a result with an integer representation. This is intended for
+	/// low-overhead programmatic use.
+	pub(crate) fn property_integer(&self, cf: &impl AsColumnFamilyRef, name: &CStr) -> Result<u64> {
+		result(self.db.property_int_value_cf(cf, name))
+			.and_then(|val| val.map_or_else(|| Err!("Property {name:?} not found."), Ok))
+	}
+
+	/// Query for database property by name receiving the result in a string.
+	pub(crate) fn property(&self, cf: &impl AsColumnFamilyRef, name: &str) -> Result<String> {
+		result(self.db.property_value_cf(cf, name))
+			.and_then(|val| val.map_or_else(|| Err!("Property {name:?} not found."), Ok))
+	}
 }
 
 pub(crate) fn repair(db_opts: &Options, path: &PathBuf) -> Result<()> {
diff --git a/src/database/map.rs b/src/database/map.rs
index 1b35a72aa..ddae8c813 100644
--- a/src/database/map.rs
+++ b/src/database/map.rs
@@ -1,4 +1,4 @@
-use std::{future::Future, mem::size_of, pin::Pin, sync::Arc};
+use std::{ffi::CStr, future::Future, mem::size_of, pin::Pin, sync::Arc};
 
 use conduit::{utils, Result};
 use rocksdb::{
@@ -189,6 +189,10 @@ pub fn watch_prefix<'a>(&'a self, prefix: &Key) -> Pin<Box<dyn Future<Output = (
 		self.watchers.watch(prefix)
 	}
 
+	pub fn property_integer(&self, name: &CStr) -> Result<u64> { self.db.property_integer(&self.cf(), name) }
+
+	pub fn property(&self, name: &str) -> Result<String> { self.db.property(&self.cf(), name) }
+
 	#[inline]
 	pub fn name(&self) -> &str { &self.name }
 
-- 
GitLab