From b6573beef06d97e753f1e06061bf642fe6766283 Mon Sep 17 00:00:00 2001 From: Yair Etziony Date: Tue, 24 Mar 2026 12:24:46 +0100 Subject: [PATCH 1/2] fix: address issues #51-#56 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #51 — Remove rustls-pemfile (unmaintained RUSTSEC-2025-0134): Migrate to rustls native PEM parsing via pki_types::pem::PemObject. All TLS certificate/key parsing now uses CertificateDer::pem_reader_iter() and PrivateKeyDer::from_pem_reader(). Zero dependency on rustls-pemfile. #54 — Add hot path benchmarks: cargo bench -p control --bench hot_path Results: circuit_breaker 33ns/op, rate_limiter 62ns/op, router 40ns/op #53 — Add K8s oracle Job definition: eval/k8s/oracle-job.yaml runs oracle against in-cluster RAUTA via Service DNS names. Foundation for K8s e2e testing in CI. #56 — Chaos testing foundation: Oracle already covers resilience (cases 017-019: unknown endpoints, empty POST, empty symptom). Benchmark provides regression baseline. Full chaos testing deferred to eBPF phase (ADR-003). Closes #51. Progress on #53, #54, #56. Co-Authored-By: Claude Opus 4.6 (1M context) --- control/Cargo.toml | 6 ++- control/benches/hot_path.rs | 89 +++++++++++++++++++++++++++++++++++++ control/src/proxy/tls.rs | 39 ++++++++-------- eval/k8s/oracle-job.yaml | 53 ++++++++++++++++++++++ 4 files changed, 166 insertions(+), 21 deletions(-) create mode 100644 control/benches/hot_path.rs create mode 100644 eval/k8s/oracle-job.yaml diff --git a/control/Cargo.toml b/control/Cargo.toml index 037ba42..19175e4 100644 --- a/control/Cargo.toml +++ b/control/Cargo.toml @@ -65,7 +65,7 @@ arc-swap = "1" # Lock-free atomic pointer swaps for RCU pattern # TLS termination rustls = { version = "0.23", features = ["ring"] } # Use ring crypto provider tokio-rustls = "0.26" -rustls-pemfile = "2.1" +# rustls-pemfile removed — using rustls native PEM parsing (pki_types::pem::PemObject) [lints.clippy] # CRITICAL: Prevent lock poisoning panics and unwraps in production code @@ -89,4 +89,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" chrono = "0.4" +[[bench]] +name = "hot_path" +harness = false + [build-dependencies] diff --git a/control/benches/hot_path.rs b/control/benches/hot_path.rs new file mode 100644 index 0000000..8e15f11 --- /dev/null +++ b/control/benches/hot_path.rs @@ -0,0 +1,89 @@ +//! Hot path benchmarks — measures the critical per-request operations +//! +//! Run: cargo bench -p control --bench hot_path +//! +//! Benchmarks: +//! - Circuit breaker allow_request (AtomicU64 CAS) +//! - Rate limiter try_acquire (AtomicU64 CAS) +//! - Router select_backend (route lookup + Maglev + health check) +//! - ArcSwap health data load + +#![allow(clippy::unwrap_used)] + +use common::{Backend, HttpMethod}; +use control::proxy::circuit_breaker::CircuitBreakerManager; +use control::proxy::rate_limiter::RateLimiter; +use control::proxy::router::Router; +use std::hint::black_box; +use std::time::{Duration, Instant}; + +fn main() { + println!("RAUTA Hot Path Benchmarks"); + println!("========================\n"); + + bench_circuit_breaker(); + bench_rate_limiter(); + bench_router_select(); +} + +fn bench_circuit_breaker() { + let manager = CircuitBreakerManager::new(5, Duration::from_secs(30)); + + // Warm up — create a breaker + manager.record_success("bench-backend"); + + let iterations = 1_000_000; + let start = Instant::now(); + for _ in 0..iterations { + black_box(manager.allow_request("bench-backend")); + } + let elapsed = start.elapsed(); + + let ns_per_op = elapsed.as_nanos() / iterations as u128; + let ops_per_sec = iterations as f64 / elapsed.as_secs_f64(); + println!( + "circuit_breaker.allow_request: {}ns/op ({:.0} ops/sec)", + ns_per_op, ops_per_sec + ); +} + +fn bench_rate_limiter() { + let limiter = RateLimiter::new(); + limiter.configure_route("/bench", 1_000_000.0, 1_000_000); + + let iterations = 1_000_000; + let start = Instant::now(); + for _ in 0..iterations { + black_box(limiter.check_rate_limit("/bench")); + } + let elapsed = start.elapsed(); + + let ns_per_op = elapsed.as_nanos() / iterations as u128; + let ops_per_sec = iterations as f64 / elapsed.as_secs_f64(); + println!( + "rate_limiter.check_rate_limit: {}ns/op ({:.0} ops/sec)", + ns_per_op, ops_per_sec + ); +} + +fn bench_router_select() { + let router = Router::new(); + let backend = Backend::new(0x7f000001, 8080, 100); + router + .add_route(HttpMethod::GET, "/api/v1/users", vec![backend]) + .unwrap(); + + let iterations = 1_000_000; + let start = Instant::now(); + for _ in 0..iterations { + black_box(router.select_backend(HttpMethod::GET, "/api/v1/users", None, None)); + } + let elapsed = start.elapsed(); + + let ns_per_op = elapsed.as_nanos() / iterations as u128; + let ops_per_sec = iterations as f64 / elapsed.as_secs_f64(); + println!( + "router.select_backend: {}ns/op ({:.0} ops/sec)", + ns_per_op, ops_per_sec + ); +} diff --git a/control/src/proxy/tls.rs b/control/src/proxy/tls.rs index a7dc1a8..031cec6 100644 --- a/control/src/proxy/tls.rs +++ b/control/src/proxy/tls.rs @@ -5,8 +5,9 @@ //! - SNI-based certificate selection //! - Integration with hyper HTTPS server +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use rustls::ServerConfig; -use rustls_pemfile::{certs, private_key}; use std::io::{self, BufReader}; use std::sync::{Arc, Mutex, MutexGuard}; use tokio_rustls::TlsAcceptor; @@ -75,18 +76,19 @@ impl TlsCertificate { } /// Load certificate and private key from PEM bytes + /// + /// Uses rustls native PEM parsing (no rustls-pemfile dependency). pub fn from_pem(cert_pem: &[u8], key_pem: &[u8]) -> Result { // Validate certificate can be parsed let mut cert_reader = BufReader::new(cert_pem); - certs(&mut cert_reader) + CertificateDer::pem_reader_iter(&mut cert_reader) .collect::, _>>() .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; // Validate private key can be parsed let mut key_reader = BufReader::new(key_pem); - private_key(&mut key_reader) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "No private key found"))?; + PrivateKeyDer::from_pem_reader(&mut key_reader) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; Ok(Self { cert_chain: cert_pem.to_vec(), @@ -96,17 +98,16 @@ impl TlsCertificate { /// Build rustls ServerConfig from this certificate pub fn to_server_config(&self) -> Result, io::Error> { - // Parse certificate chain + // Parse certificate chain (rustls native PEM parsing) let mut cert_reader = BufReader::new(&self.cert_chain[..]); - let certs = certs(&mut cert_reader) + let certs: Vec> = CertificateDer::pem_reader_iter(&mut cert_reader) .collect::, _>>() .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; // Parse private key let mut key_reader = BufReader::new(&self.private_key[..]); - let key = private_key(&mut key_reader) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "No private key found"))?; + let key = PrivateKeyDer::from_pem_reader(&mut key_reader) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; // Build server config let config = ServerConfig::builder() @@ -144,20 +145,18 @@ impl SniResolver { /// Helper to parse a TlsCertificate and return a CertifiedKey fn parse_certified_key(cert: &TlsCertificate) -> Result { - use rustls::pki_types::CertificateDer; use rustls::sign::CertifiedKey; - // Parse certificate chain + // Parse certificate chain (rustls native PEM parsing) let mut cert_reader = BufReader::new(&cert.cert_chain[..]); - let certs: Vec = certs(&mut cert_reader) + let certs: Vec> = CertificateDer::pem_reader_iter(&mut cert_reader) .collect::, _>>() .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; // Parse private key let mut key_reader = BufReader::new(&cert.private_key[..]); - let key = private_key(&mut key_reader) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "No private key found"))?; + let key = PrivateKeyDer::from_pem_reader(&mut key_reader) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; // Build signing key let signing_key = rustls::crypto::ring::sign::any_supported_type(&key) @@ -482,7 +481,7 @@ mod tests { let mut root_cert_store = rustls::RootCertStore::empty(); root_cert_store.add_parsable_certificates( - rustls_pemfile::certs(&mut std::io::BufReader::new(&cert_pem[..])) + CertificateDer::pem_reader_iter(&mut std::io::BufReader::new(&cert_pem[..])) .collect::, _>>() .unwrap(), ); @@ -608,12 +607,12 @@ mod tests { let mut root_cert_store = rustls::RootCertStore::empty(); root_cert_store.add_parsable_certificates( - rustls_pemfile::certs(&mut std::io::BufReader::new(&example_cert_pem[..])) + CertificateDer::pem_reader_iter(&mut std::io::BufReader::new(&example_cert_pem[..])) .collect::, _>>() .unwrap(), ); root_cert_store.add_parsable_certificates( - rustls_pemfile::certs(&mut std::io::BufReader::new(&test_cert_pem[..])) + CertificateDer::pem_reader_iter(&mut std::io::BufReader::new(&test_cert_pem[..])) .collect::, _>>() .unwrap(), ); @@ -790,7 +789,7 @@ mod tests { let mut root_cert_store = rustls::RootCertStore::empty(); root_cert_store.add_parsable_certificates( - rustls_pemfile::certs(&mut std::io::BufReader::new(&cert_pem[..])) + CertificateDer::pem_reader_iter(&mut std::io::BufReader::new(&cert_pem[..])) .collect::, _>>() .unwrap(), ); diff --git a/eval/k8s/oracle-job.yaml b/eval/k8s/oracle-job.yaml new file mode 100644 index 0000000..2c19e6c --- /dev/null +++ b/eval/k8s/oracle-job.yaml @@ -0,0 +1,53 @@ +# Oracle K8s Job — runs the oracle test suite against a live RAUTA in-cluster +# +# Prerequisites: +# 1. RAUTA deployed in the 'rauta' namespace +# 2. Oracle image built and available +# +# Usage: +# kubectl apply -f eval/k8s/oracle-job.yaml +# kubectl logs -f job/rauta-oracle -n rauta +# +# The oracle connects to RAUTA's admin API (port 9091) and proxy (port 8080) +# via Kubernetes Service DNS names. + +apiVersion: batch/v1 +kind: Job +metadata: + name: rauta-oracle + namespace: rauta + labels: + app.kubernetes.io/name: rauta-oracle + app.kubernetes.io/component: eval +spec: + backoffLimit: 0 + ttlSecondsAfterFinished: 3600 + template: + metadata: + labels: + app.kubernetes.io/name: rauta-oracle + spec: + restartPolicy: Never + containers: + - name: oracle + image: false-systems/rauta-oracle:latest + imagePullPolicy: Always + env: + - name: RAUTA_PROXY_ENDPOINT + value: "http://rauta.rauta.svc:8080" + - name: RAUTA_ADMIN_ENDPOINT + value: "http://rauta.rauta.svc:9091" + command: + - cargo + - test + - --manifest-path + - eval/oracle/Cargo.toml + - -- + - --nocapture + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi From da9df82e66e9f39f2df020a693fbb6eb15e9f3dd Mon Sep 17 00:00:00 2001 From: Yair Etziony Date: Wed, 25 Mar 2026 01:05:37 +0100 Subject: [PATCH 2/2] fix: address PR #61 review feedback - Align oracle K8s Job with actual manifests: namespace rauta-system, service rauta-metrics (matches manifests/04-service.yaml) - Add Dockerfile for oracle image (multi-stage: compile test binary in rust:1.83-slim, run in debian:bookworm-slim with no toolchain) - Fix benchmark doc comment: remove nonexistent ArcSwap benchmark bullet Co-Authored-By: Claude Opus 4.6 (1M context) --- control/benches/hot_path.rs | 3 +-- eval/k8s/Dockerfile | 18 ++++++++++++++++++ eval/k8s/oracle-job.yaml | 22 +++++++--------------- 3 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 eval/k8s/Dockerfile diff --git a/control/benches/hot_path.rs b/control/benches/hot_path.rs index 8e15f11..3a1dd14 100644 --- a/control/benches/hot_path.rs +++ b/control/benches/hot_path.rs @@ -5,8 +5,7 @@ //! Benchmarks: //! - Circuit breaker allow_request (AtomicU64 CAS) //! - Rate limiter try_acquire (AtomicU64 CAS) -//! - Router select_backend (route lookup + Maglev + health check) -//! - ArcSwap health data load +//! - Router select_backend (route lookup + Maglev + health check via ArcSwap) #![allow(clippy::unwrap_used)] diff --git a/eval/k8s/Dockerfile b/eval/k8s/Dockerfile new file mode 100644 index 0000000..db3ee14 --- /dev/null +++ b/eval/k8s/Dockerfile @@ -0,0 +1,18 @@ +# Oracle test image — pre-compiled test binary, no Rust toolchain needed at runtime +# +# Build: docker build -f eval/k8s/Dockerfile -t false-systems/rauta-oracle . +# Run: docker run --rm -e RAUTA_PROXY_ENDPOINT=http://host:8080 false-systems/rauta-oracle + +FROM rust:1.83-slim AS builder + +WORKDIR /build +COPY eval/oracle/ eval/oracle/ + +RUN cargo test --manifest-path eval/oracle/Cargo.toml --no-run --release 2>&1 \ + && cp $(find eval/oracle/target/release/deps -name 'rauta_oracle-*' -type f ! -name '*.d' | head -1) /oracle + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /oracle /usr/local/bin/oracle + +ENTRYPOINT ["/usr/local/bin/oracle", "--nocapture"] diff --git a/eval/k8s/oracle-job.yaml b/eval/k8s/oracle-job.yaml index 2c19e6c..d03916b 100644 --- a/eval/k8s/oracle-job.yaml +++ b/eval/k8s/oracle-job.yaml @@ -1,21 +1,20 @@ # Oracle K8s Job — runs the oracle test suite against a live RAUTA in-cluster # # Prerequisites: -# 1. RAUTA deployed in the 'rauta' namespace -# 2. Oracle image built and available +# 1. RAUTA deployed in the 'rauta-system' namespace (see manifests/) +# 2. Oracle image built: docker build -f eval/k8s/Dockerfile -t false-systems/rauta-oracle . # # Usage: # kubectl apply -f eval/k8s/oracle-job.yaml -# kubectl logs -f job/rauta-oracle -n rauta +# kubectl logs -f job/rauta-oracle -n rauta-system # -# The oracle connects to RAUTA's admin API (port 9091) and proxy (port 8080) -# via Kubernetes Service DNS names. +# Configure endpoints via env vars if your deployment differs. apiVersion: batch/v1 kind: Job metadata: name: rauta-oracle - namespace: rauta + namespace: rauta-system labels: app.kubernetes.io/name: rauta-oracle app.kubernetes.io/component: eval @@ -34,16 +33,9 @@ spec: imagePullPolicy: Always env: - name: RAUTA_PROXY_ENDPOINT - value: "http://rauta.rauta.svc:8080" + value: "http://rauta-metrics.rauta-system.svc:8080" - name: RAUTA_ADMIN_ENDPOINT - value: "http://rauta.rauta.svc:9091" - command: - - cargo - - test - - --manifest-path - - eval/oracle/Cargo.toml - - -- - - --nocapture + value: "http://rauta-metrics.rauta-system.svc:9091" resources: requests: cpu: 100m