Skip to content

Commit

Permalink
Merge pull request #719 from isucon/validate-user-notification
Browse files Browse the repository at this point in the history
ユーザーのEnroute通知の内容をバリデーションするように
  • Loading branch information
wtks authored Dec 7, 2024
2 parents f471035 + b9f72a3 commit d8025ca
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 12 deletions.
37 changes: 31 additions & 6 deletions bench/benchmarker/scenario/worldclient/worldclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,28 +441,53 @@ func (c *userClient) ConnectUserNotificationStream(ctx *world.Context, user *wor
if r.Data.Valid {
data := r.Data.V
var event world.NotificationEvent
userNotificationEvent := world.UserNotificationEvent{
Pickup: world.C(data.PickupCoordinate.Latitude, data.PickupCoordinate.Longitude),
Destination: world.C(data.DestinationCoordinate.Latitude, data.DestinationCoordinate.Longitude),
Fare: data.Fare,
Chair: nil,
}
if data.Chair.Set {
userNotificationEvent.Chair = &world.UserNotificationEventChairPayload{
ID: data.Chair.Value.ID,
Name: data.Chair.Value.Name,
Model: data.Chair.Value.Model,
Stats: world.UserNotificationEventChairStatsPayload{
TotalRidesCount: data.Chair.Value.Stats.TotalRidesCount,
TotalEvaluationAvg: data.Chair.Value.Stats.TotalEvaluationAvg,
},
}
}
switch data.Status {
case api.RideStatusMATCHING:
// event = &world.UserNotificationEventMatching{}
event = &world.UserNotificationEventMatching{
ServerRequestID: data.RideID,
UserNotificationEvent: userNotificationEvent,
}
case api.RideStatusENROUTE:
event = &world.UserNotificationEventDispatching{
ServerRequestID: data.RideID,
ServerRequestID: data.RideID,
UserNotificationEvent: userNotificationEvent,
}
case api.RideStatusPICKUP:
event = &world.UserNotificationEventDispatched{
ServerRequestID: data.RideID,
ServerRequestID: data.RideID,
UserNotificationEvent: userNotificationEvent,
}
case api.RideStatusCARRYING:
event = &world.UserNotificationEventCarrying{
ServerRequestID: data.RideID,
ServerRequestID: data.RideID,
UserNotificationEvent: userNotificationEvent,
}
case api.RideStatusARRIVED:
event = &world.UserNotificationEventArrived{
ServerRequestID: data.RideID,
ServerRequestID: data.RideID,
UserNotificationEvent: userNotificationEvent,
}
case api.RideStatusCOMPLETED:
event = &world.UserNotificationEventCompleted{
ServerRequestID: data.RideID,
ServerRequestID: data.RideID,
UserNotificationEvent: userNotificationEvent,
}
}
if event == nil {
Expand Down
3 changes: 3 additions & 0 deletions bench/benchmarker/world/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ const (
ErrorCodeLackOfNearbyChairs
// ErrorCodeMatchingTimeout マッチングに時間がかかりすぎです
ErrorCodeMatchingTimeout
// ErrorCodeUserReceivedDataIsWrong ユーザーが通知から受け取ったデータが想定と異なります
ErrorCodeUserReceivedDataIsWrong
)

var CriticalErrorCodes = map[ErrorCode]bool{
Expand Down Expand Up @@ -117,6 +119,7 @@ var ErrorTexts = map[ErrorCode]string{
ErrorCodeWrongNearbyChairs: "取得した付近の椅子情報に不備があります",
ErrorCodeLackOfNearbyChairs: "付近の椅子情報が想定よりも3つ以上足りていません",
ErrorCodeMatchingTimeout: "ライドが長時間マッチングされませんでした",
ErrorCodeUserReceivedDataIsWrong: "ユーザーが受け取った通知の内容が想定と異なります",
}

type codeError struct {
Expand Down
31 changes: 31 additions & 0 deletions bench/benchmarker/world/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,63 @@ type ChairNotificationEventUserPayload struct {
Name string
}

type UserNotificationEventMatching struct {
ServerRequestID string
UserNotificationEvent

unimplementedNotificationEvent
}

type UserNotificationEventDispatching struct {
ServerRequestID string
UserNotificationEvent

unimplementedNotificationEvent
}

type UserNotificationEventDispatched struct {
ServerRequestID string
UserNotificationEvent

unimplementedNotificationEvent
}

type UserNotificationEventCarrying struct {
ServerRequestID string
UserNotificationEvent

unimplementedNotificationEvent
}

type UserNotificationEventArrived struct {
ServerRequestID string
UserNotificationEvent

unimplementedNotificationEvent
}

type UserNotificationEventCompleted struct {
ServerRequestID string
UserNotificationEvent

unimplementedNotificationEvent
}

type UserNotificationEvent struct {
Pickup Coordinate
Destination Coordinate
Fare int
Chair *UserNotificationEventChairPayload
}

type UserNotificationEventChairPayload struct {
ID string
Name string
Model string
Stats UserNotificationEventChairStatsPayload
}

type UserNotificationEventChairStatsPayload struct {
TotalRidesCount int
TotalEvaluationAvg float64
}
130 changes: 124 additions & 6 deletions bench/benchmarker/world/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"log/slog"
"math"
"math/rand/v2"
"slices"
"sync"
Expand Down Expand Up @@ -63,6 +64,8 @@ type User struct {
notificationConn NotificationStream
// notificationQueue 通知キュー。毎Tickで最初に処理される
notificationQueue chan NotificationEvent
// validatedRideNotificationEvent 最新のバリデーション済みの通知イベント情報
validatedRideNotificationEvent *UserNotificationEvent
}

type RegisteredUserData struct {
Expand Down Expand Up @@ -364,7 +367,7 @@ func (u *User) CreateRequest(ctx *Context) error {
return nil
}

func (u *User) ChangeRequestStatus(status RequestStatus, serverRequestID string) error {
func (u *User) ChangeRequestStatus(status RequestStatus, serverRequestID string, validator func() error) error {
request := u.Request
if request == nil {
if status == RequestStatusCompleted {
Expand Down Expand Up @@ -401,37 +404,152 @@ func (u *User) ChangeRequestStatus(status RequestStatus, serverRequestID string)
return WrapCodeError(ErrorCodeUnexpectedUserRequestStatusTransitionOccurred, fmt.Errorf("ride_id: %v, expect: %v, got: %v (current: %v)", request.ServerID, request.Statuses.Desired, status, request.Statuses.User))
}
}

if validator != nil {
if err := validator(); err != nil {
return WrapCodeError(ErrorCodeUserReceivedDataIsWrong, err)
}
}

request.Statuses.User = status
return nil
}

func (u *User) HandleNotification(event NotificationEvent) error {
switch data := event.(type) {
case *UserNotificationEventDispatching:
err := u.ChangeRequestStatus(RequestStatusDispatching, data.ServerRequestID)
err := u.ChangeRequestStatus(RequestStatusDispatching, data.ServerRequestID, func() error {
if err := u.ValidateNotificationEvent(data.ServerRequestID, data.UserNotificationEvent); err != nil {
return err
}
u.validatedRideNotificationEvent = &data.UserNotificationEvent
return nil
})
if err != nil {
return err
}
case *UserNotificationEventDispatched:
err := u.ChangeRequestStatus(RequestStatusDispatched, data.ServerRequestID)
err := u.ChangeRequestStatus(RequestStatusDispatched, data.ServerRequestID, nil)
if err != nil {
return err
}
case *UserNotificationEventCarrying:
err := u.ChangeRequestStatus(RequestStatusCarrying, data.ServerRequestID)
err := u.ChangeRequestStatus(RequestStatusCarrying, data.ServerRequestID, nil)
if err != nil {
return err
}
case *UserNotificationEventArrived:
err := u.ChangeRequestStatus(RequestStatusArrived, data.ServerRequestID)
err := u.ChangeRequestStatus(RequestStatusArrived, data.ServerRequestID, nil)
if err != nil {
return err
}
case *UserNotificationEventCompleted:
err := u.ChangeRequestStatus(RequestStatusCompleted, data.ServerRequestID)
err := u.ChangeRequestStatus(RequestStatusCompleted, data.ServerRequestID, nil)
if err != nil {
return err
}
}
return nil
}

func (u *User) ValidateNotificationEvent(rideID string, serverSide UserNotificationEvent) error {
if !serverSide.Pickup.Equals(u.Request.PickupPoint) {
return fmt.Errorf("配車位置が一致しません。(ride_id: %s, got: %s, want: %s)", rideID, serverSide.Pickup, u.Request.PickupPoint)
}
if !serverSide.Destination.Equals(u.Request.DestinationPoint) {
return fmt.Errorf("目的地が一致しません。(ride_id: %s, got: %s, want: %s)", rideID, serverSide.Destination, u.Request.DestinationPoint)
}

if serverSide.Fare != u.Request.Fare() {
return fmt.Errorf("運賃が一致しません。(ride_id: %s, got: %d, want: %d)", rideID, serverSide.Fare, u.Request.Fare())
}

if serverSide.Chair == nil {
return fmt.Errorf("椅子情報がありません。(ride_id: %s)", rideID)
}

serverSideChair := serverSide.Chair
chair := u.World.ChairDB.GetByServerID(serverSideChair.ID)
if chair == nil {
return fmt.Errorf("想定していない椅子が返却されました。(ride_id: %s, chair_id: %s)", rideID, serverSide.Chair.ID)
}

if serverSideChair.Name != chair.RegisteredData.Name {
return fmt.Errorf("椅子の名前が一致しません。(ride_id: %s, chair_id: %s, got: %s, want: %s)", rideID, serverSide.Chair.ID, serverSide.Chair.Name, u.Request.Chair.RegisteredData.Name)
}
if serverSideChair.Model != chair.Model.Name {
return fmt.Errorf("椅子のモデルが一致しません。(ride_id: %s, chair_id: %s, got: %s, want: %s)", rideID, serverSide.Chair.ID, serverSide.Chair.Model, u.Request.Chair.Model.Name)
}

totalRideCount := 0
totalEvaluation := 0
for _, r := range chair.RequestHistory.Iter() {
if r.Evaluated.Load() {
totalRideCount++
totalEvaluation += r.CalculateEvaluation().Score()
}
}

if serverSideChair.Stats.TotalRidesCount != totalRideCount {
return fmt.Errorf("椅子の総乗車回数が一致しません。(ride_id: %s, chair_id: %s, got: %d, want: %d)", rideID, serverSide.Chair.ID, serverSide.Chair.Stats.TotalRidesCount, totalRideCount)
}
if totalRideCount > 0 {
if !almostEqual(serverSideChair.Stats.TotalEvaluationAvg, float64(totalEvaluation)/float64(totalRideCount), 0.01) {
return fmt.Errorf("椅子の評価の平均が一致しません。(ride_id: %s, chair_id: %s, got: %f, want: %f)", rideID, serverSide.Chair.ID, serverSide.Chair.Stats.TotalEvaluationAvg, float64(totalEvaluation)/float64(totalRideCount))
}
} else {
if serverSideChair.Stats.TotalEvaluationAvg != 0 {
return fmt.Errorf("椅子の評価の平均が一致しません。(ride_id: %s, chair_id: %s, got: %f, want: %f)", rideID, serverSide.Chair.ID, serverSide.Chair.Stats.TotalEvaluationAvg, 0.0)
}
}

return nil
}

// compareUserNotificationEvent validation済みのUserNotificationEventと比較して、一致しない場合はエラーを返す
func compareUserNotificationEvent(rideID string, old, new UserNotificationEvent) error {
if !new.Pickup.Equals(old.Pickup) {
return fmt.Errorf("配車位置が一致しません。(ride_id: %s, got: %s, want: %s)", rideID, new.Pickup, old.Pickup)
}
if !new.Destination.Equals(old.Destination) {
return fmt.Errorf("目的地が一致しません。(ride_id: %s, got: %s, want: %s)", rideID, new.Destination, old.Destination)
}

if new.Fare != old.Fare {
return fmt.Errorf("運賃が一致しません。(ride_id: %s, got: %d, want: %d)", rideID, new.Fare, old.Fare)
}

if new.Chair == nil {
return fmt.Errorf("椅子情報がありません。(ride_id: %s)", rideID)
}

if new.Chair.ID != old.Chair.ID {
return fmt.Errorf("椅子のIDが一致しません。(ride_id: %s, got: %s, want: %s)", rideID, new.Chair.ID, old.Chair.ID)
}
if new.Chair.Name != old.Chair.Name {
return fmt.Errorf("椅子の名前が一致しません。(ride_id: %s, chair_id: %s, got: %s, want: %s)", rideID, new.Chair.ID, new.Chair.Name, old.Chair.Name)
}
if new.Chair.Model != old.Chair.Model {
return fmt.Errorf("椅子のモデルが一致しません。(ride_id: %s, chair_id: %s, got: %s, want: %s)", rideID, new.Chair.ID, new.Chair.Model, old.Chair.Model)
}

if new.Chair.Stats.TotalRidesCount != old.Chair.Stats.TotalRidesCount {
return fmt.Errorf("椅子の総乗車回数が一致しません。(ride_id: %s, chair_id: %s, got: %d, want: %d)", rideID, new.Chair.ID, new.Chair.Stats.TotalRidesCount, old.Chair.Stats.TotalRidesCount)
}
if new.Chair.Stats.TotalEvaluationAvg != old.Chair.Stats.TotalEvaluationAvg {
return fmt.Errorf("椅子の評価の平均が一致しません。(ride_id: %s, chair_id: %s, got: %f, want: %f)", rideID, new.Chair.ID, new.Chair.Stats.TotalEvaluationAvg, old.Chair.Stats.TotalEvaluationAvg)
}

return nil
}

func almostEqual(a, b, epsilon float64) bool {
// 絶対誤差と相対誤差を組み合わせて比較
absDiff := math.Abs(a - b)
if absDiff <= epsilon {
return true
}
// 相対誤差の比較
maxAbs := math.Max(math.Abs(a), math.Abs(b))
return absDiff <= epsilon*maxAbs
}

0 comments on commit d8025ca

Please sign in to comment.