Skip to content

Commit

Permalink
Merge pull request #50 from CS3219-AY2425S1/titus/question-matching
Browse files Browse the repository at this point in the history
feat: question matching
  • Loading branch information
solomonng2001 authored Oct 26, 2024
2 parents bb50c9b + e58e4af commit 0f16864
Show file tree
Hide file tree
Showing 38 changed files with 1,902 additions and 265 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
MATCHING_SERVICE_PORT: ${{ vars.MATCHING_SERVICE_PORT }}
MATCHING_SERVICE_TIMEOUT: ${{ vars.MATCHING_SERVICE_TIMEOUT }}
REDIS_URL: ${{ vars.REDIS_URL }}
QUESTION_SERVICE_GRPC_URL: ${{ vars.QUESTION_SERVICE_GPRC_URL }}
run: |
cd ./apps/frontend
echo "NEXT_PUBLIC_QUESTION_SERVICE_URL=$QUESTION_SERVICE_URL" >> .env
Expand All @@ -56,6 +57,7 @@ jobs:
echo "MATCH_TIMEOUT=$MATCHING_SERVICE_TIMEOUT" >> .env
echo "JWT_SECRET=$JWT_SECRET" >> .env
echo "REDIS_URL=$REDIS_URL" >> .env
echo "QUESTION_SERVICE_GRPC_URL=$QUESTION_SERVICE_GRPC_URL" >> .env
- name: Create Database Credential Files
env:
Expand Down
2 changes: 1 addition & 1 deletion apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Once running, you can access:

- The **frontend** at http://localhost:3000
- The **user service** at http://localhost:3001
- The **question service** at http://localhost:8080
- The **question service** at http://localhost:8080 (REST) and http://localhost:50051 (gRPC)
- The **matching service** at http://localhost:8081
- The **redis service** at http://localhost:6379

Expand Down
1 change: 1 addition & 0 deletions apps/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ services:
dockerfile: Dockerfile
ports:
- 8080:8080
- 50051:50051
env_file:
- ./question-service/.env
networks:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ const DifficultySelector: React.FC<DifficultySelectorProps> = ({ selectedDifficu
<Tag.CheckableTag
className={`difficulty-tag ${difficultyOption.value}-tag`}
key={difficultyOption.value}
checked={selectedDifficulties.includes(difficultyOption.label)}
onChange={() => handleChange(difficultyOption.label)}
checked={selectedDifficulties.includes(difficultyOption.value)}
onChange={() => handleChange(difficultyOption.value)}
>
{difficultyOption.label}
</Tag.CheckableTag>
Expand Down
41 changes: 14 additions & 27 deletions apps/frontend/src/app/services/use-matching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,15 @@ export type MatchRequestParams = {
}

export type MatchFoundResponse = {
type: "match_found",
matchId: number,
partnerId: number,
partnerName: string,
} | {
type: "match_found",
matchId: string,
type: "match_question_found",
match_id: string,
user: string,
matchedUser: string,
topic: string | string[],
difficulty: string
matched_user: string,
matched_topics: string[],
question_doc_ref_id: string,
question_name: string,
question_difficulty: string,
question_topics: string[],
}

export type MatchTimeoutResponse = {
Expand Down Expand Up @@ -61,7 +59,7 @@ export default function useMatching(): MatchState {
return;
}

if (responseJson.type == "match_found") {
if (responseJson.type == "match_question_found") {
setIsSocket(false);

const info: MatchInfo = parseInfoFromResponse(responseJson);
Expand Down Expand Up @@ -136,20 +134,9 @@ export default function useMatching(): MatchState {
}

function parseInfoFromResponse(responseJson: MatchFoundResponse): MatchInfo {
// test whether old or new
if ("partnerId" in responseJson) {
return {
matchId: responseJson.matchId?.toString() ?? "unknown",
partnerId: responseJson.partnerId?.toString() ?? "unknown",
partnerName: responseJson.partnerName ?? "unknown",
myName: "unknown",
};
} else {
return {
matchId: responseJson.matchId?.toString() ?? "unknown",
partnerId: "unknown",
partnerName: responseJson.matchedUser ?? "unknown",
myName: responseJson.user ?? "unknown",
};
}
return {
matchId: responseJson.match_id?.toString() ?? "unknown",
partnerName: responseJson.matched_user ?? "unknown",
myName: responseJson.user ?? "unknown",
};
}
1 change: 0 additions & 1 deletion apps/frontend/src/contexts/websocketcontext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export type SocketState = {
};
export type MatchInfo = {
matchId: string;
partnerId: string;
myName: string;
partnerName: string;
}
Expand Down
6 changes: 4 additions & 2 deletions apps/matching-service/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ PORT=8081
MATCH_TIMEOUT=30
JWT_SECRET=you-can-replace-this-with-your-own-secret

# if you are NOT USING docker, use the below url
# If you are NOT USING docker, use the below variables
REDIS_URL=localhost:6379
QUESTION_SERVICE_GRPC_URL=localhost:50051

# if you are USING docker, use the below url
# If you are USING docker, use the below variables
# REDIS_URL=redis-container:6379
# QUESTION_SERVICE_GRPC_URL=question-service:50051
25 changes: 17 additions & 8 deletions apps/matching-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,24 @@ Client sends matching parameters:
{
"type": "match_request",
"topics": ["Algorithms", "Arrays"],
"difficulties": ["Easy", "Medium"],
"username": "Jane Doe"
"difficulties": ["easy", "medium"],
"username": "1f0myn"
}
```

Server response on successful match:

```json
{
"type": "match_found",
"matchId": "1c018916a34c5bee21af0b2670bd6156",
"user": "zkb4px",
"matchedUser": "JohnDoe",
"topic": "Algorithms",
"difficulty": "Medium"
"type": "match_question_found",
"match_id": "c377f463d380a9bd1dd03242892ef32e",
"user": "1f0myn",
"matched_user": "jrsznp",
"matched_topics": ["Graphs", "Bit Manipulation", "Databases"],
"question_doc_ref_id": "5lObMfyyKPgNXSuLcGEm",
"question_name": "Repeated DNA Sequences",
"question_difficulty": "medium",
"question_topics": ["Algorithms", "Bit Manipulation"]
}
```

Expand Down Expand Up @@ -128,21 +131,27 @@ Before running the following commands, ensure that the URL for the Redis server
To run the application via Docker, run the following command:

1. Set up the Go Docker container for the matching service

```bash
docker build -f Dockerfile -t match-go-app .
```

2. Create the Docker network for Redis and Go

```bash
docker network create redis-go-network
```

3. Start a new Redis container in detached mode using the Redis image from Docker Hub

```bash
docker run -d --name redis-container --network redis-go-network redis
```

4. Run the Go Docker container for the matching-service

```bash
docker run -d -p 8081:8081 --name go-app-container --network redis-go-network match-go-app
```

**NOTE:** As there is a dependency on the question-service to return the found questions, the matching-service does not work fully unless the question-service is present.
71 changes: 24 additions & 47 deletions apps/matching-service/databases/userqueue.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log"
"matching-service/models"
"matching-service/servers"
"strings"

"github.com/redis/go-redis/v9"
Expand All @@ -30,7 +31,7 @@ func PrintMatchingQueue(tx *redis.Tx, status string, ctx context.Context) {
}

func IsQueueEmpty(tx *redis.Tx, ctx context.Context) (bool, error) {
queueLength, err := tx.LLen(ctx, MatchmakingQueueRedisKey).Result()
queueLength, err := tx.LLen(ctx, servers.MatchmakingQueueRedisKey).Result()
if err != nil {
log.Println("Error checking queue length:", err)
return false, err
Expand All @@ -41,15 +42,15 @@ func IsQueueEmpty(tx *redis.Tx, ctx context.Context) (bool, error) {

// Enqueue a user into the matchmaking queue
func EnqueueUser(tx *redis.Tx, username string, ctx context.Context) {
err := tx.LPush(ctx, MatchmakingQueueRedisKey, username).Err()
err := tx.LPush(ctx, servers.MatchmakingQueueRedisKey, username).Err()
if err != nil {
log.Println("Error enqueuing user:", err)
}
}

// Remove user from the matchmaking queue
func DequeueUser(tx *redis.Tx, username string, ctx context.Context) {
err := tx.LRem(ctx, MatchmakingQueueRedisKey, 1, username).Err()
err := tx.LRem(ctx, servers.MatchmakingQueueRedisKey, 1, username).Err()
if err != nil {
log.Println("Error dequeuing user:", err)
return
Expand All @@ -59,23 +60,39 @@ func DequeueUser(tx *redis.Tx, username string, ctx context.Context) {
// Returns the first user's username from the queue.
func GetFirstUser(tx *redis.Tx, ctx context.Context) (string, error) {
// Peek at the user queue
username, err := tx.LIndex(ctx, MatchmakingQueueRedisKey, 0).Result()
username, err := tx.LIndex(ctx, servers.MatchmakingQueueRedisKey, 0).Result()
if err != nil {
log.Println("Error peeking user from queue:", err)
return "", err
}
return username, nil
}

// Return the usernames of all the queued users.
func GetAllQueuedUsers(tx *redis.Tx, ctx context.Context) ([]string, error) {
users, err := tx.LRange(ctx, MatchmakingQueueRedisKey, 0, -1).Result()
users, err := tx.LRange(ctx, servers.MatchmakingQueueRedisKey, 0, -1).Result()
if err != nil {
log.Println("Error retrieving users from queue:", err)
return nil, err
}
return users, nil
}

func ValidateNotDuplicateUser(tx *redis.Tx, ctx context.Context, currentUsername string) error {
queuedUsernames, err := GetAllQueuedUsers(tx, ctx)
if err != nil {
return err
}

// Check that user is not part of the existing queue
for _, username := range queuedUsernames {
if username == currentUsername {
return models.ExistingUserError
}
}
return nil
}

// Add user details into hashset in Redis
func StoreUserDetails(tx *redis.Tx, request models.MatchRequest, ctx context.Context) {
topicsJSON, err := json.Marshal(request.Topics)
Expand Down Expand Up @@ -147,55 +164,15 @@ func RemoveUserDetails(tx *redis.Tx, username string, ctx context.Context) {
}
}

// Find the first matching user based on topics
// TODO: match based on available questions
func FindMatchingUser(tx *redis.Tx, username string, ctx context.Context) (*models.MatchFound, error) {
user, err := GetUserDetails(tx, username, ctx)
if err != nil {
return nil, err
}

for _, topic := range user.Topics {
users, err := tx.SMembers(ctx, strings.ToLower(topic)).Result()
if err != nil {
return nil, err
}

for _, potentialMatch := range users {
if potentialMatch == username {
continue
}

matchedUser, err := GetUserDetails(tx, potentialMatch, ctx)
if err != nil {
return nil, err
}

commonDifficulty := models.GetCommonDifficulty(user.Difficulties, matchedUser.Difficulties)

matchFound := models.MatchFound{
Type: "match_found",
MatchedUser: potentialMatch,
Topic: topic,
Difficulty: commonDifficulty,
}

return &matchFound, nil
}
}

return nil, nil
}

func PopAndInsertUser(tx *redis.Tx, username string, ctx context.Context) {
// Pop user
username, err := tx.LPop(ctx, MatchmakingQueueRedisKey).Result()
username, err := tx.LPop(ctx, servers.MatchmakingQueueRedisKey).Result()
if err != nil {
log.Println("Error popping user from queue:", err)
}

// Insert back in queue
err = tx.LPush(ctx, MatchmakingQueueRedisKey, username).Err()
err = tx.LPush(ctx, servers.MatchmakingQueueRedisKey, username).Err()
if err != nil {
log.Println("Error enqueuing user:", err)
}
Expand Down
8 changes: 7 additions & 1 deletion apps/matching-service/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.6.2
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.35.1
)

require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
)
18 changes: 16 additions & 2 deletions apps/matching-service/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,27 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/redis/go-redis/v9 v9.6.2 h1:w0uvkRbc9KpgD98zcvo5IrVUsn0lXpRMuhNgiHDJzdk=
github.com/redis/go-redis/v9 v9.6.2/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
13 changes: 11 additions & 2 deletions apps/matching-service/handlers/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func sendTimeoutResponse(ws *websocket.Conn) {
}
}

func sendRejectionResponse(ws *websocket.Conn) {
func sendDuplicateUserRejectionResponse(ws *websocket.Conn) {
if err := ws.WriteJSON(models.MatchRejected{
Type: "match_rejected",
Message: "You are already in a matchmaking queue. Please disconnect before reconnecting.",
Expand All @@ -27,8 +27,17 @@ func sendRejectionResponse(ws *websocket.Conn) {
}
}

func sendDefaultRejectionResponse(ws *websocket.Conn) {
if err := ws.WriteJSON(models.MatchRejected{
Type: "match_rejected",
Message: "An unexpected error occurred. Please try again later.",
}); err != nil {
log.Printf("write error: %v", err)
}
}

// Send message to matched user
func sendMatchFoundResponse(ws *websocket.Conn, username string, result models.MatchFound) {
func sendMatchFoundResponse(ws *websocket.Conn, username string, result models.MatchQuestionFound) {
if err := ws.WriteJSON(result); err != nil {
log.Printf("Error sending message to user %s: %v\n", username, err)
}
Expand Down
Loading

0 comments on commit 0f16864

Please sign in to comment.