Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Revamp matching service #39

Merged
merged 7 commits into from
Oct 18, 2024
Merged

feat: Revamp matching service #39

merged 7 commits into from
Oct 18, 2024

Conversation

bensohh
Copy link

@bensohh bensohh commented Oct 18, 2024

Change the logic of matching service:

  • Users get added into a matchmaking queue
  • At the same time, we add their username into topic Sets in Redis and store their matching request information in a HashSet in Redis
  • Peek at front of the queue --> retrieve user info from hash --> Loop through topics set (based on user's specified topics) one by one and try to find the first match --> return match (if any) together with the common topic and difficulty --> trigger a notifyMatch function to send result to both websockets --> attempt to close both websockets context

Added new type match_rejected when a user within an existing queue tries to matchmake again @chiaryan @solomonng2001 can take note of this for frontend. Refer to README.md for more details.

Current Situation:

  • There is a bug where after the matched user gets informed, the matchCtx fails to cancel even though the cancelFunc() is called, resulting in retrieving of match_found then subsequently timeout caused by the timeoutCtx.
    @tituschewxj need your help to checkout my branch and apps/matching-service/handlers/websocket.go

After resolving the problem, we should be able to merge 👍

@bensohh bensohh requested a review from tituschewxj October 18, 2024 07:42
@bensohh bensohh self-assigned this Oct 18, 2024
@bensohh
Copy link
Author

bensohh commented Oct 18, 2024

Newest Update on PR

  • Fix error with matchmaking service

Detailed Explanation of Matchmaking
I utilise 3 Redis data structure in the matchmaking process:

  • A Redis Queue (to store users who wants to match, basically a matchmaking queue)
  • Multiple Redis Sets (these sets are identified by topics, for example if user1 has topics: ["Arrays", "Trees"], user1's username will be added into the Arrays set and Trees set. This is to allow us to identify potential matches and also guarantees uniqueness (properties of a set)
  • A Redis Hashset (to store users matching request, for example the json request sent via client side for easy retrieval in the future)

I also attempted to utilise mutex to ensure that each time there is only one process editing/accessing the Redis Data Structures. Since there can be multiple connections, I created several global maps for matchContext, matchChannel, websocket connections.

General Flow

  1. User client initiate websocket connection via a matchrequest
  2. Checks if username already in the live websocket connections, if it exist reject the connection, else continue
  3. User added into the matchmaking queue
  4. Loop through the topics specified and add user into the respective Redis Sets
  5. Store user MatchRequest in the Redis HashSet
  6. Only one MatchMaking algorithm runs in the background
  7. This repeats for all the users that initiate a connection

Matching Algorithm

  1. Peek at the front of the queue
  2. Retrieve user (at the front of the queue) info from Hashset
  3. Loop through its topics and find for first matching in the Topic Sets
  4. Find highest common difficulty else return min difficulty of the 2 matched users' difficulties array
  5. Notify both user of the match and result
  6. Cleanup, cancel relevant contexts and close connections
  7. If there are no matches after looping through all the topic sets, we pop the user and enqueue him to the back of the matchmaking queue again

Copy link

@tituschewxj tituschewxj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your changes. Overall, I think the matching works well for now, but there are some improvements we could make with it, such as considering the available questions while performing the matching.

Comment on lines 162 to 165
case <-matchCtx.Done():
log.Println("Match found for user HEREEE: " + username)
log.Println("Match found for user: " + username)
return
case result, ok := <-matchFoundChan:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably do not need the case for <-matchCtx.Done if we have the <-matchFoundChan that checks if matching is done

Comment on lines +149 to +158
// Remove the match context and active
if _, exists := matchContexts[username]; exists {
delete(matchContexts, username)
}
if _, exists := activeConnections[username]; exists {
delete(activeConnections, username)
}
if _, exists := matchFoundChannels[username]; exists {
delete(matchFoundChannels, username)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should abstract out the match context and active connections, and the redis cleanup, to a function. We don't have to call it here, but we can call the function after waitForResult, since it is blocking, which should clean up the code.

Comment on lines +22 to +31
// SetRedisClient sets the Redis client to a global variable
func SetRedisClient(client *redis.Client) {
redisClient = client
}

// Get redisclient
func GetRedisClient() *redis.Client {
return redisClient
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can move this to a new file under the utils folder

Comment on lines +32 to +46
func getPortNumber(addr string) (int64, error) {
// Split the string by the colon
parts := strings.Split(addr, ":")
if len(parts) < 2 {
return 0, fmt.Errorf("no port number found")
}

matchFoundChan <- result
// Convert the port string to an integer
port, err := strconv.ParseInt(parts[len(parts)-1], 10, 64)
if err != nil {
return 0, err // Return an error if conversion fails
}

return port, nil
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably can either remove this function as it is not called, or move to the utils folder if it has a use in future?

Comment on lines +49 to +52
// Acquire mutex
mu.Lock()
// Defer unlocking the mutex
defer mu.Unlock()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mechanism for ensuring mutually exclusive access to the matchmaking process might not work when there are multiple instances of the matching-service, which happens when we scale the number of matching services.

This works for now, but we probably want a database level mutex, using something like https://github.com/amyangfei/redlock-go.

var mutex sync.Mutex // Mutex for concurrency safety

// To simulate generating a random matchID for collaboration service (TODO: Future)
func GenerateMatchID() (string, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably can move this to utils?

Comment on lines +197 to +215
for _, topic := range user.Topics {
users, err := client.SMembers(ctx, strings.ToLower(topic)).Result()
if err != nil {
return "", "", "", err
}

for _, potentialMatch := range users {
if potentialMatch != username {
matchedUser, err := GetUserDetails(client, potentialMatch, ctx)
if err != nil {
return "", "", "", err
}

commonDifficulty := GetCommonDifficulty(user.Difficulties, matchedUser.Difficulties)
return potentialMatch, topic, commonDifficulty, nil
}
}
}

Copy link

@tituschewxj tituschewxj Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be a problem when both users have the topics Algorithms and Arrays, but it is always that Algorithms topic gets chosen, because it comes first in the user.Topics, so they would never match on Arrays?

Also, I realize that two users can still be matched with different topics, through a question that contains both topics from users, since a question can have more than one topic. We probably should update the matching algorithm based on this.

}

commonDifficulty := GetCommonDifficulty(user.Difficulties, matchedUser.Difficulties)
return potentialMatch, topic, commonDifficulty, nil

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to also check whether there is such a question with the difficulty and topic?

If two users match, but the matching topic and difficult pair doesn't exist, how should we handle it?

We probably want to return the matched question in this step also.

}

// Get the highest common difficulty between two users, if no common difficulty found, choose the min of the 2 arrays
func GetCommonDifficulty(userArr []string, matchedUserArr []string) string {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we can find a matching question before the matching the difficulty?

@tituschewxj tituschewxj merged commit fc0fdac into staging Oct 18, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants