test(load): add idle and tunable group scenarios
This commit is contained in:
@@ -49,7 +49,7 @@ type agentRun struct {
|
||||
}
|
||||
|
||||
func validScenario(scenario string) bool {
|
||||
return scenario == "p2p" || scenario == "group" || scenario == "mixed"
|
||||
return scenario == "p2p" || scenario == "group" || scenario == "mixed" || scenario == "idle"
|
||||
}
|
||||
|
||||
func preflightP2P(ctx context.Context, cfg *Config, manifest *Manifest, local []AccountRecord, apiKey string) error {
|
||||
@@ -680,10 +680,25 @@ func (r *agentRun) connect(ctx context.Context, accounts []AccountRecord, groupT
|
||||
}
|
||||
|
||||
func (r *agentRun) maintainConnections(ctx context.Context, accounts []AccountRecord, groupTopic, apiKey, password string) {
|
||||
type reconnectState struct {
|
||||
attempt int
|
||||
nextAt time.Time
|
||||
}
|
||||
byIndex := make(map[int]AccountRecord, len(accounts))
|
||||
for _, account := range accounts {
|
||||
byIndex[account.Index] = account
|
||||
}
|
||||
states := make(map[int]reconnectState, len(accounts))
|
||||
var stateMu sync.Mutex
|
||||
minWait := mustDuration(r.cfg.ReconnectMinWait)
|
||||
maxWait := mustDuration(r.cfg.ReconnectMaxWait)
|
||||
if maxWait < minWait {
|
||||
maxWait = minWait
|
||||
}
|
||||
reconnectWorkers := r.cfg.ReconnectWorkers
|
||||
if reconnectWorkers <= 0 {
|
||||
reconnectWorkers = 8
|
||||
}
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
@@ -692,7 +707,7 @@ func (r *agentRun) maintainConnections(ctx context.Context, accounts []AccountRe
|
||||
}
|
||||
jobs := make(chan AccountRecord)
|
||||
var wg sync.WaitGroup
|
||||
workers := 16
|
||||
workers := reconnectWorkers
|
||||
if workers > len(accounts) {
|
||||
workers = len(accounts)
|
||||
}
|
||||
@@ -714,8 +729,17 @@ func (r *agentRun) maintainConnections(ctx context.Context, accounts []AccountRe
|
||||
if ctx.Err() == nil {
|
||||
log.Printf("account=%s reconnect failed: %v", account.Username, err)
|
||||
}
|
||||
stateMu.Lock()
|
||||
state := states[index]
|
||||
state.attempt++
|
||||
state.nextAt = time.Now().Add(reconnectBackoff(minWait, maxWait, state.attempt, index))
|
||||
states[index] = state
|
||||
stateMu.Unlock()
|
||||
continue
|
||||
}
|
||||
stateMu.Lock()
|
||||
delete(states, index)
|
||||
stateMu.Unlock()
|
||||
r.setClient(index, client)
|
||||
log.Printf("account=%s connection restored", account.Username)
|
||||
}
|
||||
@@ -732,6 +756,12 @@ func (r *agentRun) maintainConnections(ctx context.Context, accounts []AccountRe
|
||||
if current != nil && current.alive() {
|
||||
continue
|
||||
}
|
||||
stateMu.Lock()
|
||||
nextAt := states[index].nextAt
|
||||
stateMu.Unlock()
|
||||
if !nextAt.IsZero() && time.Now().Before(nextAt) {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case jobs <- account:
|
||||
case <-ctx.Done():
|
||||
@@ -750,6 +780,25 @@ func (r *agentRun) maintainConnections(ctx context.Context, accounts []AccountRe
|
||||
}
|
||||
}
|
||||
|
||||
func reconnectBackoff(minWait, maxWait time.Duration, attempt, index int) time.Duration {
|
||||
if attempt < 1 {
|
||||
attempt = 1
|
||||
}
|
||||
delay := minWait
|
||||
for i := 1; i < attempt && delay < maxWait; i++ {
|
||||
delay *= 2
|
||||
}
|
||||
if delay > maxWait {
|
||||
delay = maxWait
|
||||
}
|
||||
jitterBase := delay / 4
|
||||
if jitterBase <= 0 {
|
||||
return delay
|
||||
}
|
||||
jitter := time.Duration((index*97+attempt*53)%1000) * jitterBase / 1000
|
||||
return delay + jitter
|
||||
}
|
||||
|
||||
func (r *agentRun) connectAccountWithRetry(ctx context.Context, account AccountRecord, groupTopic, apiKey, password string) (*WSClient, error) {
|
||||
var lastErr error
|
||||
for attempt := 1; attempt <= 6; attempt++ {
|
||||
@@ -787,12 +836,25 @@ func (r *agentRun) execute(ctx context.Context, start time.Time, groupTopic stri
|
||||
case "p2p":
|
||||
return r.scheduleP2P(ctx, start, r.cfg.UsersPerMachine, r.cfg.P2PMessages, mustDuration(r.cfg.P2PInterval))
|
||||
case "group":
|
||||
return r.scheduleGroup(ctx, start, groupTopic, 1, scaledUsers(r.cfg.UsersPerMachine, 1, 2), 2)
|
||||
senders := r.cfg.GroupSenders
|
||||
if senders <= 0 {
|
||||
senders = scaledUsers(r.cfg.UsersPerMachine, 1, 2)
|
||||
}
|
||||
if senders > r.cfg.UsersPerMachine {
|
||||
return fmt.Errorf("group_senders_per_machine=%d exceeds users_per_machine=%d", senders, r.cfg.UsersPerMachine)
|
||||
}
|
||||
return r.scheduleGroup(ctx, start, groupTopic, 1, senders, r.cfg.GroupRounds, mustDuration(r.cfg.GroupInterval))
|
||||
case "mixed":
|
||||
p2pUsers := scaledUsers(r.cfg.UsersPerMachine, 3, 10)
|
||||
groupSenders := scaledUsers(r.cfg.UsersPerMachine, 1, 4)
|
||||
p2pUsers := r.cfg.MixedP2PUsers
|
||||
if p2pUsers <= 0 {
|
||||
p2pUsers = scaledUsers(r.cfg.UsersPerMachine, 3, 10)
|
||||
}
|
||||
groupSenders := r.cfg.MixedGroupSenders
|
||||
if groupSenders <= 0 {
|
||||
groupSenders = scaledUsers(r.cfg.UsersPerMachine, 1, 4)
|
||||
}
|
||||
if p2pUsers+groupSenders > r.cfg.UsersPerMachine {
|
||||
groupSenders = r.cfg.UsersPerMachine - p2pUsers
|
||||
return fmt.Errorf("mixed_p2p_users_per_machine + mixed_group_senders_per_machine exceeds users_per_machine")
|
||||
}
|
||||
if groupSenders <= 0 {
|
||||
return fmt.Errorf("users_per_machine=%d is too small for mixed scenario", r.cfg.UsersPerMachine)
|
||||
@@ -803,7 +865,7 @@ func (r *agentRun) execute(ctx context.Context, start time.Time, groupTopic stri
|
||||
go func() { defer wg.Done(); errCh <- r.scheduleP2P(ctx, start, p2pUsers, 5, 20*time.Second) }()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errCh <- r.scheduleGroup(ctx, start, groupTopic, p2pUsers+1, p2pUsers+groupSenders, 2)
|
||||
errCh <- r.scheduleGroup(ctx, start, groupTopic, p2pUsers+1, p2pUsers+groupSenders, r.cfg.GroupRounds, mustDuration(r.cfg.GroupInterval))
|
||||
}()
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
@@ -813,6 +875,8 @@ func (r *agentRun) execute(ctx context.Context, start time.Time, groupTopic stri
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case "idle":
|
||||
return waitContext(ctx, mustDuration(r.cfg.IdleDuration))
|
||||
default:
|
||||
return fmt.Errorf("unknown scenario %s", r.scenario)
|
||||
}
|
||||
@@ -867,15 +931,19 @@ func (r *agentRun) scheduleP2P(ctx context.Context, start time.Time, users, mess
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (r *agentRun) scheduleGroup(ctx context.Context, start time.Time, topic string, first, last, rounds int) error {
|
||||
func (r *agentRun) scheduleGroup(ctx context.Context, start time.Time, topic string, first, last, rounds int, globalInterval time.Duration) error {
|
||||
if first < 1 || last > r.clientCount() || first > last {
|
||||
return fmt.Errorf("invalid group publisher range %d-%d", first, last)
|
||||
}
|
||||
count := last - first + 1
|
||||
roundDuration := time.Duration(count) * 200 * time.Millisecond
|
||||
if globalInterval <= 0 {
|
||||
return fmt.Errorf("group publish interval must be positive")
|
||||
}
|
||||
perMachineInterval := 2 * globalInterval
|
||||
roundDuration := time.Duration(count) * perMachineInterval
|
||||
offset := time.Duration(0)
|
||||
if r.machine == "b" {
|
||||
offset = 100 * time.Millisecond
|
||||
offset = globalInterval
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for index := first; index <= last; index++ {
|
||||
@@ -884,7 +952,7 @@ func (r *agentRun) scheduleGroup(ctx context.Context, start time.Time, topic str
|
||||
go func(index, position int) {
|
||||
defer wg.Done()
|
||||
for round := 1; round <= rounds; round++ {
|
||||
target := start.Add(time.Duration(round-1)*roundDuration + time.Duration(position)*200*time.Millisecond + offset)
|
||||
target := start.Add(time.Duration(round-1)*roundDuration + time.Duration(position)*perMachineInterval + offset)
|
||||
if waitUntil(ctx, target) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user