164 lines
3.7 KiB
Go
164 lines
3.7 KiB
Go
package loadsim
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
|
|
|
|
type Runner struct {
|
|
Dial DialFunc
|
|
}
|
|
|
|
type Stats struct {
|
|
ConnectionsOpened int64
|
|
ConnectionsFailed int64
|
|
FramesWritten int64
|
|
WriteErrors int64
|
|
ResponseBytes int64
|
|
ReadErrors int64
|
|
}
|
|
|
|
func (r Runner) Run(ctx context.Context, cfg Config) (Stats, error) {
|
|
phoneBase := cfg.JT808PhoneBase
|
|
if phoneBase <= 0 {
|
|
phoneBase = DefaultJT808PhoneBase
|
|
}
|
|
if _, err := (&FlagConfig{
|
|
protocol: string(cfg.Protocol),
|
|
addr: cfg.Addr,
|
|
connections: cfg.Connections,
|
|
connectRate: cfg.ConnectRatePerSecond,
|
|
sendInterval: cfg.SendInterval,
|
|
duration: cfg.Duration,
|
|
template: cfg.Template,
|
|
jt808PhoneBase: phoneBase,
|
|
drainResponses: cfg.DrainResponses,
|
|
}).Build(); err != nil {
|
|
return Stats{}, err
|
|
}
|
|
var factory *FrameFactory
|
|
if cfg.SendFrames {
|
|
var err error
|
|
factory, err = NewFrameFactoryWithJT808PhoneBase(cfg.Protocol, cfg.Template, phoneBase)
|
|
if err != nil {
|
|
return Stats{}, err
|
|
}
|
|
}
|
|
dial := r.Dial
|
|
if dial == nil {
|
|
dialer := &net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second}
|
|
dial = dialer.DialContext
|
|
}
|
|
|
|
runCtx, cancel := context.WithTimeout(ctx, cfg.Duration)
|
|
defer cancel()
|
|
|
|
var stats Stats
|
|
var wg sync.WaitGroup
|
|
connectionIndex := 0
|
|
for i, batch := range ConnectionBatches(cfg.Connections, cfg.ConnectRatePerSecond) {
|
|
if i > 0 {
|
|
if !sleepOrDone(runCtx, time.Second) {
|
|
break
|
|
}
|
|
}
|
|
for range batch {
|
|
if runCtx.Err() != nil {
|
|
break
|
|
}
|
|
conn, err := dial(runCtx, "tcp", cfg.Addr)
|
|
if err != nil {
|
|
atomic.AddInt64(&stats.ConnectionsFailed, 1)
|
|
continue
|
|
}
|
|
atomic.AddInt64(&stats.ConnectionsOpened, 1)
|
|
wg.Add(1)
|
|
connIndex := connectionIndex
|
|
connectionIndex++
|
|
go func() {
|
|
defer wg.Done()
|
|
if cfg.SendFrames {
|
|
var readDone chan struct{}
|
|
if cfg.DrainResponses {
|
|
readDone = make(chan struct{})
|
|
go func() {
|
|
drainResponses(runCtx, conn, &stats)
|
|
close(readDone)
|
|
}()
|
|
}
|
|
writeLoop(runCtx, conn, factory, connIndex, cfg.SendInterval, &stats)
|
|
_ = conn.Close()
|
|
if readDone != nil {
|
|
<-readDone
|
|
}
|
|
return
|
|
}
|
|
<-runCtx.Done()
|
|
_ = conn.Close()
|
|
}()
|
|
}
|
|
}
|
|
<-runCtx.Done()
|
|
wg.Wait()
|
|
return stats, nil
|
|
}
|
|
|
|
func drainResponses(ctx context.Context, conn net.Conn, stats *Stats) {
|
|
buffer := make([]byte, 4096)
|
|
for {
|
|
_ = conn.SetReadDeadline(time.Now().Add(time.Second))
|
|
read, err := conn.Read(buffer)
|
|
if read > 0 {
|
|
atomic.AddInt64(&stats.ResponseBytes, int64(read))
|
|
}
|
|
if err == nil {
|
|
continue
|
|
}
|
|
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) {
|
|
return
|
|
}
|
|
if timeout, ok := err.(net.Error); ok && timeout.Timeout() {
|
|
continue
|
|
}
|
|
atomic.AddInt64(&stats.ReadErrors, 1)
|
|
return
|
|
}
|
|
}
|
|
|
|
func writeLoop(ctx context.Context, conn net.Conn, factory *FrameFactory, connectionIndex int, interval time.Duration, stats *Stats) {
|
|
for frameIndex := int64(0); ; frameIndex++ {
|
|
payload, err := factory.Frame(connectionIndex, frameIndex)
|
|
if err != nil {
|
|
atomic.AddInt64(&stats.WriteErrors, 1)
|
|
return
|
|
}
|
|
_ = conn.SetWriteDeadline(time.Now().Add(3 * time.Second))
|
|
if _, err := conn.Write(payload); err != nil {
|
|
atomic.AddInt64(&stats.WriteErrors, 1)
|
|
return
|
|
}
|
|
atomic.AddInt64(&stats.FramesWritten, 1)
|
|
if !sleepOrDone(ctx, interval) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func sleepOrDone(ctx context.Context, duration time.Duration) bool {
|
|
timer := time.NewTimer(duration)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-timer.C:
|
|
return true
|
|
}
|
|
}
|