use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Duration;
use swale::store::{provider_options, store_path};
use swale::{
DefinitionStore, OperatorSet, Pools, RecordHook, RequestStore, Scheduler, StatusReader,
};
use taquba::Queue;
use taquba::object_store::local::LocalFileSystem;
use taquba::object_store::path::Path as ObjectPath;
use taquba::object_store::{ObjectStore, parse_url_opts};
use super::CommandResult;
const QUEUE_PATH: &str = "swale";
#[derive(clap::Args)]
pub(crate) struct StoreArg {
#[arg(long, value_parser = parse_store_arg)]
pub(crate) store: Option<String>,
}
fn parse_store_arg(raw: &str) -> Result<String, String> {
let is_url = raw.contains("://") && raw.chars().next().is_some_and(|c| c.is_ascii_alphabetic());
if !is_url {
return Ok(raw.to_string());
}
let url = url::Url::parse(raw).map_err(|e| format!("`{raw}` is not a URL: {e}"))?;
let feature = match url.scheme() {
"file" => None,
"s3" => Some(("aws", cfg!(feature = "aws"))),
"gs" => Some(("gcp", cfg!(feature = "gcp"))),
"az" | "abfs" | "abfss" => Some(("azure", cfg!(feature = "azure"))),
other => {
return Err(format!(
"scheme `{other}` is not one of s3, gs, az, abfs, abfss or file"
));
}
};
if let Some((feature, enabled)) = feature
&& !enabled
{
return Err(format!(
"scheme `{}` needs a build with the `{feature}` feature",
url.scheme()
));
}
Ok(raw.to_string())
}
fn default_store(home: Option<PathBuf>) -> Option<PathBuf> {
let home = home.filter(|dir| !dir.as_os_str().is_empty())?;
Some(home.join(".swale").join("store"))
}
pub(crate) struct Store {
pub(crate) objects: Arc<dyn ObjectStore>,
pub(crate) prefix: String,
pub(crate) queue_path: String,
}
impl Store {
pub(crate) fn definitions(&self, operators: Arc<OperatorSet>) -> Arc<DefinitionStore> {
Arc::new(DefinitionStore::new(
self.objects.clone(),
&self.prefix,
operators,
))
}
pub(crate) fn requests(&self) -> RequestStore {
RequestStore::new(self.objects.clone(), &self.prefix)
}
}
fn store_location(arg: StoreArg) -> Result<String, Box<dyn std::error::Error>> {
match arg.store {
Some(raw) => Ok(raw),
None => Ok(default_store(std::env::home_dir())
.ok_or("the default store needs a home directory: pass --store <dir or URL>")?
.to_string_lossy()
.into_owned()),
}
}
pub(crate) fn create_store(arg: StoreArg) -> Result<Store, Box<dyn std::error::Error>> {
let location = store_location(arg)?;
if !location.contains("://") {
std::fs::create_dir_all(&location)?;
}
open_location(&location)
}
pub(crate) fn open_store(arg: StoreArg) -> Result<Store, Box<dyn std::error::Error>> {
open_location(&store_location(arg)?)
}
fn open_location(location: &str) -> Result<Store, Box<dyn std::error::Error>> {
let (objects, prefix): (Arc<dyn ObjectStore>, ObjectPath) = if location.contains("://") {
let url = url::Url::parse(location)?;
let (store, prefix) = parse_url_opts(&url, provider_options(std::env::vars_os()))?;
(Arc::from(store), prefix)
} else {
(
Arc::new(LocalFileSystem::new_with_prefix(location)?),
ObjectPath::default(),
)
};
let prefix = prefix.as_ref().to_string();
let queue_path = store_path(&prefix, QUEUE_PATH);
Ok(Store {
objects,
prefix,
queue_path,
})
}
pub(crate) struct Runtime {
pub(crate) queue: Arc<Queue>,
pub(crate) pools: Arc<Pools>,
pub(crate) scheduler: Arc<Scheduler>,
}
impl Runtime {
pub(crate) async fn open(
store: Store,
operators: Arc<OperatorSet>,
definitions: Arc<DefinitionStore>,
pool_sizes: &BTreeMap<String, usize>,
) -> Result<Runtime, Box<dyn std::error::Error>> {
let queue = Arc::new(Queue::open(store.objects.clone(), &store.queue_path).await?);
let hook = RecordHook::new(queue.clock());
let mut pools = Pools::builder(queue.clone(), store.objects, operators, hook)
.poll_interval(Duration::from_millis(100))
.store_prefix(store.prefix);
for (name, steps) in pool_sizes {
pools = pools.pool(name, *steps);
}
let pools = Arc::new(pools.build());
let scheduler = Arc::new(Scheduler::new(queue.clone(), definitions, pools.clone()));
Ok(Runtime {
queue,
pools,
scheduler,
})
}
pub(crate) async fn close(self) -> Result<(), Box<dyn std::error::Error>> {
drop(self.scheduler);
drop(self.pools);
if let Ok(queue) = Arc::try_unwrap(self.queue) {
queue.close().await?;
}
Ok(())
}
}
async fn open_status(store: StoreArg) -> Result<StatusReader, Box<dyn std::error::Error>> {
let store = open_store(store)?;
Ok(StatusReader::open(
store.objects.clone(),
&store.queue_path,
store.definitions(Arc::new(OperatorSet::builtin())),
)
.await?)
}
pub(crate) async fn with_reader(
store: StoreArg,
print: impl AsyncFnOnce(&StatusReader) -> Result<(), Box<dyn std::error::Error>>,
) -> CommandResult {
let reader = open_status(store).await?;
let result = print(&reader).await;
reader.close().await?;
result?;
Ok(ExitCode::SUCCESS)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn the_default_store_is_swale_store_in_the_home_directory() {
assert_eq!(
default_store(Some(PathBuf::from("/home/u"))),
Some(Path::new("/home/u").join(".swale").join("store"))
);
assert_eq!(default_store(Some(PathBuf::new())), None);
assert_eq!(default_store(None), None);
}
}