Loading server/matchmaker.go +61 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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 } Loading Loading @@ -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 } server/matchmaker_test.go +270 −1 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 { Loading Loading
server/matchmaker.go +61 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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 } Loading Loading @@ -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 }
server/matchmaker_test.go +270 −1 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 { Loading