diff --git a/crates/extension-schema/src/lib.rs b/crates/extension-schema/src/lib.rs index bba4b0c43..66465db6e 100644 --- a/crates/extension-schema/src/lib.rs +++ b/crates/extension-schema/src/lib.rs @@ -28,6 +28,7 @@ pub enum ExtensionKind { TargetPlugin, S3Hook, OpsDiagnostics, + OpsProfiler, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -191,6 +192,22 @@ pub struct OpsProfilerContract { pub backends: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OpsProfilerRuntimeSnapshot { + pub boundary: ExtensionRuntimeBoundary, + pub disabled_by_default: bool, + pub startup_fatal: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OpsProfilerCapabilitySnapshot { + pub capability: ExtensionCapabilityRef, + pub runtime: OpsProfilerRuntimeSnapshot, + pub contract: OpsProfilerContract, +} + #[derive(Debug, Error, PartialEq, Eq)] pub enum ExtensionSchemaError { #[error("extension schema at index {index} has an empty extension id")] @@ -279,6 +296,15 @@ pub enum ExtensionContractError { #[error("ops profiler backend {backend} has an empty collection boundary")] EmptyOpsProfilerCollectionBoundary { backend: String }, + + #[error("ops profiler snapshot has unsupported capability {capability}")] + UnsupportedOpsProfilerCapability { capability: String }, + + #[error("ops profiler external runtime must be disabled by default")] + OpsProfilerExternalRuntimeEnabledByDefault, + + #[error("ops profiler runtime snapshot cannot add a startup fatal boundary")] + OpsProfilerStartupFatalBoundary, } pub fn validate_extension_schemas(schemas: &[ExtensionSchema]) -> Result<(), ExtensionSchemaError> { @@ -459,15 +485,34 @@ pub fn validate_ops_profiler_contract(contract: &OpsProfilerContract) -> Result< Ok(()) } +pub fn validate_ops_profiler_capability_snapshot(snapshot: &OpsProfilerCapabilitySnapshot) -> Result<(), ExtensionContractError> { + if snapshot.capability.as_str() != OPS_PROFILER_CAPABILITY { + return Err(ExtensionContractError::UnsupportedOpsProfilerCapability { + capability: snapshot.capability.as_str().to_string(), + }); + } + + if snapshot.runtime.boundary.requires_disabled_by_default() && !snapshot.runtime.disabled_by_default { + return Err(ExtensionContractError::OpsProfilerExternalRuntimeEnabledByDefault); + } + + if snapshot.runtime.startup_fatal { + return Err(ExtensionContractError::OpsProfilerStartupFatalBoundary); + } + + validate_ops_profiler_contract(&snapshot.contract) +} + #[cfg(test)] mod tests { use super::{ EXTENSION_SCHEMA_VERSION, ExtensionCapabilityRef, ExtensionContractError, ExtensionKind, ExtensionRuntimeBoundary, ExtensionRuntimeContract, ExtensionSchema, ExtensionSchemaError, OPS_DIAGNOSTICS_CAPABILITY, OPS_PROFILER_CAPABILITY, OpsDiagnosticSurface, OpsDiagnosticsContract, OpsProfilerBackendCapability, OpsProfilerBackendName, - OpsProfilerBackendStatus, OpsProfilerContract, OpsProfilerContractMode, OpsProfilerProvenance, OpsProfilerRedactionField, - OpsProfilerTrustLevel, S3_POST_AUTH_HOOK_CAPABILITY, S3HookContract, S3HookPoint, validate_extension_schemas, - validate_ops_diagnostics_contract, validate_ops_profiler_contract, validate_s3_hook_contract, + OpsProfilerBackendStatus, OpsProfilerCapabilitySnapshot, OpsProfilerContract, OpsProfilerContractMode, + OpsProfilerProvenance, OpsProfilerRedactionField, OpsProfilerRuntimeSnapshot, OpsProfilerTrustLevel, + S3_POST_AUTH_HOOK_CAPABILITY, S3HookContract, S3HookPoint, validate_extension_schemas, validate_ops_diagnostics_contract, + validate_ops_profiler_capability_snapshot, validate_ops_profiler_contract, validate_s3_hook_contract, }; use serde_json::json; @@ -541,7 +586,7 @@ mod tests { display_name: "Ops Profiler".to_string(), provider: "rustfs".to_string(), version: "1.0.0".to_string(), - kind: ExtensionKind::OpsDiagnostics, + kind: ExtensionKind::OpsProfiler, runtime: ExtensionRuntimeContract { api_version: "rustfs.extension.v1".to_string(), boundary: ExtensionRuntimeBoundary::Builtin, @@ -803,6 +848,130 @@ mod tests { assert!(validate_ops_profiler_contract(&contract).is_ok()); } + #[test] + fn ops_profiler_capability_snapshot_preserves_runtime_states() { + let snapshot = OpsProfilerCapabilitySnapshot { + capability: ExtensionCapabilityRef::new(OPS_PROFILER_CAPABILITY), + runtime: OpsProfilerRuntimeSnapshot { + boundary: ExtensionRuntimeBoundary::Sidecar, + disabled_by_default: true, + startup_fatal: false, + }, + contract: OpsProfilerContract { + mode: OpsProfilerContractMode::CapabilityDescription, + backends: vec![ + profiler_backend("cpu_pprof", OpsProfilerBackendStatus::Enabled, true), + profiler_backend("memory_pprof", OpsProfilerBackendStatus::Disabled, false), + profiler_backend("ebpf", OpsProfilerBackendStatus::Unsupported, false), + ], + }, + }; + + assert!(validate_ops_profiler_capability_snapshot(&snapshot).is_ok()); + + let encoded = serde_json::to_string(&snapshot).expect("ops profiler snapshot should serialize"); + let decoded: OpsProfilerCapabilitySnapshot = + serde_json::from_str(&encoded).expect("ops profiler snapshot should deserialize"); + + let states: Vec<_> = decoded.contract.backends.iter().map(|backend| backend.status).collect(); + assert_eq!( + states, + vec![ + OpsProfilerBackendStatus::Enabled, + OpsProfilerBackendStatus::Disabled, + OpsProfilerBackendStatus::Unsupported, + ] + ); + assert_eq!(decoded.runtime.boundary, ExtensionRuntimeBoundary::Sidecar); + assert!(decoded.runtime.disabled_by_default); + assert!(!decoded.runtime.startup_fatal); + } + + #[test] + fn ops_profiler_capability_snapshot_serializes_stable_json_shape() { + let snapshot = OpsProfilerCapabilitySnapshot { + capability: ExtensionCapabilityRef::new(OPS_PROFILER_CAPABILITY), + runtime: OpsProfilerRuntimeSnapshot { + boundary: ExtensionRuntimeBoundary::Builtin, + disabled_by_default: false, + startup_fatal: false, + }, + contract: OpsProfilerContract { + mode: OpsProfilerContractMode::CapabilityDescription, + backends: vec![profiler_backend("cpu_pprof", OpsProfilerBackendStatus::Enabled, true)], + }, + }; + + let value = serde_json::to_value(snapshot).expect("ops profiler snapshot should serialize"); + + assert_eq!( + value, + json!({ + "capability": "ops.profiler.v1", + "runtime": { + "boundary": "builtin", + "disabled_by_default": false, + "startup_fatal": false + }, + "contract": { + "mode": "capability_description", + "backends": [{ + "backend": "cpu_pprof", + "status": "enabled", + "supports_profile_export": true, + "redaction_required": ["secret", "token", "local_path", "host"], + "provenance": { + "source": "rustfs.profiling", + "collection_boundary": "rustfs-process", + "trust_level": "runtime_trusted" + } + }] + } + }) + ); + } + + #[test] + fn rejects_ops_profiler_snapshot_wrong_capability_or_fatal_runtime() { + let mut snapshot = OpsProfilerCapabilitySnapshot { + capability: ExtensionCapabilityRef::new("ops.not-profiler.v1"), + runtime: OpsProfilerRuntimeSnapshot { + boundary: ExtensionRuntimeBoundary::Sidecar, + disabled_by_default: true, + startup_fatal: false, + }, + contract: OpsProfilerContract { + mode: OpsProfilerContractMode::CapabilityDescription, + backends: vec![profiler_backend("cpu_pprof", OpsProfilerBackendStatus::Enabled, true)], + }, + }; + + assert_eq!( + validate_ops_profiler_capability_snapshot(&snapshot).expect_err("only ops.profiler.v1 snapshots are accepted"), + ExtensionContractError::UnsupportedOpsProfilerCapability { + capability: "ops.not-profiler.v1".to_string() + } + ); + + snapshot.capability = ExtensionCapabilityRef::new(OPS_PROFILER_CAPABILITY); + snapshot.runtime.disabled_by_default = false; + + assert_eq!( + validate_ops_profiler_capability_snapshot(&snapshot) + .expect_err("external profiler runtimes must stay disabled by default"), + ExtensionContractError::OpsProfilerExternalRuntimeEnabledByDefault + ); + + snapshot.runtime.disabled_by_default = true; + snapshot.runtime.startup_fatal = true; + + assert_eq!( + validate_ops_profiler_capability_snapshot(&snapshot) + .expect_err("optional profiler runtimes must not become startup fatal"), + ExtensionContractError::OpsProfilerStartupFatalBoundary + ); + } + #[test] fn rejects_ops_profiler_execution_requests_and_empty_backends() { let mut contract = OpsProfilerContract { diff --git a/docs/architecture/migration-progress.md b/docs/architecture/migration-progress.md index 31f4a9835..03295f1e3 100644 --- a/docs/architecture/migration-progress.md +++ b/docs/architecture/migration-progress.md @@ -5,15 +5,15 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block ## Current Context - Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660) -- Branch: `overtrue/arch-ops-profiler-schema` +- Branch: `overtrue/arch-ops-profiler-capability-tests` - Baseline: latest `origin/main` (`71809ba02ea405c3a2f0e4cb82ff608915dd519c`). - PR type for this branch: `contract` - Runtime behavior changes: none. -- Rust code changes: define the `ops.profiler.v1` extension schema contract for - profiler capability reporting, backend state, redaction, and provenance. +- Rust code changes: add the `ops.profiler.v1` extension capability snapshot + contract for disabled, unsupported, and enabled profiler runtime states. - CI/script changes: none. -- Docs changes: record the X-012 profiler schema contract slice. +- Docs changes: record the X-013 profiler capability snapshot slice. ## Phase 0 Tasks @@ -2000,20 +2000,36 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block guards, diff hygiene, Rust risk scan, branch freshness check, and pre-commit quality gate. +- [x] `X-013` Add ops profiler capability snapshot contract. + - Do: add `OpsProfilerCapabilitySnapshot` and `OpsProfilerRuntimeSnapshot` + DTOs plus validation for the `ops.profiler.v1` capability, disabled + external runtimes, and non-fatal profiler startup behavior. + - Acceptance: disabled, unsupported, and enabled profiler backend states + round-trip through the snapshot contract; sidecar/Wasm profiler runtimes + remain disabled by default; profiler snapshots cannot declare a startup + fatal boundary. + - Must preserve: no plugin execution, no sidecar startup, no profile route, + no admin API behavior changes, no runtime startup/shutdown behavior + changes, and no dependency edge from `extension-schema` to runtime or + storage implementation crates. + - Verification: extension schema check/tests, formatting, migration/layer + guards, diff hygiene, Rust risk scan, branch freshness check, and + pre-commit quality gate. + ## Next PRs -1. `contract`: continue larger extension contract coverage for disabled and - unsupported runtime capability snapshots. -2. `pure-move`: continue larger lifecycle hook slices for optional runtime +1. `pure-move`: continue larger lifecycle hook slices for optional runtime sidecars while preserving startup and shutdown ordering. +2. `contract`: continue extension contract coverage for future diagnostics and + profiler handoff surfaces after runtime owners are stable. ## Pre-Push Review Log | Expert | Status | Notes | |---|---|---| -| Quality/architecture | passed | X-012 keeps the profiler schema in `rustfs-extension-schema`, uses explicit backend/status/redaction/provenance DTOs, and adds no new dependency edges or runtime abstractions. | -| Migration preservation | passed | Runtime profiling, admin pprof routes, exporters, storage paths, telemetry, sidecar startup, and plugin execution are untouched; the new contract only describes capabilities. | -| Testing/verification | passed | Extension schema tests cover enabled, disabled, unsupported, unknown, missing-field, redaction, provenance, and credential-field rejection; focused checks, guards, formatting, diff hygiene, and final pre-commit passed. | +| Quality/architecture | passed | X-013 keeps the capability snapshot contract in `rustfs-extension-schema`, reuses explicit profiler backend/runtime DTOs, and adds no runtime dependency edges. | +| Migration preservation | passed | Runtime profiling, admin pprof routes, exporters, storage paths, telemetry, sidecar startup, and plugin execution are untouched; validation only rejects unsafe snapshot declarations. | +| Testing/verification | passed | Extension schema tests cover enabled, disabled, unsupported, snapshot JSON shape, disabled external runtime policy, and startup-fatal rejection; focused checks, guards, formatting, diff hygiene, and final pre-commit passed. | ## Verification Notes @@ -2030,6 +2046,17 @@ Passed before push: - `make pre-commit`: passed. - Three-expert review: passed. +- Issue #660 X-013 current slice: + - `cargo test -p rustfs-extension-schema`: passed. + - `cargo check -p rustfs-extension-schema`: passed. + - `./scripts/check_architecture_migration_rules.sh`: passed. + - `./scripts/check_layer_dependencies.sh`: passed. + - `cargo fmt --all --check`: passed. + - `git diff --check`: passed. + - Rust risk scan on changed Rust files: passed. + - `make pre-commit`: passed. + - Three-expert review: passed. + - Issue #660 R-020 current slice: - `cargo test -p rustfs --lib startup_profiling -- --nocapture`: passed. - `cargo check -p rustfs --lib`: passed. diff --git a/docs/architecture/profiling-numa-capability-inventory.md b/docs/architecture/profiling-numa-capability-inventory.md index 682c22227..fea336701 100644 --- a/docs/architecture/profiling-numa-capability-inventory.md +++ b/docs/architecture/profiling-numa-capability-inventory.md @@ -75,9 +75,10 @@ Future sidecars for profiling, eBPF, or NUMA must preserve these invariants: `X-013`: -- Add extension tests for disabled, unsupported, and enabled capability - snapshots. -- Verify no startup fatal boundary is added for optional profiling sidecars. +- Add the extension capability snapshot contract for disabled, unsupported, and + enabled profiler backends. +- Verify optional profiler sidecar and Wasm runtimes stay disabled by default + and cannot declare a startup fatal boundary. `R-017`: