2026-04-15 15:24:36 +08:00
|
|
|
package notify
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"net"
|
|
|
|
|
"strconv"
|
|
|
|
|
"sync"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestRecordStreamBarrierTracksAppliedSeq(t *testing.T) {
|
|
|
|
|
server := NewServer().(*ServerCommon)
|
|
|
|
|
secret := []byte("0123456789abcdef0123456789abcdef")
|
|
|
|
|
server = newRunningPeerAttachServerForTest(t, func(server *ServerCommon) {
|
|
|
|
|
server.SetSecretKey(secret)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
receivedCh := make(chan RecordMessage, 4)
|
|
|
|
|
handlerDone := make(chan error, 1)
|
|
|
|
|
server.SetRecordStreamHandler(func(info RecordAcceptInfo) error {
|
|
|
|
|
for {
|
|
|
|
|
record, err := info.RecordStream.ReadRecord(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
|
handlerDone <- nil
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
handlerDone <- err
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
receivedCh <- record
|
|
|
|
|
if err := info.RecordStream.AckRecord(record.Seq); err != nil {
|
|
|
|
|
handlerDone <- err
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
client := NewClient().(*ClientCommon)
|
|
|
|
|
client.SetSecretKey(secret)
|
|
|
|
|
left, right := net.Pipe()
|
|
|
|
|
defer right.Close()
|
|
|
|
|
bootstrapPeerAttachConnForTest(t, server, right)
|
|
|
|
|
if err := client.ConnectByConn(left); err != nil {
|
|
|
|
|
t.Fatalf("client ConnectByConn failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
client.setByeFromServer(true)
|
|
|
|
|
_ = client.Stop()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
stream, err := client.OpenRecordStream(context.Background(), RecordOpenOptions{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("OpenRecordStream failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
payloads := [][]byte{
|
|
|
|
|
[]byte("alpha"),
|
|
|
|
|
[]byte("beta"),
|
|
|
|
|
[]byte("gamma"),
|
|
|
|
|
}
|
|
|
|
|
for index, payload := range payloads {
|
|
|
|
|
seq, err := stream.WriteRecord(context.Background(), payload)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteRecord(%d) failed: %v", index, err)
|
|
|
|
|
}
|
|
|
|
|
if got, want := seq, uint64(index+1); got != want {
|
|
|
|
|
t.Fatalf("WriteRecord(%d) seq=%d want=%d", index, got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ackedSeq, err := stream.Barrier(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Barrier failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got, want := ackedSeq, uint64(len(payloads)); got != want {
|
|
|
|
|
t.Fatalf("Barrier acked=%d want=%d", got, want)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for index, want := range payloads {
|
|
|
|
|
select {
|
|
|
|
|
case got := <-receivedCh:
|
|
|
|
|
if got.Seq != uint64(index+1) {
|
|
|
|
|
t.Fatalf("received seq=%d want=%d", got.Seq, index+1)
|
|
|
|
|
}
|
|
|
|
|
if string(got.Payload) != string(want) {
|
|
|
|
|
t.Fatalf("received payload=%q want=%q", string(got.Payload), string(want))
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(2 * time.Second):
|
|
|
|
|
t.Fatalf("timed out waiting received payload %d", index)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := stream.CloseWrite(); err != nil {
|
|
|
|
|
t.Fatalf("CloseWrite failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case err := <-handlerDone:
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("record handler failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(2 * time.Second):
|
|
|
|
|
t.Fatal("timed out waiting record handler completion")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRecordStreamPropagatesStructuredFailure(t *testing.T) {
|
|
|
|
|
server := NewServer().(*ServerCommon)
|
|
|
|
|
secret := []byte("0123456789abcdef0123456789abcdef")
|
|
|
|
|
server = newRunningPeerAttachServerForTest(t, func(server *ServerCommon) {
|
|
|
|
|
server.SetSecretKey(secret)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
server.SetRecordStreamHandler(func(info RecordAcceptInfo) error {
|
|
|
|
|
record, err := info.RecordStream.ReadRecord(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return info.RecordStream.FailRecord(record.Seq, RecordFailure{
|
|
|
|
|
Code: "disk_full",
|
|
|
|
|
Retryable: true,
|
|
|
|
|
Message: "disk full",
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
client := NewClient().(*ClientCommon)
|
|
|
|
|
client.SetSecretKey(secret)
|
|
|
|
|
left, right := net.Pipe()
|
|
|
|
|
defer right.Close()
|
|
|
|
|
bootstrapPeerAttachConnForTest(t, server, right)
|
|
|
|
|
if err := client.ConnectByConn(left); err != nil {
|
|
|
|
|
t.Fatalf("client ConnectByConn failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
client.setByeFromServer(true)
|
|
|
|
|
_ = client.Stop()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
stream, err := client.OpenRecordStream(context.Background(), RecordOpenOptions{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("OpenRecordStream failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := stream.WriteRecord(context.Background(), []byte("payload")); err != nil {
|
|
|
|
|
t.Fatalf("WriteRecord failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := stream.Barrier(context.Background()); err == nil {
|
|
|
|
|
t.Fatal("Barrier should fail after remote FailRecord")
|
|
|
|
|
} else {
|
|
|
|
|
var failure RecordFailure
|
|
|
|
|
if !errors.As(err, &failure) {
|
|
|
|
|
t.Fatalf("Barrier error=%T %v, want RecordFailure", err, err)
|
|
|
|
|
}
|
|
|
|
|
if got, want := failure.FailedSeq, uint64(1); got != want {
|
|
|
|
|
t.Fatalf("failure seq=%d want=%d", got, want)
|
|
|
|
|
}
|
|
|
|
|
if got, want := failure.Code, RecordErrorCode("disk_full"); got != want {
|
|
|
|
|
t.Fatalf("failure code=%q want=%q", got, want)
|
|
|
|
|
}
|
|
|
|
|
if !failure.Retryable {
|
|
|
|
|
t.Fatal("failure retryable=false, want true")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 15:33:17 +08:00
|
|
|
func TestRecordStreamPropagatesStructuredFailureUnderReceiveBackpressure(t *testing.T) {
|
|
|
|
|
server := NewServer().(*ServerCommon)
|
|
|
|
|
if err := UseModernPSKServer(server, integrationSharedSecret, integrationModernPSKOptions()); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
accepted := make(chan RecordStream, 1)
|
|
|
|
|
server.SetRecordStreamHandler(func(info RecordAcceptInfo) error {
|
|
|
|
|
accepted <- info.RecordStream
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err := server.Listen("tcp", "127.0.0.1:0"); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
defer server.Stop()
|
|
|
|
|
|
|
|
|
|
client := NewClient().(*ClientCommon)
|
|
|
|
|
if err := UseModernPSKClient(client, integrationSharedSecret, integrationModernPSKOptions()); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := client.Connect("tcp", server.listener.Addr().String()); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
defer client.Stop()
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
local, err := client.OpenRecordStream(ctx, RecordOpenOptions{InboundQueueLimit: 1})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
var remote RecordStream
|
|
|
|
|
select {
|
|
|
|
|
case remote = <-accepted:
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
t.Fatal(ctx.Err())
|
|
|
|
|
}
|
|
|
|
|
seq, err := local.WriteRecord(ctx, []byte("request"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := local.Flush(ctx); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
request, err := remote.ReadRecord(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
|
if _, err := remote.WriteRecord(ctx, []byte("pending response")); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := remote.Flush(ctx); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
record := local.(*recordStream)
|
|
|
|
|
for {
|
|
|
|
|
record.mu.Lock()
|
|
|
|
|
received := record.inboundReceivedSeq
|
|
|
|
|
record.mu.Unlock()
|
|
|
|
|
if received == 2 && len(record.recvCh) == 1 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
t.Fatal("record receive queue did not fill")
|
|
|
|
|
case <-time.After(time.Millisecond):
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
want := RecordFailure{FailedSeq: request.Seq, Code: "disk_full", Retryable: true, Message: "disk full"}
|
|
|
|
|
if err := remote.FailRecord(request.Seq, want); err != nil {
|
|
|
|
|
t.Fatalf("FailRecord failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
select {
|
|
|
|
|
case <-local.Context().Done():
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
t.Fatal("remote failure did not terminate local record stream")
|
|
|
|
|
}
|
|
|
|
|
_, err = local.BarrierTo(ctx, seq)
|
|
|
|
|
var got RecordFailure
|
|
|
|
|
if !errors.As(err, &got) || got != want {
|
|
|
|
|
t.Fatalf("structured failure lost under receive backpressure: got=%T %v, want=%+v", err, err, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 15:24:36 +08:00
|
|
|
func TestRecordStreamBackpressureUsesUnackedRecords(t *testing.T) {
|
|
|
|
|
server := NewServer().(*ServerCommon)
|
|
|
|
|
secret := []byte("0123456789abcdef0123456789abcdef")
|
|
|
|
|
server = newRunningPeerAttachServerForTest(t, func(server *ServerCommon) {
|
|
|
|
|
server.SetSecretKey(secret)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
firstSeen := make(chan struct{}, 1)
|
|
|
|
|
server.SetRecordStreamHandler(func(info RecordAcceptInfo) error {
|
|
|
|
|
record, err := info.RecordStream.ReadRecord(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if record.Seq != 1 {
|
|
|
|
|
t.Errorf("first record seq=%d want=1", record.Seq)
|
|
|
|
|
}
|
|
|
|
|
firstSeen <- struct{}{}
|
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
|
if err := info.RecordStream.AckRecord(record.Seq); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
for {
|
|
|
|
|
_, err := info.RecordStream.ReadRecord(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
client := NewClient().(*ClientCommon)
|
|
|
|
|
client.SetSecretKey(secret)
|
|
|
|
|
left, right := net.Pipe()
|
|
|
|
|
defer right.Close()
|
|
|
|
|
bootstrapPeerAttachConnForTest(t, server, right)
|
|
|
|
|
if err := client.ConnectByConn(left); err != nil {
|
|
|
|
|
t.Fatalf("client ConnectByConn failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
client.setByeFromServer(true)
|
|
|
|
|
_ = client.Stop()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
stream, err := client.OpenRecordStream(context.Background(), RecordOpenOptions{
|
|
|
|
|
MaxUnackedRecords: 1,
|
|
|
|
|
MaxUnackedBytes: 1 << 20,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("OpenRecordStream failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := stream.WriteRecord(context.Background(), []byte("first")); err != nil {
|
|
|
|
|
t.Fatalf("first WriteRecord failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
select {
|
|
|
|
|
case <-firstSeen:
|
|
|
|
|
case <-time.After(2 * time.Second):
|
|
|
|
|
t.Fatal("timed out waiting server to receive first record")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeCtx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
|
|
|
defer cancel()
|
|
|
|
|
if _, err := stream.WriteRecord(writeCtx, []byte("second")); !errors.Is(err, context.DeadlineExceeded) {
|
|
|
|
|
t.Fatalf("second WriteRecord error=%v want=%v", err, context.DeadlineExceeded)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if acked, err := stream.Barrier(context.Background()); err != nil {
|
|
|
|
|
t.Fatalf("Barrier failed: %v", err)
|
|
|
|
|
} else if got, want := acked, uint64(1); got != want {
|
|
|
|
|
t.Fatalf("Barrier acked=%d want=%d", got, want)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := stream.CloseWrite(); err != nil {
|
|
|
|
|
t.Fatalf("CloseWrite failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRecordStreamBarrierToWaitsCheckpointSeq(t *testing.T) {
|
|
|
|
|
server := NewServer().(*ServerCommon)
|
|
|
|
|
secret := []byte("0123456789abcdef0123456789abcdef")
|
|
|
|
|
server = newRunningPeerAttachServerForTest(t, func(server *ServerCommon) {
|
|
|
|
|
server.SetSecretKey(secret)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
secondSeen := make(chan struct{}, 1)
|
|
|
|
|
server.SetRecordStreamHandler(func(info RecordAcceptInfo) error {
|
|
|
|
|
first, err := info.RecordStream.ReadRecord(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := info.RecordStream.AckRecord(first.Seq); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
second, err := info.RecordStream.ReadRecord(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
secondSeen <- struct{}{}
|
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
|
if err := info.RecordStream.AckRecord(second.Seq); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
_, err := info.RecordStream.ReadRecord(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
client := NewClient().(*ClientCommon)
|
|
|
|
|
client.SetSecretKey(secret)
|
|
|
|
|
left, right := net.Pipe()
|
|
|
|
|
defer right.Close()
|
|
|
|
|
bootstrapPeerAttachConnForTest(t, server, right)
|
|
|
|
|
if err := client.ConnectByConn(left); err != nil {
|
|
|
|
|
t.Fatalf("client ConnectByConn failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
client.setByeFromServer(true)
|
|
|
|
|
_ = client.Stop()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
stream, err := client.OpenRecordStream(context.Background(), RecordOpenOptions{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("OpenRecordStream failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
firstSeq, err := stream.WriteRecord(context.Background(), []byte("first"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("first WriteRecord failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := stream.WriteRecord(context.Background(), []byte("second")); err != nil {
|
|
|
|
|
t.Fatalf("second WriteRecord failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-secondSeen:
|
|
|
|
|
case <-time.After(2 * time.Second):
|
|
|
|
|
t.Fatal("timed out waiting server to receive second record")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start := time.Now()
|
|
|
|
|
acked, err := stream.BarrierTo(context.Background(), firstSeq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("BarrierTo failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if acked != firstSeq {
|
|
|
|
|
t.Fatalf("BarrierTo acked=%d want=%d", acked, firstSeq)
|
|
|
|
|
}
|
|
|
|
|
if elapsed := time.Since(start); elapsed >= 150*time.Millisecond {
|
|
|
|
|
t.Fatalf("BarrierTo waited too long: %s", elapsed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := stream.Barrier(context.Background()); err != nil {
|
|
|
|
|
t.Fatalf("final Barrier failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := stream.CloseWrite(); err != nil {
|
|
|
|
|
t.Fatalf("CloseWrite failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRecordStreamConcurrentWritesStayOrdered(t *testing.T) {
|
|
|
|
|
server := NewServer().(*ServerCommon)
|
|
|
|
|
secret := []byte("0123456789abcdef0123456789abcdef")
|
|
|
|
|
server = newRunningPeerAttachServerForTest(t, func(server *ServerCommon) {
|
|
|
|
|
server.SetSecretKey(secret)
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-23 15:33:17 +08:00
|
|
|
const total = 512
|
|
|
|
|
writeCtx, cancelWrites := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
defer cancelWrites()
|
2026-04-15 15:24:36 +08:00
|
|
|
receivedCh := make(chan RecordMessage, total)
|
|
|
|
|
handlerDone := make(chan error, 1)
|
|
|
|
|
server.SetRecordStreamHandler(func(info RecordAcceptInfo) error {
|
|
|
|
|
for {
|
|
|
|
|
record, err := info.RecordStream.ReadRecord(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
|
handlerDone <- nil
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
handlerDone <- err
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
receivedCh <- record
|
|
|
|
|
if err := info.RecordStream.AckRecord(record.Seq); err != nil {
|
|
|
|
|
handlerDone <- err
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
client := NewClient().(*ClientCommon)
|
|
|
|
|
client.SetSecretKey(secret)
|
|
|
|
|
left, right := net.Pipe()
|
|
|
|
|
defer right.Close()
|
|
|
|
|
bootstrapPeerAttachConnForTest(t, server, right)
|
|
|
|
|
if err := client.ConnectByConn(left); err != nil {
|
|
|
|
|
t.Fatalf("client ConnectByConn failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
client.setByeFromServer(true)
|
|
|
|
|
_ = client.Stop()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
stream, err := client.OpenRecordStream(context.Background(), RecordOpenOptions{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("OpenRecordStream failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
for index := 0; index < total; index++ {
|
|
|
|
|
index := index
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
payload := []byte("item-" + strconv.Itoa(index))
|
2026-09-23 15:33:17 +08:00
|
|
|
if _, err := stream.WriteRecord(writeCtx, payload); err != nil {
|
2026-04-15 15:24:36 +08:00
|
|
|
t.Errorf("WriteRecord(%d) failed: %v", index, err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
2026-09-23 15:33:17 +08:00
|
|
|
if acked, err := stream.Barrier(writeCtx); err != nil {
|
2026-04-15 15:24:36 +08:00
|
|
|
t.Fatalf("Barrier failed: %v", err)
|
|
|
|
|
} else if got, want := acked, uint64(total); got != want {
|
|
|
|
|
t.Fatalf("Barrier acked=%d want=%d", got, want)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seen := make(map[uint64]string, total)
|
|
|
|
|
for index := 0; index < total; index++ {
|
|
|
|
|
select {
|
|
|
|
|
case record := <-receivedCh:
|
|
|
|
|
seen[record.Seq] = string(record.Payload)
|
|
|
|
|
case <-time.After(2 * time.Second):
|
|
|
|
|
t.Fatalf("timed out waiting record %d", index)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for seq := 1; seq <= total; seq++ {
|
|
|
|
|
payload, ok := seen[uint64(seq)]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("missing seq %d", seq)
|
|
|
|
|
}
|
|
|
|
|
if payload == "" {
|
|
|
|
|
t.Fatalf("empty payload at seq %d", seq)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := stream.CloseWrite(); err != nil {
|
|
|
|
|
t.Fatalf("CloseWrite failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case err := <-handlerDone:
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("record handler failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(2 * time.Second):
|
|
|
|
|
t.Fatal("timed out waiting record handler completion")
|
|
|
|
|
}
|
|
|
|
|
}
|