perf(appsec): build WAF off the init critical path#1283
Draft
duncanista wants to merge 1 commit into
Draft
Conversation
AppSecProcessor::new zstd-decompresses a ~29KB->322KB ruleset, JSON-parses it, and compiles the libddwaf WAF (tens of ms) synchronously during init. The WAF is only needed once the first request payload is evaluated, which is strictly after the first /next, so this work does not belong on the init critical path. Replace the eager Option<Arc<Mutex<Processor>>> with a deferred, awaitable handle (Arc<OnceCell<Option<Arc<Mutex<Processor>>>>>). When AppSec is enabled, the build runs on the blocking pool (spawn_blocking) from a background task; consumers (trace processor and the runtime API proxy) resolve the handle where they actually use the WAF, awaiting the in-flight build if a request somehow arrives before it finishes. The disabled-by-default path stays cheap: the feature flag is checked synchronously and yields no handle and no build.
|
12 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Jira: none yet — add before marking ready.
Overview
When App & API Protection (AAP) is enabled, building the WAF is expensive and happens on the init critical path today.
AppSecProcessor::new(config)runs synchronously during extension init and:serde_json-parses it into aWafMap, andlibddwafWAF instance.That is tens of milliseconds of CPU-bound work that blocks startup. But the WAF is only needed once the first request payload is evaluated — in the proxy interceptor's
process_invocation_next(and on the trace path / response path) — which is strictly after the first/next. So none of it needs to be on the synchronous init path.Approach (deferred, awaitable handle +
spawn_blocking)Replace the eager
Option<Arc<TokioMutex<AppSecProcessor>>>that was threaded through every consumer with a deferred handle:appsec::defer_processor(cfg)checks the feature flag synchronously and returnsNoneimmediately — noOnceCell, no build, no spawned task. This preserves the disabled-by-default behavior (previously theErr(FeatureDisabled)arm).defer_processorcreates theOnceCell, spawns a background task that builds the processor insidetokio::task::spawn_blocking(CPU-bound work belongs on the blocking pool), and returns the handle right away. Init no longer blocks on the build.SendingTraceProcessor::send_processed_traces) and the proxy interceptor (invocation_next_proxy/invocation_response_proxy/invocation_error_proxy) callappsec::resolve(handle, cfg).awaitexactly where they need the WAF, then.lock().awaitas before.The two nested
Options encode three states cleanly:None-> feature disabled (no handle);Some(proc)-> WAF built successfully;None-> build failed (logged) -> feature is a silent no-op, exactly as the oldErr(_) => Nonearm behaved.Correctness: what if a request arrives before the WAF is built?
The handle is a
tokio::sync::OnceCell, and consumers resolve it viaget_or_init:/nextround-trip — soresolvereturns instantly.resolvebefore the background build has completed,get_or_initsimply awaits the in-flight build (or, if the background task has not been scheduled yet, runs an equivalentspawn_blockingbuild itself).OnceCellguarantees a single initializer, so there is never a double-build and never a missed evaluation — the first request transparently waits just until the WAF is ready instead of the whole init blocking on it.No request can be processed against a half-built WAF, and no evaluation is skipped.
Scope
Only affects AppSec-enabled configurations. With AAP disabled (the default), behavior is byte-for-byte unchanged and the path stays synchronous and allocation-free.
Files changed:
bottlecap/src/appsec/mod.rs— newSharedProcessor/DeferredProcessortypes,defer_processor,resolve, andbuild_processor(thespawn_blockingbuilder).bottlecap/src/bin/bottlecap/main.rs— construct viaappsec::defer_processor; thread the handle type through; passconfigto the proxy. H0 init instrumentation preserved.bottlecap/src/proxy/interceptor.rs— hold the deferred handle inInterceptorState, carryArc<Config>, resolve at each WAF use site.bottlecap/src/traces/trace_agent.rs— field/param type updated to the deferred handle.bottlecap/src/traces/trace_processor.rs—SendingTraceProcessor.appsecis now the deferred handle; resolve before locking.Testing
cargo fmtclean.cargo clippy --bin bottlecap --no-depsclean (clippy::all+pedantic+unwrap_useddenied); also clippy-clean across--all-targets. Only the pre-existingbuf_redux/multipartfuture-incompat warning remains.cargo testforappsec::*(24 tests),proxy::interceptor::tests::test_noop_proxy, andtraces::trace_processor::*(21 tests) all pass.Config::default()(AAP off) yieldsNonefromdefer_processorwith no spawned build, exercised bytest_noop_proxy.