DOCA Flow workflows
Where to start: The verbs run configure → build → modify → test → run → debug; skip ahead only when the user is already past a verb.
## test is an iterative loop (validate → cross-check → counter wiring →
negative test → loop back if the spec changed), not a one-shot pass.
For the capability matrix, version compatibility, error taxonomy,
observability, and safety policy these workflows assume, see
CAPABILITIES.md. For docs, installed-DOCA layout, or
release notes, route through
doca-public-knowledge-map.
Each verb describes the shape of the workflow, not a copy-paste
recipe: walk the user through the steps in order, verifying preconditions
before recommending the next call.
Key samples to read first
The installed samples are the canonical, compile-tested reference for THIS
install — read the relevant one before writing or quoting code (ls and
read it on the user's machine; layouts change between releases). These few
cover the bulk of customer patterns:
Sample (under /opt/mellanox/doca/samples/doca_flow/) |
Pattern it teaches |
|---|---|
flow_port_fwd/ |
Match → FWD_PORT: the simplest VNF forward |
flow_switch_single/ |
Switch mode + representor (multi-VF eswitch) |
flow_control_pipe/ |
Root control / classifier pipe branching to non-root pipes |
flow_lpm/ |
Longest-prefix-match routing pipe |
flow_acl/ |
ACL (5-tuple allow / deny) pipe |
flow_hash_pipe/ |
Hash-based fan-out / load balancing (slots = power of 2) |
flow_vxlan_encap/ |
Tunnel encap action |
flow_shared_meter/ |
Shared meter / rate limiting + parser_meta.meter_color |
flow_drop/ |
Explicit drop pipe (pairs with fwd_miss) |
flow_aging/ |
Entry aging / time-based eviction |
For connection tracking and NAT see ## flow-ct; for larger
end-to-end apps (doca_flow_ct, doca_simple_fwd_vnf, …) see
/opt/mellanox/doca/applications/.
configure
Goal: bring up a DOCA Flow port on a supported NVIDIA NIC/DPU and confirm the environment is ready for pipe construction.
- Confirm the installed DOCA version. Use the procedure in
doca-public-knowledge-map; quote the version observed, do not assume "latest". - Check device placement BEFORE anything else — host-side vs DPU
Arm. On a BlueField the hardware-steering plane is owned by only one
side of the card. Read
INTERNAL_CPU_MODELfrommlxconfig -d <pcie> q(themlxconfigcommand is owned bydoca-debug):SEPARATED_HOST→ NIC mode; the steering plane lives on the DPU Arm, and the x86 host function cannot start a Flow port (it fails with the placement signature inCAPABILITIES.md ## Error taxonomy:Failed to get hws cap/dest action ROOT … err -121). On the host in this mode, STOP and route Flow work to the DPU Arm side, or change the card's mode throughdoca-hardware-safety— do not proceed to pipe construction.EMBEDDED_CPU(DPU mode), or a NIC whose opened function advertises a usable steering plane → placement is fine; continue. Being listed bydoca_caps --list-devsis NOT proof the opened function has a steering plane — placement is the gate, enumeration is not.
- Discover device capabilities. Run the installed
doca_capstool and the Flow capability-query API; record the supported match kinds, action kinds, and pipe/entry budgets. The matrix to compare against lives inCAPABILITIES.md ## Capabilities and modes. - Enumerate ports and representors. Confirm the port the user wants
to program is visible (
devlink dev showplus the installed Flow port-enumeration sample) and that the expected representors are present. - Runtime prerequisites — hugepages + HWS devargs, before any Flow program runs. A DOCA Flow program needs hugepages allocated and mounted first, and hardware steering (HWS) must be requested through DPDK devargs:
echo '1024' | sudo tee -a /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
sudo mkdir -p /mnt/huge
sudo mount -t hugetlbfs -o pagesize=2M nodev /mnt/huge- HWS is enabled via DPDK devarg
dv_flow_en=2AND amode_argsvalue containinghws(e.g."vnf,hws"or"switch,hws"). Without HWS, many features (shared encap/decap, shared meter across entries, pipe resize, …) are unavailable. - Switch mode additionally requires the DPDK devargs
fdb_def_rule_en=0,vport_match=1,repr_matching_en=0,dv_xmeta_en=4(the DOCA samples hide these behind-r <pci>,<reps>).
Create the DPDK↔DOCA device mapping (the #1 runtime failure), then bring up the Flow port. Three runtime signatures —
mlx5_common: Probe again parameters aren't compatible,doca_dpdk_port_as_dev(...): Requested Resource Not Found,doca_flow_port_start(...): Unknown error— all trace to this mapping not being created correctly. The shippeddpdk_inithelper, wired viadoca_argp_set_dpdk_program(dpdk_init), does the whole sequence for you; the load-bearing rules if you roll your own:- EAL must NOT auto-probe the mlx5 PFs. Pass a non-existent-device
sentinel (
-a 00:0.0) torte_eal_initso EAL scans PCI but allowlists nothing, then let DOCA do the real probe. Skipping this →Probe again parameters aren't compatible. doca_dpdk_port_probe(dev, "dv_flow_en=2")creates the mapping;doca_dpdk_port_as_dev()only reads it. Open each NIC by PCI BDF first (open_doca_device_with_pci(bdf, NULL, &dev)), then probe. The NIC has no DPDK port until this probe runs:rte_eth_dev_count_avail()stays 0 and any "need N ports" check (dpdk_utils.c) aborts if you count or queue-setup before probing every device — collecting BDFs from argv is not the same as probing them. Required strict order — gate the bridge here, not at port-start:rte_eal_initwith the no-auto-probe sentinel above.- Open each
doca_devfrom argv (one per requested device). - Call
doca_dpdk_port_probe(dev, "dv_flow_en=2")for every opened device — this is the call that creates the DPDK port from thedoca_dev(samples/doca_flow/flow_common.c). If any probe fails, log that device's BDF and exact error and exit non-zero; never continue with a partially probed device set. - Only after every device has been probed, call
rte_eth_dev_count_avail()and verify it returns ≥ the expected port count (samples/doca_flow/flow_common.cprecedesapplications/common/dpdk_utils.c, where a count belownb_portsreturnsDOCA_ERROR_DRIVER). - If count == 0, or count < the expected port count, propagate the
error out of
main()with a non-zero exit code immediately — log the failure verbatim and abort. Counting before probe always reports0, so a binary that reaches the keep-alive loop in## runstep 6 withcount == 0is forwarding nothing while appearing to run — the watchdog cannot tell the difference from a healthy program. It is a hard error here, not a warning, and not something to "log and continue" past. - Then call
doca_dpdk_get_first_port_id(dev, &port_id)/RTE_ETH_FOREACH_DEV+doca_dpdk_port_as_devto derive the numeric port id, and proceed torte_eth_dev_configure/rte_eth_dev_start. In switch mode, enumerate each actual DOCA-probed DPDK port, including representors, and copy the installed shipped sample'sdoca_dev↔DPDK-port mapping. Never derive a representor id from a PF ordinal, BDF suffix, or assumed0/1numbering.
- Hardware-neutral
port_idand BDFs — never hard-code0/1or a PCI address. The DOCA-probed NIC can land atport_id 2/3when the PCI auto-scan grabs other devices first, and its BDF differs per host — a hard-coded0000:04:00.0typically lands on an unrelated/virtio function (PCI_BUS: Requested device … cannot be used→doca_flow_port_start … Unknown error). Take the device(s) from the program args after--with the shippedregister_flow_device_params()(flow_common.c,flow_common.h): it registers the canonical repeatable-a/--device pci/<bdf>,<devargs>param (DOCA_ARGP_TYPE_DEVICE) and opens eachdoca_devfor you — this is the exact shape the samples are driven with (-- -a pci/08:00.0,dv_flow_en=2). Do not invent your own (register_argp_device_param()does not exist) and do not hard-reject when no BDF is supplied — fall back to discovery (doca_devinfo_create_list/lspci -d 15b3:). Then probe and derive the numeric id from thedoca_devwithdoca_dpdk_get_first_port_id(dev, &port_id)orRTE_ETH_FOREACH_DEVdoca_dpdk_port_as_dev. The same source then runs on any host.
- Bridging two physical PFs: a naive
DOCA_FLOW_FWD_PORTacross two un-peered PFs fails at pipe-create with... are not hairpin peers. On ConnectX-7+ the simplest correct bridge is thedv_flow_en=2E-Switch domain (whatdoca_simple_fwd_vnfuses); DPDK hairpin queues (rte_eth_*_hairpin_queue_setup, withpeer_count=1,manual_bind=1,tx_explicit=1) are the alternative when you need hardware hairpin semantics.
Then choose the mode-specific per-port Flow bring-up path:
- VNF mode (
flow_port_fwd/): bothdoca_flow_port_cfg_set_port_id()anddoca_flow_port_cfg_set_dev()with the matching probeddoca_devare mandatory. Adoca_dev_repis rejected in this mode. - Switch mode (
flow_switch_single/):port_idis still mandatory, but representor ports use the installed sample'sdoca_dev_repconfiguration path rather than forcingdoca_flow_port_cfg_set_dev(). Copy that path from the installed sample and verify its setter against the installed header.
The required VNF-mode sequence is:
- EAL must NOT auto-probe the mlx5 PFs. Pass a non-existent-device
sentinel (
struct doca_flow_port_cfg *port_cfg;
doca_flow_port_cfg_create(&port_cfg);
doca_flow_port_cfg_set_port_id(port_cfg, port_id); /* MANDATORY: else "port ID is mandatory" */
doca_flow_port_cfg_set_dev(port_cfg, dpdk_dev); /* MANDATORY: else "doca_dev or doca_dev_rep must be provided" */
struct doca_flow_port *port;
doca_flow_port_start(port_cfg, &port); /* takes 2 args */
doca_flow_port_cfg_destroy(port_cfg); Bind the port with set_port_id(), not set_devargs() — the
trap behind a clean compile that dies the instant the port starts.
doca_flow_port_cfg_set_devargs() carries other per-port options
(the shipped flow_common.c passes "th_win_us=0"), not
identity; a numeric devargs string like "0" leaves the port_id
unset and doca_flow_port_start() aborts with "port ID is
mandatory". Only doca_flow_port_cfg_set_port_id() binds the port
(flow_common.c), and it is a different "devargs" from the
EAL probe devarg dv_flow_en=2 in step 5 — three unrelated uses of
the word. In VNF mode, pair it with the matching probed doca_dev
via set_dev() — mandatory, not HWS-only. In switch mode, use the
installed sample's matching doca_dev_rep path instead. Omitting both
device sources aborts with "either doca_dev or doca_dev_rep must be
provided" (engine_port.c, sometimes surfaced as a bare "Unknown
error"). The port_id MUST be the DOCA-probed id from above (per
the hardware-neutral rule earlier in this step); a hard-coded 0
surfaces as doca_dpdk_port_as_dev(0): Requested Resource Not Found.
Do this for every DPDK port the app uses, in the order the DPDK port
IDs come out (rte_eth_dev_count_avail() / RTE_ETH_FOREACH_DEV(); IDs
are 0..N-1 in -a <BDF> attach order). The lifecycle is cfg created →
port_id set → mode-appropriate device source set → port started → cfg
destroyed; do not create
pipes before the port reports started, and any DOCA_FLOW_FWD_PORT
destination port must be started before a pipe forwards to it.
7. Sanity check before any pipe work. Confirm with the user: ingress
port, egress representor(s), traffic class. If unclear, stop and ask —
do not invent.
If any step fails with a DOCA_ERROR_*, route through
CAPABILITIES.md ## Error taxonomy
before retrying.
build
Goal: construct a pipe specification that expresses the user's intent without committing to hardware yet.
- Restate the user's intent in match/action terms ("match dst MAC = X, forward to representor Y, count").
- Verify each match kind and action kind against the active
capability set from
CAPABILITIES.md ## Capabilities and modes. If the device does not support a kind, stop and offer alternatives; do not generate a spec that will fail at validate. - Prepare the
doca_flow_pipe_cfgand its explicit match-mask and action-mask declarations, but do not calldoca_flow_pipe_create()in this phase. The constructor is the validation boundary owned by## test; calling it here would collapse build and test. Implicit-anything is the leading cause of misprogrammed steering. - Attach a counter to every entry the user wants to observe in
production;
## debugassumes counters exist. PerCAPABILITIES.md ## Observabilitycounters are a two-step setup: BOTH the per-entry monitor attachment AND a global pool reservation are required, or the counter silently produces no telemetry. Reserve the pool once atdoca_flow_inittime — for NON_SHARED per-entry counters viadoca_flow_cfg_set_nr_counters(cfg, N)(sizeNto the sum ofNON_SHAREDmonitor entries across every pipe plus ~10% headroom); for shared counters / meters via the shared-pool path in## shared-resources:
struct doca_flow_cfg *cfg;
doca_flow_cfg_create(&cfg);
doca_flow_cfg_set_nr_counters(cfg, N); /* MANDATORY for NON_SHARED per-entry counters */
doca_flow_init(cfg);
doca_flow_cfg_destroy(cfg); The pool reservation above is only step one. The per-entry step
is to pass the same populated &monitor (with monitor.counter_type = DOCA_FLOW_RESOURCE_TYPE_NON_SHARED) as the monitor argument of the
pipe-type add-entry call — it is slot 6 of the 10-argument
doca_flow_pipe_basic_add_entry(queue, pipe, match, action_idx, actions, monitor, fwd, flags, usr_ctx, &entry) (doca_flow.h).
Passing NULL there is the silent-no-counter trap: the entry is added
and the call returns DOCA_SUCCESS, but doca_flow_resource_query_entry
keeps returning total_pkts == 0 under live traffic because no
per-entry counter was ever bound. For miss-path traffic the entry
API does not apply — enable the counter on the pipe-cfg with
doca_flow_pipe_cfg_set_miss_counter(cfg, true) (doca_flow.h)
and read it back with doca_flow_resource_query_pipe_miss
(doca_flow.h). Read fields sit under the counter union arm:
q.counter.total_pkts / .total_bytes, not top-level
(struct doca_flow_resource_query, doca_flow.h). Reading a counter
does not require a preceding doca_flow_entries_process() — the HW
counter updates asynchronously; entries_process drives entry-completion
callbacks, not counter refresh.
The adjacent fwd argument (slot 7) has the opposite NULL polarity
from monitor: when the pipe was created with a fixed forward, pass
NULL here and the entry inherits the pipe's forward. Only pass an
explicit per-entry doca_flow_fwd when the pipe's fwd was declared
changeable — each explicit per-entry forward is its own action handle, so
inheriting keeps the per-entry action set (and HW footprint) smaller.
Copy the add-entry argument list from the header, never from
memory. The *_add_entry() calls take 10+ positional arguments and
the count/order are pipe-type-specific: BASIC takes 10, LPM takes
11 (an extra match_mask right after match), ACL / CONTROL / HASH
each differ. The most-missed argument is uint8_t action_idx (slot 4
of doca_flow_pipe_basic_add_entry) — it selects one template from the
actions[] / monitors[] / fwds[] arrays registered at
pipe-creation time (pass 0 when there is a single template). Grep the
header and copy the declared list verbatim rather than reconstructing
it (the one-liner is in
SKILL.md ## Ground rule).
Enabling decap/encap is a two-field action, not one. Filling
actions.decap_cfg (e.g. decap_cfg.is_l2 = true) describes how to
decap but does not turn it on — you must also set actions.decap_type = DOCA_FLOW_RESOURCE_TYPE_NON_SHARED (the enable used across every
shipped app — grep decap_type = in the sample/test tree). The same
pairing holds for encap_type / encap_cfg. A pipe that sets only
*_cfg compiles and runs, but the header is never stripped/added.
5. Do not call the entry-add API yet — the spec is built, not
programmed. Hand off to ## test for validation.
When the user asks for a "first Flow app": both tracks require a
DOCA-installed Linux environment (bare-metal, VM, or NGC container) where
the agent can read the sample tree and pkg-config --modversion doca-flow
resolves. If that precondition is not met, route to
doca-setup ## no-install before
offering any source. Do not author a Flow application from prose, in
any language — ground rule #3 of AGENTS.md and
SKILL.md forbid
it; the result would not compile against a real install.
Track 1 — C / C++ consumers (the canonical case)
This is the universal derive a custom first app from a sample pattern in
doca-programming-guide ## modify,
with these Flow-specific overrides:
- Source sample (choose by the first-app shape). Pick the sample whose
forward action is closest to what the user wants and modify it; do not
scaffold from prose:
- Match-and-forward-to-port (simplest VNF case).
/opt/mellanox/doca/samples/doca_flow/flow_port_fwd/— send matched traffic out a specific DOCA port. - Match-and-pass-traffic-back-to-the-kernel (inline-filter /
traffic-gate case).
/opt/mellanox/doca/samples/doca_flow/flow_fwd_target/— the safest starting point for any demo on a port carrying live host traffic: matched traffic is observed (counters, drop) without being diverted. It usesDOCA_FLOW_FWD_TARGETwith a kernel target fromdoca_flow_get_target(DOCA_FLOW_TARGET_KERNEL, …)— seeCAPABILITIES.md ## Forward-to-target actionsfor the pattern + safety rationale. Canonical wiring (verbatim shape from the shipped sample — the agent quotes this and does not invent the field names):
- Match-and-forward-to-port (simplest VNF case).
struct doca_flow_fwd fwd = {0};
struct doca_flow_fwd fwd_miss = {0};
struct doca_flow_target *kernel_target;
doca_error_t result;
result = doca_flow_get_target(DOCA_FLOW_TARGET_KERNEL, &kernel_target);
if (result != DOCA_SUCCESS) { /* report + bail per CAPABILITIES.md ## Error taxonomy */ }
fwd.type = DOCA_FLOW_FWD_TARGET;
fwd.target = kernel_target; /* matched traffic continues to the kernel */
fwd_miss.type = DOCA_FLOW_FWD_TARGET;
fwd_miss.target = kernel_target; /* unmatched traffic ALSO continues to the kernel —
this is what makes the filter "inline" and safe
to enable on a live management port */- **Switch mode + representor (multi-VF eswitch).**
`/opt/mellanox/doca/samples/doca_flow/flow_switch_single/` (helpers
in `flow_switch_common.{c,h}`).
ls the directory and read the actual sample on the user's install
before describing it; layouts change between releases.
- Forward by ingress representor: ONE pipe + one entry per source
port, never a pipe per rule. Match "traffic from
pf0vfX" onmatch.parser_meta.port_id(the source representor's logicalport_idfromdoca_flow_port_cfg_set_port_id) — leave it changeable in the pipe match and set the value per entry. Create a single BASIC pipe ondoca_flow_port_switch_get(), size it withdoca_flow_pipe_cfg_set_nr_entries(cfg, N), then add one entry per source rep whose own per-entryfwdisDOCA_FLOW_FWD_PORTto the destination representor'sport_id, each carryingmonitor.counter_type = DOCA_FLOW_RESOURCE_TYPE_NON_SHAREDfor the per-VF counter. "Drop any other VF" is the pipe'sfwd_miss(DOCA_FLOW_FWD_DROP), not an extra pipe — leaving the entry match empty steers nothing (the classic switch-app bug). Insert the entries as a batch (DOCA_FLOW_ENTRY_FLAGS_WAIT_FOR_BATCH, last oneDOCA_FLOW_ENTRY_FLAGS_NO_WAIT) then drain once withdoca_flow_entries_process(). Keep the sample's init/teardown, the per-entry status callback, and the validation flow in## test; swap only the literals (port ids, pipe name viadoca_flow_pipe_cfg_set_name()). - Build — link more than
doca-flow.pkg-config --libs doca-flowomits the companion modules, so linking it alone fails withundefined reference(e.g.doca_error_get_descr,doca_dpdk_port_as_dev) +DSO missing from command line. Mirror the shipped sample's dependency set:doca-common+doca-argp+ the library (samples/meson.build), plusdoca-dpdk-bridge(providesdoca_dpdk_port_*) +libdpdkfor any DPDK-backed app. Outside the meson tree, build a manifest in the user's project with adependency()per module called (meson canonical; cmake/autotools work the same; discover names viapkg-config --list-all | grep doca) — don't copy a template out of this skill. Compile with-D DOCA_ALLOW_EXPERIMENTAL_APIby default: most ofdoca_flow.his experimental-tagged, so omitting it is a common first-build failure; it's a compile-time switch, not runtime code.
Track 2 — Other languages (Rust, Go, Python, …)
NVIDIA ships no verified non-C first-app sample and neither does this skill, so the agent does not author the wrapper from prose — it makes the consumer understand the C ABI their wrapper calls and routes the FFI work back to them:
- API surface = the public C ABI. Cite the
DOCA Flow Programming Guide
for behavior/lifecycle and the installed headers
(
$(pkg-config --variable=includedir doca-common)/doca_flow*.h) for signatures. A wrapper honors the same lifecycle, capability-gate, and validation rules inCAPABILITIES.md/## test— library properties, not language properties. - Bindings / FFI. Point at a community binding only after verifying it
exists (fetch its repo — never invent a name); otherwise it is direct FFI
(
bindgen,cgo,cffi/ctypes) againstpkg-config --cflags --libs doca-flow. Read the C samples regardless — the order of API calls (e.g.flow_port_fwd/) is language-independent, and## test/## run/## debugapply unchanged.
Route tables — IP routing / longest-prefix match (LPM)
For destination-prefix routing — a route table with mixed prefix lengths
(10.0.0.0/8, 192.168.0.0/16, …) — use an LPM pipe (flow_lpm/
sample). It is purpose-built for longest-prefix match, supports per-entry
counters, and forwards each prefix to its own destination.
- Add routes at runtime with
doca_flow_pipe_lpm_add_entry()and remove them withdoca_flow_pipe_remove_entry()— no pipe rebuild and no app restart. Unmatched destinations follow the pipe's miss behaviour; set it toDOCA_FLOW_FWD_DROPfor a default-drop router. - LPM cannot be a root pipe — front it with a basic or control pipe.
- Batch route changes. The LPM table is recomputed on each flush, so
flushing every route separately is slow for a large or rapidly-changing
table. Apply route changes as a batch and commit them with a single
doca_flow_entries_process()(see## runstep 3). For a small, static table this is not a concern.
modify
Goal: change an existing pipe — adding, removing, or rewriting entries — without taking the steering plane offline.
- Read current pipe statistics and counters before any change; the diff after is what tells the user whether the change did what they meant.
- Re-run capability discovery if the action set changes. A new
action kind (e.g.
encap) requires re-checking the capability matrix and re-validating against the new shape — see item 3 inCAPABILITIES.md ## Safety policy. - Construct a delta spec, not a full re-spec — removing and re-adding a pipe is much heavier than adding/removing entries.
- Validate the modified spec via the same path as
## test. - Commit in the smallest unit (one entry at a time for live pipes carrying production traffic).
- Re-read counters and statistics; confirm the diff matches intent.
run
Goal: program the validated spec into the hardware and observe that traffic does what it should.
Confirm
## testhas passed; do not enterrunfrom an un-validated spec.Confirm the port was started via
doca_flow_port_start(port_cfg, &port)during## configureand the pipe was created (validated) during## test. Pipes have no separate start operation. Lifecycle is port started → pipe created (validated) → entries added; out-of-order calls produceDOCA_ERROR_BAD_STATE.Add entries in the order the user's intent implies (most specific match first when declared priority is not honored; otherwise the priority field). After adding, call
doca_flow_entries_process()to push them to hardware. Its return value is a count, not a success flag:n >= 0is the number of entries whose status callback fired — HWS often completes the install synchronously, son == 0is normal and healthy; onlyn < 0is an error. Treating0as fatal is a common bug.When adding entries in bulk, drive this loop for insertion rate, not just correctness: give each core its own
queueid (sharing one across cores is undefined behavior), add withDOCA_FLOW_ENTRY_FLAGS_WAIT_FOR_BATCHand calldoca_flow_entries_process()once per batch (reserve0/_NO_WAITfor small or latency-sensitive inserts), and spread entries evenly across queues — skew can fragment the action-handle cache and shrink effective pipe capacity.Stage entries on a single representor before widening to all — safety policy item 2 in
CAPABILITIES.md ## Safety policyfor hairpin pipes; the same discipline applies to any high-fanout pipe carrying production traffic.Read counters under expected traffic. If they do not move, jump to
## debug.Keep
main()alive — but only after the datapath is armed. A DOCA Flow program offloads steering to hardware and must stay running — ifmain()returns shortly after init (or sleeps for a fixed time), the rules are torn down and traffic stops. Enter a long-running, signal-aware loop after setup (the shipped sample shape:while (!force_quit) { doca_flow_entries_process(...); sleep(1); }withSIGINT/SIGTERMhandlers), and confirm the loop is reachable frommain()'s actual call graph before the ordered teardown. Before entering it, drivedoca_flow_entries_process()only until every expected status callback fires or the finite deadline copied from the installed shipped sample/application expires. Missing or failed callbacks at that deadline are a hard abort. This bounded callback-drain is not the keep-alive loop and is never retried after its deadline.Keeping
main()alive is NOT success. The keep-alive loop is the last gate, not a substitute for the probe-before-count gate in## configurestep 6 — that gate (probe every device before counting,rte_eth_dev_count_avail()≥ expected, mandatoryport_idplus the mode-appropriate device source) must already have passed, and the binary must exit non-zero rather than loop over a bridge that cannot forward. Two run-time preconditions the loop additionally requires:- when two PFs are bridged, the
doca_flow_port_paircalls in BOTH directions returnedDOCA_SUCCESS(doca_flow.h); - every
DOCA_FLOW_FWD_PORTentry's status callback fired withDOCA_FLOW_ENTRY_STATUS_SUCCESS(doca_flow.h), and the per-entry counter from## teststarts incrementing under controlled traffic (doca_flow_resource_query_entry,doca_flow.h). A binary that reaches the loop with the gate unmet — count 0, a missingset_port_id/ mode-appropriate device source, a faileddoca_flow_port_pair, or no entry-status confirmation — is forwarding nothing while appearing to run, exactly the shape that lets a watchdog mark a broken binary "alive". It is a hard error; the program must abort. If only onedoca_flow_port_pairdirection succeeds, treat the pair as failed: do not add entries or enter the keep-alive loop, perform ordered teardown, and report both directional return values.
- when two PFs are bridged, the
test
Goal: validate a pipe spec — and the system context around it — before hardware programming.
## test is a bounded validation loop, not a one-shot pass. Run the
4 steps in order and, when a cross-check or counter-wiring finding mutates
the spec, loop back to step 1 once — every mutation re-opens validate.
If that second pass finds another required mutation, stop and surface the
unresolved spec/capability mismatch using the loader's REFUSED shape:
name the conflicting capability, the fields changed on each pass, and
the closest legal spec. Do not iterate again. Skipping the
re-validate after the first mutation is exactly the failure mode
CAPABILITIES.md ## Safety policy
validate-before-commit exists to prevent.
- Pipe spec validation. Validation is constructor-time: there is no
separate
doca_flow_pipe_validateAPI —doca_flow_pipe_createitself rejects an inconsistent spec (DOCA_ERROR_INVALID_VALUE/DOCA_ERROR_NOT_SUPPORTED). Build the pipe with the staged-entry / dry-run pattern from a shipped sample and confirm the constructor returnsDOCA_SUCCESSbefore any entry-add call. This is the "validate before commit" rule fromCAPABILITIES.md ## Safety policy. - Capability cross-check. Re-confirm every match kind, action kind, and tunnel header in the spec is supported by the active configuration and firmware. Validation answers "is the spec internally consistent"; this answers "will the hardware accept it". A change here loops back to step 1.
- Counter wiring check. Walk the spec and confirm every entry the
user wants to observe has a counter attached (
## debugassumes they exist). Adding one loops back to step 1. - Negative test. Construct one deliberately failing pipe spec (wrong
match kind, unsupported action) and confirm
doca_flow_pipe_createrejects it — the cheapest way to detect a wrong-version Flow library before going live. If it does not reject, escalate to## debug.
debug
Goal: investigate "traffic is not doing what I asked" and arrive at a root cause that is either fixable in the spec or escalatable.
Routing summary. This anchor is the Flow-specific debug overlay: counters, pipe statistics, programmed-state inspection, Flow's
DOCA_ERROR_*mapping. For the cross-cutting debug ladder (install / version / build / link / runtime / program / driver) and its tooling (gdb,valgrind,--sdk-log-level, container-vs-native debug, core dumps, Developer Forum escalation), seedoca-debug ## debug. Walk the cross-cutting ladder first when the symptom layer is unknown; this Flow overlay layers on top once the symptom is confirmed inside the Flow API.
Step 0 — did the port even start? (placement gate before the counter
ladder). The counter-first ladder below assumes a started port. If
doca_flow_port_start — or the first switch-port doca_flow_pipe_create
— fails with the Failed to get hws cap / dest action ROOT … err -121
signature, stop: this is not a counter / spec / data-plane bug and
none of the steps below apply. It is the device-placement signature (full
details in CAPABILITIES.md ## Error taxonomy):
the opened function has no hardware-steering plane, almost always host-side
on a BlueField in SEPARATED_HOST / NIC mode, and it reproduces on
every device in both vnf and switch modes. Route back to
## configure step 2; the fix is to run Flow on the DPU Arm
side or change the card's mode via
doca-hardware-safety — never a
pipe-spec edit.
Walk in this order — do not skip steps:
- Counters first. Read the entry-level counters (built in
## buildstep 4). If the counter for the suspected entry is zero, the entry is not matching. Stop blaming the data plane; the spec is wrong. - Pipe statistics second. If counters are non-zero but behavior is
still wrong, read the pipe-level statistics from
CAPABILITIES.md ## Observabilityto determine whether the pipe itself is healthy. - What's actually programmed / verbose logs third. To inspect what
the hardware actually has programmed, route to the
doca-flow-tunesibling skill, which owns the programmed-state surface. For verbose runtime logging during active debugging, build/link the trace flavor (pkg-config doca-flow-trace) and run with--sdk-log-level 70; the runtime mechanics live indoca-debug CAPABILITIES.md ## Observability. - Error code mapping. Any
DOCA_ERROR_*returned during the investigation routes through the taxonomy inCAPABILITIES.md ## Error taxonomy. The cross-library taxonomy (which family routes to which debug layer) is owned bydoca-programming-guide CAPABILITIES.md ## Error taxonomy; this Flow file only adds the Flow-specific overlay. - Version sanity. If a spec fails or behaves differently across
sessions, confirm the installed DOCA version did not change. The
four-source version-coherence check (
pkg-config --modversion doca-common,cat /opt/mellanox/doca/applications/VERSION,doca_caps --version, BFB version) is owned bydoca-debug ## debugLayer 2; the version-detection mechanics live indoca-public-knowledge-map ## Where to find the version. A library upgrade between sessions is a common and easy-to-miss cause. - Escalation criteria. If counters move correctly but observed
behavior is still wrong AND what's programmed (step 3) matches the spec
AND the version is unchanged, the bug is below the Flow API surface
(driver or firmware). Stop attempting Flow-spec changes; capture state per
doca-debug ## test(the read-only triple) and escalate viadoca-debug ## debugWhere to ask for help to the public DOCA Developer Forum.
flow-ct
The DOCA-Flow-CT-specific overlay on the parent verbs. Use AFTER
the parent's ## configure → ## debug
sequence has been walked for the stateless doca-flow port; this
section adds only what CT changes on top. For the capability
surface, layering rule, the single doca_flow_ct_cap_is_dev_supported
device-support query, CT-specific error overlay, and safety
policy, see
CAPABILITIES.md ## flow-ct.
configure overlay.
- Slot CT init BEFORE port start. The order is
doca_flow_init→doca_flow_ct_init(global, one-time) → port start (layering rule inCAPABILITIES.md ## flow-ct). If doca-flow isn't initialized yet, route back to## configureFIRST — CT extends, it does not replace. CT cannot be bolted onto an already-running port: if ports are started, CT init must move earlier. - Device-support check for CT. Call
doca_flow_ct_cap_is_dev_supported(devinfo)against the activedoca_devinfo. This is the ONLY CT cap query — there is no per-axisdoca_flow_ct_cap_*family for flow count, aging range, NAT variants, or overlays; do not invent one. If the device does not support CT, surface that and stop. - Size the CT module to the user's expected peak, not
average. CT entries persist until aging evicts them or
policy removes them. Set the table sizing through the
doca_flow_ct_cfg_set_*setters sized to the peak concurrent-flow estimate; oversubscription surfaces asDOCA_ERROR_FULL/_NO_MEMORYat runtime. - Build and init the CT cfg. Create
struct doca_flow_ct_cfgviadoca_flow_ct_cfg_create+ thedoca_flow_ct_cfg_set_*setters, then calldoca_flow_ct_init(cfg)once for the process (ordering per step 1; there is NO per-port CT context). - Configure NAT direction / actions through
doca_flow_ct_cfg_set_directionand the CT actions. There is no per-variant cap query, so confirm SNAT / DNAT / combined behavior only on a dedicated test representor/VF or isolated ports carrying no production traffic. Snapshot counters first and stage one entry; an unsupported request surfaces as_NOT_SUPPORTEDat entry add. Never probe a NAT variant on a live production port. - Start the ports, then attach CT-aware pipes. With the
global CT module initialized, start the doca-flow ports and
wrap existing doca-flow pipes with their CT-aware versions.
The original stateless pipes remain valid; CT-aware pipes are
added on top. CT is not a
doca_ctx; there is nodoca_ctx_starton a CT object.
build overlay.
| Slot | Value |
|---|---|
| pkg-config modules | CT adds no separate module: keep every companion module required by the parent Flow/DPDK build and include doca-flow; there is no doca-flow-ct module |
| Version anchor | pkg-config --modversion doca-flow MUST equal doca_caps --version. Mismatch → escalate to doca-version TASKS.md ## debug layer 2 BEFORE diagnosing the CT layer itself |
| Header includes | Add doca_flow_ct.h to the doca-flow headers the parent ## build prescribes; it ships in the same doca-sdk-flow devel package |
.pc discovery |
`pkg-config --list-all |
modify overlay. Take the closest shipped CT sample (an installed CT sample in the public DOCA samples bundle whose 5- tuple shape, NAT requirement, and overlay encapsulation match the user's workload), apply a minimum-diff onto the user's existing doca-flow setup:
- Do NOT recreate the user's pipe scheme from scratch — port the sample's CT bookkeeping (context creation, aging-table sizing, CT pipe-builder wrap calls) onto the existing flow.
- There is no per-variant or per-overlay CT cap-query — the only
CT cap symbol is
doca_flow_ct_cap_is_dev_supported(devinfo). Confirm new NAT variants (SNAT, DNAT, combined) and overlay shapes empirically against the shipped CT sample, not via fabricateddoca_flow_ct_cap_*axes. - After modify: rely on
doca_flow_pipe_create's constructor-time validation (any spec inconsistency surfaces as a constructor failure withDOCA_ERROR_INVALID_VALUEorDOCA_ERROR_NOT_SUPPORTED) plus the staged-entry / dry-run pattern from the shipped CT sample. There is no separatedoca_flow_pipe_validatesymbol in the public surface — do not invent it.
run / test overlay. Per
CAPABILITIES.md ## flow-ct:
- Single-flow CT smoke. ONE 5-tuple, ONE CT entry add, ONE
matching traffic flow, ONE counter increment. The parent stateless
pipe must already pass constructor-time validation
(
## teststep 1) and its counter smoke (## runstep 5) first. - Multi-flow smoke ONLY after single-flow is green: a small set of distinct 5-tuples (e.g. 16), one entry per flow, confirm per-CT-entry counters increment in lockstep with the matching traffic. All CT smokes below use the isolated test path selected in configure step 5, never production traffic.
- Aging smoke with a deliberately short aging timer accepted by the installed CT configuration setter/sample: add a CT entry, send one matching packet, stop traffic, wait at least the aging period plus granularity, confirm the entry is evicted via the per-CT-entry counter query.
- NAT-aware smoke (one requested variant at a time): add an entry whose SNAT, DNAT, or combined action is accepted by the installed sample/API, confirm the outbound traffic carries the rewritten 5-tuple, confirm reverse traffic is matched on the original tuple.
- Negative tests the agent should propose explicitly: pass one
value documented as invalid by the installed CT header/sample and
expect
_INVALID_VALUE; request one action shape rejected by that installed API and expect_NOT_SUPPORTED. Exercise_FULLonly in an isolated test sized to the configured table, never by guessing a device maximum or filling a live table. - Sustained-run loop ONLY after all four smokes are green:
stream traffic that exercises CT entry add / aging eviction
in a continuous loop while watching per-CT-entry counters,
per-entry aging timestamps, and connection-state transitions
per
CAPABILITIES.md ## flow-ct. This is the only stage where the agent should propose running the user's full workload.
debug overlay. Layered on the parent's ## debug
ladder:
DOCA_ERROR_BAD_STATEfrom a CT-layer call is always a layering / lifecycle violation:doca_flow_ct_initrun after a port was already started (it must precede port start), OR a CT entry add before the ports and wrapped pipes are up, ORdoca_flow_ct_destroycalled out of order. Walk the lifecycle in this order — doca_flow_init → doca_flow_ct_init → port-start → ct-entry-add → … → port-stop → doca_flow_ct_destroy → doca_flow_destroy — BEFORE inspecting any individual CT call.DOCA_ERROR_FULLon entry add is always a table-sizing / aging-pressure mismatch with the workload. Read the per-CT- entry counters to identify stale entries; either wait for one aging interval, then evict one confirmed-stale entry if policy permits. Retry the failed add once. If it is still full, stop and surface the configured-table/device-fit gap; do not continue an eviction/retry loop.- Traffic not matching a freshly-added CT entry is almost
always a 5-tuple-shape disagreement between the entry add
and the traffic on the wire. Read both sides verbatim — the
cap-query is NOT the right diagnostic here; the entry shape
vs traffic shape comparison is. Same shape as the parent
"pipe matches nothing" diag in
## debug, with CT's 5-tuple match instead of an arbitrary pipe match. - NAT translation conflicts surface as
DOCA_ERROR_INVALID_VALUEon entry add. PerCAPABILITIES.md ## flow-ct, do NOT invent a translation to resolve the conflict — surface the policy conflict to the user. - An aging value rejected by the installed CT configuration setter
surfaces as
DOCA_ERROR_INVALID_VALUEat context configure (NOT at entry add). Quote the accepted constraints from the installed header/sample and the setter return; there is no separate numeric cap-range query.
rollback overlay. A CT add is a pipeline-edit-class mutation (it extends
an already-up stateless port; it does not touch firmware or eswitch mode), so
the doca-hardware-safety overlay does
NOT fire — CT follows the same snapshot-first rollback discipline as
## rollback, with a CT-specific reversal. Snapshot before
mutate: record the stateless pipe scheme the agent authored (names, root
status, match/action specs, monitor/counter attachment, miss-pipe linkage)
plus the pre-CT per-entry / per-pipe-miss counter baseline
(doca_flow_resource_query_entry / doca_flow_resource_query_pipe_miss);
that record IS the rollback target.
- Document the reversal in the same answer that recommends the CT
add: (a)
doca_flow_ct_rm_entryon every CT entry added since the snapshot, in reverse-add order; (b)doca_flow_pipe_destroyon every CT-aware pipe wrapped on top of the stateless pipes, in reverse-create order; (c) once the ports are stopped,doca_flow_ct_destroy()(global, no arguments) BEFOREdoca_flow_destroy. There is no per-port CT context and nodoca_ctx_stopfor CT. The original stateless pipes are untouched by entry / CT-pipe teardown and must remain valid; a stateless-pipe edit made in addition to the CT add needs its own rollback step first. - Trigger and re-verify per
## rollbacksteps 2–3, with the single-flow CT smoke (run/test overlay step 1) as the contract's smoke probe: walk the rollback if it is not green within the bounded debug-loop iteration (mandatory on the second non-green), then re-verify the restored stateless pipe under traffic against the snapshot shape.
shared-resources
The verbatim API call sequence for using DOCA Flow's shared-resource
pools (encap / decap / counter / meter / RSS / IPsec-SA / PSP). For the
concept — when to reach for shared, the SHARED-vs-NON_SHARED
decision, the kinds table, the broken-hybrid scaling failure, and the
many-flows / few-distinct-tunnels sizing rule — see
CAPABILITIES.md ## Shareable resources.
This section is the implementation overlay; the layering rule is (a)
decide via that section first, (b) walk the four steps below verbatim,
(c) pkg-config --modversion doca-flow matches the install per
## configure step 1.
The 4-step pattern (identical for every kind — the example uses
ENCAP; substitute the matching kind from the table):
/* 1. Reserve K slots BEFORE doca_flow_init() — count first, then type. */
doca_flow_cfg_set_nr_shared_resource(cfg, K, DOCA_FLOW_SHARED_RESOURCE_ENCAP);
/* 2. Populate each slot after init (one call per distinct config). */
doca_flow_shared_resource_set_cfg(DOCA_FLOW_SHARED_RESOURCE_ENCAP, id, &res_cfg);
/* (port-scoped form: doca_flow_port_shared_resource_set_cfg(port, type, id, &res_cfg)) */
/* 3. Bind the slot range to the port once at startup. */
uint32_t ids[K] = { 0, 1, /* … */ K - 1 };
doca_flow_shared_resources_bind(DOCA_FLOW_SHARED_RESOURCE_ENCAP, ids, K, port);
/* 4. Reference from each entry: set the type marker AND the id.
* The id field is kind-specific (confirm in doca_flow.h): */
actions.encap_type = DOCA_FLOW_RESOURCE_TYPE_SHARED;
actions.shared_encap_id = slot_id; /* encap */
/* decap: actions.decap_type = ...SHARED; actions.shared_decap_id = slot_id; */
/* counter: monitor.counter_type = ...SHARED; monitor.shared_counter.shared_counter_id = slot_id; */
/* meter: monitor.meter_type = ...SHARED; monitor.shared_meter.shared_meter_id = slot_id; */Reference samples: flow_vxlan_shared_encap/, flow_shared_meter/,
flow_shared_counter/ under /opt/mellanox/doca/samples/doca_flow/ —
the agent reads the relevant sample before quoting the per-entry field
names (the actions.shared_encap_id slot is kind-specific and the
header is the source of truth, per
SKILL.md ## Ground rule).
Per-entry HW-cost reminder. With this overlay the HW resource cost
scales with K distinct configs, not N entries. Skipping any of
the four steps but writing the entry-side *_type = DOCA_FLOW_RESOURCE_TYPE_SHARED line compiles cleanly and dies at the
first entry-add (the broken-hybrid failure mode in
CAPABILITIES.md ## Shareable resources).
rollback
Every Flow pipe / entry / action add that modifies an already-up port
is pipeline-edit-class and needs the same rollback discipline as CT.
This anchor covers stateless-pipe edits: VLAN push/pop, encap/decap,
modify-header, NAT-without-CT, hairpin attach. (CT
additions route through ## flow-ct rollback overlay
instead.)
The
universal verification contract
requires "the rollback path is documented" on every change-recommending
answer.
Snapshot before mutate. Before any change-recommending edit on an
already-up port, record (a) the pipe scheme the agent authored for every
affected pipe (root status, match / action specs, monitor/counter
attachment, miss-pipe linkage), (b) the per-pipe counter baseline
(doca_flow_resource_query_pipe_miss + doca_flow_resource_query_entry),
and (c) the device cap snapshot the edit gated on (doca_caps --list-devs
- Flow cap query results). For an independent view of what is actually
programmed, route to
doca-flow-tune. That record IS the rollback target.
- Document the reverse-edit verb in the same answer that recommends the
edit, by action kind: (a) entry add → matching
doca_flow_pipe_remove_entryin reverse-add order; (b) action add/modify (VLAN push, encap, modify) →doca_flow_pipe_destroyon the modified pipe + recreate from the recorded pipe spec; (c) pipe add →doca_flow_pipe_destroyin reverse-create order; (d) monitor / counter attach —doca_flowexposes no publicdoca_flow_monitor_destroy, so the reversal isdoca_flow_pipe_destroydoca_flow_pipe_createwith adoca_flow_pipe_cfgthat does NOT calldoca_flow_pipe_cfg_set_monitor.
- Trigger from the deploy-loop bridge.
The contract's smoke probe is constructor-time validation
(
## teststep 1) plus a counter read under traffic (## runstep 5). If it does not return green (counter increment equal to the controlled generator's successfully transmitted packet count on the isolated lossless path + cleandoca_flow_pipe_createreconstruction) within the bounded debug-loop iteration, walk the rollback — on the second non-green iteration, rollback is mandatory. - After rollback, re-verify the restored pipe — constructor-time
validation (
## teststep 1) plus a counter re-read under traffic (## runstep 5). If counters do not resume the snapshot shape, the rollback itself is suspect — surface the residual gap instead of recommending another pipeline edit.
Hardware changes (mode flip, SR-IOV, firmware) are NOT pipeline edits and
route through
doca-hardware-safety's rollback
discipline instead.
Command appendix
Flow-specific commands the verbs above reach for, grouped by purpose
so the agent picks the right family without searching prose. Every row
is a class — the agent must not invent flags beyond what the row
names; the flag-discovery rule is --help against the installed
binary or pkg-config against the installed .pc, not prose recall.
| Purpose | Command | Owning step | Reads as healthy when … |
|---|---|---|---|
| Discover installed Flow version | pkg-config --modversion doca-flow |
## configure step 1 |
Matches the version pinned in other places (doca_caps --version, applications/VERSION). |
| Discover Flow link flags (release flavor) | pkg-config --cflags --libs doca-flow doca-common doca-dpdk-bridge libdpdk |
## build |
Returns the canonical -l list. doca-flow alone omits the companion DSOs → undefined reference / DSO missing from command line; pass every module the app calls. Hand-typed -l lines are the failure mode. |
| Discover device + steering capabilities | doca_caps --list-devs |
## configure step 2 + doca-caps |
Lists every device DOCA sees with the active configuration and supported match / action kinds. |
| Enumerate ports / representors | devlink dev show + the installed Flow port-enumeration sample |
## configure step 3 |
Shows the port and every representor the user expects to forward to. |
| Validate a pipe spec (read-only) | doca_flow_pipe_create itself performs constructor-time validation; treat its DOCA_ERROR_INVALID_VALUE / DOCA_ERROR_NOT_SUPPORTED return as the validate signal. There is no separate doca_flow_pipe_validate public symbol in the current release. Use the dry-run / staged-entry pattern from the shipped sample for a fuller validation surface. |
## test step 1 |
A successful return from doca_flow_pipe_create means the spec is internally consistent against the current device caps; an error means the spec is invalid — do not retry without changing the spec. |
| Raise log verbosity for a Flow run | --sdk-log-level 70 on the program command line |
## run + doca-debug CAPABILITIES.md ## Observability |
TRACE / DEBUG lines appear in stderr; the Flow lifecycle calls are visible. |
| Read per-entry / per-pipe counters | doca_flow_resource_query_entry() / doca_flow_resource_query_pipe_miss() |
## debug step 1 |
Counter for the suspected entry is non-zero under expected traffic. |
| Inspect what's actually programmed | route to doca-flow-tune |
## debug step 3 |
The programmed state matches the user's mental model of the pipe. Diff = bug. |
Three cross-cutting rules for this appendix:
- Never invent Flow flags. The Flow API is large and version-gated;
--helpon the installed binary andpkg-config --list-all | grep docaare the only safe sources. - Never paraphrase a Flow
DOCA_ERROR_*. Quotedoca_error_get_descr()verbatim — the layer-classifier indoca-debug ## debuglayer 5 needs the exact text. - Cross-cutting commands live in doca-debug. The read-only triple,
dmesg, andmlxconfig -d <pcie> qare indoca-debug TASKS.md ## Command appendix; reach there for them rather than restating them in this appendix.
Deferred task verbs
The following verbs are out of scope for this skill but are commonly asked in the same conversations. Route them as follows so the agent does not invent guidance:
- install. Installing DOCA, choosing packages, post-install
verification,
pkg-configwiring — defer to doca-public-knowledge-map ## Layout of an installed DOCA package. This skill assumes DOCA is already installed. - deploy. Deploying BlueField images, provisioning DPUs at scale,
Kubernetes operator workflows — out of scope for Phase 1 and reserved
for a future platform skill. For now, point the user at the DOCA
Platform Framework entry in
doca-public-knowledge-mapand stop there. - rollback. Coordinated steering-plane rollback across multiple
DPUs and host nodes — out of scope for Phase 1 and reserved for a
future platform skill. For single-DPU spec rollback within a session,
use the snapshot-first
## rollbackworkflow. Its reverse edit may be a## modifydelta, but only against the captured baseline and with the bounded re-verification defined there.