use anyhow::Result;
use noosphere_core::context::HasSphereContext;
use noosphere_storage::Storage;
use std::{collections::BTreeSet, marker::PhantomData, sync::Arc, thread::available_parallelism};
use tokio::{select, task::JoinSet};
use super::{SphereRenderJob, SphereRenderRequest};
use crate::native::{
paths::SpherePaths,
render::{JobKind, SphereRenderJobId},
};
const DEFAULT_RENDER_DEPTH: u32 = 3;
pub struct SphereRenderer<C, S>
where
C: HasSphereContext<S> + 'static,
S: Storage + 'static,
{
context: C,
paths: Arc<SpherePaths>,
storage_type: PhantomData<S>,
}
impl<C, S> SphereRenderer<C, S>
where
C: HasSphereContext<S> + 'static,
S: Storage + 'static,
{
pub fn new(context: C, paths: Arc<SpherePaths>) -> Self {
SphereRenderer {
context,
paths,
storage_type: PhantomData,
}
}
#[instrument(level = "debug", skip(self))]
pub async fn render(&self, depth: Option<u32>, force_full_render: bool) -> Result<()> {
std::env::set_current_dir(self.paths.root())?;
let mut render_jobs = JoinSet::<Result<()>>::new();
let mut started_jobs = BTreeSet::<SphereRenderJobId>::new();
let max_parallel_jobs = available_parallelism()?.get();
let (tx, mut rx) = tokio::sync::mpsc::channel::<SphereRenderRequest>(max_parallel_jobs);
let last_render_depth =
if let Ok(depth) = tokio::fs::read_to_string(self.paths.depth()).await {
depth.parse::<u32>().ok()
} else {
None
};
let render_depth = if let Some(depth) = depth {
depth
} else {
last_render_depth.unwrap_or(DEFAULT_RENDER_DEPTH)
};
let force_render_peers = if force_full_render {
true
} else if let Some(last_render_depth) = last_render_depth {
render_depth > last_render_depth
} else {
false
};
if force_render_peers {
self.reset_peers().await?;
debug!(
?max_parallel_jobs,
?render_depth,
"Spawning peer refresh render job for {}...",
self.context.identity().await?
);
render_jobs.spawn(
SphereRenderJob::new(
self.context.clone(),
JobKind::RefreshPeers,
self.paths.clone(),
Vec::new(),
tx.clone(),
)
.render(),
);
}
debug!(
?max_parallel_jobs,
?render_depth,
"Spawning root render job for {}...",
self.context.identity().await?
);
render_jobs.spawn(
SphereRenderJob::new(
self.context.clone(),
JobKind::Root { force_full_render },
self.paths.clone(),
Vec::new(),
tx.clone(),
)
.render(),
);
let mut job_queue_open = true;
while !render_jobs.is_empty() && job_queue_open {
select! {
result = render_jobs.join_next() => {
if let Some(join_result) = result {
match join_result {
Err(error) => {
warn!("Failed to join render job task: {}", error);
},
Ok(task_result) => match task_result {
Ok(_) => (),
Err(error) => warn!("Render job failed: {}", error),
}
}
}
},
next_job_request = rx.recv() => {
match next_job_request {
None => {
job_queue_open = false;
}
Some(job_request) => {
let job_id = job_request.as_id();
if started_jobs.contains(&job_id) {
debug!("A render job for {} @ {} has already been queued, skipping...", job_id.0, job_id.1);
continue;
}
let SphereRenderRequest(petname_path, peer, version, link_record) = job_request;
if petname_path.len() > render_depth as usize {
debug!("Skipping render job for '@{}' (exceeds max render depth {render_depth})", petname_path.join("."));
continue;
}
debug!(?petname_path, "Queuing render job for {} @ {}...", job_id.0, job_id.1);
started_jobs.insert(job_id);
if self.paths.peer(&peer, &version).exists() {
debug!(
"Content for {} @ {} is already rendered, skipping...",
peer, version
);
continue;
}
debug!("Spawning render job for {peer} @ {version}...");
render_jobs.spawn(
SphereRenderJob::new(
self.context.clone(),
JobKind::Peer(peer, version, link_record),
self.paths.clone(),
petname_path,
tx.clone()
).render()
);
}
}
}
}
}
tokio::fs::write(self.paths.depth(), render_depth.to_string()).await?;
Ok(())
}
async fn reset_peers(&self) -> Result<()> {
if let Err(error) = tokio::fs::remove_dir_all(self.paths.peers()).await {
warn!(
path = ?self.paths.peers(),
"Failed attempt to reset peers: {}", error
);
}
tokio::fs::create_dir_all(self.paths.peers()).await?;
Ok(())
}
}