Files
zeekayandHanzo Dev b08db2f70a Serve a listener the host hands over: Connect, then Accept
Serve is now Connect (dial the bus, ensure the offset bucket, watch tails,
bound partitions, start admin) followed by a TCP listener and Accept on it,
with the same behaviour as before. A host that owns the Kafka port itself
calls Connect, which fails as soon as the bus refuses rather than after a
startup timer, and then Accept on a unix socket it relays to. Metadata and
FindCoordinator keep advertising Config.BrokerHost:BrokerPort, never the
listener's own address.

Accept closes a listener that a Shutdown before it never saw, and returns
the error when its listener is closed by anything but Shutdown instead of
retrying a dead socket.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-03 15:51:27 -07:00

169 lines
4.4 KiB
Go

package protocol
import (
"context"
"errors"
"fmt"
"io"
"net"
"path/filepath"
"testing"
"time"
"github.com/hanzoai/kafka/types"
psembed "github.com/hanzoai/pubsub/embed"
"github.com/twmb/franz-go/pkg/kgo"
)
// door is a TCP port that relays every connection to a unix socket, which is
// what a host that owns the Kafka port does in front of the broker.
func door(tb testing.TB, sock string) (addr string) {
tb.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
tb.Fatalf("door listen: %v", err)
}
tb.Cleanup(func() { ln.Close() })
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
go func() {
defer c.Close()
u, err := net.Dial("unix", sock)
if err != nil {
return
}
defer u.Close()
done := make(chan struct{}, 2)
go func() { _, _ = io.Copy(u, c); done <- struct{}{} }()
go func() { _, _ = io.Copy(c, u); done <- struct{}{} }()
<-done
}()
}
}()
return ln.Addr().String()
}
// A broker serving a unix socket behind a TCP door answers clients that know
// only the door: Metadata advertises Config's port, not the socket.
func TestAcceptServesAUnixSocketBehindATCPDoor(t *testing.T) {
ps, err := psembed.Open(psembed.Options{
Host: "127.0.0.1", Port: -1, ServerName: "kafka-accept", StoreDir: t.TempDir(),
})
if err != nil {
t.Fatalf("pubsub open: %v", err)
}
t.Cleanup(ps.Shutdown)
sock := filepath.Join(t.TempDir(), "k.wire")
addr := door(t, sock)
_, port, _ := net.SplitHostPort(addr)
var p int
fmt.Sscan(port, &p)
b := NewBroker(&types.Configuration{
PubSubUrl: ps.ClientURL(),
BrokerHost: "127.0.0.1",
BrokerPort: p,
NodeID: 1,
StreamReplicas: 1,
StorageType: "file",
})
if err := b.Connect(); err != nil {
t.Fatalf("connect: %v", err)
}
ln, err := net.Listen("unix", sock)
if err != nil {
t.Fatalf("listen unix: %v", err)
}
served := make(chan error, 1)
go func() { served <- b.Accept(ln) }()
const topic = "behind-the-door"
s := &stack{b: b}
s.topic(t, topic, 1)
produce(t, addr, topic, 0, 3)
cl, err := kgo.NewClient(kgo.SeedBrokers(addr), kgo.ConsumeTopics(topic),
kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()))
if err != nil {
t.Fatalf("consumer: %v", err)
}
defer cl.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var got []string
for len(got) < 3 {
fs := cl.PollFetches(ctx)
if ctx.Err() != nil {
t.Fatalf("consumed %d of 3 records before the deadline: %v", len(got), got)
}
fs.EachRecord(func(r *kgo.Record) { got = append(got, string(r.Value)) })
}
for i, v := range got {
if want := fmt.Sprintf("record-%04d", i); v != want {
t.Fatalf("record %d = %q, want %q", i, v, want)
}
}
b.Shutdown()
select {
case err := <-served:
if err != nil {
t.Fatalf("Accept after Shutdown = %v, want nil", err)
}
case <-time.After(5 * time.Second):
t.Fatal("Accept did not return after Shutdown")
}
}
// A Shutdown that lands before Accept has published its listener must still
// stop it, or Accept blocks forever on a socket nothing will close.
func TestAcceptAfterShutdownReturns(t *testing.T) {
b := NewBroker(&types.Configuration{BrokerHost: "127.0.0.1"})
b.Shutdown()
ln, err := net.Listen("unix", filepath.Join(t.TempDir(), "k.wire"))
if err != nil {
t.Fatalf("listen unix: %v", err)
}
done := make(chan error, 1)
go func() { done <- b.Accept(ln) }()
select {
case err := <-done:
if err != nil {
t.Fatalf("Accept after Shutdown = %v, want nil", err)
}
case <-time.After(5 * time.Second):
t.Fatal("Accept blocked after Shutdown")
}
if _, err := ln.Accept(); !errors.Is(err, net.ErrClosed) {
t.Fatalf("listener still open after Accept returned: %v", err)
}
}
// A listener closed by someone other than Shutdown can never accept again, so
// Accept returns the error instead of spinning on it.
func TestAcceptReturnsWhenItsListenerCloses(t *testing.T) {
b := NewBroker(&types.Configuration{BrokerHost: "127.0.0.1"})
t.Cleanup(b.Shutdown)
ln, err := net.Listen("unix", filepath.Join(t.TempDir(), "k.wire"))
if err != nil {
t.Fatalf("listen unix: %v", err)
}
done := make(chan error, 1)
go func() { done <- b.Accept(ln) }()
time.Sleep(50 * time.Millisecond)
ln.Close()
select {
case err := <-done:
if !errors.Is(err, net.ErrClosed) {
t.Fatalf("Accept on a closed listener = %v, want net.ErrClosed", err)
}
case <-time.After(5 * time.Second):
t.Fatal("Accept kept running on a closed listener")
}
}