package notify import ( "b612.me/stario" "context" "errors" netpkg "net" "sync/atomic" "testing" "time" ) type gatedDedicatedReadConn struct { netpkg.Conn total int read int reached chan struct{} release chan struct{} closed bool } func (c *gatedDedicatedReadConn) Read(p []byte) (int, error) { n, err := c.Conn.Read(p) c.read += n if !c.closed && c.read >= c.total { c.closed = true close(c.reached) <-c.release } return n, err } func TestControlMessageErrorPreservesTransportDetachedSentinel(t *testing.T) { for name, decode := range map[string]func(string) error{ "bulk": bulkControlMessageError, "stream": streamControlMessageError, } { t.Run(name, func(t *testing.T) { err := decode("transport detached: stale transport generation=7") if !errors.Is(err, errTransportDetached) { t.Fatalf("decoded error = %v, want transport detached sentinel", err) } }) } } func TestSendDedicatedBulkAttachRequestRejectsNilDialResult(t *testing.T) { client := &ClientCommon{} bulk := newBulkHandle(context.Background(), newBulkRuntime("cblk"), clientFileScope(), BulkOpenRequest{ BulkID: "nil-dial", }, 0, nil, nil, 0, nil, nil, nil, nil, nil) if _, err := client.sendDedicatedBulkAttachRequest(context.Background(), nil, bulk); !errors.Is(err, errTransportDetached) { t.Fatalf("nil dial result error = %v, want transport detached", err) } } func TestDedicatedSidecarCloseWaitsForAttachment(t *testing.T) { left, right := netpkg.Pipe() defer left.Close() defer right.Close() sidecar := newBulkDedicatedSidecar(left, 1) entered := make(chan struct{}) release := make(chan struct{}) attachDone := make(chan error, 1) go func() { attachDone <- sidecar.withConn(func(conn netpkg.Conn) error { close(entered) <-release return nil }) }() select { case <-entered: case <-time.After(time.Second): t.Fatal("sidecar attachment did not start") } closeDone := make(chan struct{}) go func() { sidecar.close() close(closeDone) }() select { case <-closeDone: t.Fatal("sidecar closed while attachment still held") case <-time.After(25 * time.Millisecond): } close(release) if err := <-attachDone; err != nil { t.Fatalf("sidecar attachment error = %v", err) } select { case <-closeDone: case <-time.After(time.Second): t.Fatal("sidecar close did not finish after attachment release") } } func TestBulkAcceptDispatchRejectsResetBeforeHandler(t *testing.T) { bulk := newBulkHandle(context.Background(), newBulkRuntime("sblk"), serverFileDomain+":test", BulkOpenRequest{ BulkID: "stale-dispatch", }, 0, nil, nil, 0, nil, nil, nil, nil, nil) bulk.markReset(errTransportDetached) var calls atomic.Int32 err := dispatchBulkAccept(func(BulkAcceptInfo) error { calls.Add(1) return nil }, bulk, BulkAcceptInfo{Bulk: bulk}) if !errors.Is(err, errTransportDetached) { t.Fatalf("stale dispatch error = %v, want transport detached", err) } if got := calls.Load(); got != 0 { t.Fatalf("stale dispatch handler calls = %d, want 0", got) } } func TestClientDedicatedSidecarDropsFrameAfterTransportReattach(t *testing.T) { client := NewClient().(*ClientCommon) UseLegacySecurityClient(client) stopCtx, stopFn := context.WithCancel(context.Background()) defer stopFn() queue := stario.NewQueueCtx(stopCtx, 4, ^uint32(0)) oldLeft, oldRight := netpkg.Pipe() defer oldRight.Close() epoch := client.beginClientSessionEpoch() client.setClientSessionRuntime(newClientSessionRuntime(oldLeft, stopCtx, stopFn, queue, epoch)) client.markSessionStarted() defer client.markSessionStopped("test done", nil) oldRoute := client.clientSessionRouteSnapshot() sidecarLeft, sidecarRight := netpkg.Pipe() defer sidecarRight.Close() payload, err := client.encodeDedicatedBulkBatchPayload(101, []bulkDedicatedSendRequest{{ Type: bulkFastPayloadTypeData, Seq: 1, Payload: []byte("stale"), }}) if err != nil { t.Fatalf("encode stale sidecar payload: %v", err) } frameRead := make(chan struct{}) releaseFrame := make(chan struct{}) readConn := &gatedDedicatedReadConn{ Conn: sidecarLeft, total: bulkDedicatedRecordHeaderLen + len(payload), reached: frameRead, release: releaseFrame, } sidecar := newBulkDedicatedSidecar(readConn, 1) loopDone := make(chan struct{}) go func() { client.readDedicatedSidecarLoopAtRoute(sidecar, oldRoute) close(loopDone) }() writeDone := make(chan error, 1) go func() { writeDone <- writeBulkDedicatedRecord(sidecarRight, payload) }() select { case <-frameRead: case <-time.After(time.Second): t.Fatal("sidecar loop did not read the complete frame") } newLeft, newRight := netpkg.Pipe() defer newRight.Close() if err := client.attachClientSessionTransport(newLeft); err != nil { t.Fatalf("attach replacement client transport: %v", err) } runtime := client.getBulkRuntime() bulk := newBulkHandle(stopCtx, runtime, clientFileScope(), BulkOpenRequest{ BulkID: "stale-sidecar-frame", DataID: 101, Range: BulkRange{Length: 32}, }, oldRoute.epoch, nil, nil, 0, nil, nil, nil, nil, nil) bulk.setClientSnapshotOwner(client) bulk.setClientSessionRoute(oldRoute) if err := runtime.registerInbound(clientFileScope(), bulk); err != nil { t.Fatalf("register stale-route bulk: %v", err) } defer bulk.markReset(errors.New("test cleanup")) close(releaseFrame) if err := <-writeDone; err != nil { t.Fatalf("write stale sidecar payload: %v", err) } select { case <-loopDone: case <-time.After(time.Second): t.Fatal("stale-route sidecar loop did not stop after reattach") } bulk.mu.Lock() defer bulk.mu.Unlock() if len(bulk.readQueue) != 0 || len(bulk.readBuf.data) != 0 || bulk.resetErr != nil { t.Fatalf("stale sidecar frame mutated bulk: queued=%d buffered=%d reset=%v", len(bulk.readQueue), len(bulk.readBuf.data), bulk.resetErr) } }