-
Notifications
You must be signed in to change notification settings - Fork 1.3k
refactor(config): read ForgeConfig once at startup and thread it through the stack #2850
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
fed4cd1
8745692
0eb1a98
585dab0
c179aca
53221d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ use std::sync::LazyLock; | |
|
|
||
| use config::ConfigBuilder; | ||
| use config::builder::DefaultState; | ||
| use tracing::warn; | ||
|
|
||
| use crate::ForgeConfig; | ||
| use crate::legacy::LegacyConfig; | ||
|
|
@@ -101,24 +102,36 @@ impl ConfigReader { | |
| Ok(config.try_deserialize::<ForgeConfig>()?) | ||
| } | ||
|
|
||
| /// Adds `~/.forge/.forge.toml` as a config source, silently skipping if | ||
| /// absent. | ||
| /// Adds `~/.forge/.forge.toml` as a config source. | ||
| /// | ||
| /// Silently skips if the file does not exist; returns an error if the file | ||
| /// exists but cannot be parsed (e.g., malformed TOML, wrong value types). | ||
| pub fn read_global(mut self) -> Self { | ||
| let path = Self::config_path(); | ||
| self.builder = self | ||
| .builder | ||
| .add_source(config::File::from(path).required(false)); | ||
| // Only add the source if the file exists so that a missing config file | ||
| // is silently skipped, while a present-but-malformed file surfaces as | ||
| // an error during `build()`. | ||
| if path.exists() { | ||
| self.builder = self | ||
| .builder | ||
| .add_source(config::File::from(path).required(true)); | ||
| } | ||
| self | ||
| } | ||
|
|
||
| /// Reads `~/.forge/.config.json` (legacy format) and adds it as a source, | ||
| /// silently skipping errors. | ||
| /// Reads `~/.forge/.config.json` (legacy format) and adds it as a source. | ||
| /// | ||
| /// Silently skips if the file does not exist. Emits a `warn!` log if the | ||
| /// file exists but cannot be parsed, so the user is informed without | ||
| /// aborting startup. | ||
| pub fn read_legacy(self) -> Self { | ||
| let content = LegacyConfig::read(&Self::config_legacy_path()); | ||
| if let Ok(content) = content { | ||
| self.read_toml(&content) | ||
| } else { | ||
| self | ||
| match content { | ||
| Ok(content) => self.read_toml(&content), | ||
| Err(e) => { | ||
| warn!(error = ?e, "Failed to read legacy config file; skipping"); | ||
| self | ||
| } | ||
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,18 +2,18 @@ use std::collections::BTreeMap; | |||||||||||||||||||||||||||||||||||
| use std::path::PathBuf; | ||||||||||||||||||||||||||||||||||||
| use std::sync::Arc; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| use forge_app::EnvironmentInfra; | ||||||||||||||||||||||||||||||||||||
| use forge_app::{ConfigReaderInfra, EnvironmentInfra}; | ||||||||||||||||||||||||||||||||||||
| use forge_config::{ConfigReader, ForgeConfig, ModelConfig}; | ||||||||||||||||||||||||||||||||||||
| use forge_domain::{ConfigOperation, Environment}; | ||||||||||||||||||||||||||||||||||||
| use tracing::{debug, error}; | ||||||||||||||||||||||||||||||||||||
| use tracing::debug; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /// Builds a [`forge_domain::Environment`] from runtime context only. | ||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||
| /// Only the six fields that cannot be sourced from [`ForgeConfig`] are set | ||||||||||||||||||||||||||||||||||||
| /// here: `os`, `pid`, `cwd`, `home`, `shell`, and `base_path`. All | ||||||||||||||||||||||||||||||||||||
| /// configuration values are now accessed through | ||||||||||||||||||||||||||||||||||||
| /// `EnvironmentInfra::get_config()`. | ||||||||||||||||||||||||||||||||||||
| fn to_environment(cwd: PathBuf) -> Environment { | ||||||||||||||||||||||||||||||||||||
| pub fn to_environment(cwd: PathBuf) -> Environment { | ||||||||||||||||||||||||||||||||||||
| Environment { | ||||||||||||||||||||||||||||||||||||
| os: std::env::consts::OS.to_string(), | ||||||||||||||||||||||||||||||||||||
| pid: std::process::id(), | ||||||||||||||||||||||||||||||||||||
|
|
@@ -97,39 +97,36 @@ pub struct ForgeEnvironmentInfra { | |||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| impl ForgeEnvironmentInfra { | ||||||||||||||||||||||||||||||||||||
| /// Creates a new [`ForgeEnvironmentInfra`]. | ||||||||||||||||||||||||||||||||||||
| /// Creates a new [`ForgeEnvironmentInfra`] with the given pre-read config. | ||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||
| /// The cache is pre-seeded with `config` so no disk I/O occurs on the | ||||||||||||||||||||||||||||||||||||
| /// first [`EnvironmentInfra::get_config`] call. | ||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||
| /// # Arguments | ||||||||||||||||||||||||||||||||||||
| /// * `cwd` - The working directory path; used to resolve `.env` files | ||||||||||||||||||||||||||||||||||||
| pub fn new(cwd: PathBuf) -> Self { | ||||||||||||||||||||||||||||||||||||
| Self { cwd, cache: Arc::new(std::sync::Mutex::new(None)) } | ||||||||||||||||||||||||||||||||||||
| /// * `config` - The pre-read [`ForgeConfig`] to seed the in-memory cache | ||||||||||||||||||||||||||||||||||||
| pub fn new(cwd: PathBuf, config: ForgeConfig) -> Self { | ||||||||||||||||||||||||||||||||||||
| Self { cwd, cache: Arc::new(std::sync::Mutex::new(Some(config))) } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /// Reads [`ForgeConfig`] from disk via [`ForgeConfig::read`]. | ||||||||||||||||||||||||||||||||||||
| fn read_from_disk() -> ForgeConfig { | ||||||||||||||||||||||||||||||||||||
| match ForgeConfig::read() { | ||||||||||||||||||||||||||||||||||||
| Ok(config) => { | ||||||||||||||||||||||||||||||||||||
| debug!(config = ?config, "read .forge.toml"); | ||||||||||||||||||||||||||||||||||||
| config | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| Err(e) => { | ||||||||||||||||||||||||||||||||||||
| // NOTE: This should never-happen | ||||||||||||||||||||||||||||||||||||
| error!(error = ?e, "Failed to read config file. Using default config."); | ||||||||||||||||||||||||||||||||||||
| Default::default() | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /// Returns the cached [`ForgeConfig`], reading from disk if the cache is | ||||||||||||||||||||||||||||||||||||
| /// empty. | ||||||||||||||||||||||||||||||||||||
| fn cached_config(&self) -> ForgeConfig { | ||||||||||||||||||||||||||||||||||||
| /// Returns the cached [`ForgeConfig`], re-reading from disk if the cache | ||||||||||||||||||||||||||||||||||||
| /// has been invalidated by [`Self::update_environment`]. | ||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||
| /// # Errors | ||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||
| /// Returns an error if the cache is empty and the disk read fails. | ||||||||||||||||||||||||||||||||||||
| fn cached_config(&self) -> anyhow::Result<ForgeConfig> { | ||||||||||||||||||||||||||||||||||||
| let mut cache = self.cache.lock().expect("cache mutex poisoned"); | ||||||||||||||||||||||||||||||||||||
| if let Some(ref config) = *cache { | ||||||||||||||||||||||||||||||||||||
| config.clone() | ||||||||||||||||||||||||||||||||||||
| Ok(config.clone()) | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| let config = Self::read_from_disk(); | ||||||||||||||||||||||||||||||||||||
| let config = ConfigReader::default() | ||||||||||||||||||||||||||||||||||||
| .read_defaults() | ||||||||||||||||||||||||||||||||||||
| .read_global() | ||||||||||||||||||||||||||||||||||||
| .read_env() | ||||||||||||||||||||||||||||||||||||
| .build()?; | ||||||||||||||||||||||||||||||||||||
| *cache = Some(config.clone()); | ||||||||||||||||||||||||||||||||||||
| config | ||||||||||||||||||||||||||||||||||||
| Ok(config) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
@@ -149,10 +146,6 @@ impl EnvironmentInfra for ForgeEnvironmentInfra { | |||||||||||||||||||||||||||||||||||
| to_environment(self.cwd.clone()) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| fn get_config(&self) -> ForgeConfig { | ||||||||||||||||||||||||||||||||||||
| self.cached_config() | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| async fn update_environment(&self, ops: Vec<ConfigOperation>) -> anyhow::Result<()> { | ||||||||||||||||||||||||||||||||||||
| // Load the global config (with defaults applied) for the update round-trip | ||||||||||||||||||||||||||||||||||||
| let mut fc = ConfigReader::default() | ||||||||||||||||||||||||||||||||||||
|
|
@@ -169,13 +162,20 @@ impl EnvironmentInfra for ForgeEnvironmentInfra { | |||||||||||||||||||||||||||||||||||
| fc.write()?; | ||||||||||||||||||||||||||||||||||||
| debug!(config = ?fc, "written .forge.toml"); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Reset cache | ||||||||||||||||||||||||||||||||||||
| // Reset cache so next get_config() re-reads the updated values from disk | ||||||||||||||||||||||||||||||||||||
| *self.cache.lock().expect("cache mutex poisoned") = None; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| impl ConfigReaderInfra for ForgeEnvironmentInfra { | ||||||||||||||||||||||||||||||||||||
| fn get_config(&self) -> ForgeConfig { | ||||||||||||||||||||||||||||||||||||
| self.cached_config() | ||||||||||||||||||||||||||||||||||||
| .expect("ForgeConfig cache read failed after update_environment") | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
| impl ConfigReaderInfra for ForgeEnvironmentInfra { | |
| fn get_config(&self) -> ForgeConfig { | |
| self.cached_config() | |
| .expect("ForgeConfig cache read failed after update_environment") | |
| } | |
| } | |
| impl ConfigReaderInfra for ForgeEnvironmentInfra { | |
| fn get_config(&self) -> ForgeConfig { | |
| self.cached_config() | |
| .unwrap_or_else(|e| { | |
| tracing::error!(error = ?e, "Failed to read config after update; using defaults"); | |
| ForgeConfig::default() | |
| }) | |
| } | |
| } | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Completely drop ConfigInfra. We will never read config infra dynamically ever. Make sure to pass an instance of
ForgeConfigin<TypeName>::new()constructor wherever required.