open-match.dev/open-match@v1.8.1/examples/scale/scenarios/backfill/backfill.go (about)

     1  // Copyright 2020 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package backfill
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"time"
    21  
    22  	"google.golang.org/protobuf/types/known/anypb"
    23  	"google.golang.org/protobuf/types/known/wrapperspb"
    24  	"open-match.dev/open-match/pkg/pb"
    25  )
    26  
    27  const (
    28  	poolName     = "all"
    29  	openSlotsKey = "open-slots"
    30  )
    31  
    32  func Scenario() *BackfillScenario {
    33  	ticketsPerMatch := 4
    34  	return &BackfillScenario{
    35  		TicketsPerMatch:           ticketsPerMatch,
    36  		MaxTicketsPerNotFullMatch: 3,
    37  		BackfillDeleteCond: func(b *pb.Backfill) bool {
    38  			openSlots := getOpenSlots(b, ticketsPerMatch)
    39  			return openSlots <= 0
    40  		},
    41  	}
    42  }
    43  
    44  type BackfillScenario struct {
    45  	TicketsPerMatch           int
    46  	MaxTicketsPerNotFullMatch int
    47  	BackfillDeleteCond        func(*pb.Backfill) bool
    48  }
    49  
    50  func (s *BackfillScenario) Profiles() []*pb.MatchProfile {
    51  	return []*pb.MatchProfile{
    52  		{
    53  			Name: "entirePool",
    54  			Pools: []*pb.Pool{
    55  				{
    56  					Name: poolName,
    57  				},
    58  			},
    59  		},
    60  	}
    61  }
    62  
    63  func (s *BackfillScenario) Ticket() *pb.Ticket {
    64  	return &pb.Ticket{}
    65  }
    66  
    67  func (s *BackfillScenario) Backfill() *pb.Backfill {
    68  	return &pb.Backfill{}
    69  }
    70  
    71  func (s *BackfillScenario) MatchFunction(p *pb.MatchProfile, poolBackfills map[string][]*pb.Backfill, poolTickets map[string][]*pb.Ticket) ([]*pb.Match, error) {
    72  	return statefullMMF(p, poolBackfills, poolTickets, s.TicketsPerMatch, s.MaxTicketsPerNotFullMatch)
    73  }
    74  
    75  // statefullMMF is a MMF implementation which is used in scenario when we want MMF to create not full match and fill it later.
    76  // 1. The first FetchMatches is called
    77  // 2. MMF grabs maxTicketsPerNotFullMatch tickets and makes a match and new backfill for it
    78  // 3. MMF sets backfill's open slots to ticketsPerMatch - maxTicketsPerNotFullMatch
    79  // 4. MMF returns the match as a result
    80  // 5. The second FetchMatches is called
    81  // 6. MMF gets previously created backfill
    82  // 7. MMF gets backfill's open slots value
    83  // 8. MMF grabs openSlots tickets and makes a match with previously created backfill
    84  // 9. MMF sets backfill's open slots to 0
    85  // 10. MMF returns the match as a result
    86  func statefullMMF(p *pb.MatchProfile, poolBackfills map[string][]*pb.Backfill, poolTickets map[string][]*pb.Ticket, ticketsPerMatch int, maxTicketsPerNotFullMatch int) ([]*pb.Match, error) {
    87  	var matches []*pb.Match
    88  
    89  	for pool, backfills := range poolBackfills {
    90  		tickets, ok := poolTickets[pool]
    91  
    92  		if !ok || len(tickets) == 0 {
    93  			// no tickets in pool
    94  			continue
    95  		}
    96  
    97  		// process backfills first
    98  		for _, b := range backfills {
    99  			l := len(tickets)
   100  			if l == 0 {
   101  				// no tickets left
   102  				break
   103  			}
   104  
   105  			openSlots := getOpenSlots(b, ticketsPerMatch)
   106  			if openSlots <= 0 {
   107  				// no free open slots
   108  				continue
   109  			}
   110  
   111  			if l > openSlots {
   112  				l = openSlots
   113  			}
   114  
   115  			setOpenSlots(b, openSlots-l)
   116  			matches = append(matches, &pb.Match{
   117  				MatchId:       fmt.Sprintf("profile-%v-time-%v-%v", p.GetName(), time.Now().Format("2006-01-02T15:04:05.00"), len(matches)),
   118  				Tickets:       tickets[0:l],
   119  				MatchProfile:  p.GetName(),
   120  				MatchFunction: "backfill",
   121  				Backfill:      b,
   122  			})
   123  			tickets = tickets[l:]
   124  		}
   125  
   126  		// create not full matches with backfill
   127  		for {
   128  			l := len(tickets)
   129  			if l == 0 {
   130  				// no tickets left
   131  				break
   132  			}
   133  
   134  			if l > maxTicketsPerNotFullMatch {
   135  				l = maxTicketsPerNotFullMatch
   136  			}
   137  			b := pb.Backfill{}
   138  			setOpenSlots(&b, ticketsPerMatch-l)
   139  			matches = append(matches, &pb.Match{
   140  				MatchId:            fmt.Sprintf("profile-%v-time-%v-%v", p.GetName(), time.Now().Format("2006-01-02T15:04:05.00"), len(matches)),
   141  				Tickets:            tickets[0:l],
   142  				MatchProfile:       p.GetName(),
   143  				MatchFunction:      "backfill",
   144  				Backfill:           &b,
   145  				AllocateGameserver: true,
   146  			})
   147  			tickets = tickets[l:]
   148  		}
   149  	}
   150  
   151  	return matches, nil
   152  }
   153  
   154  func getOpenSlots(b *pb.Backfill, defaultVal int) int {
   155  	if b.Extensions == nil {
   156  		return defaultVal
   157  	}
   158  
   159  	any, ok := b.Extensions[openSlotsKey]
   160  	if !ok {
   161  		return defaultVal
   162  	}
   163  
   164  	var val wrapperspb.Int32Value
   165  	err := any.UnmarshalTo(&val)
   166  	if err != nil {
   167  		panic(err)
   168  	}
   169  
   170  	return int(val.Value)
   171  }
   172  
   173  func setOpenSlots(b *pb.Backfill, val int) {
   174  	if b.Extensions == nil {
   175  		b.Extensions = make(map[string]*anypb.Any)
   176  	}
   177  
   178  	any, err := anypb.New(&wrapperspb.Int32Value{Value: int32(val)})
   179  	if err != nil {
   180  		panic(err)
   181  	}
   182  
   183  	b.Extensions[openSlotsKey] = any
   184  }
   185  
   186  // statelessMMF is a MMF implementation which is used in scenario when we want MMF to fill backfills created by a Gameserver. It doesn't create
   187  // or update any backfill.
   188  // 1. FetchMatches is called
   189  // 2. MMF gets a backfill
   190  // 3. MMF grabs ticketsPerMatch tickets and makes a match with the backfill
   191  // 4. MMF returns the match as a result
   192  func statelessMMF(p *pb.MatchProfile, poolBackfills map[string][]*pb.Backfill, poolTickets map[string][]*pb.Ticket, ticketsPerMatch int) ([]*pb.Match, error) {
   193  	var matches []*pb.Match
   194  
   195  	for pool, backfills := range poolBackfills {
   196  		tickets, ok := poolTickets[pool]
   197  
   198  		if !ok || len(tickets) == 0 {
   199  			// no tickets in pool
   200  			continue
   201  		}
   202  
   203  		for _, b := range backfills {
   204  			l := len(tickets)
   205  			if l == 0 {
   206  				// no tickets left
   207  				break
   208  			}
   209  
   210  			if l > ticketsPerMatch && ticketsPerMatch > 0 {
   211  				l = ticketsPerMatch
   212  			}
   213  
   214  			matches = append(matches, &pb.Match{
   215  				MatchId:       fmt.Sprintf("profile-%v-time-%v-%v", p.GetName(), time.Now().Format("2006-01-02T15:04:05.00"), len(matches)),
   216  				Tickets:       tickets[0:l],
   217  				MatchProfile:  p.GetName(),
   218  				MatchFunction: "backfill",
   219  				Backfill:      b,
   220  			})
   221  			tickets = tickets[l:]
   222  		}
   223  	}
   224  
   225  	return matches, nil
   226  }
   227  
   228  func (s *BackfillScenario) Evaluate(stream pb.Evaluator_EvaluateServer) error {
   229  	tickets := map[string]struct{}{}
   230  	backfills := map[string]struct{}{}
   231  	matchIds := []string{}
   232  
   233  outer:
   234  	for {
   235  		req, err := stream.Recv()
   236  		if err == io.EOF {
   237  			break
   238  		}
   239  		if err != nil {
   240  			return fmt.Errorf("failed to read evaluator input stream: %w", err)
   241  		}
   242  
   243  		m := req.GetMatch()
   244  
   245  		if _, ok := backfills[m.Backfill.Id]; ok {
   246  			continue outer
   247  		}
   248  
   249  		for _, t := range m.Tickets {
   250  			if _, ok := tickets[t.Id]; ok {
   251  				continue outer
   252  			}
   253  		}
   254  
   255  		for _, t := range m.Tickets {
   256  			tickets[t.Id] = struct{}{}
   257  		}
   258  
   259  		matchIds = append(matchIds, m.GetMatchId())
   260  	}
   261  
   262  	for _, id := range matchIds {
   263  		err := stream.Send(&pb.EvaluateResponse{MatchId: id})
   264  		if err != nil {
   265  			return fmt.Errorf("failed to sending evaluator output stream: %w", err)
   266  		}
   267  	}
   268  
   269  	return nil
   270  }