use std::collections::BTreeMap;
use std::fs::{File, OpenOptions, TryLockError};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
use crate::channel::Channel;
use crate::config::{self, Config, ConfigError, LayerDef, Model, Participants};
use crate::feed::{self, Event, FeedLog, Notify};
use crate::json::{egress_context, Egress};
use crate::tuplespace::Tuplespace;
use ikigai_store::budget::EvaluationBudget;
pub struct LayerState {
pub def: LayerDef,
pub(crate) write: futures::lock::Mutex<()>,
pub(crate) feed: Mutex<FeedLog>,
pub(crate) model: ModelState,
pub(crate) egress: serde_json::Value,
}
pub enum ModelState {
Channel(RwLock<Channel>),
Tuplespace(Tuplespace),
}
impl LayerState {
pub(crate) fn channel(&self) -> &RwLock<Channel> {
match &self.model {
ModelState::Channel(channel) => channel,
_ => unreachable!("channel actions are bound only on channel layers"),
}
}
pub(crate) fn tuplespace(&self) -> &Tuplespace {
match &self.model {
ModelState::Tuplespace(space) => space,
_ => unreachable!("tuplespace actions are bound only on tuplespace layers"),
}
}
pub fn events_after(&self, after: u64) -> Vec<Event> {
self.feed.lock().expect("feed lock").after(after).to_vec()
}
pub fn last_seq(&self) -> u64 {
self.feed.lock().expect("feed lock").last()
}
}
pub const LOCK_FILE: &str = "host.lock";
pub struct Host {
config_dir: PathBuf,
layers: BTreeMap<String, Arc<LayerState>>,
notify: Arc<Notify>,
match_budget: EvaluationBudget,
json: Egress,
_lock: File,
}
impl Host {
pub fn open(
config_dir: impl AsRef<Path>,
state_dir: impl AsRef<Path>,
) -> Result<Arc<Host>, ConfigError> {
let config = Config::load(&config_dir)?;
let match_budget = EvaluationBudget::new(config::load_match_budget(&config.dir)?);
let json = Egress::new();
let mut egress = BTreeMap::new();
for (name, def) in &config.layers {
compile_shapes(def).map_err(|e| ConfigError(format!("layer `{name}`: {e}")))?;
let context = egress_context(&def.context)
.and_then(|context| json.check(name, &context).map(|()| context))
.map_err(|e| ConfigError(format!("layer `{name}`: {e}")))?;
egress.insert(name.clone(), context);
}
let state_dir = state_dir.as_ref();
let lock = lock_state(state_dir).map_err(ConfigError)?;
let mut layers = BTreeMap::new();
for (name, def) in config.layers {
let dir = state_dir.join(&name);
let context = egress.remove(&name).expect("checked above");
let state = open_layer(def, &dir, match_budget.clone(), context)
.map_err(|e| ConfigError(format!("layer `{name}` state: {e}")))?;
layers.insert(name, Arc::new(state));
}
Ok(Arc::new(Host {
config_dir: config.dir,
layers,
notify: Arc::new(Notify::default()),
match_budget,
json,
_lock: lock,
}))
}
pub fn layers(&self) -> &BTreeMap<String, Arc<LayerState>> {
&self.layers
}
pub fn layer(&self, name: &str) -> Option<&Arc<LayerState>> {
self.layers.get(name)
}
pub fn match_budget(&self) -> &EvaluationBudget {
&self.match_budget
}
pub(crate) fn json(&self) -> &Egress {
&self.json
}
pub fn notify(&self) -> &Notify {
&self.notify
}
pub fn participants(&self) -> Result<Participants, ConfigError> {
config::load_participants(&self.config_dir)
}
pub fn collabs(&self) -> Result<BTreeMap<String, config::Manifest>, ConfigError> {
self.collabs_with(&self.participants()?)
}
pub(crate) fn collabs_with(
&self,
participants: &Participants,
) -> Result<BTreeMap<String, config::Manifest>, ConfigError> {
let defs: BTreeMap<String, LayerDef> = self
.layers
.iter()
.map(|(name, state)| (name.clone(), state.def.clone()))
.collect();
config::load_collabs(&self.config_dir, &defs, participants)
}
}
fn compile_shapes(def: &LayerDef) -> Result<(), String> {
ikigai_shacl::validate_report("", &def.shapes)
.map(|_| ())
.map_err(|e| format!("the validator cannot compile its shapes ({e})"))
}
fn lock_state(state_dir: &Path) -> Result<File, String> {
std::fs::create_dir_all(state_dir)
.map_err(|e| format!("create {}: {e}", state_dir.display()))?;
let path = state_dir.join(LOCK_FILE);
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(|e| format!("open {}: {e}", path.display()))?;
match file.try_lock() {
Ok(()) => {}
Err(TryLockError::WouldBlock) => {
let mut held = String::new();
let holder = match file.read_to_string(&mut held) {
Ok(_) if !held.trim().is_empty() => format!("pid {}", held.trim()),
_ => "a pid it has not written".to_string(),
};
return Err(format!(
"state directory {} is held by another layer host ({holder}, {}): one host per \
state directory, since the lock that keeps each feed gapless is in-process",
state_dir.display(),
path.display()
));
}
Err(TryLockError::Error(e)) => {
return Err(format!("lock {}: {e}", path.display()));
}
}
file.set_len(0)
.and_then(|()| file.seek(SeekFrom::Start(0)))
.and_then(|_| writeln!(file, "{}", std::process::id()))
.and_then(|()| file.flush())
.map_err(|e| format!("write {}: {e}", path.display()))?;
Ok(file)
}
fn open_layer(
def: LayerDef,
dir: &Path,
budget: EvaluationBudget,
egress: serde_json::Value,
) -> Result<LayerState, String> {
std::fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
let feed_path = dir.join("feed.log");
feed::touch(&feed_path)?;
let mut feed = FeedLog::open(&feed_path)?;
let model = match def.model {
Model::Channel => {
let channel = Channel::open(&def, dir)?;
let logged = feed.last();
for meta in channel.entries().iter().filter(|m| m.seq > logged) {
if meta.seq != feed.last() + 1 {
return Err(format!(
"entry {} has sequence {} but the feed ends at {}: a gap no repair \
can close",
meta.iri,
meta.seq,
feed.last()
));
}
feed.append(Event {
seq: meta.seq,
time: meta.created.clone(),
kind: "say".into(),
iri: meta.iri.clone(),
by: meta.author.clone(),
})?;
}
ModelState::Channel(RwLock::new(channel))
}
Model::Tuplespace => {
let logged: Vec<String> = feed
.events()
.iter()
.filter(|e| e.kind == "out")
.map(|e| e.iri.clone())
.collect();
let (space, stored) = Tuplespace::open_with_budget(&def, dir, &logged, budget)?;
let logged_to = feed.last();
for event in stored.into_iter().filter(|e| e.seq > logged_to) {
if event.seq != feed.last() + 1 {
return Err(format!(
"{} `{}` has sequence {} but the feed ends at {}: a gap no repair \
can close",
event.kind,
event.iri,
event.seq,
feed.last()
));
}
feed.append(event)?;
}
ModelState::Tuplespace(space)
}
};
Ok(LayerState {
def,
write: futures::lock::Mutex::new(()),
feed: Mutex::new(feed),
model,
egress,
})
}