#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ConcurrencySection {
pub max_threads: usize,
pub ram_fraction_percent: u64,
}
impl Default for ConcurrencySection {
fn default() -> Self {
Self {
max_threads: crate::constants::MAX_AUTO_THREADS,
ram_fraction_percent: crate::constants::RAM_FRACTION_PERCENT,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct LockSection {
pub acquire_timeout_ms: u64,
}
impl Default for LockSection {
fn default() -> Self {
Self {
acquire_timeout_ms: crate::constants::LOCK_ACQUIRE_TIMEOUT_MS,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RetrySection {
pub base_ms: Option<u64>,
pub max_ms: Option<u64>,
pub max_elapsed_ms: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ExtractSection {
pub max_sample_rows: usize,
pub max_text_chars: usize,
pub max_sheets: usize,
pub max_slides: usize,
}
impl Default for ExtractSection {
fn default() -> Self {
use crate::commands::extract_artifact as artifact;
Self {
max_sample_rows: artifact::DEFAULT_MAX_SAMPLE_ROWS,
max_text_chars: artifact::DEFAULT_MAX_TEXT_CHARS,
max_sheets: artifact::MAX_SHEETS,
max_slides: artifact::MAX_SLIDES,
}
}
}
fn require_in_range(
section: &str,
key: &str,
value: u64,
min: u64,
max: u64,
) -> Result<(), AtomwriteError> {
if !(min..=max).contains(&value) {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"config [{section}] {key} = {value} {} {min}..={max}",
crate::constants::CONFIG_OUT_OF_RANGE_REASON_MARKER
),
});
}
Ok(())
}
fn require_not_above(
section: &str,
(key, value): (&str, u64),
(limit_key, limit): (&str, u64),
) -> Result<(), AtomwriteError> {
if value > limit {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"config [{section}] {key} = {value} {} {limit_key} = {limit}",
crate::constants::CONFIG_RELATION_REASON_MARKER
),
});
}
Ok(())
}
pub fn validate_tuning(cfg: &AtomwriteConfig) -> Result<(), AtomwriteError> {
use crate::constants as k;
let c = &cfg.concurrency;
require_in_range(
"concurrency",
"max_threads",
c.max_threads as u64,
k::CONCURRENCY_MIN_THREADS,
k::CONCURRENCY_MAX_THREADS_CEILING,
)?;
require_in_range(
"concurrency",
"ram_fraction_percent",
c.ram_fraction_percent,
k::RAM_FRACTION_PERCENT_MIN,
k::PERCENT_MAX,
)?;
require_in_range(
"lock",
"acquire_timeout_ms",
cfg.lock.acquire_timeout_ms,
k::LOCK_MIN_ACQUIRE_TIMEOUT_MS,
k::LOCK_MAX_ACQUIRE_TIMEOUT_MS,
)?;
let r = &cfg.retry;
for (key, value, max) in [
("base_ms", r.base_ms, k::RETRY_MAX_WAIT_CEILING_MS),
("max_ms", r.max_ms, k::RETRY_MAX_WAIT_CEILING_MS),
(
"max_elapsed_ms",
r.max_elapsed_ms,
k::RETRY_MAX_ELAPSED_CEILING_MS,
),
] {
if let Some(value) = value {
require_in_range("retry", key, value, k::RETRY_MIN_MS, max)?;
}
}
let base = r.base_ms.unwrap_or(k::LOCK_RETRY_BASE_MS);
require_not_above(
"retry",
("base_ms", base),
("max_ms", r.max_ms.unwrap_or(k::LOCK_RETRY_CAP_MS)),
)?;
require_not_above(
"retry",
("base_ms", base),
(
"max_elapsed_ms",
r.max_elapsed_ms.unwrap_or(k::PERSIST_RETRY_MAX_ELAPSED_MS),
),
)?;
let e = &cfg.extract;
require_in_range(
"extract",
"max_sample_rows",
e.max_sample_rows as u64,
0,
k::EXTRACT_MAX_SAMPLE_ROWS_CEILING,
)?;
require_in_range(
"extract",
"max_text_chars",
e.max_text_chars as u64,
1,
k::EXTRACT_MAX_TEXT_CHARS_CEILING,
)?;
require_in_range(
"extract",
"max_sheets",
e.max_sheets as u64,
1,
k::EXTRACT_MAX_PARTS_CEILING,
)?;
require_in_range(
"extract",
"max_slides",
e.max_slides as u64,
1,
k::EXTRACT_MAX_PARTS_CEILING,
)?;
Ok(())
}
pub fn validate_all(cfg: &AtomwriteConfig) -> Result<(), AtomwriteError> {
validate_fuzzy(&cfg.fuzzy)?;
validate_write(&cfg.write)?;
validate_search_section(&cfg.search)?;
crate::config::validate_read_output(&cfg.read, &cfg.output)?;
crate::config::validate_summary_section(&cfg.summary)?;
validate_log(&cfg.log)?;
validate_tuning(cfg)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Tuning {
pub max_threads: usize,
pub ram_fraction_percent: u64,
pub lock_acquire_timeout_ms: u64,
pub retry_base_ms: Option<u64>,
pub retry_max_ms: Option<u64>,
pub retry_max_elapsed_ms: Option<u64>,
pub search_format_is_text: bool,
pub search_max_inflight_bytes: u64,
pub extract_max_sample_rows: usize,
pub extract_max_text_chars: usize,
pub extract_max_sheets: usize,
pub extract_max_slides: usize,
pub summary_errors_max_entries: usize,
pub read_max_output_bytes: usize,
pub read_number: bool,
pub delete_backup: Option<bool>,
pub output_lean: bool,
}
impl Tuning {
#[must_use]
pub fn from_config(cfg: &AtomwriteConfig) -> Self {
Self {
max_threads: cfg.concurrency.max_threads,
ram_fraction_percent: cfg.concurrency.ram_fraction_percent,
lock_acquire_timeout_ms: cfg.lock.acquire_timeout_ms,
retry_base_ms: cfg.retry.base_ms,
retry_max_ms: cfg.retry.max_ms,
retry_max_elapsed_ms: cfg.retry.max_elapsed_ms,
search_format_is_text: cfg.search.format == "text",
search_max_inflight_bytes: cfg.search.max_inflight_bytes,
extract_max_sample_rows: cfg.extract.max_sample_rows,
extract_max_text_chars: cfg.extract.max_text_chars,
extract_max_sheets: cfg.extract.max_sheets,
extract_max_slides: cfg.extract.max_slides,
summary_errors_max_entries: cfg.summary.errors_max_entries,
read_max_output_bytes: cfg.read.max_output_bytes,
read_number: cfg.read.number,
delete_backup: cfg.delete.backup,
output_lean: cfg.output.profile.eq_ignore_ascii_case("lean"),
}
}
}
impl Default for Tuning {
fn default() -> Self {
Self::from_config(&AtomwriteConfig::default())
}
}
static TUNING: std::sync::OnceLock<Tuning> = std::sync::OnceLock::new();
pub fn install_tuning(cfg: &AtomwriteConfig) {
if TUNING.set(Tuning::from_config(cfg)).is_err() {
tracing::debug!("tuning already installed; keeping the first value");
}
}
#[must_use]
pub fn read_max_output_bytes() -> usize {
tuning().read_max_output_bytes
}
#[must_use]
pub fn read_number_default() -> bool {
tuning().read_number
}
#[must_use]
pub fn delete_backup() -> Option<bool> {
tuning().delete_backup
}
#[must_use]
pub fn tuning() -> Tuning {
TUNING.get().copied().unwrap_or_default()
}
#[must_use]
pub fn summary_errors_max_entries() -> usize {
tuning().summary_errors_max_entries
}
struct GlobalText {
path: std::path::PathBuf,
text: Result<String, String>,
}
static GLOBAL_TEXT: std::sync::OnceLock<Option<GlobalText>> = std::sync::OnceLock::new();
fn global_text() -> Option<&'static GlobalText> {
GLOBAL_TEXT
.get_or_init(|| {
let path = crate::storage::global_config_path().filter(|p| p.is_file())?;
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string());
Some(GlobalText { path, text })
})
.as_ref()
}
struct Layer {
config: AtomwriteConfig,
table: toml::Table,
}
fn parse_layer(path: &Path, content: &str) -> Result<Layer, AtomwriteError> {
let config: AtomwriteConfig =
toml::from_str(content).map_err(|e| AtomwriteError::ConfigInvalid {
reason: format!("malformed config {}: {e}", path.display()),
})?;
if config.version > crate::constants::CONFIG_SCHEMA_VERSION {
return Err(AtomwriteError::ConfigInvalid {
reason: format!(
"config {} declares version {}; this build supports up to {}",
path.display(),
config.version,
crate::constants::CONFIG_SCHEMA_VERSION
),
});
}
let table: toml::Table =
toml::from_str(content).map_err(|e| AtomwriteError::ConfigInvalid {
reason: format!("malformed config {}: {e}", path.display()),
})?;
tracing::debug!(path = %path.display(), "loaded config");
Ok(Layer { config, table })
}
fn read_layer(path: &Path) -> Result<Layer, AtomwriteError> {
let content = std::fs::read_to_string(path).map_err(|e| AtomwriteError::ConfigInvalid {
reason: format!("cannot read config {}: {e}", path.display()),
})?;
parse_layer(path, &content)
}
fn merge_tables(mut base: toml::Table, over: toml::Table) -> toml::Table {
for (key, value) in over {
match (base.remove(&key), value) {
(Some(toml::Value::Table(b)), toml::Value::Table(o)) => {
base.insert(key, toml::Value::Table(merge_tables(b, o)));
}
(_, value) => {
base.insert(key, value);
}
}
}
base
}