mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
This pull request introduces a new integration test tool for the StreamManager streaming system, adding a standalone test binary with supporting modules for simulating and verifying high-throughput data transfer. The changes include a test driver, a configurable in-memory delivery pipe for simulating network conditions, a data generator, a verifier for end-to-end integrity, and a metrics tracker. Additionally, several improvements are made to the circular buffer and StreamManager for better handling of blocking writes and out-of-order acknowledgments. **New StreamManager Integration Test Tool** * Added a new test binary `cmd/test-streammanager` with a main driver (`main-test-streammanager.go`) that orchestrates end-to-end streaming tests, including configuration for data size, delivery delay/skew, window size, slow reader simulation, and verbose logging. * Implemented a configurable `DeliveryPipe` (`deliverypipe.go`) for simulating network delivery with delay and skew, supporting separate data and ack channels, out-of-order delivery, and high water mark tracking. * Added `WriterBridge` and `ReaderBridge` modules for interfacing between brokers and the delivery pipe, enforcing correct directionality of data and acks. * Created a sequential test data generator (`generator.go`) and a verifier (`verifier.go`) for checking data integrity and reporting mismatches. [[1]](diffhunk://#diff-3f2d6e0349089e3748c001791a383687b33a2c2391fd3baccfceb83e76e6ee0dR1-R40) [[2]](diffhunk://#diff-cb3aab0bae9bec15ef0c06fe5d9e0e96094affcf4720680605a92054ab717575R1-R61) * Introduced a metrics module (`metrics.go`) for tracking throughput, packet counts, out-of-order events, and pipe usage, with a summary report at test completion. **StreamManager and CirBuf Improvements** * Refactored circular buffer (`pkg/jobmanager/cirbuf.go`) to replace blocking writes with a non-blocking `WriteAvailable` method, returning a wait channel for buffer-full scenarios, and removed context-based cancellation logic. * Updated StreamManager (`pkg/jobmanager/streammanager.go`) to track the maximum acknowledged sequence/rwnd tuple, ignoring stale or out-of-order ACKs, and resetting this state on disconnect. * Modified StreamManager's data handling to use the new non-blocking buffer write logic, ensuring correct signaling and waiting for space when needed. **Minor Cleanup** * Removed unused context import from `cirbuf.go`. * Minor whitespace cleanup in `streambroker.go`.
254 lines
6.7 KiB
Go
254 lines
6.7 KiB
Go
// Copyright 2026, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/wavetermdev/waveterm/pkg/jobmanager"
|
|
"github.com/wavetermdev/waveterm/pkg/streamclient"
|
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
|
)
|
|
|
|
type TestConfig struct {
|
|
Mode string
|
|
DataSize int64
|
|
Delay time.Duration
|
|
Skew time.Duration
|
|
WindowSize int
|
|
SlowReader int
|
|
Verbose bool
|
|
}
|
|
|
|
var config TestConfig
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "test-streammanager",
|
|
Short: "Integration test for StreamManager streaming system",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runTest(config)
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.Flags().StringVar(&config.Mode, "mode", "streammanager", "Writer mode: 'streammanager' or 'writer'")
|
|
rootCmd.Flags().Int64Var(&config.DataSize, "size", 10*1024*1024, "Total data to transfer (bytes)")
|
|
rootCmd.Flags().DurationVar(&config.Delay, "delay", 0, "Base delivery delay (e.g., 10ms)")
|
|
rootCmd.Flags().DurationVar(&config.Skew, "skew", 0, "Delivery skew +/- (e.g., 5ms)")
|
|
rootCmd.Flags().IntVar(&config.WindowSize, "windowsize", 64*1024, "Window size for both sender and receiver")
|
|
rootCmd.Flags().IntVar(&config.SlowReader, "slowreader", 0, "Slow reader mode: bytes per second (0=disabled, e.g., 1024)")
|
|
rootCmd.Flags().BoolVar(&config.Verbose, "verbose", false, "Enable verbose logging")
|
|
}
|
|
|
|
func main() {
|
|
if err := rootCmd.Execute(); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runTest(config TestConfig) error {
|
|
if config.Mode != "streammanager" && config.Mode != "writer" {
|
|
return fmt.Errorf("invalid mode: %s (must be 'streammanager' or 'writer')", config.Mode)
|
|
}
|
|
|
|
fmt.Printf("Starting Streaming Integration Test\n")
|
|
fmt.Printf(" Mode: %s\n", config.Mode)
|
|
fmt.Printf(" Data Size: %d bytes\n", config.DataSize)
|
|
fmt.Printf(" Delay: %v, Skew: %v\n", config.Delay, config.Skew)
|
|
fmt.Printf(" Window Size: %d\n", config.WindowSize)
|
|
if config.SlowReader > 0 {
|
|
fmt.Printf(" Slow Reader: %d bytes/sec\n", config.SlowReader)
|
|
}
|
|
|
|
// 1. Create metrics
|
|
metrics := NewMetrics()
|
|
|
|
// 2. Create the delivery pipe
|
|
pipe := NewDeliveryPipe(DeliveryConfig{
|
|
Delay: config.Delay,
|
|
Skew: config.Skew,
|
|
}, metrics)
|
|
|
|
// 3. Create brokers with bridges
|
|
writerBridge := &WriterBridge{pipe: pipe}
|
|
readerBridge := &ReaderBridge{pipe: pipe}
|
|
|
|
writerBroker := streamclient.NewBroker(writerBridge)
|
|
readerBroker := streamclient.NewBroker(readerBridge)
|
|
|
|
// 4. Wire up delivery targets
|
|
pipe.SetDataTarget(readerBroker.RecvData)
|
|
pipe.SetAckTarget(writerBroker.RecvAck)
|
|
|
|
// 5. Start the delivery pipe
|
|
pipe.Start()
|
|
|
|
// 6. Create the reader side
|
|
reader, streamMeta := readerBroker.CreateStreamReader("reader-route", "writer-route", int64(config.WindowSize))
|
|
|
|
// 7. Set up writer side based on mode
|
|
var writerDone chan error
|
|
if config.Mode == "streammanager" {
|
|
writerDone = runStreamManagerMode(config, writerBroker, streamMeta)
|
|
} else {
|
|
writerDone = runWriterMode(config, writerBroker, streamMeta)
|
|
}
|
|
|
|
// 8. Create verifier
|
|
verifier := NewVerifier(config.DataSize)
|
|
|
|
// 9. Create metrics writer wrapper
|
|
metricsWriter := &MetricsWriter{
|
|
writer: verifier,
|
|
metrics: metrics,
|
|
}
|
|
|
|
// 10. Wrap reader with slow reader if configured
|
|
var actualReader io.Reader = reader
|
|
if config.SlowReader > 0 {
|
|
actualReader = NewSlowReader(reader, config.SlowReader)
|
|
}
|
|
|
|
// 11. Start reading from stream reader and writing to verifier
|
|
metrics.Start()
|
|
|
|
readerDone := make(chan error)
|
|
go func() {
|
|
_, err := io.Copy(metricsWriter, actualReader)
|
|
readerDone <- err
|
|
}()
|
|
|
|
// 12. Wait for completion
|
|
var writerErr, readerErr error
|
|
if writerDone != nil {
|
|
writerErr = <-writerDone
|
|
}
|
|
readerErr = <-readerDone
|
|
metrics.End()
|
|
|
|
// 13. Cleanup
|
|
pipe.Close()
|
|
writerBroker.Close()
|
|
readerBroker.Close()
|
|
|
|
// 14. Report results
|
|
fmt.Println(metrics.Report())
|
|
fmt.Printf("Verification: received=%d, mismatches=%d\n",
|
|
verifier.TotalReceived(), verifier.Mismatches())
|
|
|
|
if writerErr != nil && writerErr != io.EOF {
|
|
return fmt.Errorf("writer error: %w", writerErr)
|
|
}
|
|
|
|
if readerErr != nil && readerErr != io.EOF {
|
|
return fmt.Errorf("reader error: %w", readerErr)
|
|
}
|
|
|
|
if verifier.Mismatches() > 0 {
|
|
return fmt.Errorf("data corruption: %d mismatches, first at byte %d",
|
|
verifier.Mismatches(), verifier.FirstMismatch())
|
|
}
|
|
|
|
fmt.Println("TEST PASSED")
|
|
return nil
|
|
}
|
|
|
|
func runStreamManagerMode(config TestConfig, writerBroker *streamclient.Broker, streamMeta *wshrpc.StreamMeta) chan error {
|
|
streamManager := jobmanager.MakeStreamManagerWithSizes(config.WindowSize, 2*1024*1024)
|
|
writerBroker.AttachStreamWriter(streamMeta, streamManager)
|
|
|
|
dataSender := &BrokerDataSender{broker: writerBroker}
|
|
startSeq, err := streamManager.ClientConnected(streamMeta.Id, dataSender, config.WindowSize, 0)
|
|
if err != nil {
|
|
fmt.Printf("failed to connect stream manager: %v\n", err)
|
|
return nil
|
|
}
|
|
fmt.Printf(" Stream connected, startSeq: %d\n", startSeq)
|
|
|
|
generator := NewTestDataGenerator(config.DataSize)
|
|
if err := streamManager.AttachReader(generator); err != nil {
|
|
fmt.Printf("failed to attach reader: %v\n", err)
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runWriterMode(config TestConfig, writerBroker *streamclient.Broker, streamMeta *wshrpc.StreamMeta) chan error {
|
|
writer, err := writerBroker.CreateStreamWriter(streamMeta)
|
|
if err != nil {
|
|
fmt.Printf("failed to create stream writer: %v\n", err)
|
|
return nil
|
|
}
|
|
fmt.Printf(" Stream writer created\n")
|
|
|
|
generator := NewTestDataGenerator(config.DataSize)
|
|
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
_, copyErr := io.Copy(writer, generator)
|
|
closeErr := writer.Close()
|
|
if copyErr != nil && copyErr != io.EOF {
|
|
done <- copyErr
|
|
} else {
|
|
done <- closeErr
|
|
}
|
|
}()
|
|
|
|
return done
|
|
}
|
|
|
|
// BrokerDataSender implements DataSender interface
|
|
type BrokerDataSender struct {
|
|
broker *streamclient.Broker
|
|
}
|
|
|
|
func (s *BrokerDataSender) SendData(dataPk wshrpc.CommandStreamData) {
|
|
s.broker.SendData(dataPk)
|
|
}
|
|
|
|
// MetricsWriter wraps an io.Writer and records bytes written to metrics
|
|
type MetricsWriter struct {
|
|
writer io.Writer
|
|
metrics *Metrics
|
|
}
|
|
|
|
func (mw *MetricsWriter) Write(p []byte) (n int, err error) {
|
|
n, err = mw.writer.Write(p)
|
|
if n > 0 {
|
|
mw.metrics.AddBytes(int64(n))
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
// SlowReader wraps an io.Reader and rate-limits reads to a specified bytes/sec
|
|
type SlowReader struct {
|
|
reader io.Reader
|
|
bytesPerSec int
|
|
}
|
|
|
|
func NewSlowReader(reader io.Reader, bytesPerSec int) *SlowReader {
|
|
return &SlowReader{
|
|
reader: reader,
|
|
bytesPerSec: bytesPerSec,
|
|
}
|
|
}
|
|
|
|
func (sr *SlowReader) Read(p []byte) (n int, err error) {
|
|
time.Sleep(1 * time.Second)
|
|
|
|
readSize := sr.bytesPerSec
|
|
if readSize > len(p) {
|
|
readSize = len(p)
|
|
}
|
|
|
|
n, err = sr.reader.Read(p[:readSize])
|
|
log.Printf("SlowReader: read %d bytes, err=%v", n, err)
|
|
return n, err
|
|
}
|