use crate::tile_cache::file_util::{FileUtil, LRU_TABLE_FILE, round_to_final_consumption};
use crate::tile_cache::lru_list::LastRecentlyUsedList;
use crate::tile_cache::tile_name_conversion::TileSpecification;
use crate::tile_cache::web_requester::{Requester, RequesterError};
use bytes::Bytes;
use std::fmt::Debug;
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Arc, Mutex};
use tokio::sync::RwLock;
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::task::JoinHandle;
const MAXIMUM_MESSAGE_CHANNEL: usize = 100;
const AMOUNT_OF_SECONDS_TILL_SAVE: u8 = 5;
struct ShareableEntries {
file_util: FileUtil,
lru_list: Mutex<LastRecentlyUsedList>,
requester: Requester,
initialization_completed: AtomicBool,
disc_writing: RwLock<bool>,
}
#[derive(Clone, PartialEq, Eq)]
pub enum CachingResultMessage {
Error { message: String },
InitializationCompleted,
CacheCleared,
TileData {
level: u8,
x: u32,
y: u32,
data: Bytes,
decoded: Option<DecodedTile>,
},
TileFailed {
level: u8,
x: u32,
y: u32,
message: String,
},
}
#[derive(Clone, PartialEq, Eq)]
pub struct DecodedTile {
pub width: u32,
pub height: u32,
pub pixels: Bytes,
}
impl DecodedTile {
fn decode(data: &[u8]) -> Option<Self> {
let image = image::load_from_memory(data).ok()?.into_rgba8();
Some(Self {
width: image.width(),
height: image.height(),
pixels: Bytes::from(image.into_raw()),
})
}
}
impl Debug for CachingResultMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CachingResultMessage::Error { message } => write!(f, "Caching error: {}", message),
CachingResultMessage::InitializationCompleted => write!(f, "Initialization completed"),
CachingResultMessage::CacheCleared => write!(f, "Cache cleared"),
CachingResultMessage::TileData {
level, x, y, data, ..
} => {
write!(
f,
"Tile data level {}, x {}, y {}, size {}",
level,
x,
y,
data.len()
)
}
CachingResultMessage::TileFailed {
level,
x,
y,
message,
} => {
write!(
f,
"Tile failed level {}, x {}, y {}, message {}",
level, x, y, message
)
}
}
}
}
pub struct CachingSystem {
cloneable_entry: Arc<ShareableEntries>,
stream_reader: Option<Receiver<CachingResultMessage>>,
stream_sender: Sender<CachingResultMessage>,
savings_counter: Arc<AtomicU8>,
timer_handle: JoinHandle<()>,
copyright_text: String,
}
impl CachingSystem {
pub fn new(
requester: Requester,
cache_base_dir: impl AsRef<Path>,
maximum_amount_of_data: u64,
copyright_text: String,
) -> CachingSystem {
let (tx, rx) = mpsc::channel::<CachingResultMessage>(MAXIMUM_MESSAGE_CHANNEL);
let sharable_entry = ShareableEntries {
file_util: FileUtil::new(cache_base_dir),
lru_list: Mutex::new(LastRecentlyUsedList::new(maximum_amount_of_data)),
requester,
initialization_completed: AtomicBool::new(false),
disc_writing: RwLock::new(true),
};
let arc_sharable_entry = Arc::new(sharable_entry);
let savings_counter = Arc::new(AtomicU8::new(0));
let timer_handle = tokio::spawn(Self::savings_timer(
savings_counter.clone(),
arc_sharable_entry.clone(),
tx.clone(),
));
Self {
cloneable_entry: arc_sharable_entry,
stream_reader: Some(rx),
stream_sender: tx,
savings_counter,
timer_handle,
copyright_text,
}
}
pub fn get_copyright_text(&self) -> String {
self.copyright_text.clone()
}
async fn savings_timer(
counter: Arc<AtomicU8>,
shareable_entries: Arc<ShareableEntries>,
sender: Sender<CachingResultMessage>,
) {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let previous = counter.try_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
Some(current.saturating_sub(1))
});
if previous == Ok(1) {
Self::save_lru_table(&shareable_entries, &sender).await;
}
}
}
pub fn get_receiver(&mut self) -> Option<Receiver<CachingResultMessage>> {
self.stream_reader.take()
}
async fn process_initialize(
sharable_entry: Arc<ShareableEntries>,
sender: Sender<CachingResultMessage>,
savings_counter: Arc<AtomicU8>,
) {
sharable_entry.file_util.remove_temps_from_base().await;
let data = sharable_entry
.file_util
.get_all_pngs_interpreted_as_u64()
.await;
let lru_data = FileUtil::convert_to_u64(
&sharable_entry
.file_util
.try_load_plain(LRU_TABLE_FILE)
.await
.unwrap_or_default(),
);
let deletion_list = sharable_entry
.lru_list
.lock()
.unwrap_or_else(|e| e.into_inner())
.reconstruct_from(&lru_data, &data);
sharable_entry.file_util.remove_files(&deletion_list).await;
sharable_entry
.initialization_completed
.store(true, Ordering::Relaxed);
savings_counter.store(AMOUNT_OF_SECONDS_TILL_SAVE, Ordering::SeqCst);
let _ = sender
.send(CachingResultMessage::InitializationCompleted)
.await;
}
pub fn initialize(&self) {
debug_assert!(
!self
.cloneable_entry
.initialization_completed
.load(Ordering::Relaxed),
"CachingSystem::initialize called twice"
);
tokio::spawn(Self::process_initialize(
self.cloneable_entry.clone(),
self.stream_sender.clone(),
self.savings_counter.clone(),
));
}
pub fn clear_cache(&self, persist_afterwards: bool) {
debug_assert!(
self.cloneable_entry
.initialization_completed
.load(Ordering::Relaxed),
"CachingSystem::clear_cache before initialization"
);
tokio::spawn(Self::process_clear_cache(
self.cloneable_entry.clone(),
self.stream_sender.clone(),
persist_afterwards,
));
}
async fn process_clear_cache(
sharable_entry: Arc<ShareableEntries>,
sender: Sender<CachingResultMessage>,
persist_afterwards: bool,
) {
let result = {
let mut disc_writing = sharable_entry.disc_writing.write().await;
*disc_writing &= persist_afterwards;
sharable_entry
.lru_list
.lock()
.unwrap_or_else(|e| e.into_inner())
.reset();
sharable_entry.file_util.remove_cache_files().await
};
if let Err(text) = result {
let _ = sender
.send(CachingResultMessage::Error {
message: "Error on clearing the cache: ".to_string() + text.as_str(),
})
.await;
}
let _ = sender.send(CachingResultMessage::CacheCleared).await;
}
pub fn request_tile(&self, level: u8, x: u32, y: u32) -> Result<(), String> {
if !self
.cloneable_entry
.initialization_completed
.load(Ordering::Relaxed)
{
return Err(String::from(
"CachingSystem::request_tile not initialized yet",
));
}
let shareable_entry = self.cloneable_entry.clone();
let sender = self.stream_sender.clone();
tokio::spawn(Self::process_request_tile(
level,
x,
y,
shareable_entry,
sender,
self.savings_counter.clone(),
));
Ok(())
}
async fn process_request_tile(
level: u8,
x: u32,
y: u32,
sharable_entry: Arc<ShareableEntries>,
sender: Sender<CachingResultMessage>,
savings_counter: Arc<AtomicU8>,
) {
let destination = TileSpecification::new(level, x, y);
if let Some(image_data) = sharable_entry
.file_util
.try_load_plain(destination.filename())
.await
{
let _ = sender
.send(CachingResultMessage::TileData {
level,
x,
y,
decoded: DecodedTile::decode(&image_data),
data: Bytes::from(image_data),
})
.await;
sharable_entry
.lru_list
.lock()
.unwrap_or_else(|e| e.into_inner())
.touch(destination.into());
} else {
Self::deal_with_cache_miss(&sharable_entry, sender, destination).await;
}
savings_counter.store(AMOUNT_OF_SECONDS_TILL_SAVE, Ordering::SeqCst);
}
async fn deal_with_cache_miss(
sharable_entry: &Arc<ShareableEntries>,
sender: Sender<CachingResultMessage>,
destination: TileSpecification,
) {
let web_access = sharable_entry.requester.get_image_data(destination).await;
let raw_data: Bytes = match web_access {
Ok(data) if data.is_empty() => {
let _ = sender
.send(CachingResultMessage::TileFailed {
x: destination.x(),
y: destination.y(),
level: destination.level(),
message: "Empty data received from server".to_string(),
})
.await;
return;
}
Ok(data) => data,
Err(text) => {
let _ = sender
.send(CachingResultMessage::TileFailed {
x: destination.x(),
y: destination.y(),
level: destination.level(),
message: text,
})
.await;
return;
}
};
let _ = sender
.send(CachingResultMessage::TileData {
level: destination.level(),
x: destination.x(),
y: destination.y(),
decoded: DecodedTile::decode(&raw_data),
data: raw_data.clone(),
})
.await;
let disc_writing = sharable_entry.disc_writing.read().await;
if !*disc_writing {
return;
}
let new_memory = round_to_final_consumption(raw_data.len() as u64);
if let Err(text) = sharable_entry
.file_util
.safe_save(destination.filename(), &raw_data)
.await
{
let _ = sender
.send(CachingResultMessage::Error { message: text })
.await;
return;
}
let to_remove = sharable_entry
.lru_list
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert_and_clear(destination.into(), new_memory);
sharable_entry.file_util.remove_files(&to_remove).await;
}
async fn save_lru_table(
sharable_entry: &Arc<ShareableEntries>,
sender: &Sender<CachingResultMessage>,
) {
let disc_writing = sharable_entry.disc_writing.read().await;
if !*disc_writing {
return;
}
let raw_data = sharable_entry
.lru_list
.lock()
.unwrap_or_else(|e| e.into_inner())
.generate_usage_list();
let res = sharable_entry
.file_util
.safe_save(LRU_TABLE_FILE, &FileUtil::convert_to_u8(&raw_data))
.await;
match res {
Ok(_) => {} Err(x) => {
let _ = sender
.send(CachingResultMessage::Error {
message: "Error on writing cache file: ".to_string() + x.as_str(),
})
.await;
}
}
}
}
impl Drop for CachingSystem {
fn drop(&mut self) {
self.timer_handle.abort();
}
}
pub fn generate_dummy_cache(
cache_base_dir: impl AsRef<Path>,
maximum_amount_of_data: u64,
) -> CachingSystem {
CachingSystem::new(
Requester::dummy(),
cache_base_dir,
maximum_amount_of_data,
"".to_string(),
)
}
pub fn generate_cache(
intro_url: &str,
post_url: &str,
user_agent: &str,
copyright_text: String,
cache_base_dir: impl AsRef<Path>,
maximum_amount_of_data: u64,
) -> Result<CachingSystem, RequesterError> {
Ok(CachingSystem::new(
Requester::new(intro_url, post_url, user_agent)?,
cache_base_dir,
maximum_amount_of_data,
copyright_text,
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
use std::time::Duration;
#[tokio::test]
async fn first_setup() {
let mut cache = generate_dummy_cache(tempfile::tempdir().unwrap(), 10_000);
cache.initialize();
let mut receiver = cache.get_receiver().unwrap();
let message = receiver.recv().await.unwrap();
assert_eq!(message, CachingResultMessage::InitializationCompleted);
}
#[tokio::test]
async fn first_fill() {
let mut cache = generate_dummy_cache(tempfile::tempdir().unwrap(), 20_000);
cache.initialize();
let mut receiver = cache.get_receiver().unwrap();
let message = receiver.recv().await.unwrap();
assert_eq!(message, CachingResultMessage::InitializationCompleted);
for y in 0..100 {
cache
.request_tile(0, 1, y)
.expect("Initialization uncompleted.");
}
for _ in 0..100 {
let message = receiver.recv().await.unwrap();
assert_matches!(
message,
CachingResultMessage::TileData { level: 0, x: 1, .. }
);
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
async fn fetch_tiles(
cache: &CachingSystem,
receiver: &mut Receiver<CachingResultMessage>,
count: u32,
) {
for y in 0..count {
cache.request_tile(2, 1, y).expect("Initialized");
}
for _ in 0..count {
let message = receiver.recv().await.unwrap();
assert_matches!(message, CachingResultMessage::TileData { level: 2, .. });
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
#[tokio::test]
async fn clear_cache_test() {
for persist_afterwards in [true, false] {
let dir = tempfile::tempdir().unwrap();
let util = FileUtil::new(&dir);
let mut cache = generate_dummy_cache(&dir, 100_000);
cache.initialize();
let mut receiver = cache.get_receiver().unwrap();
let message = receiver.recv().await.unwrap();
assert_eq!(message, CachingResultMessage::InitializationCompleted);
fetch_tiles(&cache, &mut receiver, 4).await;
assert_eq!(util.get_all_pngs_interpreted_as_u64().await.len(), 4);
cache.clear_cache(persist_afterwards);
let message = receiver.recv().await.unwrap();
assert_eq!(message, CachingResultMessage::CacheCleared);
assert!(util.get_all_pngs_interpreted_as_u64().await.is_empty());
fetch_tiles(&cache, &mut receiver, 3).await;
let expected = if persist_afterwards { 3 } else { 0 };
assert_eq!(util.get_all_pngs_interpreted_as_u64().await.len(), expected);
}
}
}