2026-04-15 15:24:36 +08:00
|
|
|
package notify
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-16 17:27:48 +08:00
|
|
|
"bytes"
|
2026-04-15 15:24:36 +08:00
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"net"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-16 17:27:48 +08:00
|
|
|
func TestCloneBulkDedicatedSendRequestsDeepCopiesPayload(t *testing.T) {
|
|
|
|
|
src := []bulkDedicatedSendRequest{
|
|
|
|
|
{
|
|
|
|
|
Type: bulkFastPayloadTypeData,
|
|
|
|
|
Seq: 1,
|
|
|
|
|
Payload: []byte("payload-a"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Type: bulkFastPayloadTypeReset,
|
|
|
|
|
Seq: 2,
|
|
|
|
|
Payload: []byte("payload-b"),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cloned := cloneBulkDedicatedSendRequests(src)
|
|
|
|
|
if len(cloned) != len(src) {
|
|
|
|
|
t.Fatalf("clone length = %d, want %d", len(cloned), len(src))
|
|
|
|
|
}
|
|
|
|
|
if &cloned[0] == &src[0] {
|
|
|
|
|
t.Fatal("request clone should not alias source slice elements")
|
|
|
|
|
}
|
|
|
|
|
if len(cloned[0].Payload) == 0 || len(src[0].Payload) == 0 {
|
|
|
|
|
t.Fatal("payload should not be empty")
|
|
|
|
|
}
|
|
|
|
|
if &cloned[0].Payload[0] == &src[0].Payload[0] {
|
|
|
|
|
t.Fatal("payload clone should not alias source bytes")
|
|
|
|
|
}
|
|
|
|
|
src[0].Payload[0] = 'X'
|
|
|
|
|
if bytes.Equal(cloned[0].Payload, src[0].Payload) {
|
|
|
|
|
t.Fatal("mutating source payload should not affect cloned payload")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 15:24:36 +08:00
|
|
|
func TestBulkDedicatedBatchPlainRoundTrip(t *testing.T) {
|
|
|
|
|
releasePayload, err := encodeBulkDedicatedReleasePayload(4096, 2)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("encodeBulkDedicatedReleasePayload failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
items := []bulkDedicatedSendRequest{
|
|
|
|
|
{
|
|
|
|
|
Type: bulkFastPayloadTypeData,
|
|
|
|
|
Seq: 7,
|
|
|
|
|
Payload: []byte("hello"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Type: bulkFastPayloadTypeClose,
|
|
|
|
|
Flags: bulkFastPayloadFlagFullClose,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Type: bulkFastPayloadTypeReset,
|
|
|
|
|
Payload: []byte("boom"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Type: bulkFastPayloadTypeRelease,
|
|
|
|
|
Payload: releasePayload,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plain, err := encodeBulkDedicatedBatchPlain(42, items)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("encodeBulkDedicatedBatchPlain failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dataID, decoded, matched, err := decodeBulkDedicatedBatchPlain(plain)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("decodeBulkDedicatedBatchPlain failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !matched {
|
|
|
|
|
t.Fatal("decodeBulkDedicatedBatchPlain should match dedicated batch")
|
|
|
|
|
}
|
|
|
|
|
if dataID != 42 {
|
|
|
|
|
t.Fatalf("decoded data id = %d, want 42", dataID)
|
|
|
|
|
}
|
|
|
|
|
if len(decoded) != len(items) {
|
|
|
|
|
t.Fatalf("decoded item count = %d, want %d", len(decoded), len(items))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := range items {
|
|
|
|
|
if decoded[i].Type != items[i].Type {
|
|
|
|
|
t.Fatalf("item %d type = %d, want %d", i, decoded[i].Type, items[i].Type)
|
|
|
|
|
}
|
|
|
|
|
if decoded[i].Flags != items[i].Flags {
|
|
|
|
|
t.Fatalf("item %d flags = %d, want %d", i, decoded[i].Flags, items[i].Flags)
|
|
|
|
|
}
|
|
|
|
|
if decoded[i].Seq != items[i].Seq {
|
|
|
|
|
t.Fatalf("item %d seq = %d, want %d", i, decoded[i].Seq, items[i].Seq)
|
|
|
|
|
}
|
|
|
|
|
if got, want := string(decoded[i].Payload), string(items[i].Payload); got != want {
|
|
|
|
|
t.Fatalf("item %d payload = %q, want %q", i, got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBulkOpenRoundTripDedicatedMultiWriteTCP(t *testing.T) {
|
|
|
|
|
server := NewServer().(*ServerCommon)
|
|
|
|
|
if err := UseModernPSKServer(server, integrationSharedSecret, integrationModernPSKOptions()); err != nil {
|
|
|
|
|
t.Fatalf("UseModernPSKServer failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acceptCh := make(chan BulkAcceptInfo, 1)
|
|
|
|
|
server.SetBulkHandler(func(info BulkAcceptInfo) error {
|
|
|
|
|
acceptCh <- info
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if err := server.Listen("tcp", "127.0.0.1:0"); err != nil {
|
|
|
|
|
t.Fatalf("server Listen failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
_ = server.Stop()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
client := NewClient().(*ClientCommon)
|
|
|
|
|
if err := UseModernPSKClient(client, integrationSharedSecret, integrationModernPSKOptions()); err != nil {
|
|
|
|
|
t.Fatalf("UseModernPSKClient failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := client.Connect("tcp", server.listener.Addr().String()); err != nil {
|
|
|
|
|
t.Fatalf("client Connect failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
_ = client.Stop()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
bulk, err := client.OpenBulk(context.Background(), BulkOpenOptions{
|
|
|
|
|
Range: BulkRange{
|
|
|
|
|
Offset: 0,
|
|
|
|
|
Length: 1024,
|
|
|
|
|
},
|
|
|
|
|
Dedicated: true,
|
|
|
|
|
ChunkSize: 4,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("client OpenBulk dedicated failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
accepted := waitAcceptedBulk(t, acceptCh, 2*time.Second)
|
|
|
|
|
|
|
|
|
|
clientParts := []string{"aa", "bb", "cc", "dd", "ee", "ff"}
|
|
|
|
|
for _, part := range clientParts {
|
|
|
|
|
if _, err := bulk.Write([]byte(part)); err != nil {
|
|
|
|
|
t.Fatalf("client dedicated bulk Write(%q) failed: %v", part, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
readBulkExactly(t, accepted.Bulk, "aabbccddeeff", 2*time.Second)
|
|
|
|
|
|
|
|
|
|
serverParts := []string{"11", "22", "33", "44", "55", "66"}
|
|
|
|
|
for _, part := range serverParts {
|
|
|
|
|
if _, err := accepted.Bulk.Write([]byte(part)); err != nil {
|
|
|
|
|
t.Fatalf("server dedicated bulk Write(%q) failed: %v", part, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
readBulkExactly(t, bulk, "112233445566", 2*time.Second)
|
|
|
|
|
|
|
|
|
|
if err := bulk.CloseWrite(); err != nil {
|
|
|
|
|
t.Fatalf("client dedicated bulk CloseWrite failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
waitForBulkReadEOF(t, accepted.Bulk, 2*time.Second)
|
|
|
|
|
|
|
|
|
|
if err := accepted.Bulk.Close(); err != nil {
|
|
|
|
|
t.Fatalf("server dedicated bulk Close failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
waitForBulkReadEOF(t, bulk, 2*time.Second)
|
|
|
|
|
waitForBulkContextDone(t, bulk.Context(), 2*time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBulkDedicatedSenderRespectsWriteDeadlineWhenReceiverStalls(t *testing.T) {
|
|
|
|
|
left, right := net.Pipe()
|
|
|
|
|
defer left.Close()
|
|
|
|
|
defer right.Close()
|
|
|
|
|
|
|
|
|
|
sender := newBulkDedicatedSender(left, 1, func(plain []byte) ([]byte, error) {
|
|
|
|
|
return plain, nil
|
|
|
|
|
}, nil, nil)
|
|
|
|
|
defer sender.stop()
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
errCh := make(chan error, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
errCh <- sender.submitControl(ctx, bulkFastPayloadTypeClose, 0, 0, nil)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case err := <-errCh:
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("sender.submitControl should fail when receiver stalls")
|
|
|
|
|
}
|
|
|
|
|
if !isTimeoutLikeError(err) {
|
|
|
|
|
t.Fatalf("sender.submitControl error = %v, want timeout-like error", err)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatal("sender.submitControl should not hang when receiver stalls")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBulkDedicatedSenderSubmitWriteDirectPathRespectsWriteDeadlineWhenReceiverStalls(t *testing.T) {
|
|
|
|
|
left, right := net.Pipe()
|
|
|
|
|
defer left.Close()
|
|
|
|
|
defer right.Close()
|
|
|
|
|
|
|
|
|
|
sender := newBulkDedicatedSender(left, 1, func(plain []byte) ([]byte, error) {
|
|
|
|
|
return plain, nil
|
|
|
|
|
}, nil, nil)
|
|
|
|
|
defer sender.stop()
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
payload := make([]byte, 256*1024)
|
|
|
|
|
errCh := make(chan error, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
_, err := sender.submitWrite(ctx, 1, payload, len(payload))
|
|
|
|
|
errCh <- err
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case err := <-errCh:
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("sender.submitWrite should fail when receiver stalls")
|
|
|
|
|
}
|
|
|
|
|
if !isTimeoutLikeError(err) {
|
|
|
|
|
t.Fatalf("sender.submitWrite error = %v, want timeout-like error", err)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatal("sender.submitWrite should not hang when receiver stalls")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBulkDedicatedSenderSkipsQueuedCanceledRequest(t *testing.T) {
|
|
|
|
|
conn := newBlockingPacketWriteConn()
|
|
|
|
|
sender := newBulkDedicatedSender(conn, 1, func(plain []byte) ([]byte, error) {
|
|
|
|
|
return plain, nil
|
|
|
|
|
}, nil, nil)
|
|
|
|
|
defer sender.stop()
|
|
|
|
|
|
|
|
|
|
firstErrCh := make(chan error, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
firstErrCh <- sender.submitControl(context.Background(), bulkFastPayloadTypeClose, 0, 1, nil)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-conn.startCh:
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatal("first dedicated bulk write did not start")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
secondErrCh := make(chan error, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
secondErrCh <- sender.submitControl(ctx, bulkFastPayloadTypeReset, 0, 2, nil)
|
|
|
|
|
}()
|
|
|
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case err := <-secondErrCh:
|
|
|
|
|
if !errors.Is(err, context.Canceled) {
|
|
|
|
|
t.Fatalf("second dedicated bulk submit error = %v, want %v", err, context.Canceled)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatal("second dedicated bulk submit did not return after cancel")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
close(conn.unblockCh)
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case err := <-firstErrCh:
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("first dedicated bulk submit failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatal("first dedicated bulk submit did not finish")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
|
if got, want := conn.writeCount.Load(), int32(2); got != want {
|
|
|
|
|
t.Fatalf("dedicated bulk write count = %d, want %d", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBulkDedicatedSenderReturnsFlushResultAfterStartedContextCancel(t *testing.T) {
|
|
|
|
|
conn := newBlockingPacketWriteConn()
|
|
|
|
|
sender := newBulkDedicatedSender(conn, 1, func(plain []byte) ([]byte, error) {
|
|
|
|
|
return plain, nil
|
|
|
|
|
}, nil, nil)
|
|
|
|
|
defer sender.stop()
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
errCh := make(chan error, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
errCh <- sender.submitControl(ctx, bulkFastPayloadTypeClose, 0, 1, nil)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-conn.startCh:
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatal("dedicated bulk write did not start")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case err := <-errCh:
|
|
|
|
|
t.Fatalf("sender.submitControl returned before flush completed: %v", err)
|
|
|
|
|
case <-time.After(50 * time.Millisecond):
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
close(conn.unblockCh)
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case err := <-errCh:
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("sender.submitControl failed after started flush: %v", err)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
t.Fatal("sender.submitControl did not return after started flush completed")
|
|
|
|
|
}
|
|
|
|
|
}
|