ℹ️ If you don't switch Linux-kernel namespaces and don't use a Go module that does, you most probably don't need to fret about
namspill
.
In the spirit of leak tests for
goroutines
and file descriptors, namspill
tests for
Linux kernel namespaces unintendedly "leaking" between goroutines due to
incorrect OS thread locking (or
rather, the lack thereof) when switching namespaces in a Go program.
namspill
is primarily designed to integrate smoothly with the
Ginkgo BDD testing framework and the
Gomega matcher/assertion library. (It may be
used also outside Ginkgo/Gomega, but such usage is out of scope.)
go get github.com/thediveo/namspill
In its simplest form, just after each test gather information about the namespaces of the tasks (threads) of the program and check that no namespaces have leaked, with some tasks being attached to other namespaces than the rest of the tasks.
As namspill
exports only very few symbols and is intended to extend the Gomega
DSL, you might want to dot-import it.
import . "github.com/thediveo/namspill"
AfterEach(func() {
// You might want to use Eventually(Tasks)... in case you don't
// have a preceeding Eventually(Goroutines)... that waits for
// the Goroutines (and thus Linux threads/tasks) to settle first.
Expect(Tasks()).To(BeUniformlyNamespaced())
})
Normally, you shouldn't then see anything unless there is a problem with threads
attached to other Linux-kernel namespaces when they shouldn't. In this case, the
BeUniformlyNamespaced
matcher will fail and show you the list of tasks with
the namespaces they're attached to.
Unfortunately, there's no way to show you which goroutine might have caused this nor the call site (and more importantly, the call stack) where the namespace switch happened.
namspill
mostly serves as a canary to (quickly) detect forgetting to switch
back namespaces before unlocking OS threads so that they can be freely scheduled
to any arbitrary goroutine.
Additionally, namespill
checks safeguard against alternatively not terminating
thread-locked goroutines. And that is where there's a catch when it comes to the
Go scheduler: if a Go program's initial thread ("M0" in Go scheduler parlance)
ends up being locked to a (non-main) goroutine and this goroutine exits ... then
the initial thread doesn't get terminated, because that usually has unwanted
side-effects on several operating systems. Instead, the Go scheduler "wedges"
the initial thread and never schedules any goroutine to it again.
This situation will (correctly) trigger failed namspill
assertions. To avoid
the initial thread getting wedged, simply lock it in an init
function to the
initial/main goroutine, so it never ends up getting scheduled on any other
goroutine (that might be subjected to locking it to a thread and then
terminating it while being locked):
func init() {
runtime.LockOSThread()
}
For further background information, please see the following references:
-
M0 is Special – many more background details and how the pieces fit together.
-
LockOSThread, switching (Linux kernel) namespaces: what happens to the main thread...? – my original question and the following highly useful discussion in the golang-nuts group drilling down into the scheduler behavior when it comes to the asymmetry between the initial thread (thread group leader) and its fellow threads.
-
runtime: on Linux, better do not treat the initial thread/task group leader as any other thread/task – points the finger to the scheduler source code where the initial thread, a.k.a. "M0", turns out to be special after all.
make
: lists all targets.make coverage
: runs all tests with coverage and then updates the coverage badge inREADME.md
.make pkgsite
: installsx/pkgsite
, as well as thebrowser-sync
andnodemon
npm packages first, if not already done so. Then runs thepkgsite
and hot reloads it whenever the documentation changes.make report
: installs@gojp/goreportcard
if not yet done so and then runs it on the code base.make test
: runs all tests.
namspill
is Copyright 2022, 2022 Harald Albrecht, and licensed under the
Apache License, Version 2.0.