Repository: isucon/isucon11-qualify (language: go)

Issue:
[bench] 存在するISUにpost conditionで不正なconditionを投げる



Discussion / hints:

Fix patch:
diff --git a/bench/model/isu.go b/bench/model/isu.go
index edb39460f..ffb93513d 100644
--- a/bench/model/isu.go
+++ b/bench/model/isu.go
@@ -96,30 +96,22 @@ func NewRandomIsuRaw(owner *User) (*Isu, *StreamsForPoster, error) {
 	return isu, streamsForPoster, nil
 }
 
-func NewIsuRawForInitData(isu *Isu, owner *User, jiaIsuUUID string) (*Isu, *StreamsForPoster, error) {
-	stateChan := make(chan IsuStateChange, 1)
+func NewIsuRawForInitData(isu *Isu, owner *User, jiaIsuUUID string) (*Isu, error) {
+	//stateChan := make(chan IsuStateChange, 1)
 	//conditionChan := make(chan []IsuCondition, 10)
 
 	isu = &Isu{
-		ID:         isu.ID,
-		Owner:      owner,
-		JIAIsuUUID: jiaIsuUUID,
-		Name:       isu.Name,
-		ImageHash:  isu.ImageHash,
-		Character:  isu.Character,
-		Conditions: isu.Conditions,
-		StreamsForScenario: &StreamsForScenario{
-			StateChan: stateChan,
-			//ConditionChan: conditionChan,
-		},
+		ID:                 isu.ID,
+		Owner:              owner,
+		JIAIsuUUID:         jiaIsuUUID,
+		Name:               isu.Name,
+		ImageHash:          isu.ImageHash,
+		Character:          isu.Character,
+		Conditions:         isu.Conditions,
+		StreamsForScenario: nil,
 	}
 
-	streamsForPoster := &StreamsForPoster{
-		StateChan: stateChan,
-		//ConditionChan: conditionChan,
-	}
-
-	return isu, streamsForPoster, nil
+	return isu, nil
 }
 
 var defaultIconHash [md5.Size]byte
@@ -160,3 +152,7 @@ func (i *Isu) AddIsuConditions(conditions []IsuCondition) {
 // 	defer i.condMutex.RUnlock()
 // 	return i.conditions[:]
 // }
+
+func (isu *Isu) IsNoPoster() bool {
+	return isu.StreamsForScenario == nil
+}
diff --git a/bench/scenario/initialize_data.go b/bench/scenario/initialize_data.go
index d3f7d8930..b9d5f0cca 100644
--- a/bench/scenario/initialize_data.go
+++ b/bench/scenario/initialize_data.go
@@ -31,7 +31,7 @@ func (s *Scenario) InitializeData(ctx context.Context) {
 
 		for key, _ := range user.IsuListByID {
 			// isu の初期化
-			isu, _, err := model.NewIsuRawForInitData(user.IsuListByID[key], &user, key)
+			isu, err := model.NewIsuRawForInitData(user.IsuListByID[key], &user, key)
 			if err != nil {
 				logger.AdminLogger.Panicln(fmt.Errorf("初期データから User インスタンスを作成するのに失敗しました: %v", err))
 			}
diff --git a/bench/scenario/load.go b/bench/scenario/load.go
index 4274f5e0d..fdefc5218 100644
--- a/bench/scenario/load.go
+++ b/bench/scenario/load.go
@@ -78,6 +78,13 @@ func (s *Scenario) Load(parent context.Context, step *isucandar.BenchmarkStep) e
 		s.userAdder(ctx, step)
 	}()
 
+	//不正なcondition
+	s.loadWaitGroup.Add(1)
+	go func() {
+		defer s.loadWaitGroup.Done()
+		s.keepPostingError(ctx)
+	}()
+
 	<-ctx.Done()
 	s.JiaPosterCancel()
 	logger.AdminLogger.Println("LOAD WAIT")
diff --git a/bench/scenario/posting.go b/bench/scenario/posting.go
index ba56e1c68..e67151d2f 100644
--- a/bench/scenario/posting.go
+++ b/bench/scenario/posting.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"net/url"
 	"path"
+	"sync"
 	"time"
 
 	"github.com/isucon/isucon11-qualify/bench/model"
@@ -16,9 +17,15 @@ import (
 
 const (
 	// MEMO: 最大でも60秒に一件しか送れないので点数上限になるが、解決できるとは思えないので良い
-	PostIntervalSecond     = 60 //Virtual Timeでのpost間隔
-	PostIntervalBlurSecond = 5  //Virtual Timeでのpost間隔のブレ幅(+-PostIntervalBlurSecond)
-	PostContentNum         = 10 //一回のpostで何要素postするか virtualTimeMulti * timerDuration(20ms) / PostIntervalSecond
+	PostIntervalSecond     = 60                     //Virtual Timeでのpost間隔
+	PostIntervalBlurSecond = 5                      //Virtual Timeでのpost間隔のブレ幅(+-PostIntervalBlurSecond)
+	PostContentNum         = 10                     //一回のpostで何要素postするか virtualTimeMulti * timerDuration(20ms) / PostIntervalSecond
+	postConditionTimeout   = 100 * time.Millisecond //MEMO: timeout は気にせずにズバズバ投げる
+)
+
+var (
+	targetBaseURLMapMutex sync.Mutex
+	targetBaseURLMap      = map[string]string{}
 )
 
 func init() {
@@ -42,7 +49,10 @@ type badCondition struct {
 
 //POST /api/condition/{jia_isu_id}をたたく Goroutine
 func (s *Scenario) keepPosting(ctx context.Context, targetBaseURL *url.URL, fqdn string, isu *model.Isu, scenarioChan *model.StreamsForPoster) {
-	postConditionTimeout := 100 * time.Millisecond //MEMO: timeout は気にせずにズバズバ投げる
+
+	targetBaseURLMapMutex.Lock()
+	targetBaseURLMap[targetBaseURL.String()] = fqdn
+	targetBaseURLMapMutex.Unlock()
 
 	targetBaseURL.Path = path.Join(targetBaseURL.Path, "/api/condition/", isu.JIAIsuUUID)
 	nowTimeStamp := s.ToVirtualTime(time.Now()).Unix()
@@ -260,3 +270,125 @@ func calcConditionLevel(condition model.IsuCondition) model.ConditionLevel {
 		return model.ConditionLevelCritical
 	}
 }
+
+//invalid
+
+//ランダムなISUにconditionを投げる
+func (s *Scenario) keepPostingError(ctx context.Context) {
+	nowTimeStamp := s.ToVirtualTime(time.Now()).Unix()
+	state := posterState{
+		// lastConditionTimestamp: 0,
+		lastConditionTimestamp: nowTimeStamp,
+		dirty:                  badCondition{0, false},
+		overWeight:             badCondition{0, false},
+		broken:                 badCondition{0, false},
+		isSitting:              false,
+	}
+	randEngine := rand.New(rand.NewSource(rand.Int63()))
+	httpClient := http.Client{}
+	httpClient.Timeout = postConditionTimeout
+	httpClient.Transport = &http.Transport{
+		TLSClientConfig:   &tls.Config{},
+		ForceAttemptHTTP2: true,
+	}
+
+	timer := time.NewTicker(1000 * time.Millisecond)
+	defer timer.Stop()
+	count := 0
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-timer.C:
+		}
+		count++
+
+		nowTimeStamp = s.ToVirtualTime(time.Now()).Unix()
+
+		//ISUを選ぶ
+		isu := s.GetRandomActivatedIsu(randEngine)
+		if isu == nil {
+			continue
+		}
+
+		//状態変化
+		stateChange := model.IsuStateChangeNone
+		if count%5 == 0 {
+			stateChange = model.IsuStateChangeClear | model.IsuStateChangeDetectOverweight | model.IsuStateChangeRepair
+		}
+
+		// 今の時間から最後のconditionの時間を引く
+		diffTimestamp := nowTimeStamp - state.lastConditionTimestamp
+		// その間に何個のconditionがあったか
+		diffConditionCount := int((diffTimestamp + PostIntervalSecond - 1) / PostIntervalSecond)
+
+		var reqLength int
+		if diffConditionCount > PostContentNum {
+			reqLength = PostContentNum
+		} else {
+			reqLength = diffConditionCount
+		}
+		conditionsReq := make([]service.PostIsuConditionRequest, 0, reqLength)
+
+		for i := 0; i < diffConditionCount; i++ {
+			// 次のstateを生成
+			state.UpdateToNextState(randEngine, stateChange)
+
+			if i+PostContentNum-diffConditionCount < 0 {
+				continue
+			}
+
+			// 作った新しいstateに基づいてconditionを生成
+			condition := state.GetNewestCondition(randEngine, stateChange, isu)
+			stateChange = model.IsuStateChangeNone //TODO: stateの適用タイミングをちゃんと考える
+
+			data := service.PostIsuConditionRequest{
+				IsSitting: condition.IsSitting,
+				Condition: condition.ConditionString(),
+				Message:   condition.Message,
+				Timestamp: condition.TimestampUnix,
+			}
+
+			//Conditionのフォーマットを崩す
+			index := randEngine.Intn(len(data.Condition) - 1)
+			data.Condition = data.Condition[:index] +
+				string((data.Condition[index]-'a'+byte(randEngine.Intn(26)))%26+'a') +
+				data.Condition[index+1:]
+
+			//リクエスト
+			conditionsReq = append(conditionsReq, data)
+		}
+
+		if len(conditionsReq) == 0 {
+			continue
+		}
+
+		//必ず一つは間違っているようにする
+		if randEngine.Intn(2) == 0 {
+			conditionsReq[len(conditionsReq)/2].Condition = "is_dirty=true,is_overweight=true,is_brokan=false"
+		} else {
+			conditionsReq[len(conditionsReq)/2].Condition = "is_dirty:true,is_overweight:true,is_broken:false"
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		default:
+		}
+
+		// targetを取得
+		var targetPath string
+		var targetServer string
+		targetBaseURLMapMutex.Lock()
+		for pathTmp, serverTmp := range targetBaseURLMap {
+			targetPath = pathTmp
+			targetServer = serverTmp
+			break
+		}
+		targetBaseURLMapMutex.Unlock()
+		targetPath = targetPath + "/api/condition/" + isu.JIAIsuUUID
+		httpClient.Transport.(*http.Transport).TLSClientConfig.ServerName = targetServer
+		// timeout も無視するので全てのエラーを見ない
+		postIsuConditionAction(ctx, httpClient, targetPath, &conditionsReq)
+	}
+}
diff --git a/bench/scenario/scenario.go b/bench/scenario/scenario.go
index 77e224bba..1c626ae3d 100644
--- a/bench/scenario/scenario.go
+++ b/bench/scenario/scenario.go
@@ -3,6 +3,7 @@ package scenario
 import (
 	"context"
 	"fmt"
+	"math/rand"
 	"net/http"
 	"net/url"
 	"sync"
@@ -318,3 +319,21 @@ func (s *Scenario) GetIsuFromID(id int) (*model.Isu, bool) {
 	isu, ok := s.isuFromID[id]
 	return isu, ok
 }
+
+func (s *Scenario) GetRandomActivatedIsu(randEngine *rand.Rand) *model.Isu {
+	targetCount := randEngine.Intn(len(s.isuFromID))
+	var isu *model.Isu
+
+	s.isuFromIDMutex.RLock()
+	defer s.isuFromIDMutex.RUnlock()
+	for _, isuP := range s.isuFromID {
+		if !isuP.IsNoPoster() {
+			isu = isuP
+		}
+		if targetCount <= 0 && isu != nil {
+			return isu
+		}
+		targetCount--
+	}
+	return isu
+}


Test patch: