mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-15 23:42:48 -04:00
This adds the following helpers:
* WaitLength waits until the buffer is non-empty or a context is canceled.
It supports batchDelays so that even if data is ready, it blocks
until the delay is over. This is useful for how we upload logs for iOS,
where we deliberately wait a few minutes to reduce wakeup costs.
* DiscardOversize asynchronously discards data in the buffer
once it exceeds the specified maxSize. It takes in an optional frameLen
to ensure discarding maintains consistent frames in the buffer.
This avoids known problems with today's ring buffer where we can get
torn frames that lead to silent data corruption.
An alternative approach would be to synchronously delete data upon
an oversize condition at Write time, but there are two reasons
not to do that:
1. Doing requires teaching each Buffer implementation about
the concept of framing, which the interface deliberately avoids.
2. We want the write path to be extremely fast as we never want to
be blocking production logic. Going over maxSize momentarily is
considered a better tradeoff than synchronously blocking writes.
* StreamReader converts a non-blocking Buffer reader into a blocking one.
This exists primarily for debugging where you can simply stream
the entirety of a buffer to stdout.
We also adjust the package to avoid wrapping io.EOF and ErrEmpty
as those are sentinel errors with very specific meanings.
Updates tailscale/corp#21363
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
475 lines
11 KiB
Go
475 lines
11 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ioqueue
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"tailscale.com/util/must"
|
|
)
|
|
|
|
func TestWaitLengthNonEmpty(t *testing.T) {
|
|
var b VolatileBuffer
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
WaitLength(ctx, &b, 0, 0)
|
|
}()
|
|
|
|
// Should not return while empty.
|
|
select {
|
|
case <-done:
|
|
t.Fatal("WaitLength returned on empty buffer")
|
|
case <-time.After(20 * time.Millisecond):
|
|
}
|
|
|
|
must.Get(b.Write([]byte("hi")))
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("WaitLength did not return after Write")
|
|
}
|
|
}
|
|
|
|
func TestWaitLengthCancel(t *testing.T) {
|
|
var b VolatileBuffer
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
WaitLength(ctx, &b, 0, 0)
|
|
}()
|
|
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("WaitLength did not return after cancel")
|
|
}
|
|
}
|
|
|
|
func TestWaitLengthBytes(t *testing.T) {
|
|
var b VolatileBuffer
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
WaitLength(ctx, &b, 10, 0)
|
|
}()
|
|
|
|
must.Get(b.Write([]byte("12345"))) // still short of 10
|
|
select {
|
|
case <-done:
|
|
t.Fatal("WaitLength returned before lengthBytes available")
|
|
case <-time.After(20 * time.Millisecond):
|
|
}
|
|
|
|
must.Get(b.Write([]byte("67890"))) // now 10 bytes; WaitUntil needs WriteOffset > ReadOffset+10
|
|
// WaitUntil(ReadOffset+10) fires when WriteOffset > ReadOffset+10, i.e. Len > 10.
|
|
// With Len==10, still waiting. Add one more byte.
|
|
must.Get(b.Write([]byte("!")))
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("WaitLength did not return after lengthBytes available")
|
|
}
|
|
if b.Len() <= 10 {
|
|
t.Fatalf("Len=%d, want > 10", b.Len())
|
|
}
|
|
}
|
|
|
|
func TestWaitLengthBatchDelay(t *testing.T) {
|
|
var b VolatileBuffer
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
const delay = 50 * time.Millisecond
|
|
start := time.Now()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
WaitLength(ctx, &b, 0, delay)
|
|
}()
|
|
|
|
must.Get(b.Write([]byte("x")))
|
|
select {
|
|
case <-done:
|
|
if d := time.Since(start); d < delay {
|
|
t.Fatalf("returned after %v, want >= %v batchDelay", d, delay)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("WaitLength did not return after batchDelay")
|
|
}
|
|
}
|
|
|
|
func TestWaitLengthCloseWrite(t *testing.T) {
|
|
var b VolatileBuffer
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
WaitLength(ctx, &b, 0, 0)
|
|
}()
|
|
|
|
must.Do(b.CloseWrite())
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("WaitLength did not return after CloseWrite")
|
|
}
|
|
}
|
|
|
|
func newlineFrameLen(p []byte) (int, error) {
|
|
i := bytes.IndexByte(p, '\n')
|
|
if i < 0 {
|
|
return 0, nil
|
|
}
|
|
return i + 1, nil
|
|
}
|
|
|
|
func TestDiscardOversizeInvalidMaxSize(t *testing.T) {
|
|
var b VolatileBuffer
|
|
_, err := DiscardOversize(context.Background(), &b, 0, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for maxSize <= 0")
|
|
}
|
|
}
|
|
|
|
func TestDiscardOversizeUnframed(t *testing.T) {
|
|
var b VolatileBuffer
|
|
must.Get(b.Write(bytes.Repeat([]byte("x"), 100)))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
done := make(chan struct{})
|
|
var discarded int64
|
|
go func() {
|
|
defer close(done)
|
|
var err error
|
|
discarded, err = DiscardOversize(ctx, &b, 40, nil)
|
|
if err != nil {
|
|
t.Errorf("DiscardOversize: %v", err)
|
|
}
|
|
}()
|
|
|
|
deadline := time.After(time.Second)
|
|
for b.Len() > 40 {
|
|
select {
|
|
case <-deadline:
|
|
t.Fatalf("Len=%d, want <= 40", b.Len())
|
|
default:
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
cancel()
|
|
<-done
|
|
if b.Len() > 40 {
|
|
t.Fatalf("Len=%d, want <= 40", b.Len())
|
|
}
|
|
if discarded < 60 {
|
|
t.Fatalf("discarded=%d, want >= 60", discarded)
|
|
}
|
|
}
|
|
|
|
func TestDiscardOversizeFramed(t *testing.T) {
|
|
var b VolatileBuffer
|
|
// Three newline-delimited frames (5 bytes each); maxSize 10 keeps two frames.
|
|
must.Get(b.Write([]byte("aaaa\nbbbb\ncccc\n")))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
_, err := DiscardOversize(ctx, &b, 10, newlineFrameLen)
|
|
if err != nil {
|
|
t.Errorf("DiscardOversize: %v", err)
|
|
}
|
|
}()
|
|
|
|
deadline := time.After(time.Second)
|
|
for b.Len() > 10 {
|
|
select {
|
|
case <-deadline:
|
|
t.Fatalf("Len=%d, want <= 10", b.Len())
|
|
default:
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
cancel()
|
|
<-done
|
|
|
|
got := make([]byte, int(b.Len()))
|
|
_, pn, err := b.Peek(got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got[:pn]) != "bbbb\ncccc\n" {
|
|
t.Fatalf("remaining = %q, want %q", got[:pn], "bbbb\ncccc\n")
|
|
}
|
|
}
|
|
|
|
func TestDiscardOversizeFramedMultiFramePeek(t *testing.T) {
|
|
// Many small frames should be drained from one Peek, not one Peek per frame.
|
|
var inner VolatileBuffer
|
|
var frames strings.Builder
|
|
const n = 200
|
|
for range n {
|
|
frames.WriteString("x\n")
|
|
}
|
|
must.Get(inner.Write([]byte(frames.String())))
|
|
|
|
pb := &peekCountingBuffer{Buffer: &inner}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
_, err := DiscardOversize(ctx, pb, 4, newlineFrameLen)
|
|
if err != nil {
|
|
t.Errorf("DiscardOversize: %v", err)
|
|
}
|
|
}()
|
|
|
|
deadline := time.After(time.Second)
|
|
for pb.Len() > 4 {
|
|
select {
|
|
case <-deadline:
|
|
t.Fatalf("Len=%d, want <= 4", pb.Len())
|
|
default:
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
cancel()
|
|
<-done
|
|
|
|
if pb.peeks != 1 {
|
|
t.Fatalf("peeks = %d, want 1 for multi-frame discard", pb.peeks)
|
|
}
|
|
if pb.Len() != 4 {
|
|
t.Fatalf("Len = %d, want 4", pb.Len())
|
|
}
|
|
}
|
|
|
|
type peekCountingBuffer struct {
|
|
Buffer
|
|
peeks int
|
|
}
|
|
|
|
func (p *peekCountingBuffer) Peek(b []byte) (int64, int64, error) {
|
|
p.peeks++
|
|
return p.Buffer.Peek(b)
|
|
}
|
|
|
|
func TestDiscardOversizeFrameTooLong(t *testing.T) {
|
|
var b VolatileBuffer
|
|
must.Get(b.Write([]byte("hello")))
|
|
frameLen := func([]byte) (int, error) { return 100, nil } // claims more than Len
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
_, err := DiscardOversize(ctx, &b, 1, frameLen)
|
|
if !errors.Is(err, ErrFrameLength) {
|
|
t.Fatalf("err = %v, want ErrFrameLength", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscardOversizeIndeterminateFrame(t *testing.T) {
|
|
var b VolatileBuffer
|
|
must.Get(b.Write([]byte("no newline here")))
|
|
frameLen := func([]byte) (int, error) { return 0, nil } // never resolves
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
_, err := DiscardOversize(ctx, &b, 1, frameLen)
|
|
if !errors.Is(err, ErrFrameLength) {
|
|
t.Fatalf("err = %v, want ErrFrameLength", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscardOversizeLengthPrefixedCorruption(t *testing.T) {
|
|
// Length-prefixed framing cannot resynchronize after a bad length:
|
|
// a small corruption in the prefix can land the reader in the middle
|
|
// of a later frame. Delimiter-based formats (newlines, COBS nulls)
|
|
// recover at the next delimiter; a length prefix cannot.
|
|
// frameLen must reject implausible lengths as a hard error rather
|
|
// than trusting them and desynchronizing the rest of the stream.
|
|
const maxFrameSize = 8
|
|
|
|
var payload []byte
|
|
for _, s := range []string{"aaaa", "bbbb", "cccc", "dddd"} {
|
|
payload = binary.AppendUvarint(payload, uint64(len(s)))
|
|
payload = append(payload, s...)
|
|
}
|
|
// First uvarint was 4. Corrupt it to 12: larger than maxFrameSize,
|
|
// but 1+12 is still within the buffer, so a naive n+int(length)
|
|
// would discard into the middle of a subsequent frame.
|
|
payload[0] = 12
|
|
|
|
frameLen := func(b []byte) (int, error) {
|
|
length, n := binary.Uvarint(b)
|
|
if n < 0 || length > maxFrameSize {
|
|
return 0, ErrFrameLength
|
|
}
|
|
return n + int(length), nil
|
|
}
|
|
|
|
var buf VolatileBuffer
|
|
must.Get(buf.Write(payload))
|
|
wantLen := buf.Len()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
n, err := DiscardOversize(ctx, &buf, 1, frameLen)
|
|
if !errors.Is(err, ErrFrameLength) {
|
|
t.Fatalf("err = %v, want ErrFrameLength", err)
|
|
}
|
|
if n != 0 {
|
|
t.Fatalf("discarded %d bytes, want 0 (must not skip on a corrupted length)", n)
|
|
}
|
|
if buf.Len() != wantLen {
|
|
t.Fatalf("Len=%d, want %d (buffer must stay intact)", buf.Len(), wantLen)
|
|
}
|
|
}
|
|
|
|
func TestDiscardOversizeCloseWrite(t *testing.T) {
|
|
var b VolatileBuffer
|
|
must.Get(b.Write(bytes.Repeat([]byte("x"), 10)))
|
|
// Under maxSize; close write and ensure DiscardOversize returns.
|
|
must.Do(b.CloseWrite())
|
|
|
|
n, err := DiscardOversize(context.Background(), &b, 100, nil)
|
|
if err != nil || n != 0 {
|
|
t.Fatalf("DiscardOversize = (%d, %v), want (0, nil)", n, err)
|
|
}
|
|
}
|
|
|
|
func TestStreamReaderBasic(t *testing.T) {
|
|
var b VolatileBuffer
|
|
must.Get(b.Write([]byte("hello")))
|
|
must.Do(b.CloseWrite())
|
|
|
|
r := StreamReader(context.Background(), &b)
|
|
got, err := io.ReadAll(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != "hello" {
|
|
t.Fatalf("ReadAll = %q, want hello", got)
|
|
}
|
|
}
|
|
|
|
func TestStreamReaderBlocksUntilWrite(t *testing.T) {
|
|
var b VolatileBuffer
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
r := StreamReader(ctx, &b)
|
|
|
|
type result struct {
|
|
n int
|
|
err error
|
|
buf []byte
|
|
}
|
|
ch := make(chan result, 1)
|
|
go func() {
|
|
p := make([]byte, 8)
|
|
n, err := r.Read(p)
|
|
ch <- result{n, err, p[:n]}
|
|
}()
|
|
|
|
select {
|
|
case <-ch:
|
|
t.Fatal("Read returned before Write")
|
|
case <-time.After(20 * time.Millisecond):
|
|
}
|
|
|
|
must.Get(b.Write([]byte("xy")))
|
|
select {
|
|
case res := <-ch:
|
|
if res.err != nil || string(res.buf) != "xy" {
|
|
t.Fatalf("Read = (%q, %v), want (xy, nil)", res.buf, res.err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Read did not return after Write")
|
|
}
|
|
}
|
|
|
|
func TestStreamReaderCloseWriteEOF(t *testing.T) {
|
|
var b VolatileBuffer
|
|
must.Get(b.Write([]byte("ab")))
|
|
r := StreamReader(context.Background(), &b)
|
|
|
|
p := make([]byte, 8)
|
|
n, err := r.Read(p)
|
|
if err != nil || string(p[:n]) != "ab" {
|
|
t.Fatalf("Read = (%d, %v, %q)", n, err, p[:n])
|
|
}
|
|
|
|
must.Do(b.CloseWrite())
|
|
n, err = r.Read(p)
|
|
if n != 0 || !errors.Is(err, io.EOF) {
|
|
t.Fatalf("Read after CloseWrite = (%d, %v), want (0, EOF)", n, err)
|
|
}
|
|
}
|
|
|
|
func TestStreamReaderCancelMapsToEOF(t *testing.T) {
|
|
var b VolatileBuffer
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
r := StreamReader(ctx, &b)
|
|
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
_, err := r.Read(make([]byte, 4))
|
|
done <- err
|
|
}()
|
|
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if !errors.Is(err, io.EOF) {
|
|
t.Fatalf("err = %v, want EOF", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Read did not return after cancel")
|
|
}
|
|
}
|
|
|
|
func TestStreamReaderDeadline(t *testing.T) {
|
|
var b VolatileBuffer
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
|
|
defer cancel()
|
|
r := StreamReader(ctx, &b)
|
|
|
|
_, err := r.Read(make([]byte, 4))
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("err = %v, want DeadlineExceeded", err)
|
|
}
|
|
}
|
|
|
|
func TestStreamReaderZeroLength(t *testing.T) {
|
|
var b VolatileBuffer
|
|
r := StreamReader(context.Background(), &b)
|
|
n, err := r.Read(nil)
|
|
if n != 0 || err != nil {
|
|
t.Fatalf("Read(nil) = (%d, %v), want (0, nil)", n, err)
|
|
}
|
|
}
|