Skip to content

Commit 0f16864

Browse files
Merge pull request #50 from CS3219-AY2425S1/titus/question-matching
feat: question matching
2 parents bb50c9b + e58e4af commit 0f16864

38 files changed

+1902
-265
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
MATCHING_SERVICE_PORT: ${{ vars.MATCHING_SERVICE_PORT }}
3737
MATCHING_SERVICE_TIMEOUT: ${{ vars.MATCHING_SERVICE_TIMEOUT }}
3838
REDIS_URL: ${{ vars.REDIS_URL }}
39+
QUESTION_SERVICE_GRPC_URL: ${{ vars.QUESTION_SERVICE_GPRC_URL }}
3940
run: |
4041
cd ./apps/frontend
4142
echo "NEXT_PUBLIC_QUESTION_SERVICE_URL=$QUESTION_SERVICE_URL" >> .env
@@ -56,6 +57,7 @@ jobs:
5657
echo "MATCH_TIMEOUT=$MATCHING_SERVICE_TIMEOUT" >> .env
5758
echo "JWT_SECRET=$JWT_SECRET" >> .env
5859
echo "REDIS_URL=$REDIS_URL" >> .env
60+
echo "QUESTION_SERVICE_GRPC_URL=$QUESTION_SERVICE_GRPC_URL" >> .env
5961
6062
- name: Create Database Credential Files
6163
env:

apps/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Once running, you can access:
5656

5757
- The **frontend** at http://localhost:3000
5858
- The **user service** at http://localhost:3001
59-
- The **question service** at http://localhost:8080
59+
- The **question service** at http://localhost:8080 (REST) and http://localhost:50051 (gRPC)
6060
- The **matching service** at http://localhost:8081
6161
- The **redis service** at http://localhost:6379
6262

apps/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ services:
3131
dockerfile: Dockerfile
3232
ports:
3333
- 8080:8080
34+
- 50051:50051
3435
env_file:
3536
- ./question-service/.env
3637
networks:

apps/frontend/src/app/matching/modalContent/FindMatchContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ const DifficultySelector: React.FC<DifficultySelectorProps> = ({ selectedDifficu
9494
<Tag.CheckableTag
9595
className={`difficulty-tag ${difficultyOption.value}-tag`}
9696
key={difficultyOption.value}
97-
checked={selectedDifficulties.includes(difficultyOption.label)}
98-
onChange={() => handleChange(difficultyOption.label)}
97+
checked={selectedDifficulties.includes(difficultyOption.value)}
98+
onChange={() => handleChange(difficultyOption.value)}
9999
>
100100
{difficultyOption.label}
101101
</Tag.CheckableTag>

apps/frontend/src/app/services/use-matching.ts

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@ export type MatchRequestParams = {
1717
}
1818

1919
export type MatchFoundResponse = {
20-
type: "match_found",
21-
matchId: number,
22-
partnerId: number,
23-
partnerName: string,
24-
} | {
25-
type: "match_found",
26-
matchId: string,
20+
type: "match_question_found",
21+
match_id: string,
2722
user: string,
28-
matchedUser: string,
29-
topic: string | string[],
30-
difficulty: string
23+
matched_user: string,
24+
matched_topics: string[],
25+
question_doc_ref_id: string,
26+
question_name: string,
27+
question_difficulty: string,
28+
question_topics: string[],
3129
}
3230

3331
export type MatchTimeoutResponse = {
@@ -61,7 +59,7 @@ export default function useMatching(): MatchState {
6159
return;
6260
}
6361

64-
if (responseJson.type == "match_found") {
62+
if (responseJson.type == "match_question_found") {
6563
setIsSocket(false);
6664

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

138136
function parseInfoFromResponse(responseJson: MatchFoundResponse): MatchInfo {
139-
// test whether old or new
140-
if ("partnerId" in responseJson) {
141-
return {
142-
matchId: responseJson.matchId?.toString() ?? "unknown",
143-
partnerId: responseJson.partnerId?.toString() ?? "unknown",
144-
partnerName: responseJson.partnerName ?? "unknown",
145-
myName: "unknown",
146-
};
147-
} else {
148-
return {
149-
matchId: responseJson.matchId?.toString() ?? "unknown",
150-
partnerId: "unknown",
151-
partnerName: responseJson.matchedUser ?? "unknown",
152-
myName: responseJson.user ?? "unknown",
153-
};
154-
}
137+
return {
138+
matchId: responseJson.match_id?.toString() ?? "unknown",
139+
partnerName: responseJson.matched_user ?? "unknown",
140+
myName: responseJson.user ?? "unknown",
141+
};
155142
}

apps/frontend/src/contexts/websocketcontext.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export type SocketState = {
1515
};
1616
export type MatchInfo = {
1717
matchId: string;
18-
partnerId: string;
1918
myName: string;
2019
partnerName: string;
2120
}

apps/matching-service/.env.example

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ PORT=8081
22
MATCH_TIMEOUT=30
33
JWT_SECRET=you-can-replace-this-with-your-own-secret
44

5-
# if you are NOT USING docker, use the below url
5+
# If you are NOT USING docker, use the below variables
66
REDIS_URL=localhost:6379
7+
QUESTION_SERVICE_GRPC_URL=localhost:50051
78

8-
# if you are USING docker, use the below url
9+
# If you are USING docker, use the below variables
910
# REDIS_URL=redis-container:6379
11+
# QUESTION_SERVICE_GRPC_URL=question-service:50051

apps/matching-service/README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,24 @@ Client sends matching parameters:
7575
{
7676
"type": "match_request",
7777
"topics": ["Algorithms", "Arrays"],
78-
"difficulties": ["Easy", "Medium"],
79-
"username": "Jane Doe"
78+
"difficulties": ["easy", "medium"],
79+
"username": "1f0myn"
8080
}
8181
```
8282

8383
Server response on successful match:
8484

8585
```json
8686
{
87-
"type": "match_found",
88-
"matchId": "1c018916a34c5bee21af0b2670bd6156",
89-
"user": "zkb4px",
90-
"matchedUser": "JohnDoe",
91-
"topic": "Algorithms",
92-
"difficulty": "Medium"
87+
"type": "match_question_found",
88+
"match_id": "c377f463d380a9bd1dd03242892ef32e",
89+
"user": "1f0myn",
90+
"matched_user": "jrsznp",
91+
"matched_topics": ["Graphs", "Bit Manipulation", "Databases"],
92+
"question_doc_ref_id": "5lObMfyyKPgNXSuLcGEm",
93+
"question_name": "Repeated DNA Sequences",
94+
"question_difficulty": "medium",
95+
"question_topics": ["Algorithms", "Bit Manipulation"]
9396
}
9497
```
9598

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

130133
1. Set up the Go Docker container for the matching service
134+
131135
```bash
132136
docker build -f Dockerfile -t match-go-app .
133137
```
134138

135139
2. Create the Docker network for Redis and Go
140+
136141
```bash
137142
docker network create redis-go-network
138143
```
139144

140145
3. Start a new Redis container in detached mode using the Redis image from Docker Hub
146+
141147
```bash
142148
docker run -d --name redis-container --network redis-go-network redis
143149
```
144150

145151
4. Run the Go Docker container for the matching-service
152+
146153
```bash
147154
docker run -d -p 8081:8081 --name go-app-container --network redis-go-network match-go-app
148155
```
156+
157+
**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.

apps/matching-service/databases/userqueue.go

Lines changed: 24 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"log"
88
"matching-service/models"
9+
"matching-service/servers"
910
"strings"
1011

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

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

4243
// Enqueue a user into the matchmaking queue
4344
func EnqueueUser(tx *redis.Tx, username string, ctx context.Context) {
44-
err := tx.LPush(ctx, MatchmakingQueueRedisKey, username).Err()
45+
err := tx.LPush(ctx, servers.MatchmakingQueueRedisKey, username).Err()
4546
if err != nil {
4647
log.Println("Error enqueuing user:", err)
4748
}
4849
}
4950

5051
// Remove user from the matchmaking queue
5152
func DequeueUser(tx *redis.Tx, username string, ctx context.Context) {
52-
err := tx.LRem(ctx, MatchmakingQueueRedisKey, 1, username).Err()
53+
err := tx.LRem(ctx, servers.MatchmakingQueueRedisKey, 1, username).Err()
5354
if err != nil {
5455
log.Println("Error dequeuing user:", err)
5556
return
@@ -59,23 +60,39 @@ func DequeueUser(tx *redis.Tx, username string, ctx context.Context) {
5960
// Returns the first user's username from the queue.
6061
func GetFirstUser(tx *redis.Tx, ctx context.Context) (string, error) {
6162
// Peek at the user queue
62-
username, err := tx.LIndex(ctx, MatchmakingQueueRedisKey, 0).Result()
63+
username, err := tx.LIndex(ctx, servers.MatchmakingQueueRedisKey, 0).Result()
6364
if err != nil {
6465
log.Println("Error peeking user from queue:", err)
6566
return "", err
6667
}
6768
return username, nil
6869
}
6970

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

81+
func ValidateNotDuplicateUser(tx *redis.Tx, ctx context.Context, currentUsername string) error {
82+
queuedUsernames, err := GetAllQueuedUsers(tx, ctx)
83+
if err != nil {
84+
return err
85+
}
86+
87+
// Check that user is not part of the existing queue
88+
for _, username := range queuedUsernames {
89+
if username == currentUsername {
90+
return models.ExistingUserError
91+
}
92+
}
93+
return nil
94+
}
95+
7996
// Add user details into hashset in Redis
8097
func StoreUserDetails(tx *redis.Tx, request models.MatchRequest, ctx context.Context) {
8198
topicsJSON, err := json.Marshal(request.Topics)
@@ -147,55 +164,15 @@ func RemoveUserDetails(tx *redis.Tx, username string, ctx context.Context) {
147164
}
148165
}
149166

150-
// Find the first matching user based on topics
151-
// TODO: match based on available questions
152-
func FindMatchingUser(tx *redis.Tx, username string, ctx context.Context) (*models.MatchFound, error) {
153-
user, err := GetUserDetails(tx, username, ctx)
154-
if err != nil {
155-
return nil, err
156-
}
157-
158-
for _, topic := range user.Topics {
159-
users, err := tx.SMembers(ctx, strings.ToLower(topic)).Result()
160-
if err != nil {
161-
return nil, err
162-
}
163-
164-
for _, potentialMatch := range users {
165-
if potentialMatch == username {
166-
continue
167-
}
168-
169-
matchedUser, err := GetUserDetails(tx, potentialMatch, ctx)
170-
if err != nil {
171-
return nil, err
172-
}
173-
174-
commonDifficulty := models.GetCommonDifficulty(user.Difficulties, matchedUser.Difficulties)
175-
176-
matchFound := models.MatchFound{
177-
Type: "match_found",
178-
MatchedUser: potentialMatch,
179-
Topic: topic,
180-
Difficulty: commonDifficulty,
181-
}
182-
183-
return &matchFound, nil
184-
}
185-
}
186-
187-
return nil, nil
188-
}
189-
190167
func PopAndInsertUser(tx *redis.Tx, username string, ctx context.Context) {
191168
// Pop user
192-
username, err := tx.LPop(ctx, MatchmakingQueueRedisKey).Result()
169+
username, err := tx.LPop(ctx, servers.MatchmakingQueueRedisKey).Result()
193170
if err != nil {
194171
log.Println("Error popping user from queue:", err)
195172
}
196173

197174
// Insert back in queue
198-
err = tx.LPush(ctx, MatchmakingQueueRedisKey, username).Err()
175+
err = tx.LPush(ctx, servers.MatchmakingQueueRedisKey, username).Err()
199176
if err != nil {
200177
log.Println("Error enqueuing user:", err)
201178
}

apps/matching-service/go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ require (
77
github.com/gorilla/websocket v1.5.3
88
github.com/joho/godotenv v1.5.1
99
github.com/redis/go-redis/v9 v9.6.2
10+
google.golang.org/grpc v1.67.1
11+
google.golang.org/protobuf v1.35.1
1012
)
1113

1214
require (
13-
github.com/cespare/xxhash/v2 v2.2.0 // indirect
15+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
1416
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
17+
golang.org/x/net v0.28.0 // indirect
18+
golang.org/x/sys v0.24.0 // indirect
19+
golang.org/x/text v0.17.0 // indirect
20+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
1521
)

0 commit comments

Comments
 (0)