From aa18a0b0769224a5a6d354ebd7bca6a4f5ce929f Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Fri, 16 Oct 2026 08:28:40 +0000 Subject: [PATCH 1/2] [p4rs, codegen] Pipeline::clear_table_entries, + helios CI bump When dpd restarts against a running softnpu pipeline, previous table entries persist, even though the daemon does not know what the keys are. `Pipeline` had support for add/remove by key and a dump command, but there was no generic way to empty tables. This work adds a `clear_table_entries(table_id)` to the trait. In codegen, we now emit a `clear__entries` per table alongside the add/remove/get ones. With this change in place, softnpu can use it for a write-only `TableClear` management message, so that dendrite's SoftNPU `TableOps::clear` can be un-stubbed. Includes: - [ci] s/helios-2.0/helios-3.0/ matching other networking repos. --- .github/buildomat/jobs/build-and-test.sh | 2 +- codegen/rust/src/pipeline.rs | 48 +++++++++++++++++++ lang/p4rs/src/lib.rs | 5 ++ test/src/lib.rs | 2 + test/src/p4/table_clear.p4 | 61 ++++++++++++++++++++++++ test/src/table_clear.rs | 54 +++++++++++++++++++++ 6 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 test/src/p4/table_clear.p4 create mode 100644 test/src/table_clear.rs diff --git a/.github/buildomat/jobs/build-and-test.sh b/.github/buildomat/jobs/build-and-test.sh index d0331ea4..e17398f0 100755 --- a/.github/buildomat/jobs/build-and-test.sh +++ b/.github/buildomat/jobs/build-and-test.sh @@ -2,7 +2,7 @@ #: #: name = "build-and-test" #: variety = "basic" -#: target = "helios-2.0" +#: target = "helios-3.0" #: rust_toolchain = "stable" #: output_rules = [ #: "/work/debug/*", diff --git a/codegen/rust/src/pipeline.rs b/codegen/rust/src/pipeline.rs index 851fb19c..0c187b32 100644 --- a/codegen/rust/src/pipeline.rs +++ b/codegen/rust/src/pipeline.rs @@ -118,6 +118,8 @@ impl<'a> PipelineGenerator<'a> { self.add_table_entry_method(ingress, egress); let remove_table_entry_method = self.remove_table_entry_method(ingress, egress); + let clear_table_entries_method = + self.clear_table_entries_method(ingress, egress); let get_table_entries_method = self.get_table_entries_method(ingress, egress); let get_table_ids_method = self.get_table_ids_method(ingress, egress); @@ -155,6 +157,7 @@ impl<'a> PipelineGenerator<'a> { #pipeline_impl_process_packet #add_table_entry_method #remove_table_entry_method + #clear_table_entries_method #get_table_entries_method #get_table_ids_method } @@ -598,6 +601,39 @@ impl<'a> PipelineGenerator<'a> { } } + fn clear_table_entries_method( + &mut self, + ingress: &Control, + egress: &Control, + ) -> TokenStream { + let mut body = TokenStream::new(); + + for control in &[ingress, egress] { + let tables = control.tables(self.ast); + for (cs, table) in tables.iter() { + let qtn = qualified_table_name(Some(control), cs, table); + let qtfn = + qualified_table_function_name(Some(control), cs, table); + let call = format_ident!("clear_{qtfn}_entries"); + body.extend(quote! { + #qtn => self.#call(), + }); + } + } + + body.extend(quote! { + x => println!("clear table entries: unknown table id {x}, ignoring"), + }); + + quote! { + fn clear_table_entries(&mut self, table_id: &str) { + match table_id { + #body + } + } + } + } + fn get_table_ids_method( &mut self, ingress: &Control, @@ -684,6 +720,7 @@ impl<'a> PipelineGenerator<'a> { table_control, &qtfn, )); + tokens.extend(self.clear_table_entries_function(&qtfn)); tokens.extend(self.get_table_entries_function( table, table_control, @@ -1020,6 +1057,17 @@ impl<'a> PipelineGenerator<'a> { } } + fn clear_table_entries_function(&mut self, qtfn: &str) -> TokenStream { + let name = format_ident!("clear_{qtfn}_entries"); + let tname = format_ident!("{qtfn}"); + + quote! { + pub fn #name(&mut self) { + self.#tname.entries.clear(); + } + } + } + fn get_table_entries_function( &mut self, _table: &Table, diff --git a/lang/p4rs/src/lib.rs b/lang/p4rs/src/lib.rs index 077ddd72..6f6fa5ea 100644 --- a/lang/p4rs/src/lib.rs +++ b/lang/p4rs/src/lib.rs @@ -179,6 +179,11 @@ pub trait Pipeline: Send { /// Remove an entry from a table identified by table_id. fn remove_table_entry(&mut self, table_id: &str, keyset_data: &[u8]); + /// Remove all entries from a table identified by table_id. + // NOTE: This maps to dendrite's SoftNPU `TableOps::clear` implementation, + // which is the analogue of `bf_rt_table_clear` on Tofino, for example. + fn clear_table_entries(&mut self, table_id: &str); + /// Get all the entries in a table. fn get_table_entries(&self, table_id: &str) -> Option>; diff --git a/test/src/lib.rs b/test/src/lib.rs index a6d4f38b..4e3c0d5d 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -25,6 +25,8 @@ mod mac_rewrite; #[cfg(test)] mod range; #[cfg(test)] +mod table_clear; +#[cfg(test)] mod table_in_egress_and_ingress; #[cfg(test)] mod vlan; diff --git a/test/src/p4/table_clear.p4 b/test/src/p4/table_clear.p4 new file mode 100644 index 00000000..5bc38b76 --- /dev/null +++ b/test/src/p4/table_clear.p4 @@ -0,0 +1,61 @@ +#include +#include +#include + +SoftNPU( + parse(), + ingress(), + egress() +) main; + +struct headers_t { + ethernet_h eth; +} + +parser parse( + packet_in pkt, + out headers_t headers, + inout ingress_metadata_t ingress, +){ + state start { + transition accept; + } +} + +control bakery( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + + action drop() { } + action forward(bit<16> port) { egress.port = port; } + table muffins { + key = { ingress.port: exact; } + actions = { drop; forward; } + default_action = drop; + } + +} + +control ingress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + bakery() bakery; + apply { + bakery.apply(hdr, ingress, egress); + } +} + +control egress( + inout headers_t hdr, + inout ingress_metadata_t ingress, + inout egress_metadata_t egress, +) { + bakery() bakery; + apply { + bakery.apply(hdr, ingress, egress); + } +} diff --git a/test/src/table_clear.rs b/test/src/table_clear.rs new file mode 100644 index 00000000..8f062f08 --- /dev/null +++ b/test/src/table_clear.rs @@ -0,0 +1,54 @@ +p4_macro::use_p4!( + p4 = "test/src/p4/table_clear.p4", + pipeline_name = "table_clear", +); + +const INGRESS_TBL: &str = "ingress.bakery.muffins"; +const EGRESS_TBL: &str = "egress.bakery.muffins"; + +fn entry_count(pipeline: &main_pipeline, table: &str) -> usize { + pipeline.get_table_entries(table).unwrap().len() +} + +fn add_forward(pipeline: &mut main_pipeline, table: &str, port: u16) { + pipeline.add_table_entry( + table, + "forward", + &port.to_le_bytes(), + &1u16.to_le_bytes(), + 0, + ); +} + +#[test] +fn clear_table_entries() { + let mut pipeline = main_pipeline::new(4); + + let ids = pipeline.get_table_ids(); + assert!(ids.contains(&INGRESS_TBL)); + assert!(ids.contains(&EGRESS_TBL)); + + for port in 0..3 { + add_forward(&mut pipeline, INGRESS_TBL, port); + add_forward(&mut pipeline, EGRESS_TBL, port); + } + assert_eq!(entry_count(&pipeline, INGRESS_TBL), 3); + assert_eq!(entry_count(&pipeline, EGRESS_TBL), 3); + + pipeline.clear_table_entries(INGRESS_TBL); + assert_eq!(entry_count(&pipeline, INGRESS_TBL), 0); + assert_eq!(entry_count(&pipeline, EGRESS_TBL), 3); + + pipeline.clear_table_entries(INGRESS_TBL); + assert_eq!(entry_count(&pipeline, INGRESS_TBL), 0); + + pipeline.clear_table_entries("ingress.bakery.scones"); + assert_eq!(entry_count(&pipeline, EGRESS_TBL), 3); + + add_forward(&mut pipeline, INGRESS_TBL, 7); + assert_eq!(entry_count(&pipeline, INGRESS_TBL), 1); + + pipeline.clear_table_entries(EGRESS_TBL); + assert_eq!(entry_count(&pipeline, EGRESS_TBL), 0); + assert_eq!(entry_count(&pipeline, INGRESS_TBL), 1); +} From 44f87a8aa9f461eb0761327b9aa34945826ba179 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Tue, 20 Oct 2026 01:54:15 +0000 Subject: [PATCH 2/2] [review] address feedback --- codegen/rust/src/pipeline.rs | 12 +++++++++--- lang/p4rs/src/lib.rs | 7 ++++++- test/src/table_clear.rs | 8 ++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/codegen/rust/src/pipeline.rs b/codegen/rust/src/pipeline.rs index 61b0fc33..4bca914e 100644 --- a/codegen/rust/src/pipeline.rs +++ b/codegen/rust/src/pipeline.rs @@ -668,17 +668,23 @@ impl<'a> PipelineGenerator<'a> { qualified_table_function_name(Some(control), cs, table); let call = format_ident!("clear_{qtfn}_entries"); body.extend(quote! { - #qtn => self.#call(), + #qtn => { + self.#call(); + true + } }); } } body.extend(quote! { - x => println!("clear table entries: unknown table id {x}, ignoring"), + x => { + println!("clear table entries: unknown table id {x}, ignoring"); + false + } }); quote! { - fn clear_table_entries(&mut self, table_id: &str) { + fn clear_table_entries(&mut self, table_id: &str) -> bool { match table_id { #body } diff --git a/lang/p4rs/src/lib.rs b/lang/p4rs/src/lib.rs index 3b621ef3..05ff9c94 100644 --- a/lang/p4rs/src/lib.rs +++ b/lang/p4rs/src/lib.rs @@ -178,9 +178,14 @@ pub trait Pipeline: Send { fn remove_table_entry(&mut self, table_id: &str, keyset_data: &[u8]); /// Remove all entries from a table identified by table_id. + /// + /// Returns `true` if the table was cleared, or `false` if `table_id` maps + /// to no table in this pipeline. This return type distinguishes an unknown + /// table from an already-empty one, like [`Pipeline::get_table_entries`] + /// does with `None`. // NOTE: This maps to dendrite's SoftNPU `TableOps::clear` implementation, // which is the analogue of `bf_rt_table_clear` on Tofino, for example. - fn clear_table_entries(&mut self, table_id: &str); + fn clear_table_entries(&mut self, table_id: &str) -> bool; /// Get all the entries in a table. fn get_table_entries(&self, table_id: &str) -> Option>; diff --git a/test/src/table_clear.rs b/test/src/table_clear.rs index 8f062f08..ad5d27c2 100644 --- a/test/src/table_clear.rs +++ b/test/src/table_clear.rs @@ -35,20 +35,20 @@ fn clear_table_entries() { assert_eq!(entry_count(&pipeline, INGRESS_TBL), 3); assert_eq!(entry_count(&pipeline, EGRESS_TBL), 3); - pipeline.clear_table_entries(INGRESS_TBL); + assert!(pipeline.clear_table_entries(INGRESS_TBL)); assert_eq!(entry_count(&pipeline, INGRESS_TBL), 0); assert_eq!(entry_count(&pipeline, EGRESS_TBL), 3); - pipeline.clear_table_entries(INGRESS_TBL); + assert!(pipeline.clear_table_entries(INGRESS_TBL)); assert_eq!(entry_count(&pipeline, INGRESS_TBL), 0); - pipeline.clear_table_entries("ingress.bakery.scones"); + assert!(!pipeline.clear_table_entries("egress.bakery.scones")); assert_eq!(entry_count(&pipeline, EGRESS_TBL), 3); add_forward(&mut pipeline, INGRESS_TBL, 7); assert_eq!(entry_count(&pipeline, INGRESS_TBL), 1); - pipeline.clear_table_entries(EGRESS_TBL); + assert!(pipeline.clear_table_entries(EGRESS_TBL)); assert_eq!(entry_count(&pipeline, EGRESS_TBL), 0); assert_eq!(entry_count(&pipeline, INGRESS_TBL), 1); }