Ctrl K
ComunidadGuías…

pkg/models/mysql/wirecache_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package mysql_test

import (
	"bytes"
	"context"
	"fmt"
	"math/rand"
	"net"
	"runtime"
	"strings"
	"sync"
	"testing"
	"weak"

	"go.keploy.io/server/v3/pkg/agent/proxy/integrations/mysql/wire"
	"go.keploy.io/server/v3/pkg/models/mysql"
	"go.uber.org/zap"
)

// serveConn returns a DecodeContext that knows a server greeting for the
// connection it returns, as replay's handshake leaves it.
func serveConn(t testing.TB) (*wire.DecodeContext, net.Conn) {
	t.Helper()
	c1, c2 := net.Pipe()
	t.Cleanup(func() { _ = c1.Close(); _ = c2.Close() })
	dc := &wire.DecodeContext{LastOp: wire.NewLastOpMap(), ServerGreetings: wire.NewGreetings()}
	dc.ServerGreetings.Store(c1, &mysql.HandshakeV10Packet{CapabilityFlags: 0x81bff7ff})
	return dc, c1
}

// answer is a text or binary result set of rows rows of string columns about
// width bytes wide, some NULL, with an EOF terminator.
func answer(r *rand.Rand, binaryRows bool, rows, width int) mysql.PacketBundle {
	ncols := 1 + r.Intn(3)
	cols := make([]*mysql.ColumnDefinition41, ncols)
	for i := range cols {
		cols[i] = &mysql.ColumnDefinition41{Header: mysql.Header{PayloadLength: 30, SequenceID: byte(2 + i)}, Catalog: "def", Schema: "app", Table: "t", OrgTable: "t",
			Name: fmt.Sprintf("c%d", i), OrgName: fmt.Sprintf("c%d", i), FixedLength: 0x0c, CharacterSet: 33, ColumnLength: 255,
			Type: byte(mysql.FieldTypeVarString), Filler: []byte{0, 0}}
	}
	seq := byte(2 + ncols)
	eof := []byte{5, 0, 0, seq, 0xfe, 0, 0, 2, 0}
	seq++
	final := &mysql.GenericResponse{Type: "EOF", Data: []byte{5, 0, 0, seq + byte(rows), 0xfe, 0, 0, 0x22, 0}}
	val := func(i int) string { return fmt.Sprintf("%d-%s", i, strings.Repeat("v", width)) }
	hdr := &mysql.PacketInfo{Header: &mysql.Header{PayloadLength: 1, SequenceID: 1}}
	if binaryRows {
		rs := &mysql.BinaryProtocolResultSet{ColumnCount: uint64(ncols), Columns: cols, EOFAfterColumns: eof, FinalResponse: final}
		for i := 0; i < rows; i++ {
			row := &mysql.BinaryRow{Header: mysql.Header{SequenceID: seq + byte(i)}, RowNullBuffer: make([]byte, (ncols+7+2)/8)}
			for j, c := range cols {
				if r.Intn(5) == 0 {
					row.RowNullBuffer[(j+2)/8] |= 1 << ((j + 2) % 8)
					row.Values = append(row.Values, mysql.ColumnEntry{Type: mysql.FieldTypeVarString, Name: c.Name})
					continue
				}
				row.Values = append(row.Values, mysql.ColumnEntry{Type: mysql.FieldTypeVarString, Name: c.Name, Value: val(i)})
			}
			rs.Rows = append(rs.Rows, row)
		}
		hdr.Type = "BinaryProtocolResultSet"
		return mysql.PacketBundle{Header: hdr, Message: rs}
	}
	rs := &mysql.TextResultSet{ColumnCount: uint64(ncols), Columns: cols, EOFAfterColumns: eof, FinalResponse: final}
	for i := 0; i < rows; i++ {
		row := &mysql.TextRow{Header: mysql.Header{SequenceID: seq + byte(i)}}
		for _, c := range cols {
			if r.Intn(5) == 0 {
				row.Values = append(row.Values, mysql.ColumnEntry{Type: mysql.FieldTypeVarString, Name: c.Name})
				continue
			}
			row.Values = append(row.Values, mysql.ColumnEntry{Type: mysql.FieldTypeVarString, Name: c.Name, Value: val(i)})
		}
		rs.Rows = append(rs.Rows, row)
	}
	hdr.Type = "TextResultSet"
	return mysql.PacketBundle{Header: hdr, Message: rs}
}

// cacheOf is the encode cache of a result set.
func cacheOf(msg interface{}) *mysql.EncodedPayload {
	switch rs := msg.(type) {
	case *mysql.TextResultSet:
		return rs.WireCache()
	case *mysql.BinaryProtocolResultSet:
		return rs.WireCache()
	}
	panic(fmt.Sprintf("%T is not a result set", msg))
}

// mainsBytes is what main serves for each answer: the encode of a new result
// set with its recorded fields. It leaves the cache empty, its budget as it
// was.
func mainsBytes(t *testing.T, answers []mysql.PacketBundle) [][]byte {
	t.Helper()
	dc, conn := serveConn(t)
	out := make([][]byte, len(answers))
	for i, a := range answers {
		var fresh interface{}
		switch rs := a.Message.(type) {
		case *mysql.TextResultSet:
			fresh = &mysql.TextResultSet{ColumnCount: rs.ColumnCount, Columns: rs.Columns, EOFAfterColumns: rs.EOFAfterColumns, Rows: rs.Rows, FinalResponse: rs.FinalResponse}
		case *mysql.BinaryProtocolResultSet:
			fresh = &mysql.BinaryProtocolResultSet{ColumnCount: rs.ColumnCount, Columns: rs.Columns, EOFAfterColumns: rs.EOFAfterColumns, Rows: rs.Rows, FinalResponse: rs.FinalResponse}
		}
		b, err := wire.EncodeToBinary(context.Background(), zap.NewNop(), &mysql.PacketBundle{Header: a.Header, Message: fresh}, conn, dc)
		if err != nil {
			t.Fatalf("answer %d: %v", i, err)
		}
		out[i] = b
	}
	_, budget := mysql.WireCacheUsage()
	mysql.ResetWireCache(budget)
	return out
}

// kept is what the budget should count for the payloads answers' caches hold.
func kept(answers []mysql.PacketBundle) int64 {
	var n int64
	for _, a := range answers {
		if b := cacheOf(a.Message).Load(0); b != nil {
			n += int64(cap(b)) + mysql.WireEntryCost
		}
	}
	return n
}

// serve serves answer i and fails unless it gets main's bytes; it returns how
// many result sets the serve encoded.
func serve(t *testing.T, dc *wire.DecodeContext, conn net.Conn, a *mysql.PacketBundle, want []byte, what string) uint64 {
	t.Helper()
	before := wire.ResultSetEncodes()
	got, err := wire.EncodeToBinary(context.Background(), zap.NewNop(), a, conn, dc)
	if err != nil || !bytes.Equal(got, want) {
		t.Fatalf("%s: served\n% x\n(%v), main serves\n% x", what, got, err, want)
	}
	return wire.ResultSetEncodes() - before
}

// Answers whose payloads add up to seven times the budget, each served twice
// and then all once more: the bytes counted never pass the budget and are
// exactly the payloads the caches hold, eviction leaves the cache full to
// within one payload, the newest payload stays cached for its next serve, an
// evicted answer is encoded again at its next serve, and every serve, before
// and after eviction, gives main's bytes.
func TestWireCacheHoldsItsBudgetAndServesMainsBytesPastIt(t *testing.T) {
	r := rand.New(rand.NewSource(5))
	var answers []mysql.PacketBundle
	for i := 0; i < 200; i++ {
		answers = append(answers, answer(r, i%2 == 1, 1+r.Intn(12), 8+r.Intn(200)))
	}
	want := mainsBytes(t, answers)
	var total int
	for _, w := range want {
		total += len(w)
	}
	const budget = 24 << 10
	if total < 7*budget {
		t.Fatalf("the answers encode to %d bytes, under seven budgets of %d: the row would evict too little", total, budget)
	}
	defer mysql.ResetWireCache(budget)()
	dc, conn := serveConn(t)
	check := func(what string) {
		t.Helper()
		held, b := mysql.WireCacheUsage()
		if b != budget || held > budget || held != kept(answers) {
			t.Fatalf("%s: the cache counts %d bytes of a %d budget (want %d), the caches hold %d", what, held, b, budget, kept(answers))
		}
	}
	var largest, served int64
	for i := range answers {
		what := fmt.Sprintf("answer %d (%T), first pass", i, answers[i].Message)
		if n := serve(t, dc, conn, &answers[i], want[i], what) + serve(t, dc, conn, &answers[i], want[i], what); n != 1 {
			t.Fatalf("%s: two serves encoded it %d times, want once: the newest payload must stay cached", what, n)
		}
		cost := int64(cap(cacheOf(answers[i].Message).Load(0))) + mysql.WireEntryCost
		largest, served = max(largest, cost), served+cost
		check(what)
		if held, _ := mysql.WireCacheUsage(); served > budget && held <= budget-largest {
			t.Fatalf("%s: the cache counts %d bytes of %d: eviction emptied more than the new payload needed (largest %d)", what, held, budget, largest)
		}
	}
	evicted := 0
	for i := range answers {
		what := fmt.Sprintf("answer %d (%T), second pass", i, answers[i].Message)
		cached := cacheOf(answers[i].Message).Load(0) != nil
		n := serve(t, dc, conn, &answers[i], want[i], what)
		if cached && n != 0 || !cached && n != 1 {
			t.Fatalf("%s: cached %v before the serve, which encoded it %d times", what, cached, n)
		}
		if !cached {
			evicted++
		}
		check(what)
	}
	if evicted < len(answers)/2 {
		t.Fatalf("only %d of %d answers were evicted before their next serve: the row tested too little eviction", evicted, len(answers))
	}
}

// An answer larger than the whole budget is served with main's bytes and
// encoded at every serve, and caching it is never tried at the cost of the
// payloads already cached.
func TestWireCacheDoesNotCacheAnAnswerLargerThanItsBudget(t *testing.T) {
	r := rand.New(rand.NewSource(9))
	answers := []mysql.PacketBundle{answer(r, false, 2, 20), answer(r, true, 2, 20), answer(r, false, 40, 400)}
	want := mainsBytes(t, answers)
	budget := int64(len(want[0])+len(want[1]))*2 + 4*mysql.WireEntryCost
	if int64(len(want[2])) <= budget {
		t.Fatalf("the large answer encodes to %d bytes, within the %d budget", len(want[2]), budget)
	}
	defer mysql.ResetWireCache(budget)()
	dc, conn := serveConn(t)
	serve(t, dc, conn, &answers[0], want[0], "small text answer")
	serve(t, dc, conn, &answers[1], want[1], "small binary answer")
	held0, _ := mysql.WireCacheUsage()
	for k := 0; k < 3; k++ {
		if n := serve(t, dc, conn, &answers[2], want[2], fmt.Sprintf("large answer, serve %d", k)); n != 1 {
			t.Fatalf("serve %d of the large answer encoded %d result sets, want 1", k, n)
		}
	}
	if held, _ := mysql.WireCacheUsage(); held != held0 || cacheOf(answers[2].Message).Load(0) != nil ||
		cacheOf(answers[0].Message).Load(0) == nil || cacheOf(answers[1].Message).Load(0) == nil {
		t.Fatalf("after the large answer the cache counts %d bytes (was %d); the large answer is cached %v, the small ones %v %v",
			held, held0, cacheOf(answers[2].Message).Load(0) != nil, cacheOf(answers[0].Message).Load(0) != nil, cacheOf(answers[1].Message).Load(0) != nil)
	}
}

//go:noinline
func serveAndDrop(t *testing.T, dc *wire.DecodeContext, conn net.Conn) weak.Pointer[mysql.TextResultSet] {
	a := answer(rand.New(rand.NewSource(13)), false, 4, 300)
	want := mainsBytes(t, []mysql.PacketBundle{a})
	serve(t, dc, conn, &a, want[0], "dropped answer")
	if cacheOf(a.Message).Load(0) == nil {
		t.Fatal("the dropped answer was not cached")
	}
	return weak.Make(a.Message.(*mysql.TextResultSet))
}

// The budget keeps no mock alive: a served result set nothing else holds is
// collected with its payload. Its bytes stay counted, as an upper bound, until
// eviction reaches them, and then leave the count.
func TestWireCacheKeepsNoCollectedAnswerAlive(t *testing.T) {
	r := rand.New(rand.NewSource(17))
	var answers []mysql.PacketBundle
	for i := 0; i < 40; i++ {
		answers = append(answers, answer(r, i%2 == 0, 4, 300))
	}
	want := mainsBytes(t, answers)
	const budget = 16 << 10
	defer mysql.ResetWireCache(budget)()
	dc, conn := serveConn(t)
	gone := serveAndDrop(t, dc, conn)
	held0, _ := mysql.WireCacheUsage()
	for i := 0; i < 20 && gone.Value() != nil; i++ {
		runtime.GC()
	}
	if gone.Value() != nil {
		t.Fatal("a served answer nothing else holds was not collected after 20 collections: the cache keeps it alive")
	}
	if held, _ := mysql.WireCacheUsage(); held != held0 || held == 0 {
		t.Fatalf("after the collection the cache counts %d bytes, want the %d it counted", held, held0)
	}
	for i := range answers {
		serve(t, dc, conn, &answers[i], want[i], fmt.Sprintf("answer %d", i))
	}
	if held, _ := mysql.WireCacheUsage(); held > budget || held != kept(answers) {
		t.Fatalf("once later answers filled the cache it counts %d bytes of %d, want exactly the %d the live caches hold", held, budget, kept(answers))
	}
}

// Connections serving many answers at once, past the budget, all get main's
// bytes, and the count still matches what the caches hold.
func TestWireCacheConcurrentServesPastItsBudget(t *testing.T) {
	r := rand.New(rand.NewSource(21))
	var answers []mysql.PacketBundle
	for i := 0; i < 48; i++ {
		answers = append(answers, answer(r, i%2 == 0, 1+r.Intn(6), 8+r.Intn(120)))
	}
	want := mainsBytes(t, answers)
	const budget = 6 << 10
	defer mysql.ResetWireCache(budget)()
	dc, conn := serveConn(t)
	var wg sync.WaitGroup
	errs := make(chan string, 8)
	for g := 0; g < 8; g++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for k := 0; k < 3*len(answers); k++ {
				i := (k*(g+1) + g) % len(answers)
				got, err := wire.EncodeToBinary(context.Background(), zap.NewNop(), &answers[i], conn, dc)
				if err != nil || !bytes.Equal(got, want[i]) {
					errs <- fmt.Sprintf("goroutine %d serve %d of answer %d: % x (%v), main serves % x", g, k, i, got, err, want[i])
					return
				}
			}
		}()
	}
	wg.Wait()
	close(errs)
	for e := range errs {
		t.Fatal(e)
	}
	if held, _ := mysql.WireCacheUsage(); held > budget || held != kept(answers) {
		t.Fatalf("the cache counts %d bytes of %d, the caches hold %d", held, budget, kept(answers))
	}
}