-
Notifications
You must be signed in to change notification settings - Fork 189
/
Copy pathcampaign.go
281 lines (237 loc) · 6.94 KB
/
campaign.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package scenario
import (
"context"
"log"
"math/rand/v2"
"sync"
"sync/atomic"
"time"
"github.com/isucon/isucon9-qualify/bench/asset"
"github.com/isucon/isucon9-qualify/bench/fails"
"github.com/isucon/isucon9-qualify/bench/server"
"github.com/isucon/isucon9-qualify/bench/session"
"github.com/morikuni/failure"
)
func Campaign(ctx context.Context) {
var wg sync.WaitGroup
closed := make(chan struct{})
// buyer用のセッションを増やしておく
// 500ユーザーを追加したら止まる
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
L:
for j := 0; j < 50; j++ {
ch := time.After(100 * time.Millisecond)
user1 := asset.GetRandomBuyer()
s, err := loginedSession(ctx, user1)
if err != nil {
// ログインに失敗しまくるとプールに溜まらないので一気に購入できなくなる
// その場合は失敗件数が多いという理由で失格にする
fails.ErrorsForCheck.Add(err)
goto Final
}
BuyerPool.Enqueue(s)
Final:
select {
case <-ch:
case <-ctx.Done():
break L
}
}
}()
}
wg.Add(1)
go func() {
defer wg.Done()
// ログインユーザーがある程度溜まらないと実施できないので少し待つ
// 8s毎に実行されるので60sだと最大で5回実行される
<-time.After(13 * time.Second)
L:
for j := 0; j < (ExecutionSeconds-13)/8; j++ {
ch := time.After(8 * time.Second)
isIncrease := popularListing(ctx, 80+j*20, 1000+j*100)
if isIncrease {
// 商品単価を上げる
log.Print("=== succeed to popular listing ===")
priceStoreCache.Add(20)
// 次の人気者出品に備えてログインユーザーのpoolを増やしておく
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
L:
for j := 0; j < 20; j++ {
ch := time.After(100 * time.Millisecond)
user1 := asset.GetRandomBuyer()
s, err := loginedSession(ctx, user1)
if err != nil {
// ログインに失敗しまくるとプールに溜まらないので一気に購入できなくなる
// その場合は失敗件数が多いという理由で失格にする
fails.ErrorsForCheck.Add(err)
goto Final
}
BuyerPool.Enqueue(s)
Final:
select {
case <-ch:
case <-ctx.Done():
break L
}
}
}()
}
}
select {
case <-ch:
case <-ctx.Done():
break L
}
}
}()
go func() {
wg.Wait()
close(closed)
}()
select {
case <-closed:
case <-ctx.Done():
}
}
// popularListing is 人気者出品
// 人気者が高額の出品を行う。高額だが出品した瞬間に大量の人が購入しようとしてくる。もちろん購入できるのは一人だけ。
func popularListing(ctx context.Context, num int, price int) (isIncrease bool) {
// buyerが足りない場合はログインを意図的に遅くしている可能性があるのでペナルティとして実行しない
l := BuyerPool.Len()
if l < num+10 {
log.Printf("login user insufficient (count: %d)", l)
return false
}
// 真のbuyerが入るチャネル。複数来たらエラーにする
buyerCh := make(chan *session.Session, 1)
popular, err := buyerSession(ctx)
if err != nil {
fails.ErrorsForCheck.Add(err)
return false
}
// 人気者出品だけはだれが買うかわからないので、カテゴリ指定なし出品
targetItem, err := sell(ctx, popular, price)
if err != nil {
fails.ErrorsForCheck.Add(err)
return false
}
var wg sync.WaitGroup
var errCnt int32
for i := 0; i < num; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 10%のユーザーは決済に失敗する
// 全員が成功するなら適当に1ユーザーでロックを取って、他のユーザーはエラーを返すだけで良い
// 成功するかどうか分からなくしておけば、何人かはロックを取っておく必要が出る
cardNumber := ""
failed := false
if rand.IntN(10) == 0 {
failed = true
cardNumber = FailedCardNumber
} else {
failed = false
cardNumber = CorrectCardNumber
}
token := sPayment.ForceSet(cardNumber, targetItem.ID, price)
s2, err := buyerSession(ctx)
if err != nil {
fails.ErrorsForCheck.Add(err)
atomic.AddInt32(&errCnt, 1)
return
}
if failed {
err := s2.BuyWithFailedOnCampaign(ctx, targetItem.ID, token)
if err != nil {
fails.ErrorsForCheck.Add(err)
atomic.AddInt32(&errCnt, 1)
return
}
return
}
transactionEvidenceID, err := s2.BuyWithMayFail(ctx, targetItem.ID, token)
if err != nil {
fails.ErrorsForCheck.Add(err)
atomic.AddInt32(&errCnt, 1)
return
}
if transactionEvidenceID != 0 {
// 0でないなら真のbuyer
buyerCh <- s2
} else {
// buyerでないならもう使わないので戻す
BuyerPool.Enqueue(s2)
}
}()
}
closed := make(chan struct{})
go func() {
wg.Wait()
close(closed)
}()
var buyer *session.Session
select {
case buyer = <-buyerCh:
case <-closed:
// 全goroutineが終了したのにbuyerがいない場合は全員が購入に失敗している
fails.ErrorsForCheck.Add(failure.New(fails.ErrApplication, failure.Messagef("商品 (item_id: %d) に対して全ユーザーが購入に失敗しました", targetItem.ID)))
return false
}
defer func() {
// 終わったら戻しておく
BuyerPool.Enqueue(buyer)
}()
go func() {
L:
for {
select {
case s := <-buyerCh:
// buyerが複数人いるとここのコードが動く
fails.ErrorsForCheck.Add(failure.New(fails.ErrCritical, failure.Messagef("売り切れ商品 (item_id: %d) に対して他のユーザー (user_id: %d) が購入できています", targetItem.ID, s.UserID)))
case <-closed:
break L
}
}
}()
reserveID, apath, err := popular.Ship(ctx, targetItem.ID)
if err != nil {
fails.ErrorsForCheck.Add(err)
return false
}
md5Str, err := popular.DownloadQRURL(ctx, apath)
if err != nil {
fails.ErrorsForCheck.Add(err)
return false
}
sShipment.ForceSetStatus(reserveID, server.StatusShipping)
if !sShipment.CheckQRMD5(reserveID, md5Str) {
fails.ErrorsForCheck.Add(failure.New(fails.ErrApplication, failure.Messagef("QRコードの画像に誤りがあります (item_id: %d, reserve_id: %s)", targetItem.ID, reserveID)))
return false
}
err = shipDone(ctx, popular, targetItem.ID)
if err != nil {
fails.ErrorsForCheck.Add(err)
return false
}
ok := sShipment.ForceSetStatus(reserveID, server.StatusDone)
if !ok {
fails.ErrorsForCheck.Add(failure.New(fails.ErrApplication, failure.Messagef("集荷予約IDに誤りがあります (item_id: %d, reserve_id: %s)", targetItem.ID, reserveID)))
return false
}
err = complete(ctx, buyer, targetItem.ID)
if err != nil {
fails.ErrorsForCheck.Add(err)
return false
}
if atomic.LoadInt32(&errCnt) > 2 {
// エラーが一定数を超えていたら単価は上がらない
return false
}
return true
}