(13) feat(cascade): in-memory LSM match-action store + real-shape ACL tests#1567
(13) feat(cascade): in-memory LSM match-action store + real-shape ACL tests#1567daniel-noland wants to merge 8 commits into
Conversation
45262b6 to
df3cbd7
Compare
There was a problem hiding this comment.
Pull request overview
The PR title explicitly says "(ignore for now) acl design test" — this is a large design-exploration PR introducing a new match-action / ACL framework spanning many new workspace crates, plus supporting changes to existing crates.
Changes:
- Introduces several new workspace crates:
fixed-size,lookup,match-action,match-action-derive,cascade,acl,dpdk-test-macros, and a scratch ACL module underdpdk. - Adds a
MatchKeyderive (with#[exact]/#[prefix]/#[mask]/#[range]field attributes), parallel<Name>Rulestruct emission, and lowering into a reference and DPDKrte_aclbackend, plus extensive property/differential tests and Criterion benches. - Adds infrastructure: a
dpdk::test_supportEAL bootstrap +#[with_eal]attribute macro, acascadecrate (LSM-style match-action storage withUpsert/MergeInto/drain subscription), abench-builderNix target, and acompute_min_input_sizeconst fnonAclBuildConfig.
Reviewed changes
Copilot reviewed 66 out of 67 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| Cargo.toml | Registers new workspace members, deps, and per-package miri/wasm metadata |
| fixed-size/** | New no_std crate defining FixedSize trait with primitive/IpAddr impls |
| lookup/** | New crate with Lookup/Projection traits and BTreeMap/HashMap impls |
| match-action/, match-action-derive/ | New backend-agnostic match-key vocabulary + #[derive(MatchKey)] |
| net/src/fixed_size.rs, net/src/lib.rs, net/Cargo.toml | FixedSize impls for TcpPort/UdpPort/UnicastIpv4Addr/Vni |
| cascade/** | New LSM-style cascade primitive with Upsert/MergeInto/drain subscription |
| dpdk/Cargo.toml, dpdk/src/lib.rs, dpdk/src/test_support.rs | Optional test feature exposing shared EAL bootstrap + #[with_eal] macro |
| dpdk-test-macros/** | New proc-macro crate providing the #[with_eal] attribute |
| dpdk/src/acl/config.rs | Makes compute_min_input_size a public const fn; doc/spelling cleanup |
| dpdk/src/acl/scratch.rs | Scratch/sketch file with non-compiling design notes |
| acl/** | New crate: DPDK rte_acl backend, reference linear-scan oracle, tests, benches |
| default.nix, justfile | New benches Nix target and just bench recipe to build/run benches |
1d88968 to
5109431
Compare
137d9d9 to
af41ab0
Compare
c347061 to
592cd9d
Compare
de5a2c1 to
fd80413
Compare
70afa5a to
ba7f579
Compare
16e2188 to
01d98b2
Compare
ba7f579 to
c9e5eb3
Compare
01d98b2 to
ddbc607
Compare
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
ddbc607 to
19ca1a1
Compare
c9e5eb3 to
0683aff
Compare
19ca1a1 to
29232af
Compare
0683aff to
057f155
Compare
Lands the static type machinery for the DPDK `rte_acl` backend
behind the new `dpdk` feature gate: how a MatchKey's FIELD_SPECS maps
into rte_acl's per-field FieldDef array, and how the four
match-action *Spec predicate kinds lower into IntoBackendField for
the `Dpdk` backend marker. The runtime install / classify path
(install.rs, lookup.rs) and the dpdk_table_alias! macro land next.
acl/src/dpdk/:
- mod.rs declares the two submodules; carries a temporary
#![allow(dead_code)] because the layout's `stride` field and the
rule.rs RuleSpec fields are consumed only once install / lookup
arrive in the next PR. The allow goes away then.
- layout.rs has the rte_acl field planner: group fields by
input_index (rte_acl requires the first field to be one byte,
remaining fields grouped into <= 4-byte buckets), insert padding
for gaps, and yield a DpdkLayout { field_defs: [FieldDef; N],
stride, user_to_dpdk }. const_extents() is const fn so a const
alias can derive N / STRIDE from K::FIELD_SPECS without unstable
generic_const_exprs. Wide fields (Ipv6Addr, u128) decompose into
four u32 sub-fields the way l3fwd-acl does.
- rule.rs holds the Dpdk backend marker, the AclWord trait (blanket
impl over FixedSize via chunks()), the IntoBackendField impls
carrying each *Spec into a backend-typed AclField group, the
RuleSpec rule-field envelope, and splice_user_fields_to_dpdk for
reordering user-declared fields into rte_acl's layout-driven
ordering.
acl/src/lib.rs picks up the #[cfg(feature = "dpdk")] gate on
pub mod dpdk; (no macro yet -- the dpdk_table_alias! macro lands with
its lookup-side referent next PR).
acl/Cargo.toml grows the dpdk feature and the optional dpdk
workspace dep. No dev-deps yet.
just fmt; cargo check --workspace --all-targets and
cargo clippy -p dataplane-acl --features dpdk -- -D warnings pass.
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Wires the layout planner and rule lowering from the previous PR into a working DPDK backend: build an AclContext from a MatchKey plus its rules, wrap it in a DpdkAclLookup, and classify packets through it. First EAL-touching PR in the acl stack. src/dpdk/: - install.rs is the from-K-plus-rules constructor: take a MatchKey, call plan_layout to get the rte_acl FieldDefs, build an AclContext, splice each user RuleSpec through layout's user_to_dpdk map into rte_acl's column order, hand the rules to the context, build, and wrap the built context in a DpdkAclLookup<K, N, STRIDE, A>. - lookup.rs is DpdkAclLookup itself: stack-packed key bytes (MAX_USER_KEY_BYTES sentinel feeds the compile-time guard in dpdk_table_alias!), the impl Lookup<K, A> single-shot path, and a batched classify_batch over a slice of K returning aligned actions. - mod.rs picks up pub mod install / pub mod lookup and drops the temporary #![allow(dead_code)] from the previous PR -- RuleSpec fields and DpdkLayout.stride now have readers. src/lib.rs gains the dpdk_table_alias! macro: dpdk_table_alias!(pub type FiveTupleTable<Verdict> = FiveTuple); yields a DpdkAclLookup<K, N, STRIDE, A> with N / STRIDE derived from K::FIELD_SPECS via const_extents. A const _: () = assert!(KEY_SIZE <= MAX_USER_KEY_BYTES) guards against keys that wouldn't fit the stack scratch buffer. The hidden __match_action module re-exports MatchKey so the macro resolves without a caller-side import. tests/eal_install_classify.rs is the smoke: derive a MatchKey, install two rules with priority precedence, classify via the single-shot path and the batch path, assert userdata. acl/Cargo.toml grows a single dev-dep -- self-overriding dpdk with the `test` feature on so #[with_eal] from dpdk-test-macros works. just fmt; cargo check --workspace --all-targets and cargo clippy -p dataplane-acl --features dpdk -- -D warnings pass. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Adds the runtime-shape twin of DpdkAclLookup -- DynDpdkLookup carries its FieldSpec layout at runtime instead of in const generics -- and the shape-fuzz oracle that proves the byte-level pipeline agrees between the reference oracle and rte_acl over an unconstrained schema. src/dpdk/dyn_table.rs is DynDpdkLookup<A>: - new(name, max_rule_num, field_specs) plans the rte_acl layout from a Vec<FieldSpec> at runtime, builds an empty AclContext, and returns a typed lookup keyed by an Erased FieldPredicate vector. - add_rules takes Vec<DynRuleSpec> -- the runtime-shape rule carrier (priority, category_mask, lowered fields, action) -- and splices each rule's field bytes through the user_to_dpdk map into rte_acl's column order, then builds. - impl Lookup<Vec<FieldBytes>, A>: pack the probe bytes onto the stack scratch buffer in the layout's column order, hand them to rte_acl_classify, and translate the userdata hit back to &A. src/dpdk/mod.rs picks up pub mod dyn_table; alongside the typed path. tests/property_dyn_shape.rs is the schema fuzz: - bolero TypeGenerator yields a random Vec<FieldSpec>, a single rule matching that shape, and packet seeds. - For each shape: install the same rule into a DynReferenceTable (oracle) and a DynDpdkLookup, then probe both with both a hit-byte seed and a miss-byte seed. Assert agreement on every probe. - No MatchKey types involved -- exercises the byte-level pipeline end-to-end and catches drift in layout planning, the splice map, and rte_acl's per-predicate semantics simultaneously. acl/Cargo.toml gains bolero + match-action[bolero] dev-deps the test needs. just fmt; cargo check --workspace --all-targets and cargo clippy -p dataplane-acl --features dpdk -- -D warnings pass. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Pure test broadening; no src changes. Adds the single-rule v4/v6 differential against the reference oracle and three Headers / metadata projection demos that exercise classify / classify_opt against real net::HeadersView packets. tests/property_predicate.rs is the differential. For a random 5-tuple rule + random hit/miss byte seeds drawn via match-action's FieldHit / FieldMiss generators, both the reference oracle and the DPDK backend must accept every hits() draw and reject every misses() draw. Parameterised over the address width via a sealed IpAddress trait so a single body covers v4 (Ipv4Addr) and v6 (Ipv6Addr) -- the DPDK wide-field split (one 16-byte address -> four 4-byte sub-fields) is exercised end-to-end by the v6 invocation. Single rule only; multi-rule differential is deferred (positional precedence vs numeric Priority). tests/eal_classify_via_projection.rs is the end-to-end projection demo: a real packet -> HeadersView -> Projection<FiveTuple> -> DPDK Lookup<FiveTuple, _> -> action. Shows Lookup::classify runs the projection and the lookup as a single call -- the call site reads table.classify(\&headers) and doesn't see the intermediate key construction. tests/metadata_projection.rs is the partial-projection demo. Header fields live in Headers; VRF / VNI live in PacketMeta. A projection source bundles &HeadersView with &PacketMeta and projects to Option<K>: the header part is total (shape proves presence), the metadata part narrows from its Option with ?. Missing metadata projects to None and Lookup::classify_opt turns that into a table miss with no explicit branch in user code. tests/net_field_types.rs uses net wire newtypes (TcpPort, UdpPort, Vni, UnicastIpv4Addr) directly as MatchKey fields with no acl-side AclWord impl, leaning on net's FixedSize impls (PR 2a) and the DPDK backend's blanket AclWord-over-FixedSize impl. acl/Cargo.toml grows the net[test_buffer, builder] dev-dep these projection demos need. just fmt; cargo check --workspace --all-targets and cargo clippy -p dataplane-acl --features dpdk -- -D warnings pass. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Adds criterion benchmarks for both backends at v4 and v6 widths, plus the nix / just plumbing to produce bench binaries from a sandboxed build. acl/benches/: - reference_five_tuple.rs sweeps a deep miss (full per-rule scan) and an early hit through the reference's O(rules * fields) linear scan. Both widths. - dpdk_five_tuple.rs is the rte_acl companion: trie walk cost (close to flat in rule count), miss vs hit, single-shot vs SIMD batch. v6 exercises the wide-field split (one 16-byte address -> four 4-byte sub-fields). Requires a live EAL. - table_build.rs measures construction cost vs rule count: reference (lower + Vec wrap) and DPDK (rte_acl_build, the update-latency cost). Both widths. iter_batched so teardown is excluded. acl/Cargo.toml gets the criterion dev-dep and three harness = false [[bench]] entries. Workspace Cargo.toml gets the criterion = 0.5.1 shared dep entry. default.nix adds a bench-builder derivation: cargo bench --no-run under the profile-appropriate DPDK sysroot, then copies each compiled benchmark into $out/bin (stripping cargo's -<hash> suffix). Linked against the optimized DPDK when profile = release. justfile adds a bench recipe that builds the benches package and runs every binary under results/benches/bin/ in turn. just fmt; cargo check --workspace --all-targets and cargo clippy -p dataplane-acl --features dpdk -- -D warnings pass. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
29232af to
fbcf630
Compare
057f155 to
4915824
Compare
DpdkAclLookup<K,A> drops N_FIELDS/STRIDE and holds Box<dyn DynClassifier>; install_table<K,A> dispatches the field count at runtime via build_classifier_n shared with the dyn install path; adds DynClassifier::classify_batch so the typed batch path keeps real rte_acl batching through the type erasure; dispatch ceiling raised to MAX_FIELDS (64); keys packed into a single runtime-layout.stride arena (batch path drops the redundant per-key copy); removes lookup_via_bytes/dpdk_key_bytes and simplifies the dpdk_table_alias! macro (no const_extents). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Introduces dataplane-cascade. Models a small in-memory LSM: writes land in a concurrent multi-writer head, periodically frozen into immutable intermediate layers, eventually compacted into an immutable tail. Readers walk head -> frozen[] -> tail and stop at the first definitive answer. The same primitive serves three problems via one Upsert trait: match-action table updates (atomic publish under load), hardware offload programming (drain output feeds the HW backend), and active-active state replication (serialized drain output ships to peer dataplanes). freeze / fuse / compact / merge are all the same Upsert operation applied to different operands. Layout: - lib.rs publishes the surface: Cascade / DrainEvent / FrozenEntry / Snapshot, the Upsert / MergeInto / MutableHead traits, Generation, and re-exports Lookup / Projection from the lookup crate. - cascade.rs is the central type plus the rotate / drain / publish state machine. Cascade::subscribe is feature-gated under `subscribe` (tokio broadcast). - head.rs / merge.rs / upsert.rs are the trait definitions plus LastWriteWins as a stock blanket-friendly Upsert impl. - generation.rs carries the monotonic Generation counter that orders layers within a cascade. - property_tests.rs is a reusable bolero harness (under the bolero feature) consumer crates use to verify their own Upsert impls against the cascade's algebraic laws. Exercised by a self-test in the next PR. tests/smoke.rs is the trait-shape end-to-end with a trivial concrete implementation: head shadows sealed shadows tail, tombstones in the head suppress lower-layer hits, rotate seals and publishes. Uses the shared tests/common/mod.rs helpers that subsequent tests also share. Cascade is independent of every other PR in this stack: no acl dep, no match-action dep. Real-shape consumer pressure plus bolero / subscribe integration tests land in the next PR. just fmt; cargo check --workspace --all-targets passes. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Three integration tests that round out the cascade's test surface -- each exercises a different part of the design pressure that shaped the trait surface in the previous PR. tests/acl_consumer.rs is the first real-shaped consumer: a minimal ACL classifier built on top of the cascade with no upfront ACL crate dep -- the toy ACL is built inline using only Cascade / MergeInto / MutableHead. Purpose: surface design pressure on the trait surface against a use case that is NOT exact-match-keyed. ACL classification looks up packets by header match expressions, not by a single key, and rules carry their own identity (priority) separate from the lookup input. If the cascade trait shape works for ACL it almost certainly works for anything simpler. Scope intentionally tight: rules install-only (no shadow-rule removal yet), match expressions src/dst IPv4 with optional single port, head Lookup always returns None (writes visible only after a rotation seals the head). Comment block at the end captures the open question about removal under cascade semantics. tests/upsert_properties.rs is the self-test of the property harness landed in the previous PR. Exercises check_upsert_order_independent against the provided LastWriteWins -- known correct by construction. If it fails, the harness itself is broken; if it passes, it's a useful black-box check for downstream Upsert impls. tests/subscribe.rs is the drain-subscription integration under tokio. Exercises Cascade::subscribe end-to-end -- gated by the cascade `subscribe` feature (enabled in dev-deps via the self-path override on Cargo.toml). just fmt; cargo check --workspace --all-targets passes. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
4915824 to
e5d8a01
Compare
c795ed9 to
19de724
Compare
Stack (13), this PR (retargeted). Base:
pr/daniel-noland/acl-dpdk.In-memory LSM-shaped match-action storage primitive, plus tests that drive it
with a real ACL-shaped consumer.
feat(cascade): in-memory LSM-shaped match-action storage primitive.test(cascade): real-shape ACL consumer + bolero + subscribe tests.Review stack (merge bottom -> top):