Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 52 additions & 34 deletions crates/tracedecay-configuration/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ pub mod work_executable_binding;

pub use tracedecay_global_db::configuration::{registry, resolver};

use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use std::sync::{Arc, OnceLock};

use tracedecay_contracts::{ConfigurationSettingActionV1, ConfigurationSettingFindingV1};
use tracedecay_domain::configuration::{
ConfigurationRevisionId, ConfigurationSnapshotV1, ConfigurationValueV1,
DIAGNOSTICS_PREWARM_SETTING_KEY, INDEX_EXCLUDE_SETTING_KEY,
Expand Down Expand Up @@ -256,8 +256,8 @@ fn runtime_config_from_snapshot(
})?;
Ok(RuntimeTraceDecayConfig {
index_paths: IndexPathPolicyV1::new(
compilable_index_patterns(snapshot, INDEX_EXCLUDE_SETTING_KEY)?,
compilable_index_patterns(snapshot, INDEX_INCLUDE_SETTING_KEY)?,
applied_index_patterns(snapshot, INDEX_EXCLUDE_SETTING_KEY)?,
applied_index_patterns(snapshot, INDEX_INCLUDE_SETTING_KEY)?,
)
.map_err(|error| config_error(format!("resolved index path policy is invalid: {error}")))?,
max_file_size: required_unsigned(snapshot, INDEX_MAX_FILE_SIZE_SETTING_KEY)?,
Expand Down Expand Up @@ -377,40 +377,58 @@ pub fn required_string_list(
}
}

/// The stored index path patterns that still compile. A new write is refused
/// up front, but an earlier release persisted patterns it never compiled, so
/// one that no longer compiles is skipped with a warning instead of failing
/// the whole runtime configuration and with it every tool.
/// Splits a stored index path pattern list into the patterns indexing
/// applies and a finding for each one that does not compile.
///
/// The pin is rebuilt on every `current()` read, so the warning is logged
/// once per setting and pattern for the process, not once per tool call.
fn compilable_index_patterns(
/// Writes refuse such a pattern, but an earlier release stored patterns it
/// never compiled. Refusing the whole pin would also refuse the `set` and
/// `unset` that repair it, so the pin applies the rest and
/// [`setting_findings`] reports each skipped pattern.
fn partition_index_patterns(
patterns: Vec<String>,
) -> (Vec<String>, Vec<ConfigurationSettingFindingV1>) {
let mut applied = Vec::with_capacity(patterns.len());
let mut findings = Vec::new();
for pattern in patterns {
match validate_index_path_patterns(std::slice::from_ref(&pattern)) {
Ok(()) => applied.push(pattern),
Err(error) => findings.push(ConfigurationSettingFindingV1::InvalidIndexPathPattern {
pattern,
message: error.message,
legal_actions: vec![
ConfigurationSettingActionV1::Set,
ConfigurationSettingActionV1::Unset,
],
}),
}
}
(applied, findings)
}

fn applied_index_patterns(
snapshot: &ConfigurationSnapshotV1,
key_name: &str,
) -> Result<Vec<String>> {
static WARNED: OnceLock<Mutex<HashSet<(String, String)>>> = OnceLock::new();
let mut patterns = required_string_list(snapshot, key_name)?;
patterns.retain(
|pattern| match validate_index_path_patterns(std::slice::from_ref(pattern)) {
Ok(()) => true,
Err(error) => {
let first = WARNED
.get_or_init(Mutex::default)
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert((key_name.to_owned(), pattern.clone()));
if first {
tracing::warn!(
setting = key_name,
%error,
"skipping a stored index path pattern that no longer compiles"
);
}
false
}
},
);
Ok(patterns)
Ok(partition_index_patterns(required_string_list(snapshot, key_name)?).0)
}

/// The parts of a stored `value` for `key` that the runtime configuration
/// does not apply.
pub fn setting_findings(
key: &SettingKey,
value: &ConfigurationValueV1,
) -> Vec<ConfigurationSettingFindingV1> {
match value {
ConfigurationValueV1::StringList(patterns)
if matches!(
key.as_str(),
INDEX_EXCLUDE_SETTING_KEY | INDEX_INCLUDE_SETTING_KEY
) =>
{
partition_index_patterns(patterns.clone()).1
}
_ => Vec::new(),
}
}

fn required_lcm_summarizer_executables(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::config::registry::ConfigurationRegistry;
use crate::config::scope_control::{
ProtectedChangePlanDraftV1, plan_protected_change, validate_apply_binding,
};
use crate::config::setting_findings;
use tracedecay_global_db::configuration::contracts::ports::{
ConfigurationControlStore, ConfigurationMutationAuthorizationPort,
ConfigurationOperationFuture, CurrentConfigurationMutationAuthorizationV1, ScopeResolutionPort,
Expand Down Expand Up @@ -163,6 +164,7 @@ where
.cloned()
.unwrap_or_else(|| definition.default_value.clone());
Ok(ResolvedSetting {
findings: setting_findings(&key, &effective_value),
key: key.clone(),
effective_value,
revision_id: current.revision_id,
Expand Down
25 changes: 25 additions & 0 deletions crates/tracedecay-contracts/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ pub struct ResolvedSetting {
pub effective_behavior_digest: ManifestDigest,
pub resolution_provenance_digest: ManifestDigest,
pub candidates: Vec<ConfigurationCandidateV1>,
/// Parts of the stored value this release does not apply. Empty when the
/// effective value applies in full.
pub findings: Vec<ConfigurationSettingFindingV1>,
}

/// A stored value the running release keeps but does not apply. The value
/// stays readable so `legal_actions` on the same key can replace it.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum ConfigurationSettingFindingV1 {
/// An `index.exclude.v1` / `index.include.v1` pattern that does not
/// compile. An earlier release stored it unchecked; indexing runs
/// without it.
InvalidIndexPathPattern {
pattern: String,
message: String,
legal_actions: Vec<ConfigurationSettingActionV1>,
},
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ConfigurationSettingActionV1 {
Set,
Unset,
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
Expand Down
10 changes: 5 additions & 5 deletions crates/tracedecay-contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ pub use configuration::{
ConfigurationGetRequestV1, ConfigurationListRequestV1, ConfigurationMutationReceipt,
ConfigurationObservedStateRequestV1, ConfigurationProtectedApplyRequestV1,
ConfigurationProtectedPreviewRequestV1, ConfigurationRollbackPreviewRequestV1,
ConfigurationSetRequestV1, ConfigurationUnsetRequestV1, ConfigurationWireRequestV1,
ResolvedSetting, SettingSummary, configuration_surface_catalog_contribution,
configuration_surface_handler_descriptors, configuration_surface_operation,
configuration_surface_request_schema, configuration_surface_result_schema,
configuration_wire_request_from_invocation_payload,
ConfigurationSetRequestV1, ConfigurationSettingActionV1, ConfigurationSettingFindingV1,
ConfigurationUnsetRequestV1, ConfigurationWireRequestV1, ResolvedSetting, SettingSummary,
configuration_surface_catalog_contribution, configuration_surface_handler_descriptors,
configuration_surface_operation, configuration_surface_request_schema,
configuration_surface_result_schema, configuration_wire_request_from_invocation_payload,
};
pub use context::{
APPLICATION_REQUEST_ID_HEADER, ApplicationRequestControlV1, CancellationContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! profile's own `ProfileSessions` store owns. No project is opened or named.

use super::*;
use tracedecay_configuration::config::setting_findings;
use tracedecay_domain::configuration::{
ConfigurationLayerIdV1, SettingKey, USER_CODE_INDEX_WORKERS_SETTING_KEY, UserProfileId,
};
Expand Down Expand Up @@ -116,6 +117,7 @@ async fn profile_configuration_outcome(
.cloned()
.ok_or(ConfigurationError::Unavailable)?;
let setting = ResolvedSetting {
findings: setting_findings(&request.key, &effective_value),
candidates: current
.snapshot
.provenance
Expand Down
4 changes: 2 additions & 2 deletions crates/tracedecay-global-db/src/configuration/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,8 @@ impl ConfigurationRegistry {
/// Stored snapshots are revalidated with `validate_value` on every read,
/// so a rule that may tighten across releases must not live there: an
/// `index.exclude.v1` pattern an earlier release accepted has to keep
/// loading (the runtime skips what no longer compiles) while a new
/// write of it is refused.
/// loading (the runtime pin leaves it unapplied and reports it as a
/// setting finding) while a new write of it is refused.
pub fn validate_written_value(
&self,
key: &SettingKey,
Expand Down
107 changes: 104 additions & 3 deletions crates/tracedecay-project/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,9 @@ mod runtime_configuration_cutover {
ConfigurationIdempotencyKey, ConfigurationLayerIdV1, ConfigurationMutationEffectV1,
ConfigurationMutationGrantReceiptV1, ConfigurationMutationOperationV1,
ConfigurationMutationSinkV1, ConfigurationRevisionId, ConfigurationValueV1,
DIAGNOSTICS_PREWARM_SETTING_KEY, INDEX_NATIVE_GRAPH_ACTIVATION_SETTING_KEY,
SOURCE_BINDINGS_SETTING_KEY, SYNC_AUTO_WATCH_SETTING_KEY, ScopeSourceBinding, SettingKey,
SourceBindingId, SourceKindV1,
DIAGNOSTICS_PREWARM_SETTING_KEY, INDEX_EXCLUDE_SETTING_KEY,
INDEX_NATIVE_GRAPH_ACTIVATION_SETTING_KEY, SOURCE_BINDINGS_SETTING_KEY,
SYNC_AUTO_WATCH_SETTING_KEY, ScopeSourceBinding, SettingKey, SourceBindingId, SourceKindV1,
};
use tracedecay_domain::{AccessPolicyDigest, ActorId, ProjectId, UtcMicros};

Expand All @@ -244,6 +244,8 @@ mod runtime_configuration_cutover {
use tracedecay_configuration::ProjectConfigurationRuntime;
use tracedecay_configuration::SyncConfig;
use tracedecay_configuration::config::PinnedRuntimeConfiguration;
use tracedecay_global_db::configuration::GlobalDbConfigurationControlStore;
use tracedecay_global_db::configuration::contracts::types::AuthorizedActor;
use tracedecay_global_db::configuration::contracts::{
ConfigurationControlStore, ConfigurationMutationAuthority, DirectConfigurationMutation,
};
Expand Down Expand Up @@ -504,6 +506,105 @@ mod runtime_configuration_cutover {
assert_eq!(runtime.configuration_target(), current.target());
}

/// A release before write-time pattern checks stored `index.exclude.v1`
/// patterns it never compiled. Opening such a store keeps the
/// configuration, and so its repair, available; the stored pattern is left
/// unapplied and `get` names it with the actions that clear it.
#[tokio::test]
async fn a_stored_uncompilable_index_pattern_is_a_typed_setting_finding() {
let profile = TempDir::new().expect("temporary profile root");
let root = TempDir::new().expect("temporary project root");
let project_id = project_id("project.configuration-uncompilable-exclude");
tracedecay_runtime_core::storage::pin_fixture_repository_identity(
root.path(),
project_id.as_str(),
)
.expect("write enrollment marker");
let layout = tracedecay_runtime_core::storage::resolve_layout(root.path(), profile.path())
.expect("resolve store layout");
std::fs::create_dir_all(&layout.data_root).expect("create data root");
let host_runtime =
HostAdmissionTestRuntimeV1::project(profile.path(), root.path(), project_id.clone())
.await
.expect("open retained project runtime");
let database = host_runtime
.registered_database_arc(tracedecay_sessions::admission::HostAdmissionScope::Project)
.expect("bind registered project database");
crate::config::install_usecase_runtime_configuration_authority()
.expect("install the root runtime configuration read ports");

let target = crate::config::runtime_configuration_target_for_layout(root.path(), &layout)
.expect("configuration target");
let exclude = SettingKey::new(INDEX_EXCLUDE_SETTING_KEY).unwrap();
let revision = revision_id("configuration.revision.unchecked-exclude");
let stored = resolve_configuration(
&ConfigurationRegistry::core().unwrap(),
&[ConfigurationLayerV1 {
layer: ConfigurationLayerIdV1::Project {
project_id: project_id.clone(),
},
revision_id: revision.clone(),
entries: BTreeMap::from([
(
SettingKey::new(SOURCE_BINDINGS_SETTING_KEY).unwrap(),
ConfigurationValueV1::SourceBindings(vec![
crate::config::daemon_project_source_binding(&target).unwrap(),
]),
),
(
exclude.clone(),
ConfigurationValueV1::StringList(vec![
"src/[abc".to_owned(),
"docs/**".to_owned(),
]),
),
]),
}],
)
.expect("read-time validation admits the stored pattern");
GlobalDbConfigurationControlStore::new_registered(database.as_ref())
.initialize_canonical(&revision, &stored, UtcMicros(1))
.await
.expect("seed the store as the earlier release left it");

let (_, opened) = crate::config::open_runtime_configuration_for_registered_database(
root.path(),
&layout,
database,
)
.await
.expect("an unapplied pattern must not refuse the configuration")
.into_parts();
let (runtime, pinned) =
ProjectConfigurationRuntime::open(opened).expect("open project configuration runtime");
assert_eq!(pinned.config().index_paths.exclude_patterns(), ["docs/**"]);

let setting = runtime
.client()
.get(
AuthorizedActor {
actor_id: ActorId::new("actor.configuration-uncompilable-exclude").unwrap(),
},
exclude,
)
.await
.expect("the stored setting stays readable");
let setting = serde_json::to_value(setting).unwrap();
assert_eq!(
setting["effective_value"],
serde_json::json!({"kind": "string_list", "value": ["src/[abc", "docs/**"]})
);
assert_eq!(
setting["findings"],
serde_json::json!([{
"kind": "invalid_index_path_pattern",
"pattern": "src/[abc",
"message": "unclosed character class; missing ']'",
"legal_actions": ["set", "unset"],
}])
);
}

/// One real journey over the production read surfaces: open (as the
/// lifecycle does), cached reads through the root cache, the lower cache
/// port, and the dashboard read port, a committed configuration change
Expand Down
Loading
Loading