use chrono::{NaiveDateTime, Utc};
use dashmap::DashMap;
use dragonfly_client_config::dfdaemon::Config;
use dragonfly_client_core::{Error, Result};
use dragonfly_client_util::{digest, http::headermap_to_hashmap};
use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tracing::{error, info, instrument};
use crate::storage_engine::{rocksdb::RocksdbStorageEngine, DatabaseObject, StorageEngineOwned};
pub const DEFAULT_DOWNLOAD_TASK_TIMEOUT: Duration = Duration::from_secs(24 * 60 * 60);
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub piece_length: Option<u64>,
pub content_length: Option<u64>,
pub response_header: HashMap<String, String>,
pub uploading_count: i64,
pub uploaded_count: u64,
pub updated_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub prefetched_at: Option<NaiveDateTime>,
pub failed_at: Option<NaiveDateTime>,
pub finished_at: Option<NaiveDateTime>,
}
impl DatabaseObject for Task {
const NAMESPACE: &'static str = "task";
}
impl Task {
pub fn is_started(&self) -> bool {
self.finished_at.is_none()
}
pub fn is_uploading(&self) -> bool {
self.uploading_count > 0
}
pub fn is_expired(&self, ttl: Duration) -> bool {
self.updated_at + ttl < Utc::now().naive_utc()
}
pub fn need_drop_page_cache(&self, idle_timeout: Duration) -> bool {
self.is_finished()
&& !self.is_uploading()
&& self.updated_at + idle_timeout < Utc::now().naive_utc()
}
pub fn need_evict(&self) -> bool {
self.is_finished()
|| self.is_failed()
|| self.created_at + DEFAULT_DOWNLOAD_TASK_TIMEOUT < Utc::now().naive_utc()
}
pub fn is_prefetched(&self) -> bool {
self.prefetched_at.is_some()
}
pub fn is_failed(&self) -> bool {
self.failed_at.is_some()
}
pub fn is_finished(&self) -> bool {
self.finished_at.is_some()
}
pub fn is_empty(&self) -> bool {
match self.content_length() {
Some(content_length) => content_length == 0,
None => false,
}
}
pub fn piece_length(&self) -> Option<u64> {
self.piece_length
}
pub fn content_length(&self) -> Option<u64> {
self.content_length
}
pub fn piece_count(&self) -> Option<u64> {
self.content_length()
.zip(self.piece_length())
.map(|(content_length, piece_length)| content_length.div_ceil(piece_length))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PersistentTask {
pub id: String,
pub persistent: bool,
pub ttl: Duration,
pub piece_length: u64,
pub content_length: u64,
pub uploading_count: i64,
pub uploaded_count: u64,
pub updated_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub failed_at: Option<NaiveDateTime>,
pub finished_at: Option<NaiveDateTime>,
}
impl DatabaseObject for PersistentTask {
const NAMESPACE: &'static str = "persistent_task";
}
impl PersistentTask {
pub fn is_started(&self) -> bool {
self.finished_at.is_none()
}
pub fn is_uploading(&self) -> bool {
self.uploading_count > 0
}
pub fn is_expired(&self) -> bool {
self.created_at + self.ttl < Utc::now().naive_utc()
}
pub fn need_drop_page_cache(&self, idle_timeout: Duration) -> bool {
self.is_finished()
&& !self.is_uploading()
&& self.updated_at + idle_timeout < Utc::now().naive_utc()
}
pub fn need_evict(&self) -> bool {
!self.is_persistent()
&& (self.is_finished()
|| self.is_failed()
|| self.created_at + DEFAULT_DOWNLOAD_TASK_TIMEOUT < Utc::now().naive_utc())
}
pub fn is_failed(&self) -> bool {
self.failed_at.is_some()
}
pub fn is_finished(&self) -> bool {
self.finished_at.is_some()
}
pub fn is_empty(&self) -> bool {
self.content_length == 0
}
pub fn is_persistent(&self) -> bool {
self.persistent
}
pub fn piece_length(&self) -> u64 {
self.piece_length
}
pub fn content_length(&self) -> u64 {
self.content_length
}
pub fn piece_count(&self) -> u64 {
self.content_length.div_ceil(self.piece_length)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PersistentCacheTask {
pub id: String,
pub persistent: bool,
pub ttl: Duration,
pub piece_length: u64,
pub content_length: u64,
pub uploading_count: i64,
pub uploaded_count: u64,
pub updated_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub failed_at: Option<NaiveDateTime>,
pub finished_at: Option<NaiveDateTime>,
}
impl DatabaseObject for PersistentCacheTask {
const NAMESPACE: &'static str = "persistent_cache_task";
}
impl PersistentCacheTask {
pub fn is_started(&self) -> bool {
self.finished_at.is_none()
}
pub fn is_uploading(&self) -> bool {
self.uploading_count > 0
}
pub fn is_expired(&self) -> bool {
self.created_at + self.ttl < Utc::now().naive_utc()
}
pub fn need_drop_page_cache(&self, idle_timeout: Duration) -> bool {
self.is_finished()
&& !self.is_uploading()
&& self.updated_at + idle_timeout < Utc::now().naive_utc()
}
pub fn need_evict(&self) -> bool {
!self.is_persistent()
&& (self.is_finished()
|| self.is_failed()
|| self.created_at + DEFAULT_DOWNLOAD_TASK_TIMEOUT < Utc::now().naive_utc())
}
pub fn is_failed(&self) -> bool {
self.failed_at.is_some()
}
pub fn is_finished(&self) -> bool {
self.finished_at.is_some()
}
pub fn is_empty(&self) -> bool {
self.content_length == 0
}
pub fn is_persistent(&self) -> bool {
self.persistent
}
pub fn piece_length(&self) -> u64 {
self.piece_length
}
pub fn content_length(&self) -> u64 {
self.content_length
}
pub fn piece_count(&self) -> u64 {
self.content_length.div_ceil(self.piece_length)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheTask {
pub id: String,
pub piece_length: Option<u64>,
pub content_length: Option<u64>,
pub response_header: HashMap<String, String>,
pub uploading_count: i64,
pub uploaded_count: u64,
pub updated_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub failed_at: Option<NaiveDateTime>,
pub finished_at: Option<NaiveDateTime>,
}
impl DatabaseObject for CacheTask {
const NAMESPACE: &'static str = "cache_task";
}
impl CacheTask {
pub fn is_started(&self) -> bool {
self.finished_at.is_none()
}
pub fn is_uploading(&self) -> bool {
self.uploading_count > 0
}
pub fn is_expired(&self, ttl: Duration) -> bool {
self.updated_at + ttl < Utc::now().naive_utc()
}
pub fn is_failed(&self) -> bool {
self.failed_at.is_some()
}
pub fn is_finished(&self) -> bool {
self.finished_at.is_some()
}
pub fn is_empty(&self) -> bool {
match self.content_length() {
Some(content_length) => content_length == 0,
None => false,
}
}
pub fn piece_length(&self) -> Option<u64> {
self.piece_length
}
pub fn content_length(&self) -> Option<u64> {
self.content_length
}
pub fn piece_count(&self) -> Option<u64> {
self.content_length()
.zip(self.piece_length())
.map(|(content_length, piece_length)| content_length.div_ceil(piece_length))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Piece {
pub number: u32,
pub offset: u64,
pub length: u64,
pub digest: String,
pub parent_id: Option<String>,
pub uploading_count: u64,
pub uploaded_count: u64,
pub updated_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub finished_at: Option<NaiveDateTime>,
}
impl DatabaseObject for Piece {
const NAMESPACE: &'static str = "piece";
}
impl Piece {
pub fn is_started(&self) -> bool {
self.finished_at.is_none()
}
pub fn is_finished(&self) -> bool {
self.finished_at.is_some()
}
pub fn cost(&self) -> Option<Duration> {
match self
.finished_at
.map(|finished_at| finished_at - self.created_at)
{
Some(cost) => match cost.to_std() {
Ok(cost) => Some(cost),
Err(err) => {
error!("convert cost error: {:?}", err);
None
}
},
None => None,
}
}
pub fn prost_cost(&self) -> Option<prost_wkt_types::Duration> {
match self.cost() {
Some(cost) => match prost_wkt_types::Duration::try_from(cost) {
Ok(cost) => Some(cost),
Err(err) => {
error!("convert cost error: {:?}", err);
None
}
},
None => None,
}
}
pub fn calculate_digest(&self) -> String {
let mut hasher = crc32fast::Hasher::new();
hasher.update(&self.number.to_be_bytes());
hasher.update(&self.offset.to_be_bytes());
hasher.update(&self.length.to_be_bytes());
hasher.update(self.digest.as_bytes());
let encoded = hasher.finalize().to_string();
digest::Digest::new(digest::Algorithm::Crc32, encoded).to_string()
}
}
#[derive(Clone, Default)]
struct UploadStats {
pub uploading_count: i64,
pub uploaded_count: u64,
}
pub struct Metadata<E = RocksdbStorageEngine>
where
E: StorageEngineOwned,
{
db: E,
upload_stats: DashMap<String, UploadStats>,
persistent_task_upload_stats: DashMap<String, UploadStats>,
persistent_cache_task_upload_stats: DashMap<String, UploadStats>,
}
impl<E: StorageEngineOwned> Metadata<E> {
#[instrument(level = "debug", skip_all)]
pub fn prepare_download_task(&self, id: &str) -> Result<(Task, bool)> {
let task = match self.db.get::<Task>(id.as_bytes())? {
Some(mut task) => {
if task.content_length().is_some()
&& task.piece_length().is_some()
&& !task.is_failed()
{
return Ok((task, true));
} else {
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
task
}
}
None => {
Task {
id: id.to_string(),
updated_at: Utc::now().naive_utc(),
created_at: Utc::now().naive_utc(),
..Default::default()
}
}
};
self.db.put(id.as_bytes(), &task)?;
Ok((task, false))
}
#[instrument(level = "debug", skip_all)]
pub fn download_task_started(
&self,
id: &str,
piece_length: u64,
content_length: u64,
response_header: Option<HeaderMap>,
) -> Result<Task> {
let response_header = response_header
.as_ref()
.map(headermap_to_hashmap)
.unwrap_or_default();
let task = match self.db.get::<Task>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
task.content_length = Some(content_length);
task.piece_length = Some(piece_length);
task.response_header = response_header;
task
}
None => Task {
id: id.to_string(),
piece_length: Some(piece_length),
content_length: Some(content_length),
response_header,
updated_at: Utc::now().naive_utc(),
created_at: Utc::now().naive_utc(),
..Default::default()
},
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_task_finished(&self, id: &str) -> Result<Task> {
let task = match self.db.get::<Task>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
task.finished_at = Some(Utc::now().naive_utc());
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_task_failed(&self, id: &str) -> Result<Task> {
let task = match self.db.get::<Task>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = Some(Utc::now().naive_utc());
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn prefetch_task_started(&self, id: &str) -> Result<Task> {
let task = match self.db.get::<Task>(id.as_bytes())? {
Some(mut task) => {
if task.is_prefetched() {
return Err(Error::InvalidState("prefetched".to_string()));
}
task.updated_at = Utc::now().naive_utc();
task.prefetched_at = Some(Utc::now().naive_utc());
task.failed_at = None;
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn prefetch_task_failed(&self, id: &str) -> Result<Task> {
let task = match self.db.get::<Task>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.prefetched_at = None;
task.failed_at = Some(Utc::now().naive_utc());
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn upload_task_started(&self, id: &str) {
self.upload_stats
.entry(id.to_string())
.or_default()
.uploading_count += 1;
}
#[instrument(level = "debug", skip_all)]
pub fn upload_task_finished(&self, id: &str) {
let mut stats = self.upload_stats.entry(id.to_string()).or_default();
stats.uploading_count = stats.uploading_count.saturating_sub(1);
stats.uploaded_count += 1;
}
#[instrument(level = "debug", skip_all)]
pub fn upload_task_failed(&self, id: &str) {
let mut stats = self.upload_stats.entry(id.to_string()).or_default();
stats.uploading_count = stats.uploading_count.saturating_sub(1);
}
fn fill_upload_stats(&self, task: &mut Task) {
if let Some(stats) = self.upload_stats.get(&task.id) {
task.uploading_count = stats.uploading_count;
task.uploaded_count += stats.uploaded_count;
}
}
#[instrument(level = "debug", skip_all)]
pub fn get_task(&self, id: &str) -> Result<Option<Task>> {
Ok(self.db.get::<Task>(id.as_bytes())?.map(|mut task| {
self.fill_upload_stats(&mut task);
task
}))
}
#[instrument(level = "debug", skip_all)]
pub fn is_task_exists(&self, id: &str) -> Result<bool> {
self.db.exists::<Task>(id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn get_tasks(&self) -> Result<Vec<Task>> {
let tasks = self
.db
.iter_raw::<Task>()?
.map(|ele| {
let (_, value) = ele?;
Ok(value)
})
.collect::<Result<Vec<Box<[u8]>>>>()?;
tasks
.iter()
.map(|task| {
let mut task = Task::deserialize_from(task)?;
self.fill_upload_stats(&mut task);
Ok(task)
})
.collect()
}
#[instrument(level = "debug", skip_all)]
pub fn delete_task(&self, id: &str) -> Result<()> {
info!("delete task metadata {}", id);
self.upload_stats.remove(id);
self.db.delete::<Task>(id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn create_persistent_task_started(
&self,
id: &str,
ttl: Duration,
piece_length: u64,
content_length: u64,
) -> Result<PersistentTask> {
let task = PersistentTask {
id: id.to_string(),
persistent: true,
ttl,
piece_length,
content_length,
updated_at: Utc::now().naive_utc(),
created_at: Utc::now().naive_utc(),
..Default::default()
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn create_persistent_task_finished(&self, id: &str) -> Result<PersistentTask> {
let task = match self.db.get::<PersistentTask>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
if task.finished_at.is_none() {
task.finished_at = Some(Utc::now().naive_utc());
}
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_persistent_task_started(
&self,
id: &str,
ttl: Duration,
persistent: bool,
piece_length: u64,
content_length: u64,
created_at: NaiveDateTime,
) -> Result<PersistentTask> {
let task = match self.db.get::<PersistentTask>(id.as_bytes())? {
Some(mut task) => {
task.ttl = ttl;
task.persistent = persistent;
task.piece_length = piece_length;
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
task
}
None => PersistentTask {
id: id.to_string(),
persistent,
ttl,
piece_length,
content_length,
updated_at: Utc::now().naive_utc(),
created_at,
..Default::default()
},
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_persistent_task_finished(&self, id: &str) -> Result<PersistentTask> {
let task = match self.db.get::<PersistentTask>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
if task.finished_at.is_none() {
task.finished_at = Some(Utc::now().naive_utc());
}
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_persistent_task_failed(&self, id: &str) -> Result<PersistentTask> {
let task = match self.db.get::<PersistentTask>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = Some(Utc::now().naive_utc());
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn upload_persistent_task_started(&self, id: &str) {
self.persistent_task_upload_stats
.entry(id.to_string())
.or_default()
.uploading_count += 1;
}
#[instrument(level = "debug", skip_all)]
pub fn upload_persistent_task_finished(&self, id: &str) {
let mut stats = self
.persistent_task_upload_stats
.entry(id.to_string())
.or_default();
stats.uploading_count = stats.uploading_count.saturating_sub(1);
stats.uploaded_count += 1;
}
#[instrument(level = "debug", skip_all)]
pub fn upload_persistent_task_failed(&self, id: &str) {
let mut stats = self
.persistent_task_upload_stats
.entry(id.to_string())
.or_default();
stats.uploading_count = stats.uploading_count.saturating_sub(1);
}
fn fill_persistent_task_upload_stats(&self, task: &mut PersistentTask) {
if let Some(stats) = self.persistent_task_upload_stats.get(&task.id) {
task.uploading_count = stats.uploading_count;
task.uploaded_count += stats.uploaded_count;
}
}
#[instrument(level = "debug", skip_all)]
pub fn persist_persistent_task(&self, id: &str) -> Result<PersistentTask> {
let task = match self.db.get::<PersistentTask>(id.as_bytes())? {
Some(mut task) => {
task.persistent = true;
task.updated_at = Utc::now().naive_utc();
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn get_persistent_task(&self, id: &str) -> Result<Option<PersistentTask>> {
Ok(self
.db
.get::<PersistentTask>(id.as_bytes())?
.map(|mut task| {
self.fill_persistent_task_upload_stats(&mut task);
task
}))
}
#[instrument(level = "debug", skip_all)]
pub fn is_persistent_task_exists(&self, id: &str) -> Result<bool> {
self.db.exists::<PersistentTask>(id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn get_persistent_tasks(&self) -> Result<Vec<PersistentTask>> {
let iter = self.db.iter::<PersistentTask>()?;
iter.map(|ele| {
ele.map(|(_, mut task)| {
self.fill_persistent_task_upload_stats(&mut task);
task
})
})
.collect()
}
#[instrument(level = "debug", skip_all)]
pub fn delete_persistent_task(&self, id: &str) -> Result<()> {
info!("delete persistent task metadata {}", id);
self.persistent_task_upload_stats.remove(id);
self.db.delete::<PersistentTask>(id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn create_persistent_cache_task_started(
&self,
id: &str,
ttl: Duration,
piece_length: u64,
content_length: u64,
) -> Result<PersistentCacheTask> {
let task = PersistentCacheTask {
id: id.to_string(),
persistent: true,
ttl,
piece_length,
content_length,
updated_at: Utc::now().naive_utc(),
created_at: Utc::now().naive_utc(),
..Default::default()
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn create_persistent_cache_task_finished(&self, id: &str) -> Result<PersistentCacheTask> {
let task = match self.db.get::<PersistentCacheTask>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
if task.finished_at.is_none() {
task.finished_at = Some(Utc::now().naive_utc());
}
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_persistent_cache_task_started(
&self,
id: &str,
ttl: Duration,
persistent: bool,
piece_length: u64,
content_length: u64,
created_at: NaiveDateTime,
) -> Result<PersistentCacheTask> {
let task = match self.db.get::<PersistentCacheTask>(id.as_bytes())? {
Some(mut task) => {
task.ttl = ttl;
task.persistent = persistent;
task.piece_length = piece_length;
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
task
}
None => PersistentCacheTask {
id: id.to_string(),
persistent,
ttl,
piece_length,
content_length,
updated_at: Utc::now().naive_utc(),
created_at,
..Default::default()
},
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_persistent_cache_task_finished(&self, id: &str) -> Result<PersistentCacheTask> {
let task = match self.db.get::<PersistentCacheTask>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
if task.finished_at.is_none() {
task.finished_at = Some(Utc::now().naive_utc());
}
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_persistent_cache_task_failed(&self, id: &str) -> Result<PersistentCacheTask> {
let task = match self.db.get::<PersistentCacheTask>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = Some(Utc::now().naive_utc());
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn upload_persistent_cache_task_started(&self, id: &str) {
self.persistent_cache_task_upload_stats
.entry(id.to_string())
.or_default()
.uploading_count += 1;
}
#[instrument(level = "debug", skip_all)]
pub fn upload_persistent_cache_task_finished(&self, id: &str) {
let mut stats = self
.persistent_cache_task_upload_stats
.entry(id.to_string())
.or_default();
stats.uploading_count = stats.uploading_count.saturating_sub(1);
stats.uploaded_count += 1;
}
#[instrument(level = "debug", skip_all)]
pub fn upload_persistent_cache_task_failed(&self, id: &str) {
let mut stats = self
.persistent_cache_task_upload_stats
.entry(id.to_string())
.or_default();
stats.uploading_count = stats.uploading_count.saturating_sub(1);
}
fn fill_persistent_cache_task_upload_stats(&self, task: &mut PersistentCacheTask) {
if let Some(stats) = self.persistent_cache_task_upload_stats.get(&task.id) {
task.uploading_count = stats.uploading_count;
task.uploaded_count += stats.uploaded_count;
}
}
#[instrument(level = "debug", skip_all)]
pub fn persist_persistent_cache_task(&self, id: &str) -> Result<PersistentCacheTask> {
let task = match self.db.get::<PersistentCacheTask>(id.as_bytes())? {
Some(mut task) => {
task.persistent = true;
task.updated_at = Utc::now().naive_utc();
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn get_persistent_cache_task(&self, id: &str) -> Result<Option<PersistentCacheTask>> {
Ok(self
.db
.get::<PersistentCacheTask>(id.as_bytes())?
.map(|mut task| {
self.fill_persistent_cache_task_upload_stats(&mut task);
task
}))
}
#[instrument(level = "debug", skip_all)]
pub fn is_persistent_cache_task_exists(&self, id: &str) -> Result<bool> {
self.db.exists::<PersistentCacheTask>(id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn get_persistent_cache_tasks(&self) -> Result<Vec<PersistentCacheTask>> {
let iter = self.db.iter::<PersistentCacheTask>()?;
iter.map(|ele| {
ele.map(|(_, mut task)| {
self.fill_persistent_cache_task_upload_stats(&mut task);
task
})
})
.collect()
}
#[instrument(level = "debug", skip_all)]
pub fn delete_persistent_cache_task(&self, id: &str) -> Result<()> {
info!("delete persistent cache task metadata {}", id);
self.persistent_cache_task_upload_stats.remove(id);
self.db.delete::<PersistentCacheTask>(id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn download_cache_task_started(
&self,
id: &str,
piece_length: u64,
content_length: u64,
response_header: Option<HeaderMap>,
) -> Result<CacheTask> {
let response_header = response_header
.as_ref()
.map(headermap_to_hashmap)
.unwrap_or_default();
let task = match self.db.get::<CacheTask>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
task.content_length = Some(content_length);
task.piece_length = Some(piece_length);
task.response_header = response_header;
task
}
None => CacheTask {
id: id.to_string(),
piece_length: Some(piece_length),
content_length: Some(content_length),
response_header,
updated_at: Utc::now().naive_utc(),
created_at: Utc::now().naive_utc(),
..Default::default()
},
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_cache_task_finished(&self, id: &str) -> Result<CacheTask> {
let task = match self.db.get::<CacheTask>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = None;
task.finished_at = Some(Utc::now().naive_utc());
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn download_cache_task_failed(&self, id: &str) -> Result<CacheTask> {
let task = match self.db.get::<CacheTask>(id.as_bytes())? {
Some(mut task) => {
task.updated_at = Utc::now().naive_utc();
task.failed_at = Some(Utc::now().naive_utc());
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn upload_cache_task_started(&self, id: &str) -> Result<CacheTask> {
let task = match self.db.get::<CacheTask>(id.as_bytes())? {
Some(mut task) => {
task.uploading_count += 1;
task.updated_at = Utc::now().naive_utc();
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn upload_cache_task_finished(&self, id: &str) -> Result<CacheTask> {
let task = match self.db.get::<CacheTask>(id.as_bytes())? {
Some(mut task) => {
task.uploading_count -= 1;
task.uploaded_count += 1;
task.updated_at = Utc::now().naive_utc();
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn upload_cache_task_failed(&self, id: &str) -> Result<CacheTask> {
let task = match self.db.get::<CacheTask>(id.as_bytes())? {
Some(mut task) => {
task.uploading_count -= 1;
task.updated_at = Utc::now().naive_utc();
task
}
None => return Err(Error::TaskNotFound(id.to_string())),
};
self.db.put(id.as_bytes(), &task)?;
Ok(task)
}
#[instrument(level = "debug", skip_all)]
pub fn get_cache_task(&self, id: &str) -> Result<Option<CacheTask>> {
self.db.get(id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn is_cache_task_exists(&self, id: &str) -> Result<bool> {
self.db.exists::<CacheTask>(id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn get_cache_tasks(&self) -> Result<Vec<CacheTask>> {
let tasks = self
.db
.iter_raw::<CacheTask>()?
.map(|ele| {
let (_, value) = ele?;
Ok(value)
})
.collect::<Result<Vec<Box<[u8]>>>>()?;
tasks
.iter()
.map(|task| CacheTask::deserialize_from(task))
.collect()
}
#[instrument(level = "debug", skip_all)]
pub fn delete_cache_task(&self, id: &str) -> Result<()> {
info!("delete cache task metadata {}", id);
self.db.delete::<CacheTask>(id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn create_persistent_piece(
&self,
piece_id: &str,
number: u32,
offset: u64,
length: u64,
digest: &str,
) -> Result<Piece> {
let piece = Piece {
number,
offset,
length,
digest: digest.to_string(),
parent_id: None,
updated_at: Utc::now().naive_utc(),
created_at: Utc::now().naive_utc(),
finished_at: Some(Utc::now().naive_utc()),
..Default::default()
};
self.db.put(piece_id.as_bytes(), &piece)?;
Ok(piece)
}
#[instrument(level = "debug", skip_all)]
pub fn create_persistent_cache_piece(
&self,
piece_id: &str,
number: u32,
offset: u64,
length: u64,
digest: &str,
) -> Result<Piece> {
let piece = Piece {
number,
offset,
length,
digest: digest.to_string(),
parent_id: None,
updated_at: Utc::now().naive_utc(),
created_at: Utc::now().naive_utc(),
finished_at: Some(Utc::now().naive_utc()),
..Default::default()
};
self.db.put(piece_id.as_bytes(), &piece)?;
Ok(piece)
}
#[instrument(level = "debug", skip_all)]
pub fn download_piece_started(
&self,
piece_id: &str,
number: u32,
offset: u64,
length: u64,
) -> Result<Piece> {
let piece = Piece {
number,
offset,
length,
updated_at: Utc::now().naive_utc(),
created_at: Utc::now().naive_utc(),
..Default::default()
};
self.db.put(piece_id.as_bytes(), &piece)?;
Ok(piece)
}
#[instrument(level = "debug", skip_all)]
pub fn download_piece_finished(
&self,
piece_id: &str,
offset: u64,
length: u64,
digest: &str,
parent_id: Option<String>,
) -> Result<Piece> {
let piece = match self.db.get::<Piece>(piece_id.as_bytes())? {
Some(mut piece) => {
piece.offset = offset;
piece.length = length;
piece.digest = digest.to_string();
piece.parent_id = parent_id;
piece.updated_at = Utc::now().naive_utc();
piece.finished_at = Some(Utc::now().naive_utc());
piece
}
None => return Err(Error::PieceNotFound(piece_id.to_string())),
};
self.db.put(piece_id.as_bytes(), &piece)?;
Ok(piece)
}
#[instrument(level = "debug", skip_all)]
pub fn download_piece_failed(&self, piece_id: &str) -> Result<()> {
if let Some(piece) = self.get_piece(piece_id)? {
if piece.is_finished() {
return Ok(());
}
}
self.delete_piece(piece_id)
}
#[instrument(level = "debug", skip_all)]
pub fn wait_for_piece_finished_failed(&self, piece_id: &str) -> Result<()> {
if let Some(piece) = self.get_piece(piece_id)? {
if piece.is_finished() {
return Ok(());
}
}
self.delete_piece(piece_id)
}
pub fn get_piece(&self, piece_id: &str) -> Result<Option<Piece>> {
self.db.get(piece_id.as_bytes())
}
pub fn get_pieces_by_ids(&self, piece_ids: &[&str]) -> Result<Vec<Option<Piece>>> {
let keys: Vec<&[u8]> = piece_ids
.iter()
.map(|piece_id| piece_id.as_bytes())
.collect();
self.db.multi_get(&keys)
}
#[instrument(level = "debug", skip_all)]
pub fn is_piece_exists(&self, piece_id: &str) -> Result<bool> {
self.db.exists::<Piece>(piece_id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn get_pieces(&self, task_id: &str) -> Result<Vec<Piece>> {
let pieces = self
.db
.prefix_iter_raw::<Piece>(task_id.as_bytes())?
.map(|ele| {
let (_, value) = ele?;
Ok(value)
})
.collect::<Result<Vec<Box<[u8]>>>>()?;
pieces
.iter()
.map(|piece| Piece::deserialize_from(piece))
.collect()
}
#[instrument(level = "debug", skip_all)]
pub fn delete_piece(&self, piece_id: &str) -> Result<()> {
info!("delete piece metadata {}", piece_id);
self.db.delete::<Piece>(piece_id.as_bytes())
}
#[instrument(level = "debug", skip_all)]
pub fn delete_pieces(&self, task_id: &str) -> Result<()> {
let piece_ids = self
.db
.prefix_iter_raw::<Piece>(task_id.as_bytes())?
.map(|ele| {
let (key, _) = ele?;
Ok(key)
})
.collect::<Result<Vec<Box<[u8]>>>>()?;
let piece_ids_refs = piece_ids
.iter()
.map(|id| {
let id_ref = id.as_ref();
info!(
"delete piece metadata {} in batch",
std::str::from_utf8(id_ref).unwrap_or_default(),
);
id_ref
})
.collect::<Vec<&[u8]>>();
self.db.batch_delete::<Piece>(piece_ids_refs)?;
Ok(())
}
#[inline]
pub fn piece_id(&self, task_id: &str, number: u32) -> String {
format!("{task_id}-{number}")
}
}
impl Metadata<RocksdbStorageEngine> {
#[instrument(level = "debug", skip_all)]
pub fn new(
config: Arc<Config>,
dir: &Path,
log_dir: &PathBuf,
) -> Result<Metadata<RocksdbStorageEngine>> {
let db = RocksdbStorageEngine::open(
dir,
log_dir,
&[
Task::NAMESPACE,
PersistentTask::NAMESPACE,
PersistentCacheTask::NAMESPACE,
CacheTask::NAMESPACE,
],
&[Piece::NAMESPACE],
config.storage.keep,
)?;
Ok(Metadata {
db,
upload_stats: DashMap::new(),
persistent_task_upload_stats: DashMap::new(),
persistent_cache_task_upload_stats: DashMap::new(),
})
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::type_complexity)]
use super::*;
use tempfile::tempdir;
const TASK_ID: &str = "d3c4e940ad06c47fc36ac67801e6f8e36cb400e2391708620bc7e865b102062c";
const OTHER_TASK_ID: &str = "a535b115f18d96870f0422ac891f91dd162f2f391e4778fb84279701fcd02dd1";
fn metadata(dir: &Path) -> Metadata {
Metadata::new(Arc::new(Config::default()), dir, &dir.join("log")).unwrap()
}
#[test]
fn task_need_drop_page_cache_on_finished_idle_task() {
let now = Utc::now().naive_utc();
let idle_timeout = Duration::from_secs(2_400);
let test_cases = vec![
(Some(now), 0, NaiveDateTime::default(), true),
(Some(now), 1, NaiveDateTime::default(), false),
(Some(now), 0, now, false),
(None, 0, NaiveDateTime::default(), false),
];
for (finished_at, uploading_count, updated_at, expected) in test_cases {
let task = Task {
finished_at,
uploading_count,
updated_at,
..Default::default()
};
assert_eq!(task.need_drop_page_cache(idle_timeout), expected);
}
}
#[test]
fn task_need_evict_on_finished_failed_or_timed_out() {
let now = Utc::now().naive_utc();
let test_cases = vec![
(now, None, None, false),
(now, Some(now), None, true),
(now, None, Some(now), true),
(NaiveDateTime::default(), None, None, true),
];
for (created_at, failed_at, finished_at, expected) in test_cases {
let task = Task {
created_at,
failed_at,
finished_at,
..Default::default()
};
assert_eq!(task.need_evict(), expected);
}
}
#[test]
fn persistent_tasks_need_evict_unless_persistent() {
let now = Utc::now().naive_utc();
let test_cases = vec![
(false, now, None, None, false),
(false, now, Some(now), None, true),
(false, now, None, Some(now), true),
(false, NaiveDateTime::default(), None, None, true),
(true, now, Some(now), Some(now), false),
(true, NaiveDateTime::default(), None, None, false),
];
for (persistent, created_at, failed_at, finished_at, expected) in test_cases {
let persistent_task = PersistentTask {
persistent,
created_at,
failed_at,
finished_at,
..Default::default()
};
let persistent_cache_task = PersistentCacheTask {
persistent,
created_at,
failed_at,
finished_at,
..Default::default()
};
assert_eq!(persistent_task.need_evict(), expected);
assert_eq!(persistent_cache_task.need_evict(), expected);
}
}
#[test]
fn task_is_empty_and_piece_count_follow_the_lengths() {
let test_cases = vec![
(None, None, false, None),
(Some(0), Some(1024), true, Some(0)),
(Some(1024), Some(1024), false, Some(1)),
(Some(1025), Some(1024), false, Some(2)),
(Some(1024), None, false, None),
(None, Some(1024), false, None),
];
for (content_length, piece_length, expected_empty, expected_piece_count) in test_cases {
let task = Task {
content_length,
piece_length,
..Default::default()
};
let cache_task = CacheTask {
content_length,
piece_length,
..Default::default()
};
assert_eq!(task.is_empty(), expected_empty);
assert_eq!(task.piece_count(), expected_piece_count);
assert_eq!(cache_task.is_empty(), expected_empty);
assert_eq!(cache_task.piece_count(), expected_piece_count);
}
}
#[test]
fn piece_cost_is_none_unless_finished_after_created() {
let created_at = Utc::now().naive_utc();
let test_cases = vec![
(None, None, None),
(
Some(created_at + Duration::from_secs(2)),
Some(Duration::from_secs(2)),
Some(prost_wkt_types::Duration {
seconds: 2,
nanos: 0,
}),
),
(Some(created_at - Duration::from_secs(2)), None, None),
];
for (finished_at, expected_cost, expected_prost_cost) in test_cases {
let piece = Piece {
created_at,
finished_at,
..Default::default()
};
assert_eq!(piece.cost(), expected_cost);
assert_eq!(piece.prost_cost(), expected_prost_cost);
}
}
#[test]
fn calculate_digest_hashes_the_piece_metadata() {
let test_cases = vec![
(1, 0, 1024, "crc32:1929153120", "crc32:3299754941"),
(2, 1024, 512, "crc32:1929153120", "crc32:3142703347"),
(0, 0, 0, "", "crc32:265657229"),
];
for (number, offset, length, digest, expected) in test_cases {
let piece = Piece {
number,
offset,
length,
digest: digest.to_string(),
..Default::default()
};
assert_eq!(piece.calculate_digest(), expected);
}
}
#[test]
fn new_metadata_is_empty() {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
assert!(metadata.get_tasks().unwrap().is_empty());
assert!(metadata.get_persistent_tasks().unwrap().is_empty());
assert!(metadata.get_persistent_cache_tasks().unwrap().is_empty());
assert!(metadata.get_cache_tasks().unwrap().is_empty());
assert!(metadata.get_pieces(TASK_ID).unwrap().is_empty());
}
#[test]
fn prepare_download_task_reuses_only_complete_unfailed_tasks() {
let test_cases: Vec<(fn(&Metadata, &str), fn(&Task, bool))> = vec![
(
|_, _| {},
|task, reused| {
assert!(!reused);
assert_eq!(task.content_length(), None);
assert_eq!(task.piece_length(), None);
assert!(!task.is_failed());
},
),
(
|metadata, id| {
metadata
.download_task_started(id, 1024, 4096, None)
.unwrap();
},
|task, reused| {
assert!(reused);
assert_eq!(task.content_length(), Some(4096));
assert_eq!(task.piece_length(), Some(1024));
},
),
(
|metadata, id| {
metadata
.download_task_started(id, 1024, 4096, None)
.unwrap();
metadata.download_task_failed(id).unwrap();
},
|task, reused| {
assert!(!reused);
assert!(!task.is_failed());
assert_eq!(task.content_length(), Some(4096));
},
),
(
|metadata, id| {
metadata.prepare_download_task(id).unwrap();
},
|task, reused| {
assert!(!reused);
assert_eq!(task.content_length(), None);
},
),
];
for (prepare, expect) in test_cases {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
prepare(&metadata, TASK_ID);
let (task, reused) = metadata.prepare_download_task(TASK_ID).unwrap();
assert_eq!(task.id, TASK_ID);
assert_eq!(metadata.get_task(TASK_ID).unwrap(), Some(task.clone()));
expect(&task, reused);
}
}
#[test]
fn task_lifecycle_tracks_download_and_upload() {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
metadata
.download_task_started(TASK_ID, 1024, 4096, None)
.unwrap();
let task = metadata.get_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.id, TASK_ID);
assert_eq!(task.piece_length(), Some(1024));
assert_eq!(task.content_length(), Some(4096));
assert!(task.response_header.is_empty());
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 0);
assert!(task.is_started());
assert!(metadata.is_task_exists(TASK_ID).unwrap());
let task = metadata.download_task_failed(TASK_ID).unwrap();
assert!(task.is_failed());
let mut response_header = HeaderMap::new();
response_header.insert("content-type", "text/plain".parse().unwrap());
let task = metadata
.download_task_started(TASK_ID, 2048, 4096, Some(response_header))
.unwrap();
assert!(!task.is_failed());
assert_eq!(task.piece_length(), Some(2048));
assert_eq!(
task.response_header.get("content-type").map(String::as_str),
Some("text/plain")
);
metadata.download_task_finished(TASK_ID).unwrap();
let task = metadata.get_task(TASK_ID).unwrap().unwrap();
assert!(task.is_finished());
assert!(!task.is_failed());
metadata.upload_task_started(TASK_ID);
let task = metadata.get_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.uploading_count, 1);
assert!(task.is_uploading());
metadata.upload_task_finished(TASK_ID);
let task = metadata.get_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 1);
metadata.upload_task_started(TASK_ID);
let task = metadata.get_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.uploading_count, 1);
metadata.upload_task_failed(TASK_ID);
let task = metadata.get_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 1);
metadata
.download_task_started(OTHER_TASK_ID, 1024, 0, None)
.unwrap();
let tasks = metadata.get_tasks().unwrap();
assert_eq!(tasks.len(), 2);
let uploaded_task = tasks.iter().find(|task| task.id == TASK_ID).unwrap();
assert_eq!(uploaded_task.uploaded_count, 1);
metadata.delete_task(OTHER_TASK_ID).unwrap();
assert_eq!(metadata.get_task(OTHER_TASK_ID).unwrap(), None);
assert!(!metadata.is_task_exists(OTHER_TASK_ID).unwrap());
}
#[test]
fn prefetch_task_starts_once_and_resets_on_failure() {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
metadata
.download_task_started(TASK_ID, 1024, 4096, None)
.unwrap();
let task = metadata.prefetch_task_started(TASK_ID).unwrap();
assert!(task.is_prefetched());
assert!(!task.is_failed());
let result = metadata.prefetch_task_started(TASK_ID);
assert!(matches!(result, Err(Error::InvalidState(ref state)) if state == "prefetched"));
let task = metadata.prefetch_task_failed(TASK_ID).unwrap();
assert!(!task.is_prefetched());
assert!(task.is_failed());
let task = metadata.prefetch_task_started(TASK_ID).unwrap();
assert!(task.is_prefetched());
assert!(!task.is_failed());
}
#[test]
fn transitions_on_a_missing_task_fail_with_task_not_found() {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
let test_cases: Vec<fn(&Metadata, &str) -> Result<()>> = vec![
|metadata, id| metadata.download_task_finished(id).map(|_| ()),
|metadata, id| metadata.download_task_failed(id).map(|_| ()),
|metadata, id| metadata.prefetch_task_started(id).map(|_| ()),
|metadata, id| metadata.prefetch_task_failed(id).map(|_| ()),
|metadata, id| metadata.create_persistent_task_finished(id).map(|_| ()),
|metadata, id| metadata.download_persistent_task_finished(id).map(|_| ()),
|metadata, id| metadata.download_persistent_task_failed(id).map(|_| ()),
|metadata, id| metadata.persist_persistent_task(id).map(|_| ()),
|metadata, id| {
metadata
.create_persistent_cache_task_finished(id)
.map(|_| ())
},
|metadata, id| {
metadata
.download_persistent_cache_task_finished(id)
.map(|_| ())
},
|metadata, id| {
metadata
.download_persistent_cache_task_failed(id)
.map(|_| ())
},
|metadata, id| metadata.persist_persistent_cache_task(id).map(|_| ()),
|metadata, id| metadata.download_cache_task_finished(id).map(|_| ()),
|metadata, id| metadata.download_cache_task_failed(id).map(|_| ()),
|metadata, id| metadata.upload_cache_task_started(id).map(|_| ()),
|metadata, id| metadata.upload_cache_task_finished(id).map(|_| ()),
|metadata, id| metadata.upload_cache_task_failed(id).map(|_| ()),
];
for run in test_cases {
let result = run(&metadata, TASK_ID);
assert!(matches!(result, Err(Error::TaskNotFound(ref id)) if id == TASK_ID));
}
}
#[test]
fn persistent_task_transitions_update_the_stored_task() {
let test_cases: Vec<(
fn(&Metadata, &str) -> Result<PersistentTask>,
fn(&PersistentTask),
)> = vec![
(
|metadata, id| metadata.create_persistent_task_finished(id),
|task| {
assert!(task.is_finished());
assert!(!task.is_failed());
},
),
(
|metadata, id| metadata.download_persistent_task_finished(id),
|task| {
assert!(task.is_finished());
assert!(!task.is_failed());
},
),
(
|metadata, id| metadata.download_persistent_task_failed(id),
|task| {
assert!(task.is_failed());
assert!(task.is_started());
},
),
(
|metadata, id| {
metadata.download_persistent_task_failed(id)?;
metadata.download_persistent_task_started(
id,
Duration::from_secs(7_200),
true,
2048,
4096,
NaiveDateTime::default(),
)
},
|task| {
assert!(!task.is_failed());
assert!(task.is_persistent());
assert_eq!(task.ttl, Duration::from_secs(7_200));
assert_eq!(task.piece_length(), 2048);
assert_eq!(task.content_length(), 4096);
},
),
(
|metadata, id| metadata.persist_persistent_task(id),
|task| {
assert!(task.is_persistent());
assert!(task.is_started());
},
),
];
for (run, expect) in test_cases {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
let created_at = Utc::now().naive_utc();
let started = metadata
.download_persistent_task_started(
TASK_ID,
Duration::from_secs(3_600),
false,
1024,
4096,
created_at,
)
.unwrap();
assert!(started.is_started());
assert!(!started.is_persistent());
let task = run(&metadata, TASK_ID).unwrap();
assert_eq!(task.id, TASK_ID);
assert_eq!(task.created_at, created_at);
assert_eq!(
metadata.get_persistent_task(TASK_ID).unwrap(),
Some(task.clone())
);
expect(&task);
}
}
#[test]
fn persistent_task_lifecycle_tracks_creation_and_upload() {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
let task = metadata
.create_persistent_task_started(TASK_ID, Duration::from_secs(3_600), 1024, 4096)
.unwrap();
assert_eq!(task.id, TASK_ID);
assert!(task.is_persistent());
assert!(task.is_started());
assert_eq!(task.ttl, Duration::from_secs(3_600));
assert_eq!(task.piece_length(), 1024);
assert_eq!(task.content_length(), 4096);
assert_eq!(task.piece_count(), 4);
assert!(!task.is_empty());
assert!(metadata.is_persistent_task_exists(TASK_ID).unwrap());
let task = metadata.create_persistent_task_finished(TASK_ID).unwrap();
assert!(task.is_finished());
let finished_at = task.finished_at;
let task = metadata.create_persistent_task_finished(TASK_ID).unwrap();
assert_eq!(task.finished_at, finished_at);
metadata.upload_persistent_task_started(TASK_ID);
let task = metadata.get_persistent_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.uploading_count, 1);
assert!(task.is_uploading());
metadata.upload_persistent_task_finished(TASK_ID);
let task = metadata.get_persistent_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 1);
metadata.upload_persistent_task_started(TASK_ID);
metadata.upload_persistent_task_failed(TASK_ID);
let task = metadata.get_persistent_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 1);
metadata
.create_persistent_task_started(OTHER_TASK_ID, Duration::from_secs(3_600), 1024, 0)
.unwrap();
let tasks = metadata.get_persistent_tasks().unwrap();
assert_eq!(tasks.len(), 2);
let uploaded_task = tasks.iter().find(|task| task.id == TASK_ID).unwrap();
assert_eq!(uploaded_task.uploaded_count, 1);
metadata.delete_persistent_task(TASK_ID).unwrap();
assert_eq!(metadata.get_persistent_task(TASK_ID).unwrap(), None);
assert!(!metadata.is_persistent_task_exists(TASK_ID).unwrap());
}
#[test]
fn persistent_cache_task_transitions_update_the_stored_task() {
let test_cases: Vec<(
fn(&Metadata, &str) -> Result<PersistentCacheTask>,
fn(&PersistentCacheTask),
)> = vec![
(
|metadata, id| metadata.create_persistent_cache_task_finished(id),
|task| {
assert!(task.is_finished());
assert!(!task.is_failed());
},
),
(
|metadata, id| metadata.download_persistent_cache_task_finished(id),
|task| {
assert!(task.is_finished());
assert!(!task.is_failed());
},
),
(
|metadata, id| metadata.download_persistent_cache_task_failed(id),
|task| {
assert!(task.is_failed());
assert!(task.is_started());
},
),
(
|metadata, id| {
metadata.download_persistent_cache_task_failed(id)?;
metadata.download_persistent_cache_task_started(
id,
Duration::from_secs(7_200),
true,
2048,
4096,
NaiveDateTime::default(),
)
},
|task| {
assert!(!task.is_failed());
assert!(task.is_persistent());
assert_eq!(task.ttl, Duration::from_secs(7_200));
assert_eq!(task.piece_length(), 2048);
assert_eq!(task.content_length(), 4096);
},
),
(
|metadata, id| metadata.persist_persistent_cache_task(id),
|task| {
assert!(task.is_persistent());
assert!(task.is_started());
},
),
];
for (run, expect) in test_cases {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
let created_at = Utc::now().naive_utc();
let started = metadata
.download_persistent_cache_task_started(
TASK_ID,
Duration::from_secs(3_600),
false,
1024,
4096,
created_at,
)
.unwrap();
assert!(started.is_started());
assert!(!started.is_persistent());
let task = run(&metadata, TASK_ID).unwrap();
assert_eq!(task.id, TASK_ID);
assert_eq!(task.created_at, created_at);
assert_eq!(
metadata.get_persistent_cache_task(TASK_ID).unwrap(),
Some(task.clone())
);
expect(&task);
}
}
#[test]
fn persistent_cache_task_lifecycle_tracks_creation_and_upload() {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
let task = metadata
.create_persistent_cache_task_started(TASK_ID, Duration::from_secs(3_600), 1024, 4096)
.unwrap();
assert_eq!(task.id, TASK_ID);
assert!(task.is_persistent());
assert!(task.is_started());
assert_eq!(task.ttl, Duration::from_secs(3_600));
assert_eq!(task.piece_length(), 1024);
assert_eq!(task.content_length(), 4096);
assert_eq!(task.piece_count(), 4);
assert!(!task.is_empty());
assert!(metadata.is_persistent_cache_task_exists(TASK_ID).unwrap());
let task = metadata
.create_persistent_cache_task_finished(TASK_ID)
.unwrap();
assert!(task.is_finished());
let finished_at = task.finished_at;
let task = metadata
.create_persistent_cache_task_finished(TASK_ID)
.unwrap();
assert_eq!(task.finished_at, finished_at);
metadata.upload_persistent_cache_task_started(TASK_ID);
let task = metadata
.get_persistent_cache_task(TASK_ID)
.unwrap()
.unwrap();
assert_eq!(task.uploading_count, 1);
assert!(task.is_uploading());
metadata.upload_persistent_cache_task_finished(TASK_ID);
let task = metadata
.get_persistent_cache_task(TASK_ID)
.unwrap()
.unwrap();
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 1);
metadata.upload_persistent_cache_task_started(TASK_ID);
metadata.upload_persistent_cache_task_failed(TASK_ID);
let task = metadata
.get_persistent_cache_task(TASK_ID)
.unwrap()
.unwrap();
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 1);
metadata
.create_persistent_cache_task_started(
OTHER_TASK_ID,
Duration::from_secs(3_600),
1024,
0,
)
.unwrap();
let tasks = metadata.get_persistent_cache_tasks().unwrap();
assert_eq!(tasks.len(), 2);
let uploaded_task = tasks.iter().find(|task| task.id == TASK_ID).unwrap();
assert_eq!(uploaded_task.uploaded_count, 1);
metadata.delete_persistent_cache_task(TASK_ID).unwrap();
assert_eq!(metadata.get_persistent_cache_task(TASK_ID).unwrap(), None);
assert!(!metadata.is_persistent_cache_task_exists(TASK_ID).unwrap());
}
#[test]
fn cache_task_lifecycle_tracks_download_and_upload() {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
metadata
.download_cache_task_started(TASK_ID, 1024, 4096, None)
.unwrap();
let task = metadata.get_cache_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.id, TASK_ID);
assert_eq!(task.piece_length(), Some(1024));
assert_eq!(task.content_length(), Some(4096));
assert!(task.response_header.is_empty());
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 0);
assert!(task.is_started());
assert!(metadata.is_cache_task_exists(TASK_ID).unwrap());
let task = metadata.download_cache_task_failed(TASK_ID).unwrap();
assert!(task.is_failed());
let mut response_header = HeaderMap::new();
response_header.insert("content-type", "text/plain".parse().unwrap());
let task = metadata
.download_cache_task_started(TASK_ID, 2048, 4096, Some(response_header))
.unwrap();
assert!(!task.is_failed());
assert_eq!(task.piece_length(), Some(2048));
assert_eq!(
task.response_header.get("content-type").map(String::as_str),
Some("text/plain")
);
metadata.download_cache_task_finished(TASK_ID).unwrap();
let task = metadata.get_cache_task(TASK_ID).unwrap().unwrap();
assert!(task.is_finished());
assert!(!task.is_failed());
metadata.upload_cache_task_started(TASK_ID).unwrap();
let task = metadata.get_cache_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.uploading_count, 1);
assert!(task.is_uploading());
metadata.upload_cache_task_finished(TASK_ID).unwrap();
let task = metadata.get_cache_task(TASK_ID).unwrap().unwrap();
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 1);
let task = metadata.upload_cache_task_started(TASK_ID).unwrap();
assert_eq!(task.uploading_count, 1);
let task = metadata.upload_cache_task_failed(TASK_ID).unwrap();
assert_eq!(task.uploading_count, 0);
assert_eq!(task.uploaded_count, 1);
assert_eq!(metadata.get_cache_task(TASK_ID).unwrap(), Some(task));
metadata
.download_cache_task_started(OTHER_TASK_ID, 1024, 0, None)
.unwrap();
assert_eq!(metadata.get_cache_tasks().unwrap().len(), 2);
metadata.delete_cache_task(OTHER_TASK_ID).unwrap();
assert_eq!(metadata.get_cache_task(OTHER_TASK_ID).unwrap(), None);
assert!(!metadata.is_cache_task_exists(OTHER_TASK_ID).unwrap());
}
#[test]
fn piece_lifecycle_tracks_download_and_deletion() {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
let piece_id = metadata.piece_id(TASK_ID, 1);
assert_eq!(piece_id, format!("{TASK_ID}-1"));
let result = metadata.download_piece_finished(&piece_id, 0, 1024, "crc32:1", None);
assert!(matches!(result, Err(Error::PieceNotFound(ref id)) if id == &piece_id));
let piece = metadata
.download_piece_started(&piece_id, 1, 1024, 1024)
.unwrap();
assert_eq!(piece.number, 1);
assert_eq!(piece.offset, 1024);
assert_eq!(piece.length, 1024);
assert!(piece.is_started());
assert!(metadata.is_piece_exists(&piece_id).unwrap());
let piece = metadata
.download_piece_finished(&piece_id, 0, 1024, "crc32:1", Some("parent-1".to_string()))
.unwrap();
assert!(piece.is_finished());
assert_eq!(piece.offset, 0);
assert_eq!(piece.length, 1024);
assert_eq!(piece.digest, "crc32:1");
assert_eq!(piece.parent_id.as_deref(), Some("parent-1"));
assert_eq!(metadata.get_piece(&piece_id).unwrap(), Some(piece.clone()));
let second_piece_id = metadata.piece_id(TASK_ID, 2);
metadata
.download_piece_started(&second_piece_id, 2, 2048, 1024)
.unwrap();
metadata
.download_piece_started(&metadata.piece_id(TASK_ID, 3), 3, 3072, 1024)
.unwrap();
assert_eq!(metadata.get_pieces(TASK_ID).unwrap().len(), 3);
assert!(metadata.get_pieces(OTHER_TASK_ID).unwrap().is_empty());
let pieces = metadata
.get_pieces_by_ids(&[second_piece_id.as_str(), "missing", piece_id.as_str()])
.unwrap();
assert_eq!(pieces[0].as_ref().map(|piece| piece.number), Some(2));
assert_eq!(pieces[1], None);
assert_eq!(pieces[2].as_ref(), Some(&piece));
metadata.delete_piece(&second_piece_id).unwrap();
assert_eq!(metadata.get_piece(&second_piece_id).unwrap(), None);
assert_eq!(metadata.get_pieces(TASK_ID).unwrap().len(), 2);
metadata.delete_pieces(TASK_ID).unwrap();
assert!(metadata.get_pieces(TASK_ID).unwrap().is_empty());
}
#[test]
fn piece_failures_delete_only_unfinished_pieces() {
let test_cases: Vec<(
fn(&Metadata, &str) -> Result<Piece>,
fn(&Metadata, &str) -> Result<()>,
fn(Piece, Option<Piece>),
)> = vec![
(
|metadata, id| metadata.download_piece_started(id, 1, 0, 1024),
Metadata::download_piece_failed,
|piece, stored| {
assert!(!piece.is_finished());
assert_eq!(stored, None);
},
),
(
|metadata, id| {
metadata.download_piece_started(id, 1, 0, 1024)?;
metadata.download_piece_finished(id, 0, 1024, "crc32:1", None)
},
Metadata::download_piece_failed,
|piece, stored| {
assert!(piece.is_finished());
assert_eq!(stored, Some(piece));
},
),
(
|metadata, id| metadata.create_persistent_piece(id, 1, 0, 1024, "crc32:1"),
Metadata::download_piece_failed,
|piece, stored| {
assert!(piece.is_finished());
assert_eq!(stored, Some(piece));
},
),
(
|metadata, id| metadata.create_persistent_cache_piece(id, 1, 0, 1024, "crc32:1"),
Metadata::download_piece_failed,
|piece, stored| {
assert!(piece.is_finished());
assert_eq!(stored, Some(piece));
},
),
(
|metadata, id| metadata.download_piece_started(id, 1, 0, 1024),
Metadata::wait_for_piece_finished_failed,
|piece, stored| {
assert!(!piece.is_finished());
assert_eq!(stored, None);
},
),
(
|metadata, id| {
metadata.download_piece_started(id, 1, 0, 1024)?;
metadata.download_piece_finished(id, 0, 1024, "crc32:1", None)
},
Metadata::wait_for_piece_finished_failed,
|piece, stored| {
assert!(piece.is_finished());
assert_eq!(stored, Some(piece));
},
),
(
|metadata, id| metadata.create_persistent_piece(id, 1, 0, 1024, "crc32:1"),
Metadata::wait_for_piece_finished_failed,
|piece, stored| {
assert!(piece.is_finished());
assert_eq!(stored, Some(piece));
},
),
(
|metadata, id| metadata.create_persistent_cache_piece(id, 1, 0, 1024, "crc32:1"),
Metadata::wait_for_piece_finished_failed,
|piece, stored| {
assert!(piece.is_finished());
assert_eq!(stored, Some(piece));
},
),
];
for (setup, fail, expect) in test_cases {
let dir = tempdir().unwrap();
let metadata = metadata(dir.path());
let piece_id = metadata.piece_id(TASK_ID, 1);
let piece = setup(&metadata, &piece_id).unwrap();
let result = fail(&metadata, &piece_id);
assert!(result.is_ok());
expect(piece, metadata.get_piece(&piece_id).unwrap());
}
}
}