mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-13 06:19:11 -04:00
The zstd Encoder and Decoder types use channels internally, created
when the coder is constructed. A coder constructed by a goroutine
inside a testing/synctest bubble therefore owns bubble-associated
channels, and if it lands in zstdframe's process-wide pools and is
later reused outside that bubble, the Go runtime kills the process:
fatal error: receive on synctest channel from outside bubble
This has been crashing test binaries that mix synctest-based tests
with regular tests exercising zstd compression in parallel, taking
out every other test in the package with it.
Add testenv.InSynctestBubble and use it in zstdframe to construct a
fresh coder per call within a bubble instead of using the pools.
Pooling behavior outside of bubbles (including in benchmarks) is
unchanged.
As of Go 1.26 there is no public API to query bubble membership, so
InSynctestBubble looks for the "synctest bubble N" annotation that
the runtime renders in the current goroutine's runtime.Stack header.
That annotation is not covered by the Go compatibility promise, so
tests fail loudly (in util/testenv directly, and in util/zstdframe by
reintroducing the pooled-coder crash) if a future Go release changes
it. The check costs ~2us and runs only in test binaries, detected by
an uncached flag.Lookup("test.v") rather than testenv.InTest: this
path is reachable from package init functions (before testing has
registered its flags), where InTest would permanently latch a false
result into its cache, breaking later InTest and AssertInTest calls.
Fixes tailscale/corp#45861
Change-Id: I7d89e2d0de51e30098ceda25c12d27918acc46e8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
49 lines
2.0 KiB
Go
49 lines
2.0 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package testenv
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"runtime"
|
|
"sync"
|
|
)
|
|
|
|
// stackBufPool holds buffers for [InSynctestBubble]'s runtime.Stack calls,
|
|
// which would otherwise allocate on every call: the compiler cannot prove
|
|
// that a stack-allocated buffer does not escape into runtime.Stack. Plain
|
|
// byte arrays, unlike the zstd coders that motivated InSynctestBubble, are
|
|
// safe to share across synctest bubble boundaries.
|
|
var stackBufPool = sync.Pool{New: func() any { return new([128]byte) }}
|
|
|
|
// InSynctestBubble reports whether the current goroutine is running within a
|
|
// testing/synctest bubble.
|
|
//
|
|
// As of Go 1.26 there is no public API to query bubble membership
|
|
// (internal/synctest.IsInBubble is runtime-internal), so this instead
|
|
// looks for the "synctest bubble N" annotation that the runtime renders
|
|
// in the current goroutine's [runtime.Stack] header. If a future Go
|
|
// release changes that annotation, this reports false; tests that depend
|
|
// on it for correctness (such as tailscale.com/util/zstdframe's) should
|
|
// exercise the misdetection failure mode so the breakage is loud.
|
|
func InSynctestBubble() bool {
|
|
// Bubbles only exist within tests, so skip the (relatively) expensive
|
|
// stack header check in non-test binaries. This deliberately does not
|
|
// use InTest: InSynctestBubble may be reached from package init
|
|
// functions (before the testing package has registered its flags),
|
|
// and InTest would permanently latch a false result there. Bubbles
|
|
// cannot exist during init, so returning false then is correct.
|
|
if flag.Lookup("test.v") == nil {
|
|
return false
|
|
}
|
|
// The current goroutine's header looks like:
|
|
// goroutine 9 [running, synctest bubble 1]:
|
|
// A 128-byte buffer always fits the header, and runtime.Stack
|
|
// truncates the rest.
|
|
buf := stackBufPool.Get().(*[128]byte)
|
|
defer stackBufPool.Put(buf)
|
|
n := runtime.Stack(buf[:], false)
|
|
return bytes.Contains(buf[:n], []byte(" synctest bubble "))
|
|
}
|