Unverified Commit c6f055dc authored by Marty Schoch's avatar Marty Schoch Committed by GitHub
Browse files

Improve support for mutual matchmaking. (#719)

parent 922fe6fb
Loading
Loading
Loading
Loading
+61 −1
Original line number Diff line number Diff line
@@ -274,6 +274,15 @@ func (m *LocalMatchmaker) process(batch *index.Batch) {
				continue
			}

			outerMutualMatch, err := validateMatch(m.ctx, indexReader, hitIndex.Query, ticket)
			if err != nil {
				m.logger.Error("error validating mutual match", zap.Error(err))
				continue
			} else if !outerMutualMatch {
				// this search hit is not a mutual match with the outer ticket
				continue
			}

			if index.MaxCount < hitIndex.MaxCount && hitIndex.Intervals <= m.config.GetMatchmaker().MaxIntervals {
				// This match would be less than the search hit's preferred max, and they can still wait. Let them wait more.
				continue
@@ -300,6 +309,7 @@ func (m *LocalMatchmaker) process(batch *index.Batch) {

			var foundComboIdx int
			var foundCombo []*MatchmakerEntry
			var mutualMatchConflict bool
			for entryComboIdx, entryCombo := range entryCombos {
				if len(entryCombo)+len(entries)+index.Count <= index.MaxCount {
					// There is room in this combo for these entries. Check if there are session ID conflicts with current combo.
@@ -308,8 +318,34 @@ func (m *LocalMatchmaker) process(batch *index.Batch) {
							sessionIdConflict = true
							break
						}
						entryMatchesSearchHitQuery, err := validateMatch(m.ctx, indexReader, hitIndex.Query, entry.Ticket)
						if err != nil {
							mutualMatchConflict = true
							m.logger.Error("error validating mutual match", zap.Error(err))
							break
						} else if !entryMatchesSearchHitQuery {
							mutualMatchConflict = true
							// this search hit is not a mutual match with the outer ticket
							break
						}
					if sessionIdConflict {
						// MatchmakerEntry's do not have the query, have to dig it back out of indexes
						if entriesIndexEntry, ok := m.indexes[entry.Ticket]; ok {
							searchHitMatchesEntryQuery, err := validateMatch(m.ctx, indexReader, entriesIndexEntry.Query, hit.ID)
							if err != nil {
								mutualMatchConflict = true
								m.logger.Error("error validating mutual match", zap.Error(err))
								break
							} else if !searchHitMatchesEntryQuery {
								mutualMatchConflict = true
								// this search hit is not a mutual match with the outer ticket
								break
							}
						} else {
							m.logger.Warn("matchmaker missing index entry for entry combo")
						}

					}
					if sessionIdConflict || mutualMatchConflict {
						continue
					}

@@ -795,3 +831,27 @@ func MapMatchmakerIndex(id string, in *MatchmakerIndex) (*bluge.Document, error)

	return rv, nil
}

func validateMatch(ctx context.Context, r *bluge.Reader, queryStr string, ticket string) (bool, error) {
	ticketQuery, err := ParseQueryString(queryStr)
	if err != nil {
		return false, err
	}

	idQuery := bluge.NewTermQuery(ticket).SetField("_id")

	topQuery := bluge.NewBooleanQuery()
	topQuery.AddMust(ticketQuery, idQuery)

	req := bluge.NewTopNSearch(0, topQuery).WithStandardAggregations()
	dmi, err := r.Search(ctx, req)
	if err != nil {
		return false, err
	}

	if dmi.Aggregations().Count() != 1 {
		return false, nil
	}

	return true, nil
}
+270 −1
Original line number Diff line number Diff line
@@ -495,7 +495,7 @@ func TestMatchmakerAddWithMatchOnRangeAndValue(t *testing.T) {
			"c2": "foo",
		},
		map[string]float64{
			"c1": 15,
			"b1": 15,
		})
	if err != nil {
		t.Fatalf("error matchmaker add: %v", err)
@@ -1360,6 +1360,275 @@ func createTestMatchmaker(t fatalable, logger *zap.Logger,
	}, nil
}

// should add to matchmaker and NOT match due to not having mutual matching queries/properties
// ticktet 2 satisfies what ticket 1 is looking for
// but ticket 1 does NOT satisfy what ticket 2 is looking for
// this should prevent a match from being made
func TestMatchmakerRequireMutualMatch(t *testing.T) {
	consoleLogger := loggerForTest(t)
	matchesSeen := make(map[string]*rtapi.MatchmakerMatched)
	matchMaker, cleanup, err := createTestMatchmaker(t, consoleLogger,
		func(presences []*PresenceID, envelope *rtapi.Envelope) {
			if len(presences) == 1 {
				matchesSeen[presences[0].SessionID.String()] = envelope.GetMatchmakerMatched()
			}
		})
	if err != nil {
		t.Fatalf("error creating test matchmaker: %v", err)
	}
	defer cleanup()

	sessionID, _ := uuid.NewV4()
	ticket1, err := matchMaker.Add([]*MatchmakerPresence{
		{
			UserId:    "a",
			SessionId: "a",
			Username:  "a",
			Node:      "a",
			SessionID: sessionID,
		},
	}, sessionID.String(), "",
		"+properties.b1:>=10 +properties.b1:<=20",
		2, 2, map[string]string{},
		map[string]float64{
			"b1": 5,
		})
	if err != nil {
		t.Fatalf("error matchmaker add: %v", err)
	}

	sessionID2, _ := uuid.NewV4()
	ticket2, err := matchMaker.Add([]*MatchmakerPresence{
		&MatchmakerPresence{
			UserId:    "b",
			SessionId: "b",
			Username:  "b",
			Node:      "b",
			SessionID: sessionID2,
		},
	}, sessionID2.String(), "",
		"+properties.b1:>=10 +properties.b1:<=20",
		2, 2, map[string]string{},
		map[string]float64{
			"b1": 15,
		})
	if err != nil {
		t.Fatalf("error matchmaker add: %v", err)
	}
	if ticket1 == "" {
		t.Fatal("expected non-empty ticket1")
	}
	if ticket2 == "" {
		t.Fatal("expected non-empty ticket2")
	}

	matchMaker.process(bluge.NewBatch())

	if len(matchesSeen) > 0 {
		t.Fatalf("expected no matches, got %#v", matchesSeen)
	}
}

// TestMatchmakerRequireMutualMatchLarger attempts to validate
// mutual matchmaking of a larger size (3)
//
// The data is carefully arranged as follows:
//
// items B and C are given non-mutually matching data
// this means if the outer-loop ever chooses to start with B or C,
// we will fail to find a match due to mutual matching making
// ensuring we do not reach the desired size (3)
// this is not the purpose of the test, but relevant to the asserted behavior
//
// in the event item A is chosen in the outer-loop, we have designed
// the boost clauses to ensure that B comes before C in the results
// B does mutually match with A, allowing us to proceed populating the entryCombos
// however, C's query does not match B, and strict mutual matching should
// prevent this match being made
func TestMatchmakerRequireMutualMatchLarger(t *testing.T) {
	consoleLogger := loggerForTest(t)
	matchesSeen := make(map[string]*rtapi.MatchmakerMatched)
	matchMaker, cleanup, err := createTestMatchmaker(t, consoleLogger,
		func(presences []*PresenceID, envelope *rtapi.Envelope) {
			if len(presences) == 1 {
				matchesSeen[presences[0].SessionID.String()] = envelope.GetMatchmakerMatched()
			}
		})
	if err != nil {
		t.Fatalf("error creating test matchmaker: %v", err)
	}
	defer cleanup()

	sessionID, _ := uuid.NewV4()
	_, err = matchMaker.Add([]*MatchmakerPresence{
		{
			UserId:    "a",
			SessionId: "a",
			Username:  "a",
			Node:      "a",
			SessionID: sessionID,
		},
	}, sessionID.String(), "",
		"+properties.foo:bar properties.b1:10^10",
		3, 3, map[string]string{
			"foo": "bar",
		},
		map[string]float64{
			"b1": 5,
		})
	if err != nil {
		t.Fatalf("error matchmaker add: %v", err)
	}

	sessionID2, _ := uuid.NewV4()
	_, err = matchMaker.Add([]*MatchmakerPresence{
		&MatchmakerPresence{
			UserId:    "b",
			SessionId: "b",
			Username:  "b",
			Node:      "b",
			SessionID: sessionID2,
		},
	}, sessionID2.String(), "",
		"+properties.foo:bar properties.b1:20^10",
		3, 3, map[string]string{
			"foo": "bar",
		},
		map[string]float64{
			"b1": 10,
		})
	if err != nil {
		t.Fatalf("error matchmaker add: %v", err)
	}

	sessionID3, _ := uuid.NewV4()
	_, err = matchMaker.Add([]*MatchmakerPresence{
		&MatchmakerPresence{
			UserId:    "c",
			SessionId: "c",
			Username:  "c",
			Node:      "c",
			SessionID: sessionID3,
		},
	}, sessionID3.String(), "",
		"+properties.foo:bar +properties.b1:<10",
		3, 3, map[string]string{
			"foo": "bar",
		},
		map[string]float64{
			"b1": 20,
		})
	if err != nil {
		t.Fatalf("error matchmaker add: %v", err)
	}

	matchMaker.process(bluge.NewBatch())

	if len(matchesSeen) > 0 {
		t.Fatalf("expected no matches, got %#v", matchesSeen)
	}
}

// TestMatchmakerRequireMutualMatchLargerReversed attempts to validate
// mutual matchmaking of a larger size (3)
//
// The data is carefully arranged as follows:
//
// items B and C are given non-mutually matching data
// this means if the outer-loop ever chooses to start with B or C,
// we will fail to find a match due to mutual matching making
// ensuring we do not reach the desired size (3)
// this is not the purpose of the test, but relevant to the asserted behavior
//
// in the event item A is chosen in the outer-loop, we have designed
// the boost clauses to ensure that B comes before C in the results
// B does mutually match with A, allowing us to proceed populating the entryCombos
// however, B's query does not match C, and strict mutual matching should
// prevent this match being made
func TestMatchmakerRequireMutualMatchLargerReversed(t *testing.T) {
	consoleLogger := loggerForTest(t)
	matchesSeen := make(map[string]*rtapi.MatchmakerMatched)
	matchMaker, cleanup, err := createTestMatchmaker(t, consoleLogger,
		func(presences []*PresenceID, envelope *rtapi.Envelope) {
			if len(presences) == 1 {
				matchesSeen[presences[0].SessionID.String()] = envelope.GetMatchmakerMatched()
			}
		})
	if err != nil {
		t.Fatalf("error creating test matchmaker: %v", err)
	}
	defer cleanup()

	sessionID, _ := uuid.NewV4()
	_, err = matchMaker.Add([]*MatchmakerPresence{
		{
			UserId:    "a",
			SessionId: "a",
			Username:  "a",
			Node:      "a",
			SessionID: sessionID,
		},
	}, sessionID.String(), "",
		"+properties.foo:bar properties.b1:10^10",
		3, 3, map[string]string{
			"foo": "bar",
		},
		map[string]float64{
			"b1": 5,
		})
	if err != nil {
		t.Fatalf("error matchmaker add: %v", err)
	}

	sessionID2, _ := uuid.NewV4()
	_, err = matchMaker.Add([]*MatchmakerPresence{
		&MatchmakerPresence{
			UserId:    "b",
			SessionId: "b",
			Username:  "b",
			Node:      "b",
			SessionID: sessionID2,
		},
	}, sessionID2.String(), "",
		"+properties.foo:bar +properties.b1:<10 properties.b1:20^10",
		3, 3, map[string]string{
			"foo": "bar",
		},
		map[string]float64{
			"b1": 10,
		})
	if err != nil {
		t.Fatalf("error matchmaker add: %v", err)
	}

	sessionID3, _ := uuid.NewV4()
	_, err = matchMaker.Add([]*MatchmakerPresence{
		&MatchmakerPresence{
			UserId:    "c",
			SessionId: "c",
			Username:  "c",
			Node:      "c",
			SessionID: sessionID3,
		},
	}, sessionID3.String(), "",
		"+properties.foo:bar",
		3, 3, map[string]string{
			"foo": "bar",
		},
		map[string]float64{
			"b1": 20,
		})
	if err != nil {
		t.Fatalf("error matchmaker add: %v", err)
	}

	matchMaker.process(bluge.NewBatch())

	if len(matchesSeen) > 0 {
		t.Fatalf("expected no matches, got %#v", matchesSeen)
	}
}

func isModeAuthoritative(props map[string]interface{}) bool {
	if mode, ok := props["mode"]; ok {
		if modeStr, ok := mode.(string); ok {