Skip to content

smoke: namespace isolation and parallel execution - #508

Merged
rjarry merged 8 commits into
DPDK:mainfrom
rjarry:smoke-parallel
Feb 18, 2026
Merged

smoke: namespace isolation and parallel execution#508
rjarry merged 8 commits into
DPDK:mainfrom
rjarry:smoke-parallel

Conversation

@rjarry

@rjarry rjarry commented Feb 17, 2026

Copy link
Copy Markdown
Collaborator

Run each smoke test in its own mount and network namespace via unshare. This allows parallel execution with make smoke-tests -jN, significantly reducing CI time.

Along the way, add interactive debugging support: INTERACTIVE=true starts a tmux session with dedicated windows for grcli, network namespaces and vtysh. GDB=true additionally runs grout under gdb.

  • Extract reusable helpers from cleanup (pause_for_debug, stop_grout, smoke_setenv)
  • Add tmux-based interactive mode with per-component windows
  • Add GDB support for grout in a tmux window
  • Isolate tests with unshare --mount --net --fork
  • Overlay FRR state dirs with per-test tmpfs mounts
  • Replace core_pattern with coredumpctl for crash analysis
  • Add per-test make targets for parallel execution
  • Swap CI rebase flags to run smoke on optimized builds

Summary by CodeRabbit

  • Tests

    • Dynamic per-test discovery with per-test execution, logging and clearer failure summaries
    • Interactive debugging via tmux, host-namespace isolation, explicit loopback setup and safer FRR isolation (tmpfs/per-test state)
  • Chores

    • CI/workflow tweaks: swapped matrix rebase settings, changed scripts to use bash, added timing wrappers and parallel smoke-test invocation
    • Tightened FRR runtime directory permissions for safer isolation

@coderabbitai

coderabbitai Bot commented Feb 17, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR updates CI and smoke-test orchestration. CI: check.sh switches to /bin/bash, wraps build/test/smoke invocations with timing and runs smoke-tests in parallel; check.yml flips rebase values for gcc-13/gcc-14 and adds systemd-coredump to dnsmasq install. Build: GNUmakefile now dynamically discovers smoke/*_test.sh, prunes FRR tests based on intro-buildoptions.json, and creates per-script phony targets. Tests: smoke/_init.sh adds unshared host-namespace flows, tmux-based interactive helpers, env propagation, grout tracking, and coredump handling; smoke/_init_frr.sh moves to ip netns, tmpfs FRR state, and tmux vtysh integration. A meson patch ensures FRR runtime dirs with mode 0700.

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@smoke/_init.sh`:
- Around line 84-90: The stop_grout function currently unconditionally runs kill
%?grcli and wait %?grcli which will error when no grcli background job exists
(e.g., INTERACTIVE=false); wrap those two calls in a guard that checks for the
job before acting (for example: use jobs %?grcli >/dev/null 2>&1 or jobs -p
%?grcli to detect existence) and only run kill %?grcli and wait %?grcli if the
job check succeeds so cleanup won’t print errors or abort under set -e.
- Around line 6-17: The mount and namespace setup commands are unreachable
because `exec env _SMOKE_UNSHARED=1 unshare ... "$0" "$@"` replaces the process
before lines like `mount --make-rprivate /` run; move those mount/setup commands
so they execute in the re-entered (unshared) process when `_SMOKE_UNSHARED=1`.
Concretely: keep the `if [ "${_SMOKE_UNSHARED:-}" != 1 ]; then exec env
_SMOKE_UNSHARED=1 unshare ... "$0" "$@"; fi` guard as the initial re-exec, and
then place the `mount --make-rprivate /`, `mkdir -p /run/netns`, `mount -t tmpfs
tmpfs /run/netns`, `touch /run/netns/host`, `mount --bind /proc/1/ns/net
/run/netns/host`, and `mount -t proc proc /proc` after that `fi` so they run
when `_SMOKE_UNSHARED=1`; reference `_SMOKE_UNSHARED` and the `exec env ...
unshare` line to locate where to move the commands.
- Around line 97-124: The stop routine currently loses the grout exit code and
always treats GDB as enabled; capture the exit status immediately after wait
"$grout_pid" by moving/localizing ret right after that wait (before any
subsequent commands like the GDB tmux block) and change the GDB check from the
truthy string test if [ "${GDB:-true}" ] to an explicit comparison such as if [
"${GDB:-true}" = "true" ] or [[ "${GDB:-true}" == "true" ]] so tmux kill-window
-t gdb only runs when GDB is actually enabled; update references around kill
-TERM "$grout_pid", wait "$grout_pid", local ret="$?" and tmux kill-window -t
gdb accordingly.

Comment thread smoke/_init.sh
Comment thread smoke/_init.sh
@rjarry
rjarry force-pushed the smoke-parallel branch 2 times, most recently from f6b0192 to aaf7650 Compare February 17, 2026 15:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@smoke/_init.sh`:
- Around line 6-17: The mount commands are unreachable because exec replaces the
process; relocate the block that performs mount --make-rprivate /, mkdir -p
/run/netns, mount -t tmpfs tmpfs /run/netns, touch /run/netns/host, mount --bind
/proc/1/ns/net /run/netns/host, and mount -t proc proc /proc so they run after
the if/fi (on re-entry when _SMOKE_UNSHARED=1). Concretely, keep the exec
unshare invocation as-is, then move the sequence of mount/mkdir/touch/mount
--bind/mount -t proc lines out of the if-body and place them immediately after
the fi so they execute in the child namespace where _SMOKE_UNSHARED is set.
- Around line 84-90: The stop_grout function currently runs "kill %?grcli" and
"wait %?grcli" which will error if no background grcli job exists; modify
stop_grout to first check for the existence of the job before killing/waiting
(for example test jobs -p %?grcli or inspect "jobs" output) and only call
kill/wait when that check succeeds, or alternatively use a safe process lookup
(pgrep/pkill with the grcli command pattern) to conditionally terminate; update
references in stop_grout to perform this guard so cleanup does not emit errors
when grcli was never started.
- Around line 97-104: The current assignment local ret="$?" occurs after the
if/fi that may run tmux and thus captures that block's exit code instead of the
grout process exit from wait "$grout_pid"; move the capture so you save the exit
status immediately after wait "$grout_pid" (e.g., right after the wait command
capture into ret from "$?") and only then run the conditional tmux block so the
grout exit code (from wait) is preserved; reference the commands kill -TERM
"$grout_pid", wait "$grout_pid", the local ret assignment, and the tmux if/fi
when making this change.

@rjarry
rjarry force-pushed the smoke-parallel branch 4 times, most recently from 81da3f8 to c7c207f Compare February 17, 2026 15:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/check.sh:
- Line 30: The echo line in check.sh prints a stray closing parenthesis; update
the echo command that currently uses "($(nproc)) parallel jobs)" to remove the
extra trailing ')' so the output becomes e.g. "smoke-tests (4) parallel jobs"
(for example change to echo "smoke-tests ($(nproc)) parallel jobs"). Locate the
echo statement in .github/workflows/check.sh and adjust the quoted string
accordingly.

In `@smoke/_init.sh`:
- Around line 299-301: The current calculation of cpu and affinity can divide by
zero when nproc returns 1; update the logic to first capture num_cpus="$(nproc)"
and branch: if num_cpus is <= 1 set cpu=0 and affinity="$cpu" (or "$cpu,$cpu"),
otherwise compute cpu="$((RANDOM % (num_cpus - 1)))" and
affinity="$cpu,$((cpu+1))". Change references to the variables cpu and affinity
accordingly so $RANDOM % (nproc - 1) is never evaluated with zero.

Comment thread .github/workflows/check.sh Outdated
Comment thread smoke/_init.sh
@rjarry
rjarry requested a review from rsafrono February 17, 2026 16:06
@rjarry
rjarry force-pushed the smoke-parallel branch 2 times, most recently from 7d16915 to 7b601b8 Compare February 17, 2026 22:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@smoke/_init.sh`:
- Around line 100-121: The wait/ret error-check block incorrectly runs in GDB
mode because grout_pid is a non-child PID; update the shutdown sequence to skip
the wait and error/status handling when GDB mode is enabled: detect GDB via the
existing "${GDB:-false}" check and, if true, avoid calling wait "$grout_pid",
avoid setting status or interpreting ret, and skip the coredumpctl/debugger
steps (leaving the tmux gdb cleanup intact); ensure normal behavior remains
unchanged when GDB is not true and continue to use grout_pid, wait, ret, status,
and coredumpctl in that branch.

---

Duplicate comments:
In `@smoke/_init.sh`:
- Around line 283-285: The current computation cpu="$(($RANDOM % ($(nproc) -
1)))" will divide by zero when nproc returns 1; change the logic in this block
to first read total="$(nproc)" and handle the single-CPU case by setting cpu=0
(or cpu=0 and affinity="$cpu" or "$cpu,$cpu") when total <= 1, otherwise compute
cpu as RANDOM % (total - 1) and set affinity="$cpu,$((cpu+1))"; update the
variables cpu and affinity (the ones referenced in this snippet) accordingly to
avoid the modulo by zero.

Comment thread smoke/_init.sh
Comment on lines +100 to +121
kill -TERM "$grout_pid"

set +x
echo "Waiting for grout (PID $grout_pid) to terminate ..."
wait "$grout_pid"
local ret="$?"

if [ "${GDB:-false}" = true ]; then
tmux kill-window -t gdb
fi

if [ "$ret" -ne 0 ]; then
status="$ret"
if [ "$ret" -gt 128 ]; then
local sig=$((ret - 128))
echo "fail: grout terminated by signal SIG$(kill -l $sig)"
else
echo "fail: grout exited with an error status $ret"
fi >&2
coredumpctl debug --no-pager -q "$grout_pid" \
--debugger-arguments="-batch -ex 'thread apply all bt'"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

wait "$grout_pid" in GDB mode operates on a non-child PID — returns 127, falsely marking every GDB run as failed.

In GDB mode (line 302 branch), grout_pid is obtained via pgrep -P $gdb_pid — grout is a grandchild of the current shell started through tmux new-window, not a direct child. In bash, wait <pid> on a non-child process returns 127 immediately. local ret="$?" on line 105 captures 127, the ret -ne 0 branch at line 111 triggers, status is overwritten with 127, and the test is reported as FAILED even when grout exited cleanly.

The fix is to skip the wait/error-check block entirely in GDB mode:

🐛 Proposed fix
 	kill -TERM "$grout_pid"
 
 	set +x
 	echo "Waiting for grout (PID $grout_pid) to terminate ..."
-	wait "$grout_pid"
-	local ret="$?"
 
 	if [ "${GDB:-false}" = true ]; then
+		wait "$grout_pid" 2>/dev/null || true
 		tmux kill-window -t gdb
-	fi
-
-	if [ "$ret" -ne 0 ]; then
-		status="$ret"
-		if [ "$ret" -gt 128 ]; then
-			local sig=$((ret - 128))
-			echo "fail: grout terminated by signal SIG$(kill -l $sig)"
-		else
-			echo "fail: grout exited with an error status $ret"
-		fi >&2
-		coredumpctl debug --no-pager -q "$grout_pid" \
-			--debugger-arguments="-batch -ex 'thread apply all bt'"
+	else
+		wait "$grout_pid"
+		local ret="$?"
+		if [ "$ret" -ne 0 ]; then
+			status="$ret"
+			if [ "$ret" -gt 128 ]; then
+				local sig=$((ret - 128))
+				echo "fail: grout terminated by signal SIG$(kill -l $sig)"
+			else
+				echo "fail: grout exited with an error status $ret"
+			fi >&2
+			coredumpctl debug --no-pager -q "$grout_pid" \
+				--debugger-arguments="-batch -ex 'thread apply all bt'"
+		fi
 	fi
 	set -x
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@smoke/_init.sh` around lines 100 - 121, The wait/ret error-check block
incorrectly runs in GDB mode because grout_pid is a non-child PID; update the
shutdown sequence to skip the wait and error/status handling when GDB mode is
enabled: detect GDB via the existing "${GDB:-false}" check and, if true, avoid
calling wait "$grout_pid", avoid setting status or interpreting ret, and skip
the coredumpctl/debugger steps (leaving the tmux gdb cleanup intact); ensure
normal behavior remains unchanged when GDB is not true and continue to use
grout_pid, wait, ret, status, and coredumpctl in that branch.

@rjarry
rjarry requested a review from aharivel February 18, 2026 09:51

@aharivel aharivel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unshare isolation is elegant. I love the idea !
That's a significant improvement.
Just tiny minors nit picking..

Comment thread smoke/_init.sh
fi
if [ -t 1 ]; then
# try to spread load on all CPUs
cpu="$(($RANDOM % ($(nproc) - 1)))"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single proc CPU are not existing anymore for sure but the division by zero is possible here !
If you think it should never happen, then it's fine.
Otherwise:

n=$(nproc)
cpu=$(($RANDOM % n))
affinity="$cpu,$(( (cpu + 1) % n ))"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it ever happens, we have a bigger problem I think :D

Comment thread smoke/_init.sh
for name in "${tmux_windows[@]}"; do
tmux kill-window -t "$name"
done
kill %?grcli

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put a kill %?grcli || true in case grcli failed at start.. (socket path issue, etc..) this would make it sure the script doesn't fail..

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is already executed with set +e, so it is fine. I just moved it in a separate function for readability.

@rsafrono

Copy link
Copy Markdown
Collaborator

Tested-by: Roman Safronov rsafrono@redhat.com

@aharivel aharivel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

In preparation for interactive tmux debugging and parallel test
execution, refactor the monolithic cleanup() function.

Move the pause-for-debug logic and the grout stop/crash-detection
sequence into standalone pause_for_debug() and stop_grout() functions.
Introduce smoke_setenv() to export environment variables, which will be
extended to propagate variables to tmux sessions.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Tested-by: Roman Safronov <rsafrono@redhat.com>
Reviewed-by: Anthony Harivel <aharivel@redhat.com>
When INTERACTIVE=true, start a tmux session and open dedicated windows
for each component: a grcli shell, one shell per network namespace, and
vtysh sessions for each FRR instance.

On test completion (pass or fail), pause before cleanup so the user can
inspect state in any window. Extend smoke_setenv() to propagate
environment variables into the tmux server so that all windows share the
same GROUT_SOCK_PATH and PATH.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Tested-by: Roman Safronov <rsafrono@redhat.com>
Reviewed-by: Anthony Harivel <aharivel@redhat.com>
When GDB=true, imply INTERACTIVE=true and start grout inside gdb in
a dedicated tmux window. Configure gdb to pass SIGTERM through to grout
so that stop_grout() can request a graceful shutdown.

Wait indefinitely for grout to open its control socket, giving the user
time to set breakpoints before issuing "run" in the gdb window. Once
grout is running, find its PID by querying the gdb tmux pane PID and
looking up its child process.

For non-gdb code paths, capture grout's PID via $! right after
backgrounding instead of using pgrep.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Tested-by: Roman Safronov <rsafrono@redhat.com>
Reviewed-by: Anthony Harivel <aharivel@redhat.com>
The FRR install script already creates etc/frr, var/log/frr and
var/lib/frr under the install prefix. Add var/run/frr with mode 0700 so
that FRR daemons can write pid and socket files without relying on
runtime directory creation.

Remove the duplicate var/lib/frr mkdir that was created without explicit
permissions.

The next commit will overlay these directories with per-test tmpfs
mounts for parallel test isolation. The mount points need to exist
before the tmpfs can be mounted on top.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Tested-by: Roman Safronov <rsafrono@redhat.com>
Reviewed-by: Anthony Harivel <aharivel@redhat.com>
Replace the named "grout" netns with unshare --mount --net --fork to
give each test its own mount tree and network stack. This enables
parallel execution without name collisions on network namespaces or
shared FRR state.

The previous commits made it possible to interact with the test
topology via tmux windows and grcli without relying on "ip netns exec
grout" from outside. The named netns is no longer needed and can be
replaced with an anonymous one from unshare --net.

Mount isolation:

* Make all mounts private to prevent propagation.
* Mount a tmpfs on /run/netns so each test starts with a clean
  namespace registry, free of stale host entries. Guard the mounts
  with _SMOKE_MOUNTS_DONE so they only run once, even when the
  script is re-executed by tmux.
* Pass the host netns as fd 3 (opened on /proc/1/ns/net before
  unshare) and bind-mount it to /run/netns/host so hardware port
  tests can move devices between the host and test network namespaces
  using "ip -n host".
* Overlay FRR state directories (etc/frr, var/log/frr, var/run/frr,
  var/lib/frr) with per-test tmpfs mounts so parallel FRR tests do not
  collide and no root-owned files are left in the build tree. Use
  mode=1777 since FRR daemons drop privileges to the user that compiled
  them (--enable-user).

Network isolation:

* Each test gets a fresh network namespace from --net, removing the need
  for the named "grout" netns.
* Sub-namespaces are created with plain "ip netns add" since we are
  already isolated.
* Hardware ports are moved in/out via the "host" netns reference.

Replace the core_pattern sysctl with coredumpctl for crash analysis.
The core_pattern sysctl is global (not per-namespace), so parallel
tests would race on it. Instead, rely on systemd-coredump and use
"coredumpctl debug" to get full thread backtraces on crash. Move
"ulimit -c unlimited" to the top so it applies to all processes
including those started in tmux windows. Add systemd-coredump to the
CI packages.

Drop the explicit "ip addr add 127.0.0.1/8 dev lo" and the grep check
that guarded it. The kernel automatically assigns 127.0.0.1/8 and
::1/128 when the loopback device is brought up (net/ipv4/devinet.c,
NETDEV_UP handler, since Linux 2.6.12). Only "ip link set lo up" is
needed.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Tested-by: Roman Safronov <rsafrono@redhat.com>
Reviewed-by: Anthony Harivel <aharivel@redhat.com>
Now that each test runs in its own set of namespaces, they can be
executed in parallel without interference.

Add individual phony make targets for each test script alongside the
existing run.sh runner. FRR tests are filtered out when the frr meson
option is not enabled, using the same jq check as run.sh.

Each target runs the test under sudo with output captured to a temporary
log. On success, the log is discarded. On failure, it is dumped to the
terminal. The test name is printed before starting so progress is
visible during parallel runs.

Parallel execution is controlled from the command line:

  make smoke-tests           # sequential
  make smoke-tests -j4       # 4 parallel tests
  make smoke-tests -j$(nproc) -k  # all CPUs, continue on failure

Spread CPU affinity across all available cores using a random offset
instead of always pinning to CPUs 0,1. This avoids contention when
multiple tests run in parallel.

Increase the socat connection retry count from 10 to 30 since grout
may take longer to start under heavy parallel load.

Update the CI script to use make -j$(nproc) -k instead of the old
sudo make smoke-tests invocation. The sudo is now in the make recipe
itself.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Tested-by: Roman Safronov <rsafrono@redhat.com>
Reviewed-by: Anthony Harivel <aharivel@redhat.com>
Swap the rebase flags between the two gcc-14 CI jobs so that
intermediate commits are smoke tested with debugoptimized builds instead
of debug+asan builds.

Running smoke tests on every intermediate commit with address sanitizer
is too slow. The optimized build catches most regressions while keeping
CI times reasonable. The asan job still runs on the final merge result.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Tested-by: Roman Safronov <rsafrono@redhat.com>
Reviewed-by: Anthony Harivel <aharivel@redhat.com>
Display duration of compilation, unit and smoke tests.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Tested-by: Roman Safronov <rsafrono@redhat.com>
Reviewed-by: Anthony Harivel <aharivel@redhat.com>
@rjarry
rjarry merged commit 79e4c99 into DPDK:main Feb 18, 2026
6 checks passed
@rjarry
rjarry deleted the smoke-parallel branch February 18, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants