use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use log::error;
use malloc_size_of_derive::MallocSizeOf;
use rusqlite::Row;
use sea_query::{ColumnDef, Expr, ExprTrait, Iden, OnConflict, Query, SqliteQueryBuilder, Table};
use sea_query_rusqlite::RusqliteBinder;
use servo_config::pref;
use servo_url::ServoUrl;
use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock};
use crate::http_cache::{
CacheEntry, CacheKey, CachedResource, HttpCacheAssignment, MemoryCacheLifecycle,
};
#[derive(MallocSizeOf)]
pub(crate) struct DiskCacheMetadata {
key: CacheKey,
size: usize,
}
impl From<&Row<'_>> for DiskCacheMetadata {
fn from(row: &Row) -> Self {
let s: String = row.get_unwrap("key");
Self {
key: CacheKey::from_url(ServoUrl::parse(&s).unwrap()),
size: row.get_unwrap("size"),
}
}
}
#[derive(MallocSizeOf)]
struct DiskCacheInner {
entries: VecDeque<DiskCacheMetadata>,
size: usize,
#[ignore_malloc_size_of = "Find a better way"]
db: rusqlite::Connection,
cache_assignment: HttpCacheAssignment,
}
#[derive(MallocSizeOf)]
pub(crate) struct DiskCache {
path: PathBuf,
max_size: usize,
inner: TokioMutex<DiskCacheInner>,
}
enum DiskCacheTable {
Table,
Key,
Data,
Size,
InsertionTimestamp,
}
impl Iden for DiskCacheTable {
fn unquoted(&self) -> &str {
match self {
DiskCacheTable::Table => "disk_cache",
DiskCacheTable::Key => "key",
DiskCacheTable::Data => "data",
DiskCacheTable::Size => "size",
DiskCacheTable::InsertionTimestamp => "insertion_timestamp",
}
}
}
fn storage_dir() -> Option<PathBuf> {
let disk_storage_path = pref!(network_http_disk_cache);
match (
servo_config::opts::get().temporary_storage,
disk_storage_path.is_empty(),
) {
(false, false) => Some(disk_storage_path.into()),
(true, true) => {
let tmp_dir = tempfile::tempdir().unwrap();
let mut path = tmp_dir.path().to_path_buf();
path.set_file_name("cache.sqlite3");
Some(path)
},
(true, false) => {
error!(
"Temporary storage cannot be set with explicit disk storage path. Disabling http_disk_cache"
);
None
},
(false, true) => None,
}
}
impl DiskCache {
pub(crate) fn new(
cache_assignment: HttpCacheAssignment,
) -> (Option<Arc<DiskCache>>, MemoryCacheLifecycle) {
let disk_cache_path = storage_dir();
if let Some(disk_cache_path) = disk_cache_path &&
cache_assignment == HttpCacheAssignment::Public
{
let Ok(max_disk_cache_size) = pref!(network_http_disk_cache_size).try_into() else {
return (None, MemoryCacheLifecycle::empty());
};
let Ok(db) = rusqlite::Connection::open(&disk_cache_path) else {
error!("Could not open disk cache database");
return (None, MemoryCacheLifecycle::empty());
};
let _ = db.execute("PRAGMA journal_mode = WAL;", ());
let query = Table::create()
.table(DiskCacheTable::Table)
.if_not_exists()
.col(
ColumnDef::new(DiskCacheTable::Key)
.text()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(DiskCacheTable::Data).blob().not_null())
.col(ColumnDef::new(DiskCacheTable::Size).integer().not_null())
.col(
ColumnDef::new(DiskCacheTable::InsertionTimestamp)
.integer()
.not_null(),
)
.build(SqliteQueryBuilder);
if let Err(e) = db.execute(query.as_str(), ()) {
error!("Could not create table. DB Error {:?}", e);
return (None, MemoryCacheLifecycle::empty());
}
let (query, values) = Query::select()
.columns([DiskCacheTable::Key, DiskCacheTable::Size])
.from(DiskCacheTable::Table)
.build_rusqlite(SqliteQueryBuilder);
let (entries, size) = {
let Ok(mut st) = db.prepare(query.as_str()) else {
error!("Could not get disk data");
return (None, MemoryCacheLifecycle::empty());
};
let entries = st
.query_map(&*values.as_params(), |row| Ok(DiskCacheMetadata::from(row)))
.unwrap()
.map(|entry| entry.unwrap())
.collect::<VecDeque<_>>();
let size = entries.iter().map(|entry| entry.size).sum();
(entries, size)
};
let inner = DiskCacheInner {
entries,
size,
db,
cache_assignment,
};
let disk_cache_data = std::sync::Arc::new(DiskCache {
inner: TokioMutex::new(inner),
path: disk_cache_path,
max_size: max_disk_cache_size,
});
(
Some(disk_cache_data.clone()),
MemoryCacheLifecycle {
disk_cache: Some(disk_cache_data),
},
)
} else {
(None, MemoryCacheLifecycle::empty())
}
}
#[servo_tracing::instrument(skip(self))]
pub(crate) async fn get(&self, key: CacheKey) -> Option<Arc<TokioRwLock<Vec<CachedResource>>>> {
let bytes = {
let mut inner = self.inner.lock().await;
let (bytes, new_size) = {
let _span = profile_traits::trace_span!("query disk cache").entered();
let (query, query_values) = Query::select()
.columns([DiskCacheTable::Data])
.from(DiskCacheTable::Table)
.and_where(Expr::col(DiskCacheTable::Key).eq(key.as_ref()))
.build_rusqlite(SqliteQueryBuilder);
let (delete, delete_values) = Query::delete()
.from_table(DiskCacheTable::Table)
.and_where(Expr::col(DiskCacheTable::Key).eq(key.as_ref()))
.build_rusqlite(SqliteQueryBuilder);
let mut st = inner.db.prepare(query.as_str()).ok()?;
let data: Vec<u8> = st
.query_one(&*query_values.as_params(), |row| Ok(row.get_unwrap("data")))
.ok()?;
if inner
.db
.execute(delete.as_str(), &*delete_values.as_params())
.is_err()
{
error!("Could not delete cached data from disk");
return None;
}
(data, self.get_disk_cache_total_size(&inner.db))
};
{
let entry_index = inner
.entries
.iter()
.position(|metadata| metadata.key == key);
if let Some(entry_index) = entry_index {
inner.entries.remove(entry_index);
}
if let Some(new_size) = new_size {
inner.size = new_size;
} else {
error!("Could not get disk cache size");
}
}
bytes
};
let _span = profile_traits::trace_span!("deserialize cache request").entered();
let Ok(value) = postcard::from_bytes(&bytes) else {
error!("Could not deserialize cached resource");
return None;
};
let deserialized_vec_cached_response = std::sync::Arc::new(TokioRwLock::new(value));
Some(deserialized_vec_cached_response)
}
#[servo_tracing::instrument(skip(self))]
pub(crate) async fn store(&self, key: CacheKey, entry: CacheEntry) {
let entry = entry.read().await;
let data_to_serialize: Vec<&CachedResource> = entry
.iter()
.filter(|cached_resource| cached_resource.is_done())
.collect();
let Ok(data) = postcard::to_stdvec(&*data_to_serialize) else {
error!("Could not deserialize value");
return;
};
{
let mut inner = self.inner.lock().await;
let data_size = data.len();
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
let (query, params) = Query::insert()
.into_table(DiskCacheTable::Table)
.columns([
DiskCacheTable::Key,
DiskCacheTable::Data,
DiskCacheTable::Size,
DiskCacheTable::InsertionTimestamp,
])
.on_conflict(
OnConflict::column(DiskCacheTable::Key)
.update_columns([
DiskCacheTable::Data,
DiskCacheTable::Data,
DiskCacheTable::Size,
DiskCacheTable::InsertionTimestamp,
])
.to_owned(),
)
.values_panic([
key.as_ref().into(),
data.into(),
(data_size as u32).into(),
timestamp.into(),
])
.build_rusqlite(SqliteQueryBuilder);
if let Err(e) = inner.db.execute(query.as_str(), &*params.as_params()) {
error!("Could not insert cache data. Error {}", e);
}
inner.entries.push_back(DiskCacheMetadata {
key,
size: data_size,
});
if let Some(new_cache_size) = self.get_disk_cache_total_size(&inner.db) {
inner.size = new_cache_size;
}
}
self.delete_until_cache_size().await;
}
#[servo_tracing::instrument(skip(self))]
async fn delete_until_cache_size(&self) {
let mut inner = self.inner.lock().await;
let mut keys_to_delete = vec![];
while self.max_size < inner.size {
if let Some(metadata) = inner.entries.pop_back() {
keys_to_delete.push(metadata.key);
inner.size -= metadata.size;
}
}
let keys_ref = keys_to_delete.iter().map(|key| key.as_ref());
let (query, values) = Query::delete()
.from_table(DiskCacheTable::Table)
.and_where(Expr::col(DiskCacheTable::Key).is_in(keys_ref))
.build_rusqlite(SqliteQueryBuilder);
if inner
.db
.execute(query.as_str(), &*values.as_params())
.is_err()
{
error!("Could not delete old disk cache entries");
}
}
#[servo_tracing::instrument(skip(self))]
fn get_disk_cache_total_size(&self, conn: &rusqlite::Connection) -> Option<usize> {
let (size, size_values) = Query::select()
.expr(Expr::col(DiskCacheTable::Size).sum())
.from(DiskCacheTable::Table)
.build_rusqlite(SqliteQueryBuilder);
let Ok(mut st) = conn.prepare(size.as_str()) else {
return None;
};
let query_result =
st.query_one(&*size_values.as_params(), |row| Ok(row.get(0).unwrap_or(0)));
if let Err(query_result) = query_result {
error!("Could nto get new sum size {}", query_result);
None
} else {
query_result.ok()
}
}
#[servo_tracing::instrument(skip(self))]
pub(crate) fn clear(&self) {
let mut inner = self.inner.blocking_lock();
let (query, params) = Query::delete()
.from_table(DiskCacheTable::Table)
.build_rusqlite(SqliteQueryBuilder);
if inner
.db
.execute(query.as_str(), &*params.as_params())
.is_err()
{
error!("Could not clear disk cache");
}
inner.entries.clear();
inner.size = 0;
}
}