Skip to content

Commit 6becc5c

Browse files
committed
Revamp matching service and update README
1 parent 57c4fd9 commit 6becc5c

File tree

7 files changed

+470
-120
lines changed

7 files changed

+470
-120
lines changed

apps/matching-service/README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ Client sends matching parameters:
7676
"type": "match_request",
7777
"topics": ["Algorithms", "Arrays"],
7878
"difficulties": ["Easy", "Medium"],
79-
"username": "Jane Doe",
80-
"email": "[email protected]" // possible to change to user ID in mongodb
79+
"username": "Jane Doe"
8180
}
8281
```
8382

@@ -86,9 +85,11 @@ Server response on successful match:
8685
```json
8786
{
8887
"type": "match_found",
89-
"matchID": "aa41c004e589642402215c2c0a3a165a",
90-
"partnerID": 54321, // currently identifying via partner's websocket port --> change to user ID in mongodb
91-
"partnerName": "John Doe" // partner username
88+
"matchId": "1c018916a34c5bee21af0b2670bd6156",
89+
"user": "zkb4px",
90+
"matchedUser": "JohnDoe",
91+
"topic": "Algorithms",
92+
"difficulty": "Medium"
9293
}
9394
```
9495

@@ -101,6 +102,15 @@ If no match is found after a set period of time, the server will send a timeout
101102
}
102103
```
103104

105+
If user has an existing websocket connection and wants to initiate another match, the server will reject the request:
106+
107+
```json
108+
{
109+
"type": "match_rejected",
110+
"message": "You are already in a matchmaking queue. Please disconnect before reconnecting."
111+
}
112+
```
113+
104114
If the server encounters an issue during the WebSocket connection or processing, the connection will be closed without any error message. The client should treat the unexpected closing as an error.
105115

106116
## Testing

apps/matching-service/handlers/websocket.go

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,24 @@ import (
77
"matching-service/processes"
88
"matching-service/utils"
99
"net/http"
10+
"sync"
1011

1112
"github.com/gorilla/websocket"
1213
)
1314

14-
var upgrader = websocket.Upgrader{
15-
CheckOrigin: func(r *http.Request) bool {
16-
// Allow all connections by skipping the origin check (set more restrictions in production)
17-
return true
18-
},
19-
}
15+
var (
16+
upgrader = websocket.Upgrader{
17+
CheckOrigin: func(r *http.Request) bool {
18+
// Allow all connections by skipping the origin check (set more restrictions in production)
19+
return true
20+
},
21+
}
22+
// A map to hold active WebSocket connections per username
23+
activeConnections = make(map[string]*websocket.Conn)
24+
// A map to hold user's match ctx cancel function
25+
matchContexts = make(map[string]context.CancelFunc)
26+
mu sync.Mutex // Mutex for thread-safe access to activeConnections
27+
)
2028

2129
// handleConnections manages WebSocket connections and matching logic.
2230
func HandleWebSocketConnections(w http.ResponseWriter, r *http.Request) {
@@ -42,10 +50,32 @@ func HandleWebSocketConnections(w http.ResponseWriter, r *http.Request) {
4250
return
4351
}
4452

53+
// Store WebSocket connection in the activeConnections map.
54+
mu.Lock()
55+
// Checks if user is already an existing websocket connection
56+
if _, exists := activeConnections[matchRequest.Username]; exists {
57+
mu.Unlock()
58+
log.Printf("User %s is already connected, rejecting new connection.", matchRequest.Username)
59+
ws.WriteJSON(models.MatchRejected{
60+
Type: "match_rejected",
61+
Message: "You are already in a matchmaking queue. Please disconnect before reconnecting.",
62+
})
63+
ws.Close()
64+
return
65+
}
66+
activeConnections[matchRequest.Username] = ws
67+
matchCtx, matchCancel := context.WithCancel(context.Background())
68+
matchContexts[matchRequest.Username] = matchCancel
69+
mu.Unlock()
70+
4571
// Create a context for cancellation
4672
ctx, cancel := context.WithCancel(context.Background())
4773
defer cancel() // Ensure cancel is called to release resources
4874

75+
processes.EnqueueUser(processes.GetRedisClient(), matchRequest.Username, ctx)
76+
processes.AddUserToTopicSets(processes.GetRedisClient(), matchRequest, ctx)
77+
processes.StoreUserDetails(processes.GetRedisClient(), matchRequest, ctx)
78+
4979
timeoutCtx, timeoutCancel, err := createTimeoutContext()
5080
if err != nil {
5181
log.Printf("Error creating timeout context: %v", err)
@@ -60,7 +90,7 @@ func HandleWebSocketConnections(w http.ResponseWriter, r *http.Request) {
6090
go processes.PerformMatching(matchRequest, ctx, matchFoundChan) // Perform matching
6191

6292
// Wait for a match, timeout, or cancellation.
63-
waitForResult(ws, ctx, timeoutCtx, matchFoundChan)
93+
waitForResult(ws, ctx, timeoutCtx, matchCtx, matchFoundChan, matchRequest.Username)
6494
}
6595

6696
// readMatchRequest reads the initial match request from the WebSocket connection.
@@ -69,7 +99,6 @@ func readMatchRequest(ws *websocket.Conn) (models.MatchRequest, error) {
6999
if err := ws.ReadJSON(&matchRequest); err != nil {
70100
return matchRequest, err
71101
}
72-
matchRequest.Port = ws.RemoteAddr().String()
73102
log.Printf("Received match request: %v", matchRequest)
74103
return matchRequest, nil
75104
}
@@ -85,25 +114,36 @@ func createTimeoutContext() (context.Context, context.CancelFunc, error) {
85114
}
86115

87116
// waitForResult waits for a match result, timeout, or cancellation.
88-
func waitForResult(ws *websocket.Conn, ctx, timeoutCtx context.Context, matchFoundChan chan models.MatchFound) {
117+
func waitForResult(ws *websocket.Conn, ctx, timeoutCtx, matchCtx context.Context, matchFoundChan chan models.MatchFound, username string) {
89118
select {
90119
case <-ctx.Done():
91120
log.Println("Matching cancelled")
121+
// Cleanup Redis
122+
processes.CleanUpUser(processes.GetRedisClient(), username, ctx)
92123
return
93124
case <-timeoutCtx.Done():
94125
log.Println("Connection timed out")
126+
// Cleanup Redis
127+
processes.CleanUpUser(processes.GetRedisClient(), username, ctx)
95128
sendTimeoutResponse(ws)
96129
return
130+
case <-matchCtx.Done():
131+
log.Println("Match found for user HEREEE: " + username)
132+
return
97133
case result, ok := <-matchFoundChan:
98134
if !ok {
99135
// Channel closed without a match, possibly due to context cancellation
100136
log.Println("Match channel closed without finding a match")
101137
return
102138
}
103-
log.Println("Match found")
104-
if err := ws.WriteJSON(result); err != nil {
105-
log.Printf("write error: %v", err)
106-
}
139+
log.Println("Match found for user: " + result.User)
140+
141+
// Notify the users about the match
142+
notifyMatch(result.User, result.MatchedUser, result)
143+
144+
// if err := ws.WriteJSON(result); err != nil {
145+
// log.Printf("write error: %v", err)
146+
// }
107147
return
108148
}
109149
}
@@ -118,3 +158,37 @@ func sendTimeoutResponse(ws *websocket.Conn) {
118158
log.Printf("write error: %v", err)
119159
}
120160
}
161+
162+
func notifyMatch(username, matchedUsername string, result models.MatchFound) {
163+
mu.Lock()
164+
defer mu.Unlock()
165+
166+
// Send message to the first user
167+
if userConn, userExists := activeConnections[username]; userExists {
168+
if err := userConn.WriteJSON(result); err != nil {
169+
log.Printf("Error sending message to user %s: %v\n", username, err)
170+
}
171+
}
172+
173+
// Send message to the matched user
174+
if matchedUserConn, matchedUserExists := activeConnections[matchedUsername]; matchedUserExists {
175+
result.User, result.MatchedUser = result.MatchedUser, result.User // Swap User and MatchedUser values
176+
if err := matchedUserConn.WriteJSON(result); err != nil {
177+
log.Printf("Error sending message to user %s: %v\n", username, err)
178+
}
179+
}
180+
181+
// Remove the match context for both users and cancel for matched user
182+
if _, exists := matchContexts[username]; exists {
183+
delete(matchContexts, username)
184+
}
185+
186+
if cancelFunc, exists := matchContexts[matchedUsername]; exists {
187+
delete(matchContexts, matchedUsername)
188+
cancelFunc()
189+
}
190+
191+
// Remove users from the activeConnections map
192+
delete(activeConnections, username)
193+
delete(activeConnections, matchedUsername)
194+
}

apps/matching-service/main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func main() {
2121
}
2222
port := os.Getenv("PORT")
2323

24-
// Set up link with redis server
24+
// Retrieve redis url env variable and setup the redis client
2525
redisAddr := os.Getenv("REDIS_URL")
2626
client := redis.NewClient(&redis.Options{
2727
Addr: redisAddr,
@@ -37,9 +37,11 @@ func main() {
3737
log.Println("Connected to Redis at the following address: " + redisAddr)
3838
}
3939

40-
// Pass the connect redis client to processes in match
40+
// Set redis client
4141
processes.SetRedisClient(client)
4242

43+
// Run a goroutine that matches users
44+
4345
// Routes
4446
http.HandleFunc("/match", handlers.HandleWebSocketConnections)
4547

apps/matching-service/models/match.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@ type MatchRequest struct {
55
Topics []string `json:"topics"`
66
Difficulties []string `json:"difficulties"`
77
Username string `json:"username"`
8-
Email string `json:"email"`
9-
Port string `json:"port"`
108
}
119

1210
type MatchFound struct {
1311
Type string `json:"type"`
1412
MatchID string `json:"matchId"`
15-
PartnerID int64 `json:"partnerId"`
16-
PartnerName string `json:"partnerName"`
13+
User string `json:"user"` // username
14+
MatchedUser string `json:"matchedUser"` // matched username
15+
Topic string `json:"topic"` // matched topic
16+
Difficulty string `json:"difficulty"` // matched difficulty
1717
}
1818

1919
type Timeout struct {
2020
Type string `json:"timeout"`
2121
Message string `json:"message"`
2222
}
23+
24+
type MatchRejected struct {
25+
Type string `json:"type"`
26+
Message string `json:"message"`
27+
}

0 commit comments

Comments
 (0)