use std::fs;
use std::num::NonZeroU64;
use std::path::Path;
use std::str;
use std::time::Duration;
use malloc_size_of::MallocSizeOf;
use rusqlite::params;
use rusqlite::types::FromSqlError;
use rusqlite::OptionalExtension;
use rusqlite::Transaction;
use rusqlite::{Error as SqlError, ErrorCode};
use connection::Connection;
use schema::Schema;
pub use schema::SchemaError;
use crate::common_metric_data::CommonMetricDataInternal;
use crate::database::migration::{self, MigrationState};
use crate::metrics::dual_labeled_counter::RECORD_SEPARATOR;
use crate::metrics::Metric;
use crate::Error;
use crate::Glean;
use crate::Lifetime;
use crate::Result;
mod connection;
mod schema;
#[derive(Debug)]
pub enum LoadState {
Ok,
Err(Error),
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum MigrationResult {
Unknown,
Error,
}
#[derive(Debug)]
pub struct Database {
pub(crate) conn: connection::Connection,
pub(crate) file_size: Option<NonZeroU64>,
load_state: LoadState,
pub(crate) migration_state: Option<MigrationState>,
pub(crate) migration_error: MigrationResult,
}
impl MallocSizeOf for Database {
fn size_of(&self, _ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
0
}
}
const DEFAULT_DATABASE_FILE_NAME: &str = "glean.sqlite";
fn database_size(dir: &Path) -> Option<NonZeroU64> {
let mut total_size = 0;
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
if let Ok(file_type) = entry.file_type() {
if file_type.is_file() {
let path = entry.path();
if let Ok(metadata) = fs::metadata(path) {
total_size += metadata.len();
} else {
continue;
}
}
}
}
}
NonZeroU64::new(total_size)
}
pub fn sqlite_open(path: &Path) -> std::result::Result<(Connection, LoadState), Error> {
match Connection::new::<Schema>(path) {
Err(e @ SchemaError::UnsupportedSchemaVersion(_)) => Err(e.into()),
Err(e @ SchemaError::Sqlite(SqlError::SqliteFailure(err, _))) => {
match err.code {
ErrorCode::PermissionDenied => Err(e.into()),
ErrorCode::NotADatabase => {
log::debug!("sqlite failed: not a database. starting from scratch.");
fs::remove_file(path).map_err(|_| rkv::StoreError::FileInvalid)?;
let conn = Connection::new::<Schema>(path)?;
Ok((conn, LoadState::Err(e.into())))
}
ErrorCode::CannotOpen => {
log::debug!("sqlite failed: cannot open. starting from scratch.");
fs::remove_file(path).map_err(|_| rkv::StoreError::FileInvalid)?;
let conn = Connection::new::<Schema>(path)?;
Ok((conn, LoadState::Err(e.into())))
}
_ => Err(e.into()),
}
}
Err(err @ SchemaError::Sqlite(SqlError::SqlInputError { .. })) => {
log::debug!("sqlite failed: schema migration failed. starting from scratch.");
fs::remove_file(path).map_err(|_| rkv::StoreError::FileInvalid)?;
let conn = Connection::new::<Schema>(path)?;
Ok((conn, LoadState::Err(err.into())))
}
other => {
let conn = other?;
Ok((conn, LoadState::Ok))
}
}
}
impl Database {
pub fn new(
data_path: &Path,
_delay_ping_lifetime_io: bool,
_ping_lifetime_threshold: usize,
_ping_lifetime_max_time: Duration,
) -> Result<Self> {
let path = data_path.join("db");
log::debug!("Database path: {:?}", path.display());
let file_size = database_size(&path);
fs::create_dir_all(&path)?;
let store_path = path.join(DEFAULT_DATABASE_FILE_NAME);
let sqlite_exists = store_path.exists();
let (conn, load_state) = sqlite_open(&store_path)?;
let mut db = Self {
conn,
file_size,
load_state,
migration_state: None,
migration_error: MigrationResult::Unknown,
};
if sqlite_exists {
log::debug!("SQLite database already exists. Not trying to migrate Rkv");
} else {
match migration::try_migrate(&path, &db) {
Ok(Some(state)) => {
log::debug!("Migration done. state={state:?}");
db.migration_state = Some(state);
}
Ok(None) => {
log::debug!("No migration.");
}
Err(e) => {
db.migration_error = MigrationResult::Error;
log::warn!("Migration failed! Continuing with SQLite backend without migrated data. Error: {e:?}")
}
}
}
Ok(db)
}
pub fn file_size(&self) -> Option<NonZeroU64> {
self.file_size
}
pub fn load_state(&self) -> Option<String> {
if let LoadState::Err(e) = &self.load_state {
Some(e.to_string())
} else {
None
}
}
pub fn iter_store<F>(
&self,
lifetime: Lifetime,
storage_name: &str,
mut transaction_fn: F,
) -> Result<()>
where
F: FnMut(&[u8], &[&str], &Metric),
{
let iter_sql = r#"
SELECT
id,
value,
labels
FROM telemetry
WHERE
lifetime = ?1
AND ping = ?2
"#;
self.conn.read(|conn| {
let mut stmt = conn.prepare_cached(iter_sql)?;
let rows = stmt.query_map(
params![lifetime.as_str().to_string(), storage_name],
|row| {
let id: String = row.get(0)?;
let blob: Vec<u8> = row.get(1)?;
let labels: String = row.get(2)?;
let blob: Metric =
rmp_serde::from_slice(&blob).map_err(|_| FromSqlError::InvalidType)?;
Ok((id, labels, blob))
},
)?;
for row in rows {
let Ok((metric_id, labels, metric)) = row else {
continue;
};
let labels = labels.split(RECORD_SEPARATOR).collect::<Vec<_>>();
transaction_fn(metric_id.as_bytes(), &labels, &metric);
}
Ok(())
})
}
pub fn get_metric(
&self,
data: &CommonMetricDataInternal,
storage_name: &str,
) -> Option<Metric> {
let get_metric_sql = r#"
SELECT
value
FROM telemetry
WHERE
id = ?1
AND ping = ?2
AND labels = ?3
LIMIT 1
"#;
let metric_identifier = &data.base_identifier();
self.conn
.read(|tx| {
let labels = data.check_labels(tx);
let mut stmt = tx.prepare_cached(get_metric_sql)?;
stmt.query_one([metric_identifier, storage_name, labels.label()], |row| {
let blob: Vec<u8> = row.get(0)?;
let blob: Metric =
rmp_serde::from_slice(&blob).map_err(|_| FromSqlError::InvalidType)?;
Ok(blob)
})
.optional()
})
.unwrap_or(None) }
pub fn has_metric(
&self,
lifetime: Lifetime,
storage_name: &str,
metric_identifier: &str,
) -> bool {
let has_metric_sql = r#"
SELECT id
FROM telemetry
WHERE
lifetime = ?1
AND ping = ?2
AND id = ?3
"#;
self.conn
.read(|conn| {
let Ok(mut stmt) = conn.prepare_cached(has_metric_sql) else {
return Ok(false);
};
let Ok(mut metric_iter) =
stmt.query([lifetime.as_str(), storage_name, metric_identifier])
else {
return Ok(false);
};
Result::<bool, ()>::Ok(metric_iter.next().map(|m| m.is_some()).unwrap_or(false))
})
.unwrap_or(false)
}
pub fn record(&self, glean: &Glean, data: &CommonMetricDataInternal, value: &Metric) {
let name = data.base_identifier();
_ = self.conn.write(|tx| {
let labels = data.check_labels(tx);
labels.record_error(glean, tx, &name, data.storage_names());
for ping_name in data.storage_names() {
if glean.is_ping_enabled(ping_name) {
if let Err(e) = self.record_per_lifetime(
tx,
data.inner.lifetime,
ping_name,
&name,
labels.label(),
value,
) {
log::error!(
"Failed to record metric '{}' into {}: {:?}",
data.base_identifier(),
ping_name,
e
);
}
}
}
Ok::<(), rusqlite::Error>(())
});
}
pub(crate) fn record_per_lifetime(
&self,
tx: &mut Transaction,
lifetime: Lifetime,
storage_name: &str,
key: &str,
labels: &str,
metric: &Metric,
) -> Result<()> {
let insert_sql = r#"
INSERT INTO
telemetry (id, ping, lifetime, labels, value)
VALUES
(?1, ?2, ?3, ?4, ?5)
ON CONFLICT(id, ping, labels) DO UPDATE SET
lifetime = excluded.lifetime,
value = excluded.value
"#;
let mut stmt = tx.prepare_cached(insert_sql)?;
let encoded = rmp_serde::to_vec(&metric).expect("IMPOSSIBLE: Serializing metric failed");
stmt.execute(params![
key,
storage_name,
lifetime.as_str(),
labels,
encoded
])?;
Ok(())
}
pub fn record_with<F>(&self, glean: &Glean, data: &CommonMetricDataInternal, transform: F)
where
F: FnMut(Option<Metric>) -> Metric,
{
_ = self
.conn
.write(|tx| self.record_with_transaction(glean, tx, data, transform));
}
pub fn record_with_transaction<F>(
&self,
glean: &Glean,
tx: &mut Transaction,
data: &CommonMetricDataInternal,
mut transform: F,
) -> Result<()>
where
F: FnMut(Option<Metric>) -> Metric,
{
let name = data.base_identifier();
let labels = data.check_labels(tx);
labels.record_error(glean, tx, &name, data.storage_names());
for ping_name in data.storage_names() {
if glean.is_ping_enabled(ping_name) {
if let Err(e) = self.record_per_lifetime_with(
tx,
data.inner.lifetime,
ping_name,
&name,
labels.label(),
&mut transform,
) {
log::error!(
"Failed to record metric '{}' into {}: {:?}",
data.base_identifier(),
ping_name,
e
);
}
}
}
Ok(())
}
fn record_per_lifetime_with<F>(
&self,
tx: &mut Transaction,
lifetime: Lifetime,
storage_name: &str,
key: &str,
labels: &str,
mut transform: F,
) -> Result<()>
where
F: FnMut(Option<Metric>) -> Metric,
{
let value_sql = r#"
SELECT value
FROM telemetry
WHERE
id = ?1
AND ping = ?2
AND lifetime = ?3
AND labels = ?4
LIMIT 1
"#;
let new_value = {
let mut stmt = tx.prepare_cached(value_sql)?;
let mut rows = stmt.query(params![
key,
storage_name,
lifetime.as_str().to_string(),
labels
])?;
if let Ok(Some(row)) = rows.next() {
let blob: Vec<u8> = row.get(0)?;
let old_value = rmp_serde::from_slice(&blob).ok();
transform(old_value)
} else {
transform(None)
}
};
let insert_sql = r#"
INSERT INTO
telemetry (id, ping, lifetime, labels, value)
VALUES
(?1, ?2, ?3, ?4, ?5)
ON CONFLICT(id, ping, labels) DO UPDATE SET
lifetime = excluded.lifetime,
value = excluded.value
"#;
{
let mut stmt = tx.prepare_cached(insert_sql)?;
let encoded =
rmp_serde::to_vec(&new_value).expect("IMPOSSIBLE: Serializing metric failed");
stmt.execute(params![
key,
storage_name,
lifetime.as_str(),
labels,
encoded
])?;
}
Ok(())
}
pub fn clear_ping_lifetime_storage(&self, storage_name: &str) -> Result<()> {
let clear_sql = "DELETE FROM telemetry WHERE lifetime = 'ping' AND ping = ?1";
self.conn.write(|tx| {
let mut stmt = tx.prepare_cached(clear_sql)?;
stmt.execute([storage_name])?;
Ok(())
})
}
pub fn clear_lifetime_storage(&self, lifetime: Lifetime, storage_name: &str) -> Result<()> {
let clear_sql = "DELETE FROM telemetry WHERE lifetime = ?1 AND ping = ?2";
self.conn.write(|tx| {
let mut stmt = tx.prepare_cached(clear_sql)?;
stmt.execute([lifetime.as_str(), storage_name])?;
Ok(())
})
}
pub fn remove_single_metric(
&self,
lifetime: Lifetime,
storage_name: &str,
metric_id: &str,
) -> Result<()> {
let clear_sql = "DELETE FROM telemetry WHERE lifetime = ?1 AND ping = ?2 AND id = ?3";
self.conn.write(|tx| {
let mut stmt = tx.prepare_cached(clear_sql)?;
stmt.execute([lifetime.as_str(), storage_name, metric_id])?;
Ok(())
})
}
pub fn clear_lifetime(&self, lifetime: Lifetime) {
let clear_sql = "DELETE FROM telemetry WHERE lifetime = ?1";
_ = self.conn.write(|tx| {
let mut stmt = tx.prepare_cached(clear_sql)?;
let res = stmt.execute([lifetime.as_str()]);
if let Err(e) = res {
log::warn!("Could not clear store for lifetime {:?}: {:?}", lifetime, e);
}
Ok::<(), rusqlite::Error>(())
});
}
pub fn clear_all(&self) {
let lifetimes = &[
Lifetime::User.as_str(),
Lifetime::Ping.as_str(),
Lifetime::Application.as_str(),
];
let clear_sql =
"DELETE FROM telemetry WHERE lifetime = ?1 OR lifetime = ?2 OR lifetime = ?3";
_ = self.conn.write(|tx| {
let mut stmt = tx.prepare_cached(clear_sql)?;
let res = stmt.execute(lifetimes);
if let Err(e) = res {
log::warn!("Could not clear store for all lifetimes: {:?}", e);
}
Ok::<(), rusqlite::Error>(())
});
}
pub fn persist_ping_lifetime_data(&self) -> Result<()> {
Ok(())
}
}